@utxopia/sdk 0.1.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/package.json +108 -0
- package/packages/btc-client/src/esplora-client.ts +153 -0
- package/packages/btc-client/src/index.ts +3 -0
- package/packages/btc-client/src/op-return.ts +93 -0
- package/packages/btc-client/src/types.ts +112 -0
- package/packages/sdk/README.md +277 -0
- package/packages/sdk/dist/announcement-client.d.ts +64 -0
- package/packages/sdk/dist/announcement-client.js +337 -0
- package/packages/sdk/dist/auditor-ciphertext.d.ts +72 -0
- package/packages/sdk/dist/auditor-ciphertext.js +135 -0
- package/packages/sdk/dist/auditor.d.ts +115 -0
- package/packages/sdk/dist/auditor.js +292 -0
- package/packages/sdk/dist/bitcoin/ika.d.ts +47 -0
- package/packages/sdk/dist/bitcoin/ika.js +74 -0
- package/packages/sdk/dist/bitcoin/index.d.ts +2 -0
- package/packages/sdk/dist/bitcoin/index.js +1 -0
- package/packages/sdk/dist/bound-params.d.ts +96 -0
- package/packages/sdk/dist/bound-params.js +249 -0
- package/packages/sdk/dist/chadbuffer.d.ts +124 -0
- package/packages/sdk/dist/chadbuffer.js +405 -0
- package/packages/sdk/dist/claim-link.d.ts +30 -0
- package/packages/sdk/dist/claim-link.js +50 -0
- package/packages/sdk/dist/client.d.ts +233 -0
- package/packages/sdk/dist/client.js +416 -0
- package/packages/sdk/dist/commitment-tree.d.ts +268 -0
- package/packages/sdk/dist/commitment-tree.js +564 -0
- package/packages/sdk/dist/config.d.ts +177 -0
- package/packages/sdk/dist/config.js +426 -0
- package/packages/sdk/dist/core/esplora.d.ts +110 -0
- package/packages/sdk/dist/core/esplora.js +187 -0
- package/packages/sdk/dist/core/mempool.d.ts +60 -0
- package/packages/sdk/dist/core/mempool.js +107 -0
- package/packages/sdk/dist/crypto-babyjub.d.ts +105 -0
- package/packages/sdk/dist/crypto-babyjub.js +312 -0
- package/packages/sdk/dist/crypto-ed25519.d.ts +116 -0
- package/packages/sdk/dist/crypto-ed25519.js +235 -0
- package/packages/sdk/dist/crypto.d.ts +59 -0
- package/packages/sdk/dist/crypto.js +140 -0
- package/packages/sdk/dist/event-client.d.ts +64 -0
- package/packages/sdk/dist/event-client.js +165 -0
- package/packages/sdk/dist/events.d.ts +127 -0
- package/packages/sdk/dist/events.js +305 -0
- package/packages/sdk/dist/explorer.d.ts +86 -0
- package/packages/sdk/dist/explorer.js +197 -0
- package/packages/sdk/dist/index.d.ts +61 -0
- package/packages/sdk/dist/index.js +216 -0
- package/packages/sdk/dist/instructions.d.ts +1125 -0
- package/packages/sdk/dist/instructions.js +1760 -0
- package/packages/sdk/dist/keys.d.ts +467 -0
- package/packages/sdk/dist/keys.js +799 -0
- package/packages/sdk/dist/logger.d.ts +10 -0
- package/packages/sdk/dist/logger.js +40 -0
- package/packages/sdk/dist/magicblock.d.ts +90 -0
- package/packages/sdk/dist/magicblock.js +164 -0
- package/packages/sdk/dist/merkle.d.ts +82 -0
- package/packages/sdk/dist/merkle.js +141 -0
- package/packages/sdk/dist/note.d.ts +348 -0
- package/packages/sdk/dist/note.js +483 -0
- package/packages/sdk/dist/pda.d.ts +160 -0
- package/packages/sdk/dist/pda.js +384 -0
- package/packages/sdk/dist/pool-state.d.ts +100 -0
- package/packages/sdk/dist/pool-state.js +126 -0
- package/packages/sdk/dist/poseidon.d.ts +53 -0
- package/packages/sdk/dist/poseidon.js +136 -0
- package/packages/sdk/dist/prover/index.d.ts +10 -0
- package/packages/sdk/dist/prover/index.js +10 -0
- package/packages/sdk/dist/prover/mobile.d.ts +45 -0
- package/packages/sdk/dist/prover/mobile.js +227 -0
- package/packages/sdk/dist/prover/web.d.ts +136 -0
- package/packages/sdk/dist/prover/web.js +597 -0
- package/packages/sdk/dist/psbt.d.ts +73 -0
- package/packages/sdk/dist/psbt.js +202 -0
- package/packages/sdk/dist/selective-disclosure.d.ts +181 -0
- package/packages/sdk/dist/selective-disclosure.js +172 -0
- package/packages/sdk/dist/sender-memo.d.ts +149 -0
- package/packages/sdk/dist/sender-memo.js +250 -0
- package/packages/sdk/dist/sns-resolver.d.ts +121 -0
- package/packages/sdk/dist/sns-resolver.js +229 -0
- package/packages/sdk/dist/solana/connection.d.ts +77 -0
- package/packages/sdk/dist/solana/connection.js +133 -0
- package/packages/sdk/dist/solana/priority-fee.d.ts +78 -0
- package/packages/sdk/dist/solana/priority-fee.js +137 -0
- package/packages/sdk/dist/stealth.d.ts +429 -0
- package/packages/sdk/dist/stealth.js +703 -0
- package/packages/sdk/dist/taproot.d.ts +172 -0
- package/packages/sdk/dist/taproot.js +494 -0
- package/packages/sdk/dist/token-registry.d.ts +96 -0
- package/packages/sdk/dist/token-registry.js +122 -0
- package/packages/sdk/dist/utils/encoding.d.ts +19 -0
- package/packages/sdk/dist/utils/encoding.js +29 -0
- package/packages/sdk/dist/vk-registry.d.ts +100 -0
- package/packages/sdk/dist/vk-registry.js +212 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Registry — fetch and manage supported tokens in the multi-token shielded pool.
|
|
3
|
+
*
|
|
4
|
+
* Each whitelisted token has a TokenConfig PDA on-chain storing:
|
|
5
|
+
* mint, token_id, vault, decimals, service_fee, min/max deposit, deposit_cap, etc.
|
|
6
|
+
*
|
|
7
|
+
* @module token-registry
|
|
8
|
+
*/
|
|
9
|
+
import { type Address } from "@solana/kit";
|
|
10
|
+
/** On-chain TokenConfig account data (parsed) */
|
|
11
|
+
export interface TokenConfigData {
|
|
12
|
+
/** SPL mint address */
|
|
13
|
+
mint: Address;
|
|
14
|
+
/** Poseidon(reduce_to_field(mint), 0) */
|
|
15
|
+
tokenId: bigint;
|
|
16
|
+
/** Token vault address (PDA-owned) */
|
|
17
|
+
vault: Address;
|
|
18
|
+
/** Token decimals */
|
|
19
|
+
decimals: number;
|
|
20
|
+
/** Whether the token is enabled for shielding */
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
/** Flat service fee per BTC operation (native units) */
|
|
23
|
+
serviceFee: bigint;
|
|
24
|
+
/** Minimum deposit amount */
|
|
25
|
+
minDeposit: bigint;
|
|
26
|
+
/** Maximum deposit amount */
|
|
27
|
+
maxDeposit: bigint;
|
|
28
|
+
/** Maximum total shielded for this token */
|
|
29
|
+
depositCap: bigint;
|
|
30
|
+
/** Current total shielded */
|
|
31
|
+
totalShielded: bigint;
|
|
32
|
+
/** Accumulated protocol fees */
|
|
33
|
+
accumulatedFees: bigint;
|
|
34
|
+
/** TokenConfig PDA address */
|
|
35
|
+
configAddress: Address;
|
|
36
|
+
}
|
|
37
|
+
/** Minimal token info for selection UI */
|
|
38
|
+
export interface SupportedToken {
|
|
39
|
+
/** Display name (from metadata or derived) */
|
|
40
|
+
name: string;
|
|
41
|
+
/** Token symbol */
|
|
42
|
+
symbol: string;
|
|
43
|
+
/** SPL mint address */
|
|
44
|
+
mint: Address;
|
|
45
|
+
/** Token decimals */
|
|
46
|
+
decimals: number;
|
|
47
|
+
/** Whether enabled */
|
|
48
|
+
enabled: boolean;
|
|
49
|
+
/** Computed token_id for circuit use */
|
|
50
|
+
tokenId: bigint;
|
|
51
|
+
/** TokenConfig PDA address */
|
|
52
|
+
configAddress: Address;
|
|
53
|
+
}
|
|
54
|
+
/** Parse TokenConfig from raw account data */
|
|
55
|
+
export declare function parseTokenConfig(data: Uint8Array, configAddress: Address): TokenConfigData | null;
|
|
56
|
+
/**
|
|
57
|
+
* Get TokenConfig for a specific mint.
|
|
58
|
+
* Derives the PDA and fetches from chain.
|
|
59
|
+
*
|
|
60
|
+
* @param rpc - Solana RPC client (must have getAccountInfo)
|
|
61
|
+
* @param mintAddress - SPL mint address (raw 32-byte pubkey)
|
|
62
|
+
*/
|
|
63
|
+
export declare function fetchTokenConfig(rpc: {
|
|
64
|
+
getAccountInfo(addr: Address, opts?: object): Promise<{
|
|
65
|
+
value: {
|
|
66
|
+
data: Uint8Array;
|
|
67
|
+
} | null;
|
|
68
|
+
}>;
|
|
69
|
+
}, mintAddress: Uint8Array): Promise<TokenConfigData | null>;
|
|
70
|
+
/**
|
|
71
|
+
* Compute token_id for a mint address (for use in circuit inputs).
|
|
72
|
+
* This is a pure computation — no RPC call needed.
|
|
73
|
+
*
|
|
74
|
+
* @param mintPubkey - 32-byte mint public key
|
|
75
|
+
* @returns tokenId as bigint
|
|
76
|
+
*/
|
|
77
|
+
export declare function getTokenId(mintPubkey: Uint8Array): bigint;
|
|
78
|
+
/**
|
|
79
|
+
* Fetch all supported tokens by scanning TokenConfig PDAs.
|
|
80
|
+
*
|
|
81
|
+
* Uses getProgramAccounts with memcmp filter on the discriminator byte.
|
|
82
|
+
*
|
|
83
|
+
* @param rpc - Solana RPC client
|
|
84
|
+
*/
|
|
85
|
+
export declare function fetchSupportedTokens(rpc: {
|
|
86
|
+
getProgramAccounts(programId: Address, opts?: object): Promise<{
|
|
87
|
+
pubkey: Address;
|
|
88
|
+
account: {
|
|
89
|
+
data: Uint8Array;
|
|
90
|
+
};
|
|
91
|
+
}[]>;
|
|
92
|
+
}): Promise<TokenConfigData[]>;
|
|
93
|
+
/**
|
|
94
|
+
* Fetch only enabled tokens (convenience wrapper).
|
|
95
|
+
*/
|
|
96
|
+
export declare function fetchEnabledTokens(rpc: Parameters<typeof fetchSupportedTokens>[0]): Promise<TokenConfigData[]>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Registry — fetch and manage supported tokens in the multi-token shielded pool.
|
|
3
|
+
*
|
|
4
|
+
* Each whitelisted token has a TokenConfig PDA on-chain storing:
|
|
5
|
+
* mint, token_id, vault, decimals, service_fee, min/max deposit, deposit_cap, etc.
|
|
6
|
+
*
|
|
7
|
+
* @module token-registry
|
|
8
|
+
*/
|
|
9
|
+
import { computeTokenId } from "./poseidon";
|
|
10
|
+
import { deriveTokenConfigPDA } from "./pda";
|
|
11
|
+
import { address, getConfig } from "./config";
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// TokenConfig PDA Layout (must match on-chain token_config.rs)
|
|
14
|
+
// ============================================================================
|
|
15
|
+
const TOKEN_CONFIG_DISCRIMINATOR = 0x0b;
|
|
16
|
+
const TOKEN_CONFIG_LEN = 164;
|
|
17
|
+
/** Parse TokenConfig from raw account data */
|
|
18
|
+
export function parseTokenConfig(data, configAddress) {
|
|
19
|
+
if (data.length < TOKEN_CONFIG_LEN)
|
|
20
|
+
return null;
|
|
21
|
+
if (data[0] !== TOKEN_CONFIG_DISCRIMINATOR)
|
|
22
|
+
return null;
|
|
23
|
+
// Layout (from token_config.rs):
|
|
24
|
+
// disc(1) + bump(1) + mint(32) + token_id(32) + vault(32) + decimals(1) + enabled(1)
|
|
25
|
+
// + service_fee(8) + min_deposit(8) + max_deposit(8) + deposit_cap(8) + total_shielded(8) + accumulated_fees(8) + reserved(16)
|
|
26
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
27
|
+
const mintBytes = data.slice(2, 34);
|
|
28
|
+
const tokenIdBytes = data.slice(34, 66);
|
|
29
|
+
const vaultBytes = data.slice(66, 98);
|
|
30
|
+
const decimals = data[98];
|
|
31
|
+
const enabled = data[99] !== 0;
|
|
32
|
+
const serviceFee = view.getBigUint64(100, true);
|
|
33
|
+
const minDeposit = view.getBigUint64(108, true);
|
|
34
|
+
const maxDeposit = view.getBigUint64(116, true);
|
|
35
|
+
const depositCap = view.getBigUint64(124, true);
|
|
36
|
+
const totalShielded = view.getBigUint64(132, true);
|
|
37
|
+
const accumulatedFees = view.getBigUint64(140, true);
|
|
38
|
+
// Convert bytes to bigint for tokenId (big-endian)
|
|
39
|
+
let tokenId = 0n;
|
|
40
|
+
for (const b of tokenIdBytes) {
|
|
41
|
+
tokenId = (tokenId << 8n) | BigInt(b);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
mint: address(Buffer.from(mintBytes).toString("base64")), // This needs proper base58
|
|
45
|
+
tokenId,
|
|
46
|
+
vault: address(Buffer.from(vaultBytes).toString("base64")),
|
|
47
|
+
decimals,
|
|
48
|
+
enabled,
|
|
49
|
+
serviceFee,
|
|
50
|
+
minDeposit,
|
|
51
|
+
maxDeposit,
|
|
52
|
+
depositCap,
|
|
53
|
+
totalShielded,
|
|
54
|
+
accumulatedFees,
|
|
55
|
+
configAddress,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Token Selection & Fetching
|
|
60
|
+
// ============================================================================
|
|
61
|
+
/**
|
|
62
|
+
* Get TokenConfig for a specific mint.
|
|
63
|
+
* Derives the PDA and fetches from chain.
|
|
64
|
+
*
|
|
65
|
+
* @param rpc - Solana RPC client (must have getAccountInfo)
|
|
66
|
+
* @param mintAddress - SPL mint address (raw 32-byte pubkey)
|
|
67
|
+
*/
|
|
68
|
+
export async function fetchTokenConfig(rpc, mintAddress) {
|
|
69
|
+
const config = getConfig();
|
|
70
|
+
const [configPda] = await deriveTokenConfigPDA(config.poolStatePda, mintAddress, config.utxopiaProgramId);
|
|
71
|
+
const accountInfo = await rpc.getAccountInfo(configPda, { encoding: "base64" });
|
|
72
|
+
if (!accountInfo?.value?.data)
|
|
73
|
+
return null;
|
|
74
|
+
const data = accountInfo.value.data instanceof Uint8Array
|
|
75
|
+
? accountInfo.value.data
|
|
76
|
+
: new Uint8Array(Buffer.from(accountInfo.value.data, "base64"));
|
|
77
|
+
return parseTokenConfig(data, configPda);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Compute token_id for a mint address (for use in circuit inputs).
|
|
81
|
+
* This is a pure computation — no RPC call needed.
|
|
82
|
+
*
|
|
83
|
+
* @param mintPubkey - 32-byte mint public key
|
|
84
|
+
* @returns tokenId as bigint
|
|
85
|
+
*/
|
|
86
|
+
export function getTokenId(mintPubkey) {
|
|
87
|
+
return computeTokenId(mintPubkey);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Fetch all supported tokens by scanning TokenConfig PDAs.
|
|
91
|
+
*
|
|
92
|
+
* Uses getProgramAccounts with memcmp filter on the discriminator byte.
|
|
93
|
+
*
|
|
94
|
+
* @param rpc - Solana RPC client
|
|
95
|
+
*/
|
|
96
|
+
export async function fetchSupportedTokens(rpc) {
|
|
97
|
+
const config = getConfig();
|
|
98
|
+
const accounts = await rpc.getProgramAccounts(config.utxopiaProgramId, {
|
|
99
|
+
filters: [
|
|
100
|
+
{ dataSize: TOKEN_CONFIG_LEN },
|
|
101
|
+
{ memcmp: { offset: 0, bytes: Buffer.from([TOKEN_CONFIG_DISCRIMINATOR]).toString("base64") } },
|
|
102
|
+
],
|
|
103
|
+
encoding: "base64",
|
|
104
|
+
});
|
|
105
|
+
const tokens = [];
|
|
106
|
+
for (const { pubkey, account } of accounts) {
|
|
107
|
+
const data = account.data instanceof Uint8Array
|
|
108
|
+
? account.data
|
|
109
|
+
: new Uint8Array(Buffer.from(account.data, "base64"));
|
|
110
|
+
const parsed = parseTokenConfig(data, pubkey);
|
|
111
|
+
if (parsed)
|
|
112
|
+
tokens.push(parsed);
|
|
113
|
+
}
|
|
114
|
+
return tokens;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Fetch only enabled tokens (convenience wrapper).
|
|
118
|
+
*/
|
|
119
|
+
export async function fetchEnabledTokens(rpc) {
|
|
120
|
+
const all = await fetchSupportedTokens(rpc);
|
|
121
|
+
return all.filter((t) => t.enabled);
|
|
122
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform encoding utilities.
|
|
3
|
+
*
|
|
4
|
+
* Works in Browser, Node.js, and React Native without polyfills.
|
|
5
|
+
* Hex helpers re-export the canonical implementations from crypto.ts.
|
|
6
|
+
*/
|
|
7
|
+
import { bytesToHex, hexToBytes } from "../crypto";
|
|
8
|
+
export { bytesToHex as toHex, hexToBytes as fromHex };
|
|
9
|
+
/**
|
|
10
|
+
* Decode a base64 string to Uint8Array.
|
|
11
|
+
* Uses atob() which is available in browsers, Node 16+, and React Native.
|
|
12
|
+
*/
|
|
13
|
+
export declare function fromBase64(b64: string): Uint8Array;
|
|
14
|
+
/**
|
|
15
|
+
* Decode a base64 string to a binary string (each char = one byte).
|
|
16
|
+
* Useful for RPC account-data decoding where the consumer expects
|
|
17
|
+
* `charCodeAt(i)` to yield raw byte values.
|
|
18
|
+
*/
|
|
19
|
+
export declare function base64ToBinaryString(b64: string): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform encoding utilities.
|
|
3
|
+
*
|
|
4
|
+
* Works in Browser, Node.js, and React Native without polyfills.
|
|
5
|
+
* Hex helpers re-export the canonical implementations from crypto.ts.
|
|
6
|
+
*/
|
|
7
|
+
import { bytesToHex, hexToBytes } from "../crypto";
|
|
8
|
+
// Re-export hex utilities under the names specified by the encoding API
|
|
9
|
+
export { bytesToHex as toHex, hexToBytes as fromHex };
|
|
10
|
+
/**
|
|
11
|
+
* Decode a base64 string to Uint8Array.
|
|
12
|
+
* Uses atob() which is available in browsers, Node 16+, and React Native.
|
|
13
|
+
*/
|
|
14
|
+
export function fromBase64(b64) {
|
|
15
|
+
const binary = atob(b64);
|
|
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;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Decode a base64 string to a binary string (each char = one byte).
|
|
24
|
+
* Useful for RPC account-data decoding where the consumer expects
|
|
25
|
+
* `charCodeAt(i)` to yield raw byte values.
|
|
26
|
+
*/
|
|
27
|
+
export function base64ToBinaryString(b64) {
|
|
28
|
+
return atob(b64);
|
|
29
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VK Registry helpers (JoinSplit Groth16 on-chain verification keys)
|
|
3
|
+
*
|
|
4
|
+
* JoinSplit verification keys are no longer embedded in the program binary.
|
|
5
|
+
* Each `(nInputs, nOutputs)` shape is stored in its own `VkRegistry` PDA holding
|
|
6
|
+
* the full verifier material: `vkHash`, `deltaG2`, and the ordered IC points.
|
|
7
|
+
*
|
|
8
|
+
* This module is chain-agnostic (Uint8Array in / Uint8Array out): it builds the
|
|
9
|
+
* `init_vk_registry` / `update_vk_registry` instruction payloads, converts a
|
|
10
|
+
* snarkjs `*.vkey.json` into on-chain VK material, and parses a fetched
|
|
11
|
+
* `VkRegistry` account. PDA derivation lives in `./pda` (`deriveVkRegistryPDA`).
|
|
12
|
+
*
|
|
13
|
+
* @module vk-registry
|
|
14
|
+
*/
|
|
15
|
+
/** VkRegistry account discriminator (0x14). */
|
|
16
|
+
export declare const VK_REGISTRY_DISCRIMINATOR = 20;
|
|
17
|
+
/** Serialized VkRegistry account length (bytes). */
|
|
18
|
+
export declare const VK_REGISTRY_LEN = 1060;
|
|
19
|
+
/**
|
|
20
|
+
* Maximum IC points the registry can hold: 1 base point + 2 fixed public inputs
|
|
21
|
+
* (merkleRoot, boundParamsHash) + `MAX_SAFE_JOINSPLIT_SIZE` (10) note inputs.
|
|
22
|
+
*/
|
|
23
|
+
export declare const MAX_IC_POINTS = 13;
|
|
24
|
+
/** Largest `nInputs + nOutputs` the audited JoinSplit scope allows. */
|
|
25
|
+
export declare const MAX_SAFE_JOINSPLIT_SIZE = 10;
|
|
26
|
+
/** init_vk_registry / update_vk_registry discriminators (see lib.rs). */
|
|
27
|
+
export declare const INIT_VK_REGISTRY_DISCRIMINATOR = 6;
|
|
28
|
+
export declare const UPDATE_VK_REGISTRY_DISCRIMINATOR = 7;
|
|
29
|
+
/** Full on-chain Groth16 VK material for a single JoinSplit shape. */
|
|
30
|
+
export type JoinSplitVkMaterial = {
|
|
31
|
+
nInputs: number;
|
|
32
|
+
nOutputs: number;
|
|
33
|
+
/** sha256 over the canonical VK component serialization (32 bytes). */
|
|
34
|
+
vkHash: Uint8Array;
|
|
35
|
+
/** Groth16 delta G2 point, Ethereum-precompile byte order (128 bytes). */
|
|
36
|
+
deltaG2: Uint8Array;
|
|
37
|
+
/** IC points (one base + one per public input), 64 bytes each. */
|
|
38
|
+
ic: Uint8Array[];
|
|
39
|
+
};
|
|
40
|
+
/** Public inputs for JoinSplit(N, M): merkleRoot + boundParamsHash + N + M. */
|
|
41
|
+
export declare function joinSplitNumPublicInputs(nInputs: number, nOutputs: number): number;
|
|
42
|
+
/** Minimal shape of a snarkjs Groth16 `*.vkey.json`. */
|
|
43
|
+
export interface SnarkjsVkeyJson {
|
|
44
|
+
nPublic: number;
|
|
45
|
+
vk_alpha_1: [string, string, string];
|
|
46
|
+
vk_beta_2: [[string, string], [string, string], [string, string]];
|
|
47
|
+
vk_gamma_2: [[string, string], [string, string], [string, string]];
|
|
48
|
+
vk_delta_2: [[string, string], [string, string], [string, string]];
|
|
49
|
+
IC: Array<[string, string, string]>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Compute the canonical `vkHash`: sha256 over alpha, beta, gamma, delta and
|
|
53
|
+
* every IC coordinate, each serialized as a 32-byte big-endian field element in
|
|
54
|
+
* snarkjs order. Matches the on-chain ops registration hash.
|
|
55
|
+
*/
|
|
56
|
+
export declare function computeVkHash(vkey: SnarkjsVkeyJson): Uint8Array;
|
|
57
|
+
/**
|
|
58
|
+
* Convert a snarkjs `*.vkey.json` into on-chain `JoinSplitVkMaterial` for the
|
|
59
|
+
* given shape. Validates that the IC count matches `3 + nInputs + nOutputs`
|
|
60
|
+
* (1 base + 2 fixed + N + M public inputs).
|
|
61
|
+
*/
|
|
62
|
+
export declare function vkeyJsonToVkMaterial(vkey: SnarkjsVkeyJson, nInputs: number, nOutputs: number): JoinSplitVkMaterial;
|
|
63
|
+
/**
|
|
64
|
+
* Build the instruction data for `init_vk_registry` (disc 6) or
|
|
65
|
+
* `update_vk_registry` (disc 7). Both share the layout:
|
|
66
|
+
*
|
|
67
|
+
* disc(1) + n_inputs(1) + n_outputs(1) + vk_hash(32)
|
|
68
|
+
* + delta_g2(128) + ic_len(1) + ic_points(64 * ic_len)
|
|
69
|
+
*/
|
|
70
|
+
export declare function buildVkRegistryData(discriminator: typeof INIT_VK_REGISTRY_DISCRIMINATOR | typeof UPDATE_VK_REGISTRY_DISCRIMINATOR, vk: JoinSplitVkMaterial): Uint8Array;
|
|
71
|
+
/** Parsed `VkRegistry` account. */
|
|
72
|
+
export interface ParsedVkRegistry {
|
|
73
|
+
discriminator: number;
|
|
74
|
+
nInputs: number;
|
|
75
|
+
nOutputs: number;
|
|
76
|
+
/** 32-byte authority pubkey. */
|
|
77
|
+
authority: Uint8Array;
|
|
78
|
+
vkHash: Uint8Array;
|
|
79
|
+
deltaG2: Uint8Array;
|
|
80
|
+
icLen: number;
|
|
81
|
+
ic: Uint8Array[];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Parse a fetched `VkRegistry` account (1060-byte layout).
|
|
85
|
+
*
|
|
86
|
+
* Offsets: disc@0, n_inputs@2, n_outputs@3, authority@4, vk_hash@36,
|
|
87
|
+
* delta_g2@68, ic_len@196, ic@228 (64 bytes each).
|
|
88
|
+
*/
|
|
89
|
+
export declare function parseVkRegistry(data: Uint8Array): ParsedVkRegistry;
|
|
90
|
+
/**
|
|
91
|
+
* Validate that a fetched account is an initialized `VkRegistry` for the exact
|
|
92
|
+
* JoinSplit shape. Throws with an actionable message if not — call this before
|
|
93
|
+
* submitting `transact`, `unshield`, or `redeem` so the failure happens
|
|
94
|
+
* client-side instead of on-chain.
|
|
95
|
+
*
|
|
96
|
+
* @param data Raw account data (null/undefined if the account does not exist).
|
|
97
|
+
*/
|
|
98
|
+
export declare function assertVkRegistryForShape(data: Uint8Array | null | undefined, nInputs: number, nOutputs: number): ParsedVkRegistry;
|
|
99
|
+
/** True if `data` is an initialized `VkRegistry` for the given shape. */
|
|
100
|
+
export declare function isVkRegistryReady(data: Uint8Array | null | undefined, nInputs: number, nOutputs: number): boolean;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VK Registry helpers (JoinSplit Groth16 on-chain verification keys)
|
|
3
|
+
*
|
|
4
|
+
* JoinSplit verification keys are no longer embedded in the program binary.
|
|
5
|
+
* Each `(nInputs, nOutputs)` shape is stored in its own `VkRegistry` PDA holding
|
|
6
|
+
* the full verifier material: `vkHash`, `deltaG2`, and the ordered IC points.
|
|
7
|
+
*
|
|
8
|
+
* This module is chain-agnostic (Uint8Array in / Uint8Array out): it builds the
|
|
9
|
+
* `init_vk_registry` / `update_vk_registry` instruction payloads, converts a
|
|
10
|
+
* snarkjs `*.vkey.json` into on-chain VK material, and parses a fetched
|
|
11
|
+
* `VkRegistry` account. PDA derivation lives in `./pda` (`deriveVkRegistryPDA`).
|
|
12
|
+
*
|
|
13
|
+
* @module vk-registry
|
|
14
|
+
*/
|
|
15
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
16
|
+
/** VkRegistry account discriminator (0x14). */
|
|
17
|
+
export const VK_REGISTRY_DISCRIMINATOR = 0x14;
|
|
18
|
+
/** Serialized VkRegistry account length (bytes). */
|
|
19
|
+
export const VK_REGISTRY_LEN = 1060;
|
|
20
|
+
/**
|
|
21
|
+
* Maximum IC points the registry can hold: 1 base point + 2 fixed public inputs
|
|
22
|
+
* (merkleRoot, boundParamsHash) + `MAX_SAFE_JOINSPLIT_SIZE` (10) note inputs.
|
|
23
|
+
*/
|
|
24
|
+
export const MAX_IC_POINTS = 13;
|
|
25
|
+
/** Largest `nInputs + nOutputs` the audited JoinSplit scope allows. */
|
|
26
|
+
export const MAX_SAFE_JOINSPLIT_SIZE = 10;
|
|
27
|
+
const VK_HASH_LEN = 32;
|
|
28
|
+
const DELTA_G2_LEN = 128;
|
|
29
|
+
const IC_POINT_LEN = 64;
|
|
30
|
+
/** init_vk_registry / update_vk_registry discriminators (see lib.rs). */
|
|
31
|
+
export const INIT_VK_REGISTRY_DISCRIMINATOR = 6;
|
|
32
|
+
export const UPDATE_VK_REGISTRY_DISCRIMINATOR = 7;
|
|
33
|
+
/** Public inputs for JoinSplit(N, M): merkleRoot + boundParamsHash + N + M. */
|
|
34
|
+
export function joinSplitNumPublicInputs(nInputs, nOutputs) {
|
|
35
|
+
return 2 + nInputs + nOutputs;
|
|
36
|
+
}
|
|
37
|
+
function assertDimensions(nInputs, nOutputs) {
|
|
38
|
+
if (nInputs < 1 || nOutputs < 1 || nInputs + nOutputs > MAX_SAFE_JOINSPLIT_SIZE) {
|
|
39
|
+
throw new Error(`JoinSplit dimensions must satisfy nInputs>=1, nOutputs>=1, nInputs+nOutputs<=${MAX_SAFE_JOINSPLIT_SIZE}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Decimal field-element string → 32-byte big-endian. */
|
|
43
|
+
function fieldTo32BE(decStr) {
|
|
44
|
+
const hex = BigInt(decStr).toString(16).padStart(64, "0");
|
|
45
|
+
const out = new Uint8Array(32);
|
|
46
|
+
for (let i = 0; i < 32; i++) {
|
|
47
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/** Encode a G1 point as `[x_BE(32) | y_BE(32)]` (64 bytes). */
|
|
52
|
+
function encodeG1(point) {
|
|
53
|
+
const out = new Uint8Array(IC_POINT_LEN);
|
|
54
|
+
out.set(fieldTo32BE(point[0]), 0);
|
|
55
|
+
out.set(fieldTo32BE(point[1]), 32);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Encode a G2 point as `[x_imag | x_real | y_imag | y_real]` (128 bytes),
|
|
60
|
+
* matching solana-bn254's Ethereum precompile order. snarkjs gives
|
|
61
|
+
* `point = [[x_real, x_imag], [y_real, y_imag]]`.
|
|
62
|
+
*/
|
|
63
|
+
function encodeG2(point) {
|
|
64
|
+
const out = new Uint8Array(DELTA_G2_LEN);
|
|
65
|
+
out.set(fieldTo32BE(point[0][1]), 0); // x_imag
|
|
66
|
+
out.set(fieldTo32BE(point[0][0]), 32); // x_real
|
|
67
|
+
out.set(fieldTo32BE(point[1][1]), 64); // y_imag
|
|
68
|
+
out.set(fieldTo32BE(point[1][0]), 96); // y_real
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Compute the canonical `vkHash`: sha256 over alpha, beta, gamma, delta and
|
|
73
|
+
* every IC coordinate, each serialized as a 32-byte big-endian field element in
|
|
74
|
+
* snarkjs order. Matches the on-chain ops registration hash.
|
|
75
|
+
*/
|
|
76
|
+
export function computeVkHash(vkey) {
|
|
77
|
+
const parts = [];
|
|
78
|
+
parts.push(vkey.vk_alpha_1[0], vkey.vk_alpha_1[1]);
|
|
79
|
+
parts.push(vkey.vk_beta_2[0][0], vkey.vk_beta_2[0][1], vkey.vk_beta_2[1][0], vkey.vk_beta_2[1][1]);
|
|
80
|
+
parts.push(vkey.vk_gamma_2[0][0], vkey.vk_gamma_2[0][1], vkey.vk_gamma_2[1][0], vkey.vk_gamma_2[1][1]);
|
|
81
|
+
parts.push(vkey.vk_delta_2[0][0], vkey.vk_delta_2[0][1], vkey.vk_delta_2[1][0], vkey.vk_delta_2[1][1]);
|
|
82
|
+
for (const ic of vkey.IC)
|
|
83
|
+
parts.push(ic[0], ic[1]);
|
|
84
|
+
const serialized = new Uint8Array(parts.length * 32);
|
|
85
|
+
parts.forEach((p, i) => serialized.set(fieldTo32BE(p), i * 32));
|
|
86
|
+
return sha256(serialized);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Convert a snarkjs `*.vkey.json` into on-chain `JoinSplitVkMaterial` for the
|
|
90
|
+
* given shape. Validates that the IC count matches `3 + nInputs + nOutputs`
|
|
91
|
+
* (1 base + 2 fixed + N + M public inputs).
|
|
92
|
+
*/
|
|
93
|
+
export function vkeyJsonToVkMaterial(vkey, nInputs, nOutputs) {
|
|
94
|
+
assertDimensions(nInputs, nOutputs);
|
|
95
|
+
const expectedIcLen = joinSplitNumPublicInputs(nInputs, nOutputs) + 1;
|
|
96
|
+
if (vkey.IC.length !== expectedIcLen) {
|
|
97
|
+
throw new Error(`vkey IC length ${vkey.IC.length} does not match shape ${nInputs}x${nOutputs} (expected ${expectedIcLen})`);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
nInputs,
|
|
101
|
+
nOutputs,
|
|
102
|
+
vkHash: computeVkHash(vkey),
|
|
103
|
+
deltaG2: encodeG2(vkey.vk_delta_2),
|
|
104
|
+
ic: vkey.IC.map(encodeG1),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// Instruction data builder (init_vk_registry = 6 / update_vk_registry = 7)
|
|
109
|
+
// =============================================================================
|
|
110
|
+
/**
|
|
111
|
+
* Build the instruction data for `init_vk_registry` (disc 6) or
|
|
112
|
+
* `update_vk_registry` (disc 7). Both share the layout:
|
|
113
|
+
*
|
|
114
|
+
* disc(1) + n_inputs(1) + n_outputs(1) + vk_hash(32)
|
|
115
|
+
* + delta_g2(128) + ic_len(1) + ic_points(64 * ic_len)
|
|
116
|
+
*/
|
|
117
|
+
export function buildVkRegistryData(discriminator, vk) {
|
|
118
|
+
assertDimensions(vk.nInputs, vk.nOutputs);
|
|
119
|
+
if (vk.vkHash.length !== VK_HASH_LEN)
|
|
120
|
+
throw new Error("vkHash must be 32 bytes");
|
|
121
|
+
if (vk.deltaG2.length !== DELTA_G2_LEN)
|
|
122
|
+
throw new Error("deltaG2 must be 128 bytes");
|
|
123
|
+
const expectedIcLen = joinSplitNumPublicInputs(vk.nInputs, vk.nOutputs) + 1;
|
|
124
|
+
if (vk.ic.length !== expectedIcLen || vk.ic.length > MAX_IC_POINTS) {
|
|
125
|
+
throw new Error(`expected ${expectedIcLen} IC points, got ${vk.ic.length}`);
|
|
126
|
+
}
|
|
127
|
+
vk.ic.forEach((p, i) => {
|
|
128
|
+
if (p.length !== IC_POINT_LEN)
|
|
129
|
+
throw new Error(`IC[${i}] must be 64 bytes`);
|
|
130
|
+
});
|
|
131
|
+
const data = new Uint8Array(1 + 2 + VK_HASH_LEN + DELTA_G2_LEN + 1 + vk.ic.length * IC_POINT_LEN);
|
|
132
|
+
let offset = 0;
|
|
133
|
+
data[offset++] = discriminator;
|
|
134
|
+
data[offset++] = vk.nInputs;
|
|
135
|
+
data[offset++] = vk.nOutputs;
|
|
136
|
+
data.set(vk.vkHash, offset);
|
|
137
|
+
offset += VK_HASH_LEN;
|
|
138
|
+
data.set(vk.deltaG2, offset);
|
|
139
|
+
offset += DELTA_G2_LEN;
|
|
140
|
+
data[offset++] = vk.ic.length;
|
|
141
|
+
for (const point of vk.ic) {
|
|
142
|
+
data.set(point, offset);
|
|
143
|
+
offset += IC_POINT_LEN;
|
|
144
|
+
}
|
|
145
|
+
return data;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Parse a fetched `VkRegistry` account (1060-byte layout).
|
|
149
|
+
*
|
|
150
|
+
* Offsets: disc@0, n_inputs@2, n_outputs@3, authority@4, vk_hash@36,
|
|
151
|
+
* delta_g2@68, ic_len@196, ic@228 (64 bytes each).
|
|
152
|
+
*/
|
|
153
|
+
export function parseVkRegistry(data) {
|
|
154
|
+
if (data.length < VK_REGISTRY_LEN) {
|
|
155
|
+
throw new Error(`VkRegistry account too small: ${data.length} < ${VK_REGISTRY_LEN}`);
|
|
156
|
+
}
|
|
157
|
+
if (data[0] !== VK_REGISTRY_DISCRIMINATOR) {
|
|
158
|
+
throw new Error(`Invalid VkRegistry discriminator: 0x${data[0].toString(16)}`);
|
|
159
|
+
}
|
|
160
|
+
const nInputs = data[2];
|
|
161
|
+
const nOutputs = data[3];
|
|
162
|
+
const icLen = data[196];
|
|
163
|
+
const expectedIcLen = joinSplitNumPublicInputs(nInputs, nOutputs) + 1;
|
|
164
|
+
if (icLen !== expectedIcLen || icLen > MAX_IC_POINTS) {
|
|
165
|
+
throw new Error(`Invalid VkRegistry IC length: ${icLen} (expected ${expectedIcLen})`);
|
|
166
|
+
}
|
|
167
|
+
const ic = [];
|
|
168
|
+
for (let i = 0; i < icLen; i++) {
|
|
169
|
+
const start = 228 + i * IC_POINT_LEN;
|
|
170
|
+
ic.push(data.subarray(start, start + IC_POINT_LEN));
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
discriminator: data[0],
|
|
174
|
+
nInputs,
|
|
175
|
+
nOutputs,
|
|
176
|
+
authority: data.subarray(4, 36),
|
|
177
|
+
vkHash: data.subarray(36, 68),
|
|
178
|
+
deltaG2: data.subarray(68, 196),
|
|
179
|
+
icLen,
|
|
180
|
+
ic,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Validate that a fetched account is an initialized `VkRegistry` for the exact
|
|
185
|
+
* JoinSplit shape. Throws with an actionable message if not — call this before
|
|
186
|
+
* submitting `transact`, `unshield`, or `redeem` so the failure happens
|
|
187
|
+
* client-side instead of on-chain.
|
|
188
|
+
*
|
|
189
|
+
* @param data Raw account data (null/undefined if the account does not exist).
|
|
190
|
+
*/
|
|
191
|
+
export function assertVkRegistryForShape(data, nInputs, nOutputs) {
|
|
192
|
+
if (!data || data.length === 0) {
|
|
193
|
+
throw new Error(`VK registry for JoinSplit ${nInputs}x${nOutputs} is not initialized. ` +
|
|
194
|
+
`Run the ops VK registration before using this proof shape.`);
|
|
195
|
+
}
|
|
196
|
+
const parsed = parseVkRegistry(data);
|
|
197
|
+
if (parsed.nInputs !== nInputs || parsed.nOutputs !== nOutputs) {
|
|
198
|
+
throw new Error(`VK registry shape mismatch: account is ${parsed.nInputs}x${parsed.nOutputs}, ` +
|
|
199
|
+
`expected ${nInputs}x${nOutputs}`);
|
|
200
|
+
}
|
|
201
|
+
return parsed;
|
|
202
|
+
}
|
|
203
|
+
/** True if `data` is an initialized `VkRegistry` for the given shape. */
|
|
204
|
+
export function isVkRegistryReady(data, nInputs, nOutputs) {
|
|
205
|
+
try {
|
|
206
|
+
assertVkRegistryForShape(data, nInputs, nOutputs);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|