From fe7c642d2b4dbecc997612f443f09b074b5aa811 Mon Sep 17 00:00:00 2001 From: jakehobbs Date: Tue, 26 May 2026 17:02:44 -0700 Subject: [PATCH 01/10] Document Solana Wallet APIs transaction flows Add Solana transaction, sponsorship, and signer guidance so v5 Wallet APIs users can follow the same docs paths as EVM users. Co-Authored-By: Claude Opus 4.6 (1M context) Co-authored-by: Cursor --- content/docs.yml | 4 +- .../pages/authentication/what-is-a-signer.mdx | 3 +- content/wallets/pages/index.mdx | 2 +- .../third-party/signers/solana-signers.mdx | 150 ++++++++++++++++++ .../wallets/pages/transactions/overview.mdx | 5 +- .../transactions/send-transactions/index.mdx | 135 +++++++++++++++- .../pages/transactions/sponsor-gas/index.mdx | 143 ++++++++++++++++- .../transactions/sponsor-gas/overview.mdx | 4 +- 8 files changed, 423 insertions(+), 23 deletions(-) create mode 100644 content/wallets/pages/third-party/signers/solana-signers.mdx diff --git a/content/docs.yml b/content/docs.yml index 75dc3d1df..90c08743e 100644 --- a/content/docs.yml +++ b/content/docs.yml @@ -895,8 +895,6 @@ navigation: path: wallets/pages/transactions/sponsor-gas/index.mdx - page: Conditional sponsorship rules path: wallets/pages/transactions/sponsor-gas/conditional-sponsorship-rules.mdx - - page: Solana sponsorship - path: wallets/pages/transactions/solana/sponsor-gas-solana.mdx - page: Pay gas with any token path: wallets/pages/transactions/pay-gas-with-any-token/index.mdx - section: Swap tokens @@ -949,6 +947,8 @@ navigation: path: wallets/pages/third-party/signers/openfort.mdx - page: Turnkey path: wallets/pages/third-party/signers/turnkey.mdx + - page: Solana signers + path: wallets/pages/third-party/signers/solana-signers.mdx - page: Other signers path: wallets/pages/third-party/signers/custom-integration.mdx - section: Account Kit (v4) diff --git a/content/wallets/pages/authentication/what-is-a-signer.mdx b/content/wallets/pages/authentication/what-is-a-signer.mdx index 41f7683fc..c15c8b584 100644 --- a/content/wallets/pages/authentication/what-is-a-signer.mdx +++ b/content/wallets/pages/authentication/what-is-a-signer.mdx @@ -44,7 +44,6 @@ Here are some important criteria to consider when choosing a signer. Non-custodial wallet providers store private keys such that they cannot access the private key without the user's involvement. For example, the user must provide a password or passkey that only they know in order to decrypt the private key stored by the provider. Users benefit from heightened security, while remaining in control of their private keys at all times. This is similar to a safety deposit box vault: the provider secures the bank vault but only the user has access to the individual safety deposit boxes (e.g. wallets). - ### MPC wallets (non-custodial) Multi-Party Computation (MPC) providers split the signer account private key into key shares that are then distributed to a number of share holders. Share holders only know the value of their key share and transaction holders can sign transactions without revealing their key shares to other holders. @@ -96,6 +95,8 @@ The `SmartWalletClient` accepts any signer that conforms to standard [viem](http For [EIP-7702](/docs/wallets/transactions/using-eip-7702) delegation, the signer must support `signAuthorization`. See the [third-party signer integration guide](/docs/wallets/third-party/signers/custom-integration) for details on bringing your own signer. +For Solana transactions, use a `SolanaSigner` or one of the [Solana signer adapters](/docs/wallets/third-party/signers/solana-signers) from `@alchemy/wallet-apis/solana`. + *** *Disclaimer: This page refers to third-party services, products software, technology, and content (collectively, "Third-Party Services") that may be integrated or interact with Alchemy's software and services. Alchemy is not responsible for any Third-Party Service, or for any compatibility issues, errors, or bugs caused in whole or in part by the Third-Party Service or any update or upgrade thereto. Your use of any Third-Party Service is at your own risk. You are responsible for obtaining any associated licenses and consents to the extent necessary for you to use the Third-Party Services. Your use of the Third-Party Services may be subject to separate terms and conditions set forth by the provider (including disclaimers or warnings), separate fees or charges, or a separate privacy notice. You are responsible for understanding and complying with any such terms or privacy notice.* diff --git a/content/wallets/pages/index.mdx b/content/wallets/pages/index.mdx index 76efeeecd..92430ee14 100644 --- a/content/wallets/pages/index.mdx +++ b/content/wallets/pages/index.mdx @@ -48,7 +48,7 @@ Simple APIs to send transactions with advanced capabilities. Gas sponsorship, ba native currency. - + Sponsor fees & rent and say goodbye to "insufficient fees". diff --git a/content/wallets/pages/third-party/signers/solana-signers.mdx b/content/wallets/pages/third-party/signers/solana-signers.mdx new file mode 100644 index 000000000..80e8c5351 --- /dev/null +++ b/content/wallets/pages/third-party/signers/solana-signers.mdx @@ -0,0 +1,150 @@ +--- +title: Solana signers +description: Use Solana signers with Wallet APIs +slug: wallets/third-party/signers/solana-signers +--- + +Solana Wallet APIs use an Ed25519 signer that can sign serialized Solana transactions. The `signer` parameter of `createSmartWalletClient` accepts a `SolanaSigner` when you use a Solana chain ID such as `"solana:devnet"` or `"solana:mainnet"`. + +```ts +type SolanaSigner = { + address: string; + signTransaction(input: { + transaction: Uint8Array; + }): Promise<{ signedTransaction: Uint8Array }>; +}; +``` + +Most apps do not need to implement this interface directly. Use one of the adapters from `@alchemy/wallet-apis/solana` to convert your Solana signer into the shape expected by Wallet APIs. + +## Install + + +```shell npm +npm install @alchemy/wallet-apis @solana/kit @solana/web3.js viem +``` + +```shell pnpm +pnpm add @alchemy/wallet-apis @solana/kit @solana/web3.js viem +``` + +```shell yarn +yarn add @alchemy/wallet-apis @solana/kit @solana/web3.js viem +``` + +```shell bun +bun add @alchemy/wallet-apis @solana/kit @solana/web3.js viem +``` + + +`@solana/kit` and `@solana/web3.js` are optional peer dependencies. Install the one that matches the adapter you use. + +## `@solana/kit` signers + +Use `fromKitSigner` for `@solana/kit` signers that implement `signTransactions`, including `KeyPairSigner` and `TransactionPartialSigner`. + +```ts title="kit-signer.ts" +import { + alchemyWalletTransport, + createSmartWalletClient, +} from "@alchemy/wallet-apis"; +import { fromKitSigner } from "@alchemy/wallet-apis/solana"; +import { generateKeyPairSigner } from "@solana/kit"; + +const kitSigner = await generateKeyPairSigner(); +const signer = fromKitSigner(kitSigner); + +const client = createSmartWalletClient({ + transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), + chain: "solana:devnet", + signer, +}); +``` + +## Raw Ed25519 keypair signers + +Use `fromKeypair` when your signer exposes an `address` and a `signMessage(bytes)` method that returns a 64-byte Ed25519 signature. + +```ts title="keypair-signer.ts" +import { + alchemyWalletTransport, + createSmartWalletClient, +} from "@alchemy/wallet-apis"; +import { + fromKeypair, + type SolanaKeypairSigner, +} from "@alchemy/wallet-apis/solana"; + +declare const yourKeypairSigner: SolanaKeypairSigner; + +const signer = fromKeypair(yourKeypairSigner); + +const client = createSmartWalletClient({ + transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), + chain: "solana:devnet", + signer, +}); +``` + +Your `signMessage` implementation must return a valid 64-byte Ed25519 signature for the message bytes. Do not return placeholder bytes; Wallet APIs rejects missing or all-zero signatures. + +## Wallet Adapter signers + +Use `fromWalletAdapter` for wallets that sign `VersionedTransaction` objects, including `useWallet()` from `@solana/wallet-adapter-react` and injected providers such as Phantom. + +```ts title="wallet-adapter-signer.ts" +import { + alchemyWalletTransport, + createSmartWalletClient, +} from "@alchemy/wallet-apis"; +import { + fromWalletAdapter, + type WalletAdapterSigner, +} from "@alchemy/wallet-apis/solana"; + +export function createClientWithWalletAdapter(wallet: WalletAdapterSigner) { + const signer = fromWalletAdapter(wallet); + + return createSmartWalletClient({ + transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), + chain: "solana:devnet", + signer, + }); +} +``` + +## Wallet Standard signers + +Use `fromWalletStandard` for wallets that implement the Wallet Standard `solana:signTransaction` feature. + +```ts title="wallet-standard-signer.ts" +import { + alchemyWalletTransport, + createSmartWalletClient, +} from "@alchemy/wallet-apis"; +import { + fromWalletStandard, + type WalletStandardAccount, + type WalletStandardWallet, +} from "@alchemy/wallet-apis/solana"; + +export function createClientWithWalletStandard( + wallet: WalletStandardWallet, + account: WalletStandardAccount, +) { + const signer = fromWalletStandard(wallet, account); + + return createSmartWalletClient({ + transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), + chain: "solana:devnet", + signer, + }); +} +``` + +## Next steps + +Use your Solana signer to: + +* [Send Solana transactions](/docs/wallets/transactions/send-transactions#solana-transactions) +* [Sponsor Solana transaction fees](/docs/wallets/transactions/sponsor-gas#solana-sponsorship) diff --git a/content/wallets/pages/transactions/overview.mdx b/content/wallets/pages/transactions/overview.mdx index 57ed4f021..3ccd2cd4b 100644 --- a/content/wallets/pages/transactions/overview.mdx +++ b/content/wallets/pages/transactions/overview.mdx @@ -25,13 +25,12 @@ When a user sends a transaction, every step is handled automatically. In this se | Capability | Description | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | -| [Send transactions](/docs/wallets/transactions/send-transactions) | Execute a single transaction | +| [Send transactions](/docs/wallets/transactions/send-transactions) | Execute a single EVM or Solana transaction | | [EIP-7702](/docs/wallets/transactions/using-eip-7702) | Upgrade embedded EOA users to smart wallets | | [Batch transactions](/docs/wallets/transactions/send-batch-transactions) | Execute multiple transactions atomically in a single step (ex: approve & swap) | -| [Sponsor gas](/docs/wallets/transactions/sponsor-gas) | Make gas disappear and say goodbye to "insufficient gas" | +| [Sponsor gas](/docs/wallets/transactions/sponsor-gas) | Sponsor EVM gas or Solana transaction fees | | [Pay gas with any token](/docs/wallets/transactions/pay-gas-with-any-token) | Pay gas with stablecoins or the sell-side token | | [Swap tokens](/docs/wallets/transactions/swap-tokens) | Swap across networks, seamlessly | | [Retry transactions](/docs/wallets/transactions/retry-transactions) | Retry transactions stuck in mempool | | [Send parallel transactions](/docs/wallets/transactions/send-parallel-transactions) | Send multiple transactions in parallel | -| [Sponsor gas on Solana](/docs/wallets/transactions/solana/sponsor-gas) | Sponsor fees & rent and say goodbye to "insufficient fees" | | [Track status](/docs/wallets/transactions/send-transactions) | Track the status of the transaction | diff --git a/content/wallets/pages/transactions/send-transactions/index.mdx b/content/wallets/pages/transactions/send-transactions/index.mdx index 718154798..fd1dc8b0a 100644 --- a/content/wallets/pages/transactions/send-transactions/index.mdx +++ b/content/wallets/pages/transactions/send-transactions/index.mdx @@ -1,19 +1,23 @@ --- title: Send transactions -description: Execute a single transaction +description: Execute EVM and Solana transactions slug: wallets/transactions/send-transactions --- -This guide covers how to send a single EVM transaction. - -The client defaults to using [EIP-7702](/docs/wallets/transactions/using-eip-7702), so your EOA will be delegated to a smart wallet to enable gas sponsorship, batching, and more. The SDK handles delegation automatically on the first transaction. +This guide covers how to send EVM and Solana transactions with Wallet APIs. ## Prerequisites * API key from your [dashboard](https://dashboard.alchemy.com/apps) -* A funded Smart Wallet to cover gas fees ([or a gas manager policy to sponsor gas](/docs/wallets/transactions/sponsor-gas)) +* `@alchemy/wallet-apis` installed in your project +* For EVM: a funded Smart Wallet to cover gas fees ([or a gas manager policy to sponsor gas](/docs/wallets/transactions/sponsor-gas)) +* For Solana: a Solana signer ([see Solana signer adapters](/docs/wallets/third-party/signers/solana-signers)) and a funded wallet to cover fees, or a Solana gas sponsorship policy + +## EVM transactions + +The EVM client defaults to using [EIP-7702](/docs/wallets/transactions/using-eip-7702), so your EOA will be delegated to a smart wallet to enable gas sponsorship, batching, and more. The SDK handles delegation automatically on the first transaction. -## Implementation +### Implementation @@ -26,7 +30,7 @@ The client defaults to using [EIP-7702](/docs/wallets/transactions/using-eip-770 -## Advanced +### Advanced If you need to encode function data (instead of sending value), do so using Viem or Foundry. @@ -43,7 +47,7 @@ The client defaults to using [EIP-7702](/docs/wallets/transactions/using-eip-770 - Instead of using the `sendCalls` abstraction, you can prepare and send calls using underlying methods. Usage of the capability will be the same as when using send calls. It is recommended to use `prepareCalls` if you want to inspect the prepared call prior to prompting the user for signature. + Instead of using the `sendCalls` abstraction, you can prepare and send EVM calls using underlying methods. Usage of the capability will be the same as when using send calls. It is recommended to use `prepareCalls` if you want to inspect the prepared call prior to prompting the user for signature. @@ -56,12 +60,127 @@ The client defaults to using [EIP-7702](/docs/wallets/transactions/using-eip-770 +## Solana transactions + +Solana uses the same Wallet API client action model as EVM, but with Solana chain IDs, Ed25519 signers, and Solana-shaped instruction calls. + +Use `createSmartWalletClient` with a Solana chain ID like `"solana:devnet"` or `"solana:mainnet"`. The client exposes `sendCalls`, `prepareCalls`, `signPreparedCalls`, `sendPreparedCalls`, `getCallsStatus`, and `waitForCallsStatus`. + +### Install Solana dependencies + + +```shell npm +npm install @alchemy/wallet-apis @solana/kit @solana/web3.js viem +``` + +```shell pnpm +pnpm add @alchemy/wallet-apis @solana/kit @solana/web3.js viem +``` + +```shell yarn +yarn add @alchemy/wallet-apis @solana/kit @solana/web3.js viem +``` + +```shell bun +bun add @alchemy/wallet-apis @solana/kit @solana/web3.js viem +``` + + +### Send Solana instructions + +The `calls` array contains Solana instructions. Each instruction has a `programId`, hex-encoded `data`, and the accounts required by the instruction. + +```ts title="send-solana-transaction.ts" +import { + alchemyWalletTransport, + createSmartWalletClient, +} from "@alchemy/wallet-apis"; +import { + fromKitSigner, + type SolanaTransactionPartialSigner, +} from "@alchemy/wallet-apis/solana"; +import { + PublicKey, + SystemProgram, + type TransactionInstruction, +} from "@solana/web3.js"; +import { bytesToHex } from "viem"; + +const ALCHEMY_API_KEY = "YOUR_ALCHEMY_API_KEY"; +declare const yourKitSigner: SolanaTransactionPartialSigner; + +function toSolanaCall(instruction: TransactionInstruction) { + return { + programId: instruction.programId.toBase58(), + accounts: instruction.keys.map((account) => ({ + pubkey: account.pubkey.toBase58(), + isSigner: account.isSigner, + isWritable: account.isWritable, + })), + data: bytesToHex(instruction.data), + }; +} + +function transferCall(from: string, to: string, lamports: number) { + return toSolanaCall( + SystemProgram.transfer({ + fromPubkey: new PublicKey(from), + toPubkey: new PublicKey(to), + lamports, + }), + ); +} + +const signer = fromKitSigner(yourKitSigner); + +const client = createSmartWalletClient({ + transport: alchemyWalletTransport({ apiKey: ALCHEMY_API_KEY }), + chain: "solana:devnet", + signer, +}); + +const { id } = await client.sendCalls({ + calls: [transferCall(client.solanaAccount, client.solanaAccount, 0)], +}); + +const result = await client.waitForCallsStatus({ id }); + +console.log("Solana transaction status:", result.status); +``` + +`client.solanaAccount` defaults to the signer's address. If you pass `account` to `createSmartWalletClient` or `sendCalls`, it must be an account that the configured signer can sign for. + + + For local devnet testing, you can create a signer with + `generateKeyPairSigner()` from `@solana/kit`, but you must fund that address + before sending an unsponsored transaction. + + +### Advanced: prepare, sign, and send + +Use the prepared call flow when you need to inspect the compiled Solana transaction before signing. + +```ts title="prepare-solana-transaction.ts" +const prepared = await client.prepareCalls({ + calls: [transferCall(client.solanaAccount, client.solanaAccount, 0)], +}); + +console.log(prepared.type); // "solana-transaction-v0" +console.log(prepared.signatureRequest.type); // "solana_signTransaction" + +const signed = await client.signPreparedCalls(prepared); +const { id } = await client.sendPreparedCalls(signed); + +const result = await client.waitForCallsStatus({ id }); +``` + ## Next steps Build more: * [Sponsor gas for users](/docs/wallets/transactions/sponsor-gas) * [Send batch transactions](/docs/wallets/transactions/send-batch-transactions) +* [Use Solana signer adapters](/docs/wallets/third-party/signers/solana-signers) Troubleshooting: diff --git a/content/wallets/pages/transactions/sponsor-gas/index.mdx b/content/wallets/pages/transactions/sponsor-gas/index.mdx index ad8a9fa58..d04cb3e61 100644 --- a/content/wallets/pages/transactions/sponsor-gas/index.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/index.mdx @@ -1,10 +1,10 @@ --- title: Sponsor gas -description: Choose a gas sponsorship integration path for your smart wallet application. +description: Sponsor EVM gas and Solana transaction fees for your users. slug: wallets/transactions/sponsor-gas --- -Gas fees are a significant barrier to entry for new users. With Gas Sponsorship, you can eliminate this friction by covering transaction costs for your users. +Gas fees are a significant barrier to entry for new users. With Gas Sponsorship, you can eliminate this friction by covering EVM gas or Solana transaction fees for your users. ## How it works @@ -17,7 +17,11 @@ and bills in fiat. * API key from your [dashboard](https://dashboard.alchemy.com/apps) * [A gas sponsorship policy](https://dashboard.alchemy.com/gas-manager/policy/create). -## Implementation +## EVM sponsorship + +Use EVM sponsorship when sending EVM calls with the smart wallet client. + +### Implementation @@ -31,10 +35,10 @@ and bills in fiat. -## Advanced +### Advanced - Gas sponsorship also works with the prepare calls methods in the various frameworks. Usage of the capability is the same as when using send calls. Use prepare calls if you want to inspect the prepared call before prompting for signature. + EVM gas sponsorship also works with the prepare calls methods in the various frameworks. Usage of the capability is the same as when using send calls. Use prepare calls if you want to inspect the prepared call before prompting for signature. @@ -49,7 +53,7 @@ and bills in fiat. - You can configure multiple policy IDs for use in gas sponsorship. The + For EVM sponsorship, you can configure multiple policy IDs. The backend chooses the first policy ID where the transaction is eligible for sponsorship. Pass an array of policy IDs instead of a single policy ID to the sponsor gas capability. See the [`wallet_prepareCalls` @@ -58,11 +62,138 @@ and bills in fiat. for reference to the `paymasterService.policyIds` parameter. +## Solana sponsorship + +Use Solana sponsorship to cover transaction fees and rent for Solana calls submitted through `@alchemy/wallet-apis`. Solana sponsorship uses the same client actions as standard Solana transactions: `sendCalls`, `prepareCalls`, `signPreparedCalls`, and `sendPreparedCalls`. + +For Solana signer setup, see [Solana signer adapters](/docs/wallets/third-party/signers/solana-signers). + +### Client-level policy + +Pass `paymaster: { policyId }` when creating the Solana smart wallet client to apply the policy to all `sendCalls` and `prepareCalls` requests from that client. + +```ts title="sponsor-solana-client.ts" +import { + alchemyWalletTransport, + createSmartWalletClient, +} from "@alchemy/wallet-apis"; +import { fromKitSigner } from "@alchemy/wallet-apis/solana"; +import { generateKeyPairSigner } from "@solana/kit"; +import { + PublicKey, + SystemProgram, + type TransactionInstruction, +} from "@solana/web3.js"; +import { bytesToHex } from "viem"; + +const ALCHEMY_API_KEY = "YOUR_ALCHEMY_API_KEY"; +const SOLANA_POLICY_ID = "YOUR_SOLANA_POLICY_ID"; + +function toSolanaCall(instruction: TransactionInstruction) { + return { + programId: instruction.programId.toBase58(), + accounts: instruction.keys.map((account) => ({ + pubkey: account.pubkey.toBase58(), + isSigner: account.isSigner, + isWritable: account.isWritable, + })), + data: bytesToHex(instruction.data), + }; +} + +function transferCall(from: string, to: string, lamports: number) { + return toSolanaCall( + SystemProgram.transfer({ + fromPubkey: new PublicKey(from), + toPubkey: new PublicKey(to), + lamports, + }), + ); +} + +const kitSigner = await generateKeyPairSigner(); +const signer = fromKitSigner(kitSigner); + +const client = createSmartWalletClient({ + transport: alchemyWalletTransport({ apiKey: ALCHEMY_API_KEY }), + chain: "solana:devnet", + signer, + paymaster: { policyId: SOLANA_POLICY_ID }, +}); + +const { id } = await client.sendCalls({ + calls: [transferCall(client.solanaAccount, client.solanaAccount, 0)], +}); + +const result = await client.waitForCallsStatus({ id }); + +console.log("Sponsored Solana transaction status:", result.status); +``` + +### Per-request policy + +Pass `capabilities.paymaster` to override the client-level policy for a single `sendCalls` or `prepareCalls` request. Request-level `capabilities.paymaster.policyId` takes priority over the client-level policy. + +```ts title="sponsor-solana-request.ts" +const { id } = await client.sendCalls({ + calls: [transferCall(client.solanaAccount, client.solanaAccount, 0)], + capabilities: { + paymaster: { + policyId: SOLANA_POLICY_ID, + webhookData: "checkout-session-123", + }, + }, +}); + +const result = await client.waitForCallsStatus({ id }); +``` + +The SDK maps the client-facing `paymaster` capability to `paymasterService` on `wallet_prepareCalls`. Solana client-level sponsorship currently accepts a single `policyId`. + +### Rent sponsorship + +Solana sponsorship can also cover rent for account creation. + +Top-level rent sponsorship applies automatically when a sponsored transaction includes top-level `SystemProgram.createAccount` or Associated Token Program `Create` / `CreateIdempotent` instructions. No extra request parameter is required for those top-level instructions. + +For accounts created inside a cross-program invocation (CPI), opt in per request with `prefundRent: true`. This asks the service to simulate the transaction, estimate the CPI rent requirement, and prefund the user's wallet before the transaction is submitted. + +```ts title="sponsor-solana-cpi-rent.ts" +const { id } = await client.sendCalls({ + calls: [transferCall(client.solanaAccount, client.solanaAccount, 0)], + capabilities: { + paymaster: { + policyId: SOLANA_POLICY_ID, + prefundRent: true, + }, + }, +}); + +const result = await client.waitForCallsStatus({ id }); +``` + + + CPI rent prefunding is allowlisted. Contact + [support@alchemy.com](mailto:support@alchemy.com) to request access. Policies + used with `prefundRent` must set `maxSpendPerTxnUsd`, and the transaction + payer must be the user's wallet: the same account your program's CPI will + debit for rent. + + + + If you are not using the Wallet APIs client and need to sponsor a serialized + Solana transaction directly, use the lower-level + [`alchemy_requestFeePayer`](/docs/wallets/transactions/solana/sponsor-gas) + flow. + + ## Next steps Build more: * [Pay gas with any token](/docs/wallets/transactions/pay-gas-with-any-token) +* [Send Solana transactions](/docs/wallets/transactions/send-transactions#solana-transactions) +* [Use Solana signer adapters](/docs/wallets/third-party/signers/solana-signers) Troubleshooting: diff --git a/content/wallets/pages/transactions/sponsor-gas/overview.mdx b/content/wallets/pages/transactions/sponsor-gas/overview.mdx index 4a2e44fc2..a7321d0ee 100644 --- a/content/wallets/pages/transactions/sponsor-gas/overview.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/overview.mdx @@ -44,9 +44,9 @@ Get started with the [gas sponsorship guide](/docs/wallets/transactions/sponsor- - Sponsor transaction fees on Solana + Sponsor transaction fees and rent on Solana From e9927e4d3c137f46d66bcb6fe4ef99da66185484 Mon Sep 17 00:00:00 2001 From: jakehobbs Date: Wed, 27 May 2026 10:43:12 -0700 Subject: [PATCH 02/10] Refine Solana Wallet API docs around validated transaction and sponsorship flows. Co-authored-by: Cursor --- content/docs.yml | 10 +- content/redirects.yml | 4 + .../pages/authentication/what-is-a-signer.mdx | 2 +- content/wallets/pages/index.mdx | 2 +- .../pages/third-party/signers/privy.mdx | 105 +++++++- .../third-party/signers/solana-signers.mdx | 249 ++++++++++++++---- .../wallets/pages/transactions/overview.mdx | 3 +- .../transactions/send-transactions/index.mdx | 117 +------- .../send-transactions/solana/client.mdx | 80 ++++++ .../solana/prepare-calls/client.mdx | 84 ++++++ .../pages/transactions/sponsor-gas/index.mdx | 139 +--------- .../transactions/sponsor-gas/overview.mdx | 4 +- .../sponsor-gas/solana/client.mdx | 83 ++++++ .../transactions/sponsor-gas/solana/index.mdx | 61 +++++ .../sponsor-gas/solana/prefund-rent.mdx | 22 ++ .../sponsor-gas/solana/request-policy.mdx | 22 ++ 16 files changed, 681 insertions(+), 306 deletions(-) create mode 100644 content/wallets/pages/transactions/send-transactions/solana/client.mdx create mode 100644 content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx create mode 100644 content/wallets/pages/transactions/sponsor-gas/solana/client.mdx create mode 100644 content/wallets/pages/transactions/sponsor-gas/solana/index.mdx create mode 100644 content/wallets/pages/transactions/sponsor-gas/solana/prefund-rent.mdx create mode 100644 content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx diff --git a/content/docs.yml b/content/docs.yml index 90c08743e..d61c64693 100644 --- a/content/docs.yml +++ b/content/docs.yml @@ -893,6 +893,8 @@ navigation: contents: - page: Full sponsorship path: wallets/pages/transactions/sponsor-gas/index.mdx + - page: Sponsor Solana fees + path: wallets/pages/transactions/sponsor-gas/solana/index.mdx - page: Conditional sponsorship rules path: wallets/pages/transactions/sponsor-gas/conditional-sponsorship-rules.mdx - page: Pay gas with any token @@ -941,14 +943,16 @@ navigation: path: wallets/wallet-integrations/privy/react-migration.mdx - page: JWT auth migration path: wallets/wallet-integrations/privy/jwt-auth-migration.mdx + - section: Solana + contents: + - page: Signers + path: wallets/pages/third-party/signers/solana-signers.mdx - section: Other signers contents: - page: Openfort path: wallets/pages/third-party/signers/openfort.mdx - page: Turnkey path: wallets/pages/third-party/signers/turnkey.mdx - - page: Solana signers - path: wallets/pages/third-party/signers/solana-signers.mdx - page: Other signers path: wallets/pages/third-party/signers/custom-integration.mdx - section: Account Kit (v4) @@ -1042,6 +1046,8 @@ navigation: contents: - page: API Endpoints path: wallets/pages/low-level-infra/gas-manager/gas-sponsorship/api-endpoints.mdx + - page: Solana + path: wallets/pages/transactions/solana/sponsor-gas-solana.mdx - section: Using SDK contents: - page: Basic Gas Sponsorship diff --git a/content/redirects.yml b/content/redirects.yml index 24f81fdff..36be94ff1 100644 --- a/content/redirects.yml +++ b/content/redirects.yml @@ -7,6 +7,10 @@ redirects: destination: /docs permanent: true + - source: /docs/wallets/third-party/signers/solana-signers + destination: /docs/wallets/solana/signers + permanent: true + # ========================================= Removed Page Redirects ========================================== - source: /docs/reference/new-pricing-for-existing-scale-and-growth-customers destination: /docs/reference/pay-as-you-go-pricing-faq diff --git a/content/wallets/pages/authentication/what-is-a-signer.mdx b/content/wallets/pages/authentication/what-is-a-signer.mdx index c15c8b584..0177951a5 100644 --- a/content/wallets/pages/authentication/what-is-a-signer.mdx +++ b/content/wallets/pages/authentication/what-is-a-signer.mdx @@ -95,7 +95,7 @@ The `SmartWalletClient` accepts any signer that conforms to standard [viem](http For [EIP-7702](/docs/wallets/transactions/using-eip-7702) delegation, the signer must support `signAuthorization`. See the [third-party signer integration guide](/docs/wallets/third-party/signers/custom-integration) for details on bringing your own signer. -For Solana transactions, use a `SolanaSigner` or one of the [Solana signer adapters](/docs/wallets/third-party/signers/solana-signers) from `@alchemy/wallet-apis/solana`. +For Solana transactions, use a `SolanaSigner` or one of the [Solana signer adapters](/docs/wallets/solana/signers) from `@alchemy/wallet-apis/solana`. *** diff --git a/content/wallets/pages/index.mdx b/content/wallets/pages/index.mdx index 92430ee14..ba343494e 100644 --- a/content/wallets/pages/index.mdx +++ b/content/wallets/pages/index.mdx @@ -48,7 +48,7 @@ Simple APIs to send transactions with advanced capabilities. Gas sponsorship, ba native currency. - + Sponsor fees & rent and say goodbye to "insufficient fees". diff --git a/content/wallets/pages/third-party/signers/privy.mdx b/content/wallets/pages/third-party/signers/privy.mdx index 80bcc12a8..37d6b9de2 100644 --- a/content/wallets/pages/third-party/signers/privy.mdx +++ b/content/wallets/pages/third-party/signers/privy.mdx @@ -1,6 +1,6 @@ --- title: Privy -description: Use Privy with Wallet APIs for EIP-7702, sponsorship, and batching +description: Use Privy with Wallet APIs for EIP-7702, Solana, sponsorship, and batching slug: wallets/third-party/signers/privy --- @@ -21,19 +21,19 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba ```shell npm - npm install @alchemy/wallet-apis @privy-io/react-auth viem + npm install @alchemy/wallet-apis @privy-io/react-auth @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js viem ``` ```shell bun - bun add @alchemy/wallet-apis @privy-io/react-auth viem + bun add @alchemy/wallet-apis @privy-io/react-auth @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js viem ``` ```shell yarn - yarn add @alchemy/wallet-apis @privy-io/react-auth viem + yarn add @alchemy/wallet-apis @privy-io/react-auth @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js viem ``` ```shell pnpm - pnpm add @alchemy/wallet-apis @privy-io/react-auth viem + pnpm add @alchemy/wallet-apis @privy-io/react-auth @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js viem ``` @@ -78,6 +78,24 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba } ``` + To create Solana embedded wallets on login, include the Solana embedded wallet configuration: + + ```tsx + + + + ``` + ### 2. Get a signer from Privy Use `toViemAccount` and `useWallets` from `@privy-io/react-auth` to convert a Privy embedded wallet into a viem `LocalAccount`: @@ -170,9 +188,84 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba } ``` + ### 5. Send a sponsored Solana transaction + + Use `useWallets` from `@privy-io/react-auth/solana` to get the connected Solana wallet. The returned wallet can be passed directly as the Wallet APIs Solana signer. + + ```tsx title="privy-sponsored-solana.tsx" + import { + alchemyWalletTransport, + createSmartWalletClient, + } from "@alchemy/wallet-apis"; + import { useWallets } from "@privy-io/react-auth/solana"; + import { + PublicKey, + SystemProgram, + type TransactionInstruction, + } from "@solana/web3.js"; + import { useCallback, useMemo } from "react"; + import { bytesToHex } from "viem"; + + function toSolanaCall(instruction: TransactionInstruction) { + return { + programId: instruction.programId.toBase58(), + accounts: instruction.keys.map((account) => ({ + pubkey: account.pubkey.toBase58(), + isSigner: account.isSigner, + isWritable: account.isWritable, + })), + data: bytesToHex(instruction.data), + }; + } + + function transferCall(from: string, to: string, lamports: number) { + return toSolanaCall( + SystemProgram.transfer({ + fromPubkey: new PublicKey(from), + toPubkey: new PublicKey(to), + lamports, + }), + ); + } + + function SendSponsoredSolanaTransaction() { + const { wallets } = useWallets(); + const signer = wallets[0]; + + const client = useMemo(() => { + if (!signer) return undefined; + + return createSmartWalletClient({ + transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), + chain: "solana:devnet", + signer, + paymaster: { policyId: "YOUR_SOLANA_POLICY_ID" }, + }); + }, [signer]); + + const handleSend = useCallback(async () => { + if (!client) return; + + const { id } = await client.sendCalls({ + calls: [transferCall(client.solanaAccount, client.solanaAccount, 0)], + }); + + const result = await client.waitForCallsStatus({ id }); + console.log("Sponsored Solana transaction status:", result.status); + }, [client]); + + return ( + + ); + } + ``` + ### Notes - * The client defaults to [EIP-7702](/docs/wallets/transactions/using-eip-7702), which delegates the Privy wallet to a smart wallet at send time. No deployment or wallet migration needed. See the [EIP-7702 guide](/docs/wallets/transactions/using-eip-7702) for non-7702 mode. + * The EVM client defaults to [EIP-7702](/docs/wallets/transactions/using-eip-7702), which delegates the Privy wallet to a smart wallet at send time. No deployment or wallet migration needed. See the [EIP-7702 guide](/docs/wallets/transactions/using-eip-7702) for non-7702 mode. + * Solana sponsorship requires a Solana gas sponsorship policy. * See the [Sponsor gas](/docs/wallets/transactions/sponsor-gas) guide for more on gas sponsorship configuration. diff --git a/content/wallets/pages/third-party/signers/solana-signers.mdx b/content/wallets/pages/third-party/signers/solana-signers.mdx index 80e8c5351..d5bd43ee9 100644 --- a/content/wallets/pages/third-party/signers/solana-signers.mdx +++ b/content/wallets/pages/third-party/signers/solana-signers.mdx @@ -1,7 +1,7 @@ --- title: Solana signers description: Use Solana signers with Wallet APIs -slug: wallets/third-party/signers/solana-signers +slug: wallets/solana/signers --- Solana Wallet APIs use an Ed25519 signer that can sign serialized Solana transactions. The `signer` parameter of `createSmartWalletClient` accepts a `SolanaSigner` when you use a Solana chain ID such as `"solana:devnet"` or `"solana:mainnet"`. @@ -15,107 +15,217 @@ type SolanaSigner = { }; ``` -Most apps do not need to implement this interface directly. Use one of the adapters from `@alchemy/wallet-apis/solana` to convert your Solana signer into the shape expected by Wallet APIs. +Most apps do not need to implement this interface directly. Some providers already return this shape. For the rest, use an adapter from `@alchemy/wallet-apis/solana`. ## Install ```shell npm -npm install @alchemy/wallet-apis @solana/kit @solana/web3.js viem +npm install @alchemy/wallet-apis ``` ```shell pnpm -pnpm add @alchemy/wallet-apis @solana/kit @solana/web3.js viem +pnpm add @alchemy/wallet-apis ``` ```shell yarn -yarn add @alchemy/wallet-apis @solana/kit @solana/web3.js viem +yarn add @alchemy/wallet-apis ``` ```shell bun -bun add @alchemy/wallet-apis @solana/kit @solana/web3.js viem +bun add @alchemy/wallet-apis ``` -`@solana/kit` and `@solana/web3.js` are optional peer dependencies. Install the one that matches the adapter you use. +Install the optional package that matches your signer source: -## `@solana/kit` signers +* Privy: `@privy-io/react-auth` +* Privy Solana peer dependencies: `@solana/kit @solana-program/memo @solana-program/system @solana-program/token` +* `@solana/wallet-adapter-react`: `@solana/wallet-adapter-react @solana/web3.js` +* Injected providers such as Phantom: `@solana/web3.js` +* `@solana/kit` signers: `@solana/kit` +* Wallet Standard discovery: `@wallet-standard/app` -Use `fromKitSigner` for `@solana/kit` signers that implement `signTransactions`, including `KeyPairSigner` and `TransactionPartialSigner`. +## Choose a signer source -```ts title="kit-signer.ts" +| What you have | What to use | +| --- | --- | +| Privy Solana wallet from `@privy-io/react-auth/solana` | No adapter needed | +| `useWallet()` from `@solana/wallet-adapter-react` | `fromWalletAdapter` | +| Injected provider such as `window.phantom.solana` | `fromWalletAdapter` | +| `@solana/kit` `KeyPairSigner` or `TransactionPartialSigner` | `fromKitSigner` | +| Raw Ed25519 keypair or key management service | `fromKeypair` | +| Low-level Wallet Standard wallet | `fromWalletStandard` | + +## Privy + +Use `useWallets` from `@privy-io/react-auth/solana` to get connected Solana wallets. The returned wallet can be passed directly to `createSmartWalletClient`. + +```tsx title="privy-solana-signer.tsx" import { alchemyWalletTransport, createSmartWalletClient, } from "@alchemy/wallet-apis"; -import { fromKitSigner } from "@alchemy/wallet-apis/solana"; -import { generateKeyPairSigner } from "@solana/kit"; +import { useWallets } from "@privy-io/react-auth/solana"; +import { useMemo } from "react"; + +function usePrivySolanaClient() { + const { wallets } = useWallets(); + const signer = wallets[0]; + + return useMemo(() => { + if (!signer) return undefined; + + return createSmartWalletClient({ + signer, + transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), + chain: "solana:devnet", + paymaster: { policyId: "YOUR_SOLANA_POLICY_ID" }, + }); + }, [signer]); +} +``` -const kitSigner = await generateKeyPairSigner(); -const signer = fromKitSigner(kitSigner); +## `@solana/wallet-adapter-react` + +Use `fromWalletAdapter` for wallets returned by `useWallet()`. This adapter handles the `Uint8Array` to `VersionedTransaction` conversion required by the wallet adapter API. + +```tsx title="wallet-adapter-signer.tsx" +import { + alchemyWalletTransport, + createSmartWalletClient, +} from "@alchemy/wallet-apis"; +import { fromWalletAdapter } from "@alchemy/wallet-apis/solana"; +import { useWallet } from "@solana/wallet-adapter-react"; +import { useMemo } from "react"; + +function useWalletAdapterSolanaClient() { + const wallet = useWallet(); + + return useMemo(() => { + if (!wallet.publicKey || !wallet.signTransaction) return undefined; + + return createSmartWalletClient({ + signer: fromWalletAdapter({ + publicKey: wallet.publicKey, + signTransaction: wallet.signTransaction, + }), + transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), + chain: "solana:devnet", + paymaster: { policyId: "YOUR_SOLANA_POLICY_ID" }, + }); + }, [wallet.publicKey, wallet.signTransaction]); +} +``` + +## Phantom injected provider + +Use the same `fromWalletAdapter` adapter for injected providers that expose `publicKey` and `signTransaction(VersionedTransaction)`, such as `window.phantom.solana`. + +```ts title="phantom-signer.ts" +import { + alchemyWalletTransport, + createSmartWalletClient, +} from "@alchemy/wallet-apis"; +import { fromWalletAdapter } from "@alchemy/wallet-apis/solana"; +import type { VersionedTransaction } from "@solana/web3.js"; + +interface PhantomSolanaProvider { + publicKey?: { toBase58(): string }; + connect(): Promise<{ publicKey: { toBase58(): string } }>; + signTransaction(transaction: T): Promise; +} + +declare global { + interface Window { + phantom?: { solana?: PhantomSolanaProvider }; + } +} + +const phantom = window.phantom?.solana; +if (!phantom) { + throw new Error("Install Phantom to use the injected Solana provider"); +} + +const { publicKey } = await phantom.connect(); const client = createSmartWalletClient({ + signer: fromWalletAdapter({ + publicKey, + signTransaction: (transaction) => phantom.signTransaction(transaction), + }), transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), chain: "solana:devnet", - signer, + paymaster: { policyId: "YOUR_SOLANA_POLICY_ID" }, }); ``` -## Raw Ed25519 keypair signers +## `@solana/kit` -Use `fromKeypair` when your signer exposes an `address` and a `signMessage(bytes)` method that returns a 64-byte Ed25519 signature. +Use `fromKitSigner` for `@solana/kit` signers that implement `signTransactions`, including `KeyPairSigner` and `TransactionPartialSigner`. -```ts title="keypair-signer.ts" +```ts title="kit-signer.ts" import { alchemyWalletTransport, createSmartWalletClient, } from "@alchemy/wallet-apis"; -import { - fromKeypair, - type SolanaKeypairSigner, -} from "@alchemy/wallet-apis/solana"; - -declare const yourKeypairSigner: SolanaKeypairSigner; +import { fromKitSigner } from "@alchemy/wallet-apis/solana"; +import { generateKeyPairSigner } from "@solana/kit"; -const signer = fromKeypair(yourKeypairSigner); +const kitSigner = await generateKeyPairSigner(); const client = createSmartWalletClient({ + signer: fromKitSigner(kitSigner), transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), chain: "solana:devnet", - signer, + paymaster: { policyId: "YOUR_SOLANA_POLICY_ID" }, }); ``` -Your `signMessage` implementation must return a valid 64-byte Ed25519 signature for the message bytes. Do not return placeholder bytes; Wallet APIs rejects missing or all-zero signatures. +## Raw Ed25519 keypair -## Wallet Adapter signers +Use `fromKeypair` when your signer exposes an `address` and a `signMessage(bytes)` method that returns a 64-byte Ed25519 signature. This is the right adapter for a bare Ed25519 keypair or a key management service. -Use `fromWalletAdapter` for wallets that sign `VersionedTransaction` objects, including `useWallet()` from `@solana/wallet-adapter-react` and injected providers such as Phantom. - -```ts title="wallet-adapter-signer.ts" +```ts title="keypair-signer.ts" import { alchemyWalletTransport, createSmartWalletClient, } from "@alchemy/wallet-apis"; -import { - fromWalletAdapter, - type WalletAdapterSigner, -} from "@alchemy/wallet-apis/solana"; +import { fromKeypair } from "@alchemy/wallet-apis/solana"; -export function createClientWithWalletAdapter(wallet: WalletAdapterSigner) { - const signer = fromWalletAdapter(wallet); +interface Ed25519Keypair { + address: string; + privateKey: CryptoKey; +} - return createSmartWalletClient({ - transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), - chain: "solana:devnet", - signer, - }); +async function loadEd25519Keypair(): Promise { + // Load this from your app's key storage or KMS. + throw new Error("Implement loadEd25519Keypair for your app"); } + +const { address, privateKey } = await loadEd25519Keypair(); + +const client = createSmartWalletClient({ + signer: fromKeypair({ + address, + async signMessage(message) { + const messageBytes = Uint8Array.from(message); + return new Uint8Array( + await crypto.subtle.sign("Ed25519", privateKey, messageBytes), + ); + }, + }), + transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), + chain: "solana:devnet", + paymaster: { policyId: "YOUR_SOLANA_POLICY_ID" }, +}); ``` -## Wallet Standard signers +Your `signMessage` implementation must return a valid 64-byte Ed25519 signature for the message bytes. Do not return placeholder bytes; Wallet APIs rejects missing or all-zero signatures. + +## Wallet Standard -Use `fromWalletStandard` for wallets that implement the Wallet Standard `solana:signTransaction` feature. +Use `fromWalletStandard` when you are working directly with the low-level Wallet Standard API. Most app developers should use Privy or `@solana/wallet-adapter-react` instead. ```ts title="wallet-standard-signer.ts" import { @@ -127,19 +237,48 @@ import { type WalletStandardAccount, type WalletStandardWallet, } from "@alchemy/wallet-apis/solana"; +import { getWallets } from "@wallet-standard/app"; + +type ConnectableWalletStandardWallet = Omit< + WalletStandardWallet, + "features" +> & { + features: { + readonly [name: string]: unknown; + readonly "standard:connect": { + connect(): Promise<{ accounts: readonly WalletStandardAccount[] }>; + }; + readonly "solana:signTransaction": unknown; + }; +}; -export function createClientWithWalletStandard( +function isSolanaStandardWallet( wallet: WalletStandardWallet, - account: WalletStandardAccount, -) { - const signer = fromWalletStandard(wallet, account); - - return createSmartWalletClient({ - transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), - chain: "solana:devnet", - signer, - }); +): wallet is ConnectableWalletStandardWallet { + return ( + "standard:connect" in wallet.features && + "solana:signTransaction" in wallet.features + ); +} + +const wallets: readonly WalletStandardWallet[] = getWallets().get(); +const wallet = wallets.find(isSolanaStandardWallet); +if (!wallet) { + throw new Error("Connect a wallet that supports solana:signTransaction"); +} + +const { accounts } = await wallet.features["standard:connect"].connect(); +const account = accounts[0]; +if (!account) { + throw new Error("No Solana account returned by the wallet"); } + +const client = createSmartWalletClient({ + signer: fromWalletStandard(wallet, account), + transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), + chain: "solana:devnet", + paymaster: { policyId: "YOUR_SOLANA_POLICY_ID" }, +}); ``` ## Next steps @@ -147,4 +286,4 @@ export function createClientWithWalletStandard( Use your Solana signer to: * [Send Solana transactions](/docs/wallets/transactions/send-transactions#solana-transactions) -* [Sponsor Solana transaction fees](/docs/wallets/transactions/sponsor-gas#solana-sponsorship) +* [Sponsor Solana transaction fees](/docs/wallets/transactions/sponsor-gas/solana) diff --git a/content/wallets/pages/transactions/overview.mdx b/content/wallets/pages/transactions/overview.mdx index 3ccd2cd4b..a8947136b 100644 --- a/content/wallets/pages/transactions/overview.mdx +++ b/content/wallets/pages/transactions/overview.mdx @@ -28,7 +28,8 @@ When a user sends a transaction, every step is handled automatically. In this se | [Send transactions](/docs/wallets/transactions/send-transactions) | Execute a single EVM or Solana transaction | | [EIP-7702](/docs/wallets/transactions/using-eip-7702) | Upgrade embedded EOA users to smart wallets | | [Batch transactions](/docs/wallets/transactions/send-batch-transactions) | Execute multiple transactions atomically in a single step (ex: approve & swap) | -| [Sponsor gas](/docs/wallets/transactions/sponsor-gas) | Sponsor EVM gas or Solana transaction fees | +| [Sponsor gas](/docs/wallets/transactions/sponsor-gas) | Sponsor EVM gas | +| [Sponsor Solana fees](/docs/wallets/transactions/sponsor-gas/solana) | Sponsor Solana transaction fees and rent | | [Pay gas with any token](/docs/wallets/transactions/pay-gas-with-any-token) | Pay gas with stablecoins or the sell-side token | | [Swap tokens](/docs/wallets/transactions/swap-tokens) | Swap across networks, seamlessly | | [Retry transactions](/docs/wallets/transactions/retry-transactions) | Retry transactions stuck in mempool | diff --git a/content/wallets/pages/transactions/send-transactions/index.mdx b/content/wallets/pages/transactions/send-transactions/index.mdx index fd1dc8b0a..de5989342 100644 --- a/content/wallets/pages/transactions/send-transactions/index.mdx +++ b/content/wallets/pages/transactions/send-transactions/index.mdx @@ -11,7 +11,7 @@ This guide covers how to send EVM and Solana transactions with Wallet APIs. * API key from your [dashboard](https://dashboard.alchemy.com/apps) * `@alchemy/wallet-apis` installed in your project * For EVM: a funded Smart Wallet to cover gas fees ([or a gas manager policy to sponsor gas](/docs/wallets/transactions/sponsor-gas)) -* For Solana: a Solana signer ([see Solana signer adapters](/docs/wallets/third-party/signers/solana-signers)) and a funded wallet to cover fees, or a Solana gas sponsorship policy +* For Solana: a Solana signer ([see Solana signer adapters](/docs/wallets/solana/signers)) and a funded wallet to cover fees, or a Solana gas sponsorship policy ## EVM transactions @@ -62,125 +62,30 @@ The EVM client defaults to using [EIP-7702](/docs/wallets/transactions/using-eip ## Solana transactions -Solana uses the same Wallet API client action model as EVM, but with Solana chain IDs, Ed25519 signers, and Solana-shaped instruction calls. - -Use `createSmartWalletClient` with a Solana chain ID like `"solana:devnet"` or `"solana:mainnet"`. The client exposes `sendCalls`, `prepareCalls`, `signPreparedCalls`, `sendPreparedCalls`, `getCallsStatus`, and `waitForCallsStatus`. - -### Install Solana dependencies - - -```shell npm -npm install @alchemy/wallet-apis @solana/kit @solana/web3.js viem -``` - -```shell pnpm -pnpm add @alchemy/wallet-apis @solana/kit @solana/web3.js viem -``` - -```shell yarn -yarn add @alchemy/wallet-apis @solana/kit @solana/web3.js viem -``` - -```shell bun -bun add @alchemy/wallet-apis @solana/kit @solana/web3.js viem -``` - - -### Send Solana instructions - -The `calls` array contains Solana instructions. Each instruction has a `programId`, hex-encoded `data`, and the accounts required by the instruction. - -```ts title="send-solana-transaction.ts" -import { - alchemyWalletTransport, - createSmartWalletClient, -} from "@alchemy/wallet-apis"; -import { - fromKitSigner, - type SolanaTransactionPartialSigner, -} from "@alchemy/wallet-apis/solana"; -import { - PublicKey, - SystemProgram, - type TransactionInstruction, -} from "@solana/web3.js"; -import { bytesToHex } from "viem"; - -const ALCHEMY_API_KEY = "YOUR_ALCHEMY_API_KEY"; -declare const yourKitSigner: SolanaTransactionPartialSigner; - -function toSolanaCall(instruction: TransactionInstruction) { - return { - programId: instruction.programId.toBase58(), - accounts: instruction.keys.map((account) => ({ - pubkey: account.pubkey.toBase58(), - isSigner: account.isSigner, - isWritable: account.isWritable, - })), - data: bytesToHex(instruction.data), - }; -} - -function transferCall(from: string, to: string, lamports: number) { - return toSolanaCall( - SystemProgram.transfer({ - fromPubkey: new PublicKey(from), - toPubkey: new PublicKey(to), - lamports, - }), - ); -} - -const signer = fromKitSigner(yourKitSigner); - -const client = createSmartWalletClient({ - transport: alchemyWalletTransport({ apiKey: ALCHEMY_API_KEY }), - chain: "solana:devnet", - signer, -}); - -const { id } = await client.sendCalls({ - calls: [transferCall(client.solanaAccount, client.solanaAccount, 0)], -}); - -const result = await client.waitForCallsStatus({ id }); - -console.log("Solana transaction status:", result.status); -``` +Solana transactions use the same client actions as EVM, but `calls` contain Solana instructions: `programId`, hex-encoded `data`, and account metadata. For signer setup, see [Solana signer adapters](/docs/wallets/solana/signers). `client.solanaAccount` defaults to the signer's address. If you pass `account` to `createSmartWalletClient` or `sendCalls`, it must be an account that the configured signer can sign for. - - For local devnet testing, you can create a signer with - `generateKeyPairSigner()` from `@solana/kit`, but you must fund that address - before sending an unsponsored transaction. - - -### Advanced: prepare, sign, and send - -Use the prepared call flow when you need to inspect the compiled Solana transaction before signing. +### Implementation -```ts title="prepare-solana-transaction.ts" -const prepared = await client.prepareCalls({ - calls: [transferCall(client.solanaAccount, client.solanaAccount, 0)], -}); + -console.log(prepared.type); // "solana-transaction-v0" -console.log(prepared.signatureRequest.type); // "solana_signTransaction" +### Advanced -const signed = await client.signPreparedCalls(prepared); -const { id } = await client.sendPreparedCalls(signed); + + Use the prepared call flow when you need to inspect the compiled Solana transaction before signing. -const result = await client.waitForCallsStatus({ id }); -``` + + ## Next steps Build more: * [Sponsor gas for users](/docs/wallets/transactions/sponsor-gas) +* [Sponsor Solana fees](/docs/wallets/transactions/sponsor-gas/solana) * [Send batch transactions](/docs/wallets/transactions/send-batch-transactions) -* [Use Solana signer adapters](/docs/wallets/third-party/signers/solana-signers) +* [Use Solana signer adapters](/docs/wallets/solana/signers) Troubleshooting: diff --git a/content/wallets/pages/transactions/send-transactions/solana/client.mdx b/content/wallets/pages/transactions/send-transactions/solana/client.mdx new file mode 100644 index 000000000..8eb14a561 --- /dev/null +++ b/content/wallets/pages/transactions/send-transactions/solana/client.mdx @@ -0,0 +1,80 @@ +You can send Solana instructions using the smart wallet client [`sendCalls`](/docs/wallets/reference/wallet-apis/functions/sendCalls) action. + + + ```ts title="sendCalls.ts" + import { client } from "./client.ts"; + import { transferSolCall } from "./utils.ts"; + + const { id } = await client.sendCalls({ + calls: [ + transferSolCall({ + from: client.solanaAccount, + to: client.solanaAccount, + lamports: 0n, + }), + ], + }); + + console.log({ id }); + + const result = await client.waitForCallsStatus({ id }); + + console.log(result); + ``` + + ```ts title="client.ts" + import { + alchemyWalletTransport, + createSmartWalletClient, + type SolanaSigner, + } from "@alchemy/wallet-apis"; + + function getYourSolanaSigner(): SolanaSigner { + // Load this from your app's signer integration. + throw new Error("Implement getYourSolanaSigner for your app"); + } + + export const client = createSmartWalletClient({ + signer: getYourSolanaSigner(), + transport: alchemyWalletTransport({ + apiKey: "YOUR_API_KEY", + }), + chain: "solana:devnet", + }); + ``` + + ```ts title="utils.ts" + import type { Hex } from "viem"; + + const SYSTEM_PROGRAM_ID = "11111111111111111111111111111111"; + + function toLittleEndianHex(value: bigint, byteLength: number) { + let hex = ""; + + for (let i = 0; i < byteLength; i++) { + const byte = Number((value >> BigInt(i * 8)) & 0xffn); + hex += byte.toString(16).padStart(2, "0"); + } + + return hex; + } + + export function transferSolCall(args: { + from: string; + to: string; + lamports: bigint; + }) { + return { + programId: SYSTEM_PROGRAM_ID, + accounts: [ + { pubkey: args.from, isSigner: true, isWritable: true }, + { pubkey: args.to, isSigner: false, isWritable: true }, + ], + data: `0x${toLittleEndianHex(2n, 4)}${toLittleEndianHex( + args.lamports, + 8, + )}` as Hex, + }; + } + ``` + diff --git a/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx b/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx new file mode 100644 index 000000000..45d2819cd --- /dev/null +++ b/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx @@ -0,0 +1,84 @@ +You can use smart wallet client actions to prepare, sign, and send Solana transactions. + + + ```ts title="sendCalls.ts" focus={4-24} + import { client } from "./client.ts"; + import { transferSolCall } from "./utils.ts"; + + const preparedCalls = await client.prepareCalls({ + calls: [ + transferSolCall({ + from: client.solanaAccount, + to: client.solanaAccount, + lamports: 0n, + }), + ], + }); + + console.log(preparedCalls.type); // "solana-transaction-v0" + console.log(preparedCalls.signatureRequest.type); // "solana_signTransaction" + + const signedCalls = await client.signPreparedCalls(preparedCalls); + const sentCalls = await client.sendPreparedCalls(signedCalls); + + console.log(sentCalls); + ``` + + ```ts title="client.ts" + import { + alchemyWalletTransport, + createSmartWalletClient, + type SolanaSigner, + } from "@alchemy/wallet-apis"; + + function getYourSolanaSigner(): SolanaSigner { + // Load this from your app's signer integration. + throw new Error("Implement getYourSolanaSigner for your app"); + } + + export const client = createSmartWalletClient({ + signer: getYourSolanaSigner(), + transport: alchemyWalletTransport({ + apiKey: "YOUR_API_KEY", + }), + chain: "solana:devnet", + }); + ``` + + ```ts title="utils.ts" + import type { Hex } from "viem"; + + const SYSTEM_PROGRAM_ID = "11111111111111111111111111111111"; + + function toLittleEndianHex(value: bigint, byteLength: number) { + let hex = ""; + + for (let i = 0; i < byteLength; i++) { + const byte = Number((value >> BigInt(i * 8)) & 0xffn); + hex += byte.toString(16).padStart(2, "0"); + } + + return hex; + } + + export function transferSolCall(args: { + from: string; + to: string; + lamports: bigint; + }) { + return { + programId: SYSTEM_PROGRAM_ID, + accounts: [ + { pubkey: args.from, isSigner: true, isWritable: true }, + { pubkey: args.to, isSigner: false, isWritable: true }, + ], + data: `0x${toLittleEndianHex(2n, 4)}${toLittleEndianHex( + args.lamports, + 8, + )}` as Hex, + }; + } + ``` + + +See the [`prepareCalls`](/docs/wallets/reference/wallet-apis/functions/prepareCalls), [`signPreparedCalls`](/docs/wallets/reference/wallet-apis/functions/signPreparedCalls), and [`sendPreparedCalls`](/docs/wallets/reference/wallet-apis/functions/sendPreparedCalls) SDK references for full parameter descriptions. diff --git a/content/wallets/pages/transactions/sponsor-gas/index.mdx b/content/wallets/pages/transactions/sponsor-gas/index.mdx index d04cb3e61..9d74686b2 100644 --- a/content/wallets/pages/transactions/sponsor-gas/index.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/index.mdx @@ -1,10 +1,10 @@ --- title: Sponsor gas -description: Sponsor EVM gas and Solana transaction fees for your users. +description: Sponsor EVM gas for your users. slug: wallets/transactions/sponsor-gas --- -Gas fees are a significant barrier to entry for new users. With Gas Sponsorship, you can eliminate this friction by covering EVM gas or Solana transaction fees for your users. +Gas fees are a significant barrier to entry for new users. With Gas Sponsorship, you can eliminate this friction by covering EVM gas for your users. ## How it works @@ -17,11 +17,7 @@ and bills in fiat. * API key from your [dashboard](https://dashboard.alchemy.com/apps) * [A gas sponsorship policy](https://dashboard.alchemy.com/gas-manager/policy/create). -## EVM sponsorship - -Use EVM sponsorship when sending EVM calls with the smart wallet client. - -### Implementation +## Implementation @@ -35,7 +31,7 @@ Use EVM sponsorship when sending EVM calls with the smart wallet client. -### Advanced +## Advanced EVM gas sponsorship also works with the prepare calls methods in the various frameworks. Usage of the capability is the same as when using send calls. Use prepare calls if you want to inspect the prepared call before prompting for signature. @@ -62,129 +58,9 @@ Use EVM sponsorship when sending EVM calls with the smart wallet client. for reference to the `paymasterService.policyIds` parameter. -## Solana sponsorship - -Use Solana sponsorship to cover transaction fees and rent for Solana calls submitted through `@alchemy/wallet-apis`. Solana sponsorship uses the same client actions as standard Solana transactions: `sendCalls`, `prepareCalls`, `signPreparedCalls`, and `sendPreparedCalls`. - -For Solana signer setup, see [Solana signer adapters](/docs/wallets/third-party/signers/solana-signers). - -### Client-level policy - -Pass `paymaster: { policyId }` when creating the Solana smart wallet client to apply the policy to all `sendCalls` and `prepareCalls` requests from that client. - -```ts title="sponsor-solana-client.ts" -import { - alchemyWalletTransport, - createSmartWalletClient, -} from "@alchemy/wallet-apis"; -import { fromKitSigner } from "@alchemy/wallet-apis/solana"; -import { generateKeyPairSigner } from "@solana/kit"; -import { - PublicKey, - SystemProgram, - type TransactionInstruction, -} from "@solana/web3.js"; -import { bytesToHex } from "viem"; - -const ALCHEMY_API_KEY = "YOUR_ALCHEMY_API_KEY"; -const SOLANA_POLICY_ID = "YOUR_SOLANA_POLICY_ID"; - -function toSolanaCall(instruction: TransactionInstruction) { - return { - programId: instruction.programId.toBase58(), - accounts: instruction.keys.map((account) => ({ - pubkey: account.pubkey.toBase58(), - isSigner: account.isSigner, - isWritable: account.isWritable, - })), - data: bytesToHex(instruction.data), - }; -} - -function transferCall(from: string, to: string, lamports: number) { - return toSolanaCall( - SystemProgram.transfer({ - fromPubkey: new PublicKey(from), - toPubkey: new PublicKey(to), - lamports, - }), - ); -} - -const kitSigner = await generateKeyPairSigner(); -const signer = fromKitSigner(kitSigner); - -const client = createSmartWalletClient({ - transport: alchemyWalletTransport({ apiKey: ALCHEMY_API_KEY }), - chain: "solana:devnet", - signer, - paymaster: { policyId: SOLANA_POLICY_ID }, -}); - -const { id } = await client.sendCalls({ - calls: [transferCall(client.solanaAccount, client.solanaAccount, 0)], -}); - -const result = await client.waitForCallsStatus({ id }); - -console.log("Sponsored Solana transaction status:", result.status); -``` - -### Per-request policy - -Pass `capabilities.paymaster` to override the client-level policy for a single `sendCalls` or `prepareCalls` request. Request-level `capabilities.paymaster.policyId` takes priority over the client-level policy. - -```ts title="sponsor-solana-request.ts" -const { id } = await client.sendCalls({ - calls: [transferCall(client.solanaAccount, client.solanaAccount, 0)], - capabilities: { - paymaster: { - policyId: SOLANA_POLICY_ID, - webhookData: "checkout-session-123", - }, - }, -}); - -const result = await client.waitForCallsStatus({ id }); -``` - -The SDK maps the client-facing `paymaster` capability to `paymasterService` on `wallet_prepareCalls`. Solana client-level sponsorship currently accepts a single `policyId`. - -### Rent sponsorship - -Solana sponsorship can also cover rent for account creation. - -Top-level rent sponsorship applies automatically when a sponsored transaction includes top-level `SystemProgram.createAccount` or Associated Token Program `Create` / `CreateIdempotent` instructions. No extra request parameter is required for those top-level instructions. - -For accounts created inside a cross-program invocation (CPI), opt in per request with `prefundRent: true`. This asks the service to simulate the transaction, estimate the CPI rent requirement, and prefund the user's wallet before the transaction is submitted. - -```ts title="sponsor-solana-cpi-rent.ts" -const { id } = await client.sendCalls({ - calls: [transferCall(client.solanaAccount, client.solanaAccount, 0)], - capabilities: { - paymaster: { - policyId: SOLANA_POLICY_ID, - prefundRent: true, - }, - }, -}); - -const result = await client.waitForCallsStatus({ id }); -``` - - - CPI rent prefunding is allowlisted. Contact - [support@alchemy.com](mailto:support@alchemy.com) to request access. Policies - used with `prefundRent` must set `maxSpendPerTxnUsd`, and the transaction - payer must be the user's wallet: the same account your program's CPI will - debit for rent. - - - If you are not using the Wallet APIs client and need to sponsor a serialized - Solana transaction directly, use the lower-level - [`alchemy_requestFeePayer`](/docs/wallets/transactions/solana/sponsor-gas) - flow. + Sponsoring Solana fees and rent uses Solana-specific policy configuration and + request options. See [Sponsor Solana fees](/docs/wallets/transactions/sponsor-gas/solana). ## Next steps @@ -192,8 +68,7 @@ const result = await client.waitForCallsStatus({ id }); Build more: * [Pay gas with any token](/docs/wallets/transactions/pay-gas-with-any-token) -* [Send Solana transactions](/docs/wallets/transactions/send-transactions#solana-transactions) -* [Use Solana signer adapters](/docs/wallets/third-party/signers/solana-signers) +* [Sponsor Solana fees](/docs/wallets/transactions/sponsor-gas/solana) Troubleshooting: diff --git a/content/wallets/pages/transactions/sponsor-gas/overview.mdx b/content/wallets/pages/transactions/sponsor-gas/overview.mdx index a7321d0ee..7b2eace09 100644 --- a/content/wallets/pages/transactions/sponsor-gas/overview.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/overview.mdx @@ -42,9 +42,9 @@ Get started with the [gas sponsorship guide](/docs/wallets/transactions/sponsor- Let users pay gas with USDC, USDT, or any token Sponsor transaction fees and rent on Solana diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx new file mode 100644 index 000000000..faff1fbf2 --- /dev/null +++ b/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx @@ -0,0 +1,83 @@ +Use `paymaster` on the smart wallet client to apply a Solana sponsorship policy to all `sendCalls` and `prepareCalls` requests from that client. + + + ```ts title="sendCalls.ts" + import { client } from "./client.ts"; + import { transferSolCall } from "./utils.ts"; + + const { id } = await client.sendCalls({ + calls: [ + transferSolCall({ + from: client.solanaAccount, + to: client.solanaAccount, + lamports: 0n, + }), + ], + }); + + const result = await client.waitForCallsStatus({ id }); + + console.log(result); + ``` + + ```ts title="client.ts" + import { + alchemyWalletTransport, + createSmartWalletClient, + type SolanaSigner, + } from "@alchemy/wallet-apis"; + + export const config = { + policyId: "YOUR_SOLANA_POLICY_ID", + }; + + function getYourSolanaSigner(): SolanaSigner { + // Load this from your app's signer integration. + throw new Error("Implement getYourSolanaSigner for your app"); + } + + export const client = createSmartWalletClient({ + signer: getYourSolanaSigner(), + transport: alchemyWalletTransport({ + apiKey: "YOUR_API_KEY", + }), + chain: "solana:devnet", + paymaster: { policyId: config.policyId }, + }); + ``` + + ```ts title="utils.ts" + import type { Hex } from "viem"; + + const SYSTEM_PROGRAM_ID = "11111111111111111111111111111111"; + + function toLittleEndianHex(value: bigint, byteLength: number) { + let hex = ""; + + for (let i = 0; i < byteLength; i++) { + const byte = Number((value >> BigInt(i * 8)) & 0xffn); + hex += byte.toString(16).padStart(2, "0"); + } + + return hex; + } + + export function transferSolCall(args: { + from: string; + to: string; + lamports: bigint; + }) { + return { + programId: SYSTEM_PROGRAM_ID, + accounts: [ + { pubkey: args.from, isSigner: true, isWritable: true }, + { pubkey: args.to, isSigner: false, isWritable: true }, + ], + data: `0x${toLittleEndianHex(2n, 4)}${toLittleEndianHex( + args.lamports, + 8, + )}` as Hex, + }; + } + ``` + diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx new file mode 100644 index 000000000..2e5b50653 --- /dev/null +++ b/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx @@ -0,0 +1,61 @@ +--- +title: Sponsor Solana fees +description: Sponsor Solana transaction fees and rent with Wallet APIs +slug: wallets/transactions/sponsor-gas/solana +--- + +Use Solana sponsorship to cover transaction fees and rent for Solana calls submitted through `@alchemy/wallet-apis`. + +## Prerequisites + +* API key from your [dashboard](https://dashboard.alchemy.com/apps) +* Solana gas sponsorship policy from the [Gas Manager dashboard](https://dashboard.alchemy.com/gas-manager/policy/create) +* Solana signer configured in your app. See [Solana signer adapters](/docs/wallets/solana/signers). + +## Implementation + + + +## Advanced + + + Pass `capabilities.paymaster` to override the client-level policy for a single `sendCalls` or `prepareCalls` request. Request-level `capabilities.paymaster.policyId` takes priority over the client-level policy. + + + + + + Solana sponsorship can also cover rent for account creation. + + Top-level rent sponsorship applies automatically when a sponsored transaction includes top-level `SystemProgram.createAccount` or Associated Token Program `Create` / `CreateIdempotent` instructions. No extra request parameter is required for those top-level instructions. + + For accounts created inside a cross-program invocation (CPI), opt in per request with `prefundRent: true`. This asks the service to simulate the transaction, estimate the CPI rent requirement, and prefund the user's wallet before the transaction is submitted. + + + + + CPI rent prefunding is allowlisted. Contact + [support@alchemy.com](mailto:support@alchemy.com) to request access. Policies + used with `prefundRent` must set `maxSpendPerTxnUsd`, and the transaction + payer must be the user's wallet: the same account your program's CPI will + debit for rent. + + + + + If you are not using the Wallet APIs client and need to sponsor a serialized + Solana transaction directly, use the lower-level + [`alchemy_requestFeePayer`](/docs/wallets/transactions/solana/sponsor-gas) + flow. + + +## Next steps + +Build more: + +* [Send Solana transactions](/docs/wallets/transactions/send-transactions#solana-transactions) +* [Use Solana signer adapters](/docs/wallets/solana/signers) + +Troubleshooting: + +* [Gas manager errors](/docs/reference/gas-manager-errors) diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/prefund-rent.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/prefund-rent.mdx new file mode 100644 index 000000000..043cdfda0 --- /dev/null +++ b/content/wallets/pages/transactions/sponsor-gas/solana/prefund-rent.mdx @@ -0,0 +1,22 @@ +```ts title="sponsor-solana-cpi-rent.ts" +import { client, config } from "./client.ts"; +import { transferSolCall } from "./utils.ts"; + +const { id } = await client.sendCalls({ + calls: [ + transferSolCall({ + from: client.solanaAccount, + to: client.solanaAccount, + lamports: 0n, + }), + ], + capabilities: { + paymaster: { + policyId: config.policyId, + prefundRent: true, + }, + }, +}); + +const result = await client.waitForCallsStatus({ id }); +``` diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx new file mode 100644 index 000000000..19f3aafb9 --- /dev/null +++ b/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx @@ -0,0 +1,22 @@ +```ts title="sponsor-solana-request.ts" +import { client, config } from "./client.ts"; +import { transferSolCall } from "./utils.ts"; + +const { id } = await client.sendCalls({ + calls: [ + transferSolCall({ + from: client.solanaAccount, + to: client.solanaAccount, + lamports: 0n, + }), + ], + capabilities: { + paymaster: { + policyId: config.policyId, + webhookData: "checkout-session-123", + }, + }, +}); + +const result = await client.waitForCallsStatus({ id }); +``` From e52916b0dafe510373861e2b0e109ac9f522aed9 Mon Sep 17 00:00:00 2001 From: jakehobbs Date: Wed, 27 May 2026 14:43:54 -0700 Subject: [PATCH 03/10] Split Privy Solana peer deps and fix per-request policy example. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../pages/third-party/signers/privy.mdx | 28 ++++++++++++++++--- .../sponsor-gas/solana/request-policy.mdx | 5 ++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/content/wallets/pages/third-party/signers/privy.mdx b/content/wallets/pages/third-party/signers/privy.mdx index 37d6b9de2..cc07c1a42 100644 --- a/content/wallets/pages/third-party/signers/privy.mdx +++ b/content/wallets/pages/third-party/signers/privy.mdx @@ -21,19 +21,39 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba ```shell npm - npm install @alchemy/wallet-apis @privy-io/react-auth @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js viem + npm install @alchemy/wallet-apis @privy-io/react-auth viem ``` ```shell bun - bun add @alchemy/wallet-apis @privy-io/react-auth @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js viem + bun add @alchemy/wallet-apis @privy-io/react-auth viem ``` ```shell yarn - yarn add @alchemy/wallet-apis @privy-io/react-auth @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js viem + yarn add @alchemy/wallet-apis @privy-io/react-auth viem ``` ```shell pnpm - pnpm add @alchemy/wallet-apis @privy-io/react-auth @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js viem + pnpm add @alchemy/wallet-apis @privy-io/react-auth viem + ``` + + + If you also want to send Solana transactions (step 5), install the Solana peer dependencies: + + + ```shell npm + npm install @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js + ``` + + ```shell bun + bun add @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js + ``` + + ```shell yarn + yarn add @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js + ``` + + ```shell pnpm + pnpm add @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js ``` diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx index 19f3aafb9..531c0cda4 100644 --- a/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx @@ -1,5 +1,5 @@ ```ts title="sponsor-solana-request.ts" -import { client, config } from "./client.ts"; +import { client } from "./client.ts"; import { transferSolCall } from "./utils.ts"; const { id } = await client.sendCalls({ @@ -12,8 +12,7 @@ const { id } = await client.sendCalls({ ], capabilities: { paymaster: { - policyId: config.policyId, - webhookData: "checkout-session-123", + policyId: "OTHER_SOLANA_POLICY_ID", }, }, }); From 8cc5b0b11c7ff2ccc7ce90da43366396203ada35 Mon Sep 17 00:00:00 2001 From: jakehobbs Date: Wed, 27 May 2026 15:52:29 -0700 Subject: [PATCH 04/10] Address review feedback on Solana wallet docs. Restructure Privy doc so Solana lives in its own H2 section instead of step 5 of the EVM flow; drop @solana/web3.js from required Privy peer deps and call it out as example-only. Simplify the SDK code partials to build instructions with @solana/web3.js's SystemProgram.transfer instead of hand-rolled little-endian hex, and replace the throwing getYourSolanaSigner placeholder with a runnable env-based signer using @solana/kit. Add raw JSON-RPC API examples (wallet_prepareCalls -> Ed25519 sign -> wallet_sendPreparedCalls) for both send-transactions and sponsor-gas Solana sections, wired in as sibling API tabs. Revert the Solana sponsorship card description on the gas overview page. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../pages/third-party/signers/privy.mdx | 101 ++++++++------ .../transactions/send-transactions/index.mdx | 20 ++- .../send-transactions/solana/api.mdx | 122 ++++++++++++++++ .../send-transactions/solana/client.mdx | 68 ++++----- .../solana/prepare-calls/client.mdx | 65 ++++----- .../transactions/sponsor-gas/overview.mdx | 2 +- .../transactions/sponsor-gas/solana/api.mdx | 131 ++++++++++++++++++ .../sponsor-gas/solana/client.mdx | 67 ++++----- .../transactions/sponsor-gas/solana/index.mdx | 10 +- .../sponsor-gas/solana/prefund-rent.mdx | 2 +- .../sponsor-gas/solana/request-policy.mdx | 3 +- 11 files changed, 447 insertions(+), 144 deletions(-) create mode 100644 content/wallets/pages/transactions/send-transactions/solana/api.mdx create mode 100644 content/wallets/pages/transactions/sponsor-gas/solana/api.mdx diff --git a/content/wallets/pages/third-party/signers/privy.mdx b/content/wallets/pages/third-party/signers/privy.mdx index cc07c1a42..cc2785f8c 100644 --- a/content/wallets/pages/third-party/signers/privy.mdx +++ b/content/wallets/pages/third-party/signers/privy.mdx @@ -37,25 +37,7 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba ``` - If you also want to send Solana transactions (step 5), install the Solana peer dependencies: - - - ```shell npm - npm install @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js - ``` - - ```shell bun - bun add @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js - ``` - - ```shell yarn - yarn add @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js - ``` - - ```shell pnpm - pnpm add @solana/kit @solana-program/memo @solana-program/system @solana-program/token @solana/web3.js - ``` - + For Solana support, see [Send Solana transactions](#send-solana-transactions) below for the additional peer dependencies. ### Prerequisites: Get your keys (API key, Policy ID, Privy App ID) @@ -98,24 +80,6 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba } ``` - To create Solana embedded wallets on login, include the Solana embedded wallet configuration: - - ```tsx - - - - ``` - ### 2. Get a signer from Privy Use `toViemAccount` and `useWallets` from `@privy-io/react-auth` to convert a Privy embedded wallet into a viem `LocalAccount`: @@ -208,7 +172,63 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba } ``` - ### 5. Send a sponsored Solana transaction + ### Notes + + * The client defaults to [EIP-7702](/docs/wallets/transactions/using-eip-7702), which delegates the Privy wallet to a smart wallet at send time. No deployment or wallet migration needed. See the [EIP-7702 guide](/docs/wallets/transactions/using-eip-7702) for non-7702 mode. + * See the [Sponsor gas](/docs/wallets/transactions/sponsor-gas) guide for more on gas sponsorship configuration. + + ## Send Solana transactions + + Privy's Solana embedded wallet plugs into Wallet APIs without an adapter — `useWallets()` from `@privy-io/react-auth/solana` returns wallets that satisfy the `SolanaSigner` interface directly. + + ### Solana peer dependencies + + Privy's Solana wallet uses `@solana/kit` internally, so install it alongside the `@solana-program/*` packages Privy needs to build transactions: + + + ```shell npm + npm install @solana/kit @solana-program/memo @solana-program/system @solana-program/token + ``` + + ```shell bun + bun add @solana/kit @solana-program/memo @solana-program/system @solana-program/token + ``` + + ```shell yarn + yarn add @solana/kit @solana-program/memo @solana-program/system @solana-program/token + ``` + + ```shell pnpm + pnpm add @solana/kit @solana-program/memo @solana-program/system @solana-program/token + ``` + + + The example below builds its transfer instruction with `@solana/web3.js` because it's the most familiar API. You only need it if you use that pattern — equivalent instructions can be built with `@solana/kit` or any library that produces a `programId`, account metas, and instruction `data` bytes. + + ```shell + npm install @solana/web3.js + ``` + + ### Configure PrivyProvider for Solana + + Enable Solana embedded wallets in your `PrivyProvider` config: + + ```tsx + + + + ``` + + ### Send a sponsored Solana transaction Use `useWallets` from `@privy-io/react-auth/solana` to get the connected Solana wallet. The returned wallet can be passed directly as the Wallet APIs Solana signer. @@ -284,9 +304,8 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba ### Notes - * The EVM client defaults to [EIP-7702](/docs/wallets/transactions/using-eip-7702), which delegates the Privy wallet to a smart wallet at send time. No deployment or wallet migration needed. See the [EIP-7702 guide](/docs/wallets/transactions/using-eip-7702) for non-7702 mode. - * Solana sponsorship requires a Solana gas sponsorship policy. - * See the [Sponsor gas](/docs/wallets/transactions/sponsor-gas) guide for more on gas sponsorship configuration. + * Solana sponsorship requires a Solana gas sponsorship policy. See [Sponsor Solana fees](/docs/wallets/transactions/sponsor-gas/solana). + * For non-Privy Solana signer sources (Phantom, `@solana/wallet-adapter-react`, raw keypairs, Wallet Standard), see [Solana signer adapters](/docs/wallets/solana/signers). diff --git a/content/wallets/pages/transactions/send-transactions/index.mdx b/content/wallets/pages/transactions/send-transactions/index.mdx index de5989342..4560830a5 100644 --- a/content/wallets/pages/transactions/send-transactions/index.mdx +++ b/content/wallets/pages/transactions/send-transactions/index.mdx @@ -68,14 +68,30 @@ Solana transactions use the same client actions as EVM, but `calls` contain Sola ### Implementation - + + + + + + + + + ### Advanced Use the prepared call flow when you need to inspect the compiled Solana transaction before signing. - + + + + + + + The Solana raw API flow already uses `wallet_prepareCalls` and `wallet_sendPreparedCalls` directly — see the API tab under [Solana transactions](#solana-transactions). + + ## Next steps diff --git a/content/wallets/pages/transactions/send-transactions/solana/api.mdx b/content/wallets/pages/transactions/send-transactions/solana/api.mdx new file mode 100644 index 000000000..e8df25108 --- /dev/null +++ b/content/wallets/pages/transactions/send-transactions/solana/api.mdx @@ -0,0 +1,122 @@ +Solana signatures are Ed25519, so the raw JSON-RPC flow uses a small TypeScript helper for signing instead of `cast`. The transport URL and method names are identical to the EVM flow. + +See the [`wallet_prepareCalls`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-prepare-calls), [`wallet_sendPreparedCalls`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-send-prepared-calls), and [`wallet_getCallsStatus`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-get-calls-status) API references for full parameter descriptions. + + + + + +```ts twoslash +import { Keypair, PublicKey, SystemProgram, VersionedTransaction } from "@solana/web3.js"; +import bs58 from "bs58"; +import { bytesToHex } from "viem"; + +const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY!; +const RPC_URL = `https://api.g.alchemy.com/v2/${ALCHEMY_API_KEY}`; +const CHAIN_ID = "solana:devnet"; + +// 64-byte Solana secret key, base64-encoded. Keep this in a secret manager or .env. +const secretKey = new Uint8Array( + Buffer.from(process.env.SOLANA_SECRET_KEY!, "base64"), +); +const userKeypair = Keypair.fromSecretKey(secretKey); +const SIGNER_ADDRESS = userKeypair.publicKey.toBase58(); + +async function rpc(method: string, params: unknown[]): Promise { + const response = await fetch(RPC_URL, { + method: "POST", + headers: { accept: "application/json", "content-type": "application/json" }, + body: JSON.stringify({ id: 1, jsonrpc: "2.0", method, params }), + }); + const body = (await response.json()) as { result?: T; error?: unknown }; + if (body.error) throw new Error(JSON.stringify(body.error)); + return body.result as T; +} +``` + + + + + +Build the Solana call (a 0-lamport self-transfer in this example) and call `wallet_prepareCalls` with the user's address as `from`. + +```ts twoslash +const transferIx = SystemProgram.transfer({ + fromPubkey: userKeypair.publicKey, + toPubkey: userKeypair.publicKey, + lamports: 0, +}); + +const prepared = await rpc<{ + type: "solana-transaction-v0"; + signatureRequest: { type: "solana_signTransaction"; data: `0x${string}` }; + data: unknown; +}>("wallet_prepareCalls", [ + { + calls: [ + { + programId: transferIx.programId.toBase58(), + accounts: transferIx.keys.map((a) => ({ + pubkey: a.pubkey.toBase58(), + isSigner: a.isSigner, + isWritable: a.isWritable, + })), + data: bytesToHex(transferIx.data), + }, + ], + from: SIGNER_ADDRESS, + chainId: CHAIN_ID, + }, +]); +``` + + + + + +`signatureRequest.data` is the full v0 transaction wire-format bytes, hex-encoded. Deserialize it, sign with the user's keypair, then base58-encode the resulting signature. + +```ts twoslash +const txBytes = Buffer.from(prepared.signatureRequest.data.slice(2), "hex"); +const tx = VersionedTransaction.deserialize(txBytes); +tx.sign([userKeypair]); + +const signerIndex = tx.message.staticAccountKeys.findIndex( + (key) => key.toBase58() === SIGNER_ADDRESS, +); +const signatureBase58 = bs58.encode(tx.signatures[signerIndex]!); +``` + + + + + +```ts twoslash +const { id } = await rpc<{ id: string }>("wallet_sendPreparedCalls", [ + { + type: prepared.type, + chainId: CHAIN_ID, + data: prepared.data, + signature: { type: "ed25519", data: signatureBase58 }, + }, +]); +``` + + + + + +Poll `wallet_getCallsStatus` until the status leaves the pending range. Solana receipts return a base58 `signature` instead of `transactionHash`. + +```ts twoslash +const status = await rpc<{ + status: number; + receipts?: Array<{ signature: string }>; +}>("wallet_getCallsStatus", [id]); + +console.log(status.status, status.receipts?.[0]?.signature); +``` + + + + diff --git a/content/wallets/pages/transactions/send-transactions/solana/client.mdx b/content/wallets/pages/transactions/send-transactions/solana/client.mdx index 8eb14a561..ed908b7f0 100644 --- a/content/wallets/pages/transactions/send-transactions/solana/client.mdx +++ b/content/wallets/pages/transactions/send-transactions/solana/client.mdx @@ -10,7 +10,7 @@ You can send Solana instructions using the smart wallet client [`sendCalls`](/do transferSolCall({ from: client.solanaAccount, to: client.solanaAccount, - lamports: 0n, + lamports: 0, }), ], }); @@ -26,55 +26,59 @@ You can send Solana instructions using the smart wallet client [`sendCalls`](/do import { alchemyWalletTransport, createSmartWalletClient, - type SolanaSigner, } from "@alchemy/wallet-apis"; + import { fromKitSigner } from "@alchemy/wallet-apis/solana"; + import { createKeyPairSignerFromBytes } from "@solana/kit"; - function getYourSolanaSigner(): SolanaSigner { - // Load this from your app's signer integration. - throw new Error("Implement getYourSolanaSigner for your app"); - } + // 64-byte Solana secret key, base64-encoded. Keep this in a secret manager or .env. + const secretKeyBytes = new Uint8Array( + Buffer.from(process.env.SOLANA_SECRET_KEY!, "base64"), + ); + const kitSigner = await createKeyPairSignerFromBytes(secretKeyBytes); export const client = createSmartWalletClient({ - signer: getYourSolanaSigner(), + signer: fromKitSigner(kitSigner), transport: alchemyWalletTransport({ - apiKey: "YOUR_API_KEY", + apiKey: process.env.ALCHEMY_API_KEY!, }), chain: "solana:devnet", }); ``` ```ts title="utils.ts" - import type { Hex } from "viem"; - - const SYSTEM_PROGRAM_ID = "11111111111111111111111111111111"; - - function toLittleEndianHex(value: bigint, byteLength: number) { - let hex = ""; - - for (let i = 0; i < byteLength; i++) { - const byte = Number((value >> BigInt(i * 8)) & 0xffn); - hex += byte.toString(16).padStart(2, "0"); - } + import { + PublicKey, + SystemProgram, + type TransactionInstruction, + } from "@solana/web3.js"; + import { bytesToHex } from "viem"; - return hex; + function toSolanaCall(instruction: TransactionInstruction) { + return { + programId: instruction.programId.toBase58(), + accounts: instruction.keys.map((account) => ({ + pubkey: account.pubkey.toBase58(), + isSigner: account.isSigner, + isWritable: account.isWritable, + })), + data: bytesToHex(instruction.data), + }; } export function transferSolCall(args: { from: string; to: string; - lamports: bigint; + lamports: number; }) { - return { - programId: SYSTEM_PROGRAM_ID, - accounts: [ - { pubkey: args.from, isSigner: true, isWritable: true }, - { pubkey: args.to, isSigner: false, isWritable: true }, - ], - data: `0x${toLittleEndianHex(2n, 4)}${toLittleEndianHex( - args.lamports, - 8, - )}` as Hex, - }; + return toSolanaCall( + SystemProgram.transfer({ + fromPubkey: new PublicKey(args.from), + toPubkey: new PublicKey(args.to), + lamports: args.lamports, + }), + ); } ``` + +This example loads a raw Ed25519 keypair from an environment variable for simplicity. For wallet-connect-style signers (Privy, Phantom, `@solana/wallet-adapter-react`, Wallet Standard), see [Solana signer adapters](/docs/wallets/solana/signers). diff --git a/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx b/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx index 45d2819cd..52f34cfaf 100644 --- a/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx +++ b/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx @@ -10,7 +10,7 @@ You can use smart wallet client actions to prepare, sign, and send Solana transa transferSolCall({ from: client.solanaAccount, to: client.solanaAccount, - lamports: 0n, + lamports: 0, }), ], }); @@ -28,55 +28,56 @@ You can use smart wallet client actions to prepare, sign, and send Solana transa import { alchemyWalletTransport, createSmartWalletClient, - type SolanaSigner, } from "@alchemy/wallet-apis"; + import { fromKitSigner } from "@alchemy/wallet-apis/solana"; + import { createKeyPairSignerFromBytes } from "@solana/kit"; - function getYourSolanaSigner(): SolanaSigner { - // Load this from your app's signer integration. - throw new Error("Implement getYourSolanaSigner for your app"); - } + const secretKeyBytes = new Uint8Array( + Buffer.from(process.env.SOLANA_SECRET_KEY!, "base64"), + ); + const kitSigner = await createKeyPairSignerFromBytes(secretKeyBytes); export const client = createSmartWalletClient({ - signer: getYourSolanaSigner(), + signer: fromKitSigner(kitSigner), transport: alchemyWalletTransport({ - apiKey: "YOUR_API_KEY", + apiKey: process.env.ALCHEMY_API_KEY!, }), chain: "solana:devnet", }); ``` ```ts title="utils.ts" - import type { Hex } from "viem"; - - const SYSTEM_PROGRAM_ID = "11111111111111111111111111111111"; - - function toLittleEndianHex(value: bigint, byteLength: number) { - let hex = ""; - - for (let i = 0; i < byteLength; i++) { - const byte = Number((value >> BigInt(i * 8)) & 0xffn); - hex += byte.toString(16).padStart(2, "0"); - } + import { + PublicKey, + SystemProgram, + type TransactionInstruction, + } from "@solana/web3.js"; + import { bytesToHex } from "viem"; - return hex; + function toSolanaCall(instruction: TransactionInstruction) { + return { + programId: instruction.programId.toBase58(), + accounts: instruction.keys.map((account) => ({ + pubkey: account.pubkey.toBase58(), + isSigner: account.isSigner, + isWritable: account.isWritable, + })), + data: bytesToHex(instruction.data), + }; } export function transferSolCall(args: { from: string; to: string; - lamports: bigint; + lamports: number; }) { - return { - programId: SYSTEM_PROGRAM_ID, - accounts: [ - { pubkey: args.from, isSigner: true, isWritable: true }, - { pubkey: args.to, isSigner: false, isWritable: true }, - ], - data: `0x${toLittleEndianHex(2n, 4)}${toLittleEndianHex( - args.lamports, - 8, - )}` as Hex, - }; + return toSolanaCall( + SystemProgram.transfer({ + fromPubkey: new PublicKey(args.from), + toPubkey: new PublicKey(args.to), + lamports: args.lamports, + }), + ); } ``` diff --git a/content/wallets/pages/transactions/sponsor-gas/overview.mdx b/content/wallets/pages/transactions/sponsor-gas/overview.mdx index 7b2eace09..6e3704815 100644 --- a/content/wallets/pages/transactions/sponsor-gas/overview.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/overview.mdx @@ -46,7 +46,7 @@ Get started with the [gas sponsorship guide](/docs/wallets/transactions/sponsor- icon="layer-group" href="/docs/wallets/transactions/sponsor-gas/solana" > - Sponsor transaction fees and rent on Solana + Sponsor transaction fees on Solana diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/api.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/api.mdx new file mode 100644 index 000000000..7e212016b --- /dev/null +++ b/content/wallets/pages/transactions/sponsor-gas/solana/api.mdx @@ -0,0 +1,131 @@ +Sponsorship is applied by passing `capabilities.paymasterService` to `wallet_prepareCalls`. The rest of the flow (sign with Ed25519, send, poll status) is identical to the unsponsored Solana raw API flow. + +See the [`wallet_prepareCalls`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-prepare-calls), [`wallet_sendPreparedCalls`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-send-prepared-calls), and [`wallet_getCallsStatus`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-get-calls-status) API references for full parameter descriptions. + + + + + +```ts twoslash +import { Keypair, PublicKey, SystemProgram, VersionedTransaction } from "@solana/web3.js"; +import bs58 from "bs58"; +import { bytesToHex } from "viem"; + +const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY!; +const SOLANA_POLICY_ID = process.env.SOLANA_POLICY_ID!; +const RPC_URL = `https://api.g.alchemy.com/v2/${ALCHEMY_API_KEY}`; +const CHAIN_ID = "solana:devnet"; + +const secretKey = new Uint8Array( + Buffer.from(process.env.SOLANA_SECRET_KEY!, "base64"), +); +const userKeypair = Keypair.fromSecretKey(secretKey); +const SIGNER_ADDRESS = userKeypair.publicKey.toBase58(); + +async function rpc(method: string, params: unknown[]): Promise { + const response = await fetch(RPC_URL, { + method: "POST", + headers: { accept: "application/json", "content-type": "application/json" }, + body: JSON.stringify({ id: 1, jsonrpc: "2.0", method, params }), + }); + const body = (await response.json()) as { result?: T; error?: unknown }; + if (body.error) throw new Error(JSON.stringify(body.error)); + return body.result as T; +} +``` + + + + + +Attach `capabilities.paymasterService.policyId` to apply your Solana sponsorship policy. Add `prefundRent: true` to opt in to CPI rent prefunding, and `webhookData` to pass extra context to custom rules. + +```ts twoslash +const transferIx = SystemProgram.transfer({ + fromPubkey: userKeypair.publicKey, + toPubkey: userKeypair.publicKey, + lamports: 0, +}); + +const prepared = await rpc<{ + type: "solana-transaction-v0"; + signatureRequest: { type: "solana_signTransaction"; data: `0x${string}` }; + data: unknown; + feePayment: { sponsored: boolean; feePayer: string }; +}>("wallet_prepareCalls", [ + { + calls: [ + { + programId: transferIx.programId.toBase58(), + accounts: transferIx.keys.map((a) => ({ + pubkey: a.pubkey.toBase58(), + isSigner: a.isSigner, + isWritable: a.isWritable, + })), + data: bytesToHex(transferIx.data), + }, + ], + from: SIGNER_ADDRESS, + chainId: CHAIN_ID, + capabilities: { + paymasterService: { + policyId: SOLANA_POLICY_ID, + // prefundRent: true, // opt in for CPI rent + // webhookData: "session-123", // optional, for custom-rule webhooks + }, + }, + }, +]); + +console.log(prepared.feePayment); // { sponsored: true, feePayer: "" } +``` + + + + + +`signatureRequest.data` already contains the paymaster signature embedded; the user only needs to add their Ed25519 signature. + +```ts twoslash +const txBytes = Buffer.from(prepared.signatureRequest.data.slice(2), "hex"); +const tx = VersionedTransaction.deserialize(txBytes); +tx.sign([userKeypair]); + +const signerIndex = tx.message.staticAccountKeys.findIndex( + (key) => key.toBase58() === SIGNER_ADDRESS, +); +const signatureBase58 = bs58.encode(tx.signatures[signerIndex]!); +``` + + + + + +```ts twoslash +const { id } = await rpc<{ id: string }>("wallet_sendPreparedCalls", [ + { + type: prepared.type, + chainId: CHAIN_ID, + data: prepared.data, + signature: { type: "ed25519", data: signatureBase58 }, + }, +]); + +const status = await rpc<{ + status: number; + receipts?: Array<{ signature: string }>; +}>("wallet_getCallsStatus", [id]); + +console.log(status.status, status.receipts?.[0]?.signature); +``` + + + + + + + CPI rent prefunding (`prefundRent: true`) is allowlisted. Contact + [support@alchemy.com](mailto:support@alchemy.com) to request access. Policies + used with `prefundRent` must set `maxSpendPerTxnUsd`, and the transaction + payer must be the user's wallet. + diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx index faff1fbf2..e72c0a63a 100644 --- a/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx @@ -10,7 +10,7 @@ Use `paymaster` on the smart wallet client to apply a Solana sponsorship policy transferSolCall({ from: client.solanaAccount, to: client.solanaAccount, - lamports: 0n, + lamports: 0, }), ], }); @@ -24,22 +24,23 @@ Use `paymaster` on the smart wallet client to apply a Solana sponsorship policy import { alchemyWalletTransport, createSmartWalletClient, - type SolanaSigner, } from "@alchemy/wallet-apis"; + import { fromKitSigner } from "@alchemy/wallet-apis/solana"; + import { createKeyPairSignerFromBytes } from "@solana/kit"; export const config = { - policyId: "YOUR_SOLANA_POLICY_ID", + policyId: process.env.SOLANA_POLICY_ID!, }; - function getYourSolanaSigner(): SolanaSigner { - // Load this from your app's signer integration. - throw new Error("Implement getYourSolanaSigner for your app"); - } + const secretKeyBytes = new Uint8Array( + Buffer.from(process.env.SOLANA_SECRET_KEY!, "base64"), + ); + const kitSigner = await createKeyPairSignerFromBytes(secretKeyBytes); export const client = createSmartWalletClient({ - signer: getYourSolanaSigner(), + signer: fromKitSigner(kitSigner), transport: alchemyWalletTransport({ - apiKey: "YOUR_API_KEY", + apiKey: process.env.ALCHEMY_API_KEY!, }), chain: "solana:devnet", paymaster: { policyId: config.policyId }, @@ -47,37 +48,37 @@ Use `paymaster` on the smart wallet client to apply a Solana sponsorship policy ``` ```ts title="utils.ts" - import type { Hex } from "viem"; - - const SYSTEM_PROGRAM_ID = "11111111111111111111111111111111"; - - function toLittleEndianHex(value: bigint, byteLength: number) { - let hex = ""; - - for (let i = 0; i < byteLength; i++) { - const byte = Number((value >> BigInt(i * 8)) & 0xffn); - hex += byte.toString(16).padStart(2, "0"); - } + import { + PublicKey, + SystemProgram, + type TransactionInstruction, + } from "@solana/web3.js"; + import { bytesToHex } from "viem"; - return hex; + function toSolanaCall(instruction: TransactionInstruction) { + return { + programId: instruction.programId.toBase58(), + accounts: instruction.keys.map((account) => ({ + pubkey: account.pubkey.toBase58(), + isSigner: account.isSigner, + isWritable: account.isWritable, + })), + data: bytesToHex(instruction.data), + }; } export function transferSolCall(args: { from: string; to: string; - lamports: bigint; + lamports: number; }) { - return { - programId: SYSTEM_PROGRAM_ID, - accounts: [ - { pubkey: args.from, isSigner: true, isWritable: true }, - { pubkey: args.to, isSigner: false, isWritable: true }, - ], - data: `0x${toLittleEndianHex(2n, 4)}${toLittleEndianHex( - args.lamports, - 8, - )}` as Hex, - }; + return toSolanaCall( + SystemProgram.transfer({ + fromPubkey: new PublicKey(args.from), + toPubkey: new PublicKey(args.to), + lamports: args.lamports, + }), + ); } ``` diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx index 2e5b50653..1ac14d0b4 100644 --- a/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx @@ -14,7 +14,15 @@ Use Solana sponsorship to cover transaction fees and rent for Solana calls submi ## Implementation - + + + + + + + + + ## Advanced diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/prefund-rent.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/prefund-rent.mdx index 043cdfda0..b1da80e78 100644 --- a/content/wallets/pages/transactions/sponsor-gas/solana/prefund-rent.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/solana/prefund-rent.mdx @@ -7,7 +7,7 @@ const { id } = await client.sendCalls({ transferSolCall({ from: client.solanaAccount, to: client.solanaAccount, - lamports: 0n, + lamports: 0, }), ], capabilities: { diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx index 531c0cda4..b65b9d68c 100644 --- a/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx @@ -7,12 +7,13 @@ const { id } = await client.sendCalls({ transferSolCall({ from: client.solanaAccount, to: client.solanaAccount, - lamports: 0n, + lamports: 0, }), ], capabilities: { paymaster: { policyId: "OTHER_SOLANA_POLICY_ID", + webhookData: "checkout-session-123", }, }, }); From 952e33440fa0adc97f74f74d82fe1b3eb8f13e7d Mon Sep 17 00:00:00 2001 From: jakehobbs Date: Wed, 27 May 2026 16:52:57 -0700 Subject: [PATCH 05/10] Address review feedback on Solana wallet docs nav. Rename "Sponsor Solana fees" to "Solana sponsorship" in the nav, page title, and link text across the docs. Flatten the empty "Solana > Signers" wrapper section into a single "Solana signers" entry, peer with Privy under Wallet integration. Nest the low-level "Solana" gas sponsorship page under "Using SDK" alongside the other SDK guides. URL slugs are unchanged, so no redirect updates are needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- content/docs.yml | 12 +++++------- content/wallets/pages/third-party/signers/privy.mdx | 2 +- content/wallets/pages/transactions/overview.mdx | 2 +- .../pages/transactions/send-transactions/index.mdx | 2 +- .../wallets/pages/transactions/sponsor-gas/index.mdx | 4 ++-- .../pages/transactions/sponsor-gas/overview.mdx | 2 +- .../pages/transactions/sponsor-gas/solana/index.mdx | 2 +- 7 files changed, 12 insertions(+), 14 deletions(-) diff --git a/content/docs.yml b/content/docs.yml index d61c64693..302022373 100644 --- a/content/docs.yml +++ b/content/docs.yml @@ -893,7 +893,7 @@ navigation: contents: - page: Full sponsorship path: wallets/pages/transactions/sponsor-gas/index.mdx - - page: Sponsor Solana fees + - page: Solana sponsorship path: wallets/pages/transactions/sponsor-gas/solana/index.mdx - page: Conditional sponsorship rules path: wallets/pages/transactions/sponsor-gas/conditional-sponsorship-rules.mdx @@ -943,10 +943,8 @@ navigation: path: wallets/wallet-integrations/privy/react-migration.mdx - page: JWT auth migration path: wallets/wallet-integrations/privy/jwt-auth-migration.mdx - - section: Solana - contents: - - page: Signers - path: wallets/pages/third-party/signers/solana-signers.mdx + - page: Solana signers + path: wallets/pages/third-party/signers/solana-signers.mdx - section: Other signers contents: - page: Openfort @@ -1046,8 +1044,6 @@ navigation: contents: - page: API Endpoints path: wallets/pages/low-level-infra/gas-manager/gas-sponsorship/api-endpoints.mdx - - page: Solana - path: wallets/pages/transactions/solana/sponsor-gas-solana.mdx - section: Using SDK contents: - page: Basic Gas Sponsorship @@ -1056,6 +1052,8 @@ navigation: path: wallets/pages/low-level-infra/gas-manager/gas-sponsorship/using-sdk/conditional-gas-sponsorship.mdx - page: Pay Gas with Any ERC20 Token path: wallets/pages/low-level-infra/gas-manager/gas-sponsorship/using-sdk/pay-gas-with-any-erc20-token.mdx + - page: Solana + path: wallets/pages/transactions/solana/sponsor-gas-solana.mdx - section: Bundler API path: wallets/pages/low-level-infra/bundler/overview.mdx contents: diff --git a/content/wallets/pages/third-party/signers/privy.mdx b/content/wallets/pages/third-party/signers/privy.mdx index cc2785f8c..f5f6fe357 100644 --- a/content/wallets/pages/third-party/signers/privy.mdx +++ b/content/wallets/pages/third-party/signers/privy.mdx @@ -304,7 +304,7 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba ### Notes - * Solana sponsorship requires a Solana gas sponsorship policy. See [Sponsor Solana fees](/docs/wallets/transactions/sponsor-gas/solana). + * Solana sponsorship requires a Solana gas sponsorship policy. See [Solana sponsorship](/docs/wallets/transactions/sponsor-gas/solana). * For non-Privy Solana signer sources (Phantom, `@solana/wallet-adapter-react`, raw keypairs, Wallet Standard), see [Solana signer adapters](/docs/wallets/solana/signers). diff --git a/content/wallets/pages/transactions/overview.mdx b/content/wallets/pages/transactions/overview.mdx index a8947136b..4381e68b0 100644 --- a/content/wallets/pages/transactions/overview.mdx +++ b/content/wallets/pages/transactions/overview.mdx @@ -29,7 +29,7 @@ When a user sends a transaction, every step is handled automatically. In this se | [EIP-7702](/docs/wallets/transactions/using-eip-7702) | Upgrade embedded EOA users to smart wallets | | [Batch transactions](/docs/wallets/transactions/send-batch-transactions) | Execute multiple transactions atomically in a single step (ex: approve & swap) | | [Sponsor gas](/docs/wallets/transactions/sponsor-gas) | Sponsor EVM gas | -| [Sponsor Solana fees](/docs/wallets/transactions/sponsor-gas/solana) | Sponsor Solana transaction fees and rent | +| [Solana sponsorship](/docs/wallets/transactions/sponsor-gas/solana) | Sponsor Solana transaction fees and rent | | [Pay gas with any token](/docs/wallets/transactions/pay-gas-with-any-token) | Pay gas with stablecoins or the sell-side token | | [Swap tokens](/docs/wallets/transactions/swap-tokens) | Swap across networks, seamlessly | | [Retry transactions](/docs/wallets/transactions/retry-transactions) | Retry transactions stuck in mempool | diff --git a/content/wallets/pages/transactions/send-transactions/index.mdx b/content/wallets/pages/transactions/send-transactions/index.mdx index 4560830a5..fb6bd585f 100644 --- a/content/wallets/pages/transactions/send-transactions/index.mdx +++ b/content/wallets/pages/transactions/send-transactions/index.mdx @@ -99,7 +99,7 @@ Solana transactions use the same client actions as EVM, but `calls` contain Sola Build more: * [Sponsor gas for users](/docs/wallets/transactions/sponsor-gas) -* [Sponsor Solana fees](/docs/wallets/transactions/sponsor-gas/solana) +* [Solana sponsorship](/docs/wallets/transactions/sponsor-gas/solana) * [Send batch transactions](/docs/wallets/transactions/send-batch-transactions) * [Use Solana signer adapters](/docs/wallets/solana/signers) diff --git a/content/wallets/pages/transactions/sponsor-gas/index.mdx b/content/wallets/pages/transactions/sponsor-gas/index.mdx index 9d74686b2..1149c97cb 100644 --- a/content/wallets/pages/transactions/sponsor-gas/index.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/index.mdx @@ -60,7 +60,7 @@ and bills in fiat. Sponsoring Solana fees and rent uses Solana-specific policy configuration and - request options. See [Sponsor Solana fees](/docs/wallets/transactions/sponsor-gas/solana). + request options. See [Solana sponsorship](/docs/wallets/transactions/sponsor-gas/solana). ## Next steps @@ -68,7 +68,7 @@ and bills in fiat. Build more: * [Pay gas with any token](/docs/wallets/transactions/pay-gas-with-any-token) -* [Sponsor Solana fees](/docs/wallets/transactions/sponsor-gas/solana) +* [Solana sponsorship](/docs/wallets/transactions/sponsor-gas/solana) Troubleshooting: diff --git a/content/wallets/pages/transactions/sponsor-gas/overview.mdx b/content/wallets/pages/transactions/sponsor-gas/overview.mdx index 6e3704815..90cbfbfd4 100644 --- a/content/wallets/pages/transactions/sponsor-gas/overview.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/overview.mdx @@ -42,7 +42,7 @@ Get started with the [gas sponsorship guide](/docs/wallets/transactions/sponsor- Let users pay gas with USDC, USDT, or any token diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx index 1ac14d0b4..cdfa78ffa 100644 --- a/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx @@ -1,5 +1,5 @@ --- -title: Sponsor Solana fees +title: Solana sponsorship description: Sponsor Solana transaction fees and rent with Wallet APIs slug: wallets/transactions/sponsor-gas/solana --- From 47286b6749896423288b68cd2475c587760a8afc Mon Sep 17 00:00:00 2001 From: jakehobbs Date: Wed, 27 May 2026 16:54:08 -0700 Subject: [PATCH 06/10] Parallelize EVM and Solana sections on Privy page. Promote "Send EVM transactions" to a sibling H2 of "Send Solana transactions" instead of folding the EVM flow under the shared Setup heading. Drop the now-redundant step numbers on the EVM subsections so both flows use named H3s. Co-Authored-By: Claude Opus 4.7 (1M context) --- content/wallets/pages/third-party/signers/privy.mdx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/content/wallets/pages/third-party/signers/privy.mdx b/content/wallets/pages/third-party/signers/privy.mdx index f5f6fe357..0688e0ce6 100644 --- a/content/wallets/pages/third-party/signers/privy.mdx +++ b/content/wallets/pages/third-party/signers/privy.mdx @@ -15,7 +15,7 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba ## Setup - Follow these steps to use Privy signers with the Wallet Client SDK. + Follow these steps to use Privy signers with the Wallet Client SDK. EVM and Solana share the same setup; each chain family has its own send-transaction flow below. ### Installation @@ -54,7 +54,9 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba The gas sponsorship policy must be linked to the application behind your Alchemy API key for sponsorship to work. - ### 1. Configure PrivyProvider + ## Send EVM transactions + + ### Configure PrivyProvider Wrap your app with `PrivyProvider` from `@privy-io/react-auth`: @@ -80,7 +82,7 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba } ``` - ### 2. Get a signer from Privy + ### Get a signer from Privy Use `toViemAccount` and `useWallets` from `@privy-io/react-auth` to convert a Privy embedded wallet into a viem `LocalAccount`: @@ -105,7 +107,7 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba }; ``` - ### 3. Handle login + ### Handle login Use `usePrivy` to manage authentication state and conditionally render your wallet UI once the user is logged in: @@ -131,7 +133,7 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba } ``` - ### 4. Create the client and send a transaction + ### Create the client and send a transaction Pass the Privy signer to `createSmartWalletClient` and use it to send transactions: From e0ff7bf0d87b9b6196e896b964c8ad65b1dcd7b8 Mon Sep 17 00:00:00 2001 From: jakehobbs Date: Wed, 27 May 2026 16:59:54 -0700 Subject: [PATCH 07/10] Switch Solana examples to @solana/kit and reorganize signer nav. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace @solana/web3.js with @solana/kit + @solana-program/system across the Privy Solana example, SDK partials (send-transactions, sponsor-gas clients), and raw API examples. Drop bs58 + VersionedTransaction in the API flow in favor of kit's partiallySignTransaction + getBase58Decoder. Split the Privy Solana example into tabbed files (component + utils.ts) so the call-building helper isn't inline. In nav, move "Solana signers" under "Other signers" (now labeled "Solana") and drop the standalone Privy section from that page — link back to the Privy guide instead, since the Privy guide owns the Privy Solana flow end to end. Co-Authored-By: Claude Opus 4.7 (1M context) --- content/docs.yml | 4 +- .../pages/third-party/signers/privy.mdx | 176 ++++++++++-------- .../third-party/signers/solana-signers.mdx | 33 +--- .../send-transactions/solana/api.mdx | 72 ++++--- .../send-transactions/solana/client.mdx | 40 ++-- .../solana/prepare-calls/client.mdx | 40 ++-- .../transactions/sponsor-gas/solana/api.mdx | 70 ++++--- .../sponsor-gas/solana/client.mdx | 40 ++-- .../sponsor-gas/solana/prefund-rent.mdx | 2 +- .../sponsor-gas/solana/request-policy.mdx | 2 +- 10 files changed, 257 insertions(+), 222 deletions(-) diff --git a/content/docs.yml b/content/docs.yml index 302022373..ef54d3175 100644 --- a/content/docs.yml +++ b/content/docs.yml @@ -943,14 +943,14 @@ navigation: path: wallets/wallet-integrations/privy/react-migration.mdx - page: JWT auth migration path: wallets/wallet-integrations/privy/jwt-auth-migration.mdx - - page: Solana signers - path: wallets/pages/third-party/signers/solana-signers.mdx - section: Other signers contents: - page: Openfort path: wallets/pages/third-party/signers/openfort.mdx - page: Turnkey path: wallets/pages/third-party/signers/turnkey.mdx + - page: Solana + path: wallets/pages/third-party/signers/solana-signers.mdx - page: Other signers path: wallets/pages/third-party/signers/custom-integration.mdx - section: Account Kit (v4) diff --git a/content/wallets/pages/third-party/signers/privy.mdx b/content/wallets/pages/third-party/signers/privy.mdx index 0688e0ce6..c85c0c4fe 100644 --- a/content/wallets/pages/third-party/signers/privy.mdx +++ b/content/wallets/pages/third-party/signers/privy.mdx @@ -183,34 +183,28 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba Privy's Solana embedded wallet plugs into Wallet APIs without an adapter — `useWallets()` from `@privy-io/react-auth/solana` returns wallets that satisfy the `SolanaSigner` interface directly. - ### Solana peer dependencies + ### Install Solana deps - Privy's Solana wallet uses `@solana/kit` internally, so install it alongside the `@solana-program/*` packages Privy needs to build transactions: + The example below uses `@solana/kit` and `@solana-program/system` to build the transfer instruction: ```shell npm - npm install @solana/kit @solana-program/memo @solana-program/system @solana-program/token + npm install @solana/kit @solana-program/system ``` ```shell bun - bun add @solana/kit @solana-program/memo @solana-program/system @solana-program/token + bun add @solana/kit @solana-program/system ``` ```shell yarn - yarn add @solana/kit @solana-program/memo @solana-program/system @solana-program/token + yarn add @solana/kit @solana-program/system ``` ```shell pnpm - pnpm add @solana/kit @solana-program/memo @solana-program/system @solana-program/token + pnpm add @solana/kit @solana-program/system ``` - The example below builds its transfer instruction with `@solana/web3.js` because it's the most familiar API. You only need it if you use that pattern — equivalent instructions can be built with `@solana/kit` or any library that produces a `programId`, account metas, and instruction `data` bytes. - - ```shell - npm install @solana/web3.js - ``` - ### Configure PrivyProvider for Solana Enable Solana embedded wallets in your `PrivyProvider` config: @@ -234,75 +228,97 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba Use `useWallets` from `@privy-io/react-auth/solana` to get the connected Solana wallet. The returned wallet can be passed directly as the Wallet APIs Solana signer. - ```tsx title="privy-sponsored-solana.tsx" - import { - alchemyWalletTransport, - createSmartWalletClient, - } from "@alchemy/wallet-apis"; - import { useWallets } from "@privy-io/react-auth/solana"; - import { - PublicKey, - SystemProgram, - type TransactionInstruction, - } from "@solana/web3.js"; - import { useCallback, useMemo } from "react"; - import { bytesToHex } from "viem"; - - function toSolanaCall(instruction: TransactionInstruction) { - return { - programId: instruction.programId.toBase58(), - accounts: instruction.keys.map((account) => ({ - pubkey: account.pubkey.toBase58(), - isSigner: account.isSigner, - isWritable: account.isWritable, - })), - data: bytesToHex(instruction.data), - }; - } - - function transferCall(from: string, to: string, lamports: number) { - return toSolanaCall( - SystemProgram.transfer({ - fromPubkey: new PublicKey(from), - toPubkey: new PublicKey(to), - lamports, - }), - ); - } - - function SendSponsoredSolanaTransaction() { - const { wallets } = useWallets(); - const signer = wallets[0]; - - const client = useMemo(() => { - if (!signer) return undefined; - - return createSmartWalletClient({ - transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), - chain: "solana:devnet", - signer, - paymaster: { policyId: "YOUR_SOLANA_POLICY_ID" }, - }); - }, [signer]); - - const handleSend = useCallback(async () => { - if (!client) return; - - const { id } = await client.sendCalls({ - calls: [transferCall(client.solanaAccount, client.solanaAccount, 0)], - }); - - const result = await client.waitForCallsStatus({ id }); - console.log("Sponsored Solana transaction status:", result.status); - }, [client]); + + ```tsx title="SendSponsoredSolanaTransaction.tsx" + import { + alchemyWalletTransport, + createSmartWalletClient, + } from "@alchemy/wallet-apis"; + import { useWallets } from "@privy-io/react-auth/solana"; + import { useCallback, useMemo } from "react"; + import { transferSolCall } from "./utils"; + + export function SendSponsoredSolanaTransaction() { + const { wallets } = useWallets(); + const signer = wallets[0]; + + const client = useMemo(() => { + if (!signer) return undefined; + + return createSmartWalletClient({ + transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), + chain: "solana:devnet", + signer, + paymaster: { policyId: "YOUR_SOLANA_POLICY_ID" }, + }); + }, [signer]); + + const handleSend = useCallback(async () => { + if (!client) return; + + const { id } = await client.sendCalls({ + calls: [ + transferSolCall({ + from: client.solanaAccount, + to: client.solanaAccount, + lamports: 0n, + }), + ], + }); + + const result = await client.waitForCallsStatus({ id }); + console.log("Sponsored Solana transaction status:", result.status); + }, [client]); + + return ( + + ); + } + ``` + + ```ts title="utils.ts" + import { getTransferSolInstruction } from "@solana-program/system"; + import { + AccountRole, + address, + createNoopSigner, + type IInstruction, + } from "@solana/kit"; + import { bytesToHex } from "viem"; + + function toSolanaCall(instruction: IInstruction) { + return { + programId: instruction.programAddress, + accounts: (instruction.accounts ?? []).map((account) => ({ + pubkey: account.address, + isSigner: + account.role === AccountRole.READONLY_SIGNER || + account.role === AccountRole.WRITABLE_SIGNER, + isWritable: + account.role === AccountRole.WRITABLE || + account.role === AccountRole.WRITABLE_SIGNER, + })), + data: bytesToHex(instruction.data ?? new Uint8Array()), + }; + } - return ( - - ); - } - ``` + export function transferSolCall(args: { + from: string; + to: string; + lamports: bigint; + }) { + return toSolanaCall( + getTransferSolInstruction({ + source: createNoopSigner(address(args.from)), + destination: address(args.to), + amount: args.lamports, + }), + ); + } + ``` + ### Notes diff --git a/content/wallets/pages/third-party/signers/solana-signers.mdx b/content/wallets/pages/third-party/signers/solana-signers.mdx index d5bd43ee9..2bf1d4a3d 100644 --- a/content/wallets/pages/third-party/signers/solana-signers.mdx +++ b/content/wallets/pages/third-party/signers/solana-signers.mdx @@ -39,8 +39,6 @@ bun add @alchemy/wallet-apis Install the optional package that matches your signer source: -* Privy: `@privy-io/react-auth` -* Privy Solana peer dependencies: `@solana/kit @solana-program/memo @solana-program/system @solana-program/token` * `@solana/wallet-adapter-react`: `@solana/wallet-adapter-react @solana/web3.js` * Injected providers such as Phantom: `@solana/web3.js` * `@solana/kit` signers: `@solana/kit` @@ -50,42 +48,13 @@ Install the optional package that matches your signer source: | What you have | What to use | | --- | --- | -| Privy Solana wallet from `@privy-io/react-auth/solana` | No adapter needed | +| Privy Solana wallet from `@privy-io/react-auth/solana` | No adapter — see the [Privy guide](/docs/wallets/third-party/signers/privy#send-solana-transactions) | | `useWallet()` from `@solana/wallet-adapter-react` | `fromWalletAdapter` | | Injected provider such as `window.phantom.solana` | `fromWalletAdapter` | | `@solana/kit` `KeyPairSigner` or `TransactionPartialSigner` | `fromKitSigner` | | Raw Ed25519 keypair or key management service | `fromKeypair` | | Low-level Wallet Standard wallet | `fromWalletStandard` | -## Privy - -Use `useWallets` from `@privy-io/react-auth/solana` to get connected Solana wallets. The returned wallet can be passed directly to `createSmartWalletClient`. - -```tsx title="privy-solana-signer.tsx" -import { - alchemyWalletTransport, - createSmartWalletClient, -} from "@alchemy/wallet-apis"; -import { useWallets } from "@privy-io/react-auth/solana"; -import { useMemo } from "react"; - -function usePrivySolanaClient() { - const { wallets } = useWallets(); - const signer = wallets[0]; - - return useMemo(() => { - if (!signer) return undefined; - - return createSmartWalletClient({ - signer, - transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), - chain: "solana:devnet", - paymaster: { policyId: "YOUR_SOLANA_POLICY_ID" }, - }); - }, [signer]); -} -``` - ## `@solana/wallet-adapter-react` Use `fromWalletAdapter` for wallets returned by `useWallet()`. This adapter handles the `Uint8Array` to `VersionedTransaction` conversion required by the wallet adapter API. diff --git a/content/wallets/pages/transactions/send-transactions/solana/api.mdx b/content/wallets/pages/transactions/send-transactions/solana/api.mdx index e8df25108..8e6b58432 100644 --- a/content/wallets/pages/transactions/send-transactions/solana/api.mdx +++ b/content/wallets/pages/transactions/send-transactions/solana/api.mdx @@ -1,4 +1,4 @@ -Solana signatures are Ed25519, so the raw JSON-RPC flow uses a small TypeScript helper for signing instead of `cast`. The transport URL and method names are identical to the EVM flow. +Solana signatures are Ed25519, so the raw JSON-RPC flow uses `@solana/kit` to sign instead of `cast`. The transport URL and method names are identical to the EVM flow. See the [`wallet_prepareCalls`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-prepare-calls), [`wallet_sendPreparedCalls`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-send-prepared-calls), and [`wallet_getCallsStatus`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-get-calls-status) API references for full parameter descriptions. @@ -7,8 +7,17 @@ See the [`wallet_prepareCalls`](/docs/wallets/api-reference/smart-wallets/wallet ```ts twoslash -import { Keypair, PublicKey, SystemProgram, VersionedTransaction } from "@solana/web3.js"; -import bs58 from "bs58"; +import { getTransferSolInstruction } from "@solana-program/system"; +import { + AccountRole, + address, + createKeyPairSignerFromBytes, + createNoopSigner, + getBase58Decoder, + getTransactionDecoder, + partiallySignTransaction, + type IInstruction, +} from "@solana/kit"; import { bytesToHex } from "viem"; const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY!; @@ -19,8 +28,8 @@ const CHAIN_ID = "solana:devnet"; const secretKey = new Uint8Array( Buffer.from(process.env.SOLANA_SECRET_KEY!, "base64"), ); -const userKeypair = Keypair.fromSecretKey(secretKey); -const SIGNER_ADDRESS = userKeypair.publicKey.toBase58(); +const userSigner = await createKeyPairSignerFromBytes(secretKey); +const SIGNER_ADDRESS = userSigner.address; async function rpc(method: string, params: unknown[]): Promise { const response = await fetch(RPC_URL, { @@ -32,6 +41,22 @@ async function rpc(method: string, params: unknown[]): Promise { if (body.error) throw new Error(JSON.stringify(body.error)); return body.result as T; } + +function toSolanaCall(instruction: IInstruction) { + return { + programId: instruction.programAddress, + accounts: (instruction.accounts ?? []).map((a) => ({ + pubkey: a.address, + isSigner: + a.role === AccountRole.READONLY_SIGNER || + a.role === AccountRole.WRITABLE_SIGNER, + isWritable: + a.role === AccountRole.WRITABLE || + a.role === AccountRole.WRITABLE_SIGNER, + })), + data: bytesToHex(instruction.data ?? new Uint8Array()), + }; +} ``` @@ -41,10 +66,10 @@ async function rpc(method: string, params: unknown[]): Promise { Build the Solana call (a 0-lamport self-transfer in this example) and call `wallet_prepareCalls` with the user's address as `from`. ```ts twoslash -const transferIx = SystemProgram.transfer({ - fromPubkey: userKeypair.publicKey, - toPubkey: userKeypair.publicKey, - lamports: 0, +const transferIx = getTransferSolInstruction({ + source: createNoopSigner(address(SIGNER_ADDRESS)), + destination: address(SIGNER_ADDRESS), + amount: 0n, }); const prepared = await rpc<{ @@ -53,17 +78,7 @@ const prepared = await rpc<{ data: unknown; }>("wallet_prepareCalls", [ { - calls: [ - { - programId: transferIx.programId.toBase58(), - accounts: transferIx.keys.map((a) => ({ - pubkey: a.pubkey.toBase58(), - isSigner: a.isSigner, - isWritable: a.isWritable, - })), - data: bytesToHex(transferIx.data), - }, - ], + calls: [toSolanaCall(transferIx)], from: SIGNER_ADDRESS, chainId: CHAIN_ID, }, @@ -74,17 +89,18 @@ const prepared = await rpc<{ -`signatureRequest.data` is the full v0 transaction wire-format bytes, hex-encoded. Deserialize it, sign with the user's keypair, then base58-encode the resulting signature. +`signatureRequest.data` is the full v0 transaction wire-format bytes, hex-encoded. Decode it with kit, partially sign with the user's keypair, and base58-encode the resulting signature. ```ts twoslash -const txBytes = Buffer.from(prepared.signatureRequest.data.slice(2), "hex"); -const tx = VersionedTransaction.deserialize(txBytes); -tx.sign([userKeypair]); - -const signerIndex = tx.message.staticAccountKeys.findIndex( - (key) => key.toBase58() === SIGNER_ADDRESS, +const txBytes = new Uint8Array( + Buffer.from(prepared.signatureRequest.data.slice(2), "hex"), ); -const signatureBase58 = bs58.encode(tx.signatures[signerIndex]!); +const tx = getTransactionDecoder().decode(txBytes); +const signed = await partiallySignTransaction([userSigner.keyPair], tx); + +const userSignatureBytes = signed.signatures[SIGNER_ADDRESS]; +if (!userSignatureBytes) throw new Error("Missing user signature"); +const signatureBase58 = getBase58Decoder().decode(userSignatureBytes); ``` diff --git a/content/wallets/pages/transactions/send-transactions/solana/client.mdx b/content/wallets/pages/transactions/send-transactions/solana/client.mdx index ed908b7f0..628994163 100644 --- a/content/wallets/pages/transactions/send-transactions/solana/client.mdx +++ b/content/wallets/pages/transactions/send-transactions/solana/client.mdx @@ -10,7 +10,7 @@ You can send Solana instructions using the smart wallet client [`sendCalls`](/do transferSolCall({ from: client.solanaAccount, to: client.solanaAccount, - lamports: 0, + lamports: 0n, }), ], }); @@ -46,35 +46,41 @@ You can send Solana instructions using the smart wallet client [`sendCalls`](/do ``` ```ts title="utils.ts" + import { getTransferSolInstruction } from "@solana-program/system"; import { - PublicKey, - SystemProgram, - type TransactionInstruction, - } from "@solana/web3.js"; + AccountRole, + address, + createNoopSigner, + type IInstruction, + } from "@solana/kit"; import { bytesToHex } from "viem"; - function toSolanaCall(instruction: TransactionInstruction) { + function toSolanaCall(instruction: IInstruction) { return { - programId: instruction.programId.toBase58(), - accounts: instruction.keys.map((account) => ({ - pubkey: account.pubkey.toBase58(), - isSigner: account.isSigner, - isWritable: account.isWritable, + programId: instruction.programAddress, + accounts: (instruction.accounts ?? []).map((account) => ({ + pubkey: account.address, + isSigner: + account.role === AccountRole.READONLY_SIGNER || + account.role === AccountRole.WRITABLE_SIGNER, + isWritable: + account.role === AccountRole.WRITABLE || + account.role === AccountRole.WRITABLE_SIGNER, })), - data: bytesToHex(instruction.data), + data: bytesToHex(instruction.data ?? new Uint8Array()), }; } export function transferSolCall(args: { from: string; to: string; - lamports: number; + lamports: bigint; }) { return toSolanaCall( - SystemProgram.transfer({ - fromPubkey: new PublicKey(args.from), - toPubkey: new PublicKey(args.to), - lamports: args.lamports, + getTransferSolInstruction({ + source: createNoopSigner(address(args.from)), + destination: address(args.to), + amount: args.lamports, }), ); } diff --git a/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx b/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx index 52f34cfaf..cbdd58da6 100644 --- a/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx +++ b/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx @@ -10,7 +10,7 @@ You can use smart wallet client actions to prepare, sign, and send Solana transa transferSolCall({ from: client.solanaAccount, to: client.solanaAccount, - lamports: 0, + lamports: 0n, }), ], }); @@ -47,35 +47,41 @@ You can use smart wallet client actions to prepare, sign, and send Solana transa ``` ```ts title="utils.ts" + import { getTransferSolInstruction } from "@solana-program/system"; import { - PublicKey, - SystemProgram, - type TransactionInstruction, - } from "@solana/web3.js"; + AccountRole, + address, + createNoopSigner, + type IInstruction, + } from "@solana/kit"; import { bytesToHex } from "viem"; - function toSolanaCall(instruction: TransactionInstruction) { + function toSolanaCall(instruction: IInstruction) { return { - programId: instruction.programId.toBase58(), - accounts: instruction.keys.map((account) => ({ - pubkey: account.pubkey.toBase58(), - isSigner: account.isSigner, - isWritable: account.isWritable, + programId: instruction.programAddress, + accounts: (instruction.accounts ?? []).map((account) => ({ + pubkey: account.address, + isSigner: + account.role === AccountRole.READONLY_SIGNER || + account.role === AccountRole.WRITABLE_SIGNER, + isWritable: + account.role === AccountRole.WRITABLE || + account.role === AccountRole.WRITABLE_SIGNER, })), - data: bytesToHex(instruction.data), + data: bytesToHex(instruction.data ?? new Uint8Array()), }; } export function transferSolCall(args: { from: string; to: string; - lamports: number; + lamports: bigint; }) { return toSolanaCall( - SystemProgram.transfer({ - fromPubkey: new PublicKey(args.from), - toPubkey: new PublicKey(args.to), - lamports: args.lamports, + getTransferSolInstruction({ + source: createNoopSigner(address(args.from)), + destination: address(args.to), + amount: args.lamports, }), ); } diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/api.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/api.mdx index 7e212016b..397f5d300 100644 --- a/content/wallets/pages/transactions/sponsor-gas/solana/api.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/solana/api.mdx @@ -1,4 +1,4 @@ -Sponsorship is applied by passing `capabilities.paymasterService` to `wallet_prepareCalls`. The rest of the flow (sign with Ed25519, send, poll status) is identical to the unsponsored Solana raw API flow. +Sponsorship is applied by passing `capabilities.paymasterService` to `wallet_prepareCalls`. The rest of the flow (sign with Ed25519 via `@solana/kit`, send, poll status) is identical to the unsponsored Solana raw API flow. See the [`wallet_prepareCalls`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-prepare-calls), [`wallet_sendPreparedCalls`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-send-prepared-calls), and [`wallet_getCallsStatus`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-get-calls-status) API references for full parameter descriptions. @@ -7,8 +7,17 @@ See the [`wallet_prepareCalls`](/docs/wallets/api-reference/smart-wallets/wallet ```ts twoslash -import { Keypair, PublicKey, SystemProgram, VersionedTransaction } from "@solana/web3.js"; -import bs58 from "bs58"; +import { getTransferSolInstruction } from "@solana-program/system"; +import { + AccountRole, + address, + createKeyPairSignerFromBytes, + createNoopSigner, + getBase58Decoder, + getTransactionDecoder, + partiallySignTransaction, + type IInstruction, +} from "@solana/kit"; import { bytesToHex } from "viem"; const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY!; @@ -19,8 +28,8 @@ const CHAIN_ID = "solana:devnet"; const secretKey = new Uint8Array( Buffer.from(process.env.SOLANA_SECRET_KEY!, "base64"), ); -const userKeypair = Keypair.fromSecretKey(secretKey); -const SIGNER_ADDRESS = userKeypair.publicKey.toBase58(); +const userSigner = await createKeyPairSignerFromBytes(secretKey); +const SIGNER_ADDRESS = userSigner.address; async function rpc(method: string, params: unknown[]): Promise { const response = await fetch(RPC_URL, { @@ -32,6 +41,22 @@ async function rpc(method: string, params: unknown[]): Promise { if (body.error) throw new Error(JSON.stringify(body.error)); return body.result as T; } + +function toSolanaCall(instruction: IInstruction) { + return { + programId: instruction.programAddress, + accounts: (instruction.accounts ?? []).map((a) => ({ + pubkey: a.address, + isSigner: + a.role === AccountRole.READONLY_SIGNER || + a.role === AccountRole.WRITABLE_SIGNER, + isWritable: + a.role === AccountRole.WRITABLE || + a.role === AccountRole.WRITABLE_SIGNER, + })), + data: bytesToHex(instruction.data ?? new Uint8Array()), + }; +} ``` @@ -41,10 +66,10 @@ async function rpc(method: string, params: unknown[]): Promise { Attach `capabilities.paymasterService.policyId` to apply your Solana sponsorship policy. Add `prefundRent: true` to opt in to CPI rent prefunding, and `webhookData` to pass extra context to custom rules. ```ts twoslash -const transferIx = SystemProgram.transfer({ - fromPubkey: userKeypair.publicKey, - toPubkey: userKeypair.publicKey, - lamports: 0, +const transferIx = getTransferSolInstruction({ + source: createNoopSigner(address(SIGNER_ADDRESS)), + destination: address(SIGNER_ADDRESS), + amount: 0n, }); const prepared = await rpc<{ @@ -54,17 +79,7 @@ const prepared = await rpc<{ feePayment: { sponsored: boolean; feePayer: string }; }>("wallet_prepareCalls", [ { - calls: [ - { - programId: transferIx.programId.toBase58(), - accounts: transferIx.keys.map((a) => ({ - pubkey: a.pubkey.toBase58(), - isSigner: a.isSigner, - isWritable: a.isWritable, - })), - data: bytesToHex(transferIx.data), - }, - ], + calls: [toSolanaCall(transferIx)], from: SIGNER_ADDRESS, chainId: CHAIN_ID, capabilities: { @@ -87,14 +102,15 @@ console.log(prepared.feePayment); // { sponsored: true, feePayer: "" `signatureRequest.data` already contains the paymaster signature embedded; the user only needs to add their Ed25519 signature. ```ts twoslash -const txBytes = Buffer.from(prepared.signatureRequest.data.slice(2), "hex"); -const tx = VersionedTransaction.deserialize(txBytes); -tx.sign([userKeypair]); - -const signerIndex = tx.message.staticAccountKeys.findIndex( - (key) => key.toBase58() === SIGNER_ADDRESS, +const txBytes = new Uint8Array( + Buffer.from(prepared.signatureRequest.data.slice(2), "hex"), ); -const signatureBase58 = bs58.encode(tx.signatures[signerIndex]!); +const tx = getTransactionDecoder().decode(txBytes); +const signed = await partiallySignTransaction([userSigner.keyPair], tx); + +const userSignatureBytes = signed.signatures[SIGNER_ADDRESS]; +if (!userSignatureBytes) throw new Error("Missing user signature"); +const signatureBase58 = getBase58Decoder().decode(userSignatureBytes); ``` diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx index e72c0a63a..840cb7941 100644 --- a/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx @@ -10,7 +10,7 @@ Use `paymaster` on the smart wallet client to apply a Solana sponsorship policy transferSolCall({ from: client.solanaAccount, to: client.solanaAccount, - lamports: 0, + lamports: 0n, }), ], }); @@ -48,35 +48,41 @@ Use `paymaster` on the smart wallet client to apply a Solana sponsorship policy ``` ```ts title="utils.ts" + import { getTransferSolInstruction } from "@solana-program/system"; import { - PublicKey, - SystemProgram, - type TransactionInstruction, - } from "@solana/web3.js"; + AccountRole, + address, + createNoopSigner, + type IInstruction, + } from "@solana/kit"; import { bytesToHex } from "viem"; - function toSolanaCall(instruction: TransactionInstruction) { + function toSolanaCall(instruction: IInstruction) { return { - programId: instruction.programId.toBase58(), - accounts: instruction.keys.map((account) => ({ - pubkey: account.pubkey.toBase58(), - isSigner: account.isSigner, - isWritable: account.isWritable, + programId: instruction.programAddress, + accounts: (instruction.accounts ?? []).map((account) => ({ + pubkey: account.address, + isSigner: + account.role === AccountRole.READONLY_SIGNER || + account.role === AccountRole.WRITABLE_SIGNER, + isWritable: + account.role === AccountRole.WRITABLE || + account.role === AccountRole.WRITABLE_SIGNER, })), - data: bytesToHex(instruction.data), + data: bytesToHex(instruction.data ?? new Uint8Array()), }; } export function transferSolCall(args: { from: string; to: string; - lamports: number; + lamports: bigint; }) { return toSolanaCall( - SystemProgram.transfer({ - fromPubkey: new PublicKey(args.from), - toPubkey: new PublicKey(args.to), - lamports: args.lamports, + getTransferSolInstruction({ + source: createNoopSigner(address(args.from)), + destination: address(args.to), + amount: args.lamports, }), ); } diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/prefund-rent.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/prefund-rent.mdx index b1da80e78..043cdfda0 100644 --- a/content/wallets/pages/transactions/sponsor-gas/solana/prefund-rent.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/solana/prefund-rent.mdx @@ -7,7 +7,7 @@ const { id } = await client.sendCalls({ transferSolCall({ from: client.solanaAccount, to: client.solanaAccount, - lamports: 0, + lamports: 0n, }), ], capabilities: { diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx index b65b9d68c..28bf31a61 100644 --- a/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx @@ -7,7 +7,7 @@ const { id } = await client.sendCalls({ transferSolCall({ from: client.solanaAccount, to: client.solanaAccount, - lamports: 0, + lamports: 0n, }), ], capabilities: { From 56da7c290f5baee7f145e1ac20aa5483f10c7043 Mon Sep 17 00:00:00 2001 From: jakehobbs Date: Wed, 27 May 2026 17:04:46 -0700 Subject: [PATCH 08/10] Address review feedback on send + sponsor-gas prereqs. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Combine the "signer" and "funded wallet" prereq bullets on the send-transactions page — they refer to the same entity, and with EIP-7702 there is no separate Smart Wallet to fund. Revert the unnecessary "EVM" qualifications on the sponsor-gas page since the page is implicitly EVM (Solana sponsorship is documented separately). Drop webhookData from the per-request Solana paymaster example so it focuses on the policy override rather than custom-rule webhooks. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../pages/transactions/send-transactions/index.mdx | 3 +-- content/wallets/pages/transactions/sponsor-gas/index.mdx | 8 ++++---- .../transactions/sponsor-gas/solana/request-policy.mdx | 1 - 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/content/wallets/pages/transactions/send-transactions/index.mdx b/content/wallets/pages/transactions/send-transactions/index.mdx index fb6bd585f..0b15b8f65 100644 --- a/content/wallets/pages/transactions/send-transactions/index.mdx +++ b/content/wallets/pages/transactions/send-transactions/index.mdx @@ -10,8 +10,7 @@ This guide covers how to send EVM and Solana transactions with Wallet APIs. * API key from your [dashboard](https://dashboard.alchemy.com/apps) * `@alchemy/wallet-apis` installed in your project -* For EVM: a funded Smart Wallet to cover gas fees ([or a gas manager policy to sponsor gas](/docs/wallets/transactions/sponsor-gas)) -* For Solana: a Solana signer ([see Solana signer adapters](/docs/wallets/solana/signers)) and a funded wallet to cover fees, or a Solana gas sponsorship policy +* A signer with funds to cover gas (or a sponsorship policy — see [Sponsor gas](/docs/wallets/transactions/sponsor-gas) for EVM and [Solana sponsorship](/docs/wallets/transactions/sponsor-gas/solana) for Solana) ## EVM transactions diff --git a/content/wallets/pages/transactions/sponsor-gas/index.mdx b/content/wallets/pages/transactions/sponsor-gas/index.mdx index 1149c97cb..25815e5ec 100644 --- a/content/wallets/pages/transactions/sponsor-gas/index.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/index.mdx @@ -1,10 +1,10 @@ --- title: Sponsor gas -description: Sponsor EVM gas for your users. +description: Choose a gas sponsorship integration path for your smart wallet application. slug: wallets/transactions/sponsor-gas --- -Gas fees are a significant barrier to entry for new users. With Gas Sponsorship, you can eliminate this friction by covering EVM gas for your users. +Gas fees are a significant barrier to entry for new users. With Gas Sponsorship, you can eliminate this friction by covering transaction costs for your users. ## How it works @@ -34,7 +34,7 @@ and bills in fiat. ## Advanced - EVM gas sponsorship also works with the prepare calls methods in the various frameworks. Usage of the capability is the same as when using send calls. Use prepare calls if you want to inspect the prepared call before prompting for signature. + Gas sponsorship also works with the prepare calls methods in the various frameworks. Usage of the capability is the same as when using send calls. Use prepare calls if you want to inspect the prepared call before prompting for signature. @@ -49,7 +49,7 @@ and bills in fiat. - For EVM sponsorship, you can configure multiple policy IDs. The + You can configure multiple policy IDs for use in gas sponsorship. The backend chooses the first policy ID where the transaction is eligible for sponsorship. Pass an array of policy IDs instead of a single policy ID to the sponsor gas capability. See the [`wallet_prepareCalls` diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx index 28bf31a61..531c0cda4 100644 --- a/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx @@ -13,7 +13,6 @@ const { id } = await client.sendCalls({ capabilities: { paymaster: { policyId: "OTHER_SOLANA_POLICY_ID", - webhookData: "checkout-session-123", }, }, }); From 24c259dc8891fe9527897a0a1764f09fa8d5a39b Mon Sep 17 00:00:00 2001 From: jakehobbs Date: Wed, 27 May 2026 19:02:58 -0700 Subject: [PATCH 09/10] Clarify Solana Wallet API docs with validated Kit flows Co-Authored-By: Codex --- content/docs.yml | 4 +- .../pages/third-party/signers/privy.mdx | 45 ++-- .../transactions/send-transactions/index.mdx | 4 +- .../send-transactions/solana/api.mdx | 220 ++++++++-------- .../send-transactions/solana/client.mdx | 43 ++-- .../solana/prepare-calls/client.mdx | 43 ++-- .../transactions/sponsor-gas/solana/api.mdx | 235 ++++++++++-------- .../sponsor-gas/solana/client.mdx | 43 ++-- .../transactions/sponsor-gas/solana/index.mdx | 2 +- 9 files changed, 317 insertions(+), 322 deletions(-) diff --git a/content/docs.yml b/content/docs.yml index ef54d3175..a8c8256db 100644 --- a/content/docs.yml +++ b/content/docs.yml @@ -893,12 +893,12 @@ navigation: contents: - page: Full sponsorship path: wallets/pages/transactions/sponsor-gas/index.mdx - - page: Solana sponsorship - path: wallets/pages/transactions/sponsor-gas/solana/index.mdx - page: Conditional sponsorship rules path: wallets/pages/transactions/sponsor-gas/conditional-sponsorship-rules.mdx - page: Pay gas with any token path: wallets/pages/transactions/pay-gas-with-any-token/index.mdx + - page: Solana sponsorship + path: wallets/pages/transactions/sponsor-gas/solana/index.mdx - section: Swap tokens collapsed: true contents: diff --git a/content/wallets/pages/third-party/signers/privy.mdx b/content/wallets/pages/third-party/signers/privy.mdx index c85c0c4fe..635cc591b 100644 --- a/content/wallets/pages/third-party/signers/privy.mdx +++ b/content/wallets/pages/third-party/signers/privy.mdx @@ -185,7 +185,7 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba ### Install Solana deps - The example below uses `@solana/kit` and `@solana-program/system` to build the transfer instruction: + The example below uses `@solana/kit` and `@solana-program/system` to build the transfer call: ```shell npm @@ -279,43 +279,34 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba ``` ```ts title="utils.ts" - import { getTransferSolInstruction } from "@solana-program/system"; import { - AccountRole, - address, - createNoopSigner, - type IInstruction, - } from "@solana/kit"; + getTransferSolInstructionDataEncoder, + SYSTEM_PROGRAM_ADDRESS, + } from "@solana-program/system"; + import { address } from "@solana/kit"; import { bytesToHex } from "viem"; - function toSolanaCall(instruction: IInstruction) { - return { - programId: instruction.programAddress, - accounts: (instruction.accounts ?? []).map((account) => ({ - pubkey: account.address, - isSigner: - account.role === AccountRole.READONLY_SIGNER || - account.role === AccountRole.WRITABLE_SIGNER, - isWritable: - account.role === AccountRole.WRITABLE || - account.role === AccountRole.WRITABLE_SIGNER, - })), - data: bytesToHex(instruction.data ?? new Uint8Array()), - }; - } - export function transferSolCall(args: { from: string; to: string; lamports: bigint; }) { - return toSolanaCall( - getTransferSolInstruction({ - source: createNoopSigner(address(args.from)), - destination: address(args.to), + const source = address(args.from); + const destination = address(args.to); + const data = bytesToHex( + getTransferSolInstructionDataEncoder().encode({ amount: args.lamports, }), ); + + return { + programId: SYSTEM_PROGRAM_ADDRESS, + accounts: [ + { pubkey: source, isSigner: true, isWritable: true }, + { pubkey: destination, isSigner: false, isWritable: true }, + ], + data, + }; } ``` diff --git a/content/wallets/pages/transactions/send-transactions/index.mdx b/content/wallets/pages/transactions/send-transactions/index.mdx index 0b15b8f65..34fda265e 100644 --- a/content/wallets/pages/transactions/send-transactions/index.mdx +++ b/content/wallets/pages/transactions/send-transactions/index.mdx @@ -72,7 +72,7 @@ Solana transactions use the same client actions as EVM, but `calls` contain Sola - + @@ -87,7 +87,7 @@ Solana transactions use the same client actions as EVM, but `calls` contain Sola - + The Solana raw API flow already uses `wallet_prepareCalls` and `wallet_sendPreparedCalls` directly — see the API tab under [Solana transactions](#solana-transactions). diff --git a/content/wallets/pages/transactions/send-transactions/solana/api.mdx b/content/wallets/pages/transactions/send-transactions/solana/api.mdx index 8e6b58432..8733a3458 100644 --- a/content/wallets/pages/transactions/send-transactions/solana/api.mdx +++ b/content/wallets/pages/transactions/send-transactions/solana/api.mdx @@ -1,136 +1,152 @@ -Solana signatures are Ed25519, so the raw JSON-RPC flow uses `@solana/kit` to sign instead of `cast`. The transport URL and method names are identical to the EVM flow. +Solana raw API calls use the same JSON-RPC endpoints as EVM. The examples +below use `curl` and `jq`; signing is wallet-dependent because Solana signatures +are Ed25519. See the [`wallet_prepareCalls`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-prepare-calls), [`wallet_sendPreparedCalls`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-send-prepared-calls), and [`wallet_getCallsStatus`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-get-calls-status) API references for full parameter descriptions. - - -```ts twoslash -import { getTransferSolInstruction } from "@solana-program/system"; -import { - AccountRole, - address, - createKeyPairSignerFromBytes, - createNoopSigner, - getBase58Decoder, - getTransactionDecoder, - partiallySignTransaction, - type IInstruction, -} from "@solana/kit"; -import { bytesToHex } from "viem"; - -const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY!; -const RPC_URL = `https://api.g.alchemy.com/v2/${ALCHEMY_API_KEY}`; -const CHAIN_ID = "solana:devnet"; - -// 64-byte Solana secret key, base64-encoded. Keep this in a secret manager or .env. -const secretKey = new Uint8Array( - Buffer.from(process.env.SOLANA_SECRET_KEY!, "base64"), -); -const userSigner = await createKeyPairSignerFromBytes(secretKey); -const SIGNER_ADDRESS = userSigner.address; - -async function rpc(method: string, params: unknown[]): Promise { - const response = await fetch(RPC_URL, { - method: "POST", - headers: { accept: "application/json", "content-type": "application/json" }, - body: JSON.stringify({ id: 1, jsonrpc: "2.0", method, params }), - }); - const body = (await response.json()) as { result?: T; error?: unknown }; - if (body.error) throw new Error(JSON.stringify(body.error)); - return body.result as T; -} - -function toSolanaCall(instruction: IInstruction) { - return { - programId: instruction.programAddress, - accounts: (instruction.accounts ?? []).map((a) => ({ - pubkey: a.address, - isSigner: - a.role === AccountRole.READONLY_SIGNER || - a.role === AccountRole.WRITABLE_SIGNER, - isWritable: - a.role === AccountRole.WRITABLE || - a.role === AccountRole.WRITABLE_SIGNER, - })), - data: bytesToHex(instruction.data ?? new Uint8Array()), - }; -} + + +The call below is a 0-lamport System Program self-transfer. Its instruction data +is `SystemInstruction::Transfer` (`2`) plus a little-endian `u64` lamport amount. + +```bash twoslash +ALCHEMY_API_KEY=your-alchemy-api-key +CHAIN_ID=solana:devnet +SIGNER_ADDRESS=your-solana-address + +RPC_URL="https://api.g.alchemy.com/v2/$ALCHEMY_API_KEY" +SYSTEM_PROGRAM_ID=11111111111111111111111111111111 +TRANSFER_0_LAMPORT_DATA=0x020000000000000000000000 ``` -Build the Solana call (a 0-lamport self-transfer in this example) and call `wallet_prepareCalls` with the user's address as `from`. - -```ts twoslash -const transferIx = getTransferSolInstruction({ - source: createNoopSigner(address(SIGNER_ADDRESS)), - destination: address(SIGNER_ADDRESS), - amount: 0n, -}); - -const prepared = await rpc<{ - type: "solana-transaction-v0"; - signatureRequest: { type: "solana_signTransaction"; data: `0x${string}` }; - data: unknown; -}>("wallet_prepareCalls", [ - { - calls: [toSolanaCall(transferIx)], - from: SIGNER_ADDRESS, - chainId: CHAIN_ID, - }, -]); +Call `wallet_prepareCalls` with the user's Solana address as `from`. + +```bash twoslash +PREPARE_CALLS_RESPONSE=$(jq -n \ + --arg signer "$SIGNER_ADDRESS" \ + --arg chain_id "$CHAIN_ID" \ + --arg system_program "$SYSTEM_PROGRAM_ID" \ + --arg transfer_data "$TRANSFER_0_LAMPORT_DATA" \ + '{ + "id": 1, + "jsonrpc": "2.0", + "method": "wallet_prepareCalls", + "params": [ + { + "calls": [ + { + "programId": $system_program, + "accounts": [ + { + "pubkey": $signer, + "isSigner": true, + "isWritable": true + }, + { + "pubkey": $signer, + "isSigner": false, + "isWritable": true + } + ], + "data": $transfer_data + } + ], + "from": $signer, + "chainId": $chain_id + } + ] + }' | curl --request POST \ + --url "$RPC_URL" \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --data @-) ``` -`signatureRequest.data` is the full v0 transaction wire-format bytes, hex-encoded. Decode it with kit, partially sign with the user's keypair, and base58-encode the resulting signature. +`signatureRequest.data` is the full v0 transaction wire-format bytes, +hex-encoded. Ask your Solana wallet to sign this transaction and return the +user's Ed25519 signature as base58. -```ts twoslash -const txBytes = new Uint8Array( - Buffer.from(prepared.signatureRequest.data.slice(2), "hex"), -); -const tx = getTransactionDecoder().decode(txBytes); -const signed = await partiallySignTransaction([userSigner.keyPair], tx); +For a raw Kit keypair, decode `signatureRequest.data` with +`getTransactionDecoder()`, sign with +`partiallySignTransaction([signer.keyPair], transaction)`, and base58-encode the +entry in `signed.signatures[signer.address]` with `getBase58Decoder()`. -const userSignatureBytes = signed.signatures[SIGNER_ADDRESS]; -if (!userSignatureBytes) throw new Error("Missing user signature"); -const signatureBase58 = getBase58Decoder().decode(userSignatureBytes); +```bash twoslash +SIGNATURE_REQUEST_HEX=$(echo "$PREPARE_CALLS_RESPONSE" | jq -r '.result.signatureRequest.data') + +# Sign SIGNATURE_REQUEST_HEX with your Solana wallet. +# The signature must be the user's base58 Ed25519 signature. +SIGNATURE_BASE58=your-base58-solana-signature ``` -```ts twoslash -const { id } = await rpc<{ id: string }>("wallet_sendPreparedCalls", [ - { - type: prepared.type, - chainId: CHAIN_ID, - data: prepared.data, - signature: { type: "ed25519", data: signatureBase58 }, - }, -]); +```bash twoslash +CALL_TYPE=$(echo "$PREPARE_CALLS_RESPONSE" | jq -r '.result.type') +CALL_DATA=$(echo "$PREPARE_CALLS_RESPONSE" | jq -c '.result.data') + +SEND_CALLS_RESPONSE=$(jq -n \ + --arg call_type "$CALL_TYPE" \ + --arg chain_id "$CHAIN_ID" \ + --argjson call_data "$CALL_DATA" \ + --arg signature "$SIGNATURE_BASE58" \ + '{ + "id": 1, + "jsonrpc": "2.0", + "method": "wallet_sendPreparedCalls", + "params": [ + { + "type": $call_type, + "chainId": $chain_id, + "data": $call_data, + "signature": { + "type": "ed25519", + "data": $signature + } + } + ] + }' | curl --request POST \ + --url "$RPC_URL" \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --data @-) + +CALL_ID=$(echo "$SEND_CALLS_RESPONSE" | jq -r '.result.id') ``` -Poll `wallet_getCallsStatus` until the status leaves the pending range. Solana receipts return a base58 `signature` instead of `transactionHash`. - -```ts twoslash -const status = await rpc<{ - status: number; - receipts?: Array<{ signature: string }>; -}>("wallet_getCallsStatus", [id]); - -console.log(status.status, status.receipts?.[0]?.signature); +Poll `wallet_getCallsStatus` until the status leaves the pending range. Solana +receipts return a base58 `signature` instead of an EVM `transactionHash`. + +```bash twoslash +curl --request POST \ + --url "$RPC_URL" \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --data ' +{ + "id": 1, + "jsonrpc": "2.0", + "method": "wallet_getCallsStatus", + "params": [ + "'$CALL_ID'" + ] +}' ``` diff --git a/content/wallets/pages/transactions/send-transactions/solana/client.mdx b/content/wallets/pages/transactions/send-transactions/solana/client.mdx index 628994163..02cb50c05 100644 --- a/content/wallets/pages/transactions/send-transactions/solana/client.mdx +++ b/content/wallets/pages/transactions/send-transactions/solana/client.mdx @@ -46,43 +46,34 @@ You can send Solana instructions using the smart wallet client [`sendCalls`](/do ``` ```ts title="utils.ts" - import { getTransferSolInstruction } from "@solana-program/system"; import { - AccountRole, - address, - createNoopSigner, - type IInstruction, - } from "@solana/kit"; + getTransferSolInstructionDataEncoder, + SYSTEM_PROGRAM_ADDRESS, + } from "@solana-program/system"; + import { address } from "@solana/kit"; import { bytesToHex } from "viem"; - function toSolanaCall(instruction: IInstruction) { - return { - programId: instruction.programAddress, - accounts: (instruction.accounts ?? []).map((account) => ({ - pubkey: account.address, - isSigner: - account.role === AccountRole.READONLY_SIGNER || - account.role === AccountRole.WRITABLE_SIGNER, - isWritable: - account.role === AccountRole.WRITABLE || - account.role === AccountRole.WRITABLE_SIGNER, - })), - data: bytesToHex(instruction.data ?? new Uint8Array()), - }; - } - export function transferSolCall(args: { from: string; to: string; lamports: bigint; }) { - return toSolanaCall( - getTransferSolInstruction({ - source: createNoopSigner(address(args.from)), - destination: address(args.to), + const source = address(args.from); + const destination = address(args.to); + const data = bytesToHex( + getTransferSolInstructionDataEncoder().encode({ amount: args.lamports, }), ); + + return { + programId: SYSTEM_PROGRAM_ADDRESS, + accounts: [ + { pubkey: source, isSigner: true, isWritable: true }, + { pubkey: destination, isSigner: false, isWritable: true }, + ], + data, + }; } ``` diff --git a/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx b/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx index cbdd58da6..62a4d4db5 100644 --- a/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx +++ b/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx @@ -47,43 +47,34 @@ You can use smart wallet client actions to prepare, sign, and send Solana transa ``` ```ts title="utils.ts" - import { getTransferSolInstruction } from "@solana-program/system"; import { - AccountRole, - address, - createNoopSigner, - type IInstruction, - } from "@solana/kit"; + getTransferSolInstructionDataEncoder, + SYSTEM_PROGRAM_ADDRESS, + } from "@solana-program/system"; + import { address } from "@solana/kit"; import { bytesToHex } from "viem"; - function toSolanaCall(instruction: IInstruction) { - return { - programId: instruction.programAddress, - accounts: (instruction.accounts ?? []).map((account) => ({ - pubkey: account.address, - isSigner: - account.role === AccountRole.READONLY_SIGNER || - account.role === AccountRole.WRITABLE_SIGNER, - isWritable: - account.role === AccountRole.WRITABLE || - account.role === AccountRole.WRITABLE_SIGNER, - })), - data: bytesToHex(instruction.data ?? new Uint8Array()), - }; - } - export function transferSolCall(args: { from: string; to: string; lamports: bigint; }) { - return toSolanaCall( - getTransferSolInstruction({ - source: createNoopSigner(address(args.from)), - destination: address(args.to), + const source = address(args.from); + const destination = address(args.to); + const data = bytesToHex( + getTransferSolInstructionDataEncoder().encode({ amount: args.lamports, }), ); + + return { + programId: SYSTEM_PROGRAM_ADDRESS, + accounts: [ + { pubkey: source, isSigner: true, isWritable: true }, + { pubkey: destination, isSigner: false, isWritable: true }, + ], + data, + }; } ``` diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/api.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/api.mdx index 397f5d300..092db575a 100644 --- a/content/wallets/pages/transactions/sponsor-gas/solana/api.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/solana/api.mdx @@ -1,138 +1,153 @@ -Sponsorship is applied by passing `capabilities.paymasterService` to `wallet_prepareCalls`. The rest of the flow (sign with Ed25519 via `@solana/kit`, send, poll status) is identical to the unsponsored Solana raw API flow. +Sponsorship is applied by passing `capabilities.paymasterService` to +`wallet_prepareCalls`. The examples below use `curl` and `jq`; signing is +wallet-dependent because Solana signatures are Ed25519. See the [`wallet_prepareCalls`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-prepare-calls), [`wallet_sendPreparedCalls`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-send-prepared-calls), and [`wallet_getCallsStatus`](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-get-calls-status) API references for full parameter descriptions. - - -```ts twoslash -import { getTransferSolInstruction } from "@solana-program/system"; -import { - AccountRole, - address, - createKeyPairSignerFromBytes, - createNoopSigner, - getBase58Decoder, - getTransactionDecoder, - partiallySignTransaction, - type IInstruction, -} from "@solana/kit"; -import { bytesToHex } from "viem"; - -const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY!; -const SOLANA_POLICY_ID = process.env.SOLANA_POLICY_ID!; -const RPC_URL = `https://api.g.alchemy.com/v2/${ALCHEMY_API_KEY}`; -const CHAIN_ID = "solana:devnet"; - -const secretKey = new Uint8Array( - Buffer.from(process.env.SOLANA_SECRET_KEY!, "base64"), -); -const userSigner = await createKeyPairSignerFromBytes(secretKey); -const SIGNER_ADDRESS = userSigner.address; - -async function rpc(method: string, params: unknown[]): Promise { - const response = await fetch(RPC_URL, { - method: "POST", - headers: { accept: "application/json", "content-type": "application/json" }, - body: JSON.stringify({ id: 1, jsonrpc: "2.0", method, params }), - }); - const body = (await response.json()) as { result?: T; error?: unknown }; - if (body.error) throw new Error(JSON.stringify(body.error)); - return body.result as T; -} - -function toSolanaCall(instruction: IInstruction) { - return { - programId: instruction.programAddress, - accounts: (instruction.accounts ?? []).map((a) => ({ - pubkey: a.address, - isSigner: - a.role === AccountRole.READONLY_SIGNER || - a.role === AccountRole.WRITABLE_SIGNER, - isWritable: - a.role === AccountRole.WRITABLE || - a.role === AccountRole.WRITABLE_SIGNER, - })), - data: bytesToHex(instruction.data ?? new Uint8Array()), - }; -} + + +The call below is a 0-lamport System Program self-transfer. Its instruction data +is `SystemInstruction::Transfer` (`2`) plus a little-endian `u64` lamport amount. + +```bash twoslash +ALCHEMY_API_KEY=your-alchemy-api-key +SOLANA_POLICY_ID=your-solana-policy-id +CHAIN_ID=solana:devnet +SIGNER_ADDRESS=your-solana-address + +RPC_URL="https://api.g.alchemy.com/v2/$ALCHEMY_API_KEY" +SYSTEM_PROGRAM_ID=11111111111111111111111111111111 +TRANSFER_0_LAMPORT_DATA=0x020000000000000000000000 ``` -Attach `capabilities.paymasterService.policyId` to apply your Solana sponsorship policy. Add `prefundRent: true` to opt in to CPI rent prefunding, and `webhookData` to pass extra context to custom rules. - -```ts twoslash -const transferIx = getTransferSolInstruction({ - source: createNoopSigner(address(SIGNER_ADDRESS)), - destination: address(SIGNER_ADDRESS), - amount: 0n, -}); - -const prepared = await rpc<{ - type: "solana-transaction-v0"; - signatureRequest: { type: "solana_signTransaction"; data: `0x${string}` }; - data: unknown; - feePayment: { sponsored: boolean; feePayer: string }; -}>("wallet_prepareCalls", [ - { - calls: [toSolanaCall(transferIx)], - from: SIGNER_ADDRESS, - chainId: CHAIN_ID, - capabilities: { - paymasterService: { - policyId: SOLANA_POLICY_ID, - // prefundRent: true, // opt in for CPI rent - // webhookData: "session-123", // optional, for custom-rule webhooks - }, - }, - }, -]); - -console.log(prepared.feePayment); // { sponsored: true, feePayer: "" } +Attach `capabilities.paymasterService.policyId` to apply your Solana sponsorship +policy. Add `prefundRent: true` to opt in to CPI rent prefunding, and +`webhookData` to pass extra context to custom rules. + +```bash twoslash +PREPARE_CALLS_RESPONSE=$(jq -n \ + --arg signer "$SIGNER_ADDRESS" \ + --arg chain_id "$CHAIN_ID" \ + --arg policy_id "$SOLANA_POLICY_ID" \ + --arg system_program "$SYSTEM_PROGRAM_ID" \ + --arg transfer_data "$TRANSFER_0_LAMPORT_DATA" \ + '{ + "id": 1, + "jsonrpc": "2.0", + "method": "wallet_prepareCalls", + "params": [ + { + "calls": [ + { + "programId": $system_program, + "accounts": [ + { + "pubkey": $signer, + "isSigner": true, + "isWritable": true + }, + { + "pubkey": $signer, + "isSigner": false, + "isWritable": true + } + ], + "data": $transfer_data + } + ], + "from": $signer, + "chainId": $chain_id, + "capabilities": { + "paymasterService": { + "policyId": $policy_id + } + } + } + ] + }' | curl --request POST \ + --url "$RPC_URL" \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --data @-) + +echo "$PREPARE_CALLS_RESPONSE" | jq '.result.feePayment' ``` -`signatureRequest.data` already contains the paymaster signature embedded; the user only needs to add their Ed25519 signature. +`signatureRequest.data` already contains the paymaster signature embedded; the +user only needs to add their Ed25519 signature. + +For a raw Kit keypair, decode `signatureRequest.data` with +`getTransactionDecoder()`, sign with +`partiallySignTransaction([signer.keyPair], transaction)`, and base58-encode the +entry in `signed.signatures[signer.address]` with `getBase58Decoder()`. -```ts twoslash -const txBytes = new Uint8Array( - Buffer.from(prepared.signatureRequest.data.slice(2), "hex"), -); -const tx = getTransactionDecoder().decode(txBytes); -const signed = await partiallySignTransaction([userSigner.keyPair], tx); +```bash twoslash +SIGNATURE_REQUEST_HEX=$(echo "$PREPARE_CALLS_RESPONSE" | jq -r '.result.signatureRequest.data') -const userSignatureBytes = signed.signatures[SIGNER_ADDRESS]; -if (!userSignatureBytes) throw new Error("Missing user signature"); -const signatureBase58 = getBase58Decoder().decode(userSignatureBytes); +# Sign SIGNATURE_REQUEST_HEX with your Solana wallet. +# The signature must be the user's base58 Ed25519 signature. +SIGNATURE_BASE58=your-base58-solana-signature ``` -```ts twoslash -const { id } = await rpc<{ id: string }>("wallet_sendPreparedCalls", [ - { - type: prepared.type, - chainId: CHAIN_ID, - data: prepared.data, - signature: { type: "ed25519", data: signatureBase58 }, - }, -]); - -const status = await rpc<{ - status: number; - receipts?: Array<{ signature: string }>; -}>("wallet_getCallsStatus", [id]); - -console.log(status.status, status.receipts?.[0]?.signature); +```bash twoslash +CALL_TYPE=$(echo "$PREPARE_CALLS_RESPONSE" | jq -r '.result.type') +CALL_DATA=$(echo "$PREPARE_CALLS_RESPONSE" | jq -c '.result.data') + +SEND_CALLS_RESPONSE=$(jq -n \ + --arg call_type "$CALL_TYPE" \ + --arg chain_id "$CHAIN_ID" \ + --argjson call_data "$CALL_DATA" \ + --arg signature "$SIGNATURE_BASE58" \ + '{ + "id": 1, + "jsonrpc": "2.0", + "method": "wallet_sendPreparedCalls", + "params": [ + { + "type": $call_type, + "chainId": $chain_id, + "data": $call_data, + "signature": { + "type": "ed25519", + "data": $signature + } + } + ] + }' | curl --request POST \ + --url "$RPC_URL" \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --data @-) + +CALL_ID=$(echo "$SEND_CALLS_RESPONSE" | jq -r '.result.id') + +curl --request POST \ + --url "$RPC_URL" \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --data ' +{ + "id": 1, + "jsonrpc": "2.0", + "method": "wallet_getCallsStatus", + "params": [ + "'$CALL_ID'" + ] +}' ``` diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx index 840cb7941..09b59f7b1 100644 --- a/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx @@ -48,43 +48,34 @@ Use `paymaster` on the smart wallet client to apply a Solana sponsorship policy ``` ```ts title="utils.ts" - import { getTransferSolInstruction } from "@solana-program/system"; import { - AccountRole, - address, - createNoopSigner, - type IInstruction, - } from "@solana/kit"; + getTransferSolInstructionDataEncoder, + SYSTEM_PROGRAM_ADDRESS, + } from "@solana-program/system"; + import { address } from "@solana/kit"; import { bytesToHex } from "viem"; - function toSolanaCall(instruction: IInstruction) { - return { - programId: instruction.programAddress, - accounts: (instruction.accounts ?? []).map((account) => ({ - pubkey: account.address, - isSigner: - account.role === AccountRole.READONLY_SIGNER || - account.role === AccountRole.WRITABLE_SIGNER, - isWritable: - account.role === AccountRole.WRITABLE || - account.role === AccountRole.WRITABLE_SIGNER, - })), - data: bytesToHex(instruction.data ?? new Uint8Array()), - }; - } - export function transferSolCall(args: { from: string; to: string; lamports: bigint; }) { - return toSolanaCall( - getTransferSolInstruction({ - source: createNoopSigner(address(args.from)), - destination: address(args.to), + const source = address(args.from); + const destination = address(args.to); + const data = bytesToHex( + getTransferSolInstructionDataEncoder().encode({ amount: args.lamports, }), ); + + return { + programId: SYSTEM_PROGRAM_ADDRESS, + accounts: [ + { pubkey: source, isSigner: true, isWritable: true }, + { pubkey: destination, isSigner: false, isWritable: true }, + ], + data, + }; } ``` diff --git a/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx b/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx index cdfa78ffa..04f65593a 100644 --- a/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx @@ -19,7 +19,7 @@ Use Solana sponsorship to cover transaction fees and rent for Solana calls submi - + From 0d311d42d2d3470d3b8dc780157498344110f564 Mon Sep 17 00:00:00 2001 From: jakehobbs Date: Wed, 27 May 2026 20:19:55 -0700 Subject: [PATCH 10/10] docs: address Solana wallet doc feedback --- content/wallets/pages/third-party/signers/privy.mdx | 2 -- content/wallets/pages/third-party/signers/solana-signers.mdx | 2 +- .../wallets/pages/transactions/send-transactions/index.mdx | 4 ++-- .../pages/transactions/send-transactions/solana/client.mdx | 2 +- .../send-transactions/solana/prepare-calls/client.mdx | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/content/wallets/pages/third-party/signers/privy.mdx b/content/wallets/pages/third-party/signers/privy.mdx index 635cc591b..3b83fd5c3 100644 --- a/content/wallets/pages/third-party/signers/privy.mdx +++ b/content/wallets/pages/third-party/signers/privy.mdx @@ -181,8 +181,6 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba ## Send Solana transactions - Privy's Solana embedded wallet plugs into Wallet APIs without an adapter — `useWallets()` from `@privy-io/react-auth/solana` returns wallets that satisfy the `SolanaSigner` interface directly. - ### Install Solana deps The example below uses `@solana/kit` and `@solana-program/system` to build the transfer call: diff --git a/content/wallets/pages/third-party/signers/solana-signers.mdx b/content/wallets/pages/third-party/signers/solana-signers.mdx index 2bf1d4a3d..d3999036f 100644 --- a/content/wallets/pages/third-party/signers/solana-signers.mdx +++ b/content/wallets/pages/third-party/signers/solana-signers.mdx @@ -4,7 +4,7 @@ description: Use Solana signers with Wallet APIs slug: wallets/solana/signers --- -Solana Wallet APIs use an Ed25519 signer that can sign serialized Solana transactions. The `signer` parameter of `createSmartWalletClient` accepts a `SolanaSigner` when you use a Solana chain ID such as `"solana:devnet"` or `"solana:mainnet"`. +Solana Wallet APIs require an Ed25519 signer that can sign serialized Solana transactions. The `signer` parameter of `createSmartWalletClient` accepts a `SolanaSigner` when you use a Solana chain ID such as `"solana:devnet"` or `"solana:mainnet"`. ```ts type SolanaSigner = { diff --git a/content/wallets/pages/transactions/send-transactions/index.mdx b/content/wallets/pages/transactions/send-transactions/index.mdx index 34fda265e..3295c4a95 100644 --- a/content/wallets/pages/transactions/send-transactions/index.mdx +++ b/content/wallets/pages/transactions/send-transactions/index.mdx @@ -63,7 +63,7 @@ The EVM client defaults to using [EIP-7702](/docs/wallets/transactions/using-eip Solana transactions use the same client actions as EVM, but `calls` contain Solana instructions: `programId`, hex-encoded `data`, and account metadata. For signer setup, see [Solana signer adapters](/docs/wallets/solana/signers). -`client.solanaAccount` defaults to the signer's address. If you pass `account` to `createSmartWalletClient` or `sendCalls`, it must be an account that the configured signer can sign for. +By default, the client uses the signer's address. ### Implementation @@ -98,8 +98,8 @@ Solana transactions use the same client actions as EVM, but `calls` contain Sola Build more: * [Sponsor gas for users](/docs/wallets/transactions/sponsor-gas) -* [Solana sponsorship](/docs/wallets/transactions/sponsor-gas/solana) * [Send batch transactions](/docs/wallets/transactions/send-batch-transactions) +* [Solana sponsorship](/docs/wallets/transactions/sponsor-gas/solana) * [Use Solana signer adapters](/docs/wallets/solana/signers) Troubleshooting: diff --git a/content/wallets/pages/transactions/send-transactions/solana/client.mdx b/content/wallets/pages/transactions/send-transactions/solana/client.mdx index 02cb50c05..67fa179d6 100644 --- a/content/wallets/pages/transactions/send-transactions/solana/client.mdx +++ b/content/wallets/pages/transactions/send-transactions/solana/client.mdx @@ -78,4 +78,4 @@ You can send Solana instructions using the smart wallet client [`sendCalls`](/do ``` -This example loads a raw Ed25519 keypair from an environment variable for simplicity. For wallet-connect-style signers (Privy, Phantom, `@solana/wallet-adapter-react`, Wallet Standard), see [Solana signer adapters](/docs/wallets/solana/signers). +This example loads a raw Ed25519 keypair from an environment variable for simplicity. For app wallets like Privy, Phantom, `@solana/wallet-adapter-react`, or Wallet Standard, see [Solana signer adapters](/docs/wallets/solana/signers). diff --git a/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx b/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx index 62a4d4db5..a5c9c09af 100644 --- a/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx +++ b/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx @@ -79,4 +79,4 @@ You can use smart wallet client actions to prepare, sign, and send Solana transa ``` -See the [`prepareCalls`](/docs/wallets/reference/wallet-apis/functions/prepareCalls), [`signPreparedCalls`](/docs/wallets/reference/wallet-apis/functions/signPreparedCalls), and [`sendPreparedCalls`](/docs/wallets/reference/wallet-apis/functions/sendPreparedCalls) SDK references for full parameter descriptions. +See the [`prepareCalls`](/docs/wallets/reference/wallet-apis/solana/functions/prepareCalls), [`signPreparedCalls`](/docs/wallets/reference/wallet-apis/solana/functions/signPreparedCalls), and [`sendPreparedCalls`](/docs/wallets/reference/wallet-apis/solana/functions/sendPreparedCalls) SDK references for full parameter descriptions.