@thru/passkey-manager 0.2.1

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.
@@ -0,0 +1,26 @@
1
+ import { WalletAccount } from './abi/thru/program/passkey_manager/types';
2
+
3
+ /**
4
+ * Parse wallet account data to extract nonce.
5
+ */
6
+ export function parseWalletNonce(data: Uint8Array): bigint {
7
+ const account = WalletAccount.from_array(data);
8
+ if (!account) return 0n;
9
+ return account.get_nonce();
10
+ }
11
+
12
+ /**
13
+ * Fetch wallet nonce from the chain.
14
+ * Takes an SDK-like object with accounts.get() method.
15
+ */
16
+ export async function fetchWalletNonce(
17
+ sdk: { accounts: { get: (address: string) => Promise<{ data?: { data?: Uint8Array } }> } },
18
+ walletAddress: string
19
+ ): Promise<bigint> {
20
+ const account = await sdk.accounts.get(walletAddress);
21
+ const data = account.data?.data;
22
+ if (!data) return 0n;
23
+ const parsed = WalletAccount.from_array(data);
24
+ if (!parsed) return 0n;
25
+ return parsed.get_nonce();
26
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Create challenge for VALIDATE instruction.
3
+ * SHA256(nonce || account_0 || account_1 || ... || trailing_instruction_bytes)
4
+ */
5
+ export async function createValidateChallenge(
6
+ nonce: bigint,
7
+ accountAddresses: string[],
8
+ trailingInstructionData: Uint8Array
9
+ ): Promise<Uint8Array> {
10
+ const encoder = new TextEncoder();
11
+ const accountBytes = accountAddresses.map((address) => {
12
+ return encoder.encode(address);
13
+ });
14
+
15
+ const totalSize =
16
+ accountBytes.reduce((sum, bytes) => sum + bytes.length, 8) +
17
+ trailingInstructionData.length;
18
+ const challengeData = new Uint8Array(totalSize);
19
+
20
+ let offset = 0;
21
+
22
+ // Write nonce as little-endian u64
23
+ let v = nonce;
24
+ for (let i = 0; i < 8; i++) {
25
+ challengeData[offset + i] = Number(v & 0xffn);
26
+ v >>= 8n;
27
+ }
28
+ offset += 8;
29
+
30
+ for (const bytes of accountBytes) {
31
+ challengeData.set(bytes, offset);
32
+ offset += bytes.length;
33
+ }
34
+
35
+ challengeData.set(trailingInstructionData, offset);
36
+
37
+ const hashBuffer = await crypto.subtle.digest('SHA-256', challengeData);
38
+ return new Uint8Array(hashBuffer);
39
+ }
@@ -0,0 +1,14 @@
1
+ export const PASSKEY_MANAGER_PROGRAM_ADDRESS =
2
+ 'taNiWOgcxRz2DYI1Vbp6AOMdiEbC9nYwXd02dQJ4jnUUpF';
3
+
4
+ // Instruction discriminants
5
+ export const INSTRUCTION_CREATE = 0x00;
6
+ export const INSTRUCTION_VALIDATE = 0x01;
7
+ export const INSTRUCTION_TRANSFER = 0x02;
8
+ export const INSTRUCTION_INVOKE = 0x03;
9
+ export const INSTRUCTION_ADD_AUTHORITY = 0x04;
10
+ export const INSTRUCTION_REMOVE_AUTHORITY = 0x05;
11
+
12
+ // Authority tags
13
+ export const AUTHORITY_TAG_PASSKEY = 1;
14
+ export const AUTHORITY_TAG_PUBKEY = 2;
package/src/context.ts ADDED
@@ -0,0 +1,112 @@
1
+ import { encodeAddress, decodeAddress } from '@thru/helpers';
2
+ import { bytesEqual, compareBytes, uniqueAccounts } from './encoding';
3
+ import { PASSKEY_MANAGER_PROGRAM_ADDRESS } from './constants';
4
+ import type { AccountContext } from './types';
5
+
6
+ /**
7
+ * Default fee payer address (manager profile).
8
+ */
9
+ export const FEE_PAYER_ADDRESS =
10
+ 'taVcZv3wB2m-euBpMHm2rF9fQRY_fO_g7WdOjs70CxDh_S';
11
+
12
+ /**
13
+ * Build account context for passkey manager transactions.
14
+ * Handles account deduplication, sorting, and index lookup.
15
+ */
16
+ export function buildAccountContext(params: {
17
+ walletAddress: string;
18
+ readWriteAccounts: Uint8Array[];
19
+ readOnlyAccounts: Uint8Array[];
20
+ feePayerAddress?: string;
21
+ programAddress?: string;
22
+ }): AccountContext {
23
+ const feePayerBytes = decodeAddress(params.feePayerAddress ?? FEE_PAYER_ADDRESS);
24
+ const programBytes = decodeAddress(params.programAddress ?? PASSKEY_MANAGER_PROGRAM_ADDRESS);
25
+ const walletBytes = decodeAddress(params.walletAddress);
26
+
27
+ const readWriteBytes = uniqueAccounts([walletBytes, ...params.readWriteAccounts])
28
+ .filter(
29
+ (addr) => !bytesEqual(addr, feePayerBytes) && !bytesEqual(addr, programBytes)
30
+ )
31
+ .sort(compareBytes);
32
+
33
+ const readOnlyBytes = uniqueAccounts(params.readOnlyAccounts)
34
+ .filter(
35
+ (addr) =>
36
+ !bytesEqual(addr, feePayerBytes) &&
37
+ !bytesEqual(addr, programBytes) &&
38
+ !readWriteBytes.some((candidate) => bytesEqual(candidate, addr))
39
+ )
40
+ .sort(compareBytes);
41
+
42
+ const readWriteAddresses = readWriteBytes.map(encodeAddress);
43
+ const readOnlyAddresses = readOnlyBytes.map(encodeAddress);
44
+
45
+ const accountAddresses = [
46
+ encodeAddress(feePayerBytes),
47
+ encodeAddress(programBytes),
48
+ ...readWriteAddresses,
49
+ ...readOnlyAddresses,
50
+ ];
51
+
52
+ const findIndex = (target: Uint8Array): number => {
53
+ if (bytesEqual(target, feePayerBytes)) return 0;
54
+ if (bytesEqual(target, programBytes)) return 1;
55
+
56
+ const rwIndex = readWriteBytes.findIndex((candidate) => bytesEqual(candidate, target));
57
+ if (rwIndex >= 0) return rwIndex + 2;
58
+
59
+ const roIndex = readOnlyBytes.findIndex((candidate) => bytesEqual(candidate, target));
60
+ if (roIndex >= 0) return roIndex + 2 + readWriteBytes.length;
61
+
62
+ return -1;
63
+ };
64
+
65
+ const walletAccountIdx = findIndex(walletBytes);
66
+ if (walletAccountIdx < 2) {
67
+ throw new Error('Wallet account must be a non-fee-payer account');
68
+ }
69
+
70
+ return {
71
+ readWriteAddresses,
72
+ readOnlyAddresses,
73
+ accountAddresses,
74
+ walletAccountIdx,
75
+ getAccountIndex: (pubkey: Uint8Array) => {
76
+ const idx = findIndex(pubkey);
77
+ if (idx < 0) {
78
+ throw new Error('Account not found in transaction accounts');
79
+ }
80
+ return idx;
81
+ },
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Build read-write accounts list for passkey manager transactions (simpler wallet-only version).
87
+ */
88
+ export function buildPasskeyReadWriteAccounts(
89
+ userAccounts: Uint8Array[],
90
+ feePayerPublicKey: Uint8Array,
91
+ programAddress: Uint8Array
92
+ ): {
93
+ readWriteAddresses: string[];
94
+ findAccountIndex: (target: Uint8Array) => number;
95
+ } {
96
+ const sortedUserAccounts = uniqueAccounts(userAccounts).sort(compareBytes);
97
+ const filteredUserAccounts = sortedUserAccounts.filter(
98
+ (addr) => !bytesEqual(addr, feePayerPublicKey) && !bytesEqual(addr, programAddress)
99
+ );
100
+ const readWriteAddresses = filteredUserAccounts.map((addr) => encodeAddress(addr));
101
+
102
+ const findAccountIndex = (target: Uint8Array): number => {
103
+ if (bytesEqual(target, feePayerPublicKey)) return 0;
104
+ if (bytesEqual(target, programAddress)) return 1;
105
+ for (let i = 0; i < filteredUserAccounts.length; i++) {
106
+ if (bytesEqual(filteredUserAccounts[i], target)) return i + 2;
107
+ }
108
+ return -1;
109
+ };
110
+
111
+ return { readWriteAddresses, findAccountIndex };
112
+ }
package/src/crypto.ts ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * P-256 curve order and half-order for low-S normalization.
3
+ */
4
+ export const P256_N =
5
+ 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
6
+ export const P256_HALF_N = P256_N >> 1n;
7
+
8
+ /**
9
+ * Parse DER-encoded ECDSA signature to get r and s components.
10
+ */
11
+ export function parseDerSignature(der: Uint8Array): { r: Uint8Array; s: Uint8Array } {
12
+ if (der[0] !== 0x30) throw new Error('Invalid DER signature');
13
+
14
+ let offset = 2;
15
+
16
+ if (der[offset] !== 0x02) throw new Error('Invalid DER signature');
17
+ offset++;
18
+
19
+ const rLen = der[offset++];
20
+ let r: Uint8Array = der.slice(offset, offset + rLen);
21
+ offset += rLen;
22
+
23
+ if (der[offset] !== 0x02) throw new Error('Invalid DER signature');
24
+ offset++;
25
+
26
+ const sLen = der[offset++];
27
+ let s: Uint8Array = der.slice(offset, offset + sLen);
28
+
29
+ r = normalizeSignatureComponent(r);
30
+ s = normalizeSignatureComponent(s);
31
+
32
+ return { r, s };
33
+ }
34
+
35
+ /**
36
+ * Ensure S is in the lower half of the curve order (BIP-62 / SEC1 compliance).
37
+ */
38
+ export function normalizeLowS(s: Uint8Array): Uint8Array {
39
+ const sValue = bytesToBigIntBE(s);
40
+ if (sValue > P256_HALF_N) {
41
+ return bigIntToBytesBE(P256_N - sValue, 32);
42
+ }
43
+ return s;
44
+ }
45
+
46
+ /**
47
+ * Normalize signature component to exactly 32 bytes.
48
+ */
49
+ export function normalizeSignatureComponent(component: Uint8Array): Uint8Array {
50
+ if (component.length === 32) return component;
51
+
52
+ if (component.length > 32) {
53
+ if (component[0] === 0x00 && component.length === 33) {
54
+ return component.slice(1);
55
+ }
56
+ throw new Error('Invalid signature component length');
57
+ }
58
+
59
+ const normalized = new Uint8Array(32);
60
+ normalized.set(component, 32 - component.length);
61
+ return normalized;
62
+ }
63
+
64
+ export function bytesToBigIntBE(bytes: Uint8Array): bigint {
65
+ let value = 0n;
66
+ for (const byte of bytes) {
67
+ value = (value << 8n) | BigInt(byte);
68
+ }
69
+ return value;
70
+ }
71
+
72
+ export function bigIntToBytesBE(value: bigint, length: number): Uint8Array {
73
+ const out = new Uint8Array(length);
74
+ let v = value;
75
+ for (let i = length - 1; i >= 0; i--) {
76
+ out[i] = Number(v & 0xffn);
77
+ v >>= 8n;
78
+ }
79
+ return out;
80
+ }
@@ -0,0 +1,67 @@
1
+ export function arrayBufferToBase64Url(buffer: ArrayBuffer | SharedArrayBuffer): string {
2
+ const bytes = new Uint8Array(buffer);
3
+ let base64 = '';
4
+ for (let i = 0; i < bytes.length; i++) {
5
+ base64 += String.fromCharCode(bytes[i]);
6
+ }
7
+ return btoa(base64).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
8
+ }
9
+
10
+ export function base64UrlToArrayBuffer(base64Url: string): ArrayBuffer {
11
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
12
+ const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
13
+ const binary = atob(padded);
14
+ const bytes = new Uint8Array(binary.length);
15
+ for (let i = 0; i < binary.length; i++) {
16
+ bytes[i] = binary.charCodeAt(i);
17
+ }
18
+ return bytes.buffer;
19
+ }
20
+
21
+ export function bytesToBase64Url(bytes: Uint8Array): string {
22
+ const slice = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
23
+ return arrayBufferToBase64Url(slice);
24
+ }
25
+
26
+ export function base64UrlToBytes(base64Url: string): Uint8Array {
27
+ return new Uint8Array(base64UrlToArrayBuffer(base64Url));
28
+ }
29
+
30
+ export function bytesToHex(bytes: Uint8Array): string {
31
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
32
+ }
33
+
34
+ export function hexToBytes(hex: string): Uint8Array {
35
+ const normalized = hex.startsWith('0x') ? hex.slice(2) : hex;
36
+ const bytes = new Uint8Array(normalized.length / 2);
37
+ for (let i = 0; i < bytes.length; i++) {
38
+ bytes[i] = parseInt(normalized.slice(i * 2, i * 2 + 2), 16);
39
+ }
40
+ return bytes;
41
+ }
42
+
43
+ export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
44
+ if (a.length !== b.length) return false;
45
+ for (let i = 0; i < a.length; i++) {
46
+ if (a[i] !== b[i]) return false;
47
+ }
48
+ return true;
49
+ }
50
+
51
+ export function compareBytes(a: Uint8Array, b: Uint8Array): number {
52
+ const len = Math.min(a.length, b.length);
53
+ for (let i = 0; i < len; i++) {
54
+ if (a[i] !== b[i]) return a[i] - b[i];
55
+ }
56
+ return a.length - b.length;
57
+ }
58
+
59
+ export function uniqueAccounts(accounts: Uint8Array[]): Uint8Array[] {
60
+ const unique: Uint8Array[] = [];
61
+ for (const account of accounts) {
62
+ if (!unique.some((candidate) => bytesEqual(candidate, account))) {
63
+ unique.push(account);
64
+ }
65
+ }
66
+ return unique;
67
+ }
package/src/index.ts ADDED
@@ -0,0 +1,73 @@
1
+ // Constants
2
+ export {
3
+ PASSKEY_MANAGER_PROGRAM_ADDRESS,
4
+ INSTRUCTION_CREATE,
5
+ INSTRUCTION_VALIDATE,
6
+ INSTRUCTION_TRANSFER,
7
+ INSTRUCTION_INVOKE,
8
+ INSTRUCTION_ADD_AUTHORITY,
9
+ INSTRUCTION_REMOVE_AUTHORITY,
10
+ AUTHORITY_TAG_PASSKEY,
11
+ AUTHORITY_TAG_PUBKEY,
12
+ } from './constants';
13
+
14
+ // Types
15
+ export type {
16
+ Authority,
17
+ CreateInstructionParams,
18
+ TransferInstructionParams,
19
+ ValidateInstructionParams,
20
+ AccountContext,
21
+ WalletSigner,
22
+ TransactionExecutionSummary,
23
+ PasskeyMetadata,
24
+ PasskeyRegistrationResult,
25
+ PasskeySigningResult,
26
+ PasskeyDiscoverableSigningResult,
27
+ } from './types';
28
+
29
+ // Instructions
30
+ export { encodeCreateInstruction } from './instructions/create';
31
+ export { encodeValidateInstruction } from './instructions/validate';
32
+ export { encodeTransferInstruction } from './instructions/transfer';
33
+ export { encodeInvokeInstruction } from './instructions/invoke';
34
+ export { encodeAddAuthorityInstruction } from './instructions/add-authority';
35
+ export { encodeRemoveAuthorityInstruction } from './instructions/remove-authority';
36
+ export { concatenateInstructions } from './instructions/shared';
37
+
38
+ // Challenge
39
+ export { createValidateChallenge } from './challenge';
40
+
41
+ // Seeds & derivation
42
+ export { createWalletSeed, deriveWalletAddress } from './seeds';
43
+
44
+ // Account context building
45
+ export { buildAccountContext, buildPasskeyReadWriteAccounts, FEE_PAYER_ADDRESS } from './context';
46
+
47
+ // Account parsing
48
+ export { parseWalletNonce, fetchWalletNonce } from './accounts';
49
+
50
+ // Crypto (platform-agnostic P-256 / DER utilities)
51
+ export {
52
+ parseDerSignature,
53
+ normalizeLowS,
54
+ normalizeSignatureComponent,
55
+ P256_N,
56
+ P256_HALF_N,
57
+ bytesToBigIntBE,
58
+ bigIntToBytesBE,
59
+ } from './crypto';
60
+
61
+ // Encoding (platform-agnostic byte/base64/hex utilities)
62
+ export {
63
+ arrayBufferToBase64Url,
64
+ base64UrlToArrayBuffer,
65
+ bytesToBase64Url,
66
+ base64UrlToBytes,
67
+ bytesToHex,
68
+ hexToBytes,
69
+ bytesEqual,
70
+ compareBytes,
71
+ uniqueAccounts,
72
+ } from './encoding';
73
+
@@ -0,0 +1,21 @@
1
+ import type { Authority } from '../types';
2
+ import {
3
+ AddAuthorityArgsBuilder,
4
+ PasskeyInstructionBuilder,
5
+ } from '../abi/thru/program/passkey_manager/types';
6
+ import { buildAuthority } from './create';
7
+
8
+ export function encodeAddAuthorityInstruction(params: { authority: Authority }): Uint8Array {
9
+ const authorityBytes = buildAuthority(params.authority);
10
+
11
+ const argsPayload = new AddAuthorityArgsBuilder()
12
+ .set_authority(authorityBytes)
13
+ .build();
14
+
15
+ return new PasskeyInstructionBuilder()
16
+ .payload()
17
+ .select('add_authority')
18
+ .writePayload(argsPayload)
19
+ .finish()
20
+ .build();
21
+ }
@@ -0,0 +1,54 @@
1
+ import type { Authority, CreateInstructionParams } from '../types';
2
+ import {
3
+ AuthorityBuilder,
4
+ CreateArgsBuilder,
5
+ PasskeyInstructionBuilder,
6
+ } from '../abi/thru/program/passkey_manager/types';
7
+
8
+ function buildAuthority(authority: Authority): Uint8Array {
9
+ const data = new Array<number>(64).fill(0);
10
+
11
+ if (authority.tag === 1) {
12
+ if (authority.pubkeyX.length !== 32) throw new Error('pubkeyX must be 32 bytes');
13
+ if (authority.pubkeyY.length !== 32) throw new Error('pubkeyY must be 32 bytes');
14
+ for (let i = 0; i < 32; i++) data[i] = authority.pubkeyX[i];
15
+ for (let i = 0; i < 32; i++) data[32 + i] = authority.pubkeyY[i];
16
+ } else if (authority.tag === 2) {
17
+ if (authority.pubkey.length !== 32) throw new Error('pubkey must be 32 bytes');
18
+ for (let i = 0; i < 32; i++) data[i] = authority.pubkey[i];
19
+ } else {
20
+ throw new Error('Invalid authority tag');
21
+ }
22
+
23
+ return new AuthorityBuilder()
24
+ .set_tag(authority.tag)
25
+ .set_data(data)
26
+ .build();
27
+ }
28
+
29
+ export { buildAuthority };
30
+
31
+ export function encodeCreateInstruction(params: CreateInstructionParams): Uint8Array {
32
+ const { walletAccountIdx, authority, seed, stateProof } = params;
33
+
34
+ if (seed.length !== 32) throw new Error('seed must be 32 bytes');
35
+ if (walletAccountIdx < 0 || walletAccountIdx > 0xffff) {
36
+ throw new Error('walletAccountIdx must be 0-65535');
37
+ }
38
+
39
+ const authorityBytes = buildAuthority(authority);
40
+
41
+ const argsPayload = new CreateArgsBuilder()
42
+ .set_wallet_account_idx(walletAccountIdx)
43
+ .set_authority(authorityBytes)
44
+ .set_seed(seed)
45
+ .set_state_proof(stateProof)
46
+ .build();
47
+
48
+ return new PasskeyInstructionBuilder()
49
+ .payload()
50
+ .select('create')
51
+ .writePayload(argsPayload)
52
+ .finish()
53
+ .build();
54
+ }
@@ -0,0 +1,25 @@
1
+ import {
2
+ InvokeArgsBuilder,
3
+ PasskeyInstructionBuilder,
4
+ } from '../abi/thru/program/passkey_manager/types';
5
+
6
+ export function encodeInvokeInstruction(
7
+ programPubkey: Uint8Array,
8
+ instruction: Uint8Array
9
+ ): Uint8Array {
10
+ if (programPubkey.length !== 32) {
11
+ throw new Error('Program pubkey must be 32 bytes');
12
+ }
13
+
14
+ const argsPayload = new InvokeArgsBuilder()
15
+ .set_program_pubkey(programPubkey)
16
+ .instr().write(instruction).finish()
17
+ .build();
18
+
19
+ return new PasskeyInstructionBuilder()
20
+ .payload()
21
+ .select('invoke')
22
+ .writePayload(argsPayload)
23
+ .finish()
24
+ .build();
25
+ }
@@ -0,0 +1,20 @@
1
+ import {
2
+ RemoveAuthorityArgsBuilder,
3
+ PasskeyInstructionBuilder,
4
+ } from '../abi/thru/program/passkey_manager/types';
5
+
6
+ export function encodeRemoveAuthorityInstruction(params: { authIdx: number }): Uint8Array {
7
+ const { authIdx } = params;
8
+ if (authIdx < 0 || authIdx > 0xff) throw new Error('authIdx must be 0-255');
9
+
10
+ const argsPayload = new RemoveAuthorityArgsBuilder()
11
+ .set_auth_idx(authIdx)
12
+ .build();
13
+
14
+ return new PasskeyInstructionBuilder()
15
+ .payload()
16
+ .select('remove_authority')
17
+ .writePayload(argsPayload)
18
+ .finish()
19
+ .build();
20
+ }
@@ -0,0 +1,12 @@
1
+ export function concatenateInstructions(instructions: Uint8Array[]): Uint8Array {
2
+ const totalLength = instructions.reduce((sum, instr) => sum + instr.length, 0);
3
+ const result = new Uint8Array(totalLength);
4
+
5
+ let offset = 0;
6
+ for (const instr of instructions) {
7
+ result.set(instr, offset);
8
+ offset += instr.length;
9
+ }
10
+
11
+ return result;
12
+ }
@@ -0,0 +1,30 @@
1
+ import type { TransferInstructionParams } from '../types';
2
+ import {
3
+ TransferArgsBuilder,
4
+ PasskeyInstructionBuilder,
5
+ } from '../abi/thru/program/passkey_manager/types';
6
+
7
+ export function encodeTransferInstruction(params: TransferInstructionParams): Uint8Array {
8
+ const { walletAccountIdx, toAccountIdx, amount } = params;
9
+
10
+ if (walletAccountIdx < 0 || walletAccountIdx > 0xffff) {
11
+ throw new Error('walletAccountIdx must be 0-65535');
12
+ }
13
+ if (toAccountIdx < 0 || toAccountIdx > 0xffff) {
14
+ throw new Error('toAccountIdx must be 0-65535');
15
+ }
16
+ if (amount < 0n) throw new Error('amount must be non-negative');
17
+
18
+ const argsPayload = new TransferArgsBuilder()
19
+ .set_wallet_account_idx(walletAccountIdx)
20
+ .set_to_account_idx(toAccountIdx)
21
+ .set_amount(amount as unknown as number)
22
+ .build();
23
+
24
+ return new PasskeyInstructionBuilder()
25
+ .payload()
26
+ .select('transfer')
27
+ .writePayload(argsPayload)
28
+ .finish()
29
+ .build();
30
+ }
@@ -0,0 +1,33 @@
1
+ import type { ValidateInstructionParams } from '../types';
2
+ import {
3
+ ValidateArgsBuilder,
4
+ PasskeyInstructionBuilder,
5
+ } from '../abi/thru/program/passkey_manager/types';
6
+
7
+ export function encodeValidateInstruction(params: ValidateInstructionParams): Uint8Array {
8
+ const { walletAccountIdx, authIdx, signatureR, signatureS, authenticatorData, clientDataJSON } =
9
+ params;
10
+
11
+ if (walletAccountIdx < 0 || walletAccountIdx > 0xffff) {
12
+ throw new Error('walletAccountIdx must be 0-65535');
13
+ }
14
+ if (authIdx < 0 || authIdx > 0xff) throw new Error('authIdx must be 0-255');
15
+ if (signatureR.length !== 32) throw new Error('signatureR must be 32 bytes');
16
+ if (signatureS.length !== 32) throw new Error('signatureS must be 32 bytes');
17
+
18
+ const argsPayload = new ValidateArgsBuilder()
19
+ .set_wallet_account_idx(walletAccountIdx)
20
+ .set_auth_idx(authIdx)
21
+ .set_signature_r(signatureR)
22
+ .set_signature_s(signatureS)
23
+ .authenticator_data().write(authenticatorData).finish()
24
+ .client_data().write(clientDataJSON).finish()
25
+ .build();
26
+
27
+ return new PasskeyInstructionBuilder()
28
+ .payload()
29
+ .select('validate')
30
+ .writePayload(argsPayload)
31
+ .finish()
32
+ .build();
33
+ }
package/src/seeds.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { decodeAddress } from '@thru/helpers';
2
+
3
+ /**
4
+ * Create a 32-byte seed from wallet name and passkey coordinates.
5
+ * SHA-256(walletName || pubkey_x || pubkey_y)
6
+ */
7
+ export async function createWalletSeed(
8
+ walletName: string,
9
+ pubkeyX: Uint8Array,
10
+ pubkeyY: Uint8Array
11
+ ): Promise<Uint8Array> {
12
+ if (pubkeyX.length !== 32) throw new Error('pubkeyX must be 32 bytes');
13
+ if (pubkeyY.length !== 32) throw new Error('pubkeyY must be 32 bytes');
14
+
15
+ const nameBytes = new TextEncoder().encode(walletName);
16
+ const data = new Uint8Array(nameBytes.length + 32 + 32);
17
+ data.set(nameBytes, 0);
18
+ data.set(pubkeyX, nameBytes.length);
19
+ data.set(pubkeyY, nameBytes.length + 32);
20
+
21
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
22
+ return new Uint8Array(hashBuffer);
23
+ }
24
+
25
+ /**
26
+ * Derive wallet account address from seed using proper PDA derivation.
27
+ * SHA256(program_address || is_ephemeral || seed)
28
+ */
29
+ export async function deriveWalletAddress(
30
+ seed: Uint8Array,
31
+ programAddress: string
32
+ ): Promise<Uint8Array> {
33
+ if (seed.length !== 32) {
34
+ throw new Error('seed must be 32 bytes');
35
+ }
36
+
37
+ const programBytes = decodeAddress(programAddress);
38
+ const isEphemeral = new Uint8Array([0]);
39
+
40
+ const preimage = new Uint8Array(32 + 1 + 32);
41
+ preimage.set(programBytes, 0);
42
+ preimage.set(isEphemeral, 32);
43
+ preimage.set(seed, 33);
44
+
45
+ const hashBuffer = await crypto.subtle.digest('SHA-256', preimage);
46
+ return new Uint8Array(hashBuffer);
47
+ }