@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.
- package/dist/account/{zkAccount.d.ts → account.d.ts} +7 -5
- package/dist/account/account.d.ts.map +1 -0
- package/dist/account/{zkAccount.js → account.js} +57 -43
- package/dist/browser/index.js +108202 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/circuits.json +74 -0
- package/dist/clients/broadcaster.d.ts +7 -2
- package/dist/clients/broadcaster.d.ts.map +1 -1
- package/dist/clients/broadcaster.js +9 -1
- package/dist/clients/http.d.ts +6 -0
- package/dist/clients/http.d.ts.map +1 -1
- package/dist/clients/http.js +24 -9
- package/dist/clients/indexer.d.ts +11 -0
- package/dist/clients/indexer.d.ts.map +1 -1
- package/dist/clients/indexer.js +40 -11
- package/dist/config.d.ts +28 -9
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +33 -26
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +5 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +5 -2
- package/dist/crypto-adapters/auto-init.d.ts +2 -0
- package/dist/crypto-adapters/auto-init.d.ts.map +1 -0
- package/dist/crypto-adapters/auto-init.js +7 -0
- package/dist/crypto-adapters/index.d.ts +22 -0
- package/dist/crypto-adapters/index.d.ts.map +1 -0
- package/dist/crypto-adapters/index.js +47 -0
- package/dist/crypto-adapters/polyfills.d.ts +5 -0
- package/dist/crypto-adapters/polyfills.d.ts.map +1 -0
- package/dist/crypto-adapters/polyfills.js +8 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +18 -0
- package/dist/history/index.d.ts +3 -0
- package/dist/history/index.d.ts.map +1 -0
- package/dist/history/index.js +2 -0
- package/dist/history/service.d.ts +46 -0
- package/dist/history/service.d.ts.map +1 -0
- package/dist/history/service.js +354 -0
- package/dist/history/types.d.ts +21 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -4
- package/dist/keys/address.d.ts +13 -0
- package/dist/keys/address.d.ts.map +1 -0
- package/dist/keys/address.js +55 -0
- package/dist/keys/derive.d.ts +37 -0
- package/dist/keys/derive.d.ts.map +1 -0
- package/dist/keys/derive.js +112 -0
- package/dist/keys/hex.d.ts +17 -0
- package/dist/keys/hex.d.ts.map +1 -0
- package/dist/keys/hex.js +66 -0
- package/dist/keys/index.d.ts +5 -0
- package/dist/keys/index.d.ts.map +1 -0
- package/dist/keys/index.js +4 -0
- package/dist/keys/mnemonic.d.ts +8 -0
- package/dist/keys/mnemonic.d.ts.map +1 -0
- package/dist/keys/mnemonic.js +23 -0
- package/dist/keys.d.ts +4 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +4 -0
- package/dist/prover/config.d.ts +1 -15
- package/dist/prover/config.d.ts.map +1 -1
- package/dist/prover/config.js +1 -11
- package/dist/prover/prover.d.ts +15 -4
- package/dist/prover/prover.d.ts.map +1 -1
- package/dist/prover/prover.js +115 -98
- package/dist/prover/registry.d.ts +3 -30
- package/dist/prover/registry.d.ts.map +1 -1
- package/dist/prover/registry.js +12 -51
- package/dist/state/merkle/hydrator.d.ts.map +1 -1
- package/dist/state/merkle/hydrator.js +3 -2
- package/dist/state/merkle/index.d.ts +1 -1
- package/dist/state/merkle/index.d.ts.map +1 -1
- package/dist/state/merkle/index.js +1 -1
- package/dist/state/merkle/merkle-tree.d.ts +8 -0
- package/dist/state/merkle/merkle-tree.d.ts.map +1 -1
- package/dist/state/merkle/merkle-tree.js +16 -7
- package/dist/state/store/ciphertext-store.d.ts +4 -0
- package/dist/state/store/ciphertext-store.d.ts.map +1 -1
- package/dist/state/store/ciphertext-store.js +12 -0
- package/dist/state/store/history-store.d.ts +24 -0
- package/dist/state/store/history-store.d.ts.map +1 -0
- package/dist/state/store/history-store.js +53 -0
- package/dist/state/store/index.d.ts +3 -2
- package/dist/state/store/index.d.ts.map +1 -1
- package/dist/state/store/index.js +1 -0
- package/dist/state/store/job-store.d.ts +7 -7
- package/dist/state/store/job-store.d.ts.map +1 -1
- package/dist/state/store/job-store.js +65 -39
- package/dist/state/store/jobs.d.ts +65 -18
- package/dist/state/store/jobs.d.ts.map +1 -1
- package/dist/state/store/leaf-store.d.ts.map +1 -1
- package/dist/state/store/leaf-store.js +0 -3
- package/dist/state/store/note-store.d.ts +7 -7
- package/dist/state/store/note-store.d.ts.map +1 -1
- package/dist/state/store/note-store.js +38 -34
- package/dist/state/store/nullifier-store.d.ts +9 -0
- package/dist/state/store/nullifier-store.d.ts.map +1 -1
- package/dist/state/store/nullifier-store.js +32 -2
- package/dist/state/store/records.d.ts +31 -2
- package/dist/state/store/records.d.ts.map +1 -1
- package/dist/state/store/root-store.d.ts.map +1 -1
- package/dist/state/store/root-store.js +0 -4
- package/dist/state/store/store.d.ts +61 -27
- package/dist/state/store/store.d.ts.map +1 -1
- package/dist/state/store/store.js +92 -1
- package/dist/storage/indexeddb.js +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +5 -1
- package/dist/transactions/deposit.d.ts +12 -15
- package/dist/transactions/deposit.d.ts.map +1 -1
- package/dist/transactions/deposit.js +203 -152
- package/dist/transactions/index.d.ts +7 -4
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/transactions/index.js +7 -2
- package/dist/transactions/note-selection.d.ts +17 -0
- package/dist/transactions/note-selection.d.ts.map +1 -0
- package/dist/transactions/note-selection.js +201 -0
- package/dist/transactions/note-sync.d.ts +5 -33
- package/dist/transactions/note-sync.d.ts.map +1 -1
- package/dist/transactions/note-sync.js +320 -155
- package/dist/transactions/reconcile.d.ts +10 -12
- package/dist/transactions/reconcile.d.ts.map +1 -1
- package/dist/transactions/reconcile.js +53 -7
- package/dist/transactions/transact.d.ts +13 -24
- package/dist/transactions/transact.d.ts.map +1 -1
- package/dist/transactions/transact.js +393 -507
- package/dist/transactions/transaction-planner.d.ts +34 -0
- package/dist/transactions/transaction-planner.d.ts.map +1 -0
- package/dist/transactions/transaction-planner.js +116 -0
- package/dist/transactions/transfer-planner.d.ts +36 -0
- package/dist/transactions/transfer-planner.d.ts.map +1 -0
- package/dist/transactions/transfer-planner.js +85 -0
- package/dist/transactions/types/deposit.d.ts +67 -0
- package/dist/transactions/types/deposit.d.ts.map +1 -0
- package/dist/transactions/types/domain.d.ts +67 -0
- package/dist/transactions/types/domain.d.ts.map +1 -0
- package/dist/transactions/types/domain.js +4 -0
- package/dist/transactions/types/index.d.ts +18 -0
- package/dist/transactions/types/index.d.ts.map +1 -0
- package/dist/transactions/types/index.js +17 -0
- package/dist/transactions/types/options.d.ts +45 -0
- package/dist/transactions/types/options.d.ts.map +1 -0
- package/dist/transactions/types/options.js +1 -0
- package/dist/transactions/types/planning.d.ts +80 -0
- package/dist/transactions/types/planning.d.ts.map +1 -0
- package/dist/transactions/types/planning.js +1 -0
- package/dist/transactions/types/state-stores.d.ts +103 -0
- package/dist/transactions/types/state-stores.d.ts.map +1 -0
- package/dist/transactions/types/state-stores.js +1 -0
- package/dist/transactions/types/transact.d.ts +76 -0
- package/dist/transactions/types/transact.d.ts.map +1 -0
- package/dist/transactions/types/transact.js +1 -0
- package/dist/transactions/withdrawal-planner.d.ts +58 -0
- package/dist/transactions/withdrawal-planner.d.ts.map +1 -0
- package/dist/transactions/withdrawal-planner.js +128 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsup.browser.config.d.ts +7 -0
- package/dist/tsup.browser.config.d.ts.map +1 -0
- package/dist/tsup.browser.config.js +34 -0
- package/dist/utils/amounts.d.ts +39 -0
- package/dist/utils/amounts.d.ts.map +1 -0
- package/dist/utils/amounts.js +89 -0
- package/dist/utils/async.d.ts +9 -0
- package/dist/utils/async.d.ts.map +1 -1
- package/dist/utils/async.js +24 -0
- package/dist/utils/bigint.js +7 -7
- package/dist/utils/crypto.d.ts +11 -5
- package/dist/utils/crypto.d.ts.map +1 -1
- package/dist/utils/crypto.js +12 -6
- package/dist/utils/format.d.ts +25 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +33 -0
- package/dist/utils/json-codec.js +1 -1
- package/dist/utils/notes.d.ts +15 -0
- package/dist/utils/notes.d.ts.map +1 -0
- package/dist/utils/notes.js +14 -0
- package/dist/utils/polling.d.ts +5 -0
- package/dist/utils/polling.d.ts.map +1 -1
- package/dist/utils/polling.js +5 -0
- package/dist/utils/random.d.ts +13 -0
- package/dist/utils/random.d.ts.map +1 -0
- package/dist/utils/random.js +27 -0
- package/dist/utils/secure-memory.d.ts +25 -0
- package/dist/utils/secure-memory.d.ts.map +1 -0
- package/dist/utils/secure-memory.js +28 -0
- package/dist/utils/signature.d.ts +6 -0
- package/dist/utils/signature.d.ts.map +1 -1
- package/dist/utils/signature.js +8 -6
- package/dist/utils/validators.d.ts +21 -10
- package/dist/utils/validators.d.ts.map +1 -1
- package/dist/utils/validators.js +37 -11
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +13 -0
- package/package.json +28 -11
- package/.eslintrc.json +0 -4
- package/account/zkAccount.test.ts +0 -316
- package/account/zkAccount.ts +0 -222
- package/clients/broadcaster.ts +0 -67
- package/clients/http.ts +0 -94
- package/clients/indexer.ts +0 -150
- package/config.ts +0 -39
- package/core.ts +0 -17
- package/dist/account/railgun-imports-prototype.d.ts +0 -12
- package/dist/account/railgun-imports-prototype.d.ts.map +0 -1
- package/dist/account/railgun-imports-prototype.js +0 -30
- package/dist/account/zkAccount.d.ts.map +0 -1
- package/dist/key-derivation/babyjubjub.d.ts +0 -9
- package/dist/key-derivation/babyjubjub.d.ts.map +0 -1
- package/dist/key-derivation/babyjubjub.js +0 -9
- package/dist/key-derivation/bech32.d.ts +0 -22
- package/dist/key-derivation/bech32.d.ts.map +0 -1
- package/dist/key-derivation/bech32.js +0 -86
- package/dist/key-derivation/bip32.d.ts +0 -17
- package/dist/key-derivation/bip32.d.ts.map +0 -1
- package/dist/key-derivation/bip32.js +0 -41
- package/dist/key-derivation/bip39.d.ts +0 -22
- package/dist/key-derivation/bip39.d.ts.map +0 -1
- package/dist/key-derivation/bip39.js +0 -56
- package/dist/key-derivation/bytes.d.ts +0 -19
- package/dist/key-derivation/bytes.d.ts.map +0 -1
- package/dist/key-derivation/bytes.js +0 -92
- package/dist/key-derivation/hash.d.ts +0 -3
- package/dist/key-derivation/hash.d.ts.map +0 -1
- package/dist/key-derivation/hash.js +0 -10
- package/dist/key-derivation/index.d.ts +0 -8
- package/dist/key-derivation/index.d.ts.map +0 -1
- package/dist/key-derivation/index.js +0 -7
- package/dist/key-derivation/wallet-node.d.ts +0 -45
- package/dist/key-derivation/wallet-node.d.ts.map +0 -1
- package/dist/key-derivation/wallet-node.js +0 -109
- package/dist/state/ciphertext-store.d.ts +0 -12
- package/dist/state/ciphertext-store.d.ts.map +0 -1
- package/dist/state/ciphertext-store.js +0 -25
- package/dist/state/hydrator.d.ts +0 -16
- package/dist/state/hydrator.d.ts.map +0 -1
- package/dist/state/hydrator.js +0 -18
- package/dist/state/job-store.d.ts +0 -12
- package/dist/state/job-store.d.ts.map +0 -1
- package/dist/state/job-store.js +0 -118
- package/dist/state/jobs.d.ts +0 -50
- package/dist/state/jobs.d.ts.map +0 -1
- package/dist/state/jobs.js +0 -1
- package/dist/state/leaf-store.d.ts +0 -17
- package/dist/state/leaf-store.d.ts.map +0 -1
- package/dist/state/leaf-store.js +0 -35
- package/dist/state/merkle-tree.d.ts +0 -34
- package/dist/state/merkle-tree.d.ts.map +0 -1
- package/dist/state/merkle-tree.js +0 -104
- package/dist/state/note-store.d.ts +0 -37
- package/dist/state/note-store.d.ts.map +0 -1
- package/dist/state/note-store.js +0 -133
- package/dist/state/nullifier-store.d.ts +0 -13
- package/dist/state/nullifier-store.d.ts.map +0 -1
- package/dist/state/nullifier-store.js +0 -21
- package/dist/state/records.d.ts +0 -57
- package/dist/state/records.d.ts.map +0 -1
- package/dist/state/root-store.d.ts +0 -13
- package/dist/state/root-store.d.ts.map +0 -1
- package/dist/state/root-store.js +0 -30
- package/dist/state/store.d.ts +0 -26
- package/dist/state/store.d.ts.map +0 -1
- package/dist/state/store.js +0 -19
- package/dist/state.d.ts +0 -83
- package/dist/state.d.ts.map +0 -1
- package/dist/state.js +0 -171
- package/dist/transactions/shield.d.ts +0 -5
- package/dist/transactions/shield.d.ts.map +0 -1
- package/dist/transactions/shield.js +0 -93
- package/dist/transactions/types.d.ts +0 -114
- package/dist/transactions/types.d.ts.map +0 -1
- package/dist/transactions/utils.d.ts +0 -10
- package/dist/transactions/utils.d.ts.map +0 -1
- package/dist/transactions/utils.js +0 -17
- package/dist/utils/time.d.ts +0 -2
- package/dist/utils/time.d.ts.map +0 -1
- package/dist/utils/time.js +0 -3
- package/dist/utils/witness.d.ts +0 -11
- package/dist/utils/witness.d.ts.map +0 -1
- package/dist/utils/witness.js +0 -19
- package/errors.ts +0 -20
- package/index.ts +0 -17
- package/key-derivation/babyjubjub.ts +0 -11
- package/key-derivation/bech32.test.ts +0 -90
- package/key-derivation/bech32.ts +0 -124
- package/key-derivation/bip32.ts +0 -56
- package/key-derivation/bip39.ts +0 -76
- package/key-derivation/bytes.ts +0 -118
- package/key-derivation/hash.ts +0 -13
- package/key-derivation/index.ts +0 -7
- package/key-derivation/wallet-node.ts +0 -155
- package/keys.ts +0 -47
- package/prover/config.ts +0 -104
- package/prover/index.ts +0 -1
- package/prover/prover.integration.test.ts +0 -162
- package/prover/prover.test.ts +0 -309
- package/prover/prover.ts +0 -405
- package/prover/registry.test.ts +0 -90
- package/prover/registry.ts +0 -82
- package/schema.ts +0 -17
- package/setup-artifacts.sh +0 -57
- package/state/index.ts +0 -2
- package/state/merkle/hydrator.ts +0 -69
- package/state/merkle/index.ts +0 -12
- package/state/merkle/merkle-tree.test.ts +0 -50
- package/state/merkle/merkle-tree.ts +0 -163
- package/state/store/ciphertext-store.ts +0 -28
- package/state/store/index.ts +0 -24
- package/state/store/job-store.ts +0 -162
- package/state/store/jobs.ts +0 -64
- package/state/store/leaf-store.ts +0 -39
- package/state/store/note-store.ts +0 -177
- package/state/store/nullifier-store.ts +0 -39
- package/state/store/records.ts +0 -61
- package/state/store/root-store.ts +0 -34
- package/state/store/store.ts +0 -25
- package/state.test.ts +0 -235
- package/storage/index.ts +0 -3
- package/storage/indexeddb.test.ts +0 -99
- package/storage/indexeddb.ts +0 -235
- package/storage/memory.test.ts +0 -59
- package/storage/memory.ts +0 -93
- package/transactions/deposit.test.ts +0 -160
- package/transactions/deposit.ts +0 -227
- package/transactions/index.ts +0 -20
- package/transactions/note-sync.test.ts +0 -155
- package/transactions/note-sync.ts +0 -452
- package/transactions/reconcile.ts +0 -73
- package/transactions/transact.test.ts +0 -451
- package/transactions/transact.ts +0 -811
- package/transactions/types.ts +0 -141
- package/tsconfig.json +0 -14
- package/types/global.d.ts +0 -15
- package/types.ts +0 -24
- package/utils/async.ts +0 -15
- package/utils/bigint.ts +0 -34
- package/utils/crypto.test.ts +0 -69
- package/utils/crypto.ts +0 -58
- package/utils/json-codec.ts +0 -38
- package/utils/polling.ts +0 -6
- package/utils/signature.ts +0 -16
- package/utils/validators.test.ts +0 -64
- package/utils/validators.ts +0 -86
- /package/dist/{transactions → history}/types.js +0 -0
- /package/dist/{state/records.js → transactions/types/deposit.js} +0 -0
|
@@ -1,169 +1,220 @@
|
|
|
1
1
|
import { Interface } from "ethers";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveFetch } from "../clients/http.js";
|
|
3
3
|
import { createIndexerClient } from "../clients/indexer.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
-
export {
|
|
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 "./
|
|
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":"
|
|
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
|
-
|
|
2
|
-
export {
|
|
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
|
+
}
|