@unlink-xyz/core 0.1.3-canary.fd5dddf → 0.1.4

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 (213) hide show
  1. package/README.md +9 -0
  2. package/dist/account/account.d.ts +31 -2
  3. package/dist/account/account.d.ts.map +1 -1
  4. package/dist/account/accounts.d.ts +42 -0
  5. package/dist/account/accounts.d.ts.map +1 -0
  6. package/dist/account/seed.d.ts +45 -0
  7. package/dist/account/seed.d.ts.map +1 -0
  8. package/dist/account/serialization.d.ts +6 -0
  9. package/dist/account/serialization.d.ts.map +1 -0
  10. package/dist/browser/index.js +34424 -86406
  11. package/dist/browser/index.js.map +1 -1
  12. package/dist/browser/wallet/index.js +55942 -0
  13. package/dist/browser/wallet/index.js.map +1 -0
  14. package/dist/clients/broadcaster.d.ts +1 -0
  15. package/dist/clients/broadcaster.d.ts.map +1 -1
  16. package/dist/clients/indexer.d.ts +5 -0
  17. package/dist/clients/indexer.d.ts.map +1 -1
  18. package/dist/config.d.ts +6 -4
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/core.d.ts.map +1 -1
  21. package/dist/crypto/adapters/index.d.ts +17 -0
  22. package/dist/crypto/adapters/index.d.ts.map +1 -0
  23. package/dist/crypto/adapters/polyfills.d.ts +5 -0
  24. package/dist/crypto/adapters/polyfills.d.ts.map +1 -0
  25. package/dist/crypto/encrypt.d.ts +33 -0
  26. package/dist/crypto/encrypt.d.ts.map +1 -0
  27. package/dist/crypto/secure-memory.d.ts.map +1 -0
  28. package/dist/errors.d.ts +8 -0
  29. package/dist/errors.d.ts.map +1 -1
  30. package/dist/index.d.ts +6 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +6721 -23
  33. package/dist/index.js.map +1 -0
  34. package/dist/keys/derive.d.ts +2 -2
  35. package/dist/keys/derive.d.ts.map +1 -1
  36. package/dist/keys/hex.d.ts +1 -4
  37. package/dist/keys/hex.d.ts.map +1 -1
  38. package/dist/keys/mnemonic.d.ts +0 -2
  39. package/dist/keys/mnemonic.d.ts.map +1 -1
  40. package/dist/keys.d.ts +1 -0
  41. package/dist/keys.d.ts.map +1 -1
  42. package/dist/prover/config.d.ts +54 -9
  43. package/dist/prover/config.d.ts.map +1 -1
  44. package/dist/prover/integrity.d.ts +20 -0
  45. package/dist/prover/integrity.d.ts.map +1 -0
  46. package/dist/prover/prover.d.ts +16 -31
  47. package/dist/prover/prover.d.ts.map +1 -1
  48. package/dist/state/merkle/hydrator.d.ts +21 -19
  49. package/dist/state/merkle/hydrator.d.ts.map +1 -1
  50. package/dist/state/merkle/index.d.ts +1 -1
  51. package/dist/state/merkle/index.d.ts.map +1 -1
  52. package/dist/state/store/ciphertext-store.d.ts +7 -0
  53. package/dist/state/store/ciphertext-store.d.ts.map +1 -1
  54. package/dist/state/store/index.d.ts +1 -1
  55. package/dist/state/store/index.d.ts.map +1 -1
  56. package/dist/state/store/job-store.d.ts.map +1 -1
  57. package/dist/state/store/jobs.d.ts +14 -16
  58. package/dist/state/store/jobs.d.ts.map +1 -1
  59. package/dist/state/store/leaf-store.d.ts +4 -0
  60. package/dist/state/store/leaf-store.d.ts.map +1 -1
  61. package/dist/state/store/nullifier-store.d.ts.map +1 -1
  62. package/dist/state/store/records.d.ts +8 -0
  63. package/dist/state/store/records.d.ts.map +1 -1
  64. package/dist/state/store/store.d.ts +18 -0
  65. package/dist/state/store/store.d.ts.map +1 -1
  66. package/dist/storage/indexeddb.d.ts.map +1 -1
  67. package/dist/storage/memory.d.ts.map +1 -1
  68. package/dist/transactions/adapter.d.ts +31 -0
  69. package/dist/transactions/adapter.d.ts.map +1 -0
  70. package/dist/transactions/deposit.d.ts +1 -1
  71. package/dist/transactions/deposit.d.ts.map +1 -1
  72. package/dist/transactions/index.d.ts +4 -2
  73. package/dist/transactions/index.d.ts.map +1 -1
  74. package/dist/transactions/note-sync.d.ts +3 -3
  75. package/dist/transactions/note-sync.d.ts.map +1 -1
  76. package/dist/transactions/reconcile.d.ts +1 -1
  77. package/dist/transactions/reconcile.d.ts.map +1 -1
  78. package/dist/transactions/transact.d.ts +21 -2
  79. package/dist/transactions/transact.d.ts.map +1 -1
  80. package/dist/transactions/transaction-planner.d.ts +1 -1
  81. package/dist/transactions/transaction-planner.d.ts.map +1 -1
  82. package/dist/transactions/transfer-planner.d.ts +2 -1
  83. package/dist/transactions/transfer-planner.d.ts.map +1 -1
  84. package/dist/transactions/types/deposit.d.ts +3 -3
  85. package/dist/transactions/types/deposit.d.ts.map +1 -1
  86. package/dist/transactions/types/domain.d.ts +3 -0
  87. package/dist/transactions/types/domain.d.ts.map +1 -1
  88. package/dist/transactions/types/options.d.ts +14 -5
  89. package/dist/transactions/types/options.d.ts.map +1 -1
  90. package/dist/transactions/types/planning.d.ts +2 -0
  91. package/dist/transactions/types/planning.d.ts.map +1 -1
  92. package/dist/transactions/types/state-stores.d.ts +53 -5
  93. package/dist/transactions/types/state-stores.d.ts.map +1 -1
  94. package/dist/transactions/types/transact.d.ts +10 -3
  95. package/dist/transactions/types/transact.d.ts.map +1 -1
  96. package/dist/transactions/withdrawal-planner.d.ts +1 -1
  97. package/dist/transactions/withdrawal-planner.d.ts.map +1 -1
  98. package/dist/tsconfig.tsbuildinfo +1 -1
  99. package/dist/tsup.config.d.ts +8 -0
  100. package/dist/tsup.config.d.ts.map +1 -0
  101. package/dist/types.d.ts +1 -0
  102. package/dist/types.d.ts.map +1 -1
  103. package/dist/utils/amounts.d.ts +0 -13
  104. package/dist/utils/amounts.d.ts.map +1 -1
  105. package/dist/utils/async.js +37 -34
  106. package/dist/utils/async.js.map +1 -0
  107. package/dist/utils/bigint.d.ts +0 -2
  108. package/dist/utils/bigint.d.ts.map +1 -1
  109. package/dist/utils/random.d.ts +5 -0
  110. package/dist/utils/random.d.ts.map +1 -1
  111. package/dist/utils/validators.d.ts.map +1 -1
  112. package/dist/vitest.config.d.ts.map +1 -1
  113. package/dist/wallet/adapter.d.ts +21 -0
  114. package/dist/wallet/adapter.d.ts.map +1 -0
  115. package/dist/wallet/burner/service.d.ts +32 -0
  116. package/dist/wallet/burner/service.d.ts.map +1 -0
  117. package/dist/wallet/burner/types.d.ts +47 -0
  118. package/dist/wallet/burner/types.d.ts.map +1 -0
  119. package/dist/wallet/index.d.ts +20 -0
  120. package/dist/wallet/index.d.ts.map +1 -0
  121. package/dist/wallet/index.js +6462 -0
  122. package/dist/wallet/index.js.map +1 -0
  123. package/dist/wallet/sdk.d.ts +48 -0
  124. package/dist/wallet/sdk.d.ts.map +1 -0
  125. package/dist/wallet/types.d.ts +457 -0
  126. package/dist/wallet/types.d.ts.map +1 -0
  127. package/dist/wallet/unlink-wallet.d.ts +187 -0
  128. package/dist/wallet/unlink-wallet.d.ts.map +1 -0
  129. package/package.json +16 -6
  130. package/dist/account/account.js +0 -142
  131. package/dist/circuits.json +0 -74
  132. package/dist/clients/broadcaster.js +0 -30
  133. package/dist/clients/http.js +0 -72
  134. package/dist/clients/indexer.js +0 -94
  135. package/dist/config.js +0 -36
  136. package/dist/constants.js +0 -5
  137. package/dist/core.js +0 -15
  138. package/dist/crypto-adapters/auto-init.d.ts +0 -2
  139. package/dist/crypto-adapters/auto-init.d.ts.map +0 -1
  140. package/dist/crypto-adapters/auto-init.js +0 -7
  141. package/dist/crypto-adapters/index.d.ts +0 -22
  142. package/dist/crypto-adapters/index.d.ts.map +0 -1
  143. package/dist/crypto-adapters/index.js +0 -47
  144. package/dist/crypto-adapters/polyfills.d.ts +0 -5
  145. package/dist/crypto-adapters/polyfills.d.ts.map +0 -1
  146. package/dist/crypto-adapters/polyfills.js +0 -8
  147. package/dist/errors.js +0 -36
  148. package/dist/history/index.js +0 -2
  149. package/dist/history/service.js +0 -354
  150. package/dist/history/types.js +0 -1
  151. package/dist/keys/address.js +0 -55
  152. package/dist/keys/derive.js +0 -112
  153. package/dist/keys/hex.js +0 -66
  154. package/dist/keys/index.js +0 -4
  155. package/dist/keys/mnemonic.js +0 -23
  156. package/dist/keys.js +0 -45
  157. package/dist/prover/config.js +0 -70
  158. package/dist/prover/index.js +0 -1
  159. package/dist/prover/prover.js +0 -291
  160. package/dist/prover/registry.js +0 -18
  161. package/dist/schema.js +0 -14
  162. package/dist/state/index.js +0 -2
  163. package/dist/state/merkle/hydrator.js +0 -37
  164. package/dist/state/merkle/index.js +0 -2
  165. package/dist/state/merkle/merkle-tree.js +0 -113
  166. package/dist/state/store/ciphertext-store.js +0 -37
  167. package/dist/state/store/history-store.js +0 -53
  168. package/dist/state/store/index.js +0 -9
  169. package/dist/state/store/job-store.js +0 -144
  170. package/dist/state/store/jobs.js +0 -1
  171. package/dist/state/store/leaf-store.js +0 -32
  172. package/dist/state/store/note-store.js +0 -146
  173. package/dist/state/store/nullifier-store.js +0 -60
  174. package/dist/state/store/records.js +0 -1
  175. package/dist/state/store/root-store.js +0 -26
  176. package/dist/state/store/store.js +0 -113
  177. package/dist/storage/index.js +0 -2
  178. package/dist/storage/indexeddb.js +0 -205
  179. package/dist/storage/memory.js +0 -91
  180. package/dist/transactions/deposit.js +0 -220
  181. package/dist/transactions/index.js +0 -9
  182. package/dist/transactions/note-selection.js +0 -201
  183. package/dist/transactions/note-sync.js +0 -485
  184. package/dist/transactions/reconcile.js +0 -85
  185. package/dist/transactions/transact.js +0 -450
  186. package/dist/transactions/transaction-planner.js +0 -116
  187. package/dist/transactions/transfer-planner.js +0 -85
  188. package/dist/transactions/types/deposit.js +0 -1
  189. package/dist/transactions/types/domain.js +0 -4
  190. package/dist/transactions/types/index.js +0 -17
  191. package/dist/transactions/types/options.js +0 -1
  192. package/dist/transactions/types/planning.js +0 -1
  193. package/dist/transactions/types/state-stores.js +0 -1
  194. package/dist/transactions/types/transact.js +0 -1
  195. package/dist/transactions/withdrawal-planner.js +0 -128
  196. package/dist/tsup.browser.config.js +0 -34
  197. package/dist/types.js +0 -1
  198. package/dist/utils/amounts.js +0 -89
  199. package/dist/utils/bigint.js +0 -29
  200. package/dist/utils/crypto.d.ts +0 -18
  201. package/dist/utils/crypto.d.ts.map +0 -1
  202. package/dist/utils/crypto.js +0 -45
  203. package/dist/utils/format.js +0 -33
  204. package/dist/utils/json-codec.js +0 -25
  205. package/dist/utils/notes.js +0 -14
  206. package/dist/utils/polling.js +0 -11
  207. package/dist/utils/random.js +0 -27
  208. package/dist/utils/secure-memory.d.ts.map +0 -1
  209. package/dist/utils/secure-memory.js +0 -28
  210. package/dist/utils/signature.js +0 -14
  211. package/dist/utils/validators.js +0 -96
  212. package/dist/vitest.config.js +0 -13
  213. /package/dist/{utils → crypto}/secure-memory.d.ts +0 -0
@@ -1,85 +0,0 @@
1
- import { CoreError } from "../errors.js";
2
- const ACTIVE_STATUSES = ["pending", "submitted", "broadcasting"];
3
- export function createJobReconciler(deps) {
4
- const { stateStore, depositClient, transactService } = deps;
5
- async function markChecked(job, err) {
6
- const latest = (await stateStore.getJob(job.relayId)) ?? job;
7
- const message = err instanceof Error
8
- ? err.message
9
- : err !== undefined
10
- ? String(err)
11
- : null;
12
- await stateStore.putJob({
13
- ...latest,
14
- lastCheckedAt: Date.now(),
15
- ...(message ? { error: message } : {}),
16
- });
17
- }
18
- async function markDead(job, reason) {
19
- const latest = (await stateStore.getJob(job.relayId)) ?? job;
20
- if (latest.status === "succeeded")
21
- return;
22
- await stateStore.putJob({
23
- ...latest,
24
- status: "dead",
25
- lastCheckedAt: Date.now(),
26
- error: reason,
27
- });
28
- }
29
- function isTimedOut(job) {
30
- return Date.now() - job.createdAt > job.timeoutMs;
31
- }
32
- async function reconcileJob(job) {
33
- const latest = (await stateStore.getJob(job.relayId)) ?? job;
34
- if (!ACTIVE_STATUSES.includes(latest.status)) {
35
- return null;
36
- }
37
- if (latest.status !== "succeeded" && isTimedOut(latest)) {
38
- await markDead(latest, "job timed out");
39
- return new Error("job timed out");
40
- }
41
- if (latest.kind === "deposit") {
42
- return depositClient.syncPendingDeposit(latest.relayId);
43
- }
44
- return transactService.syncPendingTransact(latest.relayId);
45
- }
46
- async function reconcileRelay(relayId) {
47
- const job = await stateStore.getJob(relayId);
48
- if (!job) {
49
- throw new CoreError(`unknown relay ${relayId}`);
50
- }
51
- try {
52
- const res = await reconcileJob(job);
53
- await markChecked(job);
54
- return res;
55
- }
56
- catch (err) {
57
- await markChecked(job, err);
58
- throw err;
59
- }
60
- }
61
- async function reconcileAll(filter = {}) {
62
- const jobs = await stateStore.listJobs({
63
- kind: filter.kind,
64
- statuses: filter.statuses ?? ACTIVE_STATUSES,
65
- });
66
- const results = [];
67
- for (const job of jobs) {
68
- try {
69
- const res = await reconcileJob(job);
70
- await markChecked(job);
71
- results.push(res);
72
- }
73
- catch (err) {
74
- await markChecked(job, err);
75
- // Allow caller to decide how to handle errors; we keep going.
76
- results.push(err);
77
- }
78
- }
79
- return results;
80
- }
81
- return {
82
- reconcileRelay,
83
- reconcileAll,
84
- };
85
- }
@@ -1,450 +0,0 @@
1
- import { AbiCoder, Interface, keccak256 } from "ethers";
2
- import { createBroadcasterClient } from "../clients/broadcaster.js";
3
- import { resolveFetch } from "../clients/http.js";
4
- import { createIndexerClient } from "../clients/indexer.js";
5
- import { createServiceConfig } from "../config.js";
6
- import { poseidon } from "../crypto-adapters/index.js";
7
- import { CoreError, ProofError, ValidationError } from "../errors.js";
8
- import { proveTransaction } from "../prover/index.js";
9
- import { DEFAULT_JOB_TIMEOUT_MS, rebuildTreeFromStore, resolveMerkleTrees, } from "../state/index.js";
10
- import { isNotFoundError, sleep, withTimeout } from "../utils/async.js";
11
- import { formatUint256, parseHexToBigInt } from "../utils/bigint.js";
12
- import { deriveCommitment, encryptNote } from "../utils/crypto.js";
13
- import { DEFAULT_POLL_INTERVAL_MS, DEFAULT_POLL_TIMEOUT_MS, MAX_POLL_INTERVAL_MS, } from "../utils/polling.js";
14
- import { signTransactMessage } from "../utils/signature.js";
15
- import { SNARK_SCALAR_FIELD } from "../utils/validators.js";
16
- 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)";
17
- /** Default timeout for proof generation in milliseconds (60 seconds) */
18
- export const DEFAULT_PROOF_TIMEOUT_MS = 60_000;
19
- const transactInterface = new Interface([TRANSACT_ABI]);
20
- const DEFAULT_WITHDRAWAL = {
21
- npk: 0n,
22
- amount: 0n,
23
- token: "0x0000000000000000000000000000000000000000",
24
- };
25
- export function computeBoundParamsHash(chainId, poolAddress) {
26
- const coder = AbiCoder.defaultAbiCoder();
27
- const encoded = coder.encode(["uint64", "uint160"], [BigInt(chainId), BigInt(poolAddress)]);
28
- return parseHexToBigInt(keccak256(encoded)) % SNARK_SCALAR_FIELD;
29
- }
30
- export function serializeWitness(proof, index) {
31
- return {
32
- root: formatUint256(BigInt(proof.root)),
33
- leaf: formatUint256(BigInt(proof.leaf)),
34
- pathElements: proof.siblings.map((level) => level.map((node) => formatUint256(BigInt(node)))),
35
- pathIndices: proof.pathIndices ?? [],
36
- leafIndex: index,
37
- };
38
- }
39
- export function deserializeWitness(s) {
40
- return {
41
- root: parseHexToBigInt(s.root),
42
- leaf: parseHexToBigInt(s.leaf),
43
- siblings: s.pathElements.map((level) => level.map(parseHexToBigInt)),
44
- pathIndices: s.pathIndices,
45
- leafIndex: s.leafIndex,
46
- };
47
- }
48
- /**
49
- * Build proof for a single transaction item.
50
- * This is the core proof generation logic extracted for parallelization.
51
- */
52
- async function buildSingleTransactionProof(store, account, chainId, poolAddress, tx, trees, baseIndex, opts) {
53
- if (!tx.inputs?.length)
54
- throw new ValidationError("at least one input note is required");
55
- // Build input contexts (witnesses + nullifiers)
56
- const contexts = [];
57
- let merkleRoot;
58
- for (const input of tx.inputs) {
59
- const note = await store.getNote(chainId, input.index);
60
- if (!note)
61
- throw new ValidationError(`note ${input.index} not found`);
62
- if (note.spentAt !== undefined)
63
- throw new ValidationError(`note ${input.index} already spent`);
64
- const tree = trees.getOrCreate(chainId);
65
- if (input.index >= tree.getLeafCount())
66
- throw new ValidationError(`note ${input.index} out of range`);
67
- const witness = tree.createMerkleProof(input.index);
68
- const root = formatUint256(BigInt(witness.root));
69
- if (merkleRoot && merkleRoot !== root)
70
- throw new ValidationError("inputs must share same merkle root");
71
- merkleRoot = root;
72
- const nullifier = poseidon([account.nullifyingKey, BigInt(input.index)]);
73
- contexts.push({
74
- index: input.index,
75
- nullifier,
76
- nullifierHex: formatUint256(nullifier),
77
- witness,
78
- });
79
- }
80
- // Compute output commitments for regular notes
81
- const outputs = tx.outputs.map((o, i) => {
82
- const commitment = deriveCommitment({
83
- npk: poseidon([o.mpk, o.random]),
84
- amount: o.amount,
85
- token: o.token,
86
- });
87
- return {
88
- value: commitment,
89
- hex: formatUint256(commitment),
90
- index: baseIndex + i,
91
- };
92
- });
93
- // Handle withdrawal
94
- const withdrawal = tx.withdrawal ?? DEFAULT_WITHDRAWAL;
95
- const hasWithdrawal = withdrawal.amount > 0n;
96
- if (hasWithdrawal && withdrawal.token !== tx.token) {
97
- throw new ValidationError("Withdrawal token must match transaction token");
98
- }
99
- const withdrawalCommitment = hasWithdrawal
100
- ? deriveCommitment({
101
- npk: withdrawal.npk,
102
- amount: withdrawal.amount,
103
- token: withdrawal.token,
104
- })
105
- : null;
106
- // Build arrays for circuit
107
- const allCommitmentsOut = hasWithdrawal
108
- ? [...outputs.map((o) => o.value), withdrawalCommitment]
109
- : outputs.map((o) => o.value);
110
- const allNpkOut = hasWithdrawal
111
- ? [...tx.outputs.map((o) => poseidon([o.mpk, o.random])), withdrawal.npk]
112
- : tx.outputs.map((o) => poseidon([o.mpk, o.random]));
113
- const allValueOut = hasWithdrawal
114
- ? [...tx.outputs.map((o) => o.amount), withdrawal.amount]
115
- : tx.outputs.map((o) => o.amount);
116
- // Build prover input
117
- const boundParams = computeBoundParamsHash(chainId, poolAddress);
118
- const pubSignals = [
119
- parseHexToBigInt(merkleRoot),
120
- boundParams,
121
- ...contexts.map((c) => c.nullifier),
122
- ...allCommitmentsOut,
123
- ];
124
- const inputNotes = await Promise.all(tx.inputs.map((i) => store.getNote(chainId, i.index).then((n) => n)));
125
- const sig = signTransactMessage(account.spendingKeyPair.privateKey, poseidon(pubSignals));
126
- const proofInput = {
127
- merkleRoot: parseHexToBigInt(merkleRoot),
128
- boundParamsHash: boundParams,
129
- nullifiers: contexts.map((c) => c.nullifier),
130
- commitmentsOut: allCommitmentsOut,
131
- token: BigInt(tx.token),
132
- publicKey: account.spendingKeyPair.pubkey,
133
- signature: [sig.R8[0], sig.R8[1], sig.S],
134
- randomIn: inputNotes.map((n) => BigInt(n.random)),
135
- valueIn: inputNotes.map((n) => BigInt(n.value)),
136
- pathElements: contexts.map((c) => c.witness.siblings.map((level) => level.map((s) => s))),
137
- leavesIndices: contexts.map((c) => BigInt(c.index)),
138
- nullifyingKey: account.nullifyingKey,
139
- npkOut: allNpkOut,
140
- valueOut: allValueOut,
141
- };
142
- const proofTimeoutMs = opts.proofTimeoutMs ?? DEFAULT_PROOF_TIMEOUT_MS;
143
- const txProof = await withTimeout(proveTransaction(proofInput, { rpcUrl: opts.rpcUrl }), proofTimeoutMs, new ProofError(`Proof generation timed out after ${proofTimeoutMs}ms`)).catch((e) => {
144
- if (e instanceof ProofError)
145
- throw e;
146
- throw new ProofError(`Proof generation failed: ${e.message}`);
147
- });
148
- const proof = {
149
- pA: [BigInt(txProof.proof.pi_a[0]), BigInt(txProof.proof.pi_a[1])],
150
- pB: [
151
- [BigInt(txProof.proof.pi_b[0][1]), BigInt(txProof.proof.pi_b[0][0])],
152
- [BigInt(txProof.proof.pi_b[1][1]), BigInt(txProof.proof.pi_b[1][0])],
153
- ],
154
- pC: [BigInt(txProof.proof.pi_c[0]), BigInt(txProof.proof.pi_c[1])],
155
- pubSignals,
156
- };
157
- return {
158
- proof,
159
- witnesses: contexts.map((c) => c.witness),
160
- nullifiers: contexts.map((c) => c.nullifierHex),
161
- predictedCommitments: outputs.map((o) => o.hex),
162
- merkleRoot: merkleRoot,
163
- token: tx.token,
164
- withdrawal,
165
- ciphertexts: tx.outputs.map((note) => ({
166
- data: encryptNote(note).data,
167
- })),
168
- contexts,
169
- outputs,
170
- inputNotes,
171
- };
172
- }
173
- // ============================================================================
174
- // Public API
175
- // ============================================================================
176
- /**
177
- * Execute private transaction(s): build ZK proofs (in parallel), encrypt outputs, and broadcast.
178
- * Accepts 1 or more transactions - single tx just passes [{...}].
179
- */
180
- export async function transact(store, req, opts) {
181
- if (!req.transactions?.length)
182
- throw new ValidationError("at least one transaction is required");
183
- const serviceConfig = createServiceConfig(opts.rpcUrl);
184
- const trees = resolveMerkleTrees(opts);
185
- const fetchFn = resolveFetch(opts.fetch);
186
- const broadcaster = fetchFn
187
- ? createBroadcasterClient(serviceConfig.broadcasterBaseUrl, {
188
- fetch: fetchFn,
189
- })
190
- : null;
191
- const pollInterval = Math.min(opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
192
- const pollTimeout = opts.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
193
- await rebuildTreeFromStore({
194
- chainId: req.chainId,
195
- trees,
196
- loadLeaf: store.getLeaf.bind(store),
197
- });
198
- // Calculate base indices for each transaction's outputs
199
- let baseIndex = trees.getLeafCount(req.chainId);
200
- const baseIndices = [];
201
- for (const tx of req.transactions) {
202
- baseIndices.push(baseIndex);
203
- baseIndex += tx.outputs.length;
204
- }
205
- // Generate proofs in PARALLEL for all transactions
206
- const proofPromises = req.transactions.map((tx, i) => buildSingleTransactionProof(store, req.account, req.chainId, req.poolAddress, tx, trees, baseIndices[i], opts));
207
- const results = await Promise.all(proofPromises);
208
- // Build combined calldata
209
- const calldata = transactInterface.encodeFunctionData("transact", [
210
- results.map((r) => ({
211
- proof: { pA: r.proof.pA, pB: r.proof.pB, pC: r.proof.pC },
212
- merkleRoot: parseHexToBigInt(r.merkleRoot),
213
- nullifierHashes: r.contexts.map((c) => c.nullifier),
214
- newCommitments: r.proof.pubSignals.slice(2 + r.contexts.length, // Skip root + boundParams + nullifiers
215
- r.withdrawal.amount > 0n
216
- ? r.proof.pubSignals.length - 1 // Exclude withdrawal commitment (not inserted into tree)
217
- : undefined),
218
- context: { chainId: BigInt(req.chainId), poolAddress: req.poolAddress },
219
- withdrawal: r.withdrawal,
220
- ciphertexts: r.ciphertexts,
221
- })),
222
- ]);
223
- const relayId = globalThis.crypto?.randomUUID?.() ?? `tx-${Date.now().toString(16)}`;
224
- // Determine if this is a withdrawal (any transaction has withdrawal)
225
- const hasAnyWithdrawal = results.some((r) => r.withdrawal.amount > 0n);
226
- const historyKind = hasAnyWithdrawal ? "Withdraw" : "Send";
227
- // Compute net deltas per token for history preview
228
- const deltasByToken = new Map();
229
- for (let i = 0; i < req.transactions.length; i++) {
230
- const tx = req.transactions[i];
231
- const r = results[i];
232
- const totalOutputsToRecipients = tx.outputs
233
- .filter((o) => o.owner === "recipient")
234
- .reduce((sum, o) => sum + o.amount, 0n);
235
- // For withdrawals: user loses the withdrawal amount
236
- // For sends: user loses what they send to recipients (change returns to them)
237
- const netDelta = r.withdrawal.amount > 0n
238
- ? -r.withdrawal.amount
239
- : -totalOutputsToRecipients;
240
- const existing = deltasByToken.get(tx.token) ?? 0n;
241
- deltasByToken.set(tx.token, existing + netDelta);
242
- }
243
- const jobBase = {
244
- relayId,
245
- chainId: req.chainId,
246
- status: "pending",
247
- txHash: null,
248
- createdAt: Date.now(),
249
- timeoutMs: DEFAULT_JOB_TIMEOUT_MS,
250
- poolAddress: req.poolAddress,
251
- calldata,
252
- transactions: results.map((r, i) => {
253
- const tx = req.transactions[i];
254
- return {
255
- token: tx.token,
256
- nullifiers: r.nullifiers,
257
- predictedCommitments: r.outputs.map((o) => ({ hex: o.hex })),
258
- withdrawal: r.withdrawal.amount > 0n
259
- ? {
260
- amount: r.withdrawal.amount.toString(),
261
- recipient: r.withdrawal.npk.toString(),
262
- }
263
- : undefined,
264
- };
265
- }),
266
- historyPreview: {
267
- kind: historyKind,
268
- amounts: [...deltasByToken.entries()].map(([token, delta]) => ({
269
- token,
270
- delta: delta.toString(),
271
- })),
272
- },
273
- };
274
- const job = hasAnyWithdrawal
275
- ? { ...jobBase, kind: "withdraw" }
276
- : { ...jobBase, kind: "transfer" };
277
- // Submit to broadcaster
278
- let txHash = null;
279
- if (broadcaster) {
280
- const submission = await broadcaster.submitRelay({
281
- clientTxId: relayId,
282
- chainId: req.chainId,
283
- payload: { kind: "call_data", to: req.poolAddress, data: calldata },
284
- });
285
- if (!submission.accepted) {
286
- await store.putJob({
287
- ...job,
288
- status: "failed",
289
- lastCheckedAt: Date.now(),
290
- error: submission.message ?? "broadcaster rejected",
291
- });
292
- throw new CoreError(submission.message ?? "broadcaster rejected");
293
- }
294
- const deadline = Date.now() + pollTimeout;
295
- while (Date.now() <= deadline) {
296
- const status = await broadcaster.getRelayStatus(relayId);
297
- if (status.state === "succeeded") {
298
- txHash = status.txHash ?? null;
299
- break;
300
- }
301
- if (status.state === "failed" || status.state === "dead") {
302
- await store.putJob({
303
- ...job,
304
- status: "failed",
305
- lastCheckedAt: Date.now(),
306
- error: status.error ?? "broadcaster relay failed",
307
- });
308
- throw new CoreError(status.error ?? "broadcaster relay failed");
309
- }
310
- await sleep(pollInterval);
311
- }
312
- if (!txHash && Date.now() > deadline) {
313
- await store.putJob({
314
- ...job,
315
- status: "failed",
316
- lastCheckedAt: Date.now(),
317
- error: "broadcaster relay timed out",
318
- });
319
- throw new CoreError("broadcaster relay timed out");
320
- }
321
- }
322
- await store.putJob({
323
- ...job,
324
- status: "broadcasting",
325
- txHash,
326
- });
327
- // Build result with individual transaction results
328
- const transactionResults = results.map((r) => ({
329
- proof: r.proof,
330
- witnesses: r.witnesses,
331
- nullifiers: r.nullifiers,
332
- predictedCommitments: r.predictedCommitments,
333
- }));
334
- return {
335
- relayId,
336
- calldata,
337
- transactions: transactionResults,
338
- };
339
- }
340
- /**
341
- * Wait for a pending transaction to be indexed and update local state.
342
- */
343
- export async function syncTransact(store, relayId, opts) {
344
- const record = await store.getJob(relayId);
345
- if (!record || (record.kind !== "transfer" && record.kind !== "withdraw"))
346
- throw new CoreError(`unknown pool transaction relay ${relayId}`);
347
- const job = record;
348
- const serviceConfig = createServiceConfig(opts.rpcUrl);
349
- const trees = resolveMerkleTrees(opts);
350
- const fetchFn = resolveFetch(opts.fetch);
351
- const indexer = fetchFn
352
- ? createIndexerClient(serviceConfig.indexerBaseUrl, { fetch: fetchFn })
353
- : null;
354
- const pollInterval = Math.min(opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
355
- const pollTimeout = opts.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
356
- await rebuildTreeFromStore({
357
- chainId: job.chainId,
358
- trees,
359
- loadLeaf: store.getLeaf.bind(store),
360
- });
361
- // Collect all predicted commitments from all transactions
362
- const allPredictedOutputs = job.transactions.flatMap((tx) => tx.predictedCommitments.map((c) => c.hex));
363
- // Wait for indexed commitments
364
- const indexed = [];
365
- if (indexer && allPredictedOutputs.length > 0) {
366
- const pending = new Set(allPredictedOutputs.map((h) => h.toLowerCase()));
367
- const deadline = Date.now() + pollTimeout;
368
- let delay = pollInterval;
369
- while (pending.size > 0 && Date.now() <= deadline) {
370
- for (const hex of [...pending]) {
371
- const rec = await indexer
372
- .getCommitment({ chainId: job.chainId, commitment: hex })
373
- .catch((e) => {
374
- if (!isNotFoundError(e))
375
- throw e;
376
- return null;
377
- });
378
- if (rec) {
379
- indexed.push(rec);
380
- pending.delete(hex);
381
- }
382
- }
383
- if (pending.size > 0) {
384
- await sleep(delay);
385
- delay = Math.min(delay * 2, MAX_POLL_INTERVAL_MS);
386
- }
387
- }
388
- if (pending.size > 0) {
389
- await store.putJob({
390
- ...job,
391
- status: "failed",
392
- lastCheckedAt: Date.now(),
393
- error: "commitments not indexed before timeout",
394
- });
395
- throw new CoreError("commitments not indexed before timeout");
396
- }
397
- }
398
- // Get latest root from indexed records (syncChain handles actual storage)
399
- const sortedIndexed = indexed.sort((a, b) => a.index - b.index);
400
- const latestRoot = sortedIndexed.at(-1)?.root ?? trees.getRoot(job.chainId);
401
- // Collect all nullifiers from all transactions and mark notes as spent
402
- const allNullifiers = job.transactions.flatMap((tx) => tx.nullifiers);
403
- const timestamp = Date.now();
404
- // Look up note indices by matching nullifiers to notes in the store
405
- const allNotes = await store.listNotes({
406
- chainId: job.chainId,
407
- includeSpent: true,
408
- });
409
- const nullifierToNoteIndex = new Map();
410
- for (const note of allNotes) {
411
- if (note.nullifier) {
412
- nullifierToNoteIndex.set(note.nullifier, note.index);
413
- }
414
- }
415
- // Resolve every nullifier to its note index, fail fast if any is unknown
416
- const resolved = [];
417
- for (const nullifier of allNullifiers) {
418
- const noteIndex = nullifierToNoteIndex.get(nullifier);
419
- if (noteIndex === undefined) {
420
- const short = nullifier.slice(0, 10) + "…";
421
- const error = `Unknown nullifier ${short} - note not found in local state. This may indicate a sync issue.`;
422
- await store.putJob({
423
- ...job,
424
- status: "failed",
425
- lastCheckedAt: Date.now(),
426
- error,
427
- });
428
- throw new CoreError(error);
429
- }
430
- resolved.push({ nullifier, noteIndex });
431
- }
432
- const txHash = job.txHash ?? undefined;
433
- await Promise.all(resolved.flatMap(({ nullifier, noteIndex }) => [
434
- store.putNullifier({ chainId: job.chainId, nullifier, noteIndex }),
435
- store.markNoteSpent(job.chainId, noteIndex, timestamp, txHash),
436
- ]));
437
- await store.putJob({
438
- ...job,
439
- status: "succeeded",
440
- lastCheckedAt: timestamp,
441
- });
442
- return {
443
- chainId: job.chainId,
444
- root: latestRoot,
445
- nullifiers: allNullifiers,
446
- newCommitments: indexed.map((r) => r.commitment),
447
- txHash: job.txHash ?? undefined,
448
- indexedCommitments: indexed,
449
- };
450
- }
@@ -1,116 +0,0 @@
1
- import { poseidon } from "../crypto-adapters/index.js";
2
- import { ProofError, ValidationError } from "../errors.js";
3
- import { CIRCUIT_REGISTRY } from "../prover/registry.js";
4
- import { randomBigint } from "../utils/random.js";
5
- import { selectNotesForAmount } from "./note-selection.js";
6
- /**
7
- * Select the smallest circuit that fits the given input/output counts.
8
- */
9
- function selectCircuitForTransaction(inputCount, outputCount) {
10
- // Sort circuits by total size (inputs + outputs) ascending
11
- const circuits = Object.values(CIRCUIT_REGISTRY).sort((a, b) => a.inputs + a.outputs - (b.inputs + b.outputs));
12
- const circuit = circuits.find((c) => c.inputs >= inputCount && c.outputs >= outputCount);
13
- if (!circuit) {
14
- throw new ProofError(`No circuit supports ${inputCount} inputs and ${outputCount} outputs. ` +
15
- `Available: ${circuits.map((c) => `${c.inputs}x${c.outputs}`).join(", ")}`);
16
- }
17
- return circuit;
18
- }
19
- /**
20
- * Generate a planned output note with derived npk and commitment.
21
- */
22
- function generateOutput(mpk, amount, token, owner) {
23
- const random = randomBigint();
24
- const npk = poseidon([mpk, random]);
25
- const tokenScalar = BigInt(token);
26
- const commitment = poseidon([npk, tokenScalar, amount]);
27
- return {
28
- mpk,
29
- random,
30
- token,
31
- amount,
32
- owner,
33
- npk,
34
- commitment,
35
- };
36
- }
37
- /**
38
- * Plan a transaction: select notes, choose circuit, generate outputs.
39
- *
40
- * This function handles:
41
- * 1. Note selection using consolidation-first algorithm
42
- * 2. Circuit selection (smallest that fits)
43
- * 3. Output generation with cryptographic derivations
44
- *
45
- * @param notes - Available unspent notes (pre-filtered by token/chain by caller)
46
- * @param recipients - List of recipients with their amounts
47
- * @param senderMpk - Sender's master public key (for change output)
48
- * @param token - Token address being transferred
49
- * @returns Complete transaction plan ready for building TransactRequest
50
- * @throws ValidationError if insufficient balance, no valid circuit, or other constraints fail
51
- *
52
- * @example
53
- * ```typescript
54
- * const plan = planTransaction(
55
- * spendableNotes,
56
- * [{ mpk: recipientMpk, amount: 100n }],
57
- * senderMpk,
58
- * "0x...",
59
- * );
60
- * // plan.inputs - selected notes to spend
61
- * // plan.outputs - generated outputs (recipient + change)
62
- * // plan.circuit - circuit to use for proof
63
- * ```
64
- */
65
- export function planTransaction(notes, recipients, senderMpk, token) {
66
- // Validate inputs
67
- if (!recipients.length) {
68
- throw new ValidationError("At least one recipient is required");
69
- }
70
- // Validate each recipient has a positive amount
71
- for (let i = 0; i < recipients.length; i++) {
72
- const recipient = recipients[i];
73
- if (recipient && recipient.amount <= 0n) {
74
- throw new ValidationError(`Recipient at index ${i} has invalid amount: ${recipient.amount}. All recipients must have amount > 0`);
75
- }
76
- }
77
- const totalSent = recipients.reduce((sum, r) => sum + r.amount, 0n);
78
- // Select notes using consolidation-first algorithm
79
- // Note: maxInputs/maxOutputs defaults are derived from CIRCUIT_REGISTRY in selectNotesForAmount
80
- const selection = selectNotesForAmount(notes, totalSent, {
81
- recipientCount: recipients.length,
82
- });
83
- const { selected: inputs, totalInput, change } = selection;
84
- // Determine output count: 1 per recipient + 1 for change (if any)
85
- const outputCount = recipients.length + (change > 0n ? 1 : 0);
86
- // Select smallest fitting circuit
87
- const circuit = selectCircuitForTransaction(inputs.length, outputCount);
88
- // Generate outputs
89
- const outputs = [];
90
- // Recipient outputs
91
- for (const recipient of recipients) {
92
- outputs.push(generateOutput(recipient.mpk, recipient.amount, token, "recipient"));
93
- }
94
- // Change output (if any)
95
- if (change > 0n) {
96
- outputs.push(generateOutput(senderMpk, change, token, "self"));
97
- }
98
- // Create outputNotes for TransactParams (without derived fields, but with owner metadata)
99
- const outputNotes = outputs.map((o) => ({
100
- mpk: o.mpk,
101
- random: o.random,
102
- token: o.token,
103
- amount: o.amount,
104
- owner: o.owner,
105
- }));
106
- return {
107
- inputs,
108
- outputs,
109
- outputNotes,
110
- circuit,
111
- totalInput,
112
- totalSent,
113
- change,
114
- token,
115
- };
116
- }