@utxopia/sdk 0.1.0-alpha.2 → 0.1.0-alpha.3

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.
Files changed (142) hide show
  1. package/README.md +214 -108
  2. package/{packages/sdk/dist → dist}/client.d.ts +25 -1
  3. package/{packages/sdk/dist → dist}/client.js +36 -1
  4. package/{packages/sdk/dist → dist}/crypto-ed25519.d.ts +14 -0
  5. package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
  6. package/{packages/sdk/dist → dist}/index.d.ts +3 -3
  7. package/{packages/sdk/dist → dist}/index.js +3 -3
  8. package/{packages/sdk/dist → dist}/instructions.d.ts +28 -11
  9. package/{packages/sdk/dist → dist}/instructions.js +45 -15
  10. package/{packages/sdk/dist → dist}/stealth.d.ts +104 -9
  11. package/{packages/sdk/dist → dist}/stealth.js +146 -14
  12. package/{packages/sdk/dist → dist}/taproot.d.ts +39 -2
  13. package/{packages/sdk/dist → dist}/taproot.js +54 -2
  14. package/package.json +86 -63
  15. package/src/announcement-client.ts +457 -0
  16. package/src/auditor-ciphertext.ts +181 -0
  17. package/src/auditor.ts +409 -0
  18. package/src/bitcoin/ika.ts +103 -0
  19. package/src/bitcoin/index.ts +5 -0
  20. package/src/bound-params.ts +322 -0
  21. package/src/chadbuffer.ts +603 -0
  22. package/src/circomlibjs.d.ts +51 -0
  23. package/src/claim-link.ts +53 -0
  24. package/src/client.ts +638 -0
  25. package/src/commitment-tree.ts +736 -0
  26. package/src/config.ts +772 -0
  27. package/src/core/esplora.ts +332 -0
  28. package/src/core/mempool.ts +159 -0
  29. package/src/crypto-babyjub.ts +385 -0
  30. package/src/crypto-ed25519.ts +297 -0
  31. package/src/crypto.ts +199 -0
  32. package/src/event-client.ts +231 -0
  33. package/src/events.ts +384 -0
  34. package/src/explorer.ts +300 -0
  35. package/src/index.ts +902 -0
  36. package/src/instructions.ts +2820 -0
  37. package/src/keys.ts +1228 -0
  38. package/src/logger.ts +41 -0
  39. package/src/magicblock.ts +278 -0
  40. package/src/merkle.ts +197 -0
  41. package/src/note.ts +754 -0
  42. package/src/pda.ts +516 -0
  43. package/src/pool-state.ts +176 -0
  44. package/src/poseidon.ts +175 -0
  45. package/src/prover/index.ts +19 -0
  46. package/src/prover/mobile.ts +303 -0
  47. package/src/prover/web.ts +771 -0
  48. package/src/psbt.ts +333 -0
  49. package/src/selective-disclosure.ts +284 -0
  50. package/src/sender-memo.ts +343 -0
  51. package/src/snarkjs.d.ts +19 -0
  52. package/src/sns-resolver.ts +333 -0
  53. package/src/solana/connection.ts +189 -0
  54. package/src/solana/priority-fee.ts +201 -0
  55. package/src/spend-doc.ts +163 -0
  56. package/src/stealth.ts +1477 -0
  57. package/src/taproot.ts +707 -0
  58. package/src/token-registry.ts +207 -0
  59. package/src/utils/encoding.ts +33 -0
  60. package/src/vk-registry.ts +295 -0
  61. package/LICENSE +0 -21
  62. package/packages/btc-client/src/esplora-client.ts +0 -153
  63. package/packages/btc-client/src/index.ts +0 -3
  64. package/packages/btc-client/src/op-return.ts +0 -93
  65. package/packages/btc-client/src/types.ts +0 -112
  66. package/packages/sdk/README.md +0 -277
  67. /package/{packages/sdk/dist → dist}/announcement-client.d.ts +0 -0
  68. /package/{packages/sdk/dist → dist}/announcement-client.js +0 -0
  69. /package/{packages/sdk/dist → dist}/auditor-ciphertext.d.ts +0 -0
  70. /package/{packages/sdk/dist → dist}/auditor-ciphertext.js +0 -0
  71. /package/{packages/sdk/dist → dist}/auditor.d.ts +0 -0
  72. /package/{packages/sdk/dist → dist}/auditor.js +0 -0
  73. /package/{packages/sdk/dist → dist}/bitcoin/ika.d.ts +0 -0
  74. /package/{packages/sdk/dist → dist}/bitcoin/ika.js +0 -0
  75. /package/{packages/sdk/dist → dist}/bitcoin/index.d.ts +0 -0
  76. /package/{packages/sdk/dist → dist}/bitcoin/index.js +0 -0
  77. /package/{packages/sdk/dist → dist}/bound-params.d.ts +0 -0
  78. /package/{packages/sdk/dist → dist}/bound-params.js +0 -0
  79. /package/{packages/sdk/dist → dist}/chadbuffer.d.ts +0 -0
  80. /package/{packages/sdk/dist → dist}/chadbuffer.js +0 -0
  81. /package/{packages/sdk/dist → dist}/claim-link.d.ts +0 -0
  82. /package/{packages/sdk/dist → dist}/claim-link.js +0 -0
  83. /package/{packages/sdk/dist → dist}/commitment-tree.d.ts +0 -0
  84. /package/{packages/sdk/dist → dist}/commitment-tree.js +0 -0
  85. /package/{packages/sdk/dist → dist}/config.d.ts +0 -0
  86. /package/{packages/sdk/dist → dist}/config.js +0 -0
  87. /package/{packages/sdk/dist → dist}/core/esplora.d.ts +0 -0
  88. /package/{packages/sdk/dist → dist}/core/esplora.js +0 -0
  89. /package/{packages/sdk/dist → dist}/core/mempool.d.ts +0 -0
  90. /package/{packages/sdk/dist → dist}/core/mempool.js +0 -0
  91. /package/{packages/sdk/dist → dist}/crypto-babyjub.d.ts +0 -0
  92. /package/{packages/sdk/dist → dist}/crypto-babyjub.js +0 -0
  93. /package/{packages/sdk/dist → dist}/crypto.d.ts +0 -0
  94. /package/{packages/sdk/dist → dist}/crypto.js +0 -0
  95. /package/{packages/sdk/dist → dist}/event-client.d.ts +0 -0
  96. /package/{packages/sdk/dist → dist}/event-client.js +0 -0
  97. /package/{packages/sdk/dist → dist}/events.d.ts +0 -0
  98. /package/{packages/sdk/dist → dist}/events.js +0 -0
  99. /package/{packages/sdk/dist → dist}/explorer.d.ts +0 -0
  100. /package/{packages/sdk/dist → dist}/explorer.js +0 -0
  101. /package/{packages/sdk/dist → dist}/keys.d.ts +0 -0
  102. /package/{packages/sdk/dist → dist}/keys.js +0 -0
  103. /package/{packages/sdk/dist → dist}/logger.d.ts +0 -0
  104. /package/{packages/sdk/dist → dist}/logger.js +0 -0
  105. /package/{packages/sdk/dist → dist}/magicblock.d.ts +0 -0
  106. /package/{packages/sdk/dist → dist}/magicblock.js +0 -0
  107. /package/{packages/sdk/dist → dist}/merkle.d.ts +0 -0
  108. /package/{packages/sdk/dist → dist}/merkle.js +0 -0
  109. /package/{packages/sdk/dist → dist}/note.d.ts +0 -0
  110. /package/{packages/sdk/dist → dist}/note.js +0 -0
  111. /package/{packages/sdk/dist → dist}/pda.d.ts +0 -0
  112. /package/{packages/sdk/dist → dist}/pda.js +0 -0
  113. /package/{packages/sdk/dist → dist}/pool-state.d.ts +0 -0
  114. /package/{packages/sdk/dist → dist}/pool-state.js +0 -0
  115. /package/{packages/sdk/dist → dist}/poseidon.d.ts +0 -0
  116. /package/{packages/sdk/dist → dist}/poseidon.js +0 -0
  117. /package/{packages/sdk/dist → dist}/prover/index.d.ts +0 -0
  118. /package/{packages/sdk/dist → dist}/prover/index.js +0 -0
  119. /package/{packages/sdk/dist → dist}/prover/mobile.d.ts +0 -0
  120. /package/{packages/sdk/dist → dist}/prover/mobile.js +0 -0
  121. /package/{packages/sdk/dist → dist}/prover/web.d.ts +0 -0
  122. /package/{packages/sdk/dist → dist}/prover/web.js +0 -0
  123. /package/{packages/sdk/dist → dist}/psbt.d.ts +0 -0
  124. /package/{packages/sdk/dist → dist}/psbt.js +0 -0
  125. /package/{packages/sdk/dist → dist}/selective-disclosure.d.ts +0 -0
  126. /package/{packages/sdk/dist → dist}/selective-disclosure.js +0 -0
  127. /package/{packages/sdk/dist → dist}/sender-memo.d.ts +0 -0
  128. /package/{packages/sdk/dist → dist}/sender-memo.js +0 -0
  129. /package/{packages/sdk/dist → dist}/sns-resolver.d.ts +0 -0
  130. /package/{packages/sdk/dist → dist}/sns-resolver.js +0 -0
  131. /package/{packages/sdk/dist → dist}/solana/connection.d.ts +0 -0
  132. /package/{packages/sdk/dist → dist}/solana/connection.js +0 -0
  133. /package/{packages/sdk/dist → dist}/solana/priority-fee.d.ts +0 -0
  134. /package/{packages/sdk/dist → dist}/solana/priority-fee.js +0 -0
  135. /package/{packages/sdk/dist → dist}/spend-doc.d.ts +0 -0
  136. /package/{packages/sdk/dist → dist}/spend-doc.js +0 -0
  137. /package/{packages/sdk/dist → dist}/token-registry.d.ts +0 -0
  138. /package/{packages/sdk/dist → dist}/token-registry.js +0 -0
  139. /package/{packages/sdk/dist → dist}/utils/encoding.d.ts +0 -0
  140. /package/{packages/sdk/dist → dist}/utils/encoding.js +0 -0
  141. /package/{packages/sdk/dist → dist}/vk-registry.d.ts +0 -0
  142. /package/{packages/sdk/dist → dist}/vk-registry.js +0 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Poseidon Hash - BN254 compatible with circom circuits and Solana's sol_poseidon
3
+ *
4
+ * Uses poseidon-lite: pure JavaScript, zero dependencies, works in
5
+ * Browser, Node.js, and React Native (no WASM required).
6
+ *
7
+ * UNIFIED MODEL:
8
+ * - Commitment = Poseidon(pub_key_x, amount)
9
+ * - Nullifier = Poseidon(priv_key, leaf_index)
10
+ * - Nullifier Hash = Poseidon(nullifier)
11
+ */
12
+
13
+ import {
14
+ poseidon1,
15
+ poseidon2,
16
+ poseidon3,
17
+ poseidon4,
18
+ poseidon5,
19
+ poseidon6,
20
+ poseidon7,
21
+ poseidon8,
22
+ poseidon9,
23
+ poseidon10,
24
+ poseidon11,
25
+ poseidon12,
26
+ poseidon13,
27
+ poseidon14,
28
+ poseidon15,
29
+ poseidon16,
30
+ } from "poseidon-lite";
31
+
32
+ // Lookup table for poseidon hash by input count. JoinSplit currently needs at
33
+ // most 12 (2 fixed fields + N+M<=10); the larger arities remain available to
34
+ // other SDK callers.
35
+ const poseidonFns = [
36
+ undefined, // 0 — unused
37
+ poseidon1, // 1 input
38
+ poseidon2, // 2 inputs
39
+ poseidon3, // 3 inputs
40
+ poseidon4, // 4 inputs
41
+ poseidon5, // 5 inputs
42
+ poseidon6, // 6 inputs
43
+ poseidon7, // 7 inputs
44
+ poseidon8, // 8 inputs
45
+ poseidon9, // 9 inputs
46
+ poseidon10, // 10 inputs
47
+ poseidon11, // 11 inputs
48
+ poseidon12, // 12 inputs
49
+ poseidon13, // 13 inputs
50
+ poseidon14, // 14 inputs
51
+ poseidon15, // 15 inputs
52
+ poseidon16, // 16 inputs
53
+ ] as const;
54
+
55
+ /**
56
+ * Initialize poseidon (no-op — poseidon-lite needs no initialization)
57
+ * Kept for backward API compatibility.
58
+ */
59
+ export async function initPoseidon(): Promise<void> {
60
+ // poseidon-lite is synchronous and ready immediately
61
+ }
62
+
63
+ /**
64
+ * Whether Poseidon can hash right now.
65
+ *
66
+ * Always true: poseidon-lite is pure synchronous JS with nothing to load, so `initPoseidon()`
67
+ * exists only so callers written against a WASM-backed hasher keep working. Kept honest rather
68
+ * than tracking a flag — the previous version lived in note.ts over its own state and reported
69
+ * false no matter what you called.
70
+ */
71
+ export function isPoseidonReady(): boolean {
72
+ return true;
73
+ }
74
+
75
+ /*
76
+ * The async forms of these hashes are gone: poseidonHash, computeUnifiedCommitment,
77
+ * computeNullifier, hashNullifier, computeMPK, computeNPK, computeJoinSplitCommitment and
78
+ * computeJoinSplitNullifier were each `return xSync(...)`. poseidon-lite is pure synchronous JS,
79
+ * so awaiting them promised work that never happened and pushed async up through every caller
80
+ * for nothing — which is why all eight were unused outside this file while the *Sync forms are
81
+ * called 130-odd times. Use the *Sync functions.
82
+ */
83
+
84
+ /**
85
+ * Synchronous Poseidon hash (no initialization required with poseidon-lite)
86
+ */
87
+ export function poseidonHashSync(inputs: bigint[]): bigint {
88
+ const n = inputs.length;
89
+ if (n < 1 || n > 16) {
90
+ throw new Error(`Poseidon: unsupported input count ${n} (expected 1-16)`);
91
+ }
92
+ return poseidonFns[n]!(inputs as any);
93
+ }
94
+
95
+ // BN254 scalar field prime
96
+ export const BN254_SCALAR_FIELD =
97
+ 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
98
+
99
+ // ============================================================================
100
+ // Unified Model Functions (Primary API) - Async versions
101
+ // ============================================================================
102
+
103
+ // ============================================================================
104
+ // Synchronous versions (internal use only - require prior initPoseidon call)
105
+ // These are used by prover.ts which needs sync computation for circuit inputs
106
+ // ============================================================================
107
+
108
+ export function computeUnifiedCommitmentSync(pubKeyX: bigint, amount: bigint): bigint {
109
+ return poseidonHashSync([pubKeyX, amount]);
110
+ }
111
+
112
+ export function computeNullifierSync(privKey: bigint, leafIndex: bigint): bigint {
113
+ return poseidonHashSync([privKey, leafIndex]);
114
+ }
115
+
116
+ export function hashNullifierSync(nullifier: bigint): bigint {
117
+ return poseidonHashSync([nullifier]);
118
+ }
119
+
120
+ // ============================================================================
121
+ // JoinSplit Primitives (Railgun-aligned 3-key model)
122
+ // ============================================================================
123
+
124
+ export function computeMPKSync(pkX: bigint, pkY: bigint, nullifyingKey: bigint): bigint {
125
+ return poseidonHashSync([pkX, pkY, nullifyingKey]);
126
+ }
127
+
128
+ export function computeNPKSync(mpk: bigint, random: bigint): bigint {
129
+ return poseidonHashSync([mpk, random]);
130
+ }
131
+
132
+ export function computeJoinSplitCommitmentSync(npk: bigint, token: bigint, amount: bigint): bigint {
133
+ return poseidonHashSync([npk, token, amount]);
134
+ }
135
+
136
+ export function computeJoinSplitNullifierSync(nullifyingKey: bigint, leafIndex: bigint): bigint {
137
+ return poseidonHashSync([nullifyingKey, leafIndex]);
138
+ }
139
+
140
+ // ============================================================================
141
+ // Multi-Token Utilities
142
+ // ============================================================================
143
+
144
+ /**
145
+ * Reduce a 32-byte big-endian value to BN254 scalar field (mod p).
146
+ * Must match on-chain reduce_to_field_exact in crypto.rs.
147
+ */
148
+ export function reduceToField(bytes: Uint8Array): bigint {
149
+ // Convert big-endian bytes to bigint
150
+ let value = 0n;
151
+ for (const b of bytes) {
152
+ value = (value << 8n) | BigInt(b);
153
+ }
154
+ return value % BN254_SCALAR_FIELD;
155
+ }
156
+
157
+ /**
158
+ * Compute token_id from mint address: Poseidon(reduce_to_field(mint), 0)
159
+ *
160
+ * Must match on-chain compute_token_id in crypto.rs which uses
161
+ * poseidon2_hash(reduce_to_field_exact(mint), [0u8; 32]).
162
+ */
163
+ export function computeTokenId(mintBytes: Uint8Array): bigint {
164
+ const reduced = reduceToField(mintBytes);
165
+ return poseidonHashSync([reduced, 0n]);
166
+ }
167
+
168
+ /**
169
+ * Convenience: compute token_id from a Solana address string
170
+ */
171
+ export function computeTokenIdFromAddress(mintAddress: string): bigint {
172
+ // Base58 decode — import from @solana/kit if available, otherwise use raw bytes
173
+ // For now, caller should pass raw bytes via computeTokenId
174
+ throw new Error("Use computeTokenId(mintBytes) with raw pubkey bytes");
175
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Prover Subpath
3
+ *
4
+ * Auto-detects platform and exports the appropriate prover backend.
5
+ * For explicit imports, use:
6
+ * - @utxopia/sdk/prover/web for browser/Node.js snarkjs Groth16 prover
7
+ * - @utxopia/sdk/prover/mobile for React Native Groth16 prover
8
+ */
9
+
10
+ // Re-export everything from the web prover (default for browser/Node.js)
11
+ export * from "./web";
12
+
13
+ // Re-export types
14
+ export type {
15
+ MerkleProofInput,
16
+ ProofData,
17
+ CircuitType,
18
+ JoinSplitProofInputs,
19
+ } from "./web";
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Mobile Prover for React Native (JoinSplit Architecture)
3
+ *
4
+ * Uses mopro-ffi native Groth16 prover for iOS/Android.
5
+ * Circuit .zkey files are resolved via an injected circuit resolver
6
+ * (bundled for tier 1+2, on-demand from Cloudflare R2 for others).
7
+ */
8
+
9
+ // Re-export types from web for API compatibility
10
+ export type {
11
+ MerkleProofInput,
12
+ ProofData,
13
+ CircuitType,
14
+ JoinSplitProofInputs,
15
+ } from "./web";
16
+
17
+ import type { ProofData, CircuitType, JoinSplitProofInputs } from "./web";
18
+ import {
19
+ computeJoinSplitNullifierSync,
20
+ computeJoinSplitCommitmentSync,
21
+ } from "../poseidon";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Module state
25
+ // ---------------------------------------------------------------------------
26
+
27
+ type CircuitResolver = (
28
+ circuitName: string,
29
+ onProgress?: (progress: number) => void,
30
+ ) => Promise<string>;
31
+
32
+ let moproModule: any = null;
33
+ let circuitResolver: CircuitResolver | null = null;
34
+ let proverReady = false;
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Configuration
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /**
41
+ * Inject the circuit file resolver (called from the mobile app layer).
42
+ * The resolver maps a circuit name like "joinsplit_2x2" to the local
43
+ * file-system path of the corresponding .zkey file.
44
+ */
45
+ export function setCircuitResolver(resolver: CircuitResolver): void {
46
+ circuitResolver = resolver;
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Initialisation
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Initialise the native mopro-ffi prover.
55
+ * Safe to call multiple times — subsequent calls are no-ops.
56
+ */
57
+ export async function initProver(): Promise<void> {
58
+ if (proverReady) return;
59
+
60
+ try {
61
+ // Dynamic require so bundlers don't resolve this at compile time
62
+ // when the native module isn't installed yet.
63
+ moproModule = require("mopro-ffi");
64
+ } catch {
65
+ throw new Error(
66
+ "mopro-ffi native module not found. " +
67
+ "Run the ubrn build step first (see MoproBindings/package.json).",
68
+ );
69
+ }
70
+
71
+ proverReady = true;
72
+ console.log("[MobileProver] mopro-ffi native prover initialised");
73
+ }
74
+
75
+ /**
76
+ * Check whether the native prover is available in the current runtime.
77
+ */
78
+ export async function isProverAvailable(): Promise<boolean> {
79
+ try {
80
+ if (!moproModule) {
81
+ moproModule = require("mopro-ffi");
82
+ }
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Proof generation
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /**
94
+ * Generate a JoinSplit Groth16 proof using the native mopro-ffi prover.
95
+ *
96
+ * @param inputs - JoinSplit proof inputs (typed) OR pre-formatted inputs object
97
+ * @param circuitType - Override circuit variant (default: derived from inputs)
98
+ * @param onProgress - Optional progress callback (0..1)
99
+ */
100
+ export async function generateJoinSplitProof(
101
+ inputs: JoinSplitProofInputs | any,
102
+ circuitType?: string,
103
+ onProgress?: (progress: number) => void,
104
+ ): Promise<ProofData> {
105
+ if (!proverReady) await initProver();
106
+ if (!circuitResolver) {
107
+ throw new Error(
108
+ "Circuit resolver not set. Call setCircuitResolver() before generating proofs.",
109
+ );
110
+ }
111
+
112
+ // Determine circuit variant
113
+ const nIn: number = inputs.nInputs ?? inputs.inputs?.length ?? 1;
114
+ const nOut: number = inputs.nOutputs ?? inputs.outputs?.length ?? 2;
115
+ const variant: CircuitType =
116
+ (circuitType as CircuitType) ?? `joinsplit_${nIn}x${nOut}`;
117
+
118
+ console.log(`[MobileProver] Generating ${variant} Groth16 proof...`);
119
+ const startTime = Date.now();
120
+
121
+ // Resolve the .zkey path (may trigger a download)
122
+ onProgress?.(0);
123
+ const zkeyPath = await circuitResolver(variant, onProgress);
124
+ onProgress?.(0.1);
125
+
126
+ // Format inputs for mopro's flat Record<string, string[]> format
127
+ const flatInputs = formatCircuitInputs(inputs, nIn, nOut);
128
+
129
+ // Call the native prover
130
+ onProgress?.(0.2);
131
+ const result = await moproModule.generateCircomProof(zkeyPath, flatInputs);
132
+ onProgress?.(0.9);
133
+
134
+ const elapsed = Date.now() - startTime;
135
+ console.log(`[MobileProver] Proof generated in ${elapsed}ms`);
136
+
137
+ // Convert mopro result to our ProofData format
138
+ const proofData = convertMoproResult(result);
139
+ onProgress?.(1);
140
+
141
+ return proofData;
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Input formatting
146
+ // ---------------------------------------------------------------------------
147
+
148
+ /**
149
+ * Convert JoinSplitProofInputs to mopro's flat Record<string, string[]>.
150
+ *
151
+ * mopro expects every value as a string array — scalars become single-element
152
+ * arrays, vectors become multi-element arrays.
153
+ */
154
+ function formatCircuitInputs(
155
+ inputs: JoinSplitProofInputs | any,
156
+ nIn: number,
157
+ nOut: number,
158
+ ): Record<string, string[]> {
159
+ const flat: Record<string, string[]> = {};
160
+
161
+ // If inputs are already in flat format, pass through
162
+ if (!inputs.merkleRoot && !inputs.inputs) {
163
+ const result: Record<string, string[]> = {};
164
+ for (const [key, value] of Object.entries(inputs)) {
165
+ if (Array.isArray(value)) {
166
+ result[key] = (value as any[]).map((v) => String(v));
167
+ } else {
168
+ result[key] = [String(value)];
169
+ }
170
+ }
171
+ return result;
172
+ }
173
+
174
+ // Typed JoinSplitProofInputs — convert to circuit signal names
175
+ const typed = inputs as JoinSplitProofInputs;
176
+
177
+ flat["merkleRoot"] = [typed.merkleRoot.toString()];
178
+ flat["boundParamsHash"] = [typed.boundParamsHash.toString()];
179
+ flat["token"] = [typed.token.toString()];
180
+ flat["publicKey"] = typed.publicKey.map((p) => p.toString());
181
+ flat["signature"] = typed.signature.map((s) => s.toString());
182
+ flat["nullifyingKey"] = [typed.nullifyingKey.toString()];
183
+
184
+ // Input arrays
185
+ flat["randomIn"] = typed.inputs.map((i) => i.random.toString());
186
+ flat["valueIn"] = typed.inputs.map((i) => i.value.toString());
187
+ flat["leavesIndices"] = typed.inputs.map((i) => i.leafIndex.toString());
188
+
189
+ // Output arrays
190
+ flat["npkOut"] = typed.outputs.map((o) => o.npk.toString());
191
+ flat["valueOut"] = typed.outputs.map((o) => o.value.toString());
192
+
193
+ // Compute nullifiers: Poseidon(nullifyingKey, leafIndex)
194
+ const nullifiers: bigint[] = [];
195
+ for (const inp of typed.inputs) {
196
+ nullifiers.push(computeJoinSplitNullifierSync(typed.nullifyingKey, inp.leafIndex));
197
+ }
198
+ flat["nullifiers"] = nullifiers.map((n) => n.toString());
199
+
200
+ // Compute output commitments: Poseidon(npk, token, value)
201
+ const commitmentsOut: bigint[] = [];
202
+ for (const out of typed.outputs) {
203
+ commitmentsOut.push(computeJoinSplitCommitmentSync(out.npk, typed.token, out.value));
204
+ }
205
+ flat["commitmentsOut"] = commitmentsOut.map((c) => c.toString());
206
+
207
+ // Flattened siblings pathElements[i][j]; circuit derives path bits from leavesIndices.
208
+ const pathElements: string[] = [];
209
+ for (const inp of typed.inputs) {
210
+ for (const sibling of inp.merkleProof.siblings) {
211
+ pathElements.push(sibling.toString());
212
+ }
213
+ }
214
+ flat["pathElements"] = pathElements;
215
+
216
+ return flat;
217
+ }
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // Result conversion
221
+ // ---------------------------------------------------------------------------
222
+
223
+ /**
224
+ * Convert mopro's CircomProofResult to our ProofData format.
225
+ *
226
+ * mopro returns: { proof: { a: {x, y}, b: {x: [x0,x1], y: [y0,y1]}, c: {x, y} }, inputs: string[] }
227
+ * We need: { proof: Uint8Array(256), publicInputs: string[] }
228
+ */
229
+ function convertMoproResult(result: any): ProofData {
230
+ const { proof, inputs: publicInputs } = result;
231
+
232
+ const bytes = new Uint8Array(256);
233
+
234
+ // G1 point A (64 bytes): x, y
235
+ writeBigIntBE(bytes, 0, BigInt(proof.a.x), 32);
236
+ writeBigIntBE(bytes, 32, BigInt(proof.a.y), 32);
237
+
238
+ // G2 point B (128 bytes): [x_imag, x_real, y_imag, y_real]
239
+ // mopro b.x = [x0, x1], b.y = [y0, y1]
240
+ // On-chain layout: x_imag(x1), x_real(x0), y_imag(y1), y_real(y0)
241
+ writeBigIntBE(bytes, 64, BigInt(proof.b.x[1]), 32);
242
+ writeBigIntBE(bytes, 96, BigInt(proof.b.x[0]), 32);
243
+ writeBigIntBE(bytes, 128, BigInt(proof.b.y[1]), 32);
244
+ writeBigIntBE(bytes, 160, BigInt(proof.b.y[0]), 32);
245
+
246
+ // G1 point C (64 bytes): x, y
247
+ writeBigIntBE(bytes, 192, BigInt(proof.c.x), 32);
248
+ writeBigIntBE(bytes, 224, BigInt(proof.c.y), 32);
249
+
250
+ return {
251
+ proof: bytes,
252
+ publicInputs: publicInputs ?? [],
253
+ };
254
+ }
255
+
256
+ function writeBigIntBE(
257
+ buf: Uint8Array,
258
+ offset: number,
259
+ value: bigint,
260
+ length: number,
261
+ ): void {
262
+ for (let i = length - 1; i >= 0; i--) {
263
+ buf[offset + i] = Number(value & 0xffn);
264
+ value >>= 8n;
265
+ }
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // Utilities
270
+ // ---------------------------------------------------------------------------
271
+
272
+ /**
273
+ * Convert a ProofData to raw 256-byte proof for on-chain submission.
274
+ */
275
+ export function proofToBytes(proof: ProofData): Uint8Array {
276
+ return proof.proof;
277
+ }
278
+
279
+ /**
280
+ * Check if a circuit variant is available (resolver can find the .zkey).
281
+ */
282
+ export async function circuitExists(
283
+ circuitType?: CircuitType | string,
284
+ ): Promise<boolean> {
285
+ if (!circuitResolver) return false;
286
+ const name = circuitType ?? "joinsplit_2x2";
287
+ try {
288
+ await circuitResolver(name);
289
+ return true;
290
+ } catch {
291
+ return false;
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Reset module state (for testing or hot-reload scenarios).
297
+ */
298
+ export async function cleanup(): Promise<void> {
299
+ moproModule = null;
300
+ circuitResolver = null;
301
+ proverReady = false;
302
+ console.log("[MobileProver] Cleaned up");
303
+ }