I'm using the Privy React SDK (@privy-io/react-auth v2.17.3) configured with the following setting:
<BasePrivyProvider
appId={AUTH_VENDOR_PRIVY_APP_ID}
clientId={AUTH_VENDOR_PRIVY_CLIENT_ID}
config={{
defaultChain: myDefaultNetwork,
embeddedWallets: {
ethereum: {
createOnLogin: 'users-without-wallets',
},
},
externalWallets: {
disableAllExternalWallets: false,
},
supportedChains: [
myCustomNetwork,
],
}}
>
{children}
</BasePrivyProvider>
I want to enable users to sign in with an external wallet -OR- provision them a wallet if they sign in using email, phone, OAuth, etc.
When I call sendTransaction()
, the model opens and everything works when the user has an embedded Privy wallet, but if the user has an external wallet, it does nothing. I would expect it to open the external wallet interface and prompt the user to approve.
const send = async () => {
try {
const value = parseEther(`${amountToSend}`);
const displayAmount = formatNativeCurrencyForNetwork(value, currentNetwork);
await privy.sendTransaction(
{
to: sendToAddress,
value: value,
chainId: getCurrentNetworkId(),
},
{
uiOptions: {
showWalletUIs: true,
},
address: sendFromAddress,
}
);
} catch (error) {
console.error('Error sending transaction:', error);
} finally {
void balance.refresh();
}
};
How do I get sendTransaction
to work with external wallets?
I ended up creating separate functions for sending transactions via embedded wallets and external wallets. It looks something like this:
import type {
EIP1193Provider,
LinkedAccountWithMetadata,
PrivyErrorCode,
User,
} from '@privy-io/react-auth';
import { usePrivy, useWallets } from '@privy-io/react-auth';
import { ethers } from 'ethers';
import { parseEther } from 'ethers/utils';
import { useState } from 'react';
const CHAIN_ID = 1;
const RPC_URL = 'https://ethereum-rpc.publicnode.com';
export function TransferFunds() {
const privy = usePrivy();
const { wallets: privyWallets } = useWallets();
const send = async (to: string, amount: string | bigint) => {
const from = privy.user.smartWallet?.address || privy.user.wallet?.address;
const value = parseEther(`${amount}`);
if (privy.user?.wallet) {
if (privy.user.wallet.walletClientType === 'privy') {
await sendFromPrivyEmbeddedWallet(from, to, value);
} else {
await sendFromPrivyExternalWallet(from, to, value);
}
}
}
const sendFromPrivyEmbeddedWallet = async (from: string, to: string, value: string | bigint) => {
await privy.sendTransaction(
{
to: to,
value: value,
chainId: CHAIN_ID,
},
{
uiOptions: {
showWalletUIs: true,
},
address: from,
}
);
balance.refresh();
};
const sendFromPrivyExternalWallet = async (from: string, to: string, value: string | bigint) => {
const wallet = privyWallets.find((w) => w.address === from);
if (!wallet || !wallet.getEthereumProvider) {
throw new Error('No external wallet found or provider not available');
}
const provider: EIP1193Provider = await wallet.getEthereumProvider();
const proxy = getProxyProvider(provider);
const ethersProvider = new ethers.BrowserProvider(proxy);
const signer = await ethersProvider.getSigner();
const tx = await signer.sendTransaction({
to: to,
value: value,
chainId: CHAIN_ID,
});
await tx.wait();
};
return ( <>{/* Component UI */}</> );
}
/**
* Privy adds query parameters to the RPC URL, which can cause issues with certain
* EVM network RPC endpoints. For this reason, we create a proxy provider that handles
* certain transaction pre-flight requests.
*/
function getProxyProvider(provider: EIP1193Provider): EIP1193Provider {
return {
...provider,
request: async (args: { method: string; params?: unknown[] }) => {
if (
['eth_accounts', 'eth_requestAccounts', 'eth_sendTransaction'].includes(args.method)
) {
// Use the given provider's request method for these methods
return provider.request(args);
}
const resp = await fetch(RPC_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: args.method,
params: args.params || [],
id: ethers.hexlify(ethers.randomBytes(4)),
}),
});
if (!resp.ok) {
throw new Error(`RPC request failed with status ${resp.status}`);
}
const data = await resp.json();
if (data.error) {
throw new Error(data.error.message || 'Unknown error from RPC');
}
return data.result;
},
};
}
The key here was figuring out how to use Privy to get the external wallet provider, to prompt the user to send the transaction. I also ran into issues caused by Privy adding query parameters to the RPC endpoint, which is why I added the function to create a wrapper around the provider.