reactjstypescriptnext.jsviemprivy

How do I get Privy sendTransaction to work with external wallets?


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?


Solution

  • 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.