@thru/abi-utils 0.1.36
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/index.d.ts +95 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -0
- package/src/derivation.ts +111 -0
- package/src/index.ts +4 -0
- package/src/parser.ts +78 -0
- package/src/types.ts +33 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +9 -0
- package/vitest.config.ts +7 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives the ABI seed bytes from a program account pubkey.
|
|
3
|
+
*
|
|
4
|
+
* Algorithm:
|
|
5
|
+
* 1. Get program account bytes (32 bytes)
|
|
6
|
+
* 2. Append "_abi_account" suffix (12 bytes)
|
|
7
|
+
* 3. SHA256 hash the combined 44 bytes
|
|
8
|
+
* 4. Return the 32-byte hash as the seed
|
|
9
|
+
*
|
|
10
|
+
* @param programAccountBytes - The 32-byte program account public key
|
|
11
|
+
* @returns 32-byte seed for PDA derivation
|
|
12
|
+
*/
|
|
13
|
+
declare function deriveAbiSeed(programAccountBytes: Uint8Array): Promise<Uint8Array>;
|
|
14
|
+
/**
|
|
15
|
+
* Derives a program address (PDA) from a seed and program ID.
|
|
16
|
+
*
|
|
17
|
+
* This implements the Thru PDA derivation algorithm:
|
|
18
|
+
* 1. Combine seed + program ID bytes
|
|
19
|
+
* 2. SHA256 hash the combined bytes
|
|
20
|
+
* 3. Return as the derived address
|
|
21
|
+
*
|
|
22
|
+
* @param seed - The 32-byte seed
|
|
23
|
+
* @param programId - The program ID bytes (32 bytes)
|
|
24
|
+
* @param ephemeral - Whether to derive an ephemeral address (affects prefix byte)
|
|
25
|
+
* @returns The derived 32-byte address
|
|
26
|
+
*/
|
|
27
|
+
declare function deriveProgramAddress(seed: Uint8Array, programId: Uint8Array, ephemeral?: boolean): Promise<Uint8Array>;
|
|
28
|
+
/**
|
|
29
|
+
* Derives the ABI account address for a given program account.
|
|
30
|
+
*
|
|
31
|
+
* @param programAccount - The program account address (ta-prefixed string or 32-byte array)
|
|
32
|
+
* @param abiManagerProgramId - The ABI manager program ID (ta-prefixed string or 32-byte array)
|
|
33
|
+
* @param ephemeral - Whether to derive an ephemeral address
|
|
34
|
+
* @returns The derived ABI account address as a ta-prefixed string
|
|
35
|
+
*/
|
|
36
|
+
declare function deriveAbiAddress(programAccount: string | Uint8Array, abiManagerProgramId: string | Uint8Array, ephemeral?: boolean): Promise<string>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* ABI account state values
|
|
40
|
+
*/
|
|
41
|
+
declare const ABI_STATE: {
|
|
42
|
+
readonly OPEN: 0;
|
|
43
|
+
readonly FINALIZED: 1;
|
|
44
|
+
};
|
|
45
|
+
type AbiAccountState = (typeof ABI_STATE)[keyof typeof ABI_STATE];
|
|
46
|
+
/**
|
|
47
|
+
* Parsed header from an ABI account's on-chain data
|
|
48
|
+
*/
|
|
49
|
+
interface AbiAccountHeader {
|
|
50
|
+
/** The program meta account this ABI is associated with */
|
|
51
|
+
programMetaAccount: Uint8Array;
|
|
52
|
+
/** Revision number (incremented on each upgrade) */
|
|
53
|
+
revision: bigint;
|
|
54
|
+
/** Account state: OPEN (0) or FINALIZED (1) */
|
|
55
|
+
state: AbiAccountState;
|
|
56
|
+
/** Size of the ABI content in bytes */
|
|
57
|
+
contentSize: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Full ABI account data including parsed YAML content
|
|
61
|
+
*/
|
|
62
|
+
interface AbiAccountData extends AbiAccountHeader {
|
|
63
|
+
/** Raw ABI YAML content bytes */
|
|
64
|
+
contentBytes: Uint8Array;
|
|
65
|
+
/** ABI YAML content as string */
|
|
66
|
+
content: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Size of the ABI account header in bytes:
|
|
71
|
+
* - 32 bytes: program_meta_acc (pubkey)
|
|
72
|
+
* - 8 bytes: revision (u64)
|
|
73
|
+
* - 1 byte: state (u8)
|
|
74
|
+
* - 4 bytes: content_sz (u32)
|
|
75
|
+
* Total: 45 bytes
|
|
76
|
+
*/
|
|
77
|
+
declare const ABI_ACCOUNT_HEADER_SIZE: number;
|
|
78
|
+
/**
|
|
79
|
+
* Parses the header from ABI account data.
|
|
80
|
+
*
|
|
81
|
+
* @param data - Raw account data bytes
|
|
82
|
+
* @returns Parsed header
|
|
83
|
+
* @throws If data is too small for the header
|
|
84
|
+
*/
|
|
85
|
+
declare function parseAbiAccountHeader(data: Uint8Array): AbiAccountHeader;
|
|
86
|
+
/**
|
|
87
|
+
* Parses the full ABI account data including content.
|
|
88
|
+
*
|
|
89
|
+
* @param data - Raw account data bytes
|
|
90
|
+
* @returns Parsed header and content
|
|
91
|
+
* @throws If data is too small or content size exceeds available data
|
|
92
|
+
*/
|
|
93
|
+
declare function parseAbiAccountData(data: Uint8Array): AbiAccountData;
|
|
94
|
+
|
|
95
|
+
export { ABI_ACCOUNT_HEADER_SIZE, ABI_STATE, type AbiAccountData, type AbiAccountHeader, type AbiAccountState, deriveAbiAddress, deriveAbiSeed, deriveProgramAddress, parseAbiAccountData, parseAbiAccountHeader };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// src/derivation.ts
|
|
2
|
+
import { decodeAddress, encodeAddress } from "@thru/helpers";
|
|
3
|
+
var ABI_SEED_SUFFIX = "_abi_account";
|
|
4
|
+
async function deriveAbiSeed(programAccountBytes) {
|
|
5
|
+
if (programAccountBytes.length !== 32) {
|
|
6
|
+
throw new Error(`Expected 32-byte program account, got ${programAccountBytes.length} bytes`);
|
|
7
|
+
}
|
|
8
|
+
const suffixBytes = new TextEncoder().encode(ABI_SEED_SUFFIX);
|
|
9
|
+
const combined = new Uint8Array(programAccountBytes.length + suffixBytes.length);
|
|
10
|
+
combined.set(programAccountBytes, 0);
|
|
11
|
+
combined.set(suffixBytes, programAccountBytes.length);
|
|
12
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", combined);
|
|
13
|
+
return new Uint8Array(hashBuffer);
|
|
14
|
+
}
|
|
15
|
+
async function deriveProgramAddress(seed, programId, ephemeral = false) {
|
|
16
|
+
if (seed.length !== 32) {
|
|
17
|
+
throw new Error(`Expected 32-byte seed, got ${seed.length} bytes`);
|
|
18
|
+
}
|
|
19
|
+
if (programId.length !== 32) {
|
|
20
|
+
throw new Error(`Expected 32-byte program ID, got ${programId.length} bytes`);
|
|
21
|
+
}
|
|
22
|
+
const combined = new Uint8Array(seed.length + programId.length);
|
|
23
|
+
combined.set(seed, 0);
|
|
24
|
+
combined.set(programId, seed.length);
|
|
25
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", combined);
|
|
26
|
+
const result = new Uint8Array(hashBuffer);
|
|
27
|
+
if (ephemeral) {
|
|
28
|
+
result[0] = result[0] | 128;
|
|
29
|
+
} else {
|
|
30
|
+
result[0] = result[0] & 127;
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
async function deriveAbiAddress(programAccount, abiManagerProgramId, ephemeral = false) {
|
|
35
|
+
const programAccountBytes = typeof programAccount === "string" ? decodeAddress(programAccount) : programAccount;
|
|
36
|
+
const abiManagerBytes = typeof abiManagerProgramId === "string" ? decodeAddress(abiManagerProgramId) : abiManagerProgramId;
|
|
37
|
+
const seed = await deriveAbiSeed(programAccountBytes);
|
|
38
|
+
const abiAccountBytes = await deriveProgramAddress(seed, abiManagerBytes, ephemeral);
|
|
39
|
+
return encodeAddress(abiAccountBytes);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/types.ts
|
|
43
|
+
var ABI_STATE = {
|
|
44
|
+
OPEN: 0,
|
|
45
|
+
FINALIZED: 1
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/parser.ts
|
|
49
|
+
var ABI_ACCOUNT_HEADER_SIZE = 32 + 8 + 1 + 4;
|
|
50
|
+
function parseAbiAccountHeader(data) {
|
|
51
|
+
if (data.length < ABI_ACCOUNT_HEADER_SIZE) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`ABI account data too small: ${data.length} bytes, expected at least ${ABI_ACCOUNT_HEADER_SIZE}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
57
|
+
const programMetaAccount = data.slice(0, 32);
|
|
58
|
+
const revision = view.getBigUint64(32, true);
|
|
59
|
+
const stateRaw = data[40];
|
|
60
|
+
const state = stateRaw === ABI_STATE.FINALIZED ? ABI_STATE.FINALIZED : ABI_STATE.OPEN;
|
|
61
|
+
const contentSize = view.getUint32(41, true);
|
|
62
|
+
return {
|
|
63
|
+
programMetaAccount,
|
|
64
|
+
revision,
|
|
65
|
+
state,
|
|
66
|
+
contentSize
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function parseAbiAccountData(data) {
|
|
70
|
+
const header = parseAbiAccountHeader(data);
|
|
71
|
+
const expectedSize = ABI_ACCOUNT_HEADER_SIZE + header.contentSize;
|
|
72
|
+
if (data.length < expectedSize) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`ABI account data incomplete: ${data.length} bytes, expected ${expectedSize} (header + ${header.contentSize} content bytes)`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const contentBytes = data.slice(ABI_ACCOUNT_HEADER_SIZE, ABI_ACCOUNT_HEADER_SIZE + header.contentSize);
|
|
78
|
+
const content = new TextDecoder().decode(contentBytes);
|
|
79
|
+
return {
|
|
80
|
+
...header,
|
|
81
|
+
contentBytes,
|
|
82
|
+
content
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
ABI_ACCOUNT_HEADER_SIZE,
|
|
87
|
+
ABI_STATE,
|
|
88
|
+
deriveAbiAddress,
|
|
89
|
+
deriveAbiSeed,
|
|
90
|
+
deriveProgramAddress,
|
|
91
|
+
parseAbiAccountData,
|
|
92
|
+
parseAbiAccountHeader
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/derivation.ts","../src/types.ts","../src/parser.ts"],"sourcesContent":["import { decodeAddress, encodeAddress } from \"@thru/helpers\";\n\n/**\n * Suffix appended to program account bytes for ABI seed derivation\n */\nconst ABI_SEED_SUFFIX = \"_abi_account\";\n\n/**\n * Derives the ABI seed bytes from a program account pubkey.\n *\n * Algorithm:\n * 1. Get program account bytes (32 bytes)\n * 2. Append \"_abi_account\" suffix (12 bytes)\n * 3. SHA256 hash the combined 44 bytes\n * 4. Return the 32-byte hash as the seed\n *\n * @param programAccountBytes - The 32-byte program account public key\n * @returns 32-byte seed for PDA derivation\n */\nexport async function deriveAbiSeed(programAccountBytes: Uint8Array): Promise<Uint8Array> {\n if (programAccountBytes.length !== 32) {\n throw new Error(`Expected 32-byte program account, got ${programAccountBytes.length} bytes`);\n }\n\n // Combine program account bytes with suffix\n const suffixBytes = new TextEncoder().encode(ABI_SEED_SUFFIX);\n const combined = new Uint8Array(programAccountBytes.length + suffixBytes.length);\n combined.set(programAccountBytes, 0);\n combined.set(suffixBytes, programAccountBytes.length);\n\n // SHA256 hash\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", combined);\n return new Uint8Array(hashBuffer);\n}\n\n/**\n * Derives a program address (PDA) from a seed and program ID.\n *\n * This implements the Thru PDA derivation algorithm:\n * 1. Combine seed + program ID bytes\n * 2. SHA256 hash the combined bytes\n * 3. Return as the derived address\n *\n * @param seed - The 32-byte seed\n * @param programId - The program ID bytes (32 bytes)\n * @param ephemeral - Whether to derive an ephemeral address (affects prefix byte)\n * @returns The derived 32-byte address\n */\nexport async function deriveProgramAddress(\n seed: Uint8Array,\n programId: Uint8Array,\n ephemeral: boolean = false\n): Promise<Uint8Array> {\n if (seed.length !== 32) {\n throw new Error(`Expected 32-byte seed, got ${seed.length} bytes`);\n }\n if (programId.length !== 32) {\n throw new Error(`Expected 32-byte program ID, got ${programId.length} bytes`);\n }\n\n // Combine seed + program ID\n const combined = new Uint8Array(seed.length + programId.length);\n combined.set(seed, 0);\n combined.set(programId, seed.length);\n\n // SHA256 hash\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", combined);\n const result = new Uint8Array(hashBuffer);\n\n // Set the ephemeral bit in the first byte if needed\n // Ephemeral addresses have bit 0x80 set in the first byte\n if (ephemeral) {\n result[0] = result[0] | 0x80;\n } else {\n result[0] = result[0] & 0x7f;\n }\n\n return result;\n}\n\n/**\n * Derives the ABI account address for a given program account.\n *\n * @param programAccount - The program account address (ta-prefixed string or 32-byte array)\n * @param abiManagerProgramId - The ABI manager program ID (ta-prefixed string or 32-byte array)\n * @param ephemeral - Whether to derive an ephemeral address\n * @returns The derived ABI account address as a ta-prefixed string\n */\nexport async function deriveAbiAddress(\n programAccount: string | Uint8Array,\n abiManagerProgramId: string | Uint8Array,\n ephemeral: boolean = false\n): Promise<string> {\n // Convert string addresses to bytes\n const programAccountBytes = typeof programAccount === \"string\"\n ? decodeAddress(programAccount)\n : programAccount;\n\n const abiManagerBytes = typeof abiManagerProgramId === \"string\"\n ? decodeAddress(abiManagerProgramId)\n : abiManagerProgramId;\n\n // Derive the seed\n const seed = await deriveAbiSeed(programAccountBytes);\n\n // Derive the PDA\n const abiAccountBytes = await deriveProgramAddress(seed, abiManagerBytes, ephemeral);\n\n // Encode as ta-prefixed address\n return encodeAddress(abiAccountBytes);\n}\n","/**\n * ABI account state values\n */\nexport const ABI_STATE = {\n OPEN: 0x00,\n FINALIZED: 0x01,\n} as const;\n\nexport type AbiAccountState = (typeof ABI_STATE)[keyof typeof ABI_STATE];\n\n/**\n * Parsed header from an ABI account's on-chain data\n */\nexport interface AbiAccountHeader {\n /** The program meta account this ABI is associated with */\n programMetaAccount: Uint8Array;\n /** Revision number (incremented on each upgrade) */\n revision: bigint;\n /** Account state: OPEN (0) or FINALIZED (1) */\n state: AbiAccountState;\n /** Size of the ABI content in bytes */\n contentSize: number;\n}\n\n/**\n * Full ABI account data including parsed YAML content\n */\nexport interface AbiAccountData extends AbiAccountHeader {\n /** Raw ABI YAML content bytes */\n contentBytes: Uint8Array;\n /** ABI YAML content as string */\n content: string;\n}\n","import type { AbiAccountHeader, AbiAccountData, AbiAccountState } from \"./types\";\nimport { ABI_STATE } from \"./types\";\n\n/**\n * Size of the ABI account header in bytes:\n * - 32 bytes: program_meta_acc (pubkey)\n * - 8 bytes: revision (u64)\n * - 1 byte: state (u8)\n * - 4 bytes: content_sz (u32)\n * Total: 45 bytes\n */\nexport const ABI_ACCOUNT_HEADER_SIZE = 32 + 8 + 1 + 4;\n\n/**\n * Parses the header from ABI account data.\n *\n * @param data - Raw account data bytes\n * @returns Parsed header\n * @throws If data is too small for the header\n */\nexport function parseAbiAccountHeader(data: Uint8Array): AbiAccountHeader {\n if (data.length < ABI_ACCOUNT_HEADER_SIZE) {\n throw new Error(\n `ABI account data too small: ${data.length} bytes, expected at least ${ABI_ACCOUNT_HEADER_SIZE}`\n );\n }\n\n const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n\n // program_meta_acc: bytes 0-31 (32 bytes)\n const programMetaAccount = data.slice(0, 32);\n\n // revision: bytes 32-39 (8 bytes, little-endian u64)\n const revision = view.getBigUint64(32, true);\n\n // state: byte 40 (1 byte)\n const stateRaw = data[40];\n const state: AbiAccountState = stateRaw === ABI_STATE.FINALIZED\n ? ABI_STATE.FINALIZED\n : ABI_STATE.OPEN;\n\n // content_sz: bytes 41-44 (4 bytes, little-endian u32)\n const contentSize = view.getUint32(41, true);\n\n return {\n programMetaAccount,\n revision,\n state,\n contentSize,\n };\n}\n\n/**\n * Parses the full ABI account data including content.\n *\n * @param data - Raw account data bytes\n * @returns Parsed header and content\n * @throws If data is too small or content size exceeds available data\n */\nexport function parseAbiAccountData(data: Uint8Array): AbiAccountData {\n const header = parseAbiAccountHeader(data);\n\n const expectedSize = ABI_ACCOUNT_HEADER_SIZE + header.contentSize;\n if (data.length < expectedSize) {\n throw new Error(\n `ABI account data incomplete: ${data.length} bytes, expected ${expectedSize} (header + ${header.contentSize} content bytes)`\n );\n }\n\n const contentBytes = data.slice(ABI_ACCOUNT_HEADER_SIZE, ABI_ACCOUNT_HEADER_SIZE + header.contentSize);\n const content = new TextDecoder().decode(contentBytes);\n\n return {\n ...header,\n contentBytes,\n content,\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thru/abi-utils",
|
|
3
|
+
"version": "0.1.36",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@thru/helpers": "0.1.36"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^24.10.1",
|
|
18
|
+
"tsup": "^8.5.0",
|
|
19
|
+
"typescript": "^5.9.3",
|
|
20
|
+
"vitest": "^3.2.4"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"dev": "tsup --watch",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest watch",
|
|
27
|
+
"clean": "rm -rf dist"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { decodeAddress, encodeAddress } from "@thru/helpers";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Suffix appended to program account bytes for ABI seed derivation
|
|
5
|
+
*/
|
|
6
|
+
const ABI_SEED_SUFFIX = "_abi_account";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Derives the ABI seed bytes from a program account pubkey.
|
|
10
|
+
*
|
|
11
|
+
* Algorithm:
|
|
12
|
+
* 1. Get program account bytes (32 bytes)
|
|
13
|
+
* 2. Append "_abi_account" suffix (12 bytes)
|
|
14
|
+
* 3. SHA256 hash the combined 44 bytes
|
|
15
|
+
* 4. Return the 32-byte hash as the seed
|
|
16
|
+
*
|
|
17
|
+
* @param programAccountBytes - The 32-byte program account public key
|
|
18
|
+
* @returns 32-byte seed for PDA derivation
|
|
19
|
+
*/
|
|
20
|
+
export async function deriveAbiSeed(programAccountBytes: Uint8Array): Promise<Uint8Array> {
|
|
21
|
+
if (programAccountBytes.length !== 32) {
|
|
22
|
+
throw new Error(`Expected 32-byte program account, got ${programAccountBytes.length} bytes`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Combine program account bytes with suffix
|
|
26
|
+
const suffixBytes = new TextEncoder().encode(ABI_SEED_SUFFIX);
|
|
27
|
+
const combined = new Uint8Array(programAccountBytes.length + suffixBytes.length);
|
|
28
|
+
combined.set(programAccountBytes, 0);
|
|
29
|
+
combined.set(suffixBytes, programAccountBytes.length);
|
|
30
|
+
|
|
31
|
+
// SHA256 hash
|
|
32
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", combined);
|
|
33
|
+
return new Uint8Array(hashBuffer);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Derives a program address (PDA) from a seed and program ID.
|
|
38
|
+
*
|
|
39
|
+
* This implements the Thru PDA derivation algorithm:
|
|
40
|
+
* 1. Combine seed + program ID bytes
|
|
41
|
+
* 2. SHA256 hash the combined bytes
|
|
42
|
+
* 3. Return as the derived address
|
|
43
|
+
*
|
|
44
|
+
* @param seed - The 32-byte seed
|
|
45
|
+
* @param programId - The program ID bytes (32 bytes)
|
|
46
|
+
* @param ephemeral - Whether to derive an ephemeral address (affects prefix byte)
|
|
47
|
+
* @returns The derived 32-byte address
|
|
48
|
+
*/
|
|
49
|
+
export async function deriveProgramAddress(
|
|
50
|
+
seed: Uint8Array,
|
|
51
|
+
programId: Uint8Array,
|
|
52
|
+
ephemeral: boolean = false
|
|
53
|
+
): Promise<Uint8Array> {
|
|
54
|
+
if (seed.length !== 32) {
|
|
55
|
+
throw new Error(`Expected 32-byte seed, got ${seed.length} bytes`);
|
|
56
|
+
}
|
|
57
|
+
if (programId.length !== 32) {
|
|
58
|
+
throw new Error(`Expected 32-byte program ID, got ${programId.length} bytes`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Combine seed + program ID
|
|
62
|
+
const combined = new Uint8Array(seed.length + programId.length);
|
|
63
|
+
combined.set(seed, 0);
|
|
64
|
+
combined.set(programId, seed.length);
|
|
65
|
+
|
|
66
|
+
// SHA256 hash
|
|
67
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", combined);
|
|
68
|
+
const result = new Uint8Array(hashBuffer);
|
|
69
|
+
|
|
70
|
+
// Set the ephemeral bit in the first byte if needed
|
|
71
|
+
// Ephemeral addresses have bit 0x80 set in the first byte
|
|
72
|
+
if (ephemeral) {
|
|
73
|
+
result[0] = result[0] | 0x80;
|
|
74
|
+
} else {
|
|
75
|
+
result[0] = result[0] & 0x7f;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Derives the ABI account address for a given program account.
|
|
83
|
+
*
|
|
84
|
+
* @param programAccount - The program account address (ta-prefixed string or 32-byte array)
|
|
85
|
+
* @param abiManagerProgramId - The ABI manager program ID (ta-prefixed string or 32-byte array)
|
|
86
|
+
* @param ephemeral - Whether to derive an ephemeral address
|
|
87
|
+
* @returns The derived ABI account address as a ta-prefixed string
|
|
88
|
+
*/
|
|
89
|
+
export async function deriveAbiAddress(
|
|
90
|
+
programAccount: string | Uint8Array,
|
|
91
|
+
abiManagerProgramId: string | Uint8Array,
|
|
92
|
+
ephemeral: boolean = false
|
|
93
|
+
): Promise<string> {
|
|
94
|
+
// Convert string addresses to bytes
|
|
95
|
+
const programAccountBytes = typeof programAccount === "string"
|
|
96
|
+
? decodeAddress(programAccount)
|
|
97
|
+
: programAccount;
|
|
98
|
+
|
|
99
|
+
const abiManagerBytes = typeof abiManagerProgramId === "string"
|
|
100
|
+
? decodeAddress(abiManagerProgramId)
|
|
101
|
+
: abiManagerProgramId;
|
|
102
|
+
|
|
103
|
+
// Derive the seed
|
|
104
|
+
const seed = await deriveAbiSeed(programAccountBytes);
|
|
105
|
+
|
|
106
|
+
// Derive the PDA
|
|
107
|
+
const abiAccountBytes = await deriveProgramAddress(seed, abiManagerBytes, ephemeral);
|
|
108
|
+
|
|
109
|
+
// Encode as ta-prefixed address
|
|
110
|
+
return encodeAddress(abiAccountBytes);
|
|
111
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { deriveAbiAddress, deriveAbiSeed, deriveProgramAddress } from "./derivation";
|
|
2
|
+
export type { AbiAccountHeader, AbiAccountData, AbiAccountState } from "./types";
|
|
3
|
+
export { ABI_STATE } from "./types";
|
|
4
|
+
export { parseAbiAccountHeader, parseAbiAccountData, ABI_ACCOUNT_HEADER_SIZE } from "./parser";
|
package/src/parser.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { AbiAccountHeader, AbiAccountData, AbiAccountState } from "./types";
|
|
2
|
+
import { ABI_STATE } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Size of the ABI account header in bytes:
|
|
6
|
+
* - 32 bytes: program_meta_acc (pubkey)
|
|
7
|
+
* - 8 bytes: revision (u64)
|
|
8
|
+
* - 1 byte: state (u8)
|
|
9
|
+
* - 4 bytes: content_sz (u32)
|
|
10
|
+
* Total: 45 bytes
|
|
11
|
+
*/
|
|
12
|
+
export const ABI_ACCOUNT_HEADER_SIZE = 32 + 8 + 1 + 4;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parses the header from ABI account data.
|
|
16
|
+
*
|
|
17
|
+
* @param data - Raw account data bytes
|
|
18
|
+
* @returns Parsed header
|
|
19
|
+
* @throws If data is too small for the header
|
|
20
|
+
*/
|
|
21
|
+
export function parseAbiAccountHeader(data: Uint8Array): AbiAccountHeader {
|
|
22
|
+
if (data.length < ABI_ACCOUNT_HEADER_SIZE) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`ABI account data too small: ${data.length} bytes, expected at least ${ABI_ACCOUNT_HEADER_SIZE}`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
29
|
+
|
|
30
|
+
// program_meta_acc: bytes 0-31 (32 bytes)
|
|
31
|
+
const programMetaAccount = data.slice(0, 32);
|
|
32
|
+
|
|
33
|
+
// revision: bytes 32-39 (8 bytes, little-endian u64)
|
|
34
|
+
const revision = view.getBigUint64(32, true);
|
|
35
|
+
|
|
36
|
+
// state: byte 40 (1 byte)
|
|
37
|
+
const stateRaw = data[40];
|
|
38
|
+
const state: AbiAccountState = stateRaw === ABI_STATE.FINALIZED
|
|
39
|
+
? ABI_STATE.FINALIZED
|
|
40
|
+
: ABI_STATE.OPEN;
|
|
41
|
+
|
|
42
|
+
// content_sz: bytes 41-44 (4 bytes, little-endian u32)
|
|
43
|
+
const contentSize = view.getUint32(41, true);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
programMetaAccount,
|
|
47
|
+
revision,
|
|
48
|
+
state,
|
|
49
|
+
contentSize,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parses the full ABI account data including content.
|
|
55
|
+
*
|
|
56
|
+
* @param data - Raw account data bytes
|
|
57
|
+
* @returns Parsed header and content
|
|
58
|
+
* @throws If data is too small or content size exceeds available data
|
|
59
|
+
*/
|
|
60
|
+
export function parseAbiAccountData(data: Uint8Array): AbiAccountData {
|
|
61
|
+
const header = parseAbiAccountHeader(data);
|
|
62
|
+
|
|
63
|
+
const expectedSize = ABI_ACCOUNT_HEADER_SIZE + header.contentSize;
|
|
64
|
+
if (data.length < expectedSize) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`ABI account data incomplete: ${data.length} bytes, expected ${expectedSize} (header + ${header.contentSize} content bytes)`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const contentBytes = data.slice(ABI_ACCOUNT_HEADER_SIZE, ABI_ACCOUNT_HEADER_SIZE + header.contentSize);
|
|
71
|
+
const content = new TextDecoder().decode(contentBytes);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
...header,
|
|
75
|
+
contentBytes,
|
|
76
|
+
content,
|
|
77
|
+
};
|
|
78
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ABI account state values
|
|
3
|
+
*/
|
|
4
|
+
export const ABI_STATE = {
|
|
5
|
+
OPEN: 0x00,
|
|
6
|
+
FINALIZED: 0x01,
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
export type AbiAccountState = (typeof ABI_STATE)[keyof typeof ABI_STATE];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parsed header from an ABI account's on-chain data
|
|
13
|
+
*/
|
|
14
|
+
export interface AbiAccountHeader {
|
|
15
|
+
/** The program meta account this ABI is associated with */
|
|
16
|
+
programMetaAccount: Uint8Array;
|
|
17
|
+
/** Revision number (incremented on each upgrade) */
|
|
18
|
+
revision: bigint;
|
|
19
|
+
/** Account state: OPEN (0) or FINALIZED (1) */
|
|
20
|
+
state: AbiAccountState;
|
|
21
|
+
/** Size of the ABI content in bytes */
|
|
22
|
+
contentSize: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Full ABI account data including parsed YAML content
|
|
27
|
+
*/
|
|
28
|
+
export interface AbiAccountData extends AbiAccountHeader {
|
|
29
|
+
/** Raw ABI YAML content bytes */
|
|
30
|
+
contentBytes: Uint8Array;
|
|
31
|
+
/** ABI YAML content as string */
|
|
32
|
+
content: string;
|
|
33
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"declarationMap": true,
|
|
12
|
+
"outDir": "./dist",
|
|
13
|
+
"rootDir": "./src"
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|
package/tsup.config.ts
ADDED