@unlink-xyz/core 0.1.2 → 0.1.3-canary.0877bfe

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 (350) hide show
  1. package/dist/account/{zkAccount.d.ts → account.d.ts} +7 -5
  2. package/dist/account/account.d.ts.map +1 -0
  3. package/dist/account/{zkAccount.js → account.js} +57 -43
  4. package/dist/browser/index.js +108202 -0
  5. package/dist/browser/index.js.map +1 -0
  6. package/dist/circuits.json +74 -0
  7. package/dist/clients/broadcaster.d.ts +7 -2
  8. package/dist/clients/broadcaster.d.ts.map +1 -1
  9. package/dist/clients/broadcaster.js +9 -1
  10. package/dist/clients/http.d.ts +6 -0
  11. package/dist/clients/http.d.ts.map +1 -1
  12. package/dist/clients/http.js +24 -9
  13. package/dist/clients/indexer.d.ts +11 -0
  14. package/dist/clients/indexer.d.ts.map +1 -1
  15. package/dist/clients/indexer.js +40 -11
  16. package/dist/config.d.ts +28 -9
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +33 -26
  19. package/dist/constants.d.ts +6 -0
  20. package/dist/constants.d.ts.map +1 -0
  21. package/dist/constants.js +5 -0
  22. package/dist/core.d.ts.map +1 -1
  23. package/dist/core.js +5 -2
  24. package/dist/crypto-adapters/auto-init.d.ts +2 -0
  25. package/dist/crypto-adapters/auto-init.d.ts.map +1 -0
  26. package/dist/crypto-adapters/auto-init.js +7 -0
  27. package/dist/crypto-adapters/index.d.ts +22 -0
  28. package/dist/crypto-adapters/index.d.ts.map +1 -0
  29. package/dist/crypto-adapters/index.js +47 -0
  30. package/dist/crypto-adapters/polyfills.d.ts +5 -0
  31. package/dist/crypto-adapters/polyfills.d.ts.map +1 -0
  32. package/dist/crypto-adapters/polyfills.js +8 -0
  33. package/dist/errors.d.ts +9 -0
  34. package/dist/errors.d.ts.map +1 -1
  35. package/dist/errors.js +18 -0
  36. package/dist/history/index.d.ts +3 -0
  37. package/dist/history/index.d.ts.map +1 -0
  38. package/dist/history/index.js +2 -0
  39. package/dist/history/service.d.ts +46 -0
  40. package/dist/history/service.d.ts.map +1 -0
  41. package/dist/history/service.js +354 -0
  42. package/dist/history/types.d.ts +21 -0
  43. package/dist/history/types.d.ts.map +1 -0
  44. package/dist/index.d.ts +12 -5
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +11 -4
  47. package/dist/keys/address.d.ts +13 -0
  48. package/dist/keys/address.d.ts.map +1 -0
  49. package/dist/keys/address.js +55 -0
  50. package/dist/keys/derive.d.ts +37 -0
  51. package/dist/keys/derive.d.ts.map +1 -0
  52. package/dist/keys/derive.js +112 -0
  53. package/dist/keys/hex.d.ts +17 -0
  54. package/dist/keys/hex.d.ts.map +1 -0
  55. package/dist/keys/hex.js +66 -0
  56. package/dist/keys/index.d.ts +5 -0
  57. package/dist/keys/index.d.ts.map +1 -0
  58. package/dist/keys/index.js +4 -0
  59. package/dist/keys/mnemonic.d.ts +8 -0
  60. package/dist/keys/mnemonic.d.ts.map +1 -0
  61. package/dist/keys/mnemonic.js +23 -0
  62. package/dist/keys.d.ts +4 -1
  63. package/dist/keys.d.ts.map +1 -1
  64. package/dist/keys.js +4 -0
  65. package/dist/prover/config.d.ts +1 -15
  66. package/dist/prover/config.d.ts.map +1 -1
  67. package/dist/prover/config.js +1 -11
  68. package/dist/prover/prover.d.ts +15 -4
  69. package/dist/prover/prover.d.ts.map +1 -1
  70. package/dist/prover/prover.js +115 -98
  71. package/dist/prover/registry.d.ts +3 -30
  72. package/dist/prover/registry.d.ts.map +1 -1
  73. package/dist/prover/registry.js +12 -51
  74. package/dist/state/merkle/hydrator.d.ts.map +1 -1
  75. package/dist/state/merkle/hydrator.js +3 -2
  76. package/dist/state/merkle/index.d.ts +1 -1
  77. package/dist/state/merkle/index.d.ts.map +1 -1
  78. package/dist/state/merkle/index.js +1 -1
  79. package/dist/state/merkle/merkle-tree.d.ts +8 -0
  80. package/dist/state/merkle/merkle-tree.d.ts.map +1 -1
  81. package/dist/state/merkle/merkle-tree.js +16 -7
  82. package/dist/state/store/ciphertext-store.d.ts +4 -0
  83. package/dist/state/store/ciphertext-store.d.ts.map +1 -1
  84. package/dist/state/store/ciphertext-store.js +12 -0
  85. package/dist/state/store/history-store.d.ts +24 -0
  86. package/dist/state/store/history-store.d.ts.map +1 -0
  87. package/dist/state/store/history-store.js +53 -0
  88. package/dist/state/store/index.d.ts +3 -2
  89. package/dist/state/store/index.d.ts.map +1 -1
  90. package/dist/state/store/index.js +1 -0
  91. package/dist/state/store/job-store.d.ts +7 -7
  92. package/dist/state/store/job-store.d.ts.map +1 -1
  93. package/dist/state/store/job-store.js +65 -39
  94. package/dist/state/store/jobs.d.ts +65 -18
  95. package/dist/state/store/jobs.d.ts.map +1 -1
  96. package/dist/state/store/leaf-store.d.ts.map +1 -1
  97. package/dist/state/store/leaf-store.js +0 -3
  98. package/dist/state/store/note-store.d.ts +7 -7
  99. package/dist/state/store/note-store.d.ts.map +1 -1
  100. package/dist/state/store/note-store.js +38 -34
  101. package/dist/state/store/nullifier-store.d.ts +9 -0
  102. package/dist/state/store/nullifier-store.d.ts.map +1 -1
  103. package/dist/state/store/nullifier-store.js +32 -2
  104. package/dist/state/store/records.d.ts +31 -2
  105. package/dist/state/store/records.d.ts.map +1 -1
  106. package/dist/state/store/root-store.d.ts.map +1 -1
  107. package/dist/state/store/root-store.js +0 -4
  108. package/dist/state/store/store.d.ts +61 -27
  109. package/dist/state/store/store.d.ts.map +1 -1
  110. package/dist/state/store/store.js +92 -1
  111. package/dist/storage/indexeddb.js +1 -1
  112. package/dist/storage/memory.d.ts.map +1 -1
  113. package/dist/storage/memory.js +5 -1
  114. package/dist/transactions/deposit.d.ts +12 -15
  115. package/dist/transactions/deposit.d.ts.map +1 -1
  116. package/dist/transactions/deposit.js +203 -152
  117. package/dist/transactions/index.d.ts +7 -4
  118. package/dist/transactions/index.d.ts.map +1 -1
  119. package/dist/transactions/index.js +7 -2
  120. package/dist/transactions/note-selection.d.ts +17 -0
  121. package/dist/transactions/note-selection.d.ts.map +1 -0
  122. package/dist/transactions/note-selection.js +201 -0
  123. package/dist/transactions/note-sync.d.ts +5 -33
  124. package/dist/transactions/note-sync.d.ts.map +1 -1
  125. package/dist/transactions/note-sync.js +320 -155
  126. package/dist/transactions/reconcile.d.ts +10 -12
  127. package/dist/transactions/reconcile.d.ts.map +1 -1
  128. package/dist/transactions/reconcile.js +53 -7
  129. package/dist/transactions/transact.d.ts +13 -24
  130. package/dist/transactions/transact.d.ts.map +1 -1
  131. package/dist/transactions/transact.js +393 -507
  132. package/dist/transactions/transaction-planner.d.ts +34 -0
  133. package/dist/transactions/transaction-planner.d.ts.map +1 -0
  134. package/dist/transactions/transaction-planner.js +116 -0
  135. package/dist/transactions/transfer-planner.d.ts +36 -0
  136. package/dist/transactions/transfer-planner.d.ts.map +1 -0
  137. package/dist/transactions/transfer-planner.js +85 -0
  138. package/dist/transactions/types/deposit.d.ts +67 -0
  139. package/dist/transactions/types/deposit.d.ts.map +1 -0
  140. package/dist/transactions/types/domain.d.ts +67 -0
  141. package/dist/transactions/types/domain.d.ts.map +1 -0
  142. package/dist/transactions/types/domain.js +4 -0
  143. package/dist/transactions/types/index.d.ts +18 -0
  144. package/dist/transactions/types/index.d.ts.map +1 -0
  145. package/dist/transactions/types/index.js +17 -0
  146. package/dist/transactions/types/options.d.ts +45 -0
  147. package/dist/transactions/types/options.d.ts.map +1 -0
  148. package/dist/transactions/types/options.js +1 -0
  149. package/dist/transactions/types/planning.d.ts +80 -0
  150. package/dist/transactions/types/planning.d.ts.map +1 -0
  151. package/dist/transactions/types/planning.js +1 -0
  152. package/dist/transactions/types/state-stores.d.ts +103 -0
  153. package/dist/transactions/types/state-stores.d.ts.map +1 -0
  154. package/dist/transactions/types/state-stores.js +1 -0
  155. package/dist/transactions/types/transact.d.ts +76 -0
  156. package/dist/transactions/types/transact.d.ts.map +1 -0
  157. package/dist/transactions/types/transact.js +1 -0
  158. package/dist/transactions/withdrawal-planner.d.ts +58 -0
  159. package/dist/transactions/withdrawal-planner.d.ts.map +1 -0
  160. package/dist/transactions/withdrawal-planner.js +128 -0
  161. package/dist/tsconfig.tsbuildinfo +1 -1
  162. package/dist/tsup.browser.config.d.ts +7 -0
  163. package/dist/tsup.browser.config.d.ts.map +1 -0
  164. package/dist/tsup.browser.config.js +34 -0
  165. package/dist/utils/amounts.d.ts +39 -0
  166. package/dist/utils/amounts.d.ts.map +1 -0
  167. package/dist/utils/amounts.js +89 -0
  168. package/dist/utils/async.d.ts +9 -0
  169. package/dist/utils/async.d.ts.map +1 -1
  170. package/dist/utils/async.js +24 -0
  171. package/dist/utils/bigint.js +7 -7
  172. package/dist/utils/crypto.d.ts +11 -5
  173. package/dist/utils/crypto.d.ts.map +1 -1
  174. package/dist/utils/crypto.js +12 -6
  175. package/dist/utils/format.d.ts +25 -0
  176. package/dist/utils/format.d.ts.map +1 -0
  177. package/dist/utils/format.js +33 -0
  178. package/dist/utils/json-codec.js +1 -1
  179. package/dist/utils/notes.d.ts +15 -0
  180. package/dist/utils/notes.d.ts.map +1 -0
  181. package/dist/utils/notes.js +14 -0
  182. package/dist/utils/polling.d.ts +5 -0
  183. package/dist/utils/polling.d.ts.map +1 -1
  184. package/dist/utils/polling.js +5 -0
  185. package/dist/utils/random.d.ts +13 -0
  186. package/dist/utils/random.d.ts.map +1 -0
  187. package/dist/utils/random.js +27 -0
  188. package/dist/utils/secure-memory.d.ts +25 -0
  189. package/dist/utils/secure-memory.d.ts.map +1 -0
  190. package/dist/utils/secure-memory.js +28 -0
  191. package/dist/utils/signature.d.ts +6 -0
  192. package/dist/utils/signature.d.ts.map +1 -1
  193. package/dist/utils/signature.js +8 -6
  194. package/dist/utils/validators.d.ts +21 -10
  195. package/dist/utils/validators.d.ts.map +1 -1
  196. package/dist/utils/validators.js +37 -11
  197. package/dist/vitest.config.d.ts +3 -0
  198. package/dist/vitest.config.d.ts.map +1 -0
  199. package/dist/vitest.config.js +13 -0
  200. package/package.json +28 -11
  201. package/.eslintrc.json +0 -4
  202. package/account/zkAccount.test.ts +0 -316
  203. package/account/zkAccount.ts +0 -222
  204. package/clients/broadcaster.ts +0 -67
  205. package/clients/http.ts +0 -94
  206. package/clients/indexer.ts +0 -150
  207. package/config.ts +0 -39
  208. package/core.ts +0 -17
  209. package/dist/account/railgun-imports-prototype.d.ts +0 -12
  210. package/dist/account/railgun-imports-prototype.d.ts.map +0 -1
  211. package/dist/account/railgun-imports-prototype.js +0 -30
  212. package/dist/account/zkAccount.d.ts.map +0 -1
  213. package/dist/key-derivation/babyjubjub.d.ts +0 -9
  214. package/dist/key-derivation/babyjubjub.d.ts.map +0 -1
  215. package/dist/key-derivation/babyjubjub.js +0 -9
  216. package/dist/key-derivation/bech32.d.ts +0 -22
  217. package/dist/key-derivation/bech32.d.ts.map +0 -1
  218. package/dist/key-derivation/bech32.js +0 -86
  219. package/dist/key-derivation/bip32.d.ts +0 -17
  220. package/dist/key-derivation/bip32.d.ts.map +0 -1
  221. package/dist/key-derivation/bip32.js +0 -41
  222. package/dist/key-derivation/bip39.d.ts +0 -22
  223. package/dist/key-derivation/bip39.d.ts.map +0 -1
  224. package/dist/key-derivation/bip39.js +0 -56
  225. package/dist/key-derivation/bytes.d.ts +0 -19
  226. package/dist/key-derivation/bytes.d.ts.map +0 -1
  227. package/dist/key-derivation/bytes.js +0 -92
  228. package/dist/key-derivation/hash.d.ts +0 -3
  229. package/dist/key-derivation/hash.d.ts.map +0 -1
  230. package/dist/key-derivation/hash.js +0 -10
  231. package/dist/key-derivation/index.d.ts +0 -8
  232. package/dist/key-derivation/index.d.ts.map +0 -1
  233. package/dist/key-derivation/index.js +0 -7
  234. package/dist/key-derivation/wallet-node.d.ts +0 -45
  235. package/dist/key-derivation/wallet-node.d.ts.map +0 -1
  236. package/dist/key-derivation/wallet-node.js +0 -109
  237. package/dist/state/ciphertext-store.d.ts +0 -12
  238. package/dist/state/ciphertext-store.d.ts.map +0 -1
  239. package/dist/state/ciphertext-store.js +0 -25
  240. package/dist/state/hydrator.d.ts +0 -16
  241. package/dist/state/hydrator.d.ts.map +0 -1
  242. package/dist/state/hydrator.js +0 -18
  243. package/dist/state/job-store.d.ts +0 -12
  244. package/dist/state/job-store.d.ts.map +0 -1
  245. package/dist/state/job-store.js +0 -118
  246. package/dist/state/jobs.d.ts +0 -50
  247. package/dist/state/jobs.d.ts.map +0 -1
  248. package/dist/state/jobs.js +0 -1
  249. package/dist/state/leaf-store.d.ts +0 -17
  250. package/dist/state/leaf-store.d.ts.map +0 -1
  251. package/dist/state/leaf-store.js +0 -35
  252. package/dist/state/merkle-tree.d.ts +0 -34
  253. package/dist/state/merkle-tree.d.ts.map +0 -1
  254. package/dist/state/merkle-tree.js +0 -104
  255. package/dist/state/note-store.d.ts +0 -37
  256. package/dist/state/note-store.d.ts.map +0 -1
  257. package/dist/state/note-store.js +0 -133
  258. package/dist/state/nullifier-store.d.ts +0 -13
  259. package/dist/state/nullifier-store.d.ts.map +0 -1
  260. package/dist/state/nullifier-store.js +0 -21
  261. package/dist/state/records.d.ts +0 -57
  262. package/dist/state/records.d.ts.map +0 -1
  263. package/dist/state/root-store.d.ts +0 -13
  264. package/dist/state/root-store.d.ts.map +0 -1
  265. package/dist/state/root-store.js +0 -30
  266. package/dist/state/store.d.ts +0 -26
  267. package/dist/state/store.d.ts.map +0 -1
  268. package/dist/state/store.js +0 -19
  269. package/dist/state.d.ts +0 -83
  270. package/dist/state.d.ts.map +0 -1
  271. package/dist/state.js +0 -171
  272. package/dist/transactions/shield.d.ts +0 -5
  273. package/dist/transactions/shield.d.ts.map +0 -1
  274. package/dist/transactions/shield.js +0 -93
  275. package/dist/transactions/types.d.ts +0 -114
  276. package/dist/transactions/types.d.ts.map +0 -1
  277. package/dist/transactions/utils.d.ts +0 -10
  278. package/dist/transactions/utils.d.ts.map +0 -1
  279. package/dist/transactions/utils.js +0 -17
  280. package/dist/utils/time.d.ts +0 -2
  281. package/dist/utils/time.d.ts.map +0 -1
  282. package/dist/utils/time.js +0 -3
  283. package/dist/utils/witness.d.ts +0 -11
  284. package/dist/utils/witness.d.ts.map +0 -1
  285. package/dist/utils/witness.js +0 -19
  286. package/errors.ts +0 -20
  287. package/index.ts +0 -17
  288. package/key-derivation/babyjubjub.ts +0 -11
  289. package/key-derivation/bech32.test.ts +0 -90
  290. package/key-derivation/bech32.ts +0 -124
  291. package/key-derivation/bip32.ts +0 -56
  292. package/key-derivation/bip39.ts +0 -76
  293. package/key-derivation/bytes.ts +0 -118
  294. package/key-derivation/hash.ts +0 -13
  295. package/key-derivation/index.ts +0 -7
  296. package/key-derivation/wallet-node.ts +0 -155
  297. package/keys.ts +0 -47
  298. package/prover/config.ts +0 -104
  299. package/prover/index.ts +0 -1
  300. package/prover/prover.integration.test.ts +0 -162
  301. package/prover/prover.test.ts +0 -309
  302. package/prover/prover.ts +0 -405
  303. package/prover/registry.test.ts +0 -90
  304. package/prover/registry.ts +0 -82
  305. package/schema.ts +0 -17
  306. package/setup-artifacts.sh +0 -57
  307. package/state/index.ts +0 -2
  308. package/state/merkle/hydrator.ts +0 -69
  309. package/state/merkle/index.ts +0 -12
  310. package/state/merkle/merkle-tree.test.ts +0 -50
  311. package/state/merkle/merkle-tree.ts +0 -163
  312. package/state/store/ciphertext-store.ts +0 -28
  313. package/state/store/index.ts +0 -24
  314. package/state/store/job-store.ts +0 -162
  315. package/state/store/jobs.ts +0 -64
  316. package/state/store/leaf-store.ts +0 -39
  317. package/state/store/note-store.ts +0 -177
  318. package/state/store/nullifier-store.ts +0 -39
  319. package/state/store/records.ts +0 -61
  320. package/state/store/root-store.ts +0 -34
  321. package/state/store/store.ts +0 -25
  322. package/state.test.ts +0 -235
  323. package/storage/index.ts +0 -3
  324. package/storage/indexeddb.test.ts +0 -99
  325. package/storage/indexeddb.ts +0 -235
  326. package/storage/memory.test.ts +0 -59
  327. package/storage/memory.ts +0 -93
  328. package/transactions/deposit.test.ts +0 -160
  329. package/transactions/deposit.ts +0 -227
  330. package/transactions/index.ts +0 -20
  331. package/transactions/note-sync.test.ts +0 -155
  332. package/transactions/note-sync.ts +0 -452
  333. package/transactions/reconcile.ts +0 -73
  334. package/transactions/transact.test.ts +0 -451
  335. package/transactions/transact.ts +0 -811
  336. package/transactions/types.ts +0 -141
  337. package/tsconfig.json +0 -14
  338. package/types/global.d.ts +0 -15
  339. package/types.ts +0 -24
  340. package/utils/async.ts +0 -15
  341. package/utils/bigint.ts +0 -34
  342. package/utils/crypto.test.ts +0 -69
  343. package/utils/crypto.ts +0 -58
  344. package/utils/json-codec.ts +0 -38
  345. package/utils/polling.ts +0 -6
  346. package/utils/signature.ts +0 -16
  347. package/utils/validators.test.ts +0 -64
  348. package/utils/validators.ts +0 -86
  349. /package/dist/{transactions → history}/types.js +0 -0
  350. /package/dist/{state/records.js → transactions/types/deposit.js} +0 -0
@@ -1,59 +1,31 @@
1
- import { poseidon } from "@railgun-community/circomlibjs";
2
1
  import { AbiCoder, Interface, keccak256 } from "ethers";
3
2
  import { createBroadcasterClient } from "../clients/broadcaster.js";
3
+ import { resolveFetch } from "../clients/http.js";
4
4
  import { createIndexerClient } from "../clients/indexer.js";
5
- import { serviceConfig } from "../config.js";
6
- import { CoreError } from "../errors.js";
5
+ import { createServiceConfig } from "../config.js";
6
+ import { poseidon } from "../crypto-adapters/index.js";
7
+ import { CoreError, ProofError, ValidationError } from "../errors.js";
7
8
  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";
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";
11
12
  import { deriveCommitment, encryptNote } from "../utils/crypto.js";
12
13
  import { DEFAULT_POLL_INTERVAL_MS, DEFAULT_POLL_TIMEOUT_MS, MAX_POLL_INTERVAL_MS, } from "../utils/polling.js";
13
14
  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)";
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;
16
19
  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
- function computeBoundParamsHash(chainId, poolAddress) {
50
- const chainIdBigInt = BigInt(chainId);
51
- const poolAddressBigInt = BigInt(poolAddress);
52
- const hashInput = [chainIdBigInt, poolAddressBigInt];
20
+ const DEFAULT_WITHDRAWAL = {
21
+ npk: 0n,
22
+ amount: 0n,
23
+ token: "0x0000000000000000000000000000000000000000",
24
+ };
25
+ export function computeBoundParamsHash(chainId, poolAddress) {
53
26
  const coder = AbiCoder.defaultAbiCoder();
54
- const input = coder.encode(["uint64", "uint160"], hashInput);
55
- const result = keccak256(input);
56
- return parseHexToBigInt(result) % SNARK_SCALAR_FIELD;
27
+ const encoded = coder.encode(["uint64", "uint160"], [BigInt(chainId), BigInt(poolAddress)]);
28
+ return parseHexToBigInt(keccak256(encoded)) % SNARK_SCALAR_FIELD;
57
29
  }
58
30
  export function serializeWitness(proof, index) {
59
31
  return {
@@ -64,498 +36,412 @@ export function serializeWitness(proof, index) {
64
36
  leafIndex: index,
65
37
  };
66
38
  }
67
- export function deserializeWitness(serialized) {
39
+ export function deserializeWitness(s) {
68
40
  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,
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,
74
46
  };
75
47
  }
76
48
  /**
77
- * Simulates the transact pipeline: builds Merkle witnesses, derives nullifiers, and updates local state.
49
+ * Build proof for a single transaction item.
50
+ * This is the core proof generation logic extracted for parallelization.
78
51
  */
79
- export function createTransactService(stateStore, options = {}) {
80
- if (!stateStore) {
81
- throw new Error("stateStore dependency is required");
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
+ });
82
79
  }
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,
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,
88
104
  })
89
105
  : null;
90
- const indexerClient = fetchImpl
91
- ? createIndexerClient(serviceConfig.indexerBaseUrl, { fetch: fetchImpl })
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
+ })
92
190
  : 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`);
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 };
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;
162
204
  }
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
- ])));
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),
215
+ context: { chainId: BigInt(req.chainId), poolAddress: req.poolAddress },
216
+ withdrawal: r.withdrawal,
217
+ ciphertexts: r.ciphertexts,
218
+ })),
219
+ ]);
220
+ const relayId = globalThis.crypto?.randomUUID?.() ?? `tx-${Date.now().toString(16)}`;
221
+ // Determine if this is a withdrawal (any transaction has withdrawal)
222
+ const hasAnyWithdrawal = results.some((r) => r.withdrawal.amount > 0n);
223
+ const historyKind = hasAnyWithdrawal ? "Withdraw" : "Send";
224
+ // Compute net deltas per token for history preview
225
+ const deltasByToken = new Map();
226
+ for (let i = 0; i < req.transactions.length; i++) {
227
+ const tx = req.transactions[i];
228
+ const r = results[i];
229
+ const totalOutputsToRecipients = tx.outputs
230
+ .filter((o) => o.owner === "recipient")
231
+ .reduce((sum, o) => sum + o.amount, 0n);
232
+ // For withdrawals: user loses the withdrawal amount
233
+ // For sends: user loses what they send to recipients (change returns to them)
234
+ const netDelta = r.withdrawal.amount > 0n
235
+ ? -r.withdrawal.amount
236
+ : -totalOutputsToRecipients;
237
+ const existing = deltasByToken.get(tx.token) ?? 0n;
238
+ deltasByToken.set(tx.token, existing + netDelta);
173
239
  }
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
- });
240
+ const jobBase = {
241
+ relayId,
242
+ chainId: req.chainId,
243
+ status: "pending",
244
+ txHash: null,
245
+ createdAt: Date.now(),
246
+ timeoutMs: DEFAULT_JOB_TIMEOUT_MS,
247
+ poolAddress: req.poolAddress,
248
+ calldata,
249
+ transactions: results.map((r, i) => {
250
+ const tx = req.transactions[i];
186
251
  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;
252
+ token: tx.token,
253
+ nullifiers: r.nullifiers,
254
+ predictedCommitments: r.outputs.map((o) => ({ hex: o.hex })),
255
+ withdrawal: r.withdrawal.amount > 0n
256
+ ? {
257
+ amount: r.withdrawal.amount.toString(),
258
+ recipient: r.withdrawal.npk.toString(),
208
259
  }
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;
260
+ : undefined,
261
+ };
262
+ }),
263
+ historyPreview: {
264
+ kind: historyKind,
265
+ amounts: [...deltasByToken.entries()].map(([token, delta]) => ({
266
+ token,
267
+ delta: delta.toString(),
268
+ })),
269
+ },
270
+ };
271
+ const job = hasAnyWithdrawal
272
+ ? { ...jobBase, kind: "withdraw" }
273
+ : { ...jobBase, kind: "transfer" };
274
+ // Submit to broadcaster
275
+ let txHash = null;
276
+ if (broadcaster) {
277
+ const submission = await broadcaster.submitRelay({
278
+ clientTxId: relayId,
279
+ chainId: req.chainId,
280
+ payload: { kind: "call_data", to: req.poolAddress, data: calldata },
233
281
  });
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
- },
282
+ if (!submission.accepted) {
283
+ await store.putJob({
284
+ ...job,
285
+ status: "failed",
286
+ lastCheckedAt: Date.now(),
287
+ error: submission.message ?? "broadcaster rejected",
248
288
  });
249
- if (!submission.accepted) {
250
- throw new CoreError(submission.message ??
251
- "broadcaster rejected transaction relay submission");
252
- }
253
- pending.broadcasterRelayId = submission.id;
289
+ throw new CoreError(submission.message ?? "broadcaster rejected");
254
290
  }
255
- const deadline = Date.now() + pollTimeoutMs;
291
+ const deadline = Date.now() + pollTimeout;
256
292
  while (Date.now() <= deadline) {
257
- const status = await broadcasterClient.getRelayStatus(pending.broadcasterRelayId);
293
+ const status = await broadcaster.getRelayStatus(relayId);
258
294
  if (status.state === "succeeded") {
259
- pending.txHash = status.txHash ?? pending.txHash ?? null;
260
- return;
295
+ txHash = status.txHash ?? null;
296
+ break;
261
297
  }
262
298
  if (status.state === "failed" || status.state === "dead") {
299
+ await store.putJob({
300
+ ...job,
301
+ status: "failed",
302
+ lastCheckedAt: Date.now(),
303
+ error: status.error ?? "broadcaster relay failed",
304
+ });
263
305
  throw new CoreError(status.error ?? "broadcaster relay failed");
264
306
  }
265
- await sleep(pollIntervalMs);
307
+ await sleep(pollInterval);
308
+ }
309
+ if (!txHash && Date.now() > deadline) {
310
+ await store.putJob({
311
+ ...job,
312
+ status: "failed",
313
+ lastCheckedAt: Date.now(),
314
+ error: "broadcaster relay timed out",
315
+ });
316
+ throw new CoreError("broadcaster relay timed out");
266
317
  }
267
- throw new CoreError("broadcaster relay timed out");
268
318
  }
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,
319
+ await store.putJob({
320
+ ...job,
321
+ status: "broadcasting",
322
+ txHash,
323
+ });
324
+ // Build result with individual transaction results
325
+ const transactionResults = results.map((r) => ({
326
+ proof: r.proof,
327
+ witnesses: r.witnesses,
328
+ nullifiers: r.nullifiers,
329
+ predictedCommitments: r.predictedCommitments,
330
+ }));
331
+ return {
332
+ relayId,
333
+ calldata,
334
+ transactions: transactionResults,
335
+ };
336
+ }
337
+ /**
338
+ * Wait for a pending transaction to be indexed and update local state.
339
+ */
340
+ export async function syncTransact(store, relayId, opts) {
341
+ const record = await store.getJob(relayId);
342
+ if (!record || (record.kind !== "transfer" && record.kind !== "withdraw"))
343
+ throw new CoreError(`unknown pool transaction relay ${relayId}`);
344
+ const job = record;
345
+ const serviceConfig = createServiceConfig(opts.rpcUrl);
346
+ const trees = resolveMerkleTrees(opts);
347
+ const fetchFn = resolveFetch(opts.fetch);
348
+ const indexer = fetchFn
349
+ ? createIndexerClient(serviceConfig.indexerBaseUrl, { fetch: fetchFn })
350
+ : null;
351
+ const pollInterval = Math.min(opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
352
+ const pollTimeout = opts.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
353
+ await rebuildTreeFromStore({
354
+ chainId: job.chainId,
355
+ trees,
356
+ loadLeaf: store.getLeaf.bind(store),
357
+ });
358
+ // Collect all predicted commitments from all transactions
359
+ const allPredictedOutputs = job.transactions.flatMap((tx) => tx.predictedCommitments.map((c) => c.hex));
360
+ // Wait for indexed commitments
361
+ const indexed = [];
362
+ if (indexer && allPredictedOutputs.length > 0) {
363
+ const pending = new Set(allPredictedOutputs.map((h) => h.toLowerCase()));
364
+ const deadline = Date.now() + pollTimeout;
365
+ let delay = pollInterval;
366
+ while (pending.size > 0 && Date.now() <= deadline) {
367
+ for (const hex of [...pending]) {
368
+ const rec = await indexer
369
+ .getCommitment({ chainId: job.chainId, commitment: hex })
370
+ .catch((e) => {
371
+ if (!isNotFoundError(e))
372
+ throw e;
373
+ return null;
285
374
  });
286
- continue;
287
- }
288
- if (entry.index > currentCount) {
289
- throw new CoreError(`local merkle tree gap, expected next index ${currentCount}, got indexed ${entry.index}`);
375
+ if (rec) {
376
+ indexed.push(rec);
377
+ pending.delete(hex);
378
+ }
290
379
  }
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}`);
380
+ if (pending.size > 0) {
381
+ await sleep(delay);
382
+ delay = Math.min(delay * 2, MAX_POLL_INTERVAL_MS);
295
383
  }
296
- applied.push({
297
- value,
298
- hex: entry.commitment,
299
- index,
300
- root,
384
+ }
385
+ if (pending.size > 0) {
386
+ await store.putJob({
387
+ ...job,
388
+ status: "failed",
389
+ lastCheckedAt: Date.now(),
390
+ error: "commitments not indexed before timeout",
301
391
  });
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
- ]);
392
+ throw new CoreError("commitments not indexed before timeout");
313
393
  }
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
394
  }
338
- const allocateRelayId = () => {
339
- if (typeof globalThis.crypto?.randomUUID === "function") {
340
- return globalThis.crypto.randomUUID();
395
+ // Get latest root from indexed records (syncChain handles actual storage)
396
+ const sortedIndexed = indexed.sort((a, b) => a.index - b.index);
397
+ const latestRoot = sortedIndexed.at(-1)?.root ?? trees.getRoot(job.chainId);
398
+ // Collect all nullifiers from all transactions and mark notes as spent
399
+ const allNullifiers = job.transactions.flatMap((tx) => tx.nullifiers);
400
+ const timestamp = Date.now();
401
+ // Look up note indices by matching nullifiers to notes in the store
402
+ const allNotes = await store.listNotes({
403
+ chainId: job.chainId,
404
+ includeSpent: true,
405
+ });
406
+ const nullifierToNoteIndex = new Map();
407
+ for (const note of allNotes) {
408
+ if (note.nullifier) {
409
+ nullifierToNoteIndex.set(note.nullifier, note.index);
341
410
  }
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({
411
+ }
412
+ // Resolve every nullifier to its note index, fail fast if any is unknown
413
+ const resolved = [];
414
+ for (const nullifier of allNullifiers) {
415
+ const noteIndex = nullifierToNoteIndex.get(nullifier);
416
+ if (noteIndex === undefined) {
417
+ const short = nullifier.slice(0, 10) + "…";
418
+ const error = `Unknown nullifier ${short} - note not found in local state. This may indicate a sync issue.`;
419
+ await store.putJob({
486
420
  ...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,
421
+ status: "failed",
422
+ lastCheckedAt: Date.now(),
423
+ error,
550
424
  });
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
- },
425
+ throw new CoreError(error);
426
+ }
427
+ resolved.push({ nullifier, noteIndex });
428
+ }
429
+ const txHash = job.txHash ?? undefined;
430
+ await Promise.all(resolved.flatMap(({ nullifier, noteIndex }) => [
431
+ store.putNullifier({ chainId: job.chainId, nullifier, noteIndex }),
432
+ store.markNoteSpent(job.chainId, noteIndex, timestamp, txHash),
433
+ ]));
434
+ await store.putJob({
435
+ ...job,
436
+ status: "succeeded",
437
+ lastCheckedAt: timestamp,
438
+ });
439
+ return {
440
+ chainId: job.chainId,
441
+ root: latestRoot,
442
+ nullifiers: allNullifiers,
443
+ newCommitments: indexed.map((r) => r.commitment),
444
+ txHash: job.txHash ?? undefined,
445
+ indexedCommitments: indexed,
560
446
  };
561
447
  }