@thru/programs 0.2.22
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/README.md +168 -0
- package/dist/passkey-manager/index.cjs +7364 -0
- package/dist/passkey-manager/index.cjs.map +1 -0
- package/dist/passkey-manager/index.d.cts +319 -0
- package/dist/passkey-manager/index.d.ts +319 -0
- package/dist/passkey-manager/index.js +7303 -0
- package/dist/passkey-manager/index.js.map +1 -0
- package/dist/token/index.cjs +7130 -0
- package/dist/token/index.cjs.map +1 -0
- package/dist/token/index.d.cts +1264 -0
- package/dist/token/index.d.ts +1264 -0
- package/dist/token/index.js +7102 -0
- package/dist/token/index.js.map +1 -0
- package/package.json +29 -0
- package/src/passkey-manager/abi/thru/blockchain/state_proof/types.ts +1667 -0
- package/src/passkey-manager/abi/thru/common/primitives/types.ts +2191 -0
- package/src/passkey-manager/abi/thru/program/passkey_manager/types.ts +4977 -0
- package/src/passkey-manager/accounts.ts +142 -0
- package/src/passkey-manager/authority.ts +148 -0
- package/src/passkey-manager/challenge.ts +39 -0
- package/src/passkey-manager/constants.ts +15 -0
- package/src/passkey-manager/context.ts +112 -0
- package/src/passkey-manager/crypto.ts +80 -0
- package/src/passkey-manager/encoding.ts +85 -0
- package/src/passkey-manager/index.ts +99 -0
- package/src/passkey-manager/instructions/add-authority.ts +21 -0
- package/src/passkey-manager/instructions/create.ts +54 -0
- package/src/passkey-manager/instructions/invoke.ts +25 -0
- package/src/passkey-manager/instructions/register-credential.ts +33 -0
- package/src/passkey-manager/instructions/remove-authority.ts +20 -0
- package/src/passkey-manager/instructions/shared.ts +12 -0
- package/src/passkey-manager/instructions/transfer.ts +30 -0
- package/src/passkey-manager/instructions/validate.ts +33 -0
- package/src/passkey-manager/seeds.ts +73 -0
- package/src/passkey-manager/types.ts +112 -0
- package/src/token/abi/thru/blockchain/state_proof/types.ts +1667 -0
- package/src/token/abi/thru/common/primitives/types.ts +1141 -0
- package/src/token/abi/thru/program/token/types.ts +6303 -0
- package/src/token/accounts.ts +72 -0
- package/src/token/constants.ts +3 -0
- package/src/token/derivation.ts +78 -0
- package/src/token/format.ts +24 -0
- package/src/token/index.ts +46 -0
- package/src/token/instructions/index.ts +5 -0
- package/src/token/instructions/initialize-account.ts +27 -0
- package/src/token/instructions/initialize-mint.ts +76 -0
- package/src/token/instructions/mint-to.ts +26 -0
- package/src/token/instructions/shared.ts +20 -0
- package/src/token/instructions/transfer.ts +24 -0
- package/src/token/types.ts +54 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +13 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { encodeAddress } from '@thru/sdk/helpers';
|
|
2
|
+
import {
|
|
3
|
+
CredentialLookup,
|
|
4
|
+
WalletAccount,
|
|
5
|
+
} from './abi/thru/program/passkey_manager/types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parse wallet account data to extract nonce.
|
|
9
|
+
*/
|
|
10
|
+
export function parseWalletNonce(data: Uint8Array): bigint {
|
|
11
|
+
const account = WalletAccount.from_array(data);
|
|
12
|
+
if (!account) return 0n;
|
|
13
|
+
return account.get_nonce();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fetch wallet nonce from the chain.
|
|
18
|
+
* Takes an SDK-like object with accounts.get() method.
|
|
19
|
+
*/
|
|
20
|
+
export async function fetchWalletNonce(
|
|
21
|
+
sdk: { accounts: { get: (address: string) => Promise<{ data?: { data?: Uint8Array } }> } },
|
|
22
|
+
walletAddress: string
|
|
23
|
+
): Promise<bigint> {
|
|
24
|
+
const account = await sdk.accounts.get(walletAddress);
|
|
25
|
+
const data = account.data?.data;
|
|
26
|
+
if (!data) return 0n;
|
|
27
|
+
const parsed = WalletAccount.from_array(data);
|
|
28
|
+
if (!parsed) return 0n;
|
|
29
|
+
return parsed.get_nonce();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* ------------------------------------------------------------------ */
|
|
33
|
+
/* Authority list parsing */
|
|
34
|
+
/* ------------------------------------------------------------------ */
|
|
35
|
+
|
|
36
|
+
export type ParsedAuthority =
|
|
37
|
+
| {
|
|
38
|
+
idx: number;
|
|
39
|
+
kind: 'passkey';
|
|
40
|
+
x: Uint8Array;
|
|
41
|
+
y: Uint8Array;
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
idx: number;
|
|
45
|
+
kind: 'pubkey';
|
|
46
|
+
pubkey: Uint8Array;
|
|
47
|
+
}
|
|
48
|
+
| {
|
|
49
|
+
idx: number;
|
|
50
|
+
kind: 'unknown';
|
|
51
|
+
tag: number;
|
|
52
|
+
data: Uint8Array;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export interface WalletAuthorities {
|
|
56
|
+
nonce: bigint;
|
|
57
|
+
authorities: ParsedAuthority[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const AUTHORITY_HEADER_BYTES = 9; /* num_auth (u8) + nonce (u64 LE) */
|
|
61
|
+
const AUTHORITY_ENTRY_BYTES = 65; /* tag (u8) + data (64) */
|
|
62
|
+
|
|
63
|
+
function readU64LE(data: Uint8Array, offset: number): bigint {
|
|
64
|
+
if (offset + 8 > data.length) {
|
|
65
|
+
throw new Error('Out of bounds');
|
|
66
|
+
}
|
|
67
|
+
let value = 0n;
|
|
68
|
+
for (let i = 0; i < 8; i++) {
|
|
69
|
+
value |= BigInt(data[offset + i]) << (8n * BigInt(i));
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse the on-chain WalletAccount data buffer into its nonce and full
|
|
76
|
+
* authority list. The on-chain layout is:
|
|
77
|
+
*
|
|
78
|
+
* num_auth: u8
|
|
79
|
+
* nonce: u64 LE
|
|
80
|
+
* authorities[num_auth + 1]: { tag: u8, data: [u8; 64] }
|
|
81
|
+
*
|
|
82
|
+
* (`num_auth + 1` because num_auth stores the count minus one.)
|
|
83
|
+
*/
|
|
84
|
+
export function parseWalletAuthorities(data: Uint8Array): WalletAuthorities {
|
|
85
|
+
if (data.length < AUTHORITY_HEADER_BYTES) {
|
|
86
|
+
throw new Error('Wallet data too small');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const numAuth = data[0];
|
|
90
|
+
const nonce = readU64LE(data, 1);
|
|
91
|
+
|
|
92
|
+
const count = numAuth + 1;
|
|
93
|
+
const required =
|
|
94
|
+
AUTHORITY_HEADER_BYTES + count * AUTHORITY_ENTRY_BYTES;
|
|
95
|
+
if (data.length < required) {
|
|
96
|
+
throw new Error('Wallet data truncated');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const authorities: ParsedAuthority[] = [];
|
|
100
|
+
for (let idx = 0; idx < count; idx++) {
|
|
101
|
+
const offset = AUTHORITY_HEADER_BYTES + idx * AUTHORITY_ENTRY_BYTES;
|
|
102
|
+
const tag = data[offset];
|
|
103
|
+
const payload = data.slice(offset + 1, offset + AUTHORITY_ENTRY_BYTES);
|
|
104
|
+
|
|
105
|
+
if (tag === 1) {
|
|
106
|
+
authorities.push({
|
|
107
|
+
idx,
|
|
108
|
+
kind: 'passkey',
|
|
109
|
+
x: payload.slice(0, 32),
|
|
110
|
+
y: payload.slice(32, 64),
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (tag === 2) {
|
|
116
|
+
authorities.push({ idx, kind: 'pubkey', pubkey: payload.slice(0, 32) });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
authorities.push({ idx, kind: 'unknown', tag, data: payload });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { nonce, authorities };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Encode a 32-byte pubkey to its base58 wallet address representation.
|
|
128
|
+
*/
|
|
129
|
+
export function formatAuthorityPubkey(pubkey: Uint8Array): string {
|
|
130
|
+
return encodeAddress(pubkey);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Parse a CredentialLookup account and return the wallet account pubkey
|
|
135
|
+
* stored inside it.
|
|
136
|
+
*/
|
|
137
|
+
export function parseCredentialLookupWallet(data: Uint8Array): Uint8Array | null {
|
|
138
|
+
const lookup = CredentialLookup.from_array(data);
|
|
139
|
+
if (!lookup) return null;
|
|
140
|
+
const wallet = lookup.get_wallet() as unknown as { buffer?: Uint8Array };
|
|
141
|
+
return wallet.buffer ? wallet.buffer.slice() : null;
|
|
142
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { parseWalletAuthorities, type ParsedAuthority } from './accounts';
|
|
2
|
+
import { bytesToHex } from './encoding';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_AUTHORITY_TARGET_CONCURRENCY = 8;
|
|
5
|
+
|
|
6
|
+
export interface PasskeyAuthorityIdentity {
|
|
7
|
+
publicKeyX?: string | null;
|
|
8
|
+
publicKeyY?: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CheckablePasskeyAuthorityIdentity {
|
|
12
|
+
publicKeyX: string;
|
|
13
|
+
publicKeyY: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AccountLike {
|
|
17
|
+
data?: {
|
|
18
|
+
data?: Uint8Array;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ThruAccountClient {
|
|
23
|
+
accounts: {
|
|
24
|
+
get(address: string): Promise<AccountLike>;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PasskeyAuthorityTarget<T> {
|
|
29
|
+
account: T;
|
|
30
|
+
walletAddress: string;
|
|
31
|
+
authIdx: number;
|
|
32
|
+
authorities: ParsedAuthority[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PreparePasskeyAuthorityTargetsOptions<T> {
|
|
36
|
+
accounts: T[];
|
|
37
|
+
passkey?: PasskeyAuthorityIdentity | null;
|
|
38
|
+
thru: ThruAccountClient;
|
|
39
|
+
getWalletAddress: (account: T) => string;
|
|
40
|
+
concurrency?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getAccountData(account: AccountLike | null | undefined): Uint8Array | null {
|
|
44
|
+
return account?.data?.data ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isPasskeyAuthorityCheckable(
|
|
48
|
+
passkey: PasskeyAuthorityIdentity | null | undefined
|
|
49
|
+
): passkey is CheckablePasskeyAuthorityIdentity {
|
|
50
|
+
return Boolean(passkey?.publicKeyX && passkey.publicKeyY);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function findPasskeyAuthorityIndexForIdentity(
|
|
54
|
+
authorities: ParsedAuthority[],
|
|
55
|
+
passkey: PasskeyAuthorityIdentity | null | undefined
|
|
56
|
+
): number | null {
|
|
57
|
+
if (!isPasskeyAuthorityCheckable(passkey)) return null;
|
|
58
|
+
|
|
59
|
+
const expectedX = passkey.publicKeyX.toLowerCase();
|
|
60
|
+
const expectedY = passkey.publicKeyY.toLowerCase();
|
|
61
|
+
const authority = authorities.find(
|
|
62
|
+
(item) =>
|
|
63
|
+
item.kind === 'passkey' &&
|
|
64
|
+
bytesToHex(item.x).toLowerCase() === expectedX &&
|
|
65
|
+
bytesToHex(item.y).toLowerCase() === expectedY
|
|
66
|
+
);
|
|
67
|
+
return authority?.idx ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function findPasskeyAuthorityIndexInWalletData(
|
|
71
|
+
walletData: Uint8Array,
|
|
72
|
+
passkey: PasskeyAuthorityIdentity | null | undefined
|
|
73
|
+
): { authIdx: number; authorities: ParsedAuthority[] } | null {
|
|
74
|
+
const parsed = parseWalletAuthorities(walletData);
|
|
75
|
+
const authIdx = findPasskeyAuthorityIndexForIdentity(parsed.authorities, passkey);
|
|
76
|
+
return authIdx === null ? null : { authIdx, authorities: parsed.authorities };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function resolvePasskeyAuthorityIndex(params: {
|
|
80
|
+
thru: ThruAccountClient;
|
|
81
|
+
walletAddress: string;
|
|
82
|
+
passkey?: PasskeyAuthorityIdentity | null;
|
|
83
|
+
}): Promise<number | null> {
|
|
84
|
+
const walletAccount = await params.thru.accounts.get(params.walletAddress);
|
|
85
|
+
const walletData = getAccountData(walletAccount);
|
|
86
|
+
if (!walletData) return null;
|
|
87
|
+
|
|
88
|
+
return findPasskeyAuthorityIndexInWalletData(walletData, params.passkey)?.authIdx ?? null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function mapWithConcurrency<T, R>(
|
|
92
|
+
items: T[],
|
|
93
|
+
concurrency: number,
|
|
94
|
+
mapper: (item: T) => Promise<R>
|
|
95
|
+
): Promise<R[]> {
|
|
96
|
+
const results = new Array<R>(items.length);
|
|
97
|
+
let nextIndex = 0;
|
|
98
|
+
const workerCount = Math.max(1, Math.min(Math.floor(concurrency), items.length));
|
|
99
|
+
|
|
100
|
+
await Promise.all(
|
|
101
|
+
Array.from({ length: workerCount }, async () => {
|
|
102
|
+
while (nextIndex < items.length) {
|
|
103
|
+
const currentIndex = nextIndex;
|
|
104
|
+
nextIndex += 1;
|
|
105
|
+
results[currentIndex] = await mapper(items[currentIndex]);
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return results;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function preparePasskeyAuthorityTargets<T>({
|
|
114
|
+
accounts,
|
|
115
|
+
passkey,
|
|
116
|
+
thru,
|
|
117
|
+
getWalletAddress,
|
|
118
|
+
concurrency = DEFAULT_AUTHORITY_TARGET_CONCURRENCY,
|
|
119
|
+
}: PreparePasskeyAuthorityTargetsOptions<T>): Promise<PasskeyAuthorityTarget<T>[]> {
|
|
120
|
+
if (!isPasskeyAuthorityCheckable(passkey) || accounts.length === 0) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const targets = await mapWithConcurrency(accounts, concurrency, async (account) => {
|
|
125
|
+
const walletAddress = getWalletAddress(account);
|
|
126
|
+
try {
|
|
127
|
+
const walletAccount = await thru.accounts.get(walletAddress);
|
|
128
|
+
const walletData = getAccountData(walletAccount);
|
|
129
|
+
if (!walletData) return null;
|
|
130
|
+
|
|
131
|
+
const target = findPasskeyAuthorityIndexInWalletData(walletData, passkey);
|
|
132
|
+
if (!target) return null;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
account,
|
|
136
|
+
walletAddress,
|
|
137
|
+
authIdx: target.authIdx,
|
|
138
|
+
authorities: target.authorities,
|
|
139
|
+
};
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return targets.filter(
|
|
146
|
+
(target): target is PasskeyAuthorityTarget<T> => target !== null
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -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,15 @@
|
|
|
1
|
+
export const PASSKEY_MANAGER_PROGRAM_ADDRESS =
|
|
2
|
+
'taUDdQyFxvM5i0HFRkEK3W45kWLyblAHSnMg4zplgUnz6Z';
|
|
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
|
+
export const INSTRUCTION_REGISTER_CREDENTIAL = 0x06;
|
|
12
|
+
|
|
13
|
+
// Authority tags
|
|
14
|
+
export const AUTHORITY_TAG_PASSKEY = 1;
|
|
15
|
+
export const AUTHORITY_TAG_PUBKEY = 2;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { encodeAddress, decodeAddress } from '@thru/sdk/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
|
+
}
|
|
@@ -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,85 @@
|
|
|
1
|
+
import { hexToBytes as sharedHexToBytes } from '@thru/sdk/helpers';
|
|
2
|
+
|
|
3
|
+
export function arrayBufferToBase64Url(buffer: ArrayBuffer | SharedArrayBuffer): string {
|
|
4
|
+
const bytes = new Uint8Array(buffer);
|
|
5
|
+
let base64 = '';
|
|
6
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
7
|
+
base64 += String.fromCharCode(bytes[i]);
|
|
8
|
+
}
|
|
9
|
+
return btoa(base64).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function base64UrlToArrayBuffer(base64Url: string): ArrayBuffer {
|
|
13
|
+
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
14
|
+
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
|
|
15
|
+
const binary = atob(padded);
|
|
16
|
+
const bytes = new Uint8Array(binary.length);
|
|
17
|
+
for (let i = 0; i < binary.length; i++) {
|
|
18
|
+
bytes[i] = binary.charCodeAt(i);
|
|
19
|
+
}
|
|
20
|
+
return bytes.buffer;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function bytesToBase64Url(bytes: Uint8Array): string {
|
|
24
|
+
const slice = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
25
|
+
return arrayBufferToBase64Url(slice);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function bytesToBase64(bytes: Uint8Array): string {
|
|
29
|
+
type BufferLike = {
|
|
30
|
+
from(value: Uint8Array): { toString(encoding: 'base64'): string };
|
|
31
|
+
};
|
|
32
|
+
const globalBuffer =
|
|
33
|
+
typeof globalThis !== 'undefined' ? (globalThis as { Buffer?: BufferLike }).Buffer : undefined;
|
|
34
|
+
if (globalBuffer) {
|
|
35
|
+
return globalBuffer.from(bytes).toString('base64');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof btoa === 'function') {
|
|
39
|
+
let binary = '';
|
|
40
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
41
|
+
binary += String.fromCharCode(bytes[i]);
|
|
42
|
+
}
|
|
43
|
+
return btoa(binary);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw new Error('Base64 encoding is not supported in this environment');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function base64UrlToBytes(base64Url: string): Uint8Array {
|
|
50
|
+
return new Uint8Array(base64UrlToArrayBuffer(base64Url));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function bytesToHex(bytes: Uint8Array): string {
|
|
54
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function hexToBytes(hex: string): Uint8Array {
|
|
58
|
+
return sharedHexToBytes(hex);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
62
|
+
if (a.length !== b.length) return false;
|
|
63
|
+
for (let i = 0; i < a.length; i++) {
|
|
64
|
+
if (a[i] !== b[i]) return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function compareBytes(a: Uint8Array, b: Uint8Array): number {
|
|
70
|
+
const len = Math.min(a.length, b.length);
|
|
71
|
+
for (let i = 0; i < len; i++) {
|
|
72
|
+
if (a[i] !== b[i]) return a[i] - b[i];
|
|
73
|
+
}
|
|
74
|
+
return a.length - b.length;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function uniqueAccounts(accounts: Uint8Array[]): Uint8Array[] {
|
|
78
|
+
const unique: Uint8Array[] = [];
|
|
79
|
+
for (const account of accounts) {
|
|
80
|
+
if (!unique.some((candidate) => bytesEqual(candidate, account))) {
|
|
81
|
+
unique.push(account);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return unique;
|
|
85
|
+
}
|