@invisi-labs/sdk 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sqlite.cjs CHANGED
@@ -321,8 +321,9 @@ function readProtocolLogs(receipt, protocolAddress) {
321
321
  }
322
322
 
323
323
  // src/session/discovery.ts
324
- var import_ethers3 = require("ethers");
325
324
  var import_core = require("@invisi-labs/core");
325
+ var import_ethers3 = require("ethers");
326
+ var import_core2 = require("@invisi-labs/core");
326
327
  var import_protocol = require("@invisi-labs/protocol");
327
328
  var EVENTS = /* @__PURE__ */ new Set([
328
329
  "CommitmentsInserted",
@@ -416,7 +417,7 @@ function extractDeliveryCandidates(logs, context, confirmedThrough) {
416
417
  check2(inserted, "delivery has no matching canonical insertion");
417
418
  if (commitment !== void 0)
418
419
  check2(hash(commitment) === inserted, "delivery commitment conflicts with insertion");
419
- const delivery = (0, import_protocol.normalizeDelivery)(wire, { credit: role !== "funds" });
420
+ const delivery = (0, import_protocol.normalizeOpaqueDelivery)(wire);
420
421
  check2(
421
422
  typeof token === "string" && (0, import_ethers3.isAddress)(token) && BigInt(token) !== 0n,
422
423
  "invalid event token"
@@ -447,7 +448,7 @@ function extractDeliveryCandidates(logs, context, confirmedThrough) {
447
448
  add(log, tree, index(leaves[0]), fields.creditCommitment, "owner", ds[0]);
448
449
  add(log, tree, index(leaves[0]), fields.creditCommitment, "spender", ds[1]);
449
450
  if (hasChange) add(log, tree, index(leaves[1]), change, "funds", ds[2]);
450
- else (0, import_protocol.normalizeDelivery)(ds[2], { empty: true });
451
+ else (0, import_protocol.normalizeOpaqueDelivery)(ds[2], { empty: true });
451
452
  };
452
453
  for (const log of relevant) {
453
454
  const a = object(log.args);
@@ -477,7 +478,7 @@ function extractDeliveryCandidates(logs, context, confirmedThrough) {
477
478
  const length = Number.parseInt(packed.slice(offset, offset + 4), 16);
478
479
  offset += 4;
479
480
  check2(
480
- length >= 160 && length <= 416 && offset + length * 2 + 136 <= packed.length,
481
+ length >= 1 && length <= import_core.MAX_CIPHERTEXT_BYTES && offset + length * 2 + 136 <= packed.length,
481
482
  "invalid packed delivery length"
482
483
  );
483
484
  const ciphertext = `0x${packed.slice(offset, offset + length * 2)}`;
@@ -499,8 +500,8 @@ function extractDeliveryCandidates(logs, context, confirmedThrough) {
499
500
  cs.forEach((c, i) => {
500
501
  if (BigInt(hash(c)) === 0n) {
501
502
  check2(integer(leaves[i]) === 0n, "absent remainder has nonzero position");
502
- (0, import_protocol.normalizeDelivery)(ds[2 * i], { empty: true });
503
- (0, import_protocol.normalizeDelivery)(ds[2 * i + 1], { empty: true });
503
+ (0, import_protocol.normalizeOpaqueDelivery)(ds[2 * i], { empty: true });
504
+ (0, import_protocol.normalizeOpaqueDelivery)(ds[2 * i + 1], { empty: true });
504
505
  } else {
505
506
  add(log, tree, index(leaves[i]), c, "owner", ds[2 * i]);
506
507
  add(log, tree, index(leaves[i]), c, "spender", ds[2 * i + 1]);
@@ -512,7 +513,7 @@ function extractDeliveryCandidates(logs, context, confirmedThrough) {
512
513
  }
513
514
  async function decryptCandidates(logs, options) {
514
515
  contextCheck(options.context);
515
- (0, import_core.decodeAddress)(import_core.ReceivingAddress.fromIdentity(options.viewing.walletIdentity).toString());
516
+ (0, import_core2.decodeAddress)(import_core2.ReceivingAddress.fromIdentity(options.viewing.walletIdentity).toString());
516
517
  check2(
517
518
  !logs.some(
518
519
  (log) => log.address.toLowerCase() === options.context.protocolAddress.toLowerCase() && log.removed
@@ -854,7 +855,7 @@ function walletBalances(state) {
854
855
  }
855
856
 
856
857
  // src/session/store.ts
857
- var import_core2 = require("@invisi-labs/core");
858
+ var import_core3 = require("@invisi-labs/core");
858
859
  var import_sha256 = require("@noble/hashes/sha256");
859
860
  var import_utils = require("@noble/hashes/utils");
860
861
  function scopeKey(scope) {
@@ -870,7 +871,7 @@ function scopeKey(scope) {
870
871
  return (0, import_utils.bytesToHex)(
871
872
  (0, import_sha256.sha256)(
872
873
  (0, import_utils.utf8ToBytes)(
873
- "Invisi.WalletScope.v2:" + JSON.stringify([
874
+ "Invisi.WalletScope.v1:" + JSON.stringify([
874
875
  scope.namespace,
875
876
  scope.chainId.toString(),
876
877
  scope.protocolAddress.toLowerCase(),
@@ -907,12 +908,12 @@ function validateState(state, scope, revision) {
907
908
  throw new Error("invalid spent state");
908
909
  }
909
910
  for (const record of Object.values(state.credits)) {
910
- const note = (0, import_core2.normalizeCreditDeliveryState)(record.state).note;
911
+ const note = (0, import_core3.normalizeCreditDeliveryState)(record.state).note;
911
912
  if (note.chainId !== scope.chainId || note.protocolAddress !== BigInt(scope.protocolAddress) || !["owner", "spender"].includes(record.role) || (record.role === "owner" ? note.ownerWalletIdentity : note.spenderWalletIdentity).toLowerCase() !== scope.walletIdentity.toLowerCase() || !["unspent", "unknown", "spent"].includes(record.spent))
912
913
  throw new Error("invalid stored Credit context or role");
913
914
  }
914
915
  for (const record of [...Object.values(state.notes), ...Object.values(state.credits)]) {
915
- if (!/^0x[0-9a-fA-F]{64}$/.test(record.commitment) || BigInt(record.commitment) <= 0n || BigInt(record.commitment) >= import_core2.FIELD_MODULUS || typeof record.treeId !== "bigint" || record.treeId < 1n || record.treeId >= 1n << 64n || !Number.isInteger(record.leafIndex) || record.leafIndex < 0 || record.leafIndex >= 65536 || typeof record.blockNumber !== "bigint" || record.blockNumber < 0n || !/^0x[0-9a-fA-F]{64}$/.test(record.blockHash))
916
+ if (!/^0x[0-9a-fA-F]{64}$/.test(record.commitment) || BigInt(record.commitment) <= 0n || BigInt(record.commitment) >= import_core3.FIELD_MODULUS || typeof record.treeId !== "bigint" || record.treeId < 1n || record.treeId >= 1n << 64n || !Number.isInteger(record.leafIndex) || record.leafIndex < 0 || record.leafIndex >= 65536 || typeof record.blockNumber !== "bigint" || record.blockNumber < 0n || !/^0x[0-9a-fA-F]{64}$/.test(record.blockHash))
916
917
  throw new Error("invalid stored receipt position");
917
918
  }
918
919
  for (const [id, operation] of Object.entries(state.reservations))
@@ -973,14 +974,11 @@ var TransactionalWalletStore = class {
973
974
  };
974
975
 
975
976
  // src/session/operation-protocol.ts
976
- var import_core3 = require("@invisi-labs/core");
977
+ var import_core4 = require("@invisi-labs/core");
977
978
  var import_protocol2 = require("@invisi-labs/protocol");
978
979
  function requiresAuthorization(plan) {
979
- if (plan.kind === "sweep") {
980
- if (plan.credit || !plan.inputs.length || plan.outputs.length || !plan.params || plan.params.outputCommitments.length || (plan.funding.amount ?? 0n) !== 0n || BigInt(plan.params.withdrawalAddress) !== BigInt(plan.scope.walletIdentity) || plan.params.withdrawalAmount !== plan.inputs.reduce((sum, input) => sum + input.note.amount, 0n) || plan.inputs.some((input) => input.note.walletIdentity !== plan.scope.walletIdentity))
981
- throw new Error("invalid owner-only sweep plan");
982
- return false;
983
- }
980
+ const ownerOnlyFullWithdrawal = !plan.credit && plan.inputs.length > 0 && plan.outputs.length === 0 && plan.params !== void 0 && plan.params.outputCommitments.length === 0 && (plan.funding.amount ?? 0n) === 0n && plan.fundingMode === "none" && BigInt(plan.params.withdrawalAddress) === BigInt(plan.scope.walletIdentity) && plan.params.withdrawalAmount === plan.inputs.reduce((sum, input) => sum + input.note.amount, 0n) && plan.inputs.every((input) => input.note.walletIdentity === plan.scope.walletIdentity);
981
+ if (ownerOnlyFullWithdrawal) return false;
984
982
  return !!plan.credit || plan.inputs.length > 0;
985
983
  }
986
984
  function inputFacts(plan) {
@@ -1244,7 +1242,7 @@ function operationExpired(plan, now) {
1244
1242
  if (credit?.creation && now >= credit.creation.expiry) return true;
1245
1243
  if (credit?.draw) {
1246
1244
  if (credit.draw.expiries.some((expiry) => now >= expiry)) return true;
1247
- const prepared = (0, import_core3.creditPeriods)(plan.preparedAt.timestamp), current2 = (0, import_core3.creditPeriods)(now);
1245
+ const prepared = (0, import_core4.creditPeriods)(plan.preparedAt.timestamp), current2 = (0, import_core4.creditPeriods)(now);
1248
1246
  if (prepared.currentDay !== current2.currentDay || prepared.currentMonth !== current2.currentMonth)
1249
1247
  return true;
1250
1248
  }
@@ -2743,7 +2741,8 @@ function estimateGasReference(plan, protocol, profiles) {
2743
2741
 
2744
2742
  // src/session/operation-execution.ts
2745
2743
  var import_ethers6 = require("ethers");
2746
- var import_core4 = require("@invisi-labs/core");
2744
+ var import_core5 = require("@invisi-labs/core");
2745
+ var import_protocol3 = require("@invisi-labs/protocol");
2747
2746
 
2748
2747
  // src/session/proof-integrity.ts
2749
2748
  var import_ethers5 = require("ethers");
@@ -2772,23 +2771,41 @@ async function authorizeOperation(plan, provider) {
2772
2771
  if (provider.walletIdentity !== plan.scope.walletIdentity)
2773
2772
  throw new Error("wallet authorization capability mismatch");
2774
2773
  const request = authorizationRequest(plan);
2775
- const digest = (0, import_core4.computeWalletAuthorizationDigest)(request);
2774
+ const digest = (0, import_core5.computeWalletAuthorizationDigest)(request);
2776
2775
  const signed = await provider.authorize(structuredClone(request));
2777
2776
  if (signed.walletIdentity !== plan.scope.walletIdentity || signed.digest !== digest)
2778
2777
  throw new Error("wallet authorization statement mismatch");
2779
2778
  return signed;
2780
2779
  }
2781
2780
  async function proveOperation(protocol, plan, authorization, prover, options) {
2781
+ if (Object.hasOwn(options, "target")) {
2782
+ throw new Error("prove accepts execution, not a backend target");
2783
+ }
2784
+ const { execution = "single", ...proverOptions } = options;
2785
+ if (execution !== "single" && execution !== "aggregate") {
2786
+ throw new Error("execution must be single or aggregate");
2787
+ }
2788
+ const frozen = statement(plan);
2789
+ if (execution === "aggregate" && (plan.kind !== "transfer" || plan.capacity !== 14)) {
2790
+ throw new Error("aggregate execution requires a 14-slot transfer");
2791
+ }
2792
+ if (plan.kind === "transfer") {
2793
+ plan.statement = import_protocol3.WitnessBuilder.statement(
2794
+ frozen.circuitName,
2795
+ frozen.expectedPublicInputs,
2796
+ execution === "aggregate" ? "noir-recursive" : "evm"
2797
+ );
2798
+ }
2782
2799
  const request = await witnessFor(protocol, plan, authorization);
2783
2800
  if (JSON.stringify({ ...request, witness: void 0 }) !== JSON.stringify(plan.statement))
2784
2801
  throw new Error("prepared proof statement changed");
2785
2802
  try {
2786
- const proof = publicProof(await prover.prove(request, options));
2803
+ const proof = publicProof(await prover.prove(request, proverOptions));
2787
2804
  sameStatement(proof, plan.statement);
2788
2805
  if (await prover.verify(
2789
2806
  structuredClone(proof),
2790
2807
  structuredClone(plan.statement),
2791
- options
2808
+ proverOptions
2792
2809
  ) !== true)
2793
2810
  throw new Error("proof verification failed");
2794
2811
  return proof;
@@ -2803,19 +2820,88 @@ async function signOperationFunding(saved, signer, verifier) {
2803
2820
  throw new Error("funding signer is not the payer");
2804
2821
  const typed = await fundingTypedData(saved);
2805
2822
  const signature = await signer.signTypedData(structuredClone(typed));
2823
+ return verifyFundingSignature(typed, payer, signature, verifier);
2824
+ }
2825
+ function walletSigningRequest(plan) {
2826
+ return (0, import_core5.createSigningRequest)((0, import_core5.walletAuthorizationTypedData)(authorizationRequest(plan)));
2827
+ }
2828
+ async function acceptWalletSignature(plan, signature) {
2829
+ const request = authorizationRequest(plan);
2830
+ const digest = (0, import_core5.computeWalletAuthorizationDigest)(request);
2831
+ try {
2832
+ const signed = import_ethers6.Signature.from(signature);
2833
+ const publicKey = (0, import_ethers6.getBytes)(import_ethers6.SigningKey.recoverPublicKey(digest, signed));
2834
+ const owner = await (0, import_core5.computeWalletIdentity)(publicKey);
2835
+ if (owner.toLowerCase() !== plan.scope.walletIdentity.toLowerCase()) {
2836
+ throw new Error("wallet signature identity mismatch");
2837
+ }
2838
+ return new import_core5.AuthorizedStatement(plan.scope.walletIdentity, digest, {
2839
+ public_key_x: Array.from(publicKey.slice(1, 33)),
2840
+ public_key_y: Array.from(publicKey.slice(33)),
2841
+ signature: Array.from((0, import_ethers6.getBytes)((0, import_ethers6.concat)([signed.r, signed.s])))
2842
+ });
2843
+ } catch {
2844
+ throw new Error("invalid wallet authorization signature");
2845
+ }
2846
+ }
2847
+ async function fundingSigningRequest(saved, runtime) {
2848
+ const plan = saved.plan;
2849
+ if (plan.fundingMode !== "signature") {
2850
+ throw new Error("operation does not require a funding signature");
2851
+ }
2852
+ if (plan.kind !== "shield" && !saved.proof) {
2853
+ throw new Error("proof must precede funding authorization");
2854
+ }
2855
+ await runtime.options.protocol.validateDeployment(runtime.reader);
2856
+ const block = await runtime.reader.getBlock(await runtime.reader.getLatestBlockNumber());
2857
+ if (block.timestamp > plan.deadline || block.timestamp > (plan.funding.deadline ?? 0n)) {
2858
+ throw new Error("funding authorization expired");
2859
+ }
2860
+ if (await runtime.reader.getFundingNonce(plan.context, plan.funding.payer, block) !== plan.funding.sigNonce) {
2861
+ throw new Error("funding nonce changed; prepare again");
2862
+ }
2863
+ const request = (0, import_core5.createSigningRequest)(await fundingTypedData(saved));
2864
+ if ((await runtime.reader.getBlock(block.number)).hash !== block.hash) {
2865
+ throw new Error("canonical funding block changed");
2866
+ }
2867
+ return request;
2868
+ }
2869
+ async function acceptFundingSignature(saved, signature, runtime) {
2870
+ if (!/^0x(?:[a-fA-F0-9]{2})+$/.test(signature)) {
2871
+ throw new Error("invalid EVM funding signature");
2872
+ }
2873
+ const request = await fundingSigningRequest(saved, runtime);
2874
+ return verifyFundingSignature(
2875
+ request,
2876
+ saved.plan.funding.payer,
2877
+ signature,
2878
+ runtime.options.fundingVerifier
2879
+ );
2880
+ }
2881
+ async function verifyFundingSignature(request, payer, signature, verifier) {
2882
+ if (!/^0x(?:[a-fA-F0-9]{2})+$/.test(signature)) {
2883
+ throw new Error("invalid EVM funding signature");
2884
+ }
2885
+ const types = Object.fromEntries(
2886
+ Object.entries(request.types).map(([name, fields]) => [name, [...fields]])
2887
+ );
2888
+ let validEOA = false;
2889
+ try {
2890
+ validEOA = (0, import_ethers6.verifyTypedData)(request.domain, types, request.message, signature).toLowerCase() === payer.toLowerCase();
2891
+ if (validEOA) {
2892
+ signature = import_ethers6.Signature.from(signature).serialized;
2893
+ }
2894
+ } catch {
2895
+ validEOA = false;
2896
+ }
2806
2897
  const valid = verifier ? await verifier.verifyTypedData({
2807
- ...structuredClone(typed),
2898
+ ...structuredClone(request),
2808
2899
  address: payer,
2809
2900
  signature
2810
- }) : (0, import_ethers6.verifyTypedData)(
2811
- typed.domain,
2812
- Object.fromEntries(
2813
- Object.entries(typed.types).map(([name, fields]) => [name, [...fields]])
2814
- ),
2815
- typed.message,
2816
- signature
2817
- ).toLowerCase() === payer.toLowerCase();
2818
- if (valid !== true) throw new Error("invalid EVM funding signature");
2901
+ }) : validEOA;
2902
+ if (valid !== true) {
2903
+ throw new Error("invalid EVM funding signature");
2904
+ }
2819
2905
  return signature;
2820
2906
  }
2821
2907
 
@@ -2856,16 +2942,16 @@ function operationPreview(plan, status) {
2856
2942
  }
2857
2943
 
2858
2944
  // src/session/Operation.ts
2859
- var import_core8 = require("@invisi-labs/core");
2860
- var import_protocol4 = require("@invisi-labs/protocol");
2945
+ var import_core9 = require("@invisi-labs/core");
2946
+ var import_protocol5 = require("@invisi-labs/protocol");
2861
2947
 
2862
2948
  // src/session/operation-confirmation.ts
2863
- var import_core7 = require("@invisi-labs/core");
2949
+ var import_core8 = require("@invisi-labs/core");
2864
2950
 
2865
2951
  // src/session/aggregate-receipt.ts
2866
2952
  var import_ethers7 = require("ethers");
2867
2953
  var import_contracts3 = require("@invisi-labs/protocol/contracts");
2868
- var import_protocol3 = require("@invisi-labs/protocol");
2954
+ var import_protocol4 = require("@invisi-labs/protocol");
2869
2955
  var aggregateAbi = import_contracts3.invisiProtocolAbi.filter(
2870
2956
  (item) => item.type === "function" && item.name === "transact" && item.inputs.length === 2 && item.inputs[1]?.type === "tuple[]"
2871
2957
  );
@@ -2893,7 +2979,7 @@ function matchesAggregateTransaction(saved, transaction) {
2893
2979
  });
2894
2980
  if (decoded.name !== "transact" || !Array.isArray(requests) || ![4, 8, 12].includes(requests.length))
2895
2981
  return false;
2896
- const canonical2 = (request) => (0, import_protocol3.buildTransactCalldata)({
2982
+ const canonical2 = (request) => (0, import_protocol4.buildTransactCalldata)({
2897
2983
  context: { ...plan.context, tokenAddress: request.params.token },
2898
2984
  proof: "0x00",
2899
2985
  params: request.params,
@@ -2901,7 +2987,7 @@ function matchesAggregateTransaction(saved, transaction) {
2901
2987
  funding: request.funding,
2902
2988
  caller: transaction.from
2903
2989
  }).data.toLowerCase();
2904
- const expected = (0, import_protocol3.buildTransactCalldata)({
2990
+ const expected = (0, import_protocol4.buildTransactCalldata)({
2905
2991
  context: plan.context,
2906
2992
  proof: "0x00",
2907
2993
  params: plan.params,
@@ -2916,10 +3002,10 @@ function matchesAggregateTransaction(saved, transaction) {
2916
3002
  }
2917
3003
 
2918
3004
  // src/session/credit-values.ts
2919
- var import_core5 = require("@invisi-labs/core");
3005
+ var import_core6 = require("@invisi-labs/core");
2920
3006
  async function creditLeaf(state) {
2921
3007
  const n = state.note;
2922
- return (0, import_core5.computeCreditCommitment)(
3008
+ return (0, import_core6.computeCreditCommitment)(
2923
3009
  n.chainId,
2924
3010
  n.spenderWalletIdentity,
2925
3011
  n.ownerWalletIdentity,
@@ -2929,23 +3015,23 @@ async function creditLeaf(state) {
2929
3015
  n.amount,
2930
3016
  n.nonce,
2931
3017
  n.expiry,
2932
- await (0, import_core5.computeCreditSharedKeyHash)(n.creditSharedKey),
3018
+ await (0, import_core6.computeCreditSharedKeyHash)(n.creditSharedKey),
2933
3019
  n
2934
3020
  );
2935
3021
  }
2936
3022
 
2937
3023
  // src/session/membership.ts
2938
- var import_core6 = require("@invisi-labs/core");
3024
+ var import_core7 = require("@invisi-labs/core");
2939
3025
  async function verifyMembership(reader, context, block, input, proof) {
2940
3026
  if (proof.block.hash !== block.hash || proof.block.number !== block.number || proof.leaf !== input.commitment || proof.treeId !== input.treeId || proof.leafIndex !== input.leafIndex || proof.path.length !== 16 || !Number.isInteger(input.leafIndex) || input.leafIndex < 0 || input.leafIndex >= 65536 || !await reader.isKnownRoot(context, input.treeId, proof.root, block))
2941
3027
  throw new Error("membership context/root mismatch");
2942
3028
  let root = BigInt(input.commitment);
2943
3029
  for (let i = 0; i < 16; i++) {
2944
3030
  const sibling = proof.path[i];
2945
- if (typeof sibling !== "bigint" || sibling < 0n || sibling >= import_core6.FIELD_MODULUS)
3031
+ if (typeof sibling !== "bigint" || sibling < 0n || sibling >= import_core7.FIELD_MODULUS)
2946
3032
  throw new Error("noncanonical membership path");
2947
3033
  root = BigInt(
2948
- await (0, import_core6.poseidonHash)(input.leafIndex >> i & 1 ? [sibling, root] : [root, sibling])
3034
+ await (0, import_core7.poseidonHash)(input.leafIndex >> i & 1 ? [sibling, root] : [root, sibling])
2949
3035
  );
2950
3036
  }
2951
3037
  if (root !== BigInt(proof.root))
@@ -3117,7 +3203,7 @@ async function confirmOperation(runtime, id, generation, hash2, signal) {
3117
3203
  if (credits.length) {
3118
3204
  const nullifiers = await Promise.all(
3119
3205
  credits.map(
3120
- async (record) => (0, import_core7.computeCreditNullifier)(
3206
+ async (record) => (0, import_core8.computeCreditNullifier)(
3121
3207
  plan.context.chainId,
3122
3208
  record.state.note.creditSharedKey,
3123
3209
  await creditLeaf(record.state)
@@ -3418,6 +3504,81 @@ var Operation = class _Operation {
3418
3504
  throw error;
3419
3505
  }
3420
3506
  }
3507
+ async signingCurrent() {
3508
+ await this.#runtime.store.transaction(this.#runtime.scope, (state) => {
3509
+ check6(!state.invalidated, "wallet state requires canonical resync");
3510
+ this.current(state.operations[this.id]);
3511
+ });
3512
+ }
3513
+ /** Public EIP-712 request; signing transport and key custody belong to the caller. */
3514
+ async exportWalletAuthorization(options = {}) {
3515
+ options.signal?.throwIfAborted();
3516
+ await this.signingCurrent();
3517
+ check6(
3518
+ requiresAuthorization(this.#saved.plan),
3519
+ "operation does not require wallet authorization"
3520
+ );
3521
+ check6(
3522
+ ["prepared", "authorized"].includes(this.status),
3523
+ "operation is not awaiting wallet authorization"
3524
+ );
3525
+ return walletSigningRequest(this.#saved.plan);
3526
+ }
3527
+ /** Attach an external EOA signature, retaining its witness only at the private proving boundary. */
3528
+ async withWalletSignature(signature, options = {}) {
3529
+ options.signal?.throwIfAborted();
3530
+ await this.signingCurrent();
3531
+ check6(
3532
+ requiresAuthorization(this.#saved.plan),
3533
+ "operation does not require wallet authorization"
3534
+ );
3535
+ check6(
3536
+ ["prepared", "authorized"].includes(this.status),
3537
+ "operation is not awaiting wallet authorization"
3538
+ );
3539
+ const authorization = await acceptWalletSignature(this.#saved.plan, signature);
3540
+ return this.stage(
3541
+ "wallet authorization",
3542
+ ["prepared", "authorized"],
3543
+ async () => ({ status: "authorized", authorization }),
3544
+ options.signal
3545
+ );
3546
+ }
3547
+ /** Export only after proving when the funding authorization binds an actual proof hash. */
3548
+ async exportFundingAuthorization(options = {}) {
3549
+ options.signal?.throwIfAborted();
3550
+ await this.signingCurrent();
3551
+ check6(
3552
+ this.status === (this.kind === "shield" ? "prepared" : "proved"),
3553
+ "operation is not awaiting funding authorization or proof"
3554
+ );
3555
+ const request = await fundingSigningRequest(this.#saved, this.#runtime);
3556
+ options.signal?.throwIfAborted();
3557
+ await this.signingCurrent();
3558
+ return request;
3559
+ }
3560
+ /** Verify an external signature against this handle's own frozen operation. */
3561
+ async withFundingSignature(signature, options = {}) {
3562
+ options.signal?.throwIfAborted();
3563
+ await this.signingCurrent();
3564
+ check6(
3565
+ this.status === (this.kind === "shield" ? "prepared" : "proved"),
3566
+ "operation is not awaiting funding authorization or proof"
3567
+ );
3568
+ const verified = await acceptFundingSignature(this.#saved, signature, this.#runtime);
3569
+ return this.stage(
3570
+ "funding authorization",
3571
+ this.kind === "shield" ? ["prepared"] : ["proved"],
3572
+ async (saved) => {
3573
+ saved.plan.funding.signature = verified;
3574
+ if (saved.plan.statement?.target !== "noir-recursive") {
3575
+ saved.call = await this.encode(saved);
3576
+ }
3577
+ return { status: "ready" };
3578
+ },
3579
+ options.signal
3580
+ );
3581
+ }
3421
3582
  async authorize(options = {}) {
3422
3583
  check6(
3423
3584
  requiresAuthorization(this.#saved.plan),
@@ -3438,7 +3599,8 @@ var Operation = class _Operation {
3438
3599
  options.signal
3439
3600
  );
3440
3601
  }
3441
- async prove(options = {}) {
3602
+ async prove(suppliedOptions = {}) {
3603
+ const options = { ...suppliedOptions };
3442
3604
  const plan = this.#saved.plan;
3443
3605
  check6(plan.kind !== "shield", "shield requires no proof");
3444
3606
  if (requiresAuthorization(plan)) {
@@ -3449,7 +3611,7 @@ var Operation = class _Operation {
3449
3611
  }
3450
3612
  const prover = this.#runtime.options.prover;
3451
3613
  if (!prover) {
3452
- throw new import_core8.MissingCapabilityError("prover");
3614
+ throw new import_core9.MissingCapabilityError("prover");
3453
3615
  }
3454
3616
  return this.stage(
3455
3617
  "proof",
@@ -3483,7 +3645,7 @@ var Operation = class _Operation {
3483
3645
  check6(p.kind === "shield" || this.#saved.proof, "proof must precede funding authorization");
3484
3646
  const signer = this.#runtime.options.fundingSigner;
3485
3647
  if (!signer) {
3486
- throw new import_core8.MissingCapabilityError("EVM funding signer");
3648
+ throw new import_core9.MissingCapabilityError("EVM funding signer");
3487
3649
  }
3488
3650
  check6(
3489
3651
  signer.address.toLowerCase() === p.funding.payer.toLowerCase(),
@@ -3585,7 +3747,7 @@ var Operation = class _Operation {
3585
3747
  request: {
3586
3748
  params: structuredClone(plan.params),
3587
3749
  deliveries: plan.outputs.map((output) => structuredClone(output.delivery)),
3588
- funding: (0, import_protocol4.normalizeFunding)(plan.funding)
3750
+ funding: (0, import_protocol5.normalizeFunding)(plan.funding)
3589
3751
  }
3590
3752
  };
3591
3753
  });
@@ -3672,18 +3834,181 @@ var Operation = class _Operation {
3672
3834
  }
3673
3835
  };
3674
3836
 
3837
+ // src/session/WalletSession.ts
3838
+ var import_core16 = require("@invisi-labs/core");
3839
+
3840
+ // src/session/history.ts
3841
+ function walletHistory(state, options = {}) {
3842
+ const token = options.tokenAddress?.toLowerCase();
3843
+ const outgoing = [];
3844
+ for (const record of Object.values(state.operations)) {
3845
+ const saved = record.payload;
3846
+ if (!saved?.plan || token && saved.plan.context.tokenAddress.toLowerCase() !== token) {
3847
+ continue;
3848
+ }
3849
+ const plan = saved.plan;
3850
+ outgoing.push({
3851
+ ...operationPreview(plan, record.status),
3852
+ preparedAt: { ...plan.preparedAt },
3853
+ ...record.transactionHash ? { transactionHash: record.transactionHash } : {},
3854
+ outputs: plan.outputs.map((output) => ({
3855
+ to: output.address,
3856
+ amount: output.note.amount,
3857
+ change: output.change,
3858
+ commitment: output.commitment,
3859
+ payload: Uint8Array.from(output.note.payload ?? [])
3860
+ }))
3861
+ });
3862
+ }
3863
+ outgoing.sort(
3864
+ (a, b) => a.preparedAt.number === b.preparedAt.number ? a.id.localeCompare(b.id) : a.preparedAt.number > b.preparedAt.number ? -1 : 1
3865
+ );
3866
+ const incoming = [];
3867
+ for (const record of Object.values(state.notes)) {
3868
+ const tokenAddress = `0x${record.note.tokenAddress.toString(16).padStart(40, "0")}`;
3869
+ if (token && tokenAddress !== token) continue;
3870
+ incoming.push({
3871
+ commitment: record.commitment,
3872
+ treeId: record.treeId,
3873
+ leafIndex: record.leafIndex,
3874
+ blockNumber: record.blockNumber,
3875
+ blockHash: record.blockHash,
3876
+ ...record.transactionHash ? { transactionHash: record.transactionHash } : {},
3877
+ tokenAddress,
3878
+ amount: record.note.amount,
3879
+ payload: Uint8Array.from(record.note.payload ?? []),
3880
+ spent: record.spent
3881
+ });
3882
+ }
3883
+ incoming.sort(
3884
+ (a, b) => a.blockNumber === b.blockNumber ? a.commitment.localeCompare(b.commitment) : a.blockNumber > b.blockNumber ? -1 : 1
3885
+ );
3886
+ return structuredClone({
3887
+ revision: state.revision,
3888
+ checkedThrough: state.discoveryThrough ?? null,
3889
+ invalidated: !!state.invalidated,
3890
+ outgoing,
3891
+ incoming
3892
+ });
3893
+ }
3894
+
3895
+ // src/errors.ts
3896
+ var import_core10 = require("@invisi-labs/core");
3897
+ var InvisiError = class extends Error {
3898
+ code;
3899
+ operation;
3900
+ address;
3901
+ outputIndex;
3902
+ field;
3903
+ capability;
3904
+ constructor(code, message, details = {}) {
3905
+ super(message, { cause: details.cause });
3906
+ this.name = "InvisiError";
3907
+ this.code = code;
3908
+ this.operation = details.operation;
3909
+ this.address = details.address;
3910
+ this.outputIndex = details.outputIndex;
3911
+ this.field = details.field;
3912
+ this.capability = details.capability;
3913
+ }
3914
+ };
3915
+ async function prepareWithErrors(operation, prepare) {
3916
+ try {
3917
+ return await prepare();
3918
+ } catch (cause) {
3919
+ if (cause instanceof InvisiError) {
3920
+ if (cause.operation) throw cause;
3921
+ throw new InvisiError(cause.code, cause.message, {
3922
+ operation,
3923
+ ...cause.address === void 0 ? {} : { address: cause.address },
3924
+ ...cause.outputIndex === void 0 ? {} : { outputIndex: cause.outputIndex },
3925
+ ...cause.field === void 0 ? {} : { field: cause.field },
3926
+ ...cause.capability === void 0 ? {} : { capability: cause.capability },
3927
+ cause: cause.cause
3928
+ });
3929
+ }
3930
+ throw new InvisiError(
3931
+ cause instanceof import_core10.MissingCapabilityError ? "MISSING_CAPABILITY" : "PREPARATION_FAILED",
3932
+ cause instanceof Error ? cause.message : "Transaction preparation failed",
3933
+ {
3934
+ operation,
3935
+ cause,
3936
+ ...cause instanceof import_core10.MissingCapabilityError ? { capability: cause.capability } : {}
3937
+ }
3938
+ );
3939
+ }
3940
+ }
3941
+
3675
3942
  // src/session/WalletSession.ts
3676
3943
  var import_ethers11 = require("ethers");
3677
- var import_core13 = require("@invisi-labs/core");
3944
+ var import_core17 = require("@invisi-labs/core");
3678
3945
 
3679
3946
  // src/session/transaction-preparation.ts
3680
3947
  var import_ethers10 = require("ethers");
3681
- var import_protocol7 = require("@invisi-labs/protocol");
3948
+ var import_protocol8 = require("@invisi-labs/protocol");
3949
+
3950
+ // src/resolve-recipient.ts
3951
+ var import_core11 = require("@invisi-labs/core");
3952
+ async function resolveRecipient(address2, lookup, details = {}) {
3953
+ const context = { ...details, address: address2 };
3954
+ let recipient;
3955
+ try {
3956
+ recipient = import_core11.ReceivingAddress.parse(address2);
3957
+ } catch (cause) {
3958
+ throw new InvisiError("INVALID_RECIPIENT", "Invalid recipient address", {
3959
+ ...context,
3960
+ cause
3961
+ });
3962
+ }
3963
+ if (recipient.kind === "long") return recipient;
3964
+ if (!lookup) {
3965
+ throw new InvisiError("MISSING_CAPABILITY", "Missing registry lookup capability", {
3966
+ ...context,
3967
+ capability: "registry lookup"
3968
+ });
3969
+ }
3970
+ let keys;
3971
+ try {
3972
+ keys = await lookup(recipient.walletIdentity);
3973
+ } catch (cause) {
3974
+ if (cause instanceof import_core11.MissingCapabilityError) {
3975
+ throw new InvisiError("MISSING_CAPABILITY", cause.message, {
3976
+ ...context,
3977
+ cause,
3978
+ capability: cause.capability
3979
+ });
3980
+ }
3981
+ throw new InvisiError("RECIPIENT_LOOKUP_FAILED", "Recipient registry lookup failed", {
3982
+ ...context,
3983
+ cause
3984
+ });
3985
+ }
3986
+ if (keys == null) {
3987
+ throw new InvisiError(
3988
+ "EVM_ADDRESS_NOT_REGISTERED",
3989
+ "EVM address has no registered receiving keys",
3990
+ context
3991
+ );
3992
+ }
3993
+ try {
3994
+ return import_core11.ReceivingAddress.from({
3995
+ walletIdentity: recipient.walletIdentity,
3996
+ viewingPublicKey: keys.viewingPublicKey,
3997
+ clueKey: keys.clueKey
3998
+ });
3999
+ } catch (cause) {
4000
+ throw new InvisiError(
4001
+ "RECIPIENT_LOOKUP_FAILED",
4002
+ "Registry returned invalid receiving keys",
4003
+ { ...context, cause }
4004
+ );
4005
+ }
4006
+ }
3682
4007
 
3683
4008
  // src/session/prepare-common.ts
3684
4009
  var import_ethers8 = require("ethers");
3685
- var import_core9 = require("@invisi-labs/core");
3686
- var import_protocol5 = require("@invisi-labs/protocol");
4010
+ var import_core12 = require("@invisi-labs/core");
4011
+ var import_protocol6 = require("@invisi-labs/protocol");
3687
4012
 
3688
4013
  // src/session/selection.ts
3689
4014
  function selectNotes(notes, target, reserved) {
@@ -3716,6 +4041,27 @@ function positiveAmount(value, bits = 128) {
3716
4041
  }
3717
4042
  return value;
3718
4043
  }
4044
+ function copyApplicationPayload(value) {
4045
+ if (value === void 0) return new Uint8Array();
4046
+ if (!(value instanceof Uint8Array)) throw new TypeError("payload must be Uint8Array");
4047
+ return Uint8Array.from(value);
4048
+ }
4049
+ function positiveOutputAmount(payment, outputIndex) {
4050
+ try {
4051
+ return positiveAmount(payment.amount);
4052
+ } catch (cause) {
4053
+ throw new InvisiError(
4054
+ "PREPARATION_FAILED",
4055
+ cause instanceof Error ? cause.message : "Invalid output amount",
4056
+ {
4057
+ address: payment.to,
4058
+ field: "amount",
4059
+ ...outputIndex === void 0 ? {} : { outputIndex },
4060
+ cause
4061
+ }
4062
+ );
4063
+ }
4064
+ }
3719
4065
  var Preparation = class {
3720
4066
  constructor(runtime) {
3721
4067
  this.runtime = runtime;
@@ -3769,7 +4115,7 @@ var Preparation = class {
3769
4115
  if (mode === "caller" && (source.sigNonce || source.deadline)) {
3770
4116
  throw new Error("caller funding uses zero nonce/deadline");
3771
4117
  }
3772
- const funding = (0, import_protocol5.normalizeFunding)({
4118
+ const funding = (0, import_protocol6.normalizeFunding)({
3773
4119
  payer,
3774
4120
  amount: source.amount,
3775
4121
  sigNonce: mode === "caller" ? 0n : source.sigNonce ?? await this.runtime.reader.getFundingNonce(
@@ -3785,52 +4131,70 @@ var Preparation = class {
3785
4131
  }
3786
4132
  return { funding, fundingMode: mode };
3787
4133
  }
3788
- async output(payment, context, change = false) {
3789
- positiveAmount(payment.amount);
3790
- const recipient = await import_core9.ReceivingAddress.parse(payment.to).resolve(
3791
- this.runtime.options.addressResolver
3792
- );
3793
- const memo = (0, import_core9.encodeMemo)(payment.memo);
3794
- const nonce = (0, import_core9.generateNonce)();
3795
- const identity = await (0, import_core9.computeNoteIdentity)(BigInt(recipient.walletIdentity), nonce);
3796
- const keys = recipient.getReceivingKeys();
3797
- const note = {
3798
- chainId: context.chainId,
3799
- protocolAddress: BigInt(context.protocolAddress),
3800
- tokenAddress: BigInt(context.tokenAddress),
3801
- walletIdentity: recipient.walletIdentity,
3802
- amount: payment.amount,
3803
- ...memo.length === 0 ? {} : { memo },
3804
- nonce
3805
- };
3806
- const commitment = await (0, import_core9.computeNoteCommitment)(
3807
- context.chainId,
3808
- identity,
3809
- note.tokenAddress,
3810
- note.amount
3811
- );
3812
- return {
3813
- note,
3814
- commitment,
3815
- identity,
3816
- address: payment.to,
3817
- change,
3818
- delivery: {
3819
- ciphertext: (0, import_ethers8.hexlify)(
3820
- await (0, import_core9.encryptPrivateTransferNote)(keys.viewingPublicKey, note, {
3821
- chainId: context.chainId,
3822
- protocolAddress: note.protocolAddress,
3823
- outputCommitment: commitment
3824
- })
3825
- ),
3826
- clue: (0, import_ethers8.hexlify)((0, import_core9.createClue)(keys.clueKey))
3827
- }
3828
- };
4134
+ async output(payment, context, change = false, outputIndex) {
4135
+ try {
4136
+ positiveOutputAmount(payment, outputIndex);
4137
+ const recipient = await resolveRecipient(
4138
+ payment.to,
4139
+ this.runtime.options.addressResolver,
4140
+ {
4141
+ field: change ? "changeAddress" : "to",
4142
+ ...outputIndex === void 0 ? {} : { outputIndex }
4143
+ }
4144
+ );
4145
+ const nonce = (0, import_core12.generateNonce)();
4146
+ const identity = await (0, import_core12.computeNoteIdentity)(BigInt(recipient.walletIdentity), nonce);
4147
+ const keys = recipient.getReceivingKeys();
4148
+ const note = {
4149
+ chainId: context.chainId,
4150
+ protocolAddress: BigInt(context.protocolAddress),
4151
+ tokenAddress: BigInt(context.tokenAddress),
4152
+ walletIdentity: recipient.walletIdentity,
4153
+ amount: payment.amount,
4154
+ payload: copyApplicationPayload(payment.payload),
4155
+ nonce
4156
+ };
4157
+ const commitment = await (0, import_core12.computeNoteCommitment)(
4158
+ context.chainId,
4159
+ identity,
4160
+ note.tokenAddress,
4161
+ note.amount
4162
+ );
4163
+ return {
4164
+ note,
4165
+ commitment,
4166
+ identity,
4167
+ address: payment.to,
4168
+ change,
4169
+ delivery: {
4170
+ ciphertext: (0, import_ethers8.hexlify)(
4171
+ await (0, import_core12.encryptPrivateTransferNote)(keys.viewingPublicKey, note, {
4172
+ chainId: context.chainId,
4173
+ protocolAddress: note.protocolAddress,
4174
+ outputCommitment: commitment
4175
+ })
4176
+ ),
4177
+ clue: (0, import_ethers8.hexlify)((0, import_core12.createClue)(keys.clueKey))
4178
+ }
4179
+ };
4180
+ } catch (cause) {
4181
+ if (cause instanceof InvisiError) throw cause;
4182
+ throw new InvisiError(
4183
+ "PREPARATION_FAILED",
4184
+ cause instanceof Error ? cause.message : "Output preparation failed",
4185
+ {
4186
+ address: payment.to,
4187
+ field: change ? "changeAddress" : "to",
4188
+ ...outputIndex === void 0 ? {} : { outputIndex },
4189
+ cause
4190
+ }
4191
+ );
4192
+ }
3829
4193
  }
3830
4194
  async membership(input, frame) {
3831
4195
  const { options, reader } = this.runtime;
3832
4196
  if (!options.protocolRpc)
3833
- throw new import_core9.MissingCapabilityError("Protocol RPC membership provider");
4197
+ throw new import_core12.MissingCapabilityError("Protocol RPC membership provider");
3834
4198
  const root = this.#roots.get(input.treeId);
3835
4199
  const proof = await options.protocolRpc.getMembership(
3836
4200
  frame.context,
@@ -3877,9 +4241,9 @@ var Preparation = class {
3877
4241
  for (const input of selected) {
3878
4242
  const note = input.note;
3879
4243
  const c = frame.context;
3880
- if (note.walletIdentity !== scope.walletIdentity || note.chainId !== c.chainId || note.protocolAddress !== BigInt(c.protocolAddress) || note.tokenAddress !== BigInt(c.tokenAddress) || await (0, import_core9.computeNoteCommitment)(
4244
+ if (note.walletIdentity !== scope.walletIdentity || note.chainId !== c.chainId || note.protocolAddress !== BigInt(c.protocolAddress) || note.tokenAddress !== BigInt(c.tokenAddress) || await (0, import_core12.computeNoteCommitment)(
3881
4245
  c.chainId,
3882
- await (0, import_core9.computeNoteIdentity)(BigInt(note.walletIdentity), note.nonce),
4246
+ await (0, import_core12.computeNoteIdentity)(BigInt(note.walletIdentity), note.nonce),
3883
4247
  note.tokenAddress,
3884
4248
  note.amount
3885
4249
  ) !== input.commitment) {
@@ -3931,8 +4295,8 @@ function storedCandidates(inputs) {
3931
4295
 
3932
4296
  // src/session/prepare-credit.ts
3933
4297
  var import_ethers9 = require("ethers");
3934
- var import_core10 = require("@invisi-labs/core");
3935
- var import_protocol6 = require("@invisi-labs/protocol");
4298
+ var import_core13 = require("@invisi-labs/core");
4299
+ var import_protocol7 = require("@invisi-labs/protocol");
3936
4300
  var EMPTY = { ciphertext: "0x", clue: "0x" };
3937
4301
  var CreditPreparation = class {
3938
4302
  constructor(runtime) {
@@ -3947,7 +4311,7 @@ var CreditPreparation = class {
3947
4311
  return value;
3948
4312
  }
3949
4313
  async seal(input, frame) {
3950
- const state = (0, import_core10.normalizeCreditDeliveryState)(input);
4314
+ const state = (0, import_core13.normalizeCreditDeliveryState)(input);
3951
4315
  const commitment = await creditLeaf(state);
3952
4316
  const context = {
3953
4317
  chainId: frame.context.chainId,
@@ -3959,9 +4323,9 @@ var CreditPreparation = class {
3959
4323
  const keys = role === "owner" ? state.ownerReceivingKeys : state.spenderReceivingKeys;
3960
4324
  return {
3961
4325
  ciphertext: (0, import_ethers9.hexlify)(
3962
- await (0, import_core10.encryptCreditNote)(keys.viewingPublicKey, state, { ...context, role })
4326
+ await (0, import_core13.encryptCreditNote)(keys.viewingPublicKey, state, { ...context, role })
3963
4327
  ),
3964
- clue: (0, import_ethers9.hexlify)((0, import_core10.createClue)(keys.clueKey))
4328
+ clue: (0, import_ethers9.hexlify)((0, import_core13.createClue)(keys.clueKey))
3965
4329
  };
3966
4330
  })
3967
4331
  );
@@ -3972,14 +4336,14 @@ var CreditPreparation = class {
3972
4336
  if (!stored || stored.spent !== "unspent" || stored.role !== role) {
3973
4337
  throw new Error(`confirmed unspent ${role} Credit is unavailable`);
3974
4338
  }
3975
- const state = (0, import_core10.normalizeCreditDeliveryState)(stored.state);
4339
+ const state = (0, import_core13.normalizeCreditDeliveryState)(stored.state);
3976
4340
  const n = state.note;
3977
4341
  if ((role === "owner" ? n.ownerWalletIdentity : n.spenderWalletIdentity) !== this.runtime.scope.walletIdentity || n.chainId !== frame.context.chainId || n.protocolAddress !== BigInt(frame.context.protocolAddress) || n.tokenAddress !== BigInt(frame.context.tokenAddress) || await creditLeaf(state) !== commitment) {
3978
4342
  throw new Error("Credit role, commitment or deployment mismatch");
3979
4343
  }
3980
4344
  const proof = await this.#prepare.membership(stored, frame);
3981
4345
  const path = [...proof.path];
3982
- const nullifier = await (0, import_core10.computeCreditNullifier)(n.chainId, n.creditSharedKey, commitment);
4346
+ const nullifier = await (0, import_core13.computeCreditNullifier)(n.chainId, n.creditSharedKey, commitment);
3983
4347
  await this.#prepare.assertUnspent([nullifier], frame);
3984
4348
  return {
3985
4349
  input: {
@@ -4016,10 +4380,16 @@ var CreditPreparation = class {
4016
4380
  positiveAmount(options.amount, 126);
4017
4381
  const frame = await this.#prepare.frame(options.deadline);
4018
4382
  const expiry = this.expiry(options.expiry, frame);
4019
- const spender = await import_core10.ReceivingAddress.parse(options.spender).resolve(
4020
- this.runtime.options.addressResolver
4383
+ const spender = await resolveRecipient(
4384
+ options.spender,
4385
+ this.runtime.options.addressResolver,
4386
+ { field: "spender" }
4387
+ );
4388
+ const owner = await resolveRecipient(
4389
+ this.runtime.options.wallet.getReceivingAddress().toString(),
4390
+ this.runtime.options.addressResolver,
4391
+ { field: "wallet" }
4021
4392
  );
4022
- const owner = await this.runtime.options.wallet.getReceivingAddress().resolve(this.runtime.options.addressResolver);
4023
4393
  const ownerPayout = (0, import_ethers9.getAddress)(owner.walletIdentity);
4024
4394
  const spenderPayout = (0, import_ethers9.getAddress)(spender.walletIdentity);
4025
4395
  if (BigInt(ownerPayout) === 0n || BigInt(spenderPayout) === 0n) {
@@ -4035,16 +4405,16 @@ var CreditPreparation = class {
4035
4405
  spenderWalletIdentity: spender.walletIdentity,
4036
4406
  ownerPayout: BigInt(ownerPayout),
4037
4407
  spenderPayout: BigInt(spenderPayout),
4038
- ...(0, import_core10.normalizeCreditPolicy)({
4408
+ ...(0, import_core13.normalizeCreditPolicy)({
4039
4409
  singleLimit: options.singleLimit ?? 0n,
4040
4410
  dailyLimit: options.dailyLimit ?? 0n,
4041
4411
  monthlyLimit: options.monthlyLimit ?? 0n
4042
4412
  }),
4043
4413
  amount: options.amount,
4044
- ...options.memo === void 0 ? {} : { memo: options.memo },
4414
+ payload: copyApplicationPayload(options.payload),
4045
4415
  expiry,
4046
- nonce: (0, import_core10.generateNonce)(),
4047
- creditSharedKey: (0, import_core10.generateCreditSharedKey)()
4416
+ nonce: (0, import_core13.generateNonce)(),
4417
+ creditSharedKey: (0, import_core13.generateCreditSharedKey)()
4048
4418
  },
4049
4419
  ownerReceivingKeys: owner.getReceivingKeys(),
4050
4420
  spenderReceivingKeys: spender.getReceivingKeys()
@@ -4086,7 +4456,7 @@ var CreditPreparation = class {
4086
4456
  sources: selected.sources,
4087
4457
  nullifiers: selected.nullifiers,
4088
4458
  creditCommitment: output.commitment,
4089
- changeCommitment: plan.outputs[0]?.commitment ?? (0, import_protocol6.word)(0n),
4459
+ changeCommitment: plan.outputs[0]?.commitment ?? (0, import_protocol7.word)(0n),
4090
4460
  owner: (0, import_ethers9.getAddress)((0, import_ethers9.toBeHex)(output.state.note.ownerPayout, 20)),
4091
4461
  expiry: output.state.note.expiry,
4092
4462
  deadline: frame.deadline
@@ -4106,14 +4476,14 @@ var CreditPreparation = class {
4106
4476
  creditNullifier: previous.nullifier,
4107
4477
  previousExpiry: previous.input.state.note.expiry
4108
4478
  };
4109
- plan.statement = import_protocol6.WitnessBuilder.statement(
4479
+ plan.statement = import_protocol7.WitnessBuilder.statement(
4110
4480
  "credit_top_up",
4111
- (0, import_protocol6.topUpCreditPublicInputs)(frame.context, plan.credit.topUp, deliveries, plan.funding)
4481
+ (0, import_protocol7.topUpCreditPublicInputs)(frame.context, plan.credit.topUp, deliveries, plan.funding)
4112
4482
  );
4113
4483
  } else {
4114
- plan.statement = import_protocol6.WitnessBuilder.statement(
4484
+ plan.statement = import_protocol7.WitnessBuilder.statement(
4115
4485
  "create_credit",
4116
- (0, import_protocol6.createCreditPublicInputs)(frame.context, creation, deliveries, plan.funding)
4486
+ (0, import_protocol7.createCreditPublicInputs)(frame.context, creation, deliveries, plan.funding)
4117
4487
  );
4118
4488
  }
4119
4489
  await this.#prepare.assertBlock(frame);
@@ -4142,8 +4512,10 @@ var CreditPreparation = class {
4142
4512
  monthlyLimit: options.monthlyLimit ?? old.note.monthlyLimit ?? 0n,
4143
4513
  amount,
4144
4514
  expiry,
4145
- nonce: (0, import_core10.generateNonce)(),
4146
- ...options.memo === void 0 ? {} : { memo: options.memo }
4515
+ nonce: (0, import_core13.generateNonce)(),
4516
+ payload: copyApplicationPayload(
4517
+ options.payload === void 0 ? old.note.payload : options.payload
4518
+ )
4147
4519
  }
4148
4520
  },
4149
4521
  frame
@@ -4159,7 +4531,7 @@ var CreditPreparation = class {
4159
4531
  }
4160
4532
  async draw(options) {
4161
4533
  const frame = await this.#prepare.frame(options.deadline);
4162
- const profile = (0, import_protocol6.selectCreditDrawCircuit)(options.credits.length);
4534
+ const profile = (0, import_protocol7.selectCreditDrawCircuit)(options.credits.length);
4163
4535
  if (new Set(options.credits.map((value) => value.credit)).size !== options.credits.length) {
4164
4536
  throw new Error("duplicate Credit input");
4165
4537
  }
@@ -4191,11 +4563,11 @@ var CreditPreparation = class {
4191
4563
  }
4192
4564
  sources.set(member.source.treeId, member.source.merkleRoot);
4193
4565
  amounts.push(amount);
4194
- const usage = (0, import_core10.applyCreditDrawUsage)(n, amount, frame.block.timestamp);
4566
+ const usage = (0, import_core13.applyCreditDrawUsage)(n, amount, frame.block.timestamp);
4195
4567
  const remaining = n.amount - amount;
4196
4568
  if (remaining === 0n) {
4197
- remainders.push((0, import_protocol6.word)(0n));
4198
- recoveryAmounts.push((0, import_protocol6.word)(0n));
4569
+ remainders.push((0, import_protocol7.word)(0n));
4570
+ recoveryAmounts.push((0, import_protocol7.word)(0n));
4199
4571
  deliveries.push({ ...EMPTY }, { ...EMPTY });
4200
4572
  continue;
4201
4573
  }
@@ -4213,8 +4585,10 @@ var CreditPreparation = class {
4213
4585
  ...n,
4214
4586
  ...usage,
4215
4587
  amount: remaining,
4216
- ...options.credits[i].memo === void 0 ? {} : { memo: options.credits[i].memo },
4217
- nonce: await (0, import_core10.deriveRemainderNonce)(...args)
4588
+ payload: copyApplicationPayload(
4589
+ options.credits[i].payload === void 0 ? n.payload : options.credits[i].payload
4590
+ ),
4591
+ nonce: await (0, import_core13.deriveRemainderNonce)(...args)
4218
4592
  }
4219
4593
  },
4220
4594
  frame
@@ -4222,7 +4596,7 @@ var CreditPreparation = class {
4222
4596
  outputs.push(output);
4223
4597
  remainders.push(output.commitment);
4224
4598
  recoveryAmounts.push(
4225
- (0, import_protocol6.word)((remaining + await (0, import_core10.computeCreditRecoveryPad)(...args)) % import_core10.FIELD_MODULUS)
4599
+ (0, import_protocol7.word)((remaining + await (0, import_core13.computeCreditRecoveryPad)(...args)) % import_core13.FIELD_MODULUS)
4226
4600
  );
4227
4601
  deliveries.push(...output.deliveries);
4228
4602
  }
@@ -4246,9 +4620,9 @@ var CreditPreparation = class {
4246
4620
  draw: params,
4247
4621
  amounts
4248
4622
  };
4249
- plan.statement = import_protocol6.WitnessBuilder.statement(
4623
+ plan.statement = import_protocol7.WitnessBuilder.statement(
4250
4624
  profile.name,
4251
- (0, import_protocol6.creditDrawPublicInputs)(frame.context, params, deliveries)
4625
+ (0, import_protocol7.creditDrawPublicInputs)(frame.context, params, deliveries)
4252
4626
  );
4253
4627
  await this.#prepare.assertBlock(frame);
4254
4628
  return this.runtime.complete(plan);
@@ -4268,9 +4642,9 @@ var CreditPreparation = class {
4268
4642
  };
4269
4643
  plan.capacity = 1;
4270
4644
  plan.credit = { inputs: [previous.input], outputs: [], deliveries: [], cancel: params };
4271
- plan.statement = import_protocol6.WitnessBuilder.statement(
4645
+ plan.statement = import_protocol7.WitnessBuilder.statement(
4272
4646
  "credit_cancel",
4273
- (0, import_protocol6.cancelCreditPublicInputs)(frame.context, params)
4647
+ (0, import_protocol7.cancelCreditPublicInputs)(frame.context, params)
4274
4648
  );
4275
4649
  await this.#prepare.assertBlock(frame);
4276
4650
  return this.runtime.complete(plan);
@@ -4291,87 +4665,48 @@ var TransactionPreparation = class {
4291
4665
  return this.context.scope;
4292
4666
  }
4293
4667
  async prepareTransfer(options) {
4668
+ if (Object.hasOwn(options, "target") || Object.hasOwn(options, "execution")) {
4669
+ throw new Error(
4670
+ "prepareTransfer accepts no target or execution; choose execution in prove()"
4671
+ );
4672
+ }
4294
4673
  const copied = structuredClone(options);
4295
- const preparation = new Preparation(this.runtime(copied.tokenAddress));
4674
+ const runtime = this.runtime(copied.tokenAddress);
4675
+ if (copied.commitments !== void 0 && copied.commitments.length === 0)
4676
+ throw new Error("empty explicit input selection");
4677
+ const preparation = new Preparation({
4678
+ ...runtime,
4679
+ ...copied.commitments !== void 0 ? { inputSelection: copied.commitments } : {}
4680
+ });
4296
4681
  const frame = await preparation.frame(copied.deadline);
4297
- if (copied.outputs.length < 1 || copied.outputs.length > 168) {
4298
- throw new Error("transfer requires 1..168 outputs");
4682
+ if (copied.outputs.length > 168) throw new Error("transfer supports at most 168 outputs");
4683
+ let withdrawalAddress = import_protocol8.ZERO_ADDRESS;
4684
+ let withdrawalAmount = 0n;
4685
+ if (copied.withdrawal !== void 0) {
4686
+ withdrawalAddress = (0, import_ethers10.getAddress)(copied.withdrawal.to);
4687
+ positiveAmount(copied.withdrawal.amount);
4688
+ if (BigInt(withdrawalAddress) === 0n)
4689
+ throw new Error("withdrawal destination must be nonzero");
4690
+ withdrawalAmount = copied.withdrawal.amount;
4299
4691
  }
4692
+ if (copied.outputs.length === 0 && withdrawalAmount === 0n)
4693
+ throw new Error("transfer requires a private output or public withdrawal");
4300
4694
  const total = copied.outputs.reduce(
4301
- (sum, output) => sum + positiveAmount(output.amount),
4302
- 0n
4695
+ (sum, output, index2) => sum + positiveOutputAmount(output, index2),
4696
+ withdrawalAmount
4303
4697
  );
4304
4698
  const funding = await preparation.funding(copied.funding, frame);
4305
4699
  return this.funds(
4306
- "transfer",
4307
4700
  copied.outputs,
4308
4701
  total,
4309
- import_protocol7.ZERO_ADDRESS,
4310
- 0n,
4702
+ withdrawalAddress,
4703
+ withdrawalAmount,
4311
4704
  frame,
4312
4705
  funding,
4313
- copied.target,
4314
- preparation
4315
- );
4316
- }
4317
- async prepareWithdraw(options) {
4318
- const copied = structuredClone(options);
4319
- const preparation = new Preparation(this.runtime(copied.tokenAddress));
4320
- const frame = await preparation.frame(copied.deadline);
4321
- const to = (0, import_ethers10.getAddress)(copied.to);
4322
- positiveAmount(copied.amount);
4323
- if (BigInt(to) === 0n) {
4324
- throw new Error("withdrawal destination must be nonzero");
4325
- }
4326
- return this.funds(
4327
- "withdraw",
4328
- [],
4329
- copied.amount,
4330
- to,
4331
- copied.amount,
4332
- frame,
4333
- { funding: {}, fundingMode: "none" },
4334
- copied.target,
4335
- preparation
4336
- );
4337
- }
4338
- /** Full payout to the note owner; no arbitrary destination, change or spending signature. */
4339
- async prepareSweep(options) {
4340
- const copied = structuredClone(options);
4341
- const runtime = this.runtime(copied.tokenAddress);
4342
- const state = await runtime.readInventory();
4343
- const available = Object.values(state.notes).filter(
4344
- (record) => record.note.tokenAddress === BigInt(copied.tokenAddress) && record.spent === "unspent" && !state.reservations[record.commitment]
4345
- );
4346
- const selected = copied.commitments ?? available.map((record) => record.commitment);
4347
- if (selected.length === 0) throw new Error("empty sweep input selection");
4348
- if (new Set(selected).size !== selected.length) throw new Error("duplicate sweep inputs");
4349
- const byId = new Map(available.map((record) => [record.commitment, record]));
4350
- let amount = 0n;
4351
- for (const id of selected) {
4352
- const record = byId.get(id);
4353
- if (!record) throw new Error("sweep input unavailable");
4354
- amount += record.note.amount;
4355
- }
4356
- positiveAmount(amount);
4357
- const preparation = new Preparation({
4358
- ...runtime,
4359
- inputSelection: selected
4360
- });
4361
- const frame = await preparation.frame(copied.deadline);
4362
- return this.funds(
4363
- "sweep",
4364
- [],
4365
- amount,
4366
- this.scope.walletIdentity,
4367
- amount,
4368
- frame,
4369
- { funding: {}, fundingMode: "none" },
4370
- copied.target,
4371
4706
  preparation
4372
4707
  );
4373
4708
  }
4374
- async funds(kind, payments, total, withdrawalAddress, withdrawalAmount, frame, funding, target, preparation) {
4709
+ async funds(payments, total, withdrawalAddress, withdrawalAmount, frame, funding, preparation) {
4375
4710
  if (total >= 1n << 135n) {
4376
4711
  throw new Error("transaction sum exceeds uint135");
4377
4712
  }
@@ -4381,7 +4716,7 @@ var TransactionPreparation = class {
4381
4716
  frame
4382
4717
  );
4383
4718
  const outputs = await Promise.all(
4384
- payments.map((payment) => preparation.output(payment, context))
4719
+ payments.map((payment, index2) => preparation.output(payment, context, false, index2))
4385
4720
  );
4386
4721
  const change = selection.amount + (funding.funding.amount ?? 0n) - total;
4387
4722
  if (change > 0n) {
@@ -4406,17 +4741,17 @@ var TransactionPreparation = class {
4406
4741
  deadline
4407
4742
  };
4408
4743
  const deliveries = outputs.map((output) => output.delivery);
4409
- const normalized = (0, import_protocol7.normalizeTransactRequest)(context, params, deliveries, funding.funding);
4410
- const statement2 = import_protocol7.WitnessBuilder.statement(
4744
+ const normalized = (0, import_protocol8.normalizeTransactRequest)(context, params, deliveries, funding.funding);
4745
+ const statement2 = import_protocol8.WitnessBuilder.statement(
4411
4746
  `transact_user_slots${normalized.capacity}`,
4412
- (0, import_protocol7.transactUserPublicInputs)(context, params, deliveries, funding.funding),
4413
- target ?? "evm"
4747
+ (0, import_protocol8.transactUserPublicInputs)(context, params, deliveries, funding.funding),
4748
+ "evm"
4414
4749
  );
4415
4750
  await preparation.assertBlock(frame);
4416
4751
  return preparation.runtime.complete({
4417
4752
  version: 1,
4418
4753
  id: crypto.randomUUID(),
4419
- kind,
4754
+ kind: "transfer",
4420
4755
  context,
4421
4756
  scope: this.scope,
4422
4757
  preparedAt: block,
@@ -4438,7 +4773,9 @@ var TransactionPreparation = class {
4438
4773
  throw new Error("shield requires 1..10 outputs");
4439
4774
  }
4440
4775
  const outputs = await Promise.all(
4441
- copied.outputs.map((output) => preparation.output(output, frame.context))
4776
+ copied.outputs.map(
4777
+ (output, index2) => preparation.output(output, frame.context, false, index2)
4778
+ )
4442
4779
  );
4443
4780
  if (outputs.reduce((sum, output) => sum + output.note.amount, 0n) !== (funding.funding.amount ?? 0n)) {
4444
4781
  throw new Error("shield funding must equal output total");
@@ -4543,10 +4880,10 @@ async function refreshWallet(runtime, options) {
4543
4880
  }
4544
4881
 
4545
4882
  // src/session/WalletUpdater.ts
4546
- var import_core12 = require("@invisi-labs/core");
4883
+ var import_core15 = require("@invisi-labs/core");
4547
4884
 
4548
4885
  // src/session/inventory.ts
4549
- var import_core11 = require("@invisi-labs/core");
4886
+ var import_core14 = require("@invisi-labs/core");
4550
4887
  var CanonicalConflict = class extends Error {
4551
4888
  };
4552
4889
  var CreditMetadataMismatch = class extends Error {
@@ -4598,10 +4935,10 @@ var InventoryProcessor = class {
4598
4935
  }
4599
4936
  async creditNullifier(state) {
4600
4937
  const n = state.note;
4601
- return (0, import_core11.computeCreditNullifier)(
4938
+ return (0, import_core14.computeCreditNullifier)(
4602
4939
  n.chainId,
4603
4940
  n.creditSharedKey,
4604
- await (0, import_core11.computeCreditCommitment)(
4941
+ await (0, import_core14.computeCreditCommitment)(
4605
4942
  n.chainId,
4606
4943
  n.spenderWalletIdentity,
4607
4944
  n.ownerWalletIdentity,
@@ -4611,7 +4948,7 @@ var InventoryProcessor = class {
4611
4948
  n.amount,
4612
4949
  n.nonce,
4613
4950
  n.expiry,
4614
- await (0, import_core11.computeCreditSharedKeyHash)(n.creditSharedKey),
4951
+ await (0, import_core14.computeCreditSharedKeyHash)(n.creditSharedKey),
4615
4952
  n
4616
4953
  )
4617
4954
  );
@@ -4675,7 +5012,7 @@ var InventoryProcessor = class {
4675
5012
  previous.state.note.expiry === params.expiries[index2],
4676
5013
  "draw recovery expiry mismatch"
4677
5014
  );
4678
- const remainder = await (0, import_core11.recoverCreditRemainder)(
5015
+ const remainder = await (0, import_core14.recoverCreditRemainder)(
4679
5016
  context,
4680
5017
  previous.state.note,
4681
5018
  {
@@ -4691,7 +5028,7 @@ var InventoryProcessor = class {
4691
5028
  const child = foundByKey.get(
4692
5029
  params.remainderCommitments[index2] + ":" + previous.role
4693
5030
  );
4694
- const memo = child?.state.note.memo;
5031
+ const payload = child?.state.note.payload;
4695
5032
  const record = {
4696
5033
  commitment: params.remainderCommitments[index2],
4697
5034
  treeId: BigInt(args.outputTreeId),
@@ -4704,7 +5041,7 @@ var InventoryProcessor = class {
4704
5041
  ...clone(previous.state),
4705
5042
  note: {
4706
5043
  ...remainder,
4707
- ...memo === void 0 ? {} : { memo: (0, import_core11.encodeMemo)(memo) }
5044
+ ...payload === void 0 ? {} : { payload: clone(payload) }
4708
5045
  }
4709
5046
  },
4710
5047
  receivingKeySource: { kind: "draw", commitment: previous.commitment },
@@ -4783,7 +5120,7 @@ var WalletUpdater = class extends InventoryProcessor {
4783
5120
  }
4784
5121
  async run(batch, options) {
4785
5122
  const { reader, protocol, scope, store, viewing } = this.#options;
4786
- const nullifier = import_core12.NullifierCapability.fromIdentity(scope.walletIdentity);
5123
+ const nullifier = import_core15.NullifierCapability.fromIdentity(scope.walletIdentity);
4787
5124
  const { signal, reset } = options;
4788
5125
  signal?.throwIfAborted();
4789
5126
  const context = protocol.context;
@@ -4952,6 +5289,7 @@ var WalletUpdater = class extends InventoryProcessor {
4952
5289
  draft.notes = {};
4953
5290
  draft.credits = {};
4954
5291
  delete draft.observedThrough;
5292
+ delete draft.discoveryThrough;
4955
5293
  delete draft.invalidated;
4956
5294
  }
4957
5295
  mergeReceiptRecords(draft, { notes, credits, checkpoint: block });
@@ -4983,13 +5321,38 @@ var WalletUpdater = class extends InventoryProcessor {
4983
5321
  var WalletSession = class {
4984
5322
  #options;
4985
5323
  #updater;
5324
+ #listeners = /* @__PURE__ */ new Set();
4986
5325
  #sending = /* @__PURE__ */ new Set();
4987
5326
  #refreshQueue = Promise.resolve();
4988
5327
  scope;
4989
5328
  protocol;
4990
5329
  constructor(options) {
5330
+ if ("ciphertextCodecs" in options || "envelopeVersion" in options) {
5331
+ throw new Error(
5332
+ "SDK envelope versions are fixed; encode application payload bytes externally"
5333
+ );
5334
+ }
5335
+ const store = options.store;
4991
5336
  this.#options = {
4992
5337
  ...options,
5338
+ ...store ? {
5339
+ store: {
5340
+ persistent: store.persistent,
5341
+ readSnapshot: (scope) => store.readSnapshot(scope),
5342
+ transaction: async (scope, action) => {
5343
+ const result = await store.transaction(scope, action);
5344
+ if (scopeKey(scope) === scopeKey(this.scope)) {
5345
+ for (const listener of [...this.#listeners]) {
5346
+ try {
5347
+ listener();
5348
+ } catch {
5349
+ }
5350
+ }
5351
+ }
5352
+ return result;
5353
+ }
5354
+ }
5355
+ } : {},
4993
5356
  ...options.gasBenchmarks ? { gasBenchmarks: copyGasBenchmarks(options.gasBenchmarks) } : {}
4994
5357
  };
4995
5358
  this.protocol = options.protocol;
@@ -5004,7 +5367,7 @@ var WalletSession = class {
5004
5367
  /** Delegate sending to ethers; managed operations additionally retain the resulting hash. */
5005
5368
  async sendTransaction(input, options = {}) {
5006
5369
  const signer = this.#options.signer;
5007
- if (!signer) throw new import_core13.MissingCapabilityError("ethers signer");
5370
+ if (!signer) throw new import_core17.MissingCapabilityError("ethers signer");
5008
5371
  const operation = input instanceof Operation ? input : void 0;
5009
5372
  if (operation && this.#sending.has(operation.id))
5010
5373
  throw new Error("operation is already being sent by this session");
@@ -5012,12 +5375,12 @@ var WalletSession = class {
5012
5375
  const overrides = { ...options };
5013
5376
  const plain = operation ? void 0 : { ...input };
5014
5377
  try {
5015
- if (!signer.provider) throw new import_core13.MissingCapabilityError("connected ethers signer");
5378
+ if (!signer.provider) throw new import_core17.MissingCapabilityError("connected ethers signer");
5016
5379
  if ((await signer.provider.getNetwork()).chainId !== this.protocol.context.chainId) {
5017
5380
  throw new Error("signer-provider session chain mismatch");
5018
5381
  }
5019
5382
  if (operation) {
5020
- if (!this.#options.store) throw new import_core13.MissingCapabilityError("wallet store");
5383
+ if (!this.#options.store) throw new import_core17.MissingCapabilityError("wallet store");
5021
5384
  const state = await this.#options.store.readSnapshot(this.scope);
5022
5385
  if (!state.operations[operation.id])
5023
5386
  throw new Error("operation does not belong to this wallet session");
@@ -5048,25 +5411,30 @@ var WalletSession = class {
5048
5411
  runtime(tokenAddress) {
5049
5412
  const options = this.#options;
5050
5413
  if (!options.store) {
5051
- throw new import_core13.MissingCapabilityError("wallet store");
5414
+ throw new import_core17.MissingCapabilityError("wallet store");
5052
5415
  }
5053
5416
  if (!options.chainReader) {
5054
- throw new import_core13.MissingCapabilityError("chain reader");
5417
+ throw new import_core17.MissingCapabilityError("chain reader");
5055
5418
  }
5056
5419
  return {
5057
5420
  options: { ...options, protocol: options.protocol.forToken(tokenAddress) },
5058
5421
  scope: this.scope,
5059
5422
  store: options.store,
5060
5423
  reader: options.chainReader,
5061
- authorization: () => options.authorization ?? options.wallet.exportAuthorization()
5424
+ authorization: () => {
5425
+ if (options.authorization) return options.authorization;
5426
+ if (options.wallet instanceof import_core16.Wallet) return options.wallet.exportAuthorization();
5427
+ throw new import_core17.MissingCapabilityError("wallet authorization");
5428
+ }
5062
5429
  };
5063
5430
  }
5064
5431
  updater() {
5065
5432
  if (this.#updater) return this.#updater;
5066
5433
  const options = this.#options;
5067
5434
  if (!options.store || !options.chainReader || !options.protocolRpc)
5068
- throw new import_core13.MissingCapabilityError("wallet store, chain reader and Protocol RPC");
5069
- const viewing = options.viewing ?? options.wallet.getViewer();
5435
+ throw new import_core17.MissingCapabilityError("wallet store, chain reader and Protocol RPC");
5436
+ const viewing = options.viewing ?? (options.wallet instanceof import_core16.Wallet ? options.wallet.getViewer() : void 0);
5437
+ if (!viewing) throw new import_core17.MissingCapabilityError("viewing");
5070
5438
  const receivingAddress = options.wallet.getReceivingAddress();
5071
5439
  const receivingKeys = receivingAddress.kind === "long" ? receivingAddress.getReceivingKeys() : void 0;
5072
5440
  this.#updater = new WalletUpdater({
@@ -5080,10 +5448,27 @@ var WalletSession = class {
5080
5448
  });
5081
5449
  return this.#updater;
5082
5450
  }
5451
+ /** Local session commits only. Host applications own cross-tab/store subscriptions. */
5452
+ subscribe(listener) {
5453
+ this.#listeners.add(listener);
5454
+ return () => {
5455
+ this.#listeners.delete(listener);
5456
+ };
5457
+ }
5458
+ /** Detached history for wallet UI; construction records do not imply chain success. */
5459
+ async getHistory(options = {}) {
5460
+ const tokenAddress = options.tokenAddress ? (0, import_ethers11.getAddress)(options.tokenAddress) : void 0;
5461
+ const store = this.#options.store;
5462
+ if (!store) throw new import_core17.MissingCapabilityError("wallet store");
5463
+ return walletHistory(
5464
+ await store.readSnapshot(this.scope),
5465
+ tokenAddress ? { tokenAddress } : {}
5466
+ );
5467
+ }
5083
5468
  async balance(tokenAddress) {
5084
5469
  const store = this.#options.store;
5085
5470
  if (!store) {
5086
- throw new import_core13.MissingCapabilityError("wallet store");
5471
+ throw new import_core17.MissingCapabilityError("wallet store");
5087
5472
  }
5088
5473
  return walletBalance(await store.readSnapshot(this.scope), (0, import_ethers11.getAddress)(tokenAddress));
5089
5474
  }
@@ -5091,15 +5476,15 @@ var WalletSession = class {
5091
5476
  async refresh(options) {
5092
5477
  const provider = this.#options.protocolRpc;
5093
5478
  const reader = this.#options.chainReader;
5094
- if (!reader) throw new import_core13.MissingCapabilityError("chain reader");
5095
- if (!provider) throw new import_core13.MissingCapabilityError("Protocol RPC");
5479
+ if (!reader) throw new import_core17.MissingCapabilityError("chain reader");
5480
+ if (!provider) throw new import_core17.MissingCapabilityError("Protocol RPC");
5096
5481
  const updater = this.updater();
5097
5482
  const request = {
5098
5483
  ...options,
5099
5484
  ...options.detectionKey ? { detectionKey: options.detectionKey.slice() } : {}
5100
5485
  };
5101
- const task = this.#refreshQueue.then(
5102
- () => refreshWallet(
5486
+ const task = this.#refreshQueue.then(async () => {
5487
+ const result = await refreshWallet(
5103
5488
  {
5104
5489
  provider,
5105
5490
  reader,
@@ -5108,8 +5493,21 @@ var WalletSession = class {
5108
5493
  apply: (batch, settings) => updater.applyUpdates(batch, settings)
5109
5494
  },
5110
5495
  request
5111
- )
5112
- );
5496
+ );
5497
+ await this.#options.store.transaction(this.scope, (state) => {
5498
+ request.signal?.throwIfAborted();
5499
+ if (state.invalidated) throw new Error("wallet requires canonical resync");
5500
+ const previous = state.discoveryThrough;
5501
+ const start = previous ? previous.number + 1n : this.protocol.deployment.protocol.fromBlock;
5502
+ if (request.fromBlock <= start && (!previous || result.block.number >= previous.number)) {
5503
+ state.discoveryThrough = {
5504
+ number: result.block.number,
5505
+ hash: result.block.hash
5506
+ };
5507
+ }
5508
+ });
5509
+ return result;
5510
+ });
5113
5511
  this.#refreshQueue = task.catch(() => {
5114
5512
  });
5115
5513
  return task;
@@ -5120,38 +5518,43 @@ var WalletSession = class {
5120
5518
  options: runtime.options,
5121
5519
  scope: runtime.scope,
5122
5520
  reader: runtime.reader,
5123
- authorization: runtime.authorization,
5124
5521
  readInventory: () => runtime.store.readSnapshot(runtime.scope),
5125
5522
  complete: (plan) => Operation.prepare(runtime, plan)
5126
5523
  });
5127
5524
  }
5128
5525
  async prepareTransfer(options) {
5129
- return this.preparation(options.tokenAddress).prepareTransfer(options);
5130
- }
5131
- async prepareWithdraw(options) {
5132
- return this.preparation(options.tokenAddress).prepareWithdraw(options);
5133
- }
5134
- async prepareSweep(options) {
5135
- return this.preparation(options.tokenAddress).prepareSweep(options);
5526
+ return prepareWithErrors("prepareTransfer", async () => {
5527
+ return this.preparation(options.tokenAddress).prepareTransfer(options);
5528
+ });
5136
5529
  }
5137
5530
  async prepareShield(options) {
5138
- return this.preparation(options.tokenAddress).prepareShield(options);
5531
+ return prepareWithErrors("prepareShield", async () => {
5532
+ return this.preparation(options.tokenAddress).prepareShield(options);
5533
+ });
5139
5534
  }
5140
5535
  async prepareCreateCredit(options) {
5141
- return this.preparation(options.tokenAddress).prepareCreateCredit(options);
5536
+ return prepareWithErrors("prepareCreateCredit", async () => {
5537
+ return this.preparation(options.tokenAddress).prepareCreateCredit(options);
5538
+ });
5142
5539
  }
5143
5540
  async prepareDrawCredit(options) {
5144
- return this.preparation(options.tokenAddress).prepareDrawCredit(options);
5541
+ return prepareWithErrors("prepareDrawCredit", async () => {
5542
+ return this.preparation(options.tokenAddress).prepareDrawCredit(options);
5543
+ });
5145
5544
  }
5146
5545
  async prepareTopUpCredit(options) {
5147
- return this.preparation(options.tokenAddress).prepareTopUpCredit(options);
5546
+ return prepareWithErrors("prepareTopUpCredit", async () => {
5547
+ return this.preparation(options.tokenAddress).prepareTopUpCredit(options);
5548
+ });
5148
5549
  }
5149
5550
  async prepareCancelCredit(options) {
5150
- return this.preparation(options.tokenAddress).prepareCancelCredit(options);
5551
+ return prepareWithErrors("prepareCancelCredit", async () => {
5552
+ return this.preparation(options.tokenAddress).prepareCancelCredit(options);
5553
+ });
5151
5554
  }
5152
5555
  async resume(operationId) {
5153
5556
  const store = this.#options.store;
5154
- if (!store) throw new import_core13.MissingCapabilityError("wallet store");
5557
+ if (!store) throw new import_core17.MissingCapabilityError("wallet store");
5155
5558
  const state = await store.readSnapshot(this.scope);
5156
5559
  const saved = state.operations[operationId]?.payload;
5157
5560
  if (!saved?.plan) throw new Error("operation not found");