diff --git a/content/docs.yml b/content/docs.yml index 75dc3d1df..a8c8256db 100644 --- a/content/docs.yml +++ b/content/docs.yml @@ -895,10 +895,10 @@ 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 + - page: Solana sponsorship + path: wallets/pages/transactions/sponsor-gas/solana/index.mdx - section: Swap tokens collapsed: true contents: @@ -949,6 +949,8 @@ navigation: 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) @@ -1050,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/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 41f7683fc..0177951a5 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/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..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..3b83fd5c3 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 --- @@ -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 @@ -37,6 +37,8 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba ``` + 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) * Alchemy API key: @@ -52,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`: @@ -78,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`: @@ -103,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: @@ -129,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: @@ -174,6 +178,141 @@ Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, ba * 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 + + ### Install Solana deps + + The example below uses `@solana/kit` and `@solana-program/system` to build the transfer call: + + + ```shell npm + npm install @solana/kit @solana-program/system + ``` + + ```shell bun + bun add @solana/kit @solana-program/system + ``` + + ```shell yarn + yarn add @solana/kit @solana-program/system + ``` + + ```shell pnpm + pnpm add @solana/kit @solana-program/system + ``` + + + ### 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. + + + ```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 { + getTransferSolInstructionDataEncoder, + SYSTEM_PROGRAM_ADDRESS, + } from "@solana-program/system"; + import { address } from "@solana/kit"; + import { bytesToHex } from "viem"; + + export function transferSolCall(args: { + from: string; + to: string; + lamports: bigint; + }) { + 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, + }; + } + ``` + + + ### Notes + + * 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/third-party/signers/solana-signers.mdx b/content/wallets/pages/third-party/signers/solana-signers.mdx new file mode 100644 index 000000000..d3999036f --- /dev/null +++ b/content/wallets/pages/third-party/signers/solana-signers.mdx @@ -0,0 +1,258 @@ +--- +title: Solana signers +description: Use Solana signers with Wallet APIs +slug: wallets/solana/signers +--- + +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 = { + address: string; + signTransaction(input: { + transaction: Uint8Array; + }): Promise<{ signedTransaction: Uint8Array }>; +}; +``` + +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 +``` + +```shell pnpm +pnpm add @alchemy/wallet-apis +``` + +```shell yarn +yarn add @alchemy/wallet-apis +``` + +```shell bun +bun add @alchemy/wallet-apis +``` + + +Install the optional package that matches your signer source: + +* `@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` + +## Choose a signer source + +| What you have | What to use | +| --- | --- | +| 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` | + +## `@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", + paymaster: { policyId: "YOUR_SOLANA_POLICY_ID" }, +}); +``` + +## `@solana/kit` + +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 client = createSmartWalletClient({ + signer: fromKitSigner(kitSigner), + transport: alchemyWalletTransport({ apiKey: "YOUR_ALCHEMY_API_KEY" }), + chain: "solana:devnet", + paymaster: { policyId: "YOUR_SOLANA_POLICY_ID" }, +}); +``` + +## Raw Ed25519 keypair + +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. + +```ts title="keypair-signer.ts" +import { + alchemyWalletTransport, + createSmartWalletClient, +} from "@alchemy/wallet-apis"; +import { fromKeypair } from "@alchemy/wallet-apis/solana"; + +interface Ed25519Keypair { + address: string; + privateKey: CryptoKey; +} + +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" }, +}); +``` + +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` 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 { + alchemyWalletTransport, + createSmartWalletClient, +} from "@alchemy/wallet-apis"; +import { + fromWalletStandard, + 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; + }; +}; + +function isSolanaStandardWallet( + wallet: WalletStandardWallet, +): 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 + +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) diff --git a/content/wallets/pages/transactions/overview.mdx b/content/wallets/pages/transactions/overview.mdx index 57ed4f021..4381e68b0 100644 --- a/content/wallets/pages/transactions/overview.mdx +++ b/content/wallets/pages/transactions/overview.mdx @@ -25,13 +25,13 @@ 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 | +| [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 | | [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..3295c4a95 100644 --- a/content/wallets/pages/transactions/send-transactions/index.mdx +++ b/content/wallets/pages/transactions/send-transactions/index.mdx @@ -1,19 +1,22 @@ --- 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 +* 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 -## Implementation +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 @@ -26,7 +29,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 +46,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 +59,48 @@ The client defaults to using [EIP-7702](/docs/wallets/transactions/using-eip-770 +## Solana transactions + +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). + +By default, the client uses the signer's address. + +### 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 Build more: * [Sponsor gas for users](/docs/wallets/transactions/sponsor-gas) * [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/api.mdx b/content/wallets/pages/transactions/send-transactions/solana/api.mdx new file mode 100644 index 000000000..8733a3458 --- /dev/null +++ b/content/wallets/pages/transactions/send-transactions/solana/api.mdx @@ -0,0 +1,154 @@ +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. + + + + + +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 +``` + + + + + +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. Ask your Solana wallet to sign this transaction and return the +user's Ed25519 signature as base58. + +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()`. + +```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 +``` + + + + + +```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 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 new file mode 100644 index 000000000..67fa179d6 --- /dev/null +++ b/content/wallets/pages/transactions/send-transactions/solana/client.mdx @@ -0,0 +1,81 @@ +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, + } from "@alchemy/wallet-apis"; + import { fromKitSigner } from "@alchemy/wallet-apis/solana"; + import { createKeyPairSignerFromBytes } from "@solana/kit"; + + // 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: fromKitSigner(kitSigner), + transport: alchemyWalletTransport({ + apiKey: process.env.ALCHEMY_API_KEY!, + }), + chain: "solana:devnet", + }); + ``` + + ```ts title="utils.ts" + import { + getTransferSolInstructionDataEncoder, + SYSTEM_PROGRAM_ADDRESS, + } from "@solana-program/system"; + import { address } from "@solana/kit"; + import { bytesToHex } from "viem"; + + export function transferSolCall(args: { + from: string; + to: string; + lamports: bigint; + }) { + 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, + }; + } + ``` + + +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 new file mode 100644 index 000000000..a5c9c09af --- /dev/null +++ b/content/wallets/pages/transactions/send-transactions/solana/prepare-calls/client.mdx @@ -0,0 +1,82 @@ +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, + } from "@alchemy/wallet-apis"; + import { fromKitSigner } from "@alchemy/wallet-apis/solana"; + import { createKeyPairSignerFromBytes } from "@solana/kit"; + + const secretKeyBytes = new Uint8Array( + Buffer.from(process.env.SOLANA_SECRET_KEY!, "base64"), + ); + const kitSigner = await createKeyPairSignerFromBytes(secretKeyBytes); + + export const client = createSmartWalletClient({ + signer: fromKitSigner(kitSigner), + transport: alchemyWalletTransport({ + apiKey: process.env.ALCHEMY_API_KEY!, + }), + chain: "solana:devnet", + }); + ``` + + ```ts title="utils.ts" + import { + getTransferSolInstructionDataEncoder, + SYSTEM_PROGRAM_ADDRESS, + } from "@solana-program/system"; + import { address } from "@solana/kit"; + import { bytesToHex } from "viem"; + + export function transferSolCall(args: { + from: string; + to: string; + lamports: bigint; + }) { + 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, + }; + } + ``` + + +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. diff --git a/content/wallets/pages/transactions/sponsor-gas/index.mdx b/content/wallets/pages/transactions/sponsor-gas/index.mdx index ad8a9fa58..25815e5ec 100644 --- a/content/wallets/pages/transactions/sponsor-gas/index.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/index.mdx @@ -58,11 +58,17 @@ and bills in fiat. for reference to the `paymasterService.policyIds` parameter. + + Sponsoring Solana fees and rent uses Solana-specific policy configuration and + request options. See [Solana sponsorship](/docs/wallets/transactions/sponsor-gas/solana). + + ## Next steps Build more: * [Pay gas with any token](/docs/wallets/transactions/pay-gas-with-any-token) +* [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 4a2e44fc2..90cbfbfd4 100644 --- a/content/wallets/pages/transactions/sponsor-gas/overview.mdx +++ b/content/wallets/pages/transactions/sponsor-gas/overview.mdx @@ -44,7 +44,7 @@ Get started with the [gas sponsorship guide](/docs/wallets/transactions/sponsor- 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..092db575a --- /dev/null +++ b/content/wallets/pages/transactions/sponsor-gas/solana/api.mdx @@ -0,0 +1,162 @@ +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. + + + + + +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. + +```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. + +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()`. + +```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 +``` + + + + + +```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'" + ] +}' +``` + + + + + + + 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 new file mode 100644 index 000000000..09b59f7b1 --- /dev/null +++ b/content/wallets/pages/transactions/sponsor-gas/solana/client.mdx @@ -0,0 +1,81 @@ +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, + } from "@alchemy/wallet-apis"; + import { fromKitSigner } from "@alchemy/wallet-apis/solana"; + import { createKeyPairSignerFromBytes } from "@solana/kit"; + + export const config = { + policyId: process.env.SOLANA_POLICY_ID!, + }; + + const secretKeyBytes = new Uint8Array( + Buffer.from(process.env.SOLANA_SECRET_KEY!, "base64"), + ); + const kitSigner = await createKeyPairSignerFromBytes(secretKeyBytes); + + export const client = createSmartWalletClient({ + signer: fromKitSigner(kitSigner), + transport: alchemyWalletTransport({ + apiKey: process.env.ALCHEMY_API_KEY!, + }), + chain: "solana:devnet", + paymaster: { policyId: config.policyId }, + }); + ``` + + ```ts title="utils.ts" + import { + getTransferSolInstructionDataEncoder, + SYSTEM_PROGRAM_ADDRESS, + } from "@solana-program/system"; + import { address } from "@solana/kit"; + import { bytesToHex } from "viem"; + + export function transferSolCall(args: { + from: string; + to: string; + lamports: bigint; + }) { + 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 new file mode 100644 index 000000000..04f65593a --- /dev/null +++ b/content/wallets/pages/transactions/sponsor-gas/solana/index.mdx @@ -0,0 +1,69 @@ +--- +title: Solana sponsorship +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..531c0cda4 --- /dev/null +++ b/content/wallets/pages/transactions/sponsor-gas/solana/request-policy.mdx @@ -0,0 +1,21 @@ +```ts title="sponsor-solana-request.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, + }), + ], + capabilities: { + paymaster: { + policyId: "OTHER_SOLANA_POLICY_ID", + }, + }, +}); + +const result = await client.waitForCallsStatus({ id }); +```