# Phantom and Other Wallets

This guide shows you how to sponsor transactions that your users sign in their own wallet, such as Phantom, in a React app.

In a browser app, your user's key lives in their wallet, not in your code. `@solana/react` turns the connected wallet into a signer that you build your transaction with. When your user signs, the wallet opens and asks them to approve. The paymaster pays the fee, adds its own signature, and broadcasts the transaction.

This works with any wallet that supports the [Wallet Standard](https://github.com/wallet-standard/wallet-standard), including Phantom, Solflare, and Backpack. If your users log in with Privy, see [Privy](/guides/how-to/solana/privy) instead.

## Steps

:::steps
### Install the SDK

```sh
npm install @solana/kora @solana/kit@6 @solana-program/token@0.12 @solana/react@6 @wallet-standard/react
```

`@solana/kora` 0.2 is built on `@solana/kit` 6, so install the matching versions of `@solana/kit`, `@solana-program/token`, and `@solana/react`.

### Create the client

Pass your API key in the URL's `apikey` query parameter.

```ts
import { KoraClient } from "@solana/kora";

const kora = new KoraClient({
    rpcUrl: "https://solana.pimlico.io/v1/mainnet/rpc?apikey=YOUR_API_KEY",
});
```

### Add a helper to build the transaction

Add this helper to your app. Pass it your instructions, and it returns a transaction message with the paymaster as the fee payer.

```ts [sponsored-transaction.ts]
import type { KoraClient } from "@solana/kora";
import {
    type Instruction,
    address,
    appendTransactionMessageInstructions,
    blockhash,
    createNoopSigner,
    createTransactionMessage,
    pipe,
    setTransactionMessageFeePayerSigner,
    setTransactionMessageLifetimeUsingBlockhash,
} from "@solana/kit";

export async function createSponsoredTransactionMessage(
    kora: KoraClient,
    instructions: Instruction[],
) {
    const { signer_address } = await kora.getPayerSigner();
    const { blockhash: latestBlockhash } = await kora.getBlockhash();

    // pipe passes the message through each step below, in order
    return pipe(
        // Start an empty version 0 transaction message
        createTransactionMessage({ version: 0 }),
        // The paymaster pays the fee. It signs later, so a noop signer leaves its signature slot empty.
        (m) => setTransactionMessageFeePayerSigner(createNoopSigner(address(signer_address)), m),
        // The blockhash keeps the transaction valid for about a minute. lastValidBlockHeight is only
        // read by @solana/kit's own send-and-confirm, and the paymaster sends the transaction for you.
        (m) =>
            setTransactionMessageLifetimeUsingBlockhash(
                { blockhash: blockhash(latestBlockhash), lastValidBlockHeight: 0n },
                m,
            ),
        // Add your app's instructions (transfers, swaps, etc.)
        (m) => appendTransactionMessageInstructions(instructions, m),
    );
}
```

### Connect the wallet

`useWallets` lists the wallets installed in the browser. `useConnect` asks a wallet to connect, and returns the accounts your user approved.

```tsx
import {
    type UiWallet,
    type UiWalletAccount,
    useConnect,
    useWallets,
} from "@wallet-standard/react";

function ConnectPhantom({ onConnect }: { onConnect: (account: UiWalletAccount) => void }) {
    const phantom = useWallets().find((wallet) => wallet.name === "Phantom");

    if (!phantom) {
        return <a href="https://phantom.com/download">Install Phantom</a>;
    }
    return <ConnectButton wallet={phantom} onConnect={onConnect} />;
}

function ConnectButton({
    wallet,
    onConnect,
}: {
    wallet: UiWallet;
    onConnect: (account: UiWalletAccount) => void;
}) {
    const [isConnecting, connect] = useConnect(wallet);

    return (
        <button
            type="button"
            disabled={isConnecting}
            onClick={async () => {
                const [account] = await connect();
                onConnect(account);
            }}
        >
            Connect {wallet.name}
        </button>
    );
}
```

To support more than Phantom, render a `ConnectButton` for each wallet in `useWallets()`.

### Get a signer for the wallet

`useWalletAccountTransactionSigner` turns the connected account into a signer. Its `address` is your user's wallet address. Use `"solana:devnet"` instead if you are on devnet.

```tsx
import { useWalletAccountTransactionSigner } from "@solana/react";

const user = useWalletAccountTransactionSigner(account, "solana:mainnet");
```

### Build and send the transaction

This example sends USDC from your user's wallet. Build the transaction with the helper, using the wallet signer as the token authority. `partiallySignTransactionMessageWithSigners` opens the wallet, so your user approves the transfer there.

```ts
import {
    type Address,
    type TransactionSigner,
    address,
    getBase64EncodedWireTransaction,
    partiallySignTransactionMessageWithSigners,
} from "@solana/kit";
import {
    findAssociatedTokenPda,
    getTransferInstruction,
    TOKEN_PROGRAM_ADDRESS,
} from "@solana-program/token";
import { createSponsoredTransactionMessage } from "./sponsored-transaction";

const USDC = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");

async function sendUsdc(user: TransactionSigner, recipient: Address, amount: bigint) {
    const [source] = await findAssociatedTokenPda({
        owner: user.address,
        mint: USDC,
        tokenProgram: TOKEN_PROGRAM_ADDRESS,
    });
    const [destination] = await findAssociatedTokenPda({
        owner: recipient,
        mint: USDC,
        tokenProgram: TOKEN_PROGRAM_ADDRESS,
    });

    const message = await createSponsoredTransactionMessage(kora, [
        getTransferInstruction({ source, destination, authority: user, amount }),
    ]);

    // Opens the wallet for your user to approve. The paymaster adds the fee payer signature.
    const transaction = getBase64EncodedWireTransaction(
        await partiallySignTransactionMessageWithSigners(message),
    );

    const { signature } = await kora.signAndSendTransaction({ transaction });
    return signature;
}
```

### Add a send button

Get the signer in a component, and call `sendUsdc` when your user clicks.

```tsx
function SendUsdcButton({ account }: { account: UiWalletAccount }) {
    const user = useWalletAccountTransactionSigner(account, "solana:mainnet");

    return (
        <button
            type="button"
            onClick={async () => {
                const signature = await sendUsdc(user, RECIPIENT, 1_000_000n); // 1 USDC
                console.log(`https://solscan.io/tx/${signature}`);
            }}
        >
            Send 1 USDC
        </button>
    );
}
```
:::

The wallet shows the fee as paid by another account, so your user needs no SOL.

## Paying the fee in USDC

Instead of paying the fee from your Pimlico balance, you can have your user pay it in USDC or USDT. `getPaymentInstruction` asks the paymaster what the transaction will cost, and returns a token transfer of that amount from your user to the paymaster. Append it to your instructions, and pass the wallet signer as `source_wallet`.

The quote doesn't need your user's signature, so build the quote transaction with `compileTransaction` instead of signing it. The wallet then opens only once, for the final transaction, and shows both the transfer and the USDC payment to the paymaster.

```ts
import { compileTransaction } from "@solana/kit";

async function sendUsdcPayingFeeInUsdc(user: TransactionSigner, recipient: Address, amount: bigint) {
    const [source] = await findAssociatedTokenPda({
        owner: user.address,
        mint: USDC,
        tokenProgram: TOKEN_PROGRAM_ADDRESS,
    });
    const [destination] = await findAssociatedTokenPda({
        owner: recipient,
        mint: USDC,
        tokenProgram: TOKEN_PROGRAM_ADDRESS,
    });
    const instructions = [getTransferInstruction({ source, destination, authority: user, amount })];

    // 1. Quote the fee. This doesn't open the wallet.
    const quoteMessage = await createSponsoredTransactionMessage(kora, instructions);
    const { payment_instruction, payment_amount } = await kora.getPaymentInstruction({
        transaction: getBase64EncodedWireTransaction(compileTransaction(quoteMessage)),
        fee_token: USDC,
        source_wallet: user,
    });
    console.log(`Fee: ${Number(payment_amount) / 1e6} USDC`);

    // 2. Append the payment. The wallet opens once, for both transfers.
    const message = await createSponsoredTransactionMessage(kora, [
        ...instructions,
        payment_instruction,
    ]);
    const transaction = getBase64EncodedWireTransaction(
        await partiallySignTransactionMessageWithSigners(message),
    );

    const { signature } = await kora.signAndSendTransaction({ transaction });
    return signature;
}
```

## Other wallet libraries

If you don't use `@solana/react`, have your wallet library sign the transaction bytes. Use your user's address as the authority, since there is no signer.

```ts
import {
    compileTransaction,
    getBase64Decoder,
    getTransactionEncoder,
} from "@solana/kit";

const message = await createSponsoredTransactionMessage(kora, [
    getTransferInstruction({ source, destination, authority: userAddress, amount: 1_000_000n }),
]);
const unsignedBytes = getTransactionEncoder().encode(compileTransaction(message));

// Ask the wallet to sign the bytes, using your wallet library
const signedBytes = await signTransactionInWallet(unsignedBytes);

const { signature } = await kora.signAndSendTransaction({
    transaction: getBase64Decoder().decode(signedBytes),
});
```

If your wallet library works with `@solana/web3.js` transactions, convert the bytes with `VersionedTransaction.deserialize(unsignedBytes)`, and convert the signed transaction back with `.serialize()`.

## Full example

:::code-group
```tsx [App.tsx]
import { KoraClient } from "@solana/kora";
import {
    type Address,
    type TransactionSigner,
    address,
    getBase64EncodedWireTransaction,
    partiallySignTransactionMessageWithSigners,
} from "@solana/kit";
import { useWalletAccountTransactionSigner } from "@solana/react";
import {
    findAssociatedTokenPda,
    getTransferInstruction,
    TOKEN_PROGRAM_ADDRESS,
} from "@solana-program/token";
import {
    type UiWallet,
    type UiWalletAccount,
    useConnect,
    useWallets,
} from "@wallet-standard/react";
import { useState } from "react";
import { createSponsoredTransactionMessage } from "./sponsored-transaction";

const USDC = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const RECIPIENT = address("RecipientPubkey11111111111111111111111111111");

const kora = new KoraClient({
    rpcUrl: "https://solana.pimlico.io/v1/mainnet/rpc?apikey=YOUR_API_KEY",
});

async function sendUsdc(user: TransactionSigner, recipient: Address, amount: bigint) {
    const [source] = await findAssociatedTokenPda({
        owner: user.address,
        mint: USDC,
        tokenProgram: TOKEN_PROGRAM_ADDRESS,
    });
    const [destination] = await findAssociatedTokenPda({
        owner: recipient,
        mint: USDC,
        tokenProgram: TOKEN_PROGRAM_ADDRESS,
    });

    const message = await createSponsoredTransactionMessage(kora, [
        getTransferInstruction({ source, destination, authority: user, amount }),
    ]);

    // Opens the wallet for your user to approve. The paymaster adds the fee payer signature.
    const transaction = getBase64EncodedWireTransaction(
        await partiallySignTransactionMessageWithSigners(message),
    );

    const { signature } = await kora.signAndSendTransaction({ transaction });
    return signature;
}

export function App() {
    const [account, setAccount] = useState<UiWalletAccount>();

    if (!account) {
        return <ConnectPhantom onConnect={setAccount} />;
    }
    return <SendUsdcButton account={account} />;
}

function ConnectPhantom({ onConnect }: { onConnect: (account: UiWalletAccount) => void }) {
    const phantom = useWallets().find((wallet) => wallet.name === "Phantom");

    if (!phantom) {
        return <a href="https://phantom.com/download">Install Phantom</a>;
    }
    return <ConnectButton wallet={phantom} onConnect={onConnect} />;
}

function ConnectButton({
    wallet,
    onConnect,
}: {
    wallet: UiWallet;
    onConnect: (account: UiWalletAccount) => void;
}) {
    const [isConnecting, connect] = useConnect(wallet);

    return (
        <button
            type="button"
            disabled={isConnecting}
            onClick={async () => {
                const [account] = await connect();
                onConnect(account);
            }}
        >
            Connect {wallet.name}
        </button>
    );
}

function SendUsdcButton({ account }: { account: UiWalletAccount }) {
    const user = useWalletAccountTransactionSigner(account, "solana:mainnet");

    return (
        <button
            type="button"
            onClick={async () => {
                const signature = await sendUsdc(user, RECIPIENT, 1_000_000n); // 1 USDC
                console.log(`https://solscan.io/tx/${signature}`);
            }}
        >
            Send 1 USDC
        </button>
    );
}
```

```ts [sponsored-transaction.ts]
import type { KoraClient } from "@solana/kora";
import {
    type Instruction,
    address,
    appendTransactionMessageInstructions,
    blockhash,
    createNoopSigner,
    createTransactionMessage,
    pipe,
    setTransactionMessageFeePayerSigner,
    setTransactionMessageLifetimeUsingBlockhash,
} from "@solana/kit";

export async function createSponsoredTransactionMessage(
    kora: KoraClient,
    instructions: Instruction[],
) {
    const { signer_address } = await kora.getPayerSigner();
    const { blockhash: latestBlockhash } = await kora.getBlockhash();

    // pipe passes the message through each step below, in order
    return pipe(
        // Start an empty version 0 transaction message
        createTransactionMessage({ version: 0 }),
        // The paymaster pays the fee. It signs later, so a noop signer leaves its signature slot empty.
        (m) => setTransactionMessageFeePayerSigner(createNoopSigner(address(signer_address)), m),
        // The blockhash keeps the transaction valid for about a minute. lastValidBlockHeight is only
        // read by @solana/kit's own send-and-confirm, and the paymaster sends the transaction for you.
        (m) =>
            setTransactionMessageLifetimeUsingBlockhash(
                { blockhash: blockhash(latestBlockhash), lastValidBlockHeight: 0n },
                m,
            ),
        // Add your app's instructions (transfers, swaps, etc.)
        (m) => appendTransactionMessageInstructions(instructions, m),
    );
}
```
:::
