@account-kit/privy-integration 4.71.1 → 4.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +97 -9
  2. package/dist/esm/Provider.d.ts +4 -2
  3. package/dist/esm/Provider.js +4 -2
  4. package/dist/esm/Provider.js.map +1 -1
  5. package/dist/esm/hooks/useAlchemySolanaTransaction.d.ts +163 -0
  6. package/dist/esm/hooks/useAlchemySolanaTransaction.js +233 -0
  7. package/dist/esm/hooks/useAlchemySolanaTransaction.js.map +1 -0
  8. package/dist/esm/index.d.ts +1 -0
  9. package/dist/esm/index.js +1 -0
  10. package/dist/esm/index.js.map +1 -1
  11. package/dist/esm/types.d.ts +5 -1
  12. package/dist/esm/types.js.map +1 -1
  13. package/dist/esm/util/createSolanaSponsoredTransaction.d.ts +12 -0
  14. package/dist/esm/util/createSolanaSponsoredTransaction.js +52 -0
  15. package/dist/esm/util/createSolanaSponsoredTransaction.js.map +1 -0
  16. package/dist/esm/util/createSolanaTransaction.d.ts +11 -0
  17. package/dist/esm/util/createSolanaTransaction.js +21 -0
  18. package/dist/esm/util/createSolanaTransaction.js.map +1 -0
  19. package/dist/esm/version.d.ts +1 -1
  20. package/dist/esm/version.js +1 -1
  21. package/dist/esm/version.js.map +1 -1
  22. package/dist/types/Provider.d.ts +4 -2
  23. package/dist/types/Provider.d.ts.map +1 -1
  24. package/dist/types/hooks/useAlchemySolanaTransaction.d.ts +164 -0
  25. package/dist/types/hooks/useAlchemySolanaTransaction.d.ts.map +1 -0
  26. package/dist/types/index.d.ts +1 -0
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/dist/types/types.d.ts +5 -1
  29. package/dist/types/types.d.ts.map +1 -1
  30. package/dist/types/util/createSolanaSponsoredTransaction.d.ts +13 -0
  31. package/dist/types/util/createSolanaSponsoredTransaction.d.ts.map +1 -0
  32. package/dist/types/util/createSolanaTransaction.d.ts +12 -0
  33. package/dist/types/util/createSolanaTransaction.d.ts.map +1 -0
  34. package/dist/types/version.d.ts +1 -1
  35. package/package.json +5 -5
  36. package/src/hooks/useAlchemySolanaTransaction.ts +402 -0
  37. package/src/index.ts +1 -0
  38. package/src/types.ts +7 -1
  39. package/src/util/createSolanaSponsoredTransaction.ts +74 -0
  40. package/src/util/createSolanaTransaction.ts +31 -0
  41. package/src/version.ts +1 -1
@@ -0,0 +1,402 @@
1
+ import { useCallback, useMemo, useState } from "react";
2
+ import {
3
+ Connection,
4
+ PublicKey,
5
+ SystemProgram,
6
+ Transaction,
7
+ TransactionInstruction,
8
+ VersionedTransaction,
9
+ } from "@solana/web3.js";
10
+ import { useAlchemyConfig } from "../Provider.js";
11
+ import { createSolanaSponsoredTransaction } from "../util/createSolanaSponsoredTransaction.js";
12
+ import { useSignTransaction, useWallets } from "@privy-io/react-auth/solana";
13
+ import { createSolanaTransaction } from "../util/createSolanaTransaction.js";
14
+
15
+ /**
16
+ * Type helper for values that can be synchronous or asynchronous
17
+ *
18
+ * @template T - The value type
19
+ */
20
+ export type PromiseOrValue<T> = T | Promise<T>;
21
+
22
+ /**
23
+ * Callback to modify a transaction before it's signed
24
+ * Useful for adding additional signatures or metadata
25
+ *
26
+ * @param transaction - The unsigned transaction to modify
27
+ * @returns The modified transaction
28
+ */
29
+ export type PreSend = (
30
+ this: void,
31
+ transaction: VersionedTransaction | Transaction,
32
+ ) => PromiseOrValue<VersionedTransaction | Transaction>;
33
+
34
+ /**
35
+ * Callback to transform instructions into a custom transaction
36
+ * Useful for advanced transaction construction (e.g., multi-sig, custom versioning)
37
+ *
38
+ * @param instructions - Array of Solana transaction instructions
39
+ * @returns Constructed transaction (legacy or versioned)
40
+ */
41
+ export type TransformInstruction = (
42
+ this: void,
43
+ instructions: TransactionInstruction[],
44
+ ) => PromiseOrValue<Transaction | VersionedTransaction>;
45
+
46
+ /**
47
+ * Optional transaction lifecycle hooks for advanced use cases
48
+ */
49
+ export type SolanaTransactionParamOptions = {
50
+ /** Hook called before signing the transaction */
51
+ preSend?: PreSend;
52
+ /** Custom transaction builder from instructions */
53
+ transformInstruction?: TransformInstruction;
54
+ };
55
+
56
+ /**
57
+ * Parameters for sending a Solana transaction
58
+ * Supports either a simple transfer or custom instructions
59
+ */
60
+ export type SolanaTransactionParams =
61
+ | {
62
+ /** Simple SOL transfer parameters */
63
+ transfer: {
64
+ /** Amount in lamports (accepts number or bigint) */
65
+ amount: number | bigint;
66
+ /** Recipient's base58-encoded address */
67
+ toAddress: string;
68
+ };
69
+ /** Optional transaction lifecycle hooks */
70
+ transactionComponents?: SolanaTransactionParamOptions;
71
+ /** Options for confirming the transaction on-chain */
72
+ confirmationOptions?: Parameters<Connection["confirmTransaction"]>[1];
73
+ }
74
+ | {
75
+ /** Custom Solana transaction instructions */
76
+ instructions: TransactionInstruction[];
77
+ /** Optional transaction lifecycle hooks */
78
+ transactionComponents?: SolanaTransactionParamOptions;
79
+ /** Options for confirming the transaction on-chain */
80
+ confirmationOptions?: Parameters<Connection["confirmTransaction"]>[1];
81
+ };
82
+
83
+ /**
84
+ * Result of a successful Solana transaction
85
+ */
86
+ export interface SolanaTransactionResult {
87
+ /** Base58-encoded transaction signature (hash) */
88
+ hash: string;
89
+ }
90
+
91
+ /**
92
+ * Configuration options for useAlchemySolanaTransaction hook
93
+ */
94
+ export interface UseAlchemySolanaTransactionOptions {
95
+ /** Solana RPC URL (overrides provider config) */
96
+ rpcUrl?: string;
97
+ /** Gas sponsorship policy ID (overrides provider config) */
98
+ policyId?: string | void;
99
+ /** Transaction confirmation options */
100
+ confirmationOptions?: Parameters<Connection["confirmTransaction"]>[1];
101
+ /** Specific wallet address to use (defaults to first available wallet) */
102
+ walletAddress?: string;
103
+ }
104
+
105
+ /**
106
+ * Return type of useAlchemySolanaTransaction hook
107
+ */
108
+ export interface UseAlchemySolanaTransactionResult {
109
+ /** Active Solana connection instance */
110
+ readonly connection: Connection | null;
111
+ /** Transaction result if successful */
112
+ readonly data: void | SolanaTransactionResult;
113
+ /** Whether a transaction is currently being sent */
114
+ readonly isPending: boolean;
115
+ /** Error if transaction failed */
116
+ readonly error: Error | null;
117
+ /** Reset hook state (clears error, data, isPending) */
118
+ reset(): void;
119
+ /** Send transaction (fire-and-forget, errors caught internally) */
120
+ sendTransaction(params: SolanaTransactionParams): void;
121
+ /** Send transaction and await result (throws on error) */
122
+ sendTransactionAsync(
123
+ params: SolanaTransactionParams,
124
+ ): Promise<SolanaTransactionResult>;
125
+ }
126
+
127
+ /**
128
+ * Hook to send Solana transactions with optional gas sponsorship via Alchemy
129
+ * Works with Privy's Solana wallet integration for signing transactions
130
+ * Supports both simple transfers and custom instruction sets
131
+ *
132
+ * @param {UseAlchemySolanaTransactionOptions} [opts] - Configuration options
133
+ * @param {string} [opts.rpcUrl] - Solana RPC URL (overrides provider config)
134
+ * @param {string} [opts.policyId] - Gas sponsorship policy ID (overrides provider config)
135
+ * @param {string} [opts.walletAddress] - Specific wallet address to use (defaults to first wallet)
136
+ * @param {Parameters<Connection["confirmTransaction"]>[1]} [opts.confirmationOptions] - Transaction confirmation options
137
+ * @returns {UseAlchemySolanaTransactionResult} Hook result with transaction functions and state
138
+ *
139
+ * @example Simple SOL transfer
140
+ * ```tsx
141
+ * const { sendTransactionAsync, isPending, error, data } = useAlchemySolanaTransaction({
142
+ * rpcUrl: 'https://solana-mainnet.g.alchemy.com/v2/your-api-key',
143
+ * policyId: 'your-policy-id', // Optional: for gas sponsorship
144
+ * });
145
+ *
146
+ * const handleTransfer = async () => {
147
+ * try {
148
+ * const result = await sendTransactionAsync({
149
+ * transfer: {
150
+ * amount: 1_000_000_000, // 1 SOL in lamports
151
+ * toAddress: 'recipient-address',
152
+ * },
153
+ * });
154
+ * console.log('Transaction hash:', result.hash);
155
+ * } catch (err) {
156
+ * console.error('Transaction failed:', err);
157
+ * }
158
+ * };
159
+ * ```
160
+ *
161
+ * @example Custom instructions
162
+ * ```tsx
163
+ * import { SystemProgram, PublicKey } from '@solana/web3.js';
164
+ *
165
+ * const { sendTransactionAsync } = useAlchemySolanaTransaction();
166
+ *
167
+ * // Build your custom instructions
168
+ * const transferIx = SystemProgram.transfer({
169
+ * fromPubkey: new PublicKey(walletAddress),
170
+ * toPubkey: new PublicKey(recipientAddress),
171
+ * lamports: 1_000_000,
172
+ * });
173
+ *
174
+ * // Pass instructions array to the hook
175
+ * const result = await sendTransactionAsync({
176
+ * instructions: [transferIx],
177
+ * });
178
+ * ```
179
+ *
180
+ * @example With provider configuration
181
+ * ```tsx
182
+ * // In your provider setup
183
+ * <AlchemyProvider
184
+ * solanaRpcUrl="https://solana-mainnet.g.alchemy.com/v2/..."
185
+ * solanaPolicyId="your-solana-policy-id"
186
+ * >
187
+ * <YourApp />
188
+ * </AlchemyProvider>
189
+ *
190
+ * // In your component - uses provider config automatically
191
+ * const { sendTransactionAsync } = useAlchemySolanaTransaction();
192
+ * ```
193
+ */
194
+ export function useAlchemySolanaTransaction(
195
+ opts: UseAlchemySolanaTransactionOptions = {},
196
+ ): UseAlchemySolanaTransactionResult {
197
+ const config = useAlchemyConfig();
198
+ const { wallets } = useWallets();
199
+ const { signTransaction } = useSignTransaction();
200
+
201
+ // Resolve the Privy Solana wallet to use
202
+ const embeddedWallet = useMemo(() => {
203
+ if (opts.walletAddress) {
204
+ const w = wallets.find((w) => w.address === opts.walletAddress);
205
+ if (!w) throw new Error("Specified Solana wallet not found");
206
+ return w;
207
+ }
208
+ return wallets[0];
209
+ }, [wallets, opts.walletAddress]);
210
+
211
+ // Build Solana connection from rpcUrl (hook override or provider default)
212
+ const connection = useMemo(() => {
213
+ const url = opts.rpcUrl || config.solanaRpcUrl;
214
+ return url ? new Connection(url) : null;
215
+ }, [opts.rpcUrl, config.solanaRpcUrl]);
216
+
217
+ const [isPending, setIsPending] = useState(false);
218
+ const [error, setError] = useState<Error | null>(null);
219
+ const [data, setData] = useState<void | SolanaTransactionResult>(undefined);
220
+
221
+ const resolvedPolicyId = useMemo(() => {
222
+ if (opts.policyId != null) return opts.policyId || undefined;
223
+ // Use solanaPolicyId from config, fallback to policyId for backwards compat
224
+ const configPolicy = config.solanaPolicyId || config.policyId;
225
+ return Array.isArray(configPolicy) ? configPolicy[0] : configPolicy;
226
+ }, [opts.policyId, config.solanaPolicyId, config.policyId]);
227
+
228
+ const mapTransformInstructions = useMemo(() => {
229
+ const addSponsorship: TransformInstruction = async (instructions) => {
230
+ const policyId = resolvedPolicyId;
231
+ if (!policyId)
232
+ throw new Error(
233
+ "Gas sponsorship requires a policyId (see provider or hook options).",
234
+ );
235
+ const localConnection = connection || missing("connection");
236
+ const fromAddress = embeddedWallet?.address;
237
+ if (!fromAddress) throw new Error("No embedded Solana wallet connected");
238
+ return createSolanaSponsoredTransaction(
239
+ instructions,
240
+ localConnection,
241
+ policyId,
242
+ fromAddress,
243
+ );
244
+ };
245
+ const createTx: TransformInstruction = async (instructions) => {
246
+ const localConnection = connection || missing("connection");
247
+ const fromAddress = embeddedWallet?.address;
248
+ if (!fromAddress) throw new Error("No embedded Solana wallet connected");
249
+ return createSolanaTransaction(
250
+ instructions,
251
+ localConnection,
252
+ fromAddress,
253
+ );
254
+ };
255
+ const defaultTransform =
256
+ !!resolvedPolicyId && !config.disableSponsorship
257
+ ? addSponsorship
258
+ : createTx;
259
+ return {
260
+ addSponsorship,
261
+ createTransaction: createTx,
262
+ default: defaultTransform,
263
+ } as const;
264
+ }, [
265
+ resolvedPolicyId,
266
+ config.disableSponsorship,
267
+ connection,
268
+ embeddedWallet?.address,
269
+ ]);
270
+
271
+ const buildInstructions = useCallback(
272
+ (
273
+ params: SolanaTransactionParams,
274
+ fromAddress: string,
275
+ ): TransactionInstruction[] => {
276
+ if ("instructions" in params) return params.instructions;
277
+ return [
278
+ SystemProgram.transfer({
279
+ fromPubkey: new PublicKey(fromAddress),
280
+ toPubkey: new PublicKey(params.transfer.toAddress),
281
+ lamports:
282
+ typeof params.transfer.amount === "bigint"
283
+ ? Number(params.transfer.amount) // web3.js currently expects number; callers can pass bigint safely
284
+ : params.transfer.amount,
285
+ }),
286
+ ];
287
+ },
288
+ [],
289
+ );
290
+
291
+ function toUnsignedBytes(tx: VersionedTransaction | Transaction): Uint8Array {
292
+ // Serialize the full transaction structure (with placeholder signatures)
293
+ // Privy expects the complete transaction format, not just the message
294
+ if (tx instanceof VersionedTransaction) {
295
+ // VersionedTransaction.serialize() includes signature slots
296
+ return tx.serialize();
297
+ }
298
+ // Legacy Transaction: serialize without requiring all signatures
299
+ const buf = tx.serialize({
300
+ requireAllSignatures: false,
301
+ verifySignatures: false,
302
+ });
303
+ return buf instanceof Uint8Array ? buf : new Uint8Array(buf);
304
+ }
305
+
306
+ const sendTransactionAsync = useCallback(
307
+ async (
308
+ params: SolanaTransactionParams,
309
+ ): Promise<SolanaTransactionResult> => {
310
+ setIsPending(true);
311
+ setError(null);
312
+ try {
313
+ const localConnection = connection || missing("connection");
314
+ if (!embeddedWallet?.address) {
315
+ throw new Error("No Solana wallet connected via Privy");
316
+ }
317
+
318
+ const fromAddress = embeddedWallet.address;
319
+ const {
320
+ transactionComponents: { preSend, transformInstruction } = {},
321
+ confirmationOptions,
322
+ } = params;
323
+
324
+ const instructions = buildInstructions(params, fromAddress);
325
+
326
+ let transaction: VersionedTransaction | Transaction;
327
+ if (transformInstruction) {
328
+ transaction = await transformInstruction(instructions);
329
+ } else {
330
+ transaction = await mapTransformInstructions.default(instructions);
331
+ }
332
+
333
+ transaction = (await preSend?.(transaction)) || transaction;
334
+
335
+ // Sign the transaction using Privy's useSignTransaction hook
336
+ const unsignedBytes = toUnsignedBytes(transaction);
337
+ const { signedTransaction } = await signTransaction({
338
+ transaction: unsignedBytes,
339
+ wallet: embeddedWallet,
340
+ });
341
+
342
+ // Broadcast the signed transaction
343
+ const hash = await localConnection.sendRawTransaction(
344
+ signedTransaction,
345
+ { skipPreflight: false },
346
+ );
347
+
348
+ if (confirmationOptions || opts.confirmationOptions) {
349
+ await localConnection.confirmTransaction(
350
+ hash,
351
+ confirmationOptions || opts.confirmationOptions,
352
+ );
353
+ }
354
+
355
+ setData({ hash });
356
+ return { hash };
357
+ } catch (err) {
358
+ const e = err instanceof Error ? err : new Error(String(err));
359
+ setError(e);
360
+ throw e;
361
+ } finally {
362
+ setIsPending(false);
363
+ }
364
+ },
365
+ [
366
+ embeddedWallet,
367
+ connection,
368
+ buildInstructions,
369
+ mapTransformInstructions,
370
+ opts.confirmationOptions,
371
+ signTransaction,
372
+ ],
373
+ );
374
+
375
+ const sendTransaction = useCallback(
376
+ (params: SolanaTransactionParams) => {
377
+ // Prevent unhandled rejection warnings; error state is already set in sendTransactionAsync
378
+ sendTransactionAsync(params).catch(() => {});
379
+ },
380
+ [sendTransactionAsync],
381
+ );
382
+
383
+ const reset = useCallback(() => {
384
+ setIsPending(false);
385
+ setError(null);
386
+ setData(undefined);
387
+ }, []);
388
+
389
+ return {
390
+ connection,
391
+ data,
392
+ isPending,
393
+ error,
394
+ reset,
395
+ sendTransaction,
396
+ sendTransactionAsync,
397
+ };
398
+ }
399
+
400
+ function missing(message: string): never {
401
+ throw new Error(message);
402
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export { useAlchemyClient } from "./hooks/useAlchemyClient.js";
6
6
  export { useAlchemySendTransaction } from "./hooks/useAlchemySendTransaction.js";
7
7
  export { useAlchemyPrepareSwap } from "./hooks/useAlchemyPrepareSwap.js";
8
8
  export { useAlchemySubmitSwap } from "./hooks/useAlchemySubmitSwap.js";
9
+ export { useAlchemySolanaTransaction } from "./hooks/useAlchemySolanaTransaction.js";
9
10
 
10
11
  // Types
11
12
  export type {
package/src/types.ts CHANGED
@@ -8,9 +8,15 @@ import type { z } from "zod";
8
8
  * Uses ConnectionConfigSchema to ensure valid transport configuration
9
9
  */
10
10
  export type AlchemyProviderConfig = z.infer<typeof ConnectionConfigSchema> & {
11
- /** Policy ID(s) for gas sponsorship */
11
+ /** Policy ID(s) for EVM gas sponsorship */
12
12
  policyId?: string | string[];
13
13
 
14
+ /** Policy ID(s) for Solana gas sponsorship */
15
+ solanaPolicyId?: string | string[];
16
+
17
+ /** Solana RPC URL (separate from EVM rpcUrl) */
18
+ solanaRpcUrl?: string;
19
+
14
20
  /**
15
21
  * Set to true to disable gas sponsorship by default
16
22
  * Default: false (sponsorship enabled when policyId is provided)
@@ -0,0 +1,74 @@
1
+ import type { TransactionInstruction } from "@solana/web3.js";
2
+ import {
3
+ Connection,
4
+ PublicKey,
5
+ TransactionMessage,
6
+ VersionedTransaction,
7
+ } from "@solana/web3.js";
8
+
9
+ /**
10
+ * This function wraps instructions in a sponsored transaction using Alchemy's fee payer service
11
+ *
12
+ * @param {TransactionInstruction[]} instructions - The instructions to add sponsorship to
13
+ * @param {Connection} connection - The connection to use
14
+ * @param {string} policyId - The policy id to use
15
+ * @param {string} address - The address to use
16
+ * @returns {Promise<VersionedTransaction>} - The sponsored transaction
17
+ */
18
+ export async function createSolanaSponsoredTransaction(
19
+ instructions: TransactionInstruction[],
20
+ connection: Connection,
21
+ policyId: string,
22
+ address: string,
23
+ ): Promise<VersionedTransaction> {
24
+ const { blockhash } = await connection.getLatestBlockhash({
25
+ commitment: "finalized",
26
+ });
27
+ const message = new TransactionMessage({
28
+ // Right now the backend will rewrite this payer Key to the server's address
29
+ payerKey: new PublicKey(address),
30
+ recentBlockhash: blockhash,
31
+ instructions,
32
+ }).compileToV0Message();
33
+ const versionedTransaction = new VersionedTransaction(message);
34
+ const serializedTransaction = Buffer.from(
35
+ versionedTransaction.serialize(),
36
+ ).toString("base64");
37
+ const body = JSON.stringify({
38
+ id:
39
+ crypto?.randomUUID() ??
40
+ `${Date.now()}-${Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)}`,
41
+ jsonrpc: "2.0",
42
+ method: "alchemy_requestFeePayer",
43
+ params: [
44
+ {
45
+ policyId,
46
+ serializedTransaction,
47
+ },
48
+ ],
49
+ });
50
+ const options = {
51
+ method: "POST",
52
+ headers: {
53
+ accept: "application/json",
54
+ "content-type": "application/json",
55
+ },
56
+ body,
57
+ };
58
+
59
+ const response = await fetch(connection.rpcEndpoint, options);
60
+ const jsonResponse = await response.json();
61
+ if (!jsonResponse?.result?.serializedTransaction)
62
+ throw new Error(
63
+ `Response doesn't include the serializedTransaction ${JSON.stringify(
64
+ jsonResponse,
65
+ )}`,
66
+ );
67
+ return VersionedTransaction.deserialize(
68
+ decodeBase64(jsonResponse.result.serializedTransaction),
69
+ );
70
+ }
71
+
72
+ function decodeBase64(serializedTransaction: string): Uint8Array {
73
+ return Buffer.from(serializedTransaction, "base64");
74
+ }
@@ -0,0 +1,31 @@
1
+ import type { TransactionInstruction } from "@solana/web3.js";
2
+ import {
3
+ Connection,
4
+ PublicKey,
5
+ TransactionMessage,
6
+ VersionedTransaction,
7
+ } from "@solana/web3.js";
8
+
9
+ /**
10
+ * Creates a regular (non-sponsored) Solana transaction from instructions
11
+ *
12
+ * @param {TransactionInstruction[]} instructions - The instructions to create transaction from
13
+ * @param {Connection} connection - The connection to use
14
+ * @param {string} address - The payer address
15
+ * @returns {Promise<VersionedTransaction>} - The transaction
16
+ */
17
+ export async function createSolanaTransaction(
18
+ instructions: TransactionInstruction[],
19
+ connection: Connection,
20
+ address: string,
21
+ ): Promise<VersionedTransaction> {
22
+ const { blockhash } = await connection.getLatestBlockhash({
23
+ commitment: "finalized",
24
+ });
25
+ const message = new TransactionMessage({
26
+ payerKey: new PublicKey(address),
27
+ recentBlockhash: blockhash,
28
+ instructions,
29
+ }).compileToV0Message();
30
+ return new VersionedTransaction(message);
31
+ }
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // This file is autogenerated by inject-version.ts. Any changes will be
2
2
  // overwritten on commit!
3
- export const VERSION = "4.71.1";
3
+ export const VERSION = "4.72.0";