@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,169 +1,220 @@
1
1
  import { Interface } from "ethers";
2
- import { v4 as uuidv4 } from "uuid";
2
+ import { resolveFetch } from "../clients/http.js";
3
3
  import { createIndexerClient } from "../clients/indexer.js";
4
- import { serviceConfig } from "../config.js";
5
- import { ByteLength, ByteUtils } from "../key-derivation/bytes.js";
6
- import { createMerkleTrees, DEFAULT_JOB_TIMEOUT_MS, rebuildTreeFromStore, } from "../state/index.js";
7
- import { isNotFoundError, sleep } from "../utils/async.js";
8
- import { parseHexToBigInt } from "../utils/bigint.js";
4
+ import { createServiceConfig } from "../config.js";
5
+ import { ETH_TOKEN } from "../constants.js";
6
+ import { CoreError, InitializationError, ValidationError } from "../errors.js";
7
+ import { FieldSize, Hex } from "../keys/hex.js";
8
+ import { DEFAULT_JOB_TIMEOUT_MS, rebuildTreeFromStore, resolveMerkleTrees, } from "../state/index.js";
9
+ import { sleep } from "../utils/async.js";
9
10
  import { computeCommitment, deriveNpk, encryptNote } from "../utils/crypto.js";
10
- import { DEFAULT_POLL_INTERVAL_MS, DEFAULT_POLL_TIMEOUT_MS, MAX_POLL_INTERVAL_MS, } from "../utils/polling.js";
11
- import { ensureAddress, ensureChainId } from "../utils/validators.js";
11
+ import { DEFAULT_INITIAL_POLL_DELAY_MS, DEFAULT_POLL_INTERVAL_MS, DEFAULT_POLL_TIMEOUT_MS, MAX_POLL_INTERVAL_MS, } from "../utils/polling.js";
12
12
  export const DEPOSIT_ABI = [
13
- "function deposit(address _depositor, (uint256 npk, uint256 amount, address token)[] notes, (uint256[3] data)[] ciphertexts)",
13
+ "function deposit(address _depositor, (uint256 npk, uint256 amount, address token)[] notes, (uint256[3] data)[] ciphertexts) payable",
14
14
  ];
15
15
  const depositInterface = new Interface(DEPOSIT_ABI);
16
- function validateDepositRequest(request) {
17
- ensureChainId(request.chainId);
18
- ensureAddress("pool address", request.poolAddress);
19
- ensureAddress("depositor", request.depositor);
20
- ensureAddress("token", request.note.token);
21
- if (request.note.mpk < 0n) {
22
- throw new Error("mpk must be non-negative");
23
- }
24
- if (request.note.random < 0n) {
25
- throw new Error("random must be non-negative");
16
+ /**
17
+ * Process notes: derive npk, compute commitment, and encrypt.
18
+ */
19
+ function processNotes(_account, _chainId, _poolAddress, _depositor, notes) {
20
+ return notes.map((note) => {
21
+ const npk = deriveNpk(note);
22
+ const commitment = computeCommitment(note, npk);
23
+ const commitmentHex = Hex.fromBigInt(commitment, FieldSize.SCALAR, true);
24
+ const encrypted = encryptNote(note);
25
+ return {
26
+ npk,
27
+ commitmentHex,
28
+ token: note.token,
29
+ amount: note.amount,
30
+ encrypted,
31
+ };
32
+ });
33
+ }
34
+ /**
35
+ * Build deposit calldata from processed notes.
36
+ */
37
+ function buildDepositCalldata(depositor, processedNotes) {
38
+ return depositInterface.encodeFunctionData("deposit", [
39
+ depositor,
40
+ processedNotes.map((d) => ({
41
+ npk: d.npk,
42
+ amount: d.amount,
43
+ token: d.token,
44
+ })),
45
+ processedNotes.map((d) => ({ data: d.encrypted.data })),
46
+ ]);
47
+ }
48
+ /**
49
+ * Generate a relay ID for tracking.
50
+ */
51
+ function generateRelayId(prefix) {
52
+ return (globalThis.crypto?.randomUUID?.() ?? `${prefix}-${Date.now().toString(16)}`);
53
+ }
54
+ // ============================================================================
55
+ // Public API
56
+ // ============================================================================
57
+ /**
58
+ * Prepare a deposit: compute commitments, build calldata, and persist pending job.
59
+ * Accepts 1 or more notes in a single transaction.
60
+ */
61
+ export async function deposit(store, req) {
62
+ if (!req.notes?.length) {
63
+ throw new ValidationError("At least one note is required for deposit");
26
64
  }
27
- if (request.note.amount < 0n) {
28
- throw new Error("amount must be non-negative");
65
+ const processedNotes = processNotes(req.account, req.chainId, req.poolAddress, req.depositor, req.notes);
66
+ const calldata = buildDepositCalldata(req.depositor, processedNotes);
67
+ const relayId = generateRelayId("dep");
68
+ const value = processedNotes
69
+ .filter((n) => n.token.toLowerCase() === ETH_TOKEN.toLowerCase())
70
+ .reduce((sum, n) => sum + n.amount, 0n);
71
+ const job = {
72
+ relayId,
73
+ kind: "deposit",
74
+ chainId: req.chainId,
75
+ status: "pending",
76
+ txHash: null,
77
+ createdAt: Date.now(),
78
+ timeoutMs: DEFAULT_JOB_TIMEOUT_MS,
79
+ predictedCommitments: processedNotes.map((d) => ({
80
+ hex: d.commitmentHex,
81
+ token: d.token,
82
+ amount: d.amount.toString(),
83
+ })),
84
+ };
85
+ await store.putJob(job);
86
+ return {
87
+ relayId,
88
+ to: req.poolAddress,
89
+ calldata,
90
+ value,
91
+ commitments: processedNotes.map((d) => ({
92
+ commitment: d.commitmentHex,
93
+ token: d.token,
94
+ amount: d.amount,
95
+ })),
96
+ };
97
+ }
98
+ /**
99
+ * Poll for a single commitment and verify/persist it.
100
+ */
101
+ async function pollAndVerifyCommitment(ctx, commitmentHex) {
102
+ let delay = ctx.pollInterval;
103
+ let record = null;
104
+ while (Date.now() <= ctx.deadline) {
105
+ record = await ctx.indexer.tryGetCommitment({
106
+ chainId: ctx.chainId,
107
+ commitment: commitmentHex,
108
+ });
109
+ if (record)
110
+ break;
111
+ await sleep(delay);
112
+ delay = Math.min(delay * 2, MAX_POLL_INTERVAL_MS);
29
113
  }
30
- const tokenScalar = BigInt(request.note.token);
31
- if (tokenScalar < 0n) {
32
- throw new Error("token must map to non-negative scalar");
114
+ if (!record) {
115
+ throw new CoreError(`commitment ${commitmentHex} not found before timeout`);
33
116
  }
117
+ // Note: We intentionally do NOT verify or store the leaf here. The syncChain
118
+ // flow handles leaf storage, merkle verification, and note decryption together.
119
+ // If we verified the tree here, we'd need to keep it in sync across multiple
120
+ // reconcile calls, and if we stored the leaf, syncChain would skip this index
121
+ // and notes wouldn't be decrypted.
122
+ //
123
+ // The indexer's commitment record already contains the verified index and root,
124
+ // which we return to the caller. The subsequent sync will verify consistency
125
+ // when it processes this commitment.
126
+ return {
127
+ index: record.index,
128
+ commitment: record.commitment,
129
+ root: record.root,
130
+ txHash: record.txHash,
131
+ };
34
132
  }
35
- export function createDepositClient(stateStore, options) {
36
- if (!stateStore) {
37
- throw new Error("stateStore dependency is required");
38
- }
39
- const merkleTrees = options.merkleTrees ?? createMerkleTrees();
40
- const indexerClient = createIndexerClient(serviceConfig.indexerBaseUrl, {
41
- fetch: options.fetch,
133
+ /**
134
+ * Create sync context with common setup.
135
+ */
136
+ async function createSyncContext(store, chainId, createdAt, opts) {
137
+ const fetchFn = resolveFetch(opts.fetch);
138
+ if (!fetchFn)
139
+ throw new InitializationError("fetch is required for sync");
140
+ const serviceConfig = createServiceConfig(opts.rpcUrl);
141
+ const trees = resolveMerkleTrees(opts);
142
+ const indexer = createIndexerClient(serviceConfig.indexerBaseUrl, {
143
+ fetch: fetchFn,
42
144
  });
43
- function buildCalldata(request, npk) {
44
- return depositInterface.encodeFunctionData("deposit", [
45
- request.depositor,
46
- [
47
- // TODO(julienbrs) handle multiple notes
48
- {
49
- npk: npk,
50
- amount: request.note.amount,
51
- token: request.note.token,
52
- },
53
- ],
54
- [
55
- {
56
- data: encryptNote(request.note).data,
57
- },
58
- ],
59
- ]);
60
- }
61
- async function persistDepositSuccess(job, record, indexedRoot) {
62
- await Promise.all([
63
- stateStore.putLeaf({
64
- chainId: job.chainId,
65
- index: record.index,
66
- commitment: record.commitment,
67
- }),
68
- stateStore.putRoot({
69
- chainId: job.chainId,
70
- root: indexedRoot,
71
- }),
72
- stateStore.putPendingJob({
73
- ...job,
74
- status: "succeeded",
75
- lastCheckedAt: Date.now(),
76
- txHash: record.txHash ?? job.txHash ?? null,
77
- predictedCommitment: {
78
- ...job.predictedCommitment,
79
- index: record.index,
80
- root: indexedRoot,
81
- },
82
- }),
83
- ]);
145
+ const pollInterval = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
146
+ const pollTimeout = opts.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
147
+ await rebuildTreeFromStore({
148
+ chainId,
149
+ trees,
150
+ loadLeaf: store.getLeaf.bind(store),
151
+ });
152
+ // Calculate deadline BEFORE initial delay so pollTimeout is the total wait time
153
+ const deadline = Date.now() + pollTimeout;
154
+ // Wait initial delay for fresh deposits to reduce wasted 404 polls
155
+ const initialDelay = opts.initialPollDelayMs ?? DEFAULT_INITIAL_POLL_DELAY_MS;
156
+ const jobAge = Date.now() - createdAt;
157
+ if (initialDelay > 0 && jobAge < initialDelay) {
158
+ await sleep(initialDelay - jobAge);
84
159
  }
85
160
  return {
86
- async request(request) {
87
- validateDepositRequest(request);
88
- const npk = deriveNpk(request);
89
- const calldata = buildCalldata(request, npk);
90
- const commitment = computeCommitment(request, npk);
91
- const commitmentHex = ByteUtils.nToHex(commitment, ByteLength.UINT_256, true);
92
- const relayId = uuidv4();
93
- const job = {
94
- relayId,
95
- kind: "deposit",
96
- chainId: request.chainId,
97
- status: "pending",
98
- broadcasterRelayId: null,
99
- txHash: null,
100
- createdAt: Date.now(),
101
- timeoutMs: DEFAULT_JOB_TIMEOUT_MS,
102
- predictedCommitment: {
103
- hex: commitmentHex,
104
- },
105
- };
106
- await stateStore.putPendingJob(job);
107
- return { relayId, calldata, commitment: commitmentHex };
108
- },
109
- async syncPendingDeposit(relayId) {
110
- const job = await stateStore.getPendingJob(relayId);
111
- if (!job || job.kind !== "deposit") {
112
- throw new Error(`unknown deposit relay ${relayId}`);
113
- }
114
- await rebuildTreeFromStore({
115
- chainId: job.chainId,
116
- trees: merkleTrees,
117
- loadLeaf: stateStore.getLeaf.bind(stateStore),
118
- });
119
- const commitmentHex = job.predictedCommitment.hex;
120
- const record = await waitForCommitment(job.chainId, commitmentHex);
121
- const { index, root: indexedRoot } = record;
122
- const commitmentValue = parseHexToBigInt(commitmentHex);
123
- const { index: localIndex, root: localRoot } = merkleTrees.addLeaf(job.chainId, commitmentValue);
124
- // TODO: if local state drift detected, need to re-sync since last correct state
125
- if (localIndex !== index) {
126
- throw new Error("local merkle tree desynchronized (index mismatch), local index: " +
127
- localIndex +
128
- ", indexed index: " +
129
- index);
130
- }
131
- if (localRoot.toLowerCase() !== indexedRoot.toLowerCase()) {
132
- throw new Error("local merkle tree desynchronized (root mismatch), local root: " +
133
- localRoot +
134
- ", indexed root: " +
135
- indexedRoot);
136
- }
137
- await persistDepositSuccess(job, record, indexedRoot);
138
- return {
139
- chainId: job.chainId,
140
- index,
141
- commitment: record.commitment,
142
- root: indexedRoot,
143
- };
144
- },
161
+ store,
162
+ chainId,
163
+ indexer,
164
+ trees,
165
+ pollInterval,
166
+ deadline,
145
167
  };
146
- async function waitForCommitment(chainId, commitmentHex) {
147
- const timeout = options.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
148
- const startedAt = Date.now();
149
- let delay = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
150
- while (Date.now() - startedAt <= timeout) {
151
- try {
152
- const record = await indexerClient.getCommitment({
153
- chainId,
154
- commitment: commitmentHex,
155
- });
156
- if (record)
157
- return record;
158
- }
159
- catch (err) {
160
- if (!isNotFoundError(err)) {
161
- throw err;
162
- }
163
- }
164
- await sleep(delay);
165
- delay = Math.min(delay * 2, MAX_POLL_INTERVAL_MS);
168
+ }
169
+ // ============================================================================
170
+ // Sync Public API
171
+ // ============================================================================
172
+ /**
173
+ * Wait for all commitments in a deposit to be indexed and update local state.
174
+ * Works for both single and multi-note deposits.
175
+ */
176
+ export async function syncDeposit(store, relayId, opts) {
177
+ const jobRecord = await store.getJob(relayId);
178
+ if (!jobRecord || jobRecord.kind !== "deposit")
179
+ throw new CoreError(`unknown deposit relay ${relayId}`);
180
+ const job = jobRecord;
181
+ const ctx = await createSyncContext(store, job.chainId, job.createdAt, opts);
182
+ try {
183
+ const syncedCommitments = [];
184
+ for (const predicted of job.predictedCommitments) {
185
+ const synced = await pollAndVerifyCommitment(ctx, predicted.hex);
186
+ syncedCommitments.push(synced);
166
187
  }
167
- throw new Error("commitment not found in indexer before timeout");
188
+ const finalRoot = syncedCommitments[syncedCommitments.length - 1].root;
189
+ // Update job status (syncChain handles root storage)
190
+ await store.putJob({
191
+ ...job,
192
+ status: "succeeded",
193
+ lastCheckedAt: Date.now(),
194
+ txHash: syncedCommitments[0]?.txHash ?? job.txHash ?? null,
195
+ predictedCommitments: job.predictedCommitments.map((p, i) => ({
196
+ ...p,
197
+ index: syncedCommitments[i]?.index,
198
+ root: syncedCommitments[i]?.root,
199
+ })),
200
+ });
201
+ return {
202
+ chainId: job.chainId,
203
+ commitments: syncedCommitments.map((s) => ({
204
+ index: s.index,
205
+ commitment: s.commitment,
206
+ root: s.root,
207
+ })),
208
+ finalRoot,
209
+ };
210
+ }
211
+ catch (error) {
212
+ await store.putJob({
213
+ ...job,
214
+ status: "failed",
215
+ lastCheckedAt: Date.now(),
216
+ error: error instanceof Error ? error.message : String(error),
217
+ });
218
+ throw error;
168
219
  }
169
220
  }
@@ -1,7 +1,10 @@
1
- export { createDepositClient } from "./deposit.js";
2
- export { createTransactService } from "./transact.js";
1
+ export { deposit, syncDeposit } from "./deposit.js";
2
+ export { transact, syncTransact } from "./transact.js";
3
+ export { planTransaction } from "./transaction-planner.js";
4
+ export { planWithdrawals, type WithdrawalPlan, type WithdrawalInput, } from "./withdrawal-planner.js";
5
+ export { planTransfers, type TransferInput } from "./transfer-planner.js";
6
+ export { selectNotesForAmount } from "./note-selection.js";
3
7
  export { createJobReconciler } from "./reconcile.js";
4
8
  export { createNoteSyncService } from "./note-sync.js";
5
- export type { NoteSyncStateStore } from "./note-sync.js";
6
- export type { DepositRelayResult, DepositNoteInput, DepositRequest, DepositSyncResult, DepositStateStore, Proof, OutputNoteInput, SpendNoteReference, TransactRelayResult, TransactRequest, TransactSyncResult, TransactStateStore, WithdrawalNoteInput, } from "./types.js";
9
+ export type { BaseStateStore, TransactStateStore, NoteSyncStateStore, PollingOptions, ServiceOptions, NoteSyncOptions, Proof, Ciphertext, NoteInput, WithdrawalNoteInput, SpendNoteReference, SerializedWitness, DepositParams, DepositRelayResult, DepositSyncResult, DepositCommitmentInfo, TransactParams, TransactItem, TransactRelayResult, TransactItemResult, TransactSyncResult, Recipient, PlannedOutput, TransactionPlan, SelectableNote, SelectionOptions, SelectionResult, } from "./types/index.js";
7
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../transactions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,EACL,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../transactions/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EACL,eAAe,EACf,KAAK,cAAc,EACnB,KAAK,eAAe,GACrB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAGvD,YAAY,EAEV,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAGlB,cAAc,EACd,cAAc,EACd,eAAe,EAGf,KAAK,EACL,UAAU,EACV,SAAS,EACT,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EAGjB,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EAGrB,cAAc,EACd,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAGlB,SAAS,EACT,aAAa,EACb,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,GAChB,MAAM,kBAAkB,CAAC"}
@@ -1,4 +1,9 @@
1
- export { createDepositClient } from "./deposit.js";
2
- export { createTransactService } from "./transact.js";
1
+ // Primary API
2
+ export { deposit, syncDeposit } from "./deposit.js";
3
+ export { transact, syncTransact } from "./transact.js";
4
+ export { planTransaction } from "./transaction-planner.js";
5
+ export { planWithdrawals, } from "./withdrawal-planner.js";
6
+ export { planTransfers } from "./transfer-planner.js";
7
+ export { selectNotesForAmount } from "./note-selection.js";
3
8
  export { createJobReconciler } from "./reconcile.js";
4
9
  export { createNoteSyncService } from "./note-sync.js";
@@ -0,0 +1,17 @@
1
+ import type { SelectableNote, SelectionOptions, SelectionResult } from "./types/index.js";
2
+ /**
3
+ * Select notes for a transaction with consolidation-first strategy.
4
+ *
5
+ * Priorities:
6
+ * 1. Consolidation - prefer spending more small notes
7
+ * 2. Minimize change - find combinations with less overshoot
8
+ * 3. Respect maxInputs constraint
9
+ *
10
+ * @param notes - Available unspent notes (caller filters by token)
11
+ * @param totalNeeded - Total amount needed for all recipients
12
+ * @param options - Selection options
13
+ * @returns Selection result with notes and amounts
14
+ * @throws ValidationError if insufficient balance or constraints can't be met
15
+ */
16
+ export declare function selectNotesForAmount<T>(notes: SelectableNote<T>[], totalNeeded: bigint, options?: SelectionOptions): SelectionResult<T>;
17
+ //# sourceMappingURL=note-selection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"note-selection.d.ts","sourceRoot":"","sources":["../../transactions/note-selection.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,cAAc,EACd,gBAAgB,EAChB,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAyB1B;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,EAC1B,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACzB,eAAe,CAAC,CAAC,CAAC,CAuEpB"}
@@ -0,0 +1,201 @@
1
+ import { ValidationError } from "../errors.js";
2
+ import { CIRCUIT_REGISTRY } from "../prover/registry.js";
3
+ /** Maximum inputs supported by available circuits */
4
+ const MAX_CIRCUIT_INPUTS = Math.max(...Object.values(CIRCUIT_REGISTRY).map((c) => c.inputs));
5
+ /** Maximum outputs supported by available circuits */
6
+ const MAX_CIRCUIT_OUTPUTS = Math.max(...Object.values(CIRCUIT_REGISTRY).map((c) => c.outputs));
7
+ /**
8
+ * Get the value of a note as bigint.
9
+ * Handles both bigint and string values for NoteRecord compatibility.
10
+ */
11
+ function getNoteValue(note) {
12
+ return typeof note.value === "bigint" ? note.value : BigInt(note.value);
13
+ }
14
+ /**
15
+ * Select notes for a transaction with consolidation-first strategy.
16
+ *
17
+ * Priorities:
18
+ * 1. Consolidation - prefer spending more small notes
19
+ * 2. Minimize change - find combinations with less overshoot
20
+ * 3. Respect maxInputs constraint
21
+ *
22
+ * @param notes - Available unspent notes (caller filters by token)
23
+ * @param totalNeeded - Total amount needed for all recipients
24
+ * @param options - Selection options
25
+ * @returns Selection result with notes and amounts
26
+ * @throws ValidationError if insufficient balance or constraints can't be met
27
+ */
28
+ export function selectNotesForAmount(notes, totalNeeded, options) {
29
+ const maxInputs = options?.maxInputs ?? MAX_CIRCUIT_INPUTS;
30
+ const maxOutputs = options?.maxOutputs ?? MAX_CIRCUIT_OUTPUTS;
31
+ const recipientCount = options?.recipientCount ?? 1;
32
+ // Validate inputs
33
+ if (totalNeeded <= 0n) {
34
+ throw new ValidationError("Amount must be positive");
35
+ }
36
+ if (!notes.length) {
37
+ throw new ValidationError("No notes available");
38
+ }
39
+ // Validate output constraints: recipients + potential change <= maxOutputs
40
+ // Change is needed when selected sum > totalNeeded (almost always)
41
+ // So we conservatively assume change will be needed
42
+ if (recipientCount + 1 > maxOutputs) {
43
+ throw new ValidationError(`Too many recipients: ${recipientCount} recipients + change exceeds maxOutputs (${maxOutputs})`);
44
+ }
45
+ // Check total balance
46
+ const totalAvailable = notes.reduce((sum, n) => sum + getNoteValue(n), 0n);
47
+ if (totalAvailable < totalNeeded) {
48
+ throw new ValidationError("Insufficient balance");
49
+ }
50
+ // Sort ascending for consolidation preference (smallest first)
51
+ const sortedAsc = [...notes].sort((a, b) => {
52
+ const aVal = getNoteValue(a);
53
+ const bVal = getNoteValue(b);
54
+ return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
55
+ });
56
+ // Phase 1: Try greedy smallest-first
57
+ let baseline = greedySelect(sortedAsc, totalNeeded, maxInputs);
58
+ // Phase 2: If greedy smallest-first didn't work, try largest-first as fallback
59
+ if (baseline.sum < totalNeeded) {
60
+ const sortedDesc = [...sortedAsc].reverse();
61
+ baseline = greedySelect(sortedDesc, totalNeeded, maxInputs);
62
+ }
63
+ // Phase 3: If we have a valid baseline, use branch-and-bound to optimize
64
+ // Only run B&B on a subset of notes to keep it fast
65
+ let optimized;
66
+ if (baseline.sum >= totalNeeded) {
67
+ // Limit B&B to reasonable subset for performance (30 smallest notes)
68
+ const maxNotesForBnB = Math.min(sortedAsc.length, 30);
69
+ const notesForBnB = sortedAsc.slice(0, maxNotesForBnB);
70
+ optimized = findOptimalSelection(notesForBnB, totalNeeded, maxInputs, baseline);
71
+ }
72
+ else {
73
+ optimized = baseline;
74
+ }
75
+ // If no valid solution found
76
+ if (optimized.sum < totalNeeded) {
77
+ throw new ValidationError("Cannot meet amount within maxInputs constraint");
78
+ }
79
+ return {
80
+ selected: optimized.notes,
81
+ totalInput: optimized.sum,
82
+ totalNeeded,
83
+ change: optimized.sum - totalNeeded,
84
+ };
85
+ }
86
+ /**
87
+ * Greedy selection: accumulate notes until threshold met.
88
+ * Returns a partial solution if it can't meet the target.
89
+ */
90
+ function greedySelect(sorted, target, maxInputs) {
91
+ const selected = [];
92
+ let sum = 0n;
93
+ for (const note of sorted) {
94
+ if (sum >= target)
95
+ break;
96
+ if (selected.length >= maxInputs)
97
+ break;
98
+ selected.push(note);
99
+ sum += getNoteValue(note);
100
+ }
101
+ return { notes: selected, sum };
102
+ }
103
+ /**
104
+ * Compare two solutions.
105
+ * Returns true if (candidate) is better than (best).
106
+ *
107
+ * Priority 0: Prefer valid solutions (sum >= target)
108
+ * Priority 1: Prefer more notes (consolidation)
109
+ * Priority 2: Prefer less change
110
+ */
111
+ function isBetterSolution(candidate, best, target) {
112
+ const candidateValid = candidate.sum >= target;
113
+ const bestValid = best.sum >= target;
114
+ // Priority 0: Prefer valid solutions
115
+ if (candidateValid && !bestValid)
116
+ return true;
117
+ if (!candidateValid && bestValid)
118
+ return false;
119
+ if (!candidateValid && !bestValid) {
120
+ // Neither valid - prefer higher sum (closer to target)
121
+ return candidate.sum > best.sum;
122
+ }
123
+ // Both valid - apply consolidation + change minimization
124
+ const candidateChange = candidate.sum - target;
125
+ const bestChange = best.sum - target;
126
+ // Priority 1: Prefer more notes (consolidation)
127
+ if (candidate.notes.length > best.notes.length)
128
+ return true;
129
+ if (candidate.notes.length < best.notes.length)
130
+ return false;
131
+ // Priority 2: Prefer less change
132
+ return candidateChange < bestChange;
133
+ }
134
+ /**
135
+ * Branch-and-bound search for optimal selection.
136
+ *
137
+ * Explores the solution space with aggressive pruning:
138
+ * - Stop when sum >= target (don't over-select beyond what's needed)
139
+ * - Stop when remaining notes can't reach target
140
+ * - Stop when maxInputs exceeded
141
+ * - Early termination when we find a solution with maxInputs notes and exact match
142
+ */
143
+ function findOptimalSelection(sorted, target, maxInputs, baseline) {
144
+ let best = baseline;
145
+ let foundOptimal = false;
146
+ // Precompute remaining sums for pruning
147
+ const remainingSums = new Array(sorted.length + 1).fill(0n);
148
+ for (let i = sorted.length - 1; i >= 0; i--) {
149
+ const nextSum = remainingSums[i + 1] ?? 0n;
150
+ const note = sorted[i];
151
+ const noteValue = note ? getNoteValue(note) : 0n;
152
+ remainingSums[i] = nextSum + noteValue;
153
+ }
154
+ function search(index, selected, sum) {
155
+ // Early exit if we found optimal solution
156
+ if (foundOptimal)
157
+ return;
158
+ // Pruning: too many notes selected
159
+ if (selected.length > maxInputs)
160
+ return;
161
+ // Found valid solution, now comparing with best
162
+ if (sum >= target) {
163
+ const candidate = { notes: selected, sum };
164
+ if (isBetterSolution(candidate, best, target)) {
165
+ best = { notes: [...selected], sum };
166
+ // Check if this is optimal (maxInputs notes with exact match)
167
+ if (selected.length === maxInputs && sum === target) {
168
+ foundOptimal = true;
169
+ }
170
+ }
171
+ // Don't add more notes once target is met
172
+ return;
173
+ }
174
+ // Pruning: can't reach target with remaining notes
175
+ const remaining = remainingSums[index] ?? 0n;
176
+ if (sum + remaining < target)
177
+ return;
178
+ // Pruning: reached end of notes
179
+ if (index >= sorted.length)
180
+ return;
181
+ // Pruning: can't possibly beat best's note count
182
+ const remainingSlots = maxInputs - selected.length;
183
+ const remainingNotes = sorted.length - index;
184
+ if (selected.length + Math.min(remainingSlots, remainingNotes) <
185
+ best.notes.length) {
186
+ // Even selecting all remaining notes won't beat best's count
187
+ // Only skip if best is already valid
188
+ if (best.sum >= target)
189
+ return;
190
+ }
191
+ const currentNote = sorted[index];
192
+ if (!currentNote)
193
+ return;
194
+ // Try including current note (explore this branch first for consolidation)
195
+ search(index + 1, [...selected, currentNote], sum + getNoteValue(currentNote));
196
+ // Try excluding current note
197
+ search(index + 1, selected, sum);
198
+ }
199
+ search(0, [], 0n);
200
+ return best;
201
+ }