@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/memory.cjs CHANGED
@@ -371,8 +371,9 @@ function readProtocolLogs(receipt, protocolAddress) {
371
371
  }
372
372
 
373
373
  // src/session/discovery.ts
374
- var import_ethers3 = require("ethers");
375
374
  var import_core = require("@invisi-labs/core");
375
+ var import_ethers3 = require("ethers");
376
+ var import_core2 = require("@invisi-labs/core");
376
377
  var import_protocol = require("@invisi-labs/protocol");
377
378
  var EVENTS = /* @__PURE__ */ new Set([
378
379
  "CommitmentsInserted",
@@ -466,7 +467,7 @@ function extractDeliveryCandidates(logs, context, confirmedThrough) {
466
467
  check2(inserted, "delivery has no matching canonical insertion");
467
468
  if (commitment !== void 0)
468
469
  check2(hash(commitment) === inserted, "delivery commitment conflicts with insertion");
469
- const delivery = (0, import_protocol.normalizeDelivery)(wire, { credit: role !== "funds" });
470
+ const delivery = (0, import_protocol.normalizeOpaqueDelivery)(wire);
470
471
  check2(
471
472
  typeof token === "string" && (0, import_ethers3.isAddress)(token) && BigInt(token) !== 0n,
472
473
  "invalid event token"
@@ -497,7 +498,7 @@ function extractDeliveryCandidates(logs, context, confirmedThrough) {
497
498
  add(log, tree, index(leaves[0]), fields.creditCommitment, "owner", ds[0]);
498
499
  add(log, tree, index(leaves[0]), fields.creditCommitment, "spender", ds[1]);
499
500
  if (hasChange) add(log, tree, index(leaves[1]), change, "funds", ds[2]);
500
- else (0, import_protocol.normalizeDelivery)(ds[2], { empty: true });
501
+ else (0, import_protocol.normalizeOpaqueDelivery)(ds[2], { empty: true });
501
502
  };
502
503
  for (const log of relevant) {
503
504
  const a = object(log.args);
@@ -527,7 +528,7 @@ function extractDeliveryCandidates(logs, context, confirmedThrough) {
527
528
  const length = Number.parseInt(packed.slice(offset, offset + 4), 16);
528
529
  offset += 4;
529
530
  check2(
530
- length >= 160 && length <= 416 && offset + length * 2 + 136 <= packed.length,
531
+ length >= 1 && length <= import_core.MAX_CIPHERTEXT_BYTES && offset + length * 2 + 136 <= packed.length,
531
532
  "invalid packed delivery length"
532
533
  );
533
534
  const ciphertext = `0x${packed.slice(offset, offset + length * 2)}`;
@@ -549,8 +550,8 @@ function extractDeliveryCandidates(logs, context, confirmedThrough) {
549
550
  cs.forEach((c, i) => {
550
551
  if (BigInt(hash(c)) === 0n) {
551
552
  check2(integer(leaves[i]) === 0n, "absent remainder has nonzero position");
552
- (0, import_protocol.normalizeDelivery)(ds[2 * i], { empty: true });
553
- (0, import_protocol.normalizeDelivery)(ds[2 * i + 1], { empty: true });
553
+ (0, import_protocol.normalizeOpaqueDelivery)(ds[2 * i], { empty: true });
554
+ (0, import_protocol.normalizeOpaqueDelivery)(ds[2 * i + 1], { empty: true });
554
555
  } else {
555
556
  add(log, tree, index(leaves[i]), c, "owner", ds[2 * i]);
556
557
  add(log, tree, index(leaves[i]), c, "spender", ds[2 * i + 1]);
@@ -562,7 +563,7 @@ function extractDeliveryCandidates(logs, context, confirmedThrough) {
562
563
  }
563
564
  async function decryptCandidates(logs, options) {
564
565
  contextCheck(options.context);
565
- (0, import_core.decodeAddress)(import_core.ReceivingAddress.fromIdentity(options.viewing.walletIdentity).toString());
566
+ (0, import_core2.decodeAddress)(import_core2.ReceivingAddress.fromIdentity(options.viewing.walletIdentity).toString());
566
567
  check2(
567
568
  !logs.some(
568
569
  (log) => log.address.toLowerCase() === options.context.protocolAddress.toLowerCase() && log.removed
@@ -904,7 +905,7 @@ function walletBalances(state) {
904
905
  }
905
906
 
906
907
  // src/session/store.ts
907
- var import_core2 = require("@invisi-labs/core");
908
+ var import_core3 = require("@invisi-labs/core");
908
909
  var import_sha256 = require("@noble/hashes/sha256");
909
910
  var import_utils = require("@noble/hashes/utils");
910
911
  function scopeKey(scope) {
@@ -920,7 +921,7 @@ function scopeKey(scope) {
920
921
  return (0, import_utils.bytesToHex)(
921
922
  (0, import_sha256.sha256)(
922
923
  (0, import_utils.utf8ToBytes)(
923
- "Invisi.WalletScope.v2:" + JSON.stringify([
924
+ "Invisi.WalletScope.v1:" + JSON.stringify([
924
925
  scope.namespace,
925
926
  scope.chainId.toString(),
926
927
  scope.protocolAddress.toLowerCase(),
@@ -957,12 +958,12 @@ function validateState(state, scope, revision) {
957
958
  throw new Error("invalid spent state");
958
959
  }
959
960
  for (const record of Object.values(state.credits)) {
960
- const note = (0, import_core2.normalizeCreditDeliveryState)(record.state).note;
961
+ const note = (0, import_core3.normalizeCreditDeliveryState)(record.state).note;
961
962
  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))
962
963
  throw new Error("invalid stored Credit context or role");
963
964
  }
964
965
  for (const record of [...Object.values(state.notes), ...Object.values(state.credits)]) {
965
- 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))
966
+ 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))
966
967
  throw new Error("invalid stored receipt position");
967
968
  }
968
969
  for (const [id, operation] of Object.entries(state.reservations))
@@ -1035,14 +1036,11 @@ var MemoryAtomicRecords = class {
1035
1036
  };
1036
1037
 
1037
1038
  // src/session/operation-protocol.ts
1038
- var import_core3 = require("@invisi-labs/core");
1039
+ var import_core4 = require("@invisi-labs/core");
1039
1040
  var import_protocol2 = require("@invisi-labs/protocol");
1040
1041
  function requiresAuthorization(plan) {
1041
- if (plan.kind === "sweep") {
1042
- 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))
1043
- throw new Error("invalid owner-only sweep plan");
1044
- return false;
1045
- }
1042
+ 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);
1043
+ if (ownerOnlyFullWithdrawal) return false;
1046
1044
  return !!plan.credit || plan.inputs.length > 0;
1047
1045
  }
1048
1046
  function inputFacts(plan) {
@@ -1306,7 +1304,7 @@ function operationExpired(plan, now) {
1306
1304
  if (credit?.creation && now >= credit.creation.expiry) return true;
1307
1305
  if (credit?.draw) {
1308
1306
  if (credit.draw.expiries.some((expiry) => now >= expiry)) return true;
1309
- const prepared = (0, import_core3.creditPeriods)(plan.preparedAt.timestamp), current2 = (0, import_core3.creditPeriods)(now);
1307
+ const prepared = (0, import_core4.creditPeriods)(plan.preparedAt.timestamp), current2 = (0, import_core4.creditPeriods)(now);
1310
1308
  if (prepared.currentDay !== current2.currentDay || prepared.currentMonth !== current2.currentMonth)
1311
1309
  return true;
1312
1310
  }
@@ -2805,7 +2803,8 @@ function estimateGasReference(plan, protocol, profiles) {
2805
2803
 
2806
2804
  // src/session/operation-execution.ts
2807
2805
  var import_ethers6 = require("ethers");
2808
- var import_core4 = require("@invisi-labs/core");
2806
+ var import_core5 = require("@invisi-labs/core");
2807
+ var import_protocol3 = require("@invisi-labs/protocol");
2809
2808
 
2810
2809
  // src/session/proof-integrity.ts
2811
2810
  var import_ethers5 = require("ethers");
@@ -2834,23 +2833,41 @@ async function authorizeOperation(plan, provider) {
2834
2833
  if (provider.walletIdentity !== plan.scope.walletIdentity)
2835
2834
  throw new Error("wallet authorization capability mismatch");
2836
2835
  const request = authorizationRequest(plan);
2837
- const digest = (0, import_core4.computeWalletAuthorizationDigest)(request);
2836
+ const digest = (0, import_core5.computeWalletAuthorizationDigest)(request);
2838
2837
  const signed = await provider.authorize(structuredClone(request));
2839
2838
  if (signed.walletIdentity !== plan.scope.walletIdentity || signed.digest !== digest)
2840
2839
  throw new Error("wallet authorization statement mismatch");
2841
2840
  return signed;
2842
2841
  }
2843
2842
  async function proveOperation(protocol, plan, authorization, prover, options) {
2843
+ if (Object.hasOwn(options, "target")) {
2844
+ throw new Error("prove accepts execution, not a backend target");
2845
+ }
2846
+ const { execution = "single", ...proverOptions } = options;
2847
+ if (execution !== "single" && execution !== "aggregate") {
2848
+ throw new Error("execution must be single or aggregate");
2849
+ }
2850
+ const frozen = statement(plan);
2851
+ if (execution === "aggregate" && (plan.kind !== "transfer" || plan.capacity !== 14)) {
2852
+ throw new Error("aggregate execution requires a 14-slot transfer");
2853
+ }
2854
+ if (plan.kind === "transfer") {
2855
+ plan.statement = import_protocol3.WitnessBuilder.statement(
2856
+ frozen.circuitName,
2857
+ frozen.expectedPublicInputs,
2858
+ execution === "aggregate" ? "noir-recursive" : "evm"
2859
+ );
2860
+ }
2844
2861
  const request = await witnessFor(protocol, plan, authorization);
2845
2862
  if (JSON.stringify({ ...request, witness: void 0 }) !== JSON.stringify(plan.statement))
2846
2863
  throw new Error("prepared proof statement changed");
2847
2864
  try {
2848
- const proof = publicProof(await prover.prove(request, options));
2865
+ const proof = publicProof(await prover.prove(request, proverOptions));
2849
2866
  sameStatement(proof, plan.statement);
2850
2867
  if (await prover.verify(
2851
2868
  structuredClone(proof),
2852
2869
  structuredClone(plan.statement),
2853
- options
2870
+ proverOptions
2854
2871
  ) !== true)
2855
2872
  throw new Error("proof verification failed");
2856
2873
  return proof;
@@ -2865,19 +2882,88 @@ async function signOperationFunding(saved, signer, verifier) {
2865
2882
  throw new Error("funding signer is not the payer");
2866
2883
  const typed = await fundingTypedData(saved);
2867
2884
  const signature = await signer.signTypedData(structuredClone(typed));
2885
+ return verifyFundingSignature(typed, payer, signature, verifier);
2886
+ }
2887
+ function walletSigningRequest(plan) {
2888
+ return (0, import_core5.createSigningRequest)((0, import_core5.walletAuthorizationTypedData)(authorizationRequest(plan)));
2889
+ }
2890
+ async function acceptWalletSignature(plan, signature) {
2891
+ const request = authorizationRequest(plan);
2892
+ const digest = (0, import_core5.computeWalletAuthorizationDigest)(request);
2893
+ try {
2894
+ const signed = import_ethers6.Signature.from(signature);
2895
+ const publicKey = (0, import_ethers6.getBytes)(import_ethers6.SigningKey.recoverPublicKey(digest, signed));
2896
+ const owner = await (0, import_core5.computeWalletIdentity)(publicKey);
2897
+ if (owner.toLowerCase() !== plan.scope.walletIdentity.toLowerCase()) {
2898
+ throw new Error("wallet signature identity mismatch");
2899
+ }
2900
+ return new import_core5.AuthorizedStatement(plan.scope.walletIdentity, digest, {
2901
+ public_key_x: Array.from(publicKey.slice(1, 33)),
2902
+ public_key_y: Array.from(publicKey.slice(33)),
2903
+ signature: Array.from((0, import_ethers6.getBytes)((0, import_ethers6.concat)([signed.r, signed.s])))
2904
+ });
2905
+ } catch {
2906
+ throw new Error("invalid wallet authorization signature");
2907
+ }
2908
+ }
2909
+ async function fundingSigningRequest(saved, runtime) {
2910
+ const plan = saved.plan;
2911
+ if (plan.fundingMode !== "signature") {
2912
+ throw new Error("operation does not require a funding signature");
2913
+ }
2914
+ if (plan.kind !== "shield" && !saved.proof) {
2915
+ throw new Error("proof must precede funding authorization");
2916
+ }
2917
+ await runtime.options.protocol.validateDeployment(runtime.reader);
2918
+ const block = await runtime.reader.getBlock(await runtime.reader.getLatestBlockNumber());
2919
+ if (block.timestamp > plan.deadline || block.timestamp > (plan.funding.deadline ?? 0n)) {
2920
+ throw new Error("funding authorization expired");
2921
+ }
2922
+ if (await runtime.reader.getFundingNonce(plan.context, plan.funding.payer, block) !== plan.funding.sigNonce) {
2923
+ throw new Error("funding nonce changed; prepare again");
2924
+ }
2925
+ const request = (0, import_core5.createSigningRequest)(await fundingTypedData(saved));
2926
+ if ((await runtime.reader.getBlock(block.number)).hash !== block.hash) {
2927
+ throw new Error("canonical funding block changed");
2928
+ }
2929
+ return request;
2930
+ }
2931
+ async function acceptFundingSignature(saved, signature, runtime) {
2932
+ if (!/^0x(?:[a-fA-F0-9]{2})+$/.test(signature)) {
2933
+ throw new Error("invalid EVM funding signature");
2934
+ }
2935
+ const request = await fundingSigningRequest(saved, runtime);
2936
+ return verifyFundingSignature(
2937
+ request,
2938
+ saved.plan.funding.payer,
2939
+ signature,
2940
+ runtime.options.fundingVerifier
2941
+ );
2942
+ }
2943
+ async function verifyFundingSignature(request, payer, signature, verifier) {
2944
+ if (!/^0x(?:[a-fA-F0-9]{2})+$/.test(signature)) {
2945
+ throw new Error("invalid EVM funding signature");
2946
+ }
2947
+ const types = Object.fromEntries(
2948
+ Object.entries(request.types).map(([name, fields]) => [name, [...fields]])
2949
+ );
2950
+ let validEOA = false;
2951
+ try {
2952
+ validEOA = (0, import_ethers6.verifyTypedData)(request.domain, types, request.message, signature).toLowerCase() === payer.toLowerCase();
2953
+ if (validEOA) {
2954
+ signature = import_ethers6.Signature.from(signature).serialized;
2955
+ }
2956
+ } catch {
2957
+ validEOA = false;
2958
+ }
2868
2959
  const valid = verifier ? await verifier.verifyTypedData({
2869
- ...structuredClone(typed),
2960
+ ...structuredClone(request),
2870
2961
  address: payer,
2871
2962
  signature
2872
- }) : (0, import_ethers6.verifyTypedData)(
2873
- typed.domain,
2874
- Object.fromEntries(
2875
- Object.entries(typed.types).map(([name, fields]) => [name, [...fields]])
2876
- ),
2877
- typed.message,
2878
- signature
2879
- ).toLowerCase() === payer.toLowerCase();
2880
- if (valid !== true) throw new Error("invalid EVM funding signature");
2963
+ }) : validEOA;
2964
+ if (valid !== true) {
2965
+ throw new Error("invalid EVM funding signature");
2966
+ }
2881
2967
  return signature;
2882
2968
  }
2883
2969
 
@@ -2918,16 +3004,16 @@ function operationPreview(plan, status) {
2918
3004
  }
2919
3005
 
2920
3006
  // src/session/Operation.ts
2921
- var import_core8 = require("@invisi-labs/core");
2922
- var import_protocol4 = require("@invisi-labs/protocol");
3007
+ var import_core9 = require("@invisi-labs/core");
3008
+ var import_protocol5 = require("@invisi-labs/protocol");
2923
3009
 
2924
3010
  // src/session/operation-confirmation.ts
2925
- var import_core7 = require("@invisi-labs/core");
3011
+ var import_core8 = require("@invisi-labs/core");
2926
3012
 
2927
3013
  // src/session/aggregate-receipt.ts
2928
3014
  var import_ethers7 = require("ethers");
2929
3015
  var import_contracts3 = require("@invisi-labs/protocol/contracts");
2930
- var import_protocol3 = require("@invisi-labs/protocol");
3016
+ var import_protocol4 = require("@invisi-labs/protocol");
2931
3017
  var aggregateAbi = import_contracts3.invisiProtocolAbi.filter(
2932
3018
  (item) => item.type === "function" && item.name === "transact" && item.inputs.length === 2 && item.inputs[1]?.type === "tuple[]"
2933
3019
  );
@@ -2955,7 +3041,7 @@ function matchesAggregateTransaction(saved, transaction) {
2955
3041
  });
2956
3042
  if (decoded.name !== "transact" || !Array.isArray(requests) || ![4, 8, 12].includes(requests.length))
2957
3043
  return false;
2958
- const canonical2 = (request) => (0, import_protocol3.buildTransactCalldata)({
3044
+ const canonical2 = (request) => (0, import_protocol4.buildTransactCalldata)({
2959
3045
  context: { ...plan.context, tokenAddress: request.params.token },
2960
3046
  proof: "0x00",
2961
3047
  params: request.params,
@@ -2963,7 +3049,7 @@ function matchesAggregateTransaction(saved, transaction) {
2963
3049
  funding: request.funding,
2964
3050
  caller: transaction.from
2965
3051
  }).data.toLowerCase();
2966
- const expected = (0, import_protocol3.buildTransactCalldata)({
3052
+ const expected = (0, import_protocol4.buildTransactCalldata)({
2967
3053
  context: plan.context,
2968
3054
  proof: "0x00",
2969
3055
  params: plan.params,
@@ -2978,10 +3064,10 @@ function matchesAggregateTransaction(saved, transaction) {
2978
3064
  }
2979
3065
 
2980
3066
  // src/session/credit-values.ts
2981
- var import_core5 = require("@invisi-labs/core");
3067
+ var import_core6 = require("@invisi-labs/core");
2982
3068
  async function creditLeaf(state) {
2983
3069
  const n = state.note;
2984
- return (0, import_core5.computeCreditCommitment)(
3070
+ return (0, import_core6.computeCreditCommitment)(
2985
3071
  n.chainId,
2986
3072
  n.spenderWalletIdentity,
2987
3073
  n.ownerWalletIdentity,
@@ -2991,23 +3077,23 @@ async function creditLeaf(state) {
2991
3077
  n.amount,
2992
3078
  n.nonce,
2993
3079
  n.expiry,
2994
- await (0, import_core5.computeCreditSharedKeyHash)(n.creditSharedKey),
3080
+ await (0, import_core6.computeCreditSharedKeyHash)(n.creditSharedKey),
2995
3081
  n
2996
3082
  );
2997
3083
  }
2998
3084
 
2999
3085
  // src/session/membership.ts
3000
- var import_core6 = require("@invisi-labs/core");
3086
+ var import_core7 = require("@invisi-labs/core");
3001
3087
  async function verifyMembership(reader, context, block, input, proof) {
3002
3088
  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))
3003
3089
  throw new Error("membership context/root mismatch");
3004
3090
  let root = BigInt(input.commitment);
3005
3091
  for (let i = 0; i < 16; i++) {
3006
3092
  const sibling = proof.path[i];
3007
- if (typeof sibling !== "bigint" || sibling < 0n || sibling >= import_core6.FIELD_MODULUS)
3093
+ if (typeof sibling !== "bigint" || sibling < 0n || sibling >= import_core7.FIELD_MODULUS)
3008
3094
  throw new Error("noncanonical membership path");
3009
3095
  root = BigInt(
3010
- await (0, import_core6.poseidonHash)(input.leafIndex >> i & 1 ? [sibling, root] : [root, sibling])
3096
+ await (0, import_core7.poseidonHash)(input.leafIndex >> i & 1 ? [sibling, root] : [root, sibling])
3011
3097
  );
3012
3098
  }
3013
3099
  if (root !== BigInt(proof.root))
@@ -3179,7 +3265,7 @@ async function confirmOperation(runtime, id, generation, hash2, signal) {
3179
3265
  if (credits.length) {
3180
3266
  const nullifiers = await Promise.all(
3181
3267
  credits.map(
3182
- async (record) => (0, import_core7.computeCreditNullifier)(
3268
+ async (record) => (0, import_core8.computeCreditNullifier)(
3183
3269
  plan.context.chainId,
3184
3270
  record.state.note.creditSharedKey,
3185
3271
  await creditLeaf(record.state)
@@ -3480,6 +3566,81 @@ var Operation = class _Operation {
3480
3566
  throw error;
3481
3567
  }
3482
3568
  }
3569
+ async signingCurrent() {
3570
+ await this.#runtime.store.transaction(this.#runtime.scope, (state) => {
3571
+ check6(!state.invalidated, "wallet state requires canonical resync");
3572
+ this.current(state.operations[this.id]);
3573
+ });
3574
+ }
3575
+ /** Public EIP-712 request; signing transport and key custody belong to the caller. */
3576
+ async exportWalletAuthorization(options = {}) {
3577
+ options.signal?.throwIfAborted();
3578
+ await this.signingCurrent();
3579
+ check6(
3580
+ requiresAuthorization(this.#saved.plan),
3581
+ "operation does not require wallet authorization"
3582
+ );
3583
+ check6(
3584
+ ["prepared", "authorized"].includes(this.status),
3585
+ "operation is not awaiting wallet authorization"
3586
+ );
3587
+ return walletSigningRequest(this.#saved.plan);
3588
+ }
3589
+ /** Attach an external EOA signature, retaining its witness only at the private proving boundary. */
3590
+ async withWalletSignature(signature, options = {}) {
3591
+ options.signal?.throwIfAborted();
3592
+ await this.signingCurrent();
3593
+ check6(
3594
+ requiresAuthorization(this.#saved.plan),
3595
+ "operation does not require wallet authorization"
3596
+ );
3597
+ check6(
3598
+ ["prepared", "authorized"].includes(this.status),
3599
+ "operation is not awaiting wallet authorization"
3600
+ );
3601
+ const authorization = await acceptWalletSignature(this.#saved.plan, signature);
3602
+ return this.stage(
3603
+ "wallet authorization",
3604
+ ["prepared", "authorized"],
3605
+ async () => ({ status: "authorized", authorization }),
3606
+ options.signal
3607
+ );
3608
+ }
3609
+ /** Export only after proving when the funding authorization binds an actual proof hash. */
3610
+ async exportFundingAuthorization(options = {}) {
3611
+ options.signal?.throwIfAborted();
3612
+ await this.signingCurrent();
3613
+ check6(
3614
+ this.status === (this.kind === "shield" ? "prepared" : "proved"),
3615
+ "operation is not awaiting funding authorization or proof"
3616
+ );
3617
+ const request = await fundingSigningRequest(this.#saved, this.#runtime);
3618
+ options.signal?.throwIfAborted();
3619
+ await this.signingCurrent();
3620
+ return request;
3621
+ }
3622
+ /** Verify an external signature against this handle's own frozen operation. */
3623
+ async withFundingSignature(signature, options = {}) {
3624
+ options.signal?.throwIfAborted();
3625
+ await this.signingCurrent();
3626
+ check6(
3627
+ this.status === (this.kind === "shield" ? "prepared" : "proved"),
3628
+ "operation is not awaiting funding authorization or proof"
3629
+ );
3630
+ const verified = await acceptFundingSignature(this.#saved, signature, this.#runtime);
3631
+ return this.stage(
3632
+ "funding authorization",
3633
+ this.kind === "shield" ? ["prepared"] : ["proved"],
3634
+ async (saved) => {
3635
+ saved.plan.funding.signature = verified;
3636
+ if (saved.plan.statement?.target !== "noir-recursive") {
3637
+ saved.call = await this.encode(saved);
3638
+ }
3639
+ return { status: "ready" };
3640
+ },
3641
+ options.signal
3642
+ );
3643
+ }
3483
3644
  async authorize(options = {}) {
3484
3645
  check6(
3485
3646
  requiresAuthorization(this.#saved.plan),
@@ -3500,7 +3661,8 @@ var Operation = class _Operation {
3500
3661
  options.signal
3501
3662
  );
3502
3663
  }
3503
- async prove(options = {}) {
3664
+ async prove(suppliedOptions = {}) {
3665
+ const options = { ...suppliedOptions };
3504
3666
  const plan = this.#saved.plan;
3505
3667
  check6(plan.kind !== "shield", "shield requires no proof");
3506
3668
  if (requiresAuthorization(plan)) {
@@ -3511,7 +3673,7 @@ var Operation = class _Operation {
3511
3673
  }
3512
3674
  const prover = this.#runtime.options.prover;
3513
3675
  if (!prover) {
3514
- throw new import_core8.MissingCapabilityError("prover");
3676
+ throw new import_core9.MissingCapabilityError("prover");
3515
3677
  }
3516
3678
  return this.stage(
3517
3679
  "proof",
@@ -3545,7 +3707,7 @@ var Operation = class _Operation {
3545
3707
  check6(p.kind === "shield" || this.#saved.proof, "proof must precede funding authorization");
3546
3708
  const signer = this.#runtime.options.fundingSigner;
3547
3709
  if (!signer) {
3548
- throw new import_core8.MissingCapabilityError("EVM funding signer");
3710
+ throw new import_core9.MissingCapabilityError("EVM funding signer");
3549
3711
  }
3550
3712
  check6(
3551
3713
  signer.address.toLowerCase() === p.funding.payer.toLowerCase(),
@@ -3647,7 +3809,7 @@ var Operation = class _Operation {
3647
3809
  request: {
3648
3810
  params: structuredClone(plan.params),
3649
3811
  deliveries: plan.outputs.map((output) => structuredClone(output.delivery)),
3650
- funding: (0, import_protocol4.normalizeFunding)(plan.funding)
3812
+ funding: (0, import_protocol5.normalizeFunding)(plan.funding)
3651
3813
  }
3652
3814
  };
3653
3815
  });
@@ -3734,18 +3896,181 @@ var Operation = class _Operation {
3734
3896
  }
3735
3897
  };
3736
3898
 
3899
+ // src/session/WalletSession.ts
3900
+ var import_core16 = require("@invisi-labs/core");
3901
+
3902
+ // src/session/history.ts
3903
+ function walletHistory(state, options = {}) {
3904
+ const token = options.tokenAddress?.toLowerCase();
3905
+ const outgoing = [];
3906
+ for (const record of Object.values(state.operations)) {
3907
+ const saved = record.payload;
3908
+ if (!saved?.plan || token && saved.plan.context.tokenAddress.toLowerCase() !== token) {
3909
+ continue;
3910
+ }
3911
+ const plan = saved.plan;
3912
+ outgoing.push({
3913
+ ...operationPreview(plan, record.status),
3914
+ preparedAt: { ...plan.preparedAt },
3915
+ ...record.transactionHash ? { transactionHash: record.transactionHash } : {},
3916
+ outputs: plan.outputs.map((output) => ({
3917
+ to: output.address,
3918
+ amount: output.note.amount,
3919
+ change: output.change,
3920
+ commitment: output.commitment,
3921
+ payload: Uint8Array.from(output.note.payload ?? [])
3922
+ }))
3923
+ });
3924
+ }
3925
+ outgoing.sort(
3926
+ (a, b) => a.preparedAt.number === b.preparedAt.number ? a.id.localeCompare(b.id) : a.preparedAt.number > b.preparedAt.number ? -1 : 1
3927
+ );
3928
+ const incoming = [];
3929
+ for (const record of Object.values(state.notes)) {
3930
+ const tokenAddress = `0x${record.note.tokenAddress.toString(16).padStart(40, "0")}`;
3931
+ if (token && tokenAddress !== token) continue;
3932
+ incoming.push({
3933
+ commitment: record.commitment,
3934
+ treeId: record.treeId,
3935
+ leafIndex: record.leafIndex,
3936
+ blockNumber: record.blockNumber,
3937
+ blockHash: record.blockHash,
3938
+ ...record.transactionHash ? { transactionHash: record.transactionHash } : {},
3939
+ tokenAddress,
3940
+ amount: record.note.amount,
3941
+ payload: Uint8Array.from(record.note.payload ?? []),
3942
+ spent: record.spent
3943
+ });
3944
+ }
3945
+ incoming.sort(
3946
+ (a, b) => a.blockNumber === b.blockNumber ? a.commitment.localeCompare(b.commitment) : a.blockNumber > b.blockNumber ? -1 : 1
3947
+ );
3948
+ return structuredClone({
3949
+ revision: state.revision,
3950
+ checkedThrough: state.discoveryThrough ?? null,
3951
+ invalidated: !!state.invalidated,
3952
+ outgoing,
3953
+ incoming
3954
+ });
3955
+ }
3956
+
3957
+ // src/errors.ts
3958
+ var import_core10 = require("@invisi-labs/core");
3959
+ var InvisiError = class extends Error {
3960
+ code;
3961
+ operation;
3962
+ address;
3963
+ outputIndex;
3964
+ field;
3965
+ capability;
3966
+ constructor(code, message, details = {}) {
3967
+ super(message, { cause: details.cause });
3968
+ this.name = "InvisiError";
3969
+ this.code = code;
3970
+ this.operation = details.operation;
3971
+ this.address = details.address;
3972
+ this.outputIndex = details.outputIndex;
3973
+ this.field = details.field;
3974
+ this.capability = details.capability;
3975
+ }
3976
+ };
3977
+ async function prepareWithErrors(operation, prepare) {
3978
+ try {
3979
+ return await prepare();
3980
+ } catch (cause) {
3981
+ if (cause instanceof InvisiError) {
3982
+ if (cause.operation) throw cause;
3983
+ throw new InvisiError(cause.code, cause.message, {
3984
+ operation,
3985
+ ...cause.address === void 0 ? {} : { address: cause.address },
3986
+ ...cause.outputIndex === void 0 ? {} : { outputIndex: cause.outputIndex },
3987
+ ...cause.field === void 0 ? {} : { field: cause.field },
3988
+ ...cause.capability === void 0 ? {} : { capability: cause.capability },
3989
+ cause: cause.cause
3990
+ });
3991
+ }
3992
+ throw new InvisiError(
3993
+ cause instanceof import_core10.MissingCapabilityError ? "MISSING_CAPABILITY" : "PREPARATION_FAILED",
3994
+ cause instanceof Error ? cause.message : "Transaction preparation failed",
3995
+ {
3996
+ operation,
3997
+ cause,
3998
+ ...cause instanceof import_core10.MissingCapabilityError ? { capability: cause.capability } : {}
3999
+ }
4000
+ );
4001
+ }
4002
+ }
4003
+
3737
4004
  // src/session/WalletSession.ts
3738
4005
  var import_ethers11 = require("ethers");
3739
- var import_core13 = require("@invisi-labs/core");
4006
+ var import_core17 = require("@invisi-labs/core");
3740
4007
 
3741
4008
  // src/session/transaction-preparation.ts
3742
4009
  var import_ethers10 = require("ethers");
3743
- var import_protocol7 = require("@invisi-labs/protocol");
4010
+ var import_protocol8 = require("@invisi-labs/protocol");
4011
+
4012
+ // src/resolve-recipient.ts
4013
+ var import_core11 = require("@invisi-labs/core");
4014
+ async function resolveRecipient(address2, lookup, details = {}) {
4015
+ const context = { ...details, address: address2 };
4016
+ let recipient;
4017
+ try {
4018
+ recipient = import_core11.ReceivingAddress.parse(address2);
4019
+ } catch (cause) {
4020
+ throw new InvisiError("INVALID_RECIPIENT", "Invalid recipient address", {
4021
+ ...context,
4022
+ cause
4023
+ });
4024
+ }
4025
+ if (recipient.kind === "long") return recipient;
4026
+ if (!lookup) {
4027
+ throw new InvisiError("MISSING_CAPABILITY", "Missing registry lookup capability", {
4028
+ ...context,
4029
+ capability: "registry lookup"
4030
+ });
4031
+ }
4032
+ let keys;
4033
+ try {
4034
+ keys = await lookup(recipient.walletIdentity);
4035
+ } catch (cause) {
4036
+ if (cause instanceof import_core11.MissingCapabilityError) {
4037
+ throw new InvisiError("MISSING_CAPABILITY", cause.message, {
4038
+ ...context,
4039
+ cause,
4040
+ capability: cause.capability
4041
+ });
4042
+ }
4043
+ throw new InvisiError("RECIPIENT_LOOKUP_FAILED", "Recipient registry lookup failed", {
4044
+ ...context,
4045
+ cause
4046
+ });
4047
+ }
4048
+ if (keys == null) {
4049
+ throw new InvisiError(
4050
+ "EVM_ADDRESS_NOT_REGISTERED",
4051
+ "EVM address has no registered receiving keys",
4052
+ context
4053
+ );
4054
+ }
4055
+ try {
4056
+ return import_core11.ReceivingAddress.from({
4057
+ walletIdentity: recipient.walletIdentity,
4058
+ viewingPublicKey: keys.viewingPublicKey,
4059
+ clueKey: keys.clueKey
4060
+ });
4061
+ } catch (cause) {
4062
+ throw new InvisiError(
4063
+ "RECIPIENT_LOOKUP_FAILED",
4064
+ "Registry returned invalid receiving keys",
4065
+ { ...context, cause }
4066
+ );
4067
+ }
4068
+ }
3744
4069
 
3745
4070
  // src/session/prepare-common.ts
3746
4071
  var import_ethers8 = require("ethers");
3747
- var import_core9 = require("@invisi-labs/core");
3748
- var import_protocol5 = require("@invisi-labs/protocol");
4072
+ var import_core12 = require("@invisi-labs/core");
4073
+ var import_protocol6 = require("@invisi-labs/protocol");
3749
4074
 
3750
4075
  // src/session/selection.ts
3751
4076
  function selectNotes(notes, target, reserved) {
@@ -3778,6 +4103,27 @@ function positiveAmount(value, bits = 128) {
3778
4103
  }
3779
4104
  return value;
3780
4105
  }
4106
+ function copyApplicationPayload(value) {
4107
+ if (value === void 0) return new Uint8Array();
4108
+ if (!(value instanceof Uint8Array)) throw new TypeError("payload must be Uint8Array");
4109
+ return Uint8Array.from(value);
4110
+ }
4111
+ function positiveOutputAmount(payment, outputIndex) {
4112
+ try {
4113
+ return positiveAmount(payment.amount);
4114
+ } catch (cause) {
4115
+ throw new InvisiError(
4116
+ "PREPARATION_FAILED",
4117
+ cause instanceof Error ? cause.message : "Invalid output amount",
4118
+ {
4119
+ address: payment.to,
4120
+ field: "amount",
4121
+ ...outputIndex === void 0 ? {} : { outputIndex },
4122
+ cause
4123
+ }
4124
+ );
4125
+ }
4126
+ }
3781
4127
  var Preparation = class {
3782
4128
  constructor(runtime) {
3783
4129
  this.runtime = runtime;
@@ -3831,7 +4177,7 @@ var Preparation = class {
3831
4177
  if (mode === "caller" && (source.sigNonce || source.deadline)) {
3832
4178
  throw new Error("caller funding uses zero nonce/deadline");
3833
4179
  }
3834
- const funding = (0, import_protocol5.normalizeFunding)({
4180
+ const funding = (0, import_protocol6.normalizeFunding)({
3835
4181
  payer,
3836
4182
  amount: source.amount,
3837
4183
  sigNonce: mode === "caller" ? 0n : source.sigNonce ?? await this.runtime.reader.getFundingNonce(
@@ -3847,52 +4193,70 @@ var Preparation = class {
3847
4193
  }
3848
4194
  return { funding, fundingMode: mode };
3849
4195
  }
3850
- async output(payment, context, change = false) {
3851
- positiveAmount(payment.amount);
3852
- const recipient = await import_core9.ReceivingAddress.parse(payment.to).resolve(
3853
- this.runtime.options.addressResolver
3854
- );
3855
- const memo = (0, import_core9.encodeMemo)(payment.memo);
3856
- const nonce = (0, import_core9.generateNonce)();
3857
- const identity = await (0, import_core9.computeNoteIdentity)(BigInt(recipient.walletIdentity), nonce);
3858
- const keys = recipient.getReceivingKeys();
3859
- const note = {
3860
- chainId: context.chainId,
3861
- protocolAddress: BigInt(context.protocolAddress),
3862
- tokenAddress: BigInt(context.tokenAddress),
3863
- walletIdentity: recipient.walletIdentity,
3864
- amount: payment.amount,
3865
- ...memo.length === 0 ? {} : { memo },
3866
- nonce
3867
- };
3868
- const commitment = await (0, import_core9.computeNoteCommitment)(
3869
- context.chainId,
3870
- identity,
3871
- note.tokenAddress,
3872
- note.amount
3873
- );
3874
- return {
3875
- note,
3876
- commitment,
3877
- identity,
3878
- address: payment.to,
3879
- change,
3880
- delivery: {
3881
- ciphertext: (0, import_ethers8.hexlify)(
3882
- await (0, import_core9.encryptPrivateTransferNote)(keys.viewingPublicKey, note, {
3883
- chainId: context.chainId,
3884
- protocolAddress: note.protocolAddress,
3885
- outputCommitment: commitment
3886
- })
3887
- ),
3888
- clue: (0, import_ethers8.hexlify)((0, import_core9.createClue)(keys.clueKey))
3889
- }
3890
- };
4196
+ async output(payment, context, change = false, outputIndex) {
4197
+ try {
4198
+ positiveOutputAmount(payment, outputIndex);
4199
+ const recipient = await resolveRecipient(
4200
+ payment.to,
4201
+ this.runtime.options.addressResolver,
4202
+ {
4203
+ field: change ? "changeAddress" : "to",
4204
+ ...outputIndex === void 0 ? {} : { outputIndex }
4205
+ }
4206
+ );
4207
+ const nonce = (0, import_core12.generateNonce)();
4208
+ const identity = await (0, import_core12.computeNoteIdentity)(BigInt(recipient.walletIdentity), nonce);
4209
+ const keys = recipient.getReceivingKeys();
4210
+ const note = {
4211
+ chainId: context.chainId,
4212
+ protocolAddress: BigInt(context.protocolAddress),
4213
+ tokenAddress: BigInt(context.tokenAddress),
4214
+ walletIdentity: recipient.walletIdentity,
4215
+ amount: payment.amount,
4216
+ payload: copyApplicationPayload(payment.payload),
4217
+ nonce
4218
+ };
4219
+ const commitment = await (0, import_core12.computeNoteCommitment)(
4220
+ context.chainId,
4221
+ identity,
4222
+ note.tokenAddress,
4223
+ note.amount
4224
+ );
4225
+ return {
4226
+ note,
4227
+ commitment,
4228
+ identity,
4229
+ address: payment.to,
4230
+ change,
4231
+ delivery: {
4232
+ ciphertext: (0, import_ethers8.hexlify)(
4233
+ await (0, import_core12.encryptPrivateTransferNote)(keys.viewingPublicKey, note, {
4234
+ chainId: context.chainId,
4235
+ protocolAddress: note.protocolAddress,
4236
+ outputCommitment: commitment
4237
+ })
4238
+ ),
4239
+ clue: (0, import_ethers8.hexlify)((0, import_core12.createClue)(keys.clueKey))
4240
+ }
4241
+ };
4242
+ } catch (cause) {
4243
+ if (cause instanceof InvisiError) throw cause;
4244
+ throw new InvisiError(
4245
+ "PREPARATION_FAILED",
4246
+ cause instanceof Error ? cause.message : "Output preparation failed",
4247
+ {
4248
+ address: payment.to,
4249
+ field: change ? "changeAddress" : "to",
4250
+ ...outputIndex === void 0 ? {} : { outputIndex },
4251
+ cause
4252
+ }
4253
+ );
4254
+ }
3891
4255
  }
3892
4256
  async membership(input, frame) {
3893
4257
  const { options, reader } = this.runtime;
3894
4258
  if (!options.protocolRpc)
3895
- throw new import_core9.MissingCapabilityError("Protocol RPC membership provider");
4259
+ throw new import_core12.MissingCapabilityError("Protocol RPC membership provider");
3896
4260
  const root = this.#roots.get(input.treeId);
3897
4261
  const proof = await options.protocolRpc.getMembership(
3898
4262
  frame.context,
@@ -3939,9 +4303,9 @@ var Preparation = class {
3939
4303
  for (const input of selected) {
3940
4304
  const note = input.note;
3941
4305
  const c = frame.context;
3942
- if (note.walletIdentity !== scope.walletIdentity || note.chainId !== c.chainId || note.protocolAddress !== BigInt(c.protocolAddress) || note.tokenAddress !== BigInt(c.tokenAddress) || await (0, import_core9.computeNoteCommitment)(
4306
+ if (note.walletIdentity !== scope.walletIdentity || note.chainId !== c.chainId || note.protocolAddress !== BigInt(c.protocolAddress) || note.tokenAddress !== BigInt(c.tokenAddress) || await (0, import_core12.computeNoteCommitment)(
3943
4307
  c.chainId,
3944
- await (0, import_core9.computeNoteIdentity)(BigInt(note.walletIdentity), note.nonce),
4308
+ await (0, import_core12.computeNoteIdentity)(BigInt(note.walletIdentity), note.nonce),
3945
4309
  note.tokenAddress,
3946
4310
  note.amount
3947
4311
  ) !== input.commitment) {
@@ -3993,8 +4357,8 @@ function storedCandidates(inputs) {
3993
4357
 
3994
4358
  // src/session/prepare-credit.ts
3995
4359
  var import_ethers9 = require("ethers");
3996
- var import_core10 = require("@invisi-labs/core");
3997
- var import_protocol6 = require("@invisi-labs/protocol");
4360
+ var import_core13 = require("@invisi-labs/core");
4361
+ var import_protocol7 = require("@invisi-labs/protocol");
3998
4362
  var EMPTY = { ciphertext: "0x", clue: "0x" };
3999
4363
  var CreditPreparation = class {
4000
4364
  constructor(runtime) {
@@ -4009,7 +4373,7 @@ var CreditPreparation = class {
4009
4373
  return value;
4010
4374
  }
4011
4375
  async seal(input, frame) {
4012
- const state = (0, import_core10.normalizeCreditDeliveryState)(input);
4376
+ const state = (0, import_core13.normalizeCreditDeliveryState)(input);
4013
4377
  const commitment = await creditLeaf(state);
4014
4378
  const context = {
4015
4379
  chainId: frame.context.chainId,
@@ -4021,9 +4385,9 @@ var CreditPreparation = class {
4021
4385
  const keys = role === "owner" ? state.ownerReceivingKeys : state.spenderReceivingKeys;
4022
4386
  return {
4023
4387
  ciphertext: (0, import_ethers9.hexlify)(
4024
- await (0, import_core10.encryptCreditNote)(keys.viewingPublicKey, state, { ...context, role })
4388
+ await (0, import_core13.encryptCreditNote)(keys.viewingPublicKey, state, { ...context, role })
4025
4389
  ),
4026
- clue: (0, import_ethers9.hexlify)((0, import_core10.createClue)(keys.clueKey))
4390
+ clue: (0, import_ethers9.hexlify)((0, import_core13.createClue)(keys.clueKey))
4027
4391
  };
4028
4392
  })
4029
4393
  );
@@ -4034,14 +4398,14 @@ var CreditPreparation = class {
4034
4398
  if (!stored || stored.spent !== "unspent" || stored.role !== role) {
4035
4399
  throw new Error(`confirmed unspent ${role} Credit is unavailable`);
4036
4400
  }
4037
- const state = (0, import_core10.normalizeCreditDeliveryState)(stored.state);
4401
+ const state = (0, import_core13.normalizeCreditDeliveryState)(stored.state);
4038
4402
  const n = state.note;
4039
4403
  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) {
4040
4404
  throw new Error("Credit role, commitment or deployment mismatch");
4041
4405
  }
4042
4406
  const proof = await this.#prepare.membership(stored, frame);
4043
4407
  const path = [...proof.path];
4044
- const nullifier = await (0, import_core10.computeCreditNullifier)(n.chainId, n.creditSharedKey, commitment);
4408
+ const nullifier = await (0, import_core13.computeCreditNullifier)(n.chainId, n.creditSharedKey, commitment);
4045
4409
  await this.#prepare.assertUnspent([nullifier], frame);
4046
4410
  return {
4047
4411
  input: {
@@ -4078,10 +4442,16 @@ var CreditPreparation = class {
4078
4442
  positiveAmount(options.amount, 126);
4079
4443
  const frame = await this.#prepare.frame(options.deadline);
4080
4444
  const expiry = this.expiry(options.expiry, frame);
4081
- const spender = await import_core10.ReceivingAddress.parse(options.spender).resolve(
4082
- this.runtime.options.addressResolver
4445
+ const spender = await resolveRecipient(
4446
+ options.spender,
4447
+ this.runtime.options.addressResolver,
4448
+ { field: "spender" }
4449
+ );
4450
+ const owner = await resolveRecipient(
4451
+ this.runtime.options.wallet.getReceivingAddress().toString(),
4452
+ this.runtime.options.addressResolver,
4453
+ { field: "wallet" }
4083
4454
  );
4084
- const owner = await this.runtime.options.wallet.getReceivingAddress().resolve(this.runtime.options.addressResolver);
4085
4455
  const ownerPayout = (0, import_ethers9.getAddress)(owner.walletIdentity);
4086
4456
  const spenderPayout = (0, import_ethers9.getAddress)(spender.walletIdentity);
4087
4457
  if (BigInt(ownerPayout) === 0n || BigInt(spenderPayout) === 0n) {
@@ -4097,16 +4467,16 @@ var CreditPreparation = class {
4097
4467
  spenderWalletIdentity: spender.walletIdentity,
4098
4468
  ownerPayout: BigInt(ownerPayout),
4099
4469
  spenderPayout: BigInt(spenderPayout),
4100
- ...(0, import_core10.normalizeCreditPolicy)({
4470
+ ...(0, import_core13.normalizeCreditPolicy)({
4101
4471
  singleLimit: options.singleLimit ?? 0n,
4102
4472
  dailyLimit: options.dailyLimit ?? 0n,
4103
4473
  monthlyLimit: options.monthlyLimit ?? 0n
4104
4474
  }),
4105
4475
  amount: options.amount,
4106
- ...options.memo === void 0 ? {} : { memo: options.memo },
4476
+ payload: copyApplicationPayload(options.payload),
4107
4477
  expiry,
4108
- nonce: (0, import_core10.generateNonce)(),
4109
- creditSharedKey: (0, import_core10.generateCreditSharedKey)()
4478
+ nonce: (0, import_core13.generateNonce)(),
4479
+ creditSharedKey: (0, import_core13.generateCreditSharedKey)()
4110
4480
  },
4111
4481
  ownerReceivingKeys: owner.getReceivingKeys(),
4112
4482
  spenderReceivingKeys: spender.getReceivingKeys()
@@ -4148,7 +4518,7 @@ var CreditPreparation = class {
4148
4518
  sources: selected.sources,
4149
4519
  nullifiers: selected.nullifiers,
4150
4520
  creditCommitment: output.commitment,
4151
- changeCommitment: plan.outputs[0]?.commitment ?? (0, import_protocol6.word)(0n),
4521
+ changeCommitment: plan.outputs[0]?.commitment ?? (0, import_protocol7.word)(0n),
4152
4522
  owner: (0, import_ethers9.getAddress)((0, import_ethers9.toBeHex)(output.state.note.ownerPayout, 20)),
4153
4523
  expiry: output.state.note.expiry,
4154
4524
  deadline: frame.deadline
@@ -4168,14 +4538,14 @@ var CreditPreparation = class {
4168
4538
  creditNullifier: previous.nullifier,
4169
4539
  previousExpiry: previous.input.state.note.expiry
4170
4540
  };
4171
- plan.statement = import_protocol6.WitnessBuilder.statement(
4541
+ plan.statement = import_protocol7.WitnessBuilder.statement(
4172
4542
  "credit_top_up",
4173
- (0, import_protocol6.topUpCreditPublicInputs)(frame.context, plan.credit.topUp, deliveries, plan.funding)
4543
+ (0, import_protocol7.topUpCreditPublicInputs)(frame.context, plan.credit.topUp, deliveries, plan.funding)
4174
4544
  );
4175
4545
  } else {
4176
- plan.statement = import_protocol6.WitnessBuilder.statement(
4546
+ plan.statement = import_protocol7.WitnessBuilder.statement(
4177
4547
  "create_credit",
4178
- (0, import_protocol6.createCreditPublicInputs)(frame.context, creation, deliveries, plan.funding)
4548
+ (0, import_protocol7.createCreditPublicInputs)(frame.context, creation, deliveries, plan.funding)
4179
4549
  );
4180
4550
  }
4181
4551
  await this.#prepare.assertBlock(frame);
@@ -4204,8 +4574,10 @@ var CreditPreparation = class {
4204
4574
  monthlyLimit: options.monthlyLimit ?? old.note.monthlyLimit ?? 0n,
4205
4575
  amount,
4206
4576
  expiry,
4207
- nonce: (0, import_core10.generateNonce)(),
4208
- ...options.memo === void 0 ? {} : { memo: options.memo }
4577
+ nonce: (0, import_core13.generateNonce)(),
4578
+ payload: copyApplicationPayload(
4579
+ options.payload === void 0 ? old.note.payload : options.payload
4580
+ )
4209
4581
  }
4210
4582
  },
4211
4583
  frame
@@ -4221,7 +4593,7 @@ var CreditPreparation = class {
4221
4593
  }
4222
4594
  async draw(options) {
4223
4595
  const frame = await this.#prepare.frame(options.deadline);
4224
- const profile = (0, import_protocol6.selectCreditDrawCircuit)(options.credits.length);
4596
+ const profile = (0, import_protocol7.selectCreditDrawCircuit)(options.credits.length);
4225
4597
  if (new Set(options.credits.map((value) => value.credit)).size !== options.credits.length) {
4226
4598
  throw new Error("duplicate Credit input");
4227
4599
  }
@@ -4253,11 +4625,11 @@ var CreditPreparation = class {
4253
4625
  }
4254
4626
  sources.set(member.source.treeId, member.source.merkleRoot);
4255
4627
  amounts.push(amount);
4256
- const usage = (0, import_core10.applyCreditDrawUsage)(n, amount, frame.block.timestamp);
4628
+ const usage = (0, import_core13.applyCreditDrawUsage)(n, amount, frame.block.timestamp);
4257
4629
  const remaining = n.amount - amount;
4258
4630
  if (remaining === 0n) {
4259
- remainders.push((0, import_protocol6.word)(0n));
4260
- recoveryAmounts.push((0, import_protocol6.word)(0n));
4631
+ remainders.push((0, import_protocol7.word)(0n));
4632
+ recoveryAmounts.push((0, import_protocol7.word)(0n));
4261
4633
  deliveries.push({ ...EMPTY }, { ...EMPTY });
4262
4634
  continue;
4263
4635
  }
@@ -4275,8 +4647,10 @@ var CreditPreparation = class {
4275
4647
  ...n,
4276
4648
  ...usage,
4277
4649
  amount: remaining,
4278
- ...options.credits[i].memo === void 0 ? {} : { memo: options.credits[i].memo },
4279
- nonce: await (0, import_core10.deriveRemainderNonce)(...args)
4650
+ payload: copyApplicationPayload(
4651
+ options.credits[i].payload === void 0 ? n.payload : options.credits[i].payload
4652
+ ),
4653
+ nonce: await (0, import_core13.deriveRemainderNonce)(...args)
4280
4654
  }
4281
4655
  },
4282
4656
  frame
@@ -4284,7 +4658,7 @@ var CreditPreparation = class {
4284
4658
  outputs.push(output);
4285
4659
  remainders.push(output.commitment);
4286
4660
  recoveryAmounts.push(
4287
- (0, import_protocol6.word)((remaining + await (0, import_core10.computeCreditRecoveryPad)(...args)) % import_core10.FIELD_MODULUS)
4661
+ (0, import_protocol7.word)((remaining + await (0, import_core13.computeCreditRecoveryPad)(...args)) % import_core13.FIELD_MODULUS)
4288
4662
  );
4289
4663
  deliveries.push(...output.deliveries);
4290
4664
  }
@@ -4308,9 +4682,9 @@ var CreditPreparation = class {
4308
4682
  draw: params,
4309
4683
  amounts
4310
4684
  };
4311
- plan.statement = import_protocol6.WitnessBuilder.statement(
4685
+ plan.statement = import_protocol7.WitnessBuilder.statement(
4312
4686
  profile.name,
4313
- (0, import_protocol6.creditDrawPublicInputs)(frame.context, params, deliveries)
4687
+ (0, import_protocol7.creditDrawPublicInputs)(frame.context, params, deliveries)
4314
4688
  );
4315
4689
  await this.#prepare.assertBlock(frame);
4316
4690
  return this.runtime.complete(plan);
@@ -4330,9 +4704,9 @@ var CreditPreparation = class {
4330
4704
  };
4331
4705
  plan.capacity = 1;
4332
4706
  plan.credit = { inputs: [previous.input], outputs: [], deliveries: [], cancel: params };
4333
- plan.statement = import_protocol6.WitnessBuilder.statement(
4707
+ plan.statement = import_protocol7.WitnessBuilder.statement(
4334
4708
  "credit_cancel",
4335
- (0, import_protocol6.cancelCreditPublicInputs)(frame.context, params)
4709
+ (0, import_protocol7.cancelCreditPublicInputs)(frame.context, params)
4336
4710
  );
4337
4711
  await this.#prepare.assertBlock(frame);
4338
4712
  return this.runtime.complete(plan);
@@ -4353,87 +4727,48 @@ var TransactionPreparation = class {
4353
4727
  return this.context.scope;
4354
4728
  }
4355
4729
  async prepareTransfer(options) {
4730
+ if (Object.hasOwn(options, "target") || Object.hasOwn(options, "execution")) {
4731
+ throw new Error(
4732
+ "prepareTransfer accepts no target or execution; choose execution in prove()"
4733
+ );
4734
+ }
4356
4735
  const copied = structuredClone(options);
4357
- const preparation = new Preparation(this.runtime(copied.tokenAddress));
4736
+ const runtime = this.runtime(copied.tokenAddress);
4737
+ if (copied.commitments !== void 0 && copied.commitments.length === 0)
4738
+ throw new Error("empty explicit input selection");
4739
+ const preparation = new Preparation({
4740
+ ...runtime,
4741
+ ...copied.commitments !== void 0 ? { inputSelection: copied.commitments } : {}
4742
+ });
4358
4743
  const frame = await preparation.frame(copied.deadline);
4359
- if (copied.outputs.length < 1 || copied.outputs.length > 168) {
4360
- throw new Error("transfer requires 1..168 outputs");
4744
+ if (copied.outputs.length > 168) throw new Error("transfer supports at most 168 outputs");
4745
+ let withdrawalAddress = import_protocol8.ZERO_ADDRESS;
4746
+ let withdrawalAmount = 0n;
4747
+ if (copied.withdrawal !== void 0) {
4748
+ withdrawalAddress = (0, import_ethers10.getAddress)(copied.withdrawal.to);
4749
+ positiveAmount(copied.withdrawal.amount);
4750
+ if (BigInt(withdrawalAddress) === 0n)
4751
+ throw new Error("withdrawal destination must be nonzero");
4752
+ withdrawalAmount = copied.withdrawal.amount;
4361
4753
  }
4754
+ if (copied.outputs.length === 0 && withdrawalAmount === 0n)
4755
+ throw new Error("transfer requires a private output or public withdrawal");
4362
4756
  const total = copied.outputs.reduce(
4363
- (sum, output) => sum + positiveAmount(output.amount),
4364
- 0n
4757
+ (sum, output, index2) => sum + positiveOutputAmount(output, index2),
4758
+ withdrawalAmount
4365
4759
  );
4366
4760
  const funding = await preparation.funding(copied.funding, frame);
4367
4761
  return this.funds(
4368
- "transfer",
4369
4762
  copied.outputs,
4370
4763
  total,
4371
- import_protocol7.ZERO_ADDRESS,
4372
- 0n,
4764
+ withdrawalAddress,
4765
+ withdrawalAmount,
4373
4766
  frame,
4374
4767
  funding,
4375
- copied.target,
4376
- preparation
4377
- );
4378
- }
4379
- async prepareWithdraw(options) {
4380
- const copied = structuredClone(options);
4381
- const preparation = new Preparation(this.runtime(copied.tokenAddress));
4382
- const frame = await preparation.frame(copied.deadline);
4383
- const to = (0, import_ethers10.getAddress)(copied.to);
4384
- positiveAmount(copied.amount);
4385
- if (BigInt(to) === 0n) {
4386
- throw new Error("withdrawal destination must be nonzero");
4387
- }
4388
- return this.funds(
4389
- "withdraw",
4390
- [],
4391
- copied.amount,
4392
- to,
4393
- copied.amount,
4394
- frame,
4395
- { funding: {}, fundingMode: "none" },
4396
- copied.target,
4397
- preparation
4398
- );
4399
- }
4400
- /** Full payout to the note owner; no arbitrary destination, change or spending signature. */
4401
- async prepareSweep(options) {
4402
- const copied = structuredClone(options);
4403
- const runtime = this.runtime(copied.tokenAddress);
4404
- const state = await runtime.readInventory();
4405
- const available = Object.values(state.notes).filter(
4406
- (record) => record.note.tokenAddress === BigInt(copied.tokenAddress) && record.spent === "unspent" && !state.reservations[record.commitment]
4407
- );
4408
- const selected = copied.commitments ?? available.map((record) => record.commitment);
4409
- if (selected.length === 0) throw new Error("empty sweep input selection");
4410
- if (new Set(selected).size !== selected.length) throw new Error("duplicate sweep inputs");
4411
- const byId = new Map(available.map((record) => [record.commitment, record]));
4412
- let amount = 0n;
4413
- for (const id of selected) {
4414
- const record = byId.get(id);
4415
- if (!record) throw new Error("sweep input unavailable");
4416
- amount += record.note.amount;
4417
- }
4418
- positiveAmount(amount);
4419
- const preparation = new Preparation({
4420
- ...runtime,
4421
- inputSelection: selected
4422
- });
4423
- const frame = await preparation.frame(copied.deadline);
4424
- return this.funds(
4425
- "sweep",
4426
- [],
4427
- amount,
4428
- this.scope.walletIdentity,
4429
- amount,
4430
- frame,
4431
- { funding: {}, fundingMode: "none" },
4432
- copied.target,
4433
4768
  preparation
4434
4769
  );
4435
4770
  }
4436
- async funds(kind, payments, total, withdrawalAddress, withdrawalAmount, frame, funding, target, preparation) {
4771
+ async funds(payments, total, withdrawalAddress, withdrawalAmount, frame, funding, preparation) {
4437
4772
  if (total >= 1n << 135n) {
4438
4773
  throw new Error("transaction sum exceeds uint135");
4439
4774
  }
@@ -4443,7 +4778,7 @@ var TransactionPreparation = class {
4443
4778
  frame
4444
4779
  );
4445
4780
  const outputs = await Promise.all(
4446
- payments.map((payment) => preparation.output(payment, context))
4781
+ payments.map((payment, index2) => preparation.output(payment, context, false, index2))
4447
4782
  );
4448
4783
  const change = selection.amount + (funding.funding.amount ?? 0n) - total;
4449
4784
  if (change > 0n) {
@@ -4468,17 +4803,17 @@ var TransactionPreparation = class {
4468
4803
  deadline
4469
4804
  };
4470
4805
  const deliveries = outputs.map((output) => output.delivery);
4471
- const normalized = (0, import_protocol7.normalizeTransactRequest)(context, params, deliveries, funding.funding);
4472
- const statement2 = import_protocol7.WitnessBuilder.statement(
4806
+ const normalized = (0, import_protocol8.normalizeTransactRequest)(context, params, deliveries, funding.funding);
4807
+ const statement2 = import_protocol8.WitnessBuilder.statement(
4473
4808
  `transact_user_slots${normalized.capacity}`,
4474
- (0, import_protocol7.transactUserPublicInputs)(context, params, deliveries, funding.funding),
4475
- target ?? "evm"
4809
+ (0, import_protocol8.transactUserPublicInputs)(context, params, deliveries, funding.funding),
4810
+ "evm"
4476
4811
  );
4477
4812
  await preparation.assertBlock(frame);
4478
4813
  return preparation.runtime.complete({
4479
4814
  version: 1,
4480
4815
  id: crypto.randomUUID(),
4481
- kind,
4816
+ kind: "transfer",
4482
4817
  context,
4483
4818
  scope: this.scope,
4484
4819
  preparedAt: block,
@@ -4500,7 +4835,9 @@ var TransactionPreparation = class {
4500
4835
  throw new Error("shield requires 1..10 outputs");
4501
4836
  }
4502
4837
  const outputs = await Promise.all(
4503
- copied.outputs.map((output) => preparation.output(output, frame.context))
4838
+ copied.outputs.map(
4839
+ (output, index2) => preparation.output(output, frame.context, false, index2)
4840
+ )
4504
4841
  );
4505
4842
  if (outputs.reduce((sum, output) => sum + output.note.amount, 0n) !== (funding.funding.amount ?? 0n)) {
4506
4843
  throw new Error("shield funding must equal output total");
@@ -4605,10 +4942,10 @@ async function refreshWallet(runtime, options) {
4605
4942
  }
4606
4943
 
4607
4944
  // src/session/WalletUpdater.ts
4608
- var import_core12 = require("@invisi-labs/core");
4945
+ var import_core15 = require("@invisi-labs/core");
4609
4946
 
4610
4947
  // src/session/inventory.ts
4611
- var import_core11 = require("@invisi-labs/core");
4948
+ var import_core14 = require("@invisi-labs/core");
4612
4949
  var CanonicalConflict = class extends Error {
4613
4950
  };
4614
4951
  var CreditMetadataMismatch = class extends Error {
@@ -4660,10 +4997,10 @@ var InventoryProcessor = class {
4660
4997
  }
4661
4998
  async creditNullifier(state) {
4662
4999
  const n = state.note;
4663
- return (0, import_core11.computeCreditNullifier)(
5000
+ return (0, import_core14.computeCreditNullifier)(
4664
5001
  n.chainId,
4665
5002
  n.creditSharedKey,
4666
- await (0, import_core11.computeCreditCommitment)(
5003
+ await (0, import_core14.computeCreditCommitment)(
4667
5004
  n.chainId,
4668
5005
  n.spenderWalletIdentity,
4669
5006
  n.ownerWalletIdentity,
@@ -4673,7 +5010,7 @@ var InventoryProcessor = class {
4673
5010
  n.amount,
4674
5011
  n.nonce,
4675
5012
  n.expiry,
4676
- await (0, import_core11.computeCreditSharedKeyHash)(n.creditSharedKey),
5013
+ await (0, import_core14.computeCreditSharedKeyHash)(n.creditSharedKey),
4677
5014
  n
4678
5015
  )
4679
5016
  );
@@ -4737,7 +5074,7 @@ var InventoryProcessor = class {
4737
5074
  previous.state.note.expiry === params.expiries[index2],
4738
5075
  "draw recovery expiry mismatch"
4739
5076
  );
4740
- const remainder = await (0, import_core11.recoverCreditRemainder)(
5077
+ const remainder = await (0, import_core14.recoverCreditRemainder)(
4741
5078
  context,
4742
5079
  previous.state.note,
4743
5080
  {
@@ -4753,7 +5090,7 @@ var InventoryProcessor = class {
4753
5090
  const child = foundByKey.get(
4754
5091
  params.remainderCommitments[index2] + ":" + previous.role
4755
5092
  );
4756
- const memo = child?.state.note.memo;
5093
+ const payload = child?.state.note.payload;
4757
5094
  const record = {
4758
5095
  commitment: params.remainderCommitments[index2],
4759
5096
  treeId: BigInt(args.outputTreeId),
@@ -4766,7 +5103,7 @@ var InventoryProcessor = class {
4766
5103
  ...clone(previous.state),
4767
5104
  note: {
4768
5105
  ...remainder,
4769
- ...memo === void 0 ? {} : { memo: (0, import_core11.encodeMemo)(memo) }
5106
+ ...payload === void 0 ? {} : { payload: clone(payload) }
4770
5107
  }
4771
5108
  },
4772
5109
  receivingKeySource: { kind: "draw", commitment: previous.commitment },
@@ -4845,7 +5182,7 @@ var WalletUpdater = class extends InventoryProcessor {
4845
5182
  }
4846
5183
  async run(batch, options) {
4847
5184
  const { reader, protocol, scope, store, viewing } = this.#options;
4848
- const nullifier = import_core12.NullifierCapability.fromIdentity(scope.walletIdentity);
5185
+ const nullifier = import_core15.NullifierCapability.fromIdentity(scope.walletIdentity);
4849
5186
  const { signal, reset } = options;
4850
5187
  signal?.throwIfAborted();
4851
5188
  const context = protocol.context;
@@ -5014,6 +5351,7 @@ var WalletUpdater = class extends InventoryProcessor {
5014
5351
  draft.notes = {};
5015
5352
  draft.credits = {};
5016
5353
  delete draft.observedThrough;
5354
+ delete draft.discoveryThrough;
5017
5355
  delete draft.invalidated;
5018
5356
  }
5019
5357
  mergeReceiptRecords(draft, { notes, credits, checkpoint: block });
@@ -5045,13 +5383,38 @@ var WalletUpdater = class extends InventoryProcessor {
5045
5383
  var WalletSession = class {
5046
5384
  #options;
5047
5385
  #updater;
5386
+ #listeners = /* @__PURE__ */ new Set();
5048
5387
  #sending = /* @__PURE__ */ new Set();
5049
5388
  #refreshQueue = Promise.resolve();
5050
5389
  scope;
5051
5390
  protocol;
5052
5391
  constructor(options) {
5392
+ if ("ciphertextCodecs" in options || "envelopeVersion" in options) {
5393
+ throw new Error(
5394
+ "SDK envelope versions are fixed; encode application payload bytes externally"
5395
+ );
5396
+ }
5397
+ const store = options.store;
5053
5398
  this.#options = {
5054
5399
  ...options,
5400
+ ...store ? {
5401
+ store: {
5402
+ persistent: store.persistent,
5403
+ readSnapshot: (scope) => store.readSnapshot(scope),
5404
+ transaction: async (scope, action) => {
5405
+ const result = await store.transaction(scope, action);
5406
+ if (scopeKey(scope) === scopeKey(this.scope)) {
5407
+ for (const listener of [...this.#listeners]) {
5408
+ try {
5409
+ listener();
5410
+ } catch {
5411
+ }
5412
+ }
5413
+ }
5414
+ return result;
5415
+ }
5416
+ }
5417
+ } : {},
5055
5418
  ...options.gasBenchmarks ? { gasBenchmarks: copyGasBenchmarks(options.gasBenchmarks) } : {}
5056
5419
  };
5057
5420
  this.protocol = options.protocol;
@@ -5066,7 +5429,7 @@ var WalletSession = class {
5066
5429
  /** Delegate sending to ethers; managed operations additionally retain the resulting hash. */
5067
5430
  async sendTransaction(input, options = {}) {
5068
5431
  const signer = this.#options.signer;
5069
- if (!signer) throw new import_core13.MissingCapabilityError("ethers signer");
5432
+ if (!signer) throw new import_core17.MissingCapabilityError("ethers signer");
5070
5433
  const operation = input instanceof Operation ? input : void 0;
5071
5434
  if (operation && this.#sending.has(operation.id))
5072
5435
  throw new Error("operation is already being sent by this session");
@@ -5074,12 +5437,12 @@ var WalletSession = class {
5074
5437
  const overrides = { ...options };
5075
5438
  const plain = operation ? void 0 : { ...input };
5076
5439
  try {
5077
- if (!signer.provider) throw new import_core13.MissingCapabilityError("connected ethers signer");
5440
+ if (!signer.provider) throw new import_core17.MissingCapabilityError("connected ethers signer");
5078
5441
  if ((await signer.provider.getNetwork()).chainId !== this.protocol.context.chainId) {
5079
5442
  throw new Error("signer-provider session chain mismatch");
5080
5443
  }
5081
5444
  if (operation) {
5082
- if (!this.#options.store) throw new import_core13.MissingCapabilityError("wallet store");
5445
+ if (!this.#options.store) throw new import_core17.MissingCapabilityError("wallet store");
5083
5446
  const state = await this.#options.store.readSnapshot(this.scope);
5084
5447
  if (!state.operations[operation.id])
5085
5448
  throw new Error("operation does not belong to this wallet session");
@@ -5110,25 +5473,30 @@ var WalletSession = class {
5110
5473
  runtime(tokenAddress) {
5111
5474
  const options = this.#options;
5112
5475
  if (!options.store) {
5113
- throw new import_core13.MissingCapabilityError("wallet store");
5476
+ throw new import_core17.MissingCapabilityError("wallet store");
5114
5477
  }
5115
5478
  if (!options.chainReader) {
5116
- throw new import_core13.MissingCapabilityError("chain reader");
5479
+ throw new import_core17.MissingCapabilityError("chain reader");
5117
5480
  }
5118
5481
  return {
5119
5482
  options: { ...options, protocol: options.protocol.forToken(tokenAddress) },
5120
5483
  scope: this.scope,
5121
5484
  store: options.store,
5122
5485
  reader: options.chainReader,
5123
- authorization: () => options.authorization ?? options.wallet.exportAuthorization()
5486
+ authorization: () => {
5487
+ if (options.authorization) return options.authorization;
5488
+ if (options.wallet instanceof import_core16.Wallet) return options.wallet.exportAuthorization();
5489
+ throw new import_core17.MissingCapabilityError("wallet authorization");
5490
+ }
5124
5491
  };
5125
5492
  }
5126
5493
  updater() {
5127
5494
  if (this.#updater) return this.#updater;
5128
5495
  const options = this.#options;
5129
5496
  if (!options.store || !options.chainReader || !options.protocolRpc)
5130
- throw new import_core13.MissingCapabilityError("wallet store, chain reader and Protocol RPC");
5131
- const viewing = options.viewing ?? options.wallet.getViewer();
5497
+ throw new import_core17.MissingCapabilityError("wallet store, chain reader and Protocol RPC");
5498
+ const viewing = options.viewing ?? (options.wallet instanceof import_core16.Wallet ? options.wallet.getViewer() : void 0);
5499
+ if (!viewing) throw new import_core17.MissingCapabilityError("viewing");
5132
5500
  const receivingAddress = options.wallet.getReceivingAddress();
5133
5501
  const receivingKeys = receivingAddress.kind === "long" ? receivingAddress.getReceivingKeys() : void 0;
5134
5502
  this.#updater = new WalletUpdater({
@@ -5142,10 +5510,27 @@ var WalletSession = class {
5142
5510
  });
5143
5511
  return this.#updater;
5144
5512
  }
5513
+ /** Local session commits only. Host applications own cross-tab/store subscriptions. */
5514
+ subscribe(listener) {
5515
+ this.#listeners.add(listener);
5516
+ return () => {
5517
+ this.#listeners.delete(listener);
5518
+ };
5519
+ }
5520
+ /** Detached history for wallet UI; construction records do not imply chain success. */
5521
+ async getHistory(options = {}) {
5522
+ const tokenAddress = options.tokenAddress ? (0, import_ethers11.getAddress)(options.tokenAddress) : void 0;
5523
+ const store = this.#options.store;
5524
+ if (!store) throw new import_core17.MissingCapabilityError("wallet store");
5525
+ return walletHistory(
5526
+ await store.readSnapshot(this.scope),
5527
+ tokenAddress ? { tokenAddress } : {}
5528
+ );
5529
+ }
5145
5530
  async balance(tokenAddress) {
5146
5531
  const store = this.#options.store;
5147
5532
  if (!store) {
5148
- throw new import_core13.MissingCapabilityError("wallet store");
5533
+ throw new import_core17.MissingCapabilityError("wallet store");
5149
5534
  }
5150
5535
  return walletBalance(await store.readSnapshot(this.scope), (0, import_ethers11.getAddress)(tokenAddress));
5151
5536
  }
@@ -5153,15 +5538,15 @@ var WalletSession = class {
5153
5538
  async refresh(options) {
5154
5539
  const provider = this.#options.protocolRpc;
5155
5540
  const reader = this.#options.chainReader;
5156
- if (!reader) throw new import_core13.MissingCapabilityError("chain reader");
5157
- if (!provider) throw new import_core13.MissingCapabilityError("Protocol RPC");
5541
+ if (!reader) throw new import_core17.MissingCapabilityError("chain reader");
5542
+ if (!provider) throw new import_core17.MissingCapabilityError("Protocol RPC");
5158
5543
  const updater = this.updater();
5159
5544
  const request = {
5160
5545
  ...options,
5161
5546
  ...options.detectionKey ? { detectionKey: options.detectionKey.slice() } : {}
5162
5547
  };
5163
- const task = this.#refreshQueue.then(
5164
- () => refreshWallet(
5548
+ const task = this.#refreshQueue.then(async () => {
5549
+ const result = await refreshWallet(
5165
5550
  {
5166
5551
  provider,
5167
5552
  reader,
@@ -5170,8 +5555,21 @@ var WalletSession = class {
5170
5555
  apply: (batch, settings) => updater.applyUpdates(batch, settings)
5171
5556
  },
5172
5557
  request
5173
- )
5174
- );
5558
+ );
5559
+ await this.#options.store.transaction(this.scope, (state) => {
5560
+ request.signal?.throwIfAborted();
5561
+ if (state.invalidated) throw new Error("wallet requires canonical resync");
5562
+ const previous = state.discoveryThrough;
5563
+ const start = previous ? previous.number + 1n : this.protocol.deployment.protocol.fromBlock;
5564
+ if (request.fromBlock <= start && (!previous || result.block.number >= previous.number)) {
5565
+ state.discoveryThrough = {
5566
+ number: result.block.number,
5567
+ hash: result.block.hash
5568
+ };
5569
+ }
5570
+ });
5571
+ return result;
5572
+ });
5175
5573
  this.#refreshQueue = task.catch(() => {
5176
5574
  });
5177
5575
  return task;
@@ -5182,38 +5580,43 @@ var WalletSession = class {
5182
5580
  options: runtime.options,
5183
5581
  scope: runtime.scope,
5184
5582
  reader: runtime.reader,
5185
- authorization: runtime.authorization,
5186
5583
  readInventory: () => runtime.store.readSnapshot(runtime.scope),
5187
5584
  complete: (plan) => Operation.prepare(runtime, plan)
5188
5585
  });
5189
5586
  }
5190
5587
  async prepareTransfer(options) {
5191
- return this.preparation(options.tokenAddress).prepareTransfer(options);
5192
- }
5193
- async prepareWithdraw(options) {
5194
- return this.preparation(options.tokenAddress).prepareWithdraw(options);
5195
- }
5196
- async prepareSweep(options) {
5197
- return this.preparation(options.tokenAddress).prepareSweep(options);
5588
+ return prepareWithErrors("prepareTransfer", async () => {
5589
+ return this.preparation(options.tokenAddress).prepareTransfer(options);
5590
+ });
5198
5591
  }
5199
5592
  async prepareShield(options) {
5200
- return this.preparation(options.tokenAddress).prepareShield(options);
5593
+ return prepareWithErrors("prepareShield", async () => {
5594
+ return this.preparation(options.tokenAddress).prepareShield(options);
5595
+ });
5201
5596
  }
5202
5597
  async prepareCreateCredit(options) {
5203
- return this.preparation(options.tokenAddress).prepareCreateCredit(options);
5598
+ return prepareWithErrors("prepareCreateCredit", async () => {
5599
+ return this.preparation(options.tokenAddress).prepareCreateCredit(options);
5600
+ });
5204
5601
  }
5205
5602
  async prepareDrawCredit(options) {
5206
- return this.preparation(options.tokenAddress).prepareDrawCredit(options);
5603
+ return prepareWithErrors("prepareDrawCredit", async () => {
5604
+ return this.preparation(options.tokenAddress).prepareDrawCredit(options);
5605
+ });
5207
5606
  }
5208
5607
  async prepareTopUpCredit(options) {
5209
- return this.preparation(options.tokenAddress).prepareTopUpCredit(options);
5608
+ return prepareWithErrors("prepareTopUpCredit", async () => {
5609
+ return this.preparation(options.tokenAddress).prepareTopUpCredit(options);
5610
+ });
5210
5611
  }
5211
5612
  async prepareCancelCredit(options) {
5212
- return this.preparation(options.tokenAddress).prepareCancelCredit(options);
5613
+ return prepareWithErrors("prepareCancelCredit", async () => {
5614
+ return this.preparation(options.tokenAddress).prepareCancelCredit(options);
5615
+ });
5213
5616
  }
5214
5617
  async resume(operationId) {
5215
5618
  const store = this.#options.store;
5216
- if (!store) throw new import_core13.MissingCapabilityError("wallet store");
5619
+ if (!store) throw new import_core17.MissingCapabilityError("wallet store");
5217
5620
  const state = await store.readSnapshot(this.scope);
5218
5621
  const saved = state.operations[operationId]?.payload;
5219
5622
  if (!saved?.plan) throw new Error("operation not found");