@thru/passkey 0.2.22 → 0.2.23

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server/create-wallet.ts","../src/server/utils.ts","../src/server/challenge.ts","../src/server/submit.ts","../src/server/handlers.ts"],"sourcesContent":["import {\n PASSKEY_MANAGER_PROGRAM_ADDRESS,\n base64UrlToBytes,\n buildAccountContext,\n createCredentialLookupSeed,\n createWalletSeed,\n deriveCredentialLookupAddress,\n deriveWalletAddress,\n encodeCreateInstruction,\n encodeRegisterCredentialInstruction,\n} from '@thru/programs/passkey-manager';\nimport {\n toThruAddress,\n getStateProof,\n trackTransaction,\n withSerializedFeePayer,\n} from \"./utils\";\nimport type { ThruClient } from \"./types\";\n\nexport async function createPasskeyWallet(opts: {\n client: ThruClient;\n adminPublicKey: Uint8Array;\n adminPrivateKey: Uint8Array;\n adminAddress: string;\n pubkeyX: Uint8Array;\n pubkeyY: Uint8Array;\n credentialId?: string;\n walletName?: string;\n}): Promise<{ walletAddress: string; credentialLookupAddress?: string }> {\n const walletName = opts.walletName ?? \"default\";\n const seed = await createWalletSeed(walletName, opts.pubkeyX, opts.pubkeyY);\n const walletBytes = await deriveWalletAddress(\n seed,\n PASSKEY_MANAGER_PROGRAM_ADDRESS,\n );\n const walletAddress = toThruAddress(walletBytes);\n\n await withSerializedFeePayer(opts.adminPublicKey, async () => {\n let walletExists = false;\n try {\n await opts.client.accounts.get(walletAddress);\n walletExists = true;\n } catch {\n walletExists = false;\n }\n\n if (walletExists) return;\n\n const stateProof = await getStateProof(opts.client, walletAddress);\n const accountCtx = buildAccountContext({\n walletAddress,\n readWriteAccounts: [],\n readOnlyAccounts: [],\n feePayerAddress: opts.adminAddress,\n programAddress: PASSKEY_MANAGER_PROGRAM_ADDRESS,\n });\n\n const createIx = encodeCreateInstruction({\n walletAccountIdx: accountCtx.walletAccountIdx,\n authority: {\n tag: 1,\n pubkeyX: opts.pubkeyX,\n pubkeyY: opts.pubkeyY,\n },\n seed,\n stateProof,\n });\n\n const transaction = await opts.client.transactions.build({\n feePayer: { publicKey: opts.adminPublicKey },\n program: PASSKEY_MANAGER_PROGRAM_ADDRESS,\n instructionData: createIx,\n accounts: {\n readWrite: accountCtx.readWriteAddresses,\n readOnly: accountCtx.readOnlyAddresses,\n },\n header: { fee: 0n },\n });\n\n await transaction.sign(opts.adminPrivateKey);\n const signature = await opts.client.transactions.send(transaction.toWire());\n const result = await trackTransaction(opts.client, signature, 60000);\n if (result.status !== \"finalized\") {\n throw new Error(\n `Wallet creation failed with status: ${result.status}${\n result.errorCode !== undefined\n ? ` (error code: ${result.errorCode})`\n : \"\"\n }`,\n );\n }\n });\n\n let credentialLookupAddress: string | undefined;\n if (opts.credentialId) {\n const credentialIdBytes = base64UrlToBytes(opts.credentialId);\n const lookupAddressBytes = await deriveCredentialLookupAddress(\n credentialIdBytes,\n PASSKEY_MANAGER_PROGRAM_ADDRESS,\n );\n const lookupAddress = toThruAddress(lookupAddressBytes);\n\n credentialLookupAddress = lookupAddress;\n\n try {\n await withSerializedFeePayer(opts.adminPublicKey, async () => {\n let lookupExists = false;\n try {\n await opts.client.accounts.get(lookupAddress);\n lookupExists = true;\n } catch {\n lookupExists = false;\n }\n\n if (lookupExists) return;\n\n const credSeed = await createCredentialLookupSeed(credentialIdBytes);\n const stateProof = await getStateProof(opts.client, lookupAddress);\n const accountCtx = buildAccountContext({\n walletAddress,\n readWriteAccounts: [lookupAddressBytes],\n readOnlyAccounts: [],\n feePayerAddress: opts.adminAddress,\n programAddress: PASSKEY_MANAGER_PROGRAM_ADDRESS,\n });\n\n const registerIx = encodeRegisterCredentialInstruction({\n walletAccountIdx: accountCtx.walletAccountIdx,\n lookupAccountIdx: accountCtx.getAccountIndex(lookupAddressBytes),\n seed: credSeed,\n stateProof,\n });\n\n const transaction = await opts.client.transactions.build({\n feePayer: { publicKey: opts.adminPublicKey },\n program: PASSKEY_MANAGER_PROGRAM_ADDRESS,\n instructionData: registerIx,\n accounts: {\n readWrite: accountCtx.readWriteAddresses,\n readOnly: accountCtx.readOnlyAddresses,\n },\n header: { fee: 0n },\n });\n\n await transaction.sign(opts.adminPrivateKey);\n const signature = await opts.client.transactions.send(\n transaction.toWire(),\n );\n const result = await trackTransaction(opts.client, signature, 60000);\n if (result.status !== \"finalized\") {\n throw new Error(\n `Credential registration failed with status: ${result.status}${\n result.errorCode !== undefined\n ? ` (error code: ${result.errorCode})`\n : \"\"\n }`,\n );\n }\n });\n } catch (error) {\n console.warn(\"Credential registration failed (non-fatal):\", error);\n }\n }\n\n return {\n walletAddress,\n credentialLookupAddress,\n };\n}\n","import { encodeAddress } from '@thru/sdk/helpers';\nimport type { ThruClient, TransactionResult } from './types';\n\nconst feePayerQueueSymbol = Symbol.for('thru.sharedFeePayerQueues');\n\nfunction getFeePayerQueues(): Map<string, Promise<void>> {\n const globalQueues = globalThis as typeof globalThis & {\n [feePayerQueueSymbol]?: Map<string, Promise<void>>;\n };\n\n if (!globalQueues[feePayerQueueSymbol]) {\n globalQueues[feePayerQueueSymbol] = new Map<string, Promise<void>>();\n }\n\n return globalQueues[feePayerQueueSymbol];\n}\n\nexport async function getStateProof(\n client: ThruClient,\n address: string,\n proofType: number = 1,\n targetSlot?: bigint\n): Promise<Uint8Array> {\n const proofRequest: {\n address: string;\n proofType: number;\n targetSlot?: bigint;\n } = {\n address,\n proofType,\n };\n\n if (targetSlot !== undefined) {\n proofRequest.targetSlot = targetSlot;\n }\n\n const proof = await client.proofs.generate(proofRequest);\n\n if (!proof.proof || proof.proof.length === 0) {\n throw new Error(`No state proof returned for ${address}`);\n }\n\n return proof.proof;\n}\n\nexport async function trackTransaction(\n client: ThruClient,\n signature: string,\n timeoutMs: number = 5000\n): Promise<TransactionResult> {\n let finalizedSeen = false;\n\n try {\n for await (const update of client.transactions.track(signature, { timeoutMs })) {\n if (update.executionResult) {\n const vmError =\n update.executionResult.vmError !== undefined && update.executionResult.vmError !== null\n ? BigInt(update.executionResult.vmError)\n : 0n;\n const userErrorCode = update.executionResult.userErrorCode;\n const executionError =\n update.executionResult.executionResult !== undefined &&\n update.executionResult.executionResult !== null\n ? BigInt(update.executionResult.executionResult)\n : 0n;\n const success = vmError === 0n && executionError === 0n && userErrorCode === 0n;\n\n return {\n signature,\n status: success ? 'finalized' : 'failed',\n errorCode: vmError !== 0n ? vmError : executionError !== 0n ? executionError : userErrorCode,\n };\n }\n\n if (update.statusCode === 3) {\n finalizedSeen = true;\n }\n }\n\n if (finalizedSeen) {\n return {\n signature,\n status: 'finalized_without_execution',\n };\n }\n } catch {\n if (finalizedSeen) {\n return {\n signature,\n status: 'finalized_without_execution',\n };\n }\n\n return {\n signature,\n status: 'timeout',\n };\n }\n\n return {\n signature,\n status: 'timeout',\n };\n}\n\nexport function toThruAddress(bytes: Uint8Array): string {\n return encodeAddress(bytes);\n}\n\nexport async function withSerializedFeePayer<T>(\n feePayerPublicKey: Uint8Array,\n work: () => Promise<T>\n): Promise<T> {\n const queueKey = toThruAddress(feePayerPublicKey);\n const feePayerQueues = getFeePayerQueues();\n const previous = feePayerQueues.get(queueKey) ?? Promise.resolve();\n let release!: () => void;\n const current = new Promise<void>((resolve) => {\n release = resolve;\n });\n const tail = previous.then(() => current);\n feePayerQueues.set(queueKey, tail);\n\n await previous;\n\n try {\n return await work();\n } finally {\n release();\n if (feePayerQueues.get(queueKey) === tail) {\n feePayerQueues.delete(queueKey);\n }\n }\n}\n","import {\n bytesToBase64Url,\n createValidateChallenge,\n fetchWalletNonce,\n} from '@thru/programs/passkey-manager';\nimport type { AccountContext } from '@thru/programs/passkey-manager';\nimport type { PasskeyChallengeResult, ThruClient } from './types';\n\nexport async function createPasskeyChallenge(opts: {\n client: ThruClient;\n walletAddress: string;\n accountCtx: AccountContext;\n invokeIx: Uint8Array;\n}): Promise<PasskeyChallengeResult> {\n const nonce = await fetchWalletNonce(opts.client, opts.walletAddress);\n const challenge = await createValidateChallenge(\n nonce,\n opts.accountCtx.accountAddresses,\n opts.invokeIx\n );\n\n return {\n challenge: bytesToBase64Url(challenge),\n nonce: nonce.toString(),\n };\n}\n","import {\n PASSKEY_MANAGER_PROGRAM_ADDRESS,\n concatenateInstructions,\n encodeValidateInstruction,\n hexToBytes,\n} from '@thru/programs/passkey-manager';\nimport type { AccountContext } from '@thru/programs/passkey-manager';\nimport { trackTransaction, withSerializedFeePayer } from './utils';\nimport type {\n BuiltPasskeyTransaction,\n PasskeySignaturePayload,\n PasskeyTransactionHeaderOverrides,\n ThruClient,\n TransactionResult,\n} from './types';\n\nfunction base64ToBytes(base64: string): Uint8Array {\n type BufferLike = {\n from(value: string, encoding: 'base64'): Uint8Array;\n };\n const globalBuffer = (globalThis as { Buffer?: BufferLike }).Buffer;\n if (globalBuffer) {\n return globalBuffer.from(base64, 'base64');\n }\n\n if (typeof atob === 'function') {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n }\n\n throw new Error('Base64 decoding is not supported in this environment');\n}\n\n/**\n * Builds and signs a passkey-manager transaction without submitting it.\n *\n * Callers that override the transaction nonce are responsible for coordinating\n * fee-payer nonce allocation before calling this helper. Use\n * `submitPasskeyTransaction` for the serialized one-off submit path.\n */\nexport async function buildPasskeyTransaction(opts: {\n client: ThruClient;\n adminPublicKey: Uint8Array;\n adminPrivateKey: Uint8Array;\n walletAddress: string;\n accountCtx: AccountContext;\n invokeIx: Uint8Array;\n header?: PasskeyTransactionHeaderOverrides;\n} & PasskeySignaturePayload): Promise<BuiltPasskeyTransaction> {\n const validateIx = encodeValidateInstruction({\n walletAccountIdx: opts.accountCtx.walletAccountIdx,\n authIdx: 0,\n signatureR: hexToBytes(opts.signatureR),\n signatureS: hexToBytes(opts.signatureS),\n authenticatorData: base64ToBytes(opts.authenticatorData),\n clientDataJSON: base64ToBytes(opts.clientDataJSON),\n });\n\n const instructionData = concatenateInstructions([validateIx, opts.invokeIx]);\n const transaction = await opts.client.transactions.build({\n feePayer: { publicKey: opts.adminPublicKey },\n program: PASSKEY_MANAGER_PROGRAM_ADDRESS,\n instructionData,\n accounts: {\n readWrite: opts.accountCtx.readWriteAddresses,\n readOnly: opts.accountCtx.readOnlyAddresses,\n },\n header: {\n fee: 0n,\n ...opts.header,\n },\n });\n\n await transaction.sign(opts.adminPrivateKey);\n return {\n transaction,\n rawTransaction: transaction.toWire(),\n };\n}\n\nexport async function submitPasskeyTransaction(opts: {\n client: ThruClient;\n adminPublicKey: Uint8Array;\n adminPrivateKey: Uint8Array;\n walletAddress: string;\n accountCtx: AccountContext;\n invokeIx: Uint8Array;\n header?: PasskeyTransactionHeaderOverrides;\n} & PasskeySignaturePayload): Promise<TransactionResult> {\n return withSerializedFeePayer(opts.adminPublicKey, async () => {\n const { rawTransaction } = await buildPasskeyTransaction(opts);\n const signature = await opts.client.transactions.send(rawTransaction);\n return trackTransaction(opts.client, signature);\n });\n}\n","import type { PasskeyContextResult } from './types';\nimport { createPasskeyChallenge } from './challenge';\nimport { submitPasskeyTransaction } from './submit';\nimport type {\n PasskeyChallengeSubmitPayload,\n ThruClient,\n TransactionResult,\n} from './types';\n\nexport function createPasskeyHandlers<P>(opts: {\n buildContext: (params: P) => Promise<PasskeyContextResult>;\n adminPublicKey: Uint8Array;\n adminPrivateKey: Uint8Array;\n client: ThruClient;\n challengeTtlMs?: number;\n}) {\n const pendingContexts = new Map<\n string,\n { context: PasskeyContextResult; createdAt: number }\n >();\n const challengeTtlMs = opts.challengeTtlMs ?? 5 * 60_000;\n\n function createPendingContextKey(\n walletAddress: string,\n nonce: string,\n challenge: string\n ): string {\n return `${walletAddress}:${nonce}:${challenge}`;\n }\n\n function prunePendingContexts(now = Date.now()): void {\n for (const [nonce, entry] of pendingContexts.entries()) {\n if (now - entry.createdAt > challengeTtlMs) {\n pendingContexts.delete(nonce);\n }\n }\n }\n\n return {\n challenge: async (walletAddress: string, params: P) => {\n prunePendingContexts();\n\n const context = await opts.buildContext(params);\n const challenge = await createPasskeyChallenge({\n client: opts.client,\n walletAddress,\n accountCtx: context.accountCtx,\n invokeIx: context.invokeIx,\n });\n\n pendingContexts.set(\n createPendingContextKey(walletAddress, challenge.nonce, challenge.challenge),\n {\n context,\n createdAt: Date.now(),\n }\n );\n\n return challenge;\n },\n submit: async (\n walletAddress: string,\n params: P,\n payload: PasskeyChallengeSubmitPayload\n ): Promise<TransactionResult> => {\n void params;\n prunePendingContexts();\n\n const pendingKey = createPendingContextKey(\n walletAddress,\n payload.nonce,\n payload.challenge\n );\n const pending = pendingContexts.get(pendingKey);\n if (!pending) {\n throw new Error('Missing or expired challenge nonce');\n }\n\n pendingContexts.delete(pendingKey);\n const { nonce: _nonce, challenge: _challenge, ...signaturePayload } = payload;\n\n return submitPasskeyTransaction({\n client: opts.client,\n adminPublicKey: opts.adminPublicKey,\n adminPrivateKey: opts.adminPrivateKey,\n walletAddress,\n accountCtx: pending.context.accountCtx,\n invokeIx: pending.context.invokeIx,\n ...signaturePayload,\n });\n },\n };\n}\n"],"mappings":"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","names":["PASSKEY_MANAGER_PROGRAM_ADDRESS","PASSKEY_MANAGER_PROGRAM_ADDRESS"]}
1
+ {"version":3,"sources":["../src/server/create-wallet.ts","../src/server/utils.ts","../src/server/challenge.ts","../src/server/submit.ts","../src/server/handlers.ts"],"sourcesContent":["import {\n PASSKEY_MANAGER_PROGRAM_ADDRESS,\n base64UrlToBytes,\n buildAccountContext,\n createCredentialLookupSeed,\n createWalletSeed,\n deriveCredentialLookupAddress,\n deriveWalletAddress,\n encodeCreateInstruction,\n encodeRegisterCredentialInstruction,\n} from '@thru/programs/passkey-manager';\nimport {\n toThruAddress,\n getStateProof,\n trackTransaction,\n withSerializedFeePayer,\n} from \"./utils\";\nimport type { ThruClient } from \"./types\";\n\nexport async function createPasskeyWallet(opts: {\n client: ThruClient;\n adminPublicKey: Uint8Array;\n adminPrivateKey: Uint8Array;\n adminAddress: string;\n pubkeyX: Uint8Array;\n pubkeyY: Uint8Array;\n credentialId?: string;\n walletName?: string;\n}): Promise<{ walletAddress: string; credentialLookupAddress?: string }> {\n const walletName = opts.walletName ?? \"default\";\n const seed = await createWalletSeed(walletName, opts.pubkeyX, opts.pubkeyY);\n const walletBytes = await deriveWalletAddress(\n seed,\n PASSKEY_MANAGER_PROGRAM_ADDRESS,\n );\n const walletAddress = toThruAddress(walletBytes);\n\n await withSerializedFeePayer(opts.adminPublicKey, async () => {\n let walletExists = false;\n try {\n await opts.client.accounts.get(walletAddress);\n walletExists = true;\n } catch {\n walletExists = false;\n }\n\n if (walletExists) return;\n\n const stateProof = await getStateProof(opts.client, walletAddress);\n const accountCtx = buildAccountContext({\n walletAddress,\n readWriteAccounts: [],\n readOnlyAccounts: [],\n feePayerAddress: opts.adminAddress,\n programAddress: PASSKEY_MANAGER_PROGRAM_ADDRESS,\n });\n\n const createIx = encodeCreateInstruction({\n walletAccountIdx: accountCtx.walletAccountIdx,\n authority: {\n tag: 1,\n pubkeyX: opts.pubkeyX,\n pubkeyY: opts.pubkeyY,\n },\n seed,\n stateProof,\n });\n\n const transaction = await opts.client.transactions.build({\n feePayer: { publicKey: opts.adminPublicKey },\n program: PASSKEY_MANAGER_PROGRAM_ADDRESS,\n instructionData: createIx,\n accounts: {\n readWrite: accountCtx.readWriteAddresses,\n readOnly: accountCtx.readOnlyAddresses,\n },\n header: { fee: 0n },\n });\n\n await transaction.sign(opts.adminPrivateKey);\n const signature = await opts.client.transactions.send(transaction.toWire());\n const result = await trackTransaction(opts.client, signature, 60000);\n if (result.status !== \"finalized\") {\n throw new Error(\n `Wallet creation failed with status: ${result.status}${\n result.errorCode !== undefined\n ? ` (error code: ${result.errorCode})`\n : \"\"\n }`,\n );\n }\n });\n\n let credentialLookupAddress: string | undefined;\n if (opts.credentialId) {\n const credentialIdBytes = base64UrlToBytes(opts.credentialId);\n const lookupAddressBytes = await deriveCredentialLookupAddress(\n credentialIdBytes,\n PASSKEY_MANAGER_PROGRAM_ADDRESS,\n );\n const lookupAddress = toThruAddress(lookupAddressBytes);\n\n credentialLookupAddress = lookupAddress;\n\n try {\n await withSerializedFeePayer(opts.adminPublicKey, async () => {\n let lookupExists = false;\n try {\n await opts.client.accounts.get(lookupAddress);\n lookupExists = true;\n } catch {\n lookupExists = false;\n }\n\n if (lookupExists) return;\n\n const credSeed = await createCredentialLookupSeed(credentialIdBytes);\n const stateProof = await getStateProof(opts.client, lookupAddress);\n const accountCtx = buildAccountContext({\n walletAddress,\n readWriteAccounts: [lookupAddressBytes],\n readOnlyAccounts: [],\n feePayerAddress: opts.adminAddress,\n programAddress: PASSKEY_MANAGER_PROGRAM_ADDRESS,\n });\n\n const registerIx = encodeRegisterCredentialInstruction({\n walletAccountIdx: accountCtx.walletAccountIdx,\n lookupAccountIdx: accountCtx.getAccountIndex(lookupAddressBytes),\n seed: credSeed,\n stateProof,\n });\n\n const transaction = await opts.client.transactions.build({\n feePayer: { publicKey: opts.adminPublicKey },\n program: PASSKEY_MANAGER_PROGRAM_ADDRESS,\n instructionData: registerIx,\n accounts: {\n readWrite: accountCtx.readWriteAddresses,\n readOnly: accountCtx.readOnlyAddresses,\n },\n header: { fee: 0n },\n });\n\n await transaction.sign(opts.adminPrivateKey);\n const signature = await opts.client.transactions.send(\n transaction.toWire(),\n );\n const result = await trackTransaction(opts.client, signature, 60000);\n if (result.status !== \"finalized\") {\n throw new Error(\n `Credential registration failed with status: ${result.status}${\n result.errorCode !== undefined\n ? ` (error code: ${result.errorCode})`\n : \"\"\n }`,\n );\n }\n });\n } catch (error) {\n console.warn(\"Credential registration failed (non-fatal):\", error);\n }\n }\n\n return {\n walletAddress,\n credentialLookupAddress,\n };\n}\n","import { encodeAddress } from '@thru/sdk/helpers';\nimport type { ThruClient, TransactionResult } from './types';\n\nconst feePayerQueueSymbol = Symbol.for('thru.sharedFeePayerQueues');\n\nfunction getFeePayerQueues(): Map<string, Promise<void>> {\n const globalQueues = globalThis as typeof globalThis & {\n [feePayerQueueSymbol]?: Map<string, Promise<void>>;\n };\n\n if (!globalQueues[feePayerQueueSymbol]) {\n globalQueues[feePayerQueueSymbol] = new Map<string, Promise<void>>();\n }\n\n return globalQueues[feePayerQueueSymbol];\n}\n\nexport async function getStateProof(\n client: ThruClient,\n address: string,\n proofType: number = 1,\n targetSlot?: bigint\n): Promise<Uint8Array> {\n const proofRequest: {\n address: string;\n proofType: number;\n targetSlot?: bigint;\n } = {\n address,\n proofType,\n };\n\n if (targetSlot !== undefined) {\n proofRequest.targetSlot = targetSlot;\n }\n\n const proof = await client.proofs.generate(proofRequest);\n\n if (!proof.proof || proof.proof.length === 0) {\n throw new Error(`No state proof returned for ${address}`);\n }\n\n return proof.proof;\n}\n\nexport async function trackTransaction(\n client: ThruClient,\n signature: string,\n timeoutMs: number = 5000\n): Promise<TransactionResult> {\n let finalizedSeen = false;\n\n try {\n for await (const update of client.transactions.track(signature, { timeoutMs })) {\n if (update.executionResult) {\n const vmError =\n update.executionResult.vmError !== undefined && update.executionResult.vmError !== null\n ? BigInt(update.executionResult.vmError)\n : 0n;\n const userErrorCode = update.executionResult.userErrorCode;\n const executionError =\n update.executionResult.executionResult !== undefined &&\n update.executionResult.executionResult !== null\n ? BigInt(update.executionResult.executionResult)\n : 0n;\n const success = vmError === 0n && executionError === 0n && userErrorCode === 0n;\n\n return {\n signature,\n status: success ? 'finalized' : 'failed',\n errorCode: vmError !== 0n ? vmError : executionError !== 0n ? executionError : userErrorCode,\n };\n }\n\n if (update.statusCode === 3) {\n finalizedSeen = true;\n }\n }\n\n if (finalizedSeen) {\n return {\n signature,\n status: 'finalized_without_execution',\n };\n }\n } catch {\n if (finalizedSeen) {\n return {\n signature,\n status: 'finalized_without_execution',\n };\n }\n\n return {\n signature,\n status: 'timeout',\n };\n }\n\n return {\n signature,\n status: 'timeout',\n };\n}\n\nexport function toThruAddress(bytes: Uint8Array): string {\n return encodeAddress(bytes);\n}\n\nexport async function withSerializedFeePayer<T>(\n feePayerPublicKey: Uint8Array,\n work: () => Promise<T>\n): Promise<T> {\n const queueKey = toThruAddress(feePayerPublicKey);\n const feePayerQueues = getFeePayerQueues();\n const previous = feePayerQueues.get(queueKey) ?? Promise.resolve();\n let release!: () => void;\n const current = new Promise<void>((resolve) => {\n release = resolve;\n });\n const tail = previous.then(() => current);\n feePayerQueues.set(queueKey, tail);\n\n await previous;\n\n try {\n return await work();\n } finally {\n release();\n if (feePayerQueues.get(queueKey) === tail) {\n feePayerQueues.delete(queueKey);\n }\n }\n}\n","import {\n bytesToBase64Url,\n createValidateChallenge,\n decodeAddress,\n fetchWalletNonce,\n} from '@thru/programs/passkey-manager';\nimport type { AccountContext } from '@thru/programs/passkey-manager';\nimport type { PasskeyChallengeResult, ThruClient } from './types';\n\nexport async function createPasskeyChallenge(opts: {\n client: ThruClient;\n walletAddress: string;\n accountCtx: AccountContext;\n targetProgramAddress: string;\n instructionData: Uint8Array;\n authIdx?: number;\n}): Promise<PasskeyChallengeResult> {\n const nonce = await fetchWalletNonce(opts.client, opts.walletAddress);\n const targetProgramIdx = opts.accountCtx.getAccountIndex(\n decodeAddress(opts.targetProgramAddress)\n );\n const challenge = await createValidateChallenge(\n nonce,\n opts.accountCtx.accountAddresses,\n opts.accountCtx.walletAccountIdx,\n opts.authIdx ?? 0,\n {\n programIdx: targetProgramIdx,\n instructionData: opts.instructionData,\n }\n );\n\n return {\n challenge: bytesToBase64Url(challenge),\n nonce: nonce.toString(),\n };\n}\n","import {\n PASSKEY_MANAGER_PROGRAM_ADDRESS,\n decodeAddress,\n encodeValidateInstruction,\n hexToBytes,\n} from '@thru/programs/passkey-manager';\nimport type { AccountContext } from '@thru/programs/passkey-manager';\nimport { trackTransaction, withSerializedFeePayer } from './utils';\nimport type {\n BuiltPasskeyTransaction,\n PasskeySignaturePayload,\n PasskeyTransactionHeaderOverrides,\n ThruClient,\n TransactionResult,\n} from './types';\n\nfunction base64ToBytes(base64: string): Uint8Array {\n type BufferLike = {\n from(value: string, encoding: 'base64'): Uint8Array;\n };\n const globalBuffer = (globalThis as { Buffer?: BufferLike }).Buffer;\n if (globalBuffer) {\n return globalBuffer.from(base64, 'base64');\n }\n\n if (typeof atob === 'function') {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n }\n\n throw new Error('Base64 decoding is not supported in this environment');\n}\n\n/**\n * Builds and signs a passkey-manager transaction without submitting it.\n *\n * Callers that override the transaction nonce are responsible for coordinating\n * fee-payer nonce allocation before calling this helper. Use\n * `submitPasskeyTransaction` for the serialized one-off submit path.\n */\nexport async function buildPasskeyTransaction(opts: {\n client: ThruClient;\n adminPublicKey: Uint8Array;\n adminPrivateKey: Uint8Array;\n walletAddress: string;\n accountCtx: AccountContext;\n targetProgramAddress: string;\n instructionData: Uint8Array;\n authIdx?: number;\n header?: PasskeyTransactionHeaderOverrides;\n} & PasskeySignaturePayload): Promise<BuiltPasskeyTransaction> {\n const targetProgramIdx = opts.accountCtx.getAccountIndex(\n decodeAddress(opts.targetProgramAddress)\n );\n const validateIx = encodeValidateInstruction({\n walletAccountIdx: opts.accountCtx.walletAccountIdx,\n authIdx: opts.authIdx ?? 0,\n targetInstruction: {\n programIdx: targetProgramIdx,\n instructionData: opts.instructionData,\n },\n signatureR: hexToBytes(opts.signatureR),\n signatureS: hexToBytes(opts.signatureS),\n authenticatorData: base64ToBytes(opts.authenticatorData),\n clientDataJSON: base64ToBytes(opts.clientDataJSON),\n });\n\n const transaction = await opts.client.transactions.build({\n feePayer: { publicKey: opts.adminPublicKey },\n program: PASSKEY_MANAGER_PROGRAM_ADDRESS,\n instructionData: validateIx,\n accounts: {\n readWrite: opts.accountCtx.readWriteAddresses,\n readOnly: opts.accountCtx.readOnlyAddresses,\n },\n header: {\n fee: 0n,\n ...opts.header,\n },\n });\n\n await transaction.sign(opts.adminPrivateKey);\n return {\n transaction,\n rawTransaction: transaction.toWire(),\n };\n}\n\nexport async function submitPasskeyTransaction(opts: {\n client: ThruClient;\n adminPublicKey: Uint8Array;\n adminPrivateKey: Uint8Array;\n walletAddress: string;\n accountCtx: AccountContext;\n targetProgramAddress: string;\n instructionData: Uint8Array;\n authIdx?: number;\n header?: PasskeyTransactionHeaderOverrides;\n} & PasskeySignaturePayload): Promise<TransactionResult> {\n return withSerializedFeePayer(opts.adminPublicKey, async () => {\n const { rawTransaction } = await buildPasskeyTransaction(opts);\n const signature = await opts.client.transactions.send(rawTransaction);\n return trackTransaction(opts.client, signature);\n });\n}\n","import type { PasskeyContextResult } from './types';\nimport { createPasskeyChallenge } from './challenge';\nimport { submitPasskeyTransaction } from './submit';\nimport type {\n PasskeyChallengeSubmitPayload,\n ThruClient,\n TransactionResult,\n} from './types';\n\nexport function createPasskeyHandlers<P>(opts: {\n buildContext: (params: P) => Promise<PasskeyContextResult>;\n adminPublicKey: Uint8Array;\n adminPrivateKey: Uint8Array;\n client: ThruClient;\n challengeTtlMs?: number;\n}) {\n const pendingContexts = new Map<\n string,\n { context: PasskeyContextResult; createdAt: number }\n >();\n const challengeTtlMs = opts.challengeTtlMs ?? 5 * 60_000;\n\n function createPendingContextKey(\n walletAddress: string,\n nonce: string,\n challenge: string\n ): string {\n return `${walletAddress}:${nonce}:${challenge}`;\n }\n\n function prunePendingContexts(now = Date.now()): void {\n for (const [nonce, entry] of pendingContexts.entries()) {\n if (now - entry.createdAt > challengeTtlMs) {\n pendingContexts.delete(nonce);\n }\n }\n }\n\n return {\n challenge: async (walletAddress: string, params: P) => {\n prunePendingContexts();\n\n const context = await opts.buildContext(params);\n const challenge = await createPasskeyChallenge({\n client: opts.client,\n walletAddress,\n accountCtx: context.accountCtx,\n targetProgramAddress: context.targetProgramAddress,\n instructionData: context.instructionData,\n authIdx: context.authIdx,\n });\n\n pendingContexts.set(\n createPendingContextKey(walletAddress, challenge.nonce, challenge.challenge),\n {\n context,\n createdAt: Date.now(),\n }\n );\n\n return challenge;\n },\n submit: async (\n walletAddress: string,\n params: P,\n payload: PasskeyChallengeSubmitPayload\n ): Promise<TransactionResult> => {\n void params;\n prunePendingContexts();\n\n const pendingKey = createPendingContextKey(\n walletAddress,\n payload.nonce,\n payload.challenge\n );\n const pending = pendingContexts.get(pendingKey);\n if (!pending) {\n throw new Error('Missing or expired challenge nonce');\n }\n\n pendingContexts.delete(pendingKey);\n const { nonce: _nonce, challenge: _challenge, ...signaturePayload } = payload;\n\n return submitPasskeyTransaction({\n client: opts.client,\n adminPublicKey: opts.adminPublicKey,\n adminPrivateKey: opts.adminPrivateKey,\n walletAddress,\n accountCtx: pending.context.accountCtx,\n targetProgramAddress: pending.context.targetProgramAddress,\n instructionData: pending.context.instructionData,\n authIdx: pending.context.authIdx,\n ...signaturePayload,\n });\n },\n };\n}\n"],"mappings":"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","names":["PASSKEY_MANAGER_PROGRAM_ADDRESS","decodeAddress","decodeAddress","PASSKEY_MANAGER_PROGRAM_ADDRESS"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thru/passkey",
3
- "version": "0.2.22",
3
+ "version": "0.2.23",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -43,8 +43,8 @@
43
43
  }
44
44
  },
45
45
  "dependencies": {
46
- "@thru/programs": "0.2.22",
47
- "@thru/sdk": "0.2.22"
46
+ "@thru/sdk": "0.2.23",
47
+ "@thru/programs": "0.2.23"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "expo-secure-store": "*",
@@ -4,7 +4,8 @@
4
4
  step can reuse the exact same flow.
5
5
 
6
6
  Builds the on-chain transaction:
7
- VALIDATE(existingAuthority) + ADD_AUTHORITY(newPasskey) [+ REGISTER_CREDENTIAL]
7
+ VALIDATE(existingAuthority -> ADD_AUTHORITY(newPasskey))
8
+ or VALIDATE(existingAuthority -> multicall[ADD_AUTHORITY, REGISTER_CREDENTIAL])
8
9
  asks the caller's existing passkey to sign the challenge, then asks
9
10
  the caller's wallet signer to sign the assembled transaction, sends
10
11
  it, and returns the result. */
@@ -12,9 +13,9 @@
12
13
  import {
13
14
  type Authority,
14
15
  buildAccountContext,
15
- concatenateInstructions,
16
16
  createValidateChallenge,
17
17
  createCredentialLookupSeed,
18
+ decodeAddress,
18
19
  deriveWalletAddress,
19
20
  encodeAddAuthorityInstruction,
20
21
  encodeRegisterCredentialInstruction,
@@ -23,6 +24,10 @@ import {
23
24
  type ParsedAuthority,
24
25
  type WalletSigner,
25
26
  } from "@thru/programs/passkey-manager";
27
+ import {
28
+ MULTICALL_PROGRAM_PUBKEY,
29
+ buildMulticallInstruction,
30
+ } from "@thru/programs/multicall";
26
31
 
27
32
  /** Minimal shape required from a passkey signer. Both web's
28
33
  `signWithDiscoverablePasskey`/`signWithPasskey` and mobile's
@@ -125,17 +130,14 @@ export async function addDeviceToAccount(
125
130
  /* The new authority will land at the next free slot. */
126
131
  const newAuthorityIdx = parsed.authorities.length;
127
132
 
128
- /* Build the instruction sequence after the VALIDATE. */
129
- const addAuthorityInstruction = encodeAddAuthorityInstruction({
130
- authority: params.newAuthority,
131
- });
132
-
133
- let trailingInstructionData = addAuthorityInstruction;
134
133
  let readWriteAccounts: Uint8Array[] = [];
134
+ let lookupSeed: Uint8Array | undefined;
135
+ let lookupAddressBytes: Uint8Array | undefined;
136
+ let lookupProof: Uint8Array | undefined;
135
137
 
136
138
  if (params.credentialId) {
137
- const lookupSeed = await createCredentialLookupSeed(params.credentialId);
138
- const lookupAddressBytes = await deriveWalletAddress(
139
+ lookupSeed = await createCredentialLookupSeed(params.credentialId);
140
+ lookupAddressBytes = await deriveWalletAddress(
139
141
  lookupSeed,
140
142
  params.programAddress,
141
143
  );
@@ -145,39 +147,61 @@ export async function addDeviceToAccount(
145
147
  proofType: 1 /* StateProofType.CREATING */,
146
148
  address: lookupAddressBytes,
147
149
  });
150
+ lookupProof = proofResult.proof;
151
+ readWriteAccounts = [lookupAddressBytes];
152
+ }
148
153
 
149
- const ctx = buildAccountContext({
150
- walletAddress: params.walletAddress,
151
- readWriteAccounts: [lookupAddressBytes],
152
- readOnlyAccounts: [],
153
- });
154
+ const ctx = buildAccountContext({
155
+ walletAddress: params.walletAddress,
156
+ readWriteAccounts,
157
+ readOnlyAccounts: params.credentialId ? [MULTICALL_PROGRAM_PUBKEY] : [],
158
+ programAddress: params.programAddress,
159
+ });
160
+
161
+ const passkeyProgramPubkey = decodeAddress(params.programAddress);
162
+ const addAuthorityInstruction = encodeAddAuthorityInstruction({
163
+ walletAccountIdx: ctx.walletAccountIdx,
164
+ authority: params.newAuthority,
165
+ });
166
+
167
+ let targetProgramIdx = ctx.getAccountIndex(passkeyProgramPubkey);
168
+ let targetInstructionData = addAuthorityInstruction;
169
+
170
+ if (params.credentialId) {
171
+ if (!lookupSeed || !lookupAddressBytes || !lookupProof) {
172
+ throw new Error("Credential lookup proof data missing");
173
+ }
154
174
 
155
175
  const registerCredentialInstruction = encodeRegisterCredentialInstruction({
156
176
  walletAccountIdx: ctx.walletAccountIdx,
157
177
  lookupAccountIdx: ctx.getAccountIndex(lookupAddressBytes),
158
178
  seed: lookupSeed,
159
- stateProof: proofResult.proof,
179
+ stateProof: lookupProof,
160
180
  });
161
181
 
162
- trailingInstructionData = concatenateInstructions([
163
- addAuthorityInstruction,
164
- registerCredentialInstruction,
182
+ targetProgramIdx = ctx.getAccountIndex(MULTICALL_PROGRAM_PUBKEY);
183
+ targetInstructionData = buildMulticallInstruction([
184
+ {
185
+ programIdx: ctx.getAccountIndex(passkeyProgramPubkey),
186
+ instructionData: addAuthorityInstruction,
187
+ },
188
+ {
189
+ programIdx: ctx.getAccountIndex(passkeyProgramPubkey),
190
+ instructionData: registerCredentialInstruction,
191
+ },
165
192
  ]);
166
- readWriteAccounts = [lookupAddressBytes];
167
193
  }
168
194
 
169
- /* Build the VALIDATE challenge over (nonce, account_addresses,
170
- trailing_instruction_data) and ask the caller's passkey to sign. */
171
- const ctx = buildAccountContext({
172
- walletAddress: params.walletAddress,
173
- readWriteAccounts,
174
- readOnlyAccounts: [],
175
- });
176
-
195
+ /* Build the VALIDATE challenge over the target CPI and ask the caller's passkey to sign. */
177
196
  const challenge = await createValidateChallenge(
178
197
  parsed.nonce,
179
198
  ctx.accountAddresses,
180
- trailingInstructionData,
199
+ ctx.walletAccountIdx,
200
+ params.authIdx,
201
+ {
202
+ programIdx: targetProgramIdx,
203
+ instructionData: targetInstructionData,
204
+ },
181
205
  );
182
206
 
183
207
  status("Waiting for passkey approval...");
@@ -186,27 +210,26 @@ export async function addDeviceToAccount(
186
210
  const validateInstruction = encodeValidateInstruction({
187
211
  walletAccountIdx: ctx.walletAccountIdx,
188
212
  authIdx: params.authIdx,
213
+ targetInstruction: {
214
+ programIdx: targetProgramIdx,
215
+ instructionData: targetInstructionData,
216
+ },
189
217
  signatureR: signature.signatureR,
190
218
  signatureS: signature.signatureS,
191
219
  authenticatorData: signature.authenticatorData,
192
220
  clientDataJSON: signature.clientDataJSON,
193
221
  });
194
222
 
195
- const instructionData = concatenateInstructions([
196
- validateInstruction,
197
- trailingInstructionData,
198
- ]);
199
-
200
223
  status("Sending transaction...");
201
224
  const result = await params.executor({
202
225
  thru: params.thru,
203
226
  walletSigner: params.walletSigner,
204
- instructionData,
227
+ instructionData: validateInstruction,
205
228
  readWriteAddresses: ctx.readWriteAddresses,
206
229
  readOnlyAddresses: ctx.readOnlyAddresses,
207
230
  label: params.credentialId
208
- ? "VALIDATE + ADD_AUTHORITY + REGISTER_CREDENTIAL"
209
- : "VALIDATE + ADD_AUTHORITY",
231
+ ? "VALIDATE -> MULTICALL(ADD_AUTHORITY, REGISTER_CREDENTIAL)"
232
+ : "VALIDATE -> ADD_AUTHORITY",
210
233
  });
211
234
 
212
235
  return { ...result, newAuthorityIdx };
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  bytesToBase64Url,
3
3
  createValidateChallenge,
4
+ decodeAddress,
4
5
  fetchWalletNonce,
5
6
  } from '@thru/programs/passkey-manager';
6
7
  import type { AccountContext } from '@thru/programs/passkey-manager';
@@ -10,13 +11,23 @@ export async function createPasskeyChallenge(opts: {
10
11
  client: ThruClient;
11
12
  walletAddress: string;
12
13
  accountCtx: AccountContext;
13
- invokeIx: Uint8Array;
14
+ targetProgramAddress: string;
15
+ instructionData: Uint8Array;
16
+ authIdx?: number;
14
17
  }): Promise<PasskeyChallengeResult> {
15
18
  const nonce = await fetchWalletNonce(opts.client, opts.walletAddress);
19
+ const targetProgramIdx = opts.accountCtx.getAccountIndex(
20
+ decodeAddress(opts.targetProgramAddress)
21
+ );
16
22
  const challenge = await createValidateChallenge(
17
23
  nonce,
18
24
  opts.accountCtx.accountAddresses,
19
- opts.invokeIx
25
+ opts.accountCtx.walletAccountIdx,
26
+ opts.authIdx ?? 0,
27
+ {
28
+ programIdx: targetProgramIdx,
29
+ instructionData: opts.instructionData,
30
+ }
20
31
  );
21
32
 
22
33
  return {
@@ -45,7 +45,9 @@ export function createPasskeyHandlers<P>(opts: {
45
45
  client: opts.client,
46
46
  walletAddress,
47
47
  accountCtx: context.accountCtx,
48
- invokeIx: context.invokeIx,
48
+ targetProgramAddress: context.targetProgramAddress,
49
+ instructionData: context.instructionData,
50
+ authIdx: context.authIdx,
49
51
  });
50
52
 
51
53
  pendingContexts.set(
@@ -85,7 +87,9 @@ export function createPasskeyHandlers<P>(opts: {
85
87
  adminPrivateKey: opts.adminPrivateKey,
86
88
  walletAddress,
87
89
  accountCtx: pending.context.accountCtx,
88
- invokeIx: pending.context.invokeIx,
90
+ targetProgramAddress: pending.context.targetProgramAddress,
91
+ instructionData: pending.context.instructionData,
92
+ authIdx: pending.context.authIdx,
89
93
  ...signaturePayload,
90
94
  });
91
95
  },
@@ -2,19 +2,23 @@ import { describe, expect, it, vi } from 'vitest';
2
2
  import type { AccountContext } from '@thru/programs/passkey-manager';
3
3
  import type { ThruClient } from './types';
4
4
 
5
- vi.mock('@thru/programs/passkey-manager', () => ({
5
+ const passkeyManagerMocks = vi.hoisted(() => ({
6
6
  PASSKEY_MANAGER_PROGRAM_ADDRESS: 'passkey-program',
7
- concatenateInstructions: (instructions: Uint8Array[]) => new Uint8Array(instructions.flatMap((ix) => Array.from(ix))),
8
- encodeValidateInstruction: () => new Uint8Array([1, 2]),
7
+ decodeAddress: vi.fn(() => new Uint8Array(32).fill(7)),
8
+ encodeValidateInstruction: vi.fn(() => new Uint8Array([1, 2])),
9
9
  hexToBytes: (value: string) => new Uint8Array(Buffer.from(value, 'hex')),
10
10
  }));
11
11
 
12
+ vi.mock('@thru/programs/passkey-manager', () => passkeyManagerMocks);
13
+
12
14
  import { buildPasskeyTransaction, submitPasskeyTransaction } from './submit';
13
15
 
14
16
  const accountCtx = {
15
17
  walletAccountIdx: 3,
18
+ accountAddresses: ['fee-payer', 'passkey-program', 'wallet-address', 'readonly-address'],
16
19
  readWriteAddresses: ['lookup-address', 'wallet-address'],
17
20
  readOnlyAddresses: ['readonly-address'],
21
+ getAccountIndex: vi.fn(() => 4),
18
22
  } as AccountContext;
19
23
 
20
24
  const signaturePayload = {
@@ -58,7 +62,8 @@ describe('passkey submit', () => {
58
62
  adminPrivateKey: new Uint8Array(32).fill(2),
59
63
  walletAddress: 'wallet-address',
60
64
  accountCtx,
61
- invokeIx: new Uint8Array([3, 4]),
65
+ targetProgramAddress: 'target-program',
66
+ instructionData: new Uint8Array([3, 4]),
62
67
  header: {
63
68
  fee: 0n,
64
69
  nonce: 42n,
@@ -76,7 +81,19 @@ describe('passkey submit', () => {
76
81
  fee: 0n,
77
82
  nonce: 42n,
78
83
  },
84
+ instructionData: new Uint8Array([1, 2]),
79
85
  }));
86
+ expect(passkeyManagerMocks.decodeAddress).toHaveBeenCalledWith('target-program');
87
+ expect(passkeyManagerMocks.encodeValidateInstruction).toHaveBeenCalledWith(
88
+ expect.objectContaining({
89
+ walletAccountIdx: 3,
90
+ authIdx: 0,
91
+ targetInstruction: {
92
+ programIdx: 4,
93
+ instructionData: new Uint8Array([3, 4]),
94
+ },
95
+ })
96
+ );
80
97
  expect(transaction.sign).toHaveBeenCalledWith(new Uint8Array(32).fill(2));
81
98
  expect(result.rawTransaction).toEqual(new Uint8Array([9, 9, 9]));
82
99
  });
@@ -90,7 +107,8 @@ describe('passkey submit', () => {
90
107
  adminPrivateKey: new Uint8Array(32).fill(2),
91
108
  walletAddress: 'wallet-address',
92
109
  accountCtx,
93
- invokeIx: new Uint8Array([3, 4]),
110
+ targetProgramAddress: 'target-program',
111
+ instructionData: new Uint8Array([3, 4]),
94
112
  ...signaturePayload,
95
113
  })).resolves.toEqual({
96
114
  signature: 'tx-signature',
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PASSKEY_MANAGER_PROGRAM_ADDRESS,
3
- concatenateInstructions,
3
+ decodeAddress,
4
4
  encodeValidateInstruction,
5
5
  hexToBytes,
6
6
  } from '@thru/programs/passkey-manager';
@@ -48,23 +48,31 @@ export async function buildPasskeyTransaction(opts: {
48
48
  adminPrivateKey: Uint8Array;
49
49
  walletAddress: string;
50
50
  accountCtx: AccountContext;
51
- invokeIx: Uint8Array;
51
+ targetProgramAddress: string;
52
+ instructionData: Uint8Array;
53
+ authIdx?: number;
52
54
  header?: PasskeyTransactionHeaderOverrides;
53
55
  } & PasskeySignaturePayload): Promise<BuiltPasskeyTransaction> {
56
+ const targetProgramIdx = opts.accountCtx.getAccountIndex(
57
+ decodeAddress(opts.targetProgramAddress)
58
+ );
54
59
  const validateIx = encodeValidateInstruction({
55
60
  walletAccountIdx: opts.accountCtx.walletAccountIdx,
56
- authIdx: 0,
61
+ authIdx: opts.authIdx ?? 0,
62
+ targetInstruction: {
63
+ programIdx: targetProgramIdx,
64
+ instructionData: opts.instructionData,
65
+ },
57
66
  signatureR: hexToBytes(opts.signatureR),
58
67
  signatureS: hexToBytes(opts.signatureS),
59
68
  authenticatorData: base64ToBytes(opts.authenticatorData),
60
69
  clientDataJSON: base64ToBytes(opts.clientDataJSON),
61
70
  });
62
71
 
63
- const instructionData = concatenateInstructions([validateIx, opts.invokeIx]);
64
72
  const transaction = await opts.client.transactions.build({
65
73
  feePayer: { publicKey: opts.adminPublicKey },
66
74
  program: PASSKEY_MANAGER_PROGRAM_ADDRESS,
67
- instructionData,
75
+ instructionData: validateIx,
68
76
  accounts: {
69
77
  readWrite: opts.accountCtx.readWriteAddresses,
70
78
  readOnly: opts.accountCtx.readOnlyAddresses,
@@ -88,7 +96,9 @@ export async function submitPasskeyTransaction(opts: {
88
96
  adminPrivateKey: Uint8Array;
89
97
  walletAddress: string;
90
98
  accountCtx: AccountContext;
91
- invokeIx: Uint8Array;
99
+ targetProgramAddress: string;
100
+ instructionData: Uint8Array;
101
+ authIdx?: number;
92
102
  header?: PasskeyTransactionHeaderOverrides;
93
103
  } & PasskeySignaturePayload): Promise<TransactionResult> {
94
104
  return withSerializedFeePayer(opts.adminPublicKey, async () => {
@@ -79,5 +79,7 @@ export interface PasskeyChallengeResult {
79
79
 
80
80
  export interface PasskeyContextResult {
81
81
  accountCtx: AccountContext;
82
- invokeIx: Uint8Array;
82
+ targetProgramAddress: string;
83
+ instructionData: Uint8Array;
84
+ authIdx?: number;
83
85
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/auth/add-device.ts"],"sourcesContent":["/* Add-passkey-to-account transaction builder, lifted from\n `web/wallet-auth-manager/app/page.tsx` (`runValidateThen`,\n `handleSubmitAddPasskey`) so the wallet's `/embedded` post-connect\n step can reuse the exact same flow.\n\n Builds the on-chain transaction:\n VALIDATE(existingAuthority) + ADD_AUTHORITY(newPasskey) [+ REGISTER_CREDENTIAL]\n asks the caller's existing passkey to sign the challenge, then asks\n the caller's wallet signer to sign the assembled transaction, sends\n it, and returns the result. */\n\nimport {\n type Authority,\n buildAccountContext,\n concatenateInstructions,\n createValidateChallenge,\n createCredentialLookupSeed,\n deriveWalletAddress,\n encodeAddAuthorityInstruction,\n encodeRegisterCredentialInstruction,\n encodeValidateInstruction,\n parseWalletAuthorities,\n type ParsedAuthority,\n type WalletSigner,\n} from \"@thru/programs/passkey-manager\";\n\n/** Minimal shape required from a passkey signer. Both web's\n `signWithDiscoverablePasskey`/`signWithPasskey` and mobile's\n counterparts conform. */\nexport interface PasskeyChallengeSigner {\n signChallenge: (challenge: Uint8Array) => Promise<{\n signatureR: Uint8Array;\n signatureS: Uint8Array;\n authenticatorData: Uint8Array;\n clientDataJSON: Uint8Array;\n }>;\n}\n\n/** Minimal shape required from a Thru chain client. Loosely-typed\n because @thru/sdk's DTS emit is currently broken in this\n repo. The caller passes the real Thru and we narrow operationally. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyThruClient = any;\n\nexport interface AddDeviceParams {\n /** Loosely-typed Thru chain client (`@thru/sdk/client`). */\n thru: AnyThruClient;\n /** Wallet (the on-chain WalletAccount) to attach the passkey to. */\n walletAddress: string;\n /** Index of the existing authority that approves this change. Must\n currently be a passkey authority. */\n authIdx: number;\n /** New passkey to attach. tag = 1 (passkey). */\n newAuthority: Authority;\n /** Optional credential-lookup registration so the new passkey is\n discoverable on subsequent sign-ins. */\n credentialId?: Uint8Array;\n walletName?: string;\n /** Existing-passkey challenge signer (web or mobile). */\n passkey: PasskeyChallengeSigner;\n /** Wallet transaction signer that returns base64(signed bytes). */\n walletSigner: WalletSigner;\n /** Passkey program address (base58). */\n programAddress: string;\n /** Sign-and-send executor (lifted from passkey-transaction.ts in the\n wallet-auth-manager - wallet apps own this because it depends on\n the `Thru` client's transaction builder). */\n executor: TxExecutor;\n /** Optional status callback so UIs can show progress. */\n onStatus?: (message: string) => void;\n}\n\nexport interface TxExecutorParams {\n thru: AnyThruClient;\n walletSigner: WalletSigner;\n instructionData: Uint8Array;\n readWriteAddresses: string[];\n readOnlyAddresses: string[];\n label: string;\n}\n\nexport interface TxExecutorResult {\n signature: string;\n /** Loosely-typed because @thru/sdk types aren't available. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n execution: any;\n}\n\nexport type TxExecutor = (\n params: TxExecutorParams,\n) => Promise<TxExecutorResult>;\n\nexport interface AddDeviceResult extends TxExecutorResult {\n /** The new passkey's authority index after the transaction lands. */\n newAuthorityIdx: number;\n}\n\n/**\n * Run VALIDATE + ADD_AUTHORITY [+ REGISTER_CREDENTIAL] to attach a new\n * passkey to an on-chain WalletAccount.\n */\nexport async function addDeviceToAccount(\n params: AddDeviceParams,\n): Promise<AddDeviceResult> {\n const status = params.onStatus ?? (() => {});\n\n const walletAccount = await params.thru.accounts.get(params.walletAddress);\n const walletData: Uint8Array | undefined = walletAccount?.data?.data;\n if (!walletData) {\n throw new Error(\"Wallet account data missing\");\n }\n\n const parsed = parseWalletAuthorities(walletData);\n const authorizing: ParsedAuthority | undefined =\n parsed.authorities[params.authIdx];\n if (!authorizing) {\n throw new Error(\"Authorization index out of bounds\");\n }\n if (authorizing.kind !== \"passkey\") {\n throw new Error(\n \"addDeviceToAccount currently requires a passkey authority for VALIDATE\",\n );\n }\n\n /* The new authority will land at the next free slot. */\n const newAuthorityIdx = parsed.authorities.length;\n\n /* Build the instruction sequence after the VALIDATE. */\n const addAuthorityInstruction = encodeAddAuthorityInstruction({\n authority: params.newAuthority,\n });\n\n let trailingInstructionData = addAuthorityInstruction;\n let readWriteAccounts: Uint8Array[] = [];\n\n if (params.credentialId) {\n const lookupSeed = await createCredentialLookupSeed(params.credentialId);\n const lookupAddressBytes = await deriveWalletAddress(\n lookupSeed,\n params.programAddress,\n );\n\n status(\"Fetching state proof for credential lookup...\");\n const proofResult = await params.thru.proofs.generate({\n proofType: 1 /* StateProofType.CREATING */,\n address: lookupAddressBytes,\n });\n\n const ctx = buildAccountContext({\n walletAddress: params.walletAddress,\n readWriteAccounts: [lookupAddressBytes],\n readOnlyAccounts: [],\n });\n\n const registerCredentialInstruction = encodeRegisterCredentialInstruction({\n walletAccountIdx: ctx.walletAccountIdx,\n lookupAccountIdx: ctx.getAccountIndex(lookupAddressBytes),\n seed: lookupSeed,\n stateProof: proofResult.proof,\n });\n\n trailingInstructionData = concatenateInstructions([\n addAuthorityInstruction,\n registerCredentialInstruction,\n ]);\n readWriteAccounts = [lookupAddressBytes];\n }\n\n /* Build the VALIDATE challenge over (nonce, account_addresses,\n trailing_instruction_data) and ask the caller's passkey to sign. */\n const ctx = buildAccountContext({\n walletAddress: params.walletAddress,\n readWriteAccounts,\n readOnlyAccounts: [],\n });\n\n const challenge = await createValidateChallenge(\n parsed.nonce,\n ctx.accountAddresses,\n trailingInstructionData,\n );\n\n status(\"Waiting for passkey approval...\");\n const signature = await params.passkey.signChallenge(challenge);\n\n const validateInstruction = encodeValidateInstruction({\n walletAccountIdx: ctx.walletAccountIdx,\n authIdx: params.authIdx,\n signatureR: signature.signatureR,\n signatureS: signature.signatureS,\n authenticatorData: signature.authenticatorData,\n clientDataJSON: signature.clientDataJSON,\n });\n\n const instructionData = concatenateInstructions([\n validateInstruction,\n trailingInstructionData,\n ]);\n\n status(\"Sending transaction...\");\n const result = await params.executor({\n thru: params.thru,\n walletSigner: params.walletSigner,\n instructionData,\n readWriteAddresses: ctx.readWriteAddresses,\n readOnlyAddresses: ctx.readOnlyAddresses,\n label: params.credentialId\n ? \"VALIDATE + ADD_AUTHORITY + REGISTER_CREDENTIAL\"\n : \"VALIDATE + ADD_AUTHORITY\",\n });\n\n return { ...result, newAuthorityIdx };\n}\n"],"mappings":";AAWA;AAAA,EAEE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AA6EP,eAAsB,mBACpB,QAC0B;AAC1B,QAAM,SAAS,OAAO,aAAa,MAAM;AAAA,EAAC;AAE1C,QAAM,gBAAgB,MAAM,OAAO,KAAK,SAAS,IAAI,OAAO,aAAa;AACzE,QAAM,aAAqC,eAAe,MAAM;AAChE,MAAI,CAAC,YAAY;AACf,UAAM,IAAI,MAAM,6BAA6B;AAAA,EAC/C;AAEA,QAAM,SAAS,uBAAuB,UAAU;AAChD,QAAM,cACJ,OAAO,YAAY,OAAO,OAAO;AACnC,MAAI,CAAC,aAAa;AAChB,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AACA,MAAI,YAAY,SAAS,WAAW;AAClC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,QAAM,kBAAkB,OAAO,YAAY;AAG3C,QAAM,0BAA0B,8BAA8B;AAAA,IAC5D,WAAW,OAAO;AAAA,EACpB,CAAC;AAED,MAAI,0BAA0B;AAC9B,MAAI,oBAAkC,CAAC;AAEvC,MAAI,OAAO,cAAc;AACvB,UAAM,aAAa,MAAM,2BAA2B,OAAO,YAAY;AACvE,UAAM,qBAAqB,MAAM;AAAA,MAC/B;AAAA,MACA,OAAO;AAAA,IACT;AAEA,WAAO,+CAA+C;AACtD,UAAM,cAAc,MAAM,OAAO,KAAK,OAAO,SAAS;AAAA,MACpD,WAAW;AAAA,MACX,SAAS;AAAA,IACX,CAAC;AAED,UAAMA,OAAM,oBAAoB;AAAA,MAC9B,eAAe,OAAO;AAAA,MACtB,mBAAmB,CAAC,kBAAkB;AAAA,MACtC,kBAAkB,CAAC;AAAA,IACrB,CAAC;AAED,UAAM,gCAAgC,oCAAoC;AAAA,MACxE,kBAAkBA,KAAI;AAAA,MACtB,kBAAkBA,KAAI,gBAAgB,kBAAkB;AAAA,MACxD,MAAM;AAAA,MACN,YAAY,YAAY;AAAA,IAC1B,CAAC;AAED,8BAA0B,wBAAwB;AAAA,MAChD;AAAA,MACA;AAAA,IACF,CAAC;AACD,wBAAoB,CAAC,kBAAkB;AAAA,EACzC;AAIA,QAAM,MAAM,oBAAoB;AAAA,IAC9B,eAAe,OAAO;AAAA,IACtB;AAAA,IACA,kBAAkB,CAAC;AAAA,EACrB,CAAC;AAED,QAAM,YAAY,MAAM;AAAA,IACtB,OAAO;AAAA,IACP,IAAI;AAAA,IACJ;AAAA,EACF;AAEA,SAAO,iCAAiC;AACxC,QAAM,YAAY,MAAM,OAAO,QAAQ,cAAc,SAAS;AAE9D,QAAM,sBAAsB,0BAA0B;AAAA,IACpD,kBAAkB,IAAI;AAAA,IACtB,SAAS,OAAO;AAAA,IAChB,YAAY,UAAU;AAAA,IACtB,YAAY,UAAU;AAAA,IACtB,mBAAmB,UAAU;AAAA,IAC7B,gBAAgB,UAAU;AAAA,EAC5B,CAAC;AAED,QAAM,kBAAkB,wBAAwB;AAAA,IAC9C;AAAA,IACA;AAAA,EACF,CAAC;AAED,SAAO,wBAAwB;AAC/B,QAAM,SAAS,MAAM,OAAO,SAAS;AAAA,IACnC,MAAM,OAAO;AAAA,IACb,cAAc,OAAO;AAAA,IACrB;AAAA,IACA,oBAAoB,IAAI;AAAA,IACxB,mBAAmB,IAAI;AAAA,IACvB,OAAO,OAAO,eACV,mDACA;AAAA,EACN,CAAC;AAED,SAAO,EAAE,GAAG,QAAQ,gBAAgB;AACtC;","names":["ctx"]}