@unlink-xyz/core 0.1.0

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 (185) hide show
  1. package/dist/account/zkAccount.d.ts +38 -0
  2. package/dist/account/zkAccount.d.ts.map +1 -0
  3. package/dist/account/zkAccount.js +128 -0
  4. package/dist/clients/broadcaster.d.ts +33 -0
  5. package/dist/clients/broadcaster.d.ts.map +1 -0
  6. package/dist/clients/broadcaster.js +23 -0
  7. package/dist/clients/http.d.ts +23 -0
  8. package/dist/clients/http.d.ts.map +1 -0
  9. package/dist/clients/http.js +57 -0
  10. package/dist/clients/indexer.d.ts +44 -0
  11. package/dist/clients/indexer.d.ts.map +1 -0
  12. package/dist/clients/indexer.js +67 -0
  13. package/dist/config.d.ts +12 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +29 -0
  16. package/dist/core.d.ts +10 -0
  17. package/dist/core.d.ts.map +1 -0
  18. package/dist/core.js +12 -0
  19. package/dist/errors.d.ts +10 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +18 -0
  22. package/dist/index.d.ts +18 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +16 -0
  25. package/dist/key-derivation/babyjubjub.d.ts +9 -0
  26. package/dist/key-derivation/babyjubjub.d.ts.map +1 -0
  27. package/dist/key-derivation/babyjubjub.js +9 -0
  28. package/dist/key-derivation/bech32.d.ts +22 -0
  29. package/dist/key-derivation/bech32.d.ts.map +1 -0
  30. package/dist/key-derivation/bech32.js +86 -0
  31. package/dist/key-derivation/bip32.d.ts +17 -0
  32. package/dist/key-derivation/bip32.d.ts.map +1 -0
  33. package/dist/key-derivation/bip32.js +41 -0
  34. package/dist/key-derivation/bip39.d.ts +22 -0
  35. package/dist/key-derivation/bip39.d.ts.map +1 -0
  36. package/dist/key-derivation/bip39.js +56 -0
  37. package/dist/key-derivation/bytes.d.ts +19 -0
  38. package/dist/key-derivation/bytes.d.ts.map +1 -0
  39. package/dist/key-derivation/bytes.js +92 -0
  40. package/dist/key-derivation/hash.d.ts +3 -0
  41. package/dist/key-derivation/hash.d.ts.map +1 -0
  42. package/dist/key-derivation/hash.js +10 -0
  43. package/dist/key-derivation/index.d.ts +8 -0
  44. package/dist/key-derivation/index.d.ts.map +1 -0
  45. package/dist/key-derivation/index.js +7 -0
  46. package/dist/key-derivation/wallet-node.d.ts +45 -0
  47. package/dist/key-derivation/wallet-node.d.ts.map +1 -0
  48. package/dist/key-derivation/wallet-node.js +109 -0
  49. package/dist/keys.d.ts +22 -0
  50. package/dist/keys.d.ts.map +1 -0
  51. package/dist/keys.js +41 -0
  52. package/dist/prover/config.d.ts +60 -0
  53. package/dist/prover/config.d.ts.map +1 -0
  54. package/dist/prover/config.js +80 -0
  55. package/dist/prover/index.d.ts +2 -0
  56. package/dist/prover/index.d.ts.map +1 -0
  57. package/dist/prover/index.js +1 -0
  58. package/dist/prover/prover.d.ts +59 -0
  59. package/dist/prover/prover.d.ts.map +1 -0
  60. package/dist/prover/prover.js +274 -0
  61. package/dist/prover/registry.d.ts +39 -0
  62. package/dist/prover/registry.d.ts.map +1 -0
  63. package/dist/prover/registry.js +57 -0
  64. package/dist/schema.d.ts +4 -0
  65. package/dist/schema.d.ts.map +1 -0
  66. package/dist/schema.js +14 -0
  67. package/dist/state/ciphertext-store.d.ts +12 -0
  68. package/dist/state/ciphertext-store.d.ts.map +1 -0
  69. package/dist/state/ciphertext-store.js +25 -0
  70. package/dist/state/index.d.ts +3 -0
  71. package/dist/state/index.d.ts.map +1 -0
  72. package/dist/state/index.js +2 -0
  73. package/dist/state/leaf-store.d.ts +17 -0
  74. package/dist/state/leaf-store.d.ts.map +1 -0
  75. package/dist/state/leaf-store.js +35 -0
  76. package/dist/state/merkle/hydrator.d.ts +27 -0
  77. package/dist/state/merkle/hydrator.d.ts.map +1 -0
  78. package/dist/state/merkle/hydrator.js +36 -0
  79. package/dist/state/merkle/index.d.ts +3 -0
  80. package/dist/state/merkle/index.d.ts.map +1 -0
  81. package/dist/state/merkle/index.js +2 -0
  82. package/dist/state/merkle/merkle-tree.d.ts +34 -0
  83. package/dist/state/merkle/merkle-tree.d.ts.map +1 -0
  84. package/dist/state/merkle/merkle-tree.js +104 -0
  85. package/dist/state/merkle-tree.d.ts +34 -0
  86. package/dist/state/merkle-tree.d.ts.map +1 -0
  87. package/dist/state/merkle-tree.js +104 -0
  88. package/dist/state/note-store.d.ts +37 -0
  89. package/dist/state/note-store.d.ts.map +1 -0
  90. package/dist/state/note-store.js +133 -0
  91. package/dist/state/nullifier-store.d.ts +13 -0
  92. package/dist/state/nullifier-store.d.ts.map +1 -0
  93. package/dist/state/nullifier-store.js +21 -0
  94. package/dist/state/records.d.ts +57 -0
  95. package/dist/state/records.d.ts.map +1 -0
  96. package/dist/state/records.js +1 -0
  97. package/dist/state/root-store.d.ts +13 -0
  98. package/dist/state/root-store.d.ts.map +1 -0
  99. package/dist/state/root-store.js +30 -0
  100. package/dist/state/store/ciphertext-store.d.ts +12 -0
  101. package/dist/state/store/ciphertext-store.d.ts.map +1 -0
  102. package/dist/state/store/ciphertext-store.js +25 -0
  103. package/dist/state/store/index.d.ts +10 -0
  104. package/dist/state/store/index.d.ts.map +1 -0
  105. package/dist/state/store/index.js +8 -0
  106. package/dist/state/store/job-store.d.ts +12 -0
  107. package/dist/state/store/job-store.d.ts.map +1 -0
  108. package/dist/state/store/job-store.js +118 -0
  109. package/dist/state/store/jobs.d.ts +50 -0
  110. package/dist/state/store/jobs.d.ts.map +1 -0
  111. package/dist/state/store/jobs.js +1 -0
  112. package/dist/state/store/leaf-store.d.ts +17 -0
  113. package/dist/state/store/leaf-store.d.ts.map +1 -0
  114. package/dist/state/store/leaf-store.js +35 -0
  115. package/dist/state/store/note-store.d.ts +38 -0
  116. package/dist/state/store/note-store.d.ts.map +1 -0
  117. package/dist/state/store/note-store.js +142 -0
  118. package/dist/state/store/nullifier-store.d.ts +17 -0
  119. package/dist/state/store/nullifier-store.d.ts.map +1 -0
  120. package/dist/state/store/nullifier-store.js +30 -0
  121. package/dist/state/store/records.d.ts +57 -0
  122. package/dist/state/store/records.d.ts.map +1 -0
  123. package/dist/state/store/records.js +1 -0
  124. package/dist/state/store/root-store.d.ts +13 -0
  125. package/dist/state/store/root-store.d.ts.map +1 -0
  126. package/dist/state/store/root-store.js +30 -0
  127. package/dist/state/store/store.d.ts +34 -0
  128. package/dist/state/store/store.d.ts.map +1 -0
  129. package/dist/state/store/store.js +22 -0
  130. package/dist/state/store.d.ts +26 -0
  131. package/dist/state/store.d.ts.map +1 -0
  132. package/dist/state/store.js +19 -0
  133. package/dist/storage/index.d.ts +4 -0
  134. package/dist/storage/index.d.ts.map +1 -0
  135. package/dist/storage/index.js +2 -0
  136. package/dist/storage/indexeddb.d.ts +27 -0
  137. package/dist/storage/indexeddb.d.ts.map +1 -0
  138. package/dist/storage/indexeddb.js +205 -0
  139. package/dist/storage/memory.d.ts +25 -0
  140. package/dist/storage/memory.d.ts.map +1 -0
  141. package/dist/storage/memory.js +87 -0
  142. package/dist/transactions/deposit.d.ts +18 -0
  143. package/dist/transactions/deposit.d.ts.map +1 -0
  144. package/dist/transactions/deposit.js +173 -0
  145. package/dist/transactions/index.d.ts +7 -0
  146. package/dist/transactions/index.d.ts.map +1 -0
  147. package/dist/transactions/index.js +4 -0
  148. package/dist/transactions/note-sync.d.ts +46 -0
  149. package/dist/transactions/note-sync.d.ts.map +1 -0
  150. package/dist/transactions/note-sync.js +320 -0
  151. package/dist/transactions/reconcile.d.ts +22 -0
  152. package/dist/transactions/reconcile.d.ts.map +1 -0
  153. package/dist/transactions/reconcile.js +39 -0
  154. package/dist/transactions/transact.d.ts +34 -0
  155. package/dist/transactions/transact.d.ts.map +1 -0
  156. package/dist/transactions/transact.js +561 -0
  157. package/dist/transactions/types.d.ts +114 -0
  158. package/dist/transactions/types.d.ts.map +1 -0
  159. package/dist/transactions/types.js +1 -0
  160. package/dist/tsconfig.tsbuildinfo +1 -0
  161. package/dist/types.d.ts +27 -0
  162. package/dist/types.d.ts.map +1 -0
  163. package/dist/types.js +1 -0
  164. package/dist/utils/async.d.ts +10 -0
  165. package/dist/utils/async.d.ts.map +1 -0
  166. package/dist/utils/async.js +13 -0
  167. package/dist/utils/bigint.d.ts +9 -0
  168. package/dist/utils/bigint.d.ts.map +1 -0
  169. package/dist/utils/bigint.js +29 -0
  170. package/dist/utils/crypto.d.ts +12 -0
  171. package/dist/utils/crypto.d.ts.map +1 -0
  172. package/dist/utils/crypto.js +39 -0
  173. package/dist/utils/json-codec.d.ts +9 -0
  174. package/dist/utils/json-codec.d.ts.map +1 -0
  175. package/dist/utils/json-codec.js +25 -0
  176. package/dist/utils/polling.d.ts +7 -0
  177. package/dist/utils/polling.d.ts.map +1 -0
  178. package/dist/utils/polling.js +6 -0
  179. package/dist/utils/signature.d.ts +9 -0
  180. package/dist/utils/signature.d.ts.map +1 -0
  181. package/dist/utils/signature.js +12 -0
  182. package/dist/utils/validators.d.ts +30 -0
  183. package/dist/utils/validators.d.ts.map +1 -0
  184. package/dist/utils/validators.js +70 -0
  185. package/package.json +52 -0
@@ -0,0 +1,561 @@
1
+ import { poseidon } from "@railgun-community/circomlibjs";
2
+ import { AbiCoder, Interface, keccak256 } from "ethers";
3
+ import { createBroadcasterClient } from "../clients/broadcaster.js";
4
+ import { createIndexerClient } from "../clients/indexer.js";
5
+ import { serviceConfig } from "../config.js";
6
+ import { CoreError } from "../errors.js";
7
+ import { proveTransaction } from "../prover/index.js";
8
+ import { createMerkleTrees, DEFAULT_JOB_TIMEOUT_MS, rebuildTreeFromStore, } from "../state/index.js";
9
+ import { isNotFoundError, sleep } from "../utils/async.js";
10
+ import { ensureBigint, formatUint256, parseHexToBigInt, } from "../utils/bigint.js";
11
+ import { deriveCommitment, encryptNote } from "../utils/crypto.js";
12
+ import { DEFAULT_POLL_INTERVAL_MS, DEFAULT_POLL_TIMEOUT_MS, MAX_POLL_INTERVAL_MS, } from "../utils/polling.js";
13
+ import { signTransactMessage } from "../utils/signature.js";
14
+ import { ensureAddress, ensureChainId, ensureNoteCommitmentInput, ensureWithdrawalInput, SNARK_SCALAR_FIELD, } from "../utils/validators.js";
15
+ export const TRANSACT_ABI = "function transact(((uint256[2] pA, uint256[2][2] pB, uint256[2] pC) proof, uint256 merkleRoot, uint256[] nullifierHashes, uint256[] newCommitments, (uint64 chainID, address poolAddress) context, (uint256 npk, uint256 amount, address token) withdrawal, (uint256[3] data)[] ciphertexts)[] _transactions)";
16
+ const transactInterface = new Interface([TRANSACT_ABI]);
17
+ function validateTransactRequest(request) {
18
+ ensureChainId(request.chainId);
19
+ ensureAddress("pool address", request.poolAddress);
20
+ ensureAddress("token", request.token);
21
+ ensureBigint("nullifyingKey", request.zkAccount.nullifyingKey);
22
+ if (!Array.isArray(request.inputs) || request.inputs.length === 0) {
23
+ throw new CoreError("at least one input note is required");
24
+ }
25
+ request.inputs.forEach((input, idx) => {
26
+ if (!Number.isInteger(input.index) || input.index < 0) {
27
+ throw new CoreError(`inputs[${idx}].index must be a non-negative integer`);
28
+ }
29
+ });
30
+ ensureWithdrawalInput("request.withdrawal", request.withdrawal);
31
+ request.outputs.forEach((output, idx) => {
32
+ if (output.mpk < 0n) {
33
+ throw new CoreError(`outputs[${idx}].mpk must be non-negative`);
34
+ }
35
+ if (output.random < 0n) {
36
+ throw new CoreError(`outputs[${idx}].random must be non-negative`);
37
+ }
38
+ ensureNoteCommitmentInput(`outputs[${idx}]`, {
39
+ npk: poseidon([output.mpk, output.random]),
40
+ amount: output.amount,
41
+ token: output.token,
42
+ });
43
+ });
44
+ }
45
+ /**
46
+ * Computes the bound parameters hash from chain ID, pool address, and withdrawal parameters.
47
+ * This hash binds the transaction to specific chain and withdrawal context.
48
+ */
49
+ export function computeBoundParamsHash(chainId, poolAddress) {
50
+ const chainIdBigInt = BigInt(chainId);
51
+ const poolAddressBigInt = BigInt(poolAddress);
52
+ const hashInput = [chainIdBigInt, poolAddressBigInt];
53
+ const coder = AbiCoder.defaultAbiCoder();
54
+ const input = coder.encode(["uint64", "uint160"], hashInput);
55
+ const result = keccak256(input);
56
+ return parseHexToBigInt(result) % SNARK_SCALAR_FIELD;
57
+ }
58
+ export function serializeWitness(proof, index) {
59
+ return {
60
+ root: formatUint256(BigInt(proof.root)),
61
+ leaf: formatUint256(BigInt(proof.leaf)),
62
+ pathElements: proof.siblings.map((level) => level.map((node) => formatUint256(BigInt(node)))),
63
+ pathIndices: proof.pathIndices ?? [],
64
+ leafIndex: index,
65
+ };
66
+ }
67
+ export function deserializeWitness(serialized) {
68
+ return {
69
+ root: parseHexToBigInt(serialized.root),
70
+ leaf: parseHexToBigInt(serialized.leaf),
71
+ siblings: serialized.pathElements.map((level) => level.map((node) => parseHexToBigInt(node))),
72
+ pathIndices: serialized.pathIndices,
73
+ leafIndex: serialized.leafIndex,
74
+ };
75
+ }
76
+ /**
77
+ * Simulates the transact pipeline: builds Merkle witnesses, derives nullifiers, and updates local state.
78
+ */
79
+ export function createTransactService(stateStore, options = {}) {
80
+ if (!stateStore) {
81
+ throw new Error("stateStore dependency is required");
82
+ }
83
+ const trees = options.merkleTrees ?? createMerkleTrees();
84
+ const fetchImpl = options.fetch ?? (typeof fetch === "function" ? fetch : undefined);
85
+ const broadcasterClient = fetchImpl
86
+ ? createBroadcasterClient(serviceConfig.broadcasterBaseUrl, {
87
+ fetch: fetchImpl,
88
+ })
89
+ : null;
90
+ const indexerClient = fetchImpl
91
+ ? createIndexerClient(serviceConfig.indexerBaseUrl, { fetch: fetchImpl })
92
+ : null;
93
+ const pollIntervalMs = Math.min(options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
94
+ const pollTimeoutMs = options.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
95
+ // Gather witnesses and nullifiers for each input note; also guards against stale or inconsistent state.
96
+ async function buildInputContexts(request) {
97
+ const contexts = [];
98
+ let sharedRoot;
99
+ for (const [idx, input] of request.inputs.entries()) {
100
+ if (!Number.isInteger(input.index) || input.index < 0) {
101
+ throw new CoreError(`inputs[${idx}].index must be a non-negative integer`);
102
+ }
103
+ const note = await stateStore.getNote(request.chainId, input.index);
104
+ if (!note) {
105
+ throw new CoreError(`note ${input.index} not found for chain ${request.chainId}`);
106
+ }
107
+ if (note.spentAt !== undefined) {
108
+ throw new CoreError(`note ${input.index} is already spent`);
109
+ }
110
+ const tree = trees.getOrCreate(request.chainId);
111
+ if (input.index >= tree.getLeafCount()) {
112
+ throw new CoreError(`note ${input.index} out of range for tree`);
113
+ }
114
+ const proof = tree.createMerkleProof(input.index);
115
+ const proofRoot = formatUint256(BigInt(proof.root));
116
+ if (!sharedRoot) {
117
+ sharedRoot = proofRoot;
118
+ }
119
+ else if (sharedRoot !== proofRoot) {
120
+ throw new CoreError("input notes must share the same Merkle root");
121
+ }
122
+ const mpk = parseHexToBigInt(note.mpk);
123
+ const random = parseHexToBigInt(note.random);
124
+ const derivedNpk = poseidon([mpk, random]);
125
+ const storedNpk = parseHexToBigInt(note.npk);
126
+ if (storedNpk !== derivedNpk) {
127
+ throw new CoreError(`note ${input.index} npk mismatch`);
128
+ }
129
+ const amount = BigInt(note.value);
130
+ if (amount < 0n) {
131
+ throw new CoreError(`note ${input.index} amount must be non-negative`);
132
+ }
133
+ ensureAddress("note token", note.token);
134
+ const tokenScalar = BigInt(note.token);
135
+ const commitment = poseidon([derivedNpk, tokenScalar, amount]);
136
+ const storedCommitment = parseHexToBigInt(note.commitment);
137
+ if (storedCommitment !== commitment) {
138
+ throw new CoreError(`note ${input.index} commitment mismatch`);
139
+ }
140
+ const proofLeaf = BigInt(proof.leaf);
141
+ if (proofLeaf !== storedCommitment) {
142
+ throw new CoreError(`note ${input.index} proof leaf mismatch`);
143
+ }
144
+ const nullifierValue = poseidon([
145
+ request.zkAccount.nullifyingKey,
146
+ BigInt(input.index),
147
+ ]);
148
+ const context = {
149
+ index: input.index,
150
+ nullifier: {
151
+ value: nullifierValue,
152
+ hex: formatUint256(nullifierValue),
153
+ },
154
+ witness: proof,
155
+ };
156
+ contexts.push(context);
157
+ }
158
+ if (!sharedRoot) {
159
+ throw new CoreError("at least one input note is required");
160
+ }
161
+ return { root: sharedRoot, contexts };
162
+ }
163
+ // Record the nullifiers and mark each consumed note as spent at the same timestamp.
164
+ async function persistNullifiersAndSpend(chainId, contexts, timestamp) {
165
+ await Promise.all(contexts.map((context) => Promise.all([
166
+ stateStore.putNullifier({
167
+ chainId,
168
+ nullifier: context.nullifier.hex,
169
+ noteIndex: context.index,
170
+ }),
171
+ stateStore.markNoteSpent(chainId, context.index, timestamp),
172
+ ])));
173
+ }
174
+ /**
175
+ * Derive commitments and assign provisional indexes without mutating local state.
176
+ * The local tree length stands in for the on-chain leaf count when predicting indices.
177
+ */
178
+ function computeAssignedCommitments(chainId, outputs = []) {
179
+ const baseIndex = trees.getLeafCount(chainId);
180
+ return outputs.map((output, idx) => {
181
+ const commitment = deriveCommitment({
182
+ npk: poseidon([output.mpk, output.random]),
183
+ amount: output.amount,
184
+ token: output.token,
185
+ });
186
+ return {
187
+ value: commitment,
188
+ hex: formatUint256(commitment),
189
+ index: baseIndex + idx,
190
+ };
191
+ });
192
+ }
193
+ async function waitForIndexedCommitments(chainId, outputs) {
194
+ if (!indexerClient) {
195
+ return [];
196
+ }
197
+ const pending = new Set(outputs.map((output) => output.hex.toLowerCase()));
198
+ const records = new Map();
199
+ let delay = pollIntervalMs;
200
+ const deadline = Date.now() + pollTimeoutMs;
201
+ while (pending.size > 0 && Date.now() <= deadline) {
202
+ for (const commitment of [...pending]) {
203
+ const record = await indexerClient
204
+ .getCommitment({ chainId, commitment })
205
+ .catch((err) => {
206
+ if (!isNotFoundError(err)) {
207
+ throw err;
208
+ }
209
+ return null;
210
+ });
211
+ if (record) {
212
+ const key = record.commitment.toLowerCase();
213
+ records.set(key, record);
214
+ pending.delete(commitment);
215
+ }
216
+ }
217
+ if (pending.size === 0) {
218
+ break;
219
+ }
220
+ await sleep(delay);
221
+ delay = Math.min(delay * 2, MAX_POLL_INTERVAL_MS);
222
+ }
223
+ if (pending.size > 0) {
224
+ throw new CoreError("commitments not found in indexer before timeout");
225
+ }
226
+ return outputs.map((output) => {
227
+ const key = output.hex.toLowerCase();
228
+ const record = records.get(key);
229
+ if (!record) {
230
+ throw new CoreError(`missing indexed commitment ${output.hex} after fetch`);
231
+ }
232
+ return record;
233
+ });
234
+ }
235
+ async function submitAndAwaitBroadcasterRelay(relayId, pending) {
236
+ if (!broadcasterClient) {
237
+ return;
238
+ }
239
+ if (!pending.broadcasterRelayId) {
240
+ const submission = await broadcasterClient.submitRelay({
241
+ clientTxId: relayId,
242
+ chainId: pending.chainId,
243
+ payload: {
244
+ kind: "call_data",
245
+ to: pending.poolAddress,
246
+ data: pending.calldata,
247
+ },
248
+ });
249
+ if (!submission.accepted) {
250
+ throw new CoreError(submission.message ??
251
+ "broadcaster rejected transaction relay submission");
252
+ }
253
+ pending.broadcasterRelayId = submission.id;
254
+ }
255
+ const deadline = Date.now() + pollTimeoutMs;
256
+ while (Date.now() <= deadline) {
257
+ const status = await broadcasterClient.getRelayStatus(pending.broadcasterRelayId);
258
+ if (status.state === "succeeded") {
259
+ pending.txHash = status.txHash ?? pending.txHash ?? null;
260
+ return;
261
+ }
262
+ if (status.state === "failed" || status.state === "dead") {
263
+ throw new CoreError(status.error ?? "broadcaster relay failed");
264
+ }
265
+ await sleep(pollIntervalMs);
266
+ }
267
+ throw new CoreError("broadcaster relay timed out");
268
+ }
269
+ async function applyIndexerUpdates(chainId, records) {
270
+ const applied = [];
271
+ const sorted = [...records].sort((a, b) => a.index - b.index);
272
+ for (const entry of sorted) {
273
+ const value = parseHexToBigInt(entry.commitment);
274
+ const currentCount = trees.getLeafCount(chainId);
275
+ if (entry.index < currentCount) {
276
+ const proof = trees.createMerkleProof(chainId, entry.index);
277
+ if (BigInt(proof.leaf) !== value) {
278
+ throw new CoreError(`existing leaf mismatch at index ${entry.index}, stored ${proof.leaf.toString(16)}, indexed ${entry.commitment}`);
279
+ }
280
+ applied.push({
281
+ value,
282
+ hex: entry.commitment,
283
+ index: entry.index,
284
+ root: entry.root,
285
+ });
286
+ continue;
287
+ }
288
+ if (entry.index > currentCount) {
289
+ throw new CoreError(`local merkle tree gap, expected next index ${currentCount}, got indexed ${entry.index}`);
290
+ }
291
+ // Replay the commitment into the local Merkle tree and ensure the assigned index matches reality.
292
+ const { index, root } = trees.addLeaf(chainId, value);
293
+ if (index !== entry.index) {
294
+ throw new CoreError(`local merkle tree desynchronized, indexed ${entry.index}, local ${index}`);
295
+ }
296
+ applied.push({
297
+ value,
298
+ hex: entry.commitment,
299
+ index,
300
+ root,
301
+ });
302
+ await Promise.all([
303
+ stateStore.putLeaf({
304
+ chainId,
305
+ index,
306
+ commitment: entry.commitment,
307
+ }),
308
+ stateStore.putRoot({
309
+ chainId,
310
+ root,
311
+ }),
312
+ ]);
313
+ }
314
+ return applied;
315
+ }
316
+ async function persistTransactSuccess(job, pending, persisted) {
317
+ await stateStore.putRoot({
318
+ chainId: pending.chainId,
319
+ root: persisted.latestRoot,
320
+ });
321
+ await stateStore.putPendingJob({
322
+ ...job,
323
+ status: "succeeded",
324
+ broadcasterRelayId: pending.broadcasterRelayId ?? job.broadcasterRelayId,
325
+ txHash: pending.txHash ?? job.txHash ?? null,
326
+ lastCheckedAt: Date.now(),
327
+ predictedOutputs: job.predictedOutputs.map((output) => {
328
+ const matched = persisted.rootedCommitments.find((rc) => rc.hex.toLowerCase() === output.hex.toLowerCase());
329
+ return {
330
+ ...output,
331
+ index: matched?.index ?? output.index,
332
+ root: matched?.root ?? output.root,
333
+ };
334
+ }),
335
+ expectedRoot: persisted.latestRoot,
336
+ });
337
+ }
338
+ const allocateRelayId = () => {
339
+ if (typeof globalThis.crypto?.randomUUID === "function") {
340
+ return globalThis.crypto.randomUUID();
341
+ }
342
+ return `transact-${Date.now().toString(16)}-${Math.floor(Math.random() * 1e6)}`;
343
+ };
344
+ return {
345
+ async transact(request) {
346
+ validateTransactRequest(request);
347
+ await rebuildTreeFromStore({
348
+ chainId: request.chainId,
349
+ trees,
350
+ loadLeaf: stateStore.getLeaf.bind(stateStore),
351
+ });
352
+ const { root, contexts } = await buildInputContexts(request);
353
+ const assignedOutputs = computeAssignedCommitments(request.chainId, request.outputs);
354
+ // Compute bound parameters hash
355
+ const boundParamsHash = computeBoundParamsHash(request.chainId, request.poolAddress);
356
+ // Public signals must match the circuit's public input order:
357
+ // [merkleRoot, boundParamsHash, ...nullifiers, ...commitmentsOut]
358
+ const pubSignals = [
359
+ parseHexToBigInt(root),
360
+ boundParamsHash,
361
+ ...contexts.map((context) => context.nullifier.value),
362
+ ...assignedOutputs.map((output) => output.value),
363
+ ];
364
+ // Retrieve input notes to access random and value fields
365
+ const inputNotes = await Promise.all(request.inputs.map(async (input) => {
366
+ const note = await stateStore.getNote(request.chainId, input.index);
367
+ if (!note) {
368
+ throw new CoreError(`note ${input.index} not found for chain`);
369
+ }
370
+ return note;
371
+ }));
372
+ // Generate signature over the message (hash of public signals)
373
+ const message = poseidon(pubSignals);
374
+ const signature = signTransactMessage(request.zkAccount.spendingKeyPair.privateKey, message);
375
+ // Construct the complete raw input for the prover
376
+ const rawInput = {
377
+ merkleRoot: root,
378
+ boundParamsHash: boundParamsHash.toString(),
379
+ nullifiers: contexts.map((c) => c.nullifier.hex),
380
+ commitmentsOut: assignedOutputs.map((o) => o.hex),
381
+ token: request.token,
382
+ publicKey: request.zkAccount.spendingKeyPair.pubkey.map((pk) => pk.toString()),
383
+ signature: [
384
+ signature.R8[0].toString(),
385
+ signature.R8[1].toString(),
386
+ signature.S.toString(),
387
+ ],
388
+ randomIn: inputNotes.map((note) => note.random),
389
+ valueIn: inputNotes.map((note) => note.value),
390
+ pathElements: contexts.map((c) => c.witness.siblings.map((s) => s.toString())),
391
+ leavesIndices: contexts.map((c) => c.index),
392
+ nullifyingKey: formatUint256(request.zkAccount.nullifyingKey),
393
+ npkOut: request.outputs.map((o) => formatUint256(poseidon([o.mpk, o.random]))),
394
+ valueOut: request.outputs.map((o) => o.amount.toString()),
395
+ };
396
+ const proofInput = {
397
+ merkleRoot: BigInt(rawInput.merkleRoot),
398
+ boundParamsHash: BigInt(rawInput.boundParamsHash),
399
+ nullifiers: rawInput.nullifiers.map((n) => BigInt(n)),
400
+ commitmentsOut: rawInput.commitmentsOut.map((c) => BigInt(c)),
401
+ token: BigInt(rawInput.token),
402
+ publicKey: rawInput.publicKey.map((pk) => BigInt(pk)),
403
+ signature: rawInput.signature.map((s) => BigInt(s)),
404
+ randomIn: rawInput.randomIn.map((r) => BigInt(r)),
405
+ valueIn: rawInput.valueIn.map((v) => BigInt(v)),
406
+ pathElements: rawInput.pathElements.map((pe) => pe.map((e) => BigInt(e))),
407
+ leavesIndices: rawInput.leavesIndices.map((i) => BigInt(i)),
408
+ nullifyingKey: BigInt(rawInput.nullifyingKey),
409
+ npkOut: rawInput.npkOut.map((npk) => BigInt(npk)),
410
+ valueOut: rawInput.valueOut.map((v) => BigInt(v)),
411
+ };
412
+ const txProof = await proveTransaction(proofInput).catch((e) => {
413
+ throw new CoreError(`Proof generation failed: ${e.message}`);
414
+ });
415
+ ensureWithdrawalInput("request.withdrawal", request.withdrawal);
416
+ const proof = {
417
+ pA: [BigInt(txProof.proof.pi_a[0]), BigInt(txProof.proof.pi_a[1])],
418
+ pB: [
419
+ [
420
+ BigInt(txProof.proof.pi_b[0][1]),
421
+ BigInt(txProof.proof.pi_b[0][0]),
422
+ ],
423
+ [
424
+ BigInt(txProof.proof.pi_b[1][1]),
425
+ BigInt(txProof.proof.pi_b[1][0]),
426
+ ],
427
+ ],
428
+ pC: [BigInt(txProof.proof.pi_c[0]), BigInt(txProof.proof.pi_c[1])],
429
+ pubSignals,
430
+ };
431
+ const calldata = transactInterface.encodeFunctionData("transact", [
432
+ [
433
+ {
434
+ proof: { ...proof, pubSignals: undefined },
435
+ merkleRoot: parseHexToBigInt(root),
436
+ nullifierHashes: contexts.map((context) => context.nullifier.value),
437
+ newCommitments: assignedOutputs.map((output) => output.value),
438
+ context: {
439
+ chainID: BigInt(request.chainId),
440
+ poolAddress: request.poolAddress,
441
+ },
442
+ withdrawal: request.withdrawal,
443
+ ciphertexts: request.outputs.map((note) => ({
444
+ data: encryptNote(note).data,
445
+ })),
446
+ },
447
+ ],
448
+ ]);
449
+ const relayId = allocateRelayId();
450
+ const job = {
451
+ relayId,
452
+ kind: "transact",
453
+ chainId: request.chainId,
454
+ status: "pending",
455
+ broadcasterRelayId: null,
456
+ txHash: null,
457
+ createdAt: Date.now(),
458
+ timeoutMs: DEFAULT_JOB_TIMEOUT_MS,
459
+ poolAddress: request.poolAddress,
460
+ calldata,
461
+ contexts: contexts.map((context) => ({
462
+ index: context.index,
463
+ nullifier: context.nullifier.hex,
464
+ witness: serializeWitness(context.witness, context.index),
465
+ root,
466
+ })),
467
+ predictedOutputs: assignedOutputs.map((output) => ({
468
+ hex: output.hex,
469
+ index: output.index,
470
+ })),
471
+ expectedRoot: root,
472
+ };
473
+ // Submit to broadcaster immediately so reconciliation never has to send the relay.
474
+ const pendingForBroadcast = {
475
+ chainId: job.chainId,
476
+ root,
477
+ contexts,
478
+ assignedOutputs,
479
+ poolAddress: job.poolAddress,
480
+ calldata: job.calldata,
481
+ broadcasterRelayId: job.broadcasterRelayId,
482
+ txHash: job.txHash,
483
+ };
484
+ await submitAndAwaitBroadcasterRelay(relayId, pendingForBroadcast);
485
+ await stateStore.putPendingJob({
486
+ ...job,
487
+ status: "broadcasting",
488
+ broadcasterRelayId: pendingForBroadcast.broadcasterRelayId,
489
+ txHash: pendingForBroadcast.txHash,
490
+ });
491
+ return {
492
+ relayId,
493
+ calldata,
494
+ proof,
495
+ witnesses: contexts.map((context) => context.witness),
496
+ nullifiers: contexts.map((context) => context.nullifier.hex),
497
+ predictedCommitments: assignedOutputs.map((output) => output.hex),
498
+ };
499
+ },
500
+ async syncPendingTransact(relayId) {
501
+ const job = await stateStore.getPendingJob(relayId);
502
+ if (!job || job.kind !== "transact") {
503
+ throw new Error(`unknown transact relay ${relayId}`);
504
+ }
505
+ const contexts = job.contexts.map((context) => ({
506
+ index: context.index,
507
+ nullifier: {
508
+ value: parseHexToBigInt(context.nullifier),
509
+ hex: context.nullifier,
510
+ },
511
+ witness: deserializeWitness(context.witness),
512
+ }));
513
+ const assignedOutputs = job.predictedOutputs.map((output) => ({
514
+ hex: output.hex,
515
+ value: parseHexToBigInt(output.hex),
516
+ index: output.index,
517
+ }));
518
+ const pending = {
519
+ chainId: job.chainId,
520
+ root: job.expectedRoot ??
521
+ job.contexts[0]?.root ??
522
+ trees.getRoot(job.chainId),
523
+ contexts,
524
+ assignedOutputs,
525
+ poolAddress: job.poolAddress,
526
+ calldata: job.calldata,
527
+ broadcasterRelayId: job.broadcasterRelayId ?? null,
528
+ txHash: job.txHash ?? null,
529
+ };
530
+ await rebuildTreeFromStore({
531
+ chainId: job.chainId,
532
+ trees,
533
+ loadLeaf: stateStore.getLeaf.bind(stateStore),
534
+ });
535
+ await stateStore.putRoot({
536
+ chainId: pending.chainId,
537
+ root: pending.root,
538
+ });
539
+ const indexedRecords = await waitForIndexedCommitments(pending.chainId, pending.assignedOutputs);
540
+ const rootedCommitments = await applyIndexerUpdates(pending.chainId, indexedRecords);
541
+ const timestamp = Date.now();
542
+ await persistNullifiersAndSpend(pending.chainId, pending.contexts, timestamp);
543
+ const lastCommitment = rootedCommitments[rootedCommitments.length - 1] ?? null;
544
+ const latestRoot = lastCommitment
545
+ ? lastCommitment.root
546
+ : trees.getRoot(pending.chainId);
547
+ await persistTransactSuccess(job, pending, {
548
+ rootedCommitments,
549
+ latestRoot,
550
+ });
551
+ return {
552
+ chainId: pending.chainId,
553
+ root: latestRoot,
554
+ nullifiers: pending.contexts.map((context) => context.nullifier.hex),
555
+ newCommitments: rootedCommitments.map((output) => output.hex),
556
+ txHash: pending.txHash ?? undefined,
557
+ indexedCommitments: indexedRecords,
558
+ };
559
+ },
560
+ };
561
+ }
@@ -0,0 +1,114 @@
1
+ import { IMTMerkleProof } from "@zk-kit/imt";
2
+ import type { ZkAccount } from "../account/zkAccount.js";
3
+ import type { CommitmentRecord } from "../clients/indexer.js";
4
+ import type { JobStatus, LeafRecord, NoteRecord, NullifierRecord, PendingJobKind, PendingJobRecord, RootRecord } from "../state/index.js";
5
+ /**
6
+ * Base state store interface with common methods shared across services.
7
+ */
8
+ export type BaseStateStore = {
9
+ putLeaf(record: LeafRecord): Promise<void>;
10
+ getLeaf(chainId: number, index: number): Promise<LeafRecord | null>;
11
+ clearLeaves(chainId: number): Promise<void>;
12
+ putRoot(record: RootRecord): Promise<void>;
13
+ putPendingJob(job: PendingJobRecord): Promise<void>;
14
+ getPendingJob(relayId: string): Promise<PendingJobRecord | null>;
15
+ listPendingJobs(filter?: {
16
+ kind?: PendingJobKind;
17
+ statuses?: JobStatus[];
18
+ }): Promise<PendingJobRecord[]>;
19
+ deletePendingJob(relayId: string): Promise<void>;
20
+ };
21
+ export type DepositStateStore = BaseStateStore;
22
+ export type TransactStateStore = BaseStateStore & {
23
+ getNote(chainId: number, index: number): Promise<NoteRecord | null>;
24
+ markNoteSpent(chainId: number, index: number, spentAt?: number): Promise<NoteRecord>;
25
+ putNullifier(record: NullifierRecord): Promise<void>;
26
+ };
27
+ export type DepositNoteInput = {
28
+ mpk: bigint;
29
+ random: bigint;
30
+ token: string;
31
+ amount: bigint;
32
+ };
33
+ export type DepositRequest = {
34
+ zkAccount: ZkAccount;
35
+ chainId: number;
36
+ poolAddress: string;
37
+ depositor: string;
38
+ note: DepositNoteInput;
39
+ };
40
+ export type DepositRelayResult = {
41
+ relayId: string;
42
+ calldata: string;
43
+ commitment: string;
44
+ };
45
+ export type DepositSyncResult = {
46
+ chainId: number;
47
+ index: number;
48
+ commitment: string;
49
+ root: string;
50
+ };
51
+ /**
52
+ * Minimal reference to an input note; index resolves the canonical note state.
53
+ */
54
+ export type SpendNoteReference = {
55
+ index: number;
56
+ };
57
+ export type WithdrawalNoteInput = {
58
+ npk: bigint;
59
+ amount: bigint;
60
+ token: string;
61
+ };
62
+ /**
63
+ * Private output note to append back into the pool after spending.
64
+ */
65
+ export type OutputNoteInput = {
66
+ mpk: bigint;
67
+ random: bigint;
68
+ token: string;
69
+ amount: bigint;
70
+ };
71
+ /**
72
+ * Placeholder proof structure matching the pool interface; pubSignals remain meaningful.
73
+ */
74
+ export type Proof = {
75
+ pA: [bigint, bigint];
76
+ pB: [[bigint, bigint], [bigint, bigint]];
77
+ pC: [bigint, bigint];
78
+ pubSignals: bigint[];
79
+ };
80
+ /**
81
+ * Parameters required to a mock transact call against the local state.
82
+ */
83
+ export type TransactRequest = {
84
+ zkAccount: ZkAccount;
85
+ chainId: number;
86
+ poolAddress: string;
87
+ token: string;
88
+ inputs: SpendNoteReference[];
89
+ withdrawal: WithdrawalNoteInput;
90
+ outputs: OutputNoteInput[];
91
+ };
92
+ /**
93
+ * Result of the mock transact flow, including calldata and state deltas.
94
+ */
95
+ export type TransactRelayResult = {
96
+ relayId: string;
97
+ calldata: string;
98
+ proof: Proof;
99
+ witnesses: IMTMerkleProof[];
100
+ nullifiers: string[];
101
+ predictedCommitments: string[];
102
+ };
103
+ export type TransactSyncResult = {
104
+ chainId: number;
105
+ root: string;
106
+ nullifiers: string[];
107
+ newCommitments: string[];
108
+ txHash?: string;
109
+ indexedCommitments?: CommitmentRecord[];
110
+ };
111
+ export type Ciphertext = {
112
+ data: [bigint, bigint, bigint];
113
+ };
114
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../transactions/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};