@thru/passkey 0.2.15 → 0.2.17

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.
@@ -9,13 +9,18 @@ import {
9
9
  encodeCreateInstruction,
10
10
  encodeRegisterCredentialInstruction,
11
11
  } from '@thru/passkey-manager';
12
- import { toThruAddress, getStateProof, trackTransaction } from './utils';
12
+ import {
13
+ toThruAddress,
14
+ getStateProof,
15
+ trackTransaction,
16
+ withSerializedFeePayer,
17
+ } from './utils';
13
18
  import type { ThruClient } from './types';
14
19
 
15
20
  export async function createPasskeyWallet(opts: {
16
21
  client: ThruClient;
17
22
  adminPublicKey: Uint8Array;
18
- adminPrivateKey: string;
23
+ adminPrivateKey: Uint8Array;
19
24
  adminAddress: string;
20
25
  pubkeyX: Uint8Array;
21
26
  pubkeyY: Uint8Array;
@@ -27,15 +32,17 @@ export async function createPasskeyWallet(opts: {
27
32
  const walletBytes = await deriveWalletAddress(seed, PASSKEY_MANAGER_PROGRAM_ADDRESS);
28
33
  const walletAddress = toThruAddress(walletBytes);
29
34
 
30
- let walletExists = false;
31
- try {
32
- await opts.client.accounts.get(walletAddress);
33
- walletExists = true;
34
- } catch {
35
- walletExists = false;
36
- }
35
+ await withSerializedFeePayer(opts.adminPublicKey, async () => {
36
+ let walletExists = false;
37
+ try {
38
+ await opts.client.accounts.get(walletAddress);
39
+ walletExists = true;
40
+ } catch {
41
+ walletExists = false;
42
+ }
43
+
44
+ if (walletExists) return;
37
45
 
38
- if (!walletExists) {
39
46
  const stateProof = await getStateProof(opts.client, walletAddress);
40
47
  const accountCtx = buildAccountContext({
41
48
  walletAddress,
@@ -61,8 +68,8 @@ export async function createPasskeyWallet(opts: {
61
68
  program: PASSKEY_MANAGER_PROGRAM_ADDRESS,
62
69
  instructionData: createIx,
63
70
  accounts: {
64
- readWrite: [walletAddress],
65
- readOnly: [],
71
+ readWrite: accountCtx.readWriteAddresses,
72
+ readOnly: accountCtx.readOnlyAddresses,
66
73
  },
67
74
  header: { fee: 0n },
68
75
  });
@@ -72,10 +79,12 @@ export async function createPasskeyWallet(opts: {
72
79
  const result = await trackTransaction(opts.client, signature, 60000);
73
80
  if (result.status !== 'finalized') {
74
81
  throw new Error(
75
- `Wallet creation failed with error code: ${result.errorCode ?? 'unknown'}`
82
+ `Wallet creation failed with status: ${result.status}${
83
+ result.errorCode !== undefined ? ` (error code: ${result.errorCode})` : ''
84
+ }`
76
85
  );
77
86
  }
78
- }
87
+ });
79
88
 
80
89
  let credentialLookupAddress: string | undefined;
81
90
  if (opts.credentialId) {
@@ -85,21 +94,24 @@ export async function createPasskeyWallet(opts: {
85
94
  walletName,
86
95
  PASSKEY_MANAGER_PROGRAM_ADDRESS
87
96
  );
97
+ const lookupAddress = toThruAddress(lookupAddressBytes);
88
98
 
89
- credentialLookupAddress = toThruAddress(lookupAddressBytes);
99
+ credentialLookupAddress = lookupAddress;
90
100
 
91
- let lookupExists = false;
92
101
  try {
93
- await opts.client.accounts.get(credentialLookupAddress);
94
- lookupExists = true;
95
- } catch {
96
- lookupExists = false;
97
- }
102
+ await withSerializedFeePayer(opts.adminPublicKey, async () => {
103
+ let lookupExists = false;
104
+ try {
105
+ await opts.client.accounts.get(lookupAddress);
106
+ lookupExists = true;
107
+ } catch {
108
+ lookupExists = false;
109
+ }
110
+
111
+ if (lookupExists) return;
98
112
 
99
- if (!lookupExists) {
100
- try {
101
113
  const credSeed = await createCredentialLookupSeed(credentialIdBytes, walletName);
102
- const stateProof = await getStateProof(opts.client, credentialLookupAddress);
114
+ const stateProof = await getStateProof(opts.client, lookupAddress);
103
115
  const accountCtx = buildAccountContext({
104
116
  walletAddress,
105
117
  readWriteAccounts: [lookupAddressBytes],
@@ -120,8 +132,8 @@ export async function createPasskeyWallet(opts: {
120
132
  program: PASSKEY_MANAGER_PROGRAM_ADDRESS,
121
133
  instructionData: registerIx,
122
134
  accounts: {
123
- readWrite: [walletAddress, credentialLookupAddress],
124
- readOnly: [],
135
+ readWrite: accountCtx.readWriteAddresses,
136
+ readOnly: accountCtx.readOnlyAddresses,
125
137
  },
126
138
  header: { fee: 0n },
127
139
  });
@@ -136,9 +148,9 @@ export async function createPasskeyWallet(opts: {
136
148
  }`
137
149
  );
138
150
  }
139
- } catch (error) {
140
- console.warn('Credential registration failed (non-fatal):', error);
141
- }
151
+ });
152
+ } catch (error) {
153
+ console.warn('Credential registration failed (non-fatal):', error);
142
154
  }
143
155
  }
144
156
 
@@ -10,7 +10,7 @@ import type {
10
10
  export function createPasskeyHandlers<P>(opts: {
11
11
  buildContext: (params: P) => Promise<PasskeyContextResult>;
12
12
  adminPublicKey: Uint8Array;
13
- adminPrivateKey: string;
13
+ adminPrivateKey: Uint8Array;
14
14
  client: ThruClient;
15
15
  challengeTtlMs?: number;
16
16
  }) {
@@ -5,7 +5,7 @@ import {
5
5
  hexToBytes,
6
6
  } from '@thru/passkey-manager';
7
7
  import type { AccountContext } from '@thru/passkey-manager';
8
- import { trackTransaction } from './utils';
8
+ import { trackTransaction, withSerializedFeePayer } from './utils';
9
9
  import type {
10
10
  PasskeySignaturePayload,
11
11
  ThruClient,
@@ -15,33 +15,35 @@ import type {
15
15
  export async function submitPasskeyTransaction(opts: {
16
16
  client: ThruClient;
17
17
  adminPublicKey: Uint8Array;
18
- adminPrivateKey: string;
18
+ adminPrivateKey: Uint8Array;
19
19
  walletAddress: string;
20
20
  accountCtx: AccountContext;
21
21
  invokeIx: Uint8Array;
22
22
  } & PasskeySignaturePayload): Promise<TransactionResult> {
23
- const validateIx = encodeValidateInstruction({
24
- walletAccountIdx: opts.accountCtx.walletAccountIdx,
25
- authIdx: 0,
26
- signatureR: hexToBytes(opts.signatureR),
27
- signatureS: hexToBytes(opts.signatureS),
28
- authenticatorData: Buffer.from(opts.authenticatorData, 'base64'),
29
- clientDataJSON: Buffer.from(opts.clientDataJSON, 'base64'),
30
- });
23
+ return withSerializedFeePayer(opts.adminPublicKey, async () => {
24
+ const validateIx = encodeValidateInstruction({
25
+ walletAccountIdx: opts.accountCtx.walletAccountIdx,
26
+ authIdx: 0,
27
+ signatureR: hexToBytes(opts.signatureR),
28
+ signatureS: hexToBytes(opts.signatureS),
29
+ authenticatorData: Buffer.from(opts.authenticatorData, 'base64'),
30
+ clientDataJSON: Buffer.from(opts.clientDataJSON, 'base64'),
31
+ });
31
32
 
32
- const instructionData = concatenateInstructions([validateIx, opts.invokeIx]);
33
- const transaction = await opts.client.transactions.build({
34
- feePayer: { publicKey: opts.adminPublicKey },
35
- program: PASSKEY_MANAGER_PROGRAM_ADDRESS,
36
- instructionData,
37
- accounts: {
38
- readWrite: opts.accountCtx.readWriteAddresses,
39
- readOnly: opts.accountCtx.readOnlyAddresses,
40
- },
41
- header: { fee: 0n },
42
- });
33
+ const instructionData = concatenateInstructions([validateIx, opts.invokeIx]);
34
+ const transaction = await opts.client.transactions.build({
35
+ feePayer: { publicKey: opts.adminPublicKey },
36
+ program: PASSKEY_MANAGER_PROGRAM_ADDRESS,
37
+ instructionData,
38
+ accounts: {
39
+ readWrite: opts.accountCtx.readWriteAddresses,
40
+ readOnly: opts.accountCtx.readOnlyAddresses,
41
+ },
42
+ header: { fee: 0n },
43
+ });
43
44
 
44
- await transaction.sign(opts.adminPrivateKey);
45
- const signature = await opts.client.transactions.send(transaction.toWire());
46
- return trackTransaction(opts.client, signature);
45
+ await transaction.sign(opts.adminPrivateKey);
46
+ const signature = await opts.client.transactions.send(transaction.toWire());
47
+ return trackTransaction(opts.client, signature);
48
+ });
47
49
  }
@@ -25,7 +25,7 @@ export interface ThruClient {
25
25
  };
26
26
  header: { fee: bigint };
27
27
  }) => Promise<{
28
- sign: (privateKey: string) => Promise<unknown>;
28
+ sign: (privateKey: Uint8Array) => Promise<unknown>;
29
29
  toWire: () => Uint8Array;
30
30
  }>;
31
31
  send: (transaction: Uint8Array) => Promise<string>;
@@ -35,6 +35,8 @@ export interface ThruClient {
35
35
  ) => AsyncIterable<{
36
36
  executionResult?: {
37
37
  userErrorCode: bigint;
38
+ vmError?: bigint | number | null;
39
+ executionResult?: bigint | number | null;
38
40
  };
39
41
  statusCode?: number;
40
42
  }>;
@@ -55,7 +57,7 @@ export interface PasskeyChallengeSubmitPayload extends PasskeySignaturePayload {
55
57
 
56
58
  export interface TransactionResult {
57
59
  signature: string;
58
- status: 'finalized' | 'failed' | 'timeout';
60
+ status: 'finalized' | 'failed' | 'timeout' | 'finalized_without_execution';
59
61
  errorCode?: bigint;
60
62
  }
61
63
 
@@ -0,0 +1,51 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import type { ThruClient } from './types';
3
+ import { trackTransaction } from './utils';
4
+
5
+ const feePayerQueueSymbol = Symbol.for('thru.sharedFeePayerQueues');
6
+
7
+ function clearFeePayerQueues(): void {
8
+ const globalQueues = globalThis as typeof globalThis & {
9
+ [feePayerQueueSymbol]?: Map<string, Promise<void>>;
10
+ };
11
+
12
+ globalQueues[feePayerQueueSymbol]?.clear();
13
+ delete globalQueues[feePayerQueueSymbol];
14
+ }
15
+
16
+ describe('trackTransaction', () => {
17
+ beforeEach(() => {
18
+ clearFeePayerQueues();
19
+ });
20
+
21
+ it('returns a distinct status when finalized arrives without an execution payload', async () => {
22
+ const client = {
23
+ transactions: {
24
+ track: async function* () {
25
+ yield { statusCode: 3 };
26
+ },
27
+ },
28
+ } as ThruClient;
29
+
30
+ await expect(trackTransaction(client, 'sig-1')).resolves.toEqual({
31
+ signature: 'sig-1',
32
+ status: 'finalized_without_execution',
33
+ });
34
+ });
35
+
36
+ it('preserves finalized status if the tracking stream errors afterward', async () => {
37
+ const client = {
38
+ transactions: {
39
+ track: async function* () {
40
+ yield { statusCode: 3 };
41
+ throw new Error('stream closed');
42
+ },
43
+ },
44
+ } as ThruClient;
45
+
46
+ await expect(trackTransaction(client, 'sig-2')).resolves.toEqual({
47
+ signature: 'sig-2',
48
+ status: 'finalized_without_execution',
49
+ });
50
+ });
51
+ });
@@ -1,6 +1,20 @@
1
1
  import { encodeAddress } from '@thru/helpers';
2
2
  import type { ThruClient, TransactionResult } from './types';
3
3
 
4
+ const feePayerQueueSymbol = Symbol.for('thru.sharedFeePayerQueues');
5
+
6
+ function getFeePayerQueues(): Map<string, Promise<void>> {
7
+ const globalQueues = globalThis as typeof globalThis & {
8
+ [feePayerQueueSymbol]?: Map<string, Promise<void>>;
9
+ };
10
+
11
+ if (!globalQueues[feePayerQueueSymbol]) {
12
+ globalQueues[feePayerQueueSymbol] = new Map<string, Promise<void>>();
13
+ }
14
+
15
+ return globalQueues[feePayerQueueSymbol];
16
+ }
17
+
4
18
  export async function getStateProof(
5
19
  client: ThruClient,
6
20
  address: string,
@@ -34,24 +48,49 @@ export async function trackTransaction(
34
48
  signature: string,
35
49
  timeoutMs: number = 5000
36
50
  ): Promise<TransactionResult> {
51
+ let finalizedSeen = false;
52
+
37
53
  try {
38
54
  for await (const update of client.transactions.track(signature, { timeoutMs })) {
39
55
  if (update.executionResult) {
56
+ const vmError =
57
+ update.executionResult.vmError !== undefined && update.executionResult.vmError !== null
58
+ ? BigInt(update.executionResult.vmError)
59
+ : 0n;
60
+ const userErrorCode = update.executionResult.userErrorCode;
61
+ const executionError =
62
+ update.executionResult.executionResult !== undefined &&
63
+ update.executionResult.executionResult !== null
64
+ ? BigInt(update.executionResult.executionResult)
65
+ : 0n;
66
+ const success = vmError === 0n && executionError === 0n && userErrorCode === 0n;
67
+
40
68
  return {
41
69
  signature,
42
- status: update.executionResult.userErrorCode === 0n ? 'finalized' : 'failed',
43
- errorCode: update.executionResult.userErrorCode,
70
+ status: success ? 'finalized' : 'failed',
71
+ errorCode: vmError !== 0n ? vmError : executionError !== 0n ? executionError : userErrorCode,
44
72
  };
45
73
  }
46
74
 
47
75
  if (update.statusCode === 3) {
48
- return {
49
- signature,
50
- status: 'finalized',
51
- };
76
+ finalizedSeen = true;
52
77
  }
53
78
  }
79
+
80
+ if (finalizedSeen) {
81
+ return {
82
+ signature,
83
+ status: 'finalized_without_execution',
84
+ };
85
+ }
54
86
  } catch {
87
+ if (finalizedSeen) {
88
+ return {
89
+ signature,
90
+ status: 'finalized_without_execution',
91
+ };
92
+ }
93
+
55
94
  return {
56
95
  signature,
57
96
  status: 'timeout',
@@ -67,3 +106,29 @@ export async function trackTransaction(
67
106
  export function toThruAddress(bytes: Uint8Array): string {
68
107
  return encodeAddress(bytes);
69
108
  }
109
+
110
+ export async function withSerializedFeePayer<T>(
111
+ feePayerPublicKey: Uint8Array,
112
+ work: () => Promise<T>
113
+ ): Promise<T> {
114
+ const queueKey = toThruAddress(feePayerPublicKey);
115
+ const feePayerQueues = getFeePayerQueues();
116
+ const previous = feePayerQueues.get(queueKey) ?? Promise.resolve();
117
+ let release!: () => void;
118
+ const current = new Promise<void>((resolve) => {
119
+ release = resolve;
120
+ });
121
+ const tail = previous.then(() => current);
122
+ feePayerQueues.set(queueKey, tail);
123
+
124
+ await previous;
125
+
126
+ try {
127
+ return await work();
128
+ } finally {
129
+ release();
130
+ if (feePayerQueues.get(queueKey) === tail) {
131
+ feePayerQueues.delete(queueKey);
132
+ }
133
+ }
134
+ }