@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.
- package/dist/server.cjs +101 -54
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +7 -5
- package/dist/server.d.ts +7 -5
- package/dist/server.js +101 -54
- package/dist/server.js.map +1 -1
- package/package.json +7 -3
- package/src/expo-secure-store.d.ts +13 -0
- package/src/react-native-passkeys.d.ts +25 -0
- package/src/server/create-wallet.test.ts +186 -0
- package/src/server/create-wallet.ts +41 -29
- package/src/server/handlers.ts +1 -1
- package/src/server/submit.ts +26 -24
- package/src/server/types.ts +4 -2
- package/src/server/utils.test.ts +51 -0
- package/src/server/utils.ts +71 -6
|
@@ -9,13 +9,18 @@ import {
|
|
|
9
9
|
encodeCreateInstruction,
|
|
10
10
|
encodeRegisterCredentialInstruction,
|
|
11
11
|
} from '@thru/passkey-manager';
|
|
12
|
-
import {
|
|
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:
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
99
|
+
credentialLookupAddress = lookupAddress;
|
|
90
100
|
|
|
91
|
-
let lookupExists = false;
|
|
92
101
|
try {
|
|
93
|
-
await opts.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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,
|
|
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:
|
|
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
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
151
|
+
});
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.warn('Credential registration failed (non-fatal):', error);
|
|
142
154
|
}
|
|
143
155
|
}
|
|
144
156
|
|
package/src/server/handlers.ts
CHANGED
|
@@ -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:
|
|
13
|
+
adminPrivateKey: Uint8Array;
|
|
14
14
|
client: ThruClient;
|
|
15
15
|
challengeTtlMs?: number;
|
|
16
16
|
}) {
|
package/src/server/submit.ts
CHANGED
|
@@ -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:
|
|
18
|
+
adminPrivateKey: Uint8Array;
|
|
19
19
|
walletAddress: string;
|
|
20
20
|
accountCtx: AccountContext;
|
|
21
21
|
invokeIx: Uint8Array;
|
|
22
22
|
} & PasskeySignaturePayload): Promise<TransactionResult> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
}
|
package/src/server/types.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface ThruClient {
|
|
|
25
25
|
};
|
|
26
26
|
header: { fee: bigint };
|
|
27
27
|
}) => Promise<{
|
|
28
|
-
sign: (privateKey:
|
|
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
|
+
});
|
package/src/server/utils.ts
CHANGED
|
@@ -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:
|
|
43
|
-
errorCode:
|
|
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
|
-
|
|
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
|
+
}
|