@invisi-labs/sdk 1.1.6 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/{NoteOutput-DRkVnwlx.d.cts → broadcaster-preparation-DMxxZ_1Q.d.cts} +309 -120
  2. package/dist/{NoteOutput-DpDC6T2O.d.ts → broadcaster-preparation-DMxxZ_1Q.d.ts} +309 -120
  3. package/dist/broadcaster-preparation-RMWQFYNL.js +11 -0
  4. package/dist/broadcaster-preparation-UGOTO4SX.cjs +11 -0
  5. package/dist/broadcaster-preparation-UGOTO4SX.cjs.map +1 -0
  6. package/dist/chunk-7F7ZZW3L.cjs +374 -0
  7. package/dist/chunk-7F7ZZW3L.cjs.map +1 -0
  8. package/dist/{chunk-4SD3MW6N.js → chunk-EHP5TBC6.js} +1347 -2493
  9. package/dist/chunk-EHP5TBC6.js.map +1 -0
  10. package/dist/{broadcaster-preparation-QVL62KM6.js → chunk-GTPUYPQL.js} +6 -6
  11. package/dist/chunk-GTPUYPQL.js.map +1 -0
  12. package/dist/{chunk-FL4TIGWW.js → chunk-ITTGBIG2.js} +617 -521
  13. package/dist/chunk-ITTGBIG2.js.map +1 -0
  14. package/dist/chunk-J2JH6HIG.cjs +1877 -0
  15. package/dist/chunk-J2JH6HIG.cjs.map +1 -0
  16. package/dist/chunk-LOU7AP2B.cjs +128 -0
  17. package/dist/chunk-LOU7AP2B.cjs.map +1 -0
  18. package/dist/chunk-RHNIHI4P.cjs +3565 -0
  19. package/dist/chunk-RHNIHI4P.cjs.map +1 -0
  20. package/dist/{chunk-QC6TQR3R.js → chunk-XHKQ2BO2.js} +12 -12
  21. package/dist/chunk-XHKQ2BO2.js.map +1 -0
  22. package/dist/ethers.cjs +11 -385
  23. package/dist/ethers.cjs.map +1 -1
  24. package/dist/ethers.d.cts +3 -4
  25. package/dist/ethers.d.ts +3 -4
  26. package/dist/ethers.js +1 -1
  27. package/dist/index.cjs +451 -7845
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +72 -702
  30. package/dist/index.d.ts +72 -702
  31. package/dist/index.js +374 -862
  32. package/dist/index.js.map +1 -1
  33. package/dist/receipt-logs-CvC951p7.d.cts +318 -0
  34. package/dist/receipt-logs-m0-bhaxW.d.ts +318 -0
  35. package/dist/session-support.cjs +168 -0
  36. package/dist/session-support.cjs.map +1 -0
  37. package/dist/session-support.d.cts +252 -0
  38. package/dist/session-support.d.ts +252 -0
  39. package/dist/session-support.js +168 -0
  40. package/dist/session-support.js.map +1 -0
  41. package/package.json +10 -20
  42. package/dist/broadcaster-preparation-QVL62KM6.js.map +0 -1
  43. package/dist/chunk-4SD3MW6N.js.map +0 -1
  44. package/dist/chunk-FL4TIGWW.js.map +0 -1
  45. package/dist/chunk-QC6TQR3R.js.map +0 -1
  46. package/dist/indexeddb.cjs +0 -6878
  47. package/dist/indexeddb.cjs.map +0 -1
  48. package/dist/indexeddb.d.cts +0 -20
  49. package/dist/indexeddb.d.ts +0 -20
  50. package/dist/indexeddb.js +0 -71
  51. package/dist/indexeddb.js.map +0 -1
  52. package/dist/memory.cjs +0 -6886
  53. package/dist/memory.cjs.map +0 -1
  54. package/dist/memory.d.cts +0 -12
  55. package/dist/memory.d.ts +0 -12
  56. package/dist/memory.js +0 -11
  57. package/dist/sqlite.cjs +0 -6862
  58. package/dist/sqlite.cjs.map +0 -1
  59. package/dist/sqlite.d.cts +0 -28
  60. package/dist/sqlite.d.ts +0 -28
  61. package/dist/sqlite.js +0 -55
  62. package/dist/sqlite.js.map +0 -1
  63. package/dist/state-C4HZFotm.d.cts +0 -78
  64. package/dist/state-C4HZFotm.d.ts +0 -78
  65. package/dist/store-8aBhnbGr.d.ts +0 -42
  66. package/dist/store-YrbXb6CY.d.cts +0 -42
  67. /package/dist/{memory.js.map → broadcaster-preparation-RMWQFYNL.js.map} +0 -0
@@ -2,28 +2,67 @@ import {
2
2
  TransactionPreparation,
3
3
  authorizationRequest,
4
4
  configureRecipientInfrastructure,
5
- creditLeaf,
6
- creditReservationKey,
7
- encodeOperation,
8
5
  exportBroadcasterIntent,
9
- fundingTypedData,
10
- inputFacts,
11
- operationExpired,
12
- operationSources,
6
+ exportPaymentNote,
7
+ positiveAmount,
13
8
  prepareWithErrors,
14
- publicProof,
15
9
  requiresAuthorization,
16
- statement,
17
- verifyMembership,
18
- walletBalance,
19
- walletBalances,
20
- witnessFor
21
- } from "./chunk-FL4TIGWW.js";
10
+ statement
11
+ } from "./chunk-ITTGBIG2.js";
22
12
  import {
23
- EthersChainReader
24
- } from "./chunk-QC6TQR3R.js";
13
+ EthersChainReader,
14
+ EthersFundingVerifier
15
+ } from "./chunk-XHKQ2BO2.js";
16
+
17
+ // src/transactions/records.ts
18
+ function creditReservationKey(commitment) {
19
+ return "credit:" + commitment;
20
+ }
21
+
22
+ // src/transactions/preparation/inputs.ts
23
+ function inputFacts(plan) {
24
+ const credit = plan.credit;
25
+ const noteNullifiers = plan.params?.nullifiers ?? credit?.creation?.nullifiers ?? [];
26
+ let creditNullifiers = [];
27
+ if (credit?.draw) {
28
+ creditNullifiers = credit.draw.nullifiers;
29
+ } else if (credit?.topUp) {
30
+ creditNullifiers = [credit.topUp.creditNullifier];
31
+ } else if (credit?.cancel) {
32
+ creditNullifiers = [credit.cancel.nullifier];
33
+ }
34
+ if (noteNullifiers.length !== plan.inputs.length || creditNullifiers.length !== (credit?.inputs.length ?? 0)) {
35
+ throw new Error("operation input statement count mismatch");
36
+ }
37
+ return [
38
+ ...plan.inputs.map((input, index2) => ({
39
+ key: input.commitment,
40
+ reservationKey: input.commitment,
41
+ nullifier: noteNullifiers[index2],
42
+ kind: "notes"
43
+ })),
44
+ ...(credit?.inputs ?? []).map((input, index2) => ({
45
+ key: input.commitment + ":" + input.role,
46
+ reservationKey: creditReservationKey(input.commitment),
47
+ nullifier: creditNullifiers[index2],
48
+ kind: "credits"
49
+ }))
50
+ ];
51
+ }
52
+ function operationSources(plan) {
53
+ const noteSources = plan.params?.sources;
54
+ if (noteSources != null) return noteSources;
55
+ const credit = plan.credit;
56
+ if (credit?.topUp) {
57
+ return [...credit.topUp.creation.sources, credit.topUp.creditSource];
58
+ }
59
+ if (credit?.creation) return credit.creation.sources;
60
+ if (credit?.draw) return credit.draw.sources;
61
+ if (credit?.cancel) return [credit.cancel.source];
62
+ return [];
63
+ }
25
64
 
26
- // src/data/gas-benchmarks.json
65
+ // src/gas/data/gas-benchmarks.json
27
66
  var gas_benchmarks_default = [
28
67
  {
29
68
  formatVersion: 1,
@@ -1724,7 +1763,7 @@ var gas_benchmarks_default = [
1724
1763
  }
1725
1764
  ];
1726
1765
 
1727
- // src/gas-estimation.ts
1766
+ // src/gas/profiles.ts
1728
1767
  var word = /^0x[0-9a-fA-F]{64}$/;
1729
1768
  var address = /^0x[0-9a-fA-F]{40}$/;
1730
1769
  var decimal = /^(0|[1-9][0-9]*)$/;
@@ -1807,6 +1846,86 @@ function sameDeployment(a, b) {
1807
1846
  function sameCircuit(a, b) {
1808
1847
  return a === null ? b === null : b !== null && a.circuitName === b.circuitName && a.target === b.target && a.artifactSha256.toLowerCase() === b.artifactSha256.toLowerCase() && a.vkSha256.toLowerCase() === b.vkSha256.toLowerCase();
1809
1848
  }
1849
+
1850
+ // src/gas/shape.ts
1851
+ function estimateShapeGas(request, profiles = defaultProfiles) {
1852
+ if (request.shape.target === "noir-recursive") {
1853
+ throw new Error(
1854
+ "Estimate gas for the complete aggregate, not an individual recursive member"
1855
+ );
1856
+ }
1857
+ const measurements = profiles.flatMap((profile) => profile.measurements);
1858
+ const matching = measurements.filter(
1859
+ (measurement) => measurement.shape.kind === request.shape.kind && measurement.shape.target === request.shape.target && sameCircuit(measurement.circuit, request.circuit)
1860
+ );
1861
+ const exact = matching.filter(
1862
+ (measurement) => shapeKeys.every((key) => measurement.shape[key] === request.shape[key])
1863
+ );
1864
+ if (exact.length) {
1865
+ const means = exact.map((m) => {
1866
+ const count = BigInt(m.samples.length);
1867
+ return (m.samples.reduce((sum, s) => sum + BigInt(s.gasUsed), 0n) + count - 1n) / count;
1868
+ });
1869
+ return { gasUnits: means.reduce((max, v) => v > max ? v : max) };
1870
+ }
1871
+ if (["create-credit", "draw-credit", "top-up-credit", "cancel-credit"].includes(
1872
+ request.shape.kind
1873
+ ) && !matching.length)
1874
+ throw new Error("No current Credit gas reference for this artifact and verification key");
1875
+ const candidates = matching.length ? matching : measurements.filter(
1876
+ (measurement) => measurement.shape.kind === request.shape.kind && measurement.shape.target === request.shape.target
1877
+ );
1878
+ if (!candidates.length) throw new Error("No gas reference for this operation kind and target");
1879
+ const dimensions = [
1880
+ "noteInputs",
1881
+ "noteOutputs",
1882
+ "sourceTrees",
1883
+ "creditInputs",
1884
+ "creditOutputs",
1885
+ "creditRemainders",
1886
+ "deliveryBytes",
1887
+ ...!matching.length ? ["capacity"] : []
1888
+ ];
1889
+ const groups = /* @__PURE__ */ new Map();
1890
+ for (const candidate of candidates) {
1891
+ const key = JSON.stringify([
1892
+ shapeKeys.map((k) => candidate.shape[k]),
1893
+ candidate.circuit?.circuitName,
1894
+ candidate.circuit?.target,
1895
+ candidate.circuit?.artifactSha256.toLowerCase(),
1896
+ candidate.circuit?.vkSha256.toLowerCase()
1897
+ ]);
1898
+ const previous = groups.get(key);
1899
+ groups.set(
1900
+ key,
1901
+ previous ? { ...candidate, samples: [...previous.samples, ...candidate.samples] } : candidate
1902
+ );
1903
+ }
1904
+ const estimates = [...groups.values()].map((measurement) => {
1905
+ let numerator = 1n;
1906
+ let denominator = 1n;
1907
+ for (const key of dimensions) {
1908
+ const current = BigInt(request.shape[key]) + 1n;
1909
+ const baseline = BigInt(measurement.shape[key]) + 1n;
1910
+ if (current * denominator > numerator * baseline) {
1911
+ numerator = current;
1912
+ denominator = baseline;
1913
+ }
1914
+ }
1915
+ let gas = measurement.samples.reduce(
1916
+ (max, sample) => BigInt(sample.gasUsed) > max ? BigInt(sample.gasUsed) : max,
1917
+ 0n
1918
+ );
1919
+ gas = (gas * numerator + denominator - 1n) / denominator;
1920
+ if (request.shape.funding === "signature" && measurement.shape.funding !== "signature")
1921
+ gas *= 2n;
1922
+ if (request.shape.permit && !measurement.shape.permit) gas *= 2n;
1923
+ return gas;
1924
+ });
1925
+ return { gasUnits: estimates.reduce((min, gas) => gas < min ? gas : min) };
1926
+ }
1927
+
1928
+ // src/gas/gas-estimation.ts
1810
1929
  function estimateBenchmarkGas(plan, protocol, profiles) {
1811
1930
  const credit = plan.credit;
1812
1931
  const deliveries = credit?.deliveries ?? plan.outputs.map((out) => out.delivery);
@@ -1882,680 +2001,278 @@ function estimateBenchmarkGas(plan, protocol, profiles) {
1882
2001
  }
1883
2002
  return { ...base, available: false, reason: "shape-not-covered" };
1884
2003
  }
1885
- function estimateShapeGas(request, profiles = defaultProfiles) {
1886
- if (request.shape.target === "noir-recursive") {
1887
- throw new Error(
1888
- "Estimate gas for the complete aggregate, not an individual recursive member"
1889
- );
2004
+ function estimateGasReference(plan, protocol, profiles) {
2005
+ const reference = estimateBenchmarkGas(plan, protocol, profiles);
2006
+ if (reference.available) return { gasUnits: reference.gasUnits };
2007
+ return estimateShapeGas(reference.request);
2008
+ }
2009
+
2010
+ // src/protocol/AddressRegistry.ts
2011
+ import { AddressRegistry as Registry } from "@invisi-labs/protocol";
2012
+ var AddressRegistry = class extends Registry {
2013
+ prepareRegistration(options) {
2014
+ return prepareWithErrors("prepareRegistration", async () => {
2015
+ if ("provider" in options) {
2016
+ const { provider, ...preparation } = options;
2017
+ return super.prepareRegistration({
2018
+ ...preparation,
2019
+ reader: new EthersChainReader(provider)
2020
+ });
2021
+ }
2022
+ return super.prepareRegistration(options);
2023
+ });
1890
2024
  }
1891
- const measurements = profiles.flatMap((profile) => profile.measurements);
1892
- const matching = measurements.filter(
1893
- (measurement) => measurement.shape.kind === request.shape.kind && measurement.shape.target === request.shape.target && sameCircuit(measurement.circuit, request.circuit)
2025
+ };
2026
+
2027
+ // src/notes/TransactionOutput.ts
2028
+ var TransactionOutput = class {
2029
+ #output;
2030
+ constructor(output) {
2031
+ this.#output = structuredClone(output);
2032
+ Object.freeze(this);
2033
+ }
2034
+ get context() {
2035
+ return structuredClone(this.#output.context);
2036
+ }
2037
+ get recipient() {
2038
+ return structuredClone(this.#output.recipient);
2039
+ }
2040
+ get amount() {
2041
+ return this.#output.amount;
2042
+ }
2043
+ get commitment() {
2044
+ return this.#output.commitment;
2045
+ }
2046
+ get identity() {
2047
+ return this.#output.identity;
2048
+ }
2049
+ get nonce() {
2050
+ return this.#output.note?.nonce;
2051
+ }
2052
+ get change() {
2053
+ return this.#output.change;
2054
+ }
2055
+ exportPaymentNote() {
2056
+ return exportPaymentNote(this.#output);
2057
+ }
2058
+ };
2059
+
2060
+ // src/transactions/proving/proof.ts
2061
+ function publicProof(bundle) {
2062
+ return {
2063
+ formatVersion: bundle.formatVersion,
2064
+ circuitName: bundle.circuitName,
2065
+ target: bundle.target,
2066
+ bbVersion: bundle.bbVersion,
2067
+ artifactSha256: bundle.artifactSha256,
2068
+ vkSha256: bundle.vkSha256,
2069
+ proofHex: bundle.proofHex,
2070
+ proofSha256: bundle.proofSha256,
2071
+ publicInputs: [...bundle.publicInputs]
2072
+ };
2073
+ }
2074
+
2075
+ // src/transactions/proving/proof-integrity.ts
2076
+ import { sha256 } from "@noble/hashes/sha256";
2077
+ import { bytesToHex } from "@noble/hashes/utils";
2078
+ import { getBytes } from "ethers";
2079
+ function check(ok, message) {
2080
+ if (!ok) throw new Error(message);
2081
+ }
2082
+ function sameStatement(proof, expected) {
2083
+ check(
2084
+ proof.formatVersion === 1 && proof.bbVersion === "6.0.0" && proof.circuitName === expected.circuitName && proof.target === expected.target && proof.artifactSha256 === expected.artifactSha256 && proof.vkSha256 === expected.vkSha256,
2085
+ "proof release/target mismatch"
1894
2086
  );
1895
- const exact = matching.filter(
1896
- (measurement) => shapeKeys.every((key) => measurement.shape[key] === request.shape[key])
2087
+ check(
2088
+ proof.publicInputs.length === expected.expectedPublicInputs.length && proof.publicInputs.every((word2, i) => word2 === expected.expectedPublicInputs[i]),
2089
+ "proof public statement mismatch"
1897
2090
  );
1898
- if (exact.length) {
1899
- const means = exact.map((m) => {
1900
- const count = BigInt(m.samples.length);
1901
- return (m.samples.reduce((sum, s) => sum + BigInt(s.gasUsed), 0n) + count - 1n) / count;
1902
- });
1903
- return { gasUnits: means.reduce((max, v) => v > max ? v : max) };
1904
- }
1905
- if (["create-credit", "draw-credit", "top-up-credit", "cancel-credit"].includes(
1906
- request.shape.kind
1907
- ) && !matching.length)
1908
- throw new Error("No current Credit gas reference for this artifact and verification key");
1909
- const candidates = matching.length ? matching : measurements.filter(
1910
- (measurement) => measurement.shape.kind === request.shape.kind && measurement.shape.target === request.shape.target
2091
+ check(
2092
+ /^0x(?:[a-fA-F0-9]{2})+$/.test(proof.proofHex) && bytesToHex(sha256(getBytes(proof.proofHex))) === proof.proofSha256,
2093
+ "proof integrity mismatch"
1911
2094
  );
1912
- if (!candidates.length) throw new Error("No gas reference for this operation kind and target");
1913
- const dimensions = [
1914
- "noteInputs",
1915
- "noteOutputs",
1916
- "sourceTrees",
1917
- "creditInputs",
1918
- "creditOutputs",
1919
- "creditRemainders",
1920
- "deliveryBytes",
1921
- ...!matching.length ? ["capacity"] : []
1922
- ];
1923
- const groups = /* @__PURE__ */ new Map();
1924
- for (const candidate of candidates) {
1925
- const key = JSON.stringify([
1926
- shapeKeys.map((k) => candidate.shape[k]),
1927
- candidate.circuit?.circuitName,
1928
- candidate.circuit?.target,
1929
- candidate.circuit?.artifactSha256.toLowerCase(),
1930
- candidate.circuit?.vkSha256.toLowerCase()
1931
- ]);
1932
- const previous = groups.get(key);
1933
- groups.set(
1934
- key,
1935
- previous ? { ...candidate, samples: [...previous.samples, ...candidate.samples] } : candidate
1936
- );
2095
+ }
2096
+
2097
+ // src/transactions/execution/calldata.ts
2098
+ import {
2099
+ buildCancelCreditCalldata,
2100
+ buildCreateCreditCalldata,
2101
+ buildCreditDrawCalldata,
2102
+ buildShieldCalldata,
2103
+ buildTopUpCreditCalldata,
2104
+ buildTransactCalldata
2105
+ } from "@invisi-labs/protocol";
2106
+ async function encodeOperation(saved) {
2107
+ const plan = saved.plan;
2108
+ const credit = plan.credit;
2109
+ const caller = plan.fundingMode === "caller" ? plan.funding.payer : void 0;
2110
+ const common = { context: plan.context, proof: saved.proof?.proofHex ?? "0x" };
2111
+ const funded = { ...common, funding: plan.funding, ...caller ? { caller } : {} };
2112
+ let encoded;
2113
+ if (credit?.topUp) {
2114
+ encoded = buildTopUpCreditCalldata({
2115
+ ...funded,
2116
+ params: credit.topUp,
2117
+ deliveries: credit.deliveries
2118
+ });
2119
+ } else if (credit?.creation) {
2120
+ encoded = buildCreateCreditCalldata({
2121
+ ...funded,
2122
+ params: credit.creation,
2123
+ deliveries: credit.deliveries
2124
+ });
2125
+ } else if (credit?.draw) {
2126
+ encoded = buildCreditDrawCalldata({
2127
+ ...common,
2128
+ params: credit.draw,
2129
+ deliveries: credit.deliveries
2130
+ });
2131
+ } else if (credit?.cancel) {
2132
+ encoded = buildCancelCreditCalldata({ ...common, params: credit.cancel });
2133
+ } else if (plan.kind === "shield") {
2134
+ encoded = await buildShieldCalldata({
2135
+ ...funded,
2136
+ params: plan.shieldParams,
2137
+ outputs: plan.shieldOutputs,
2138
+ deliveries: plan.outputs.map((output) => output.delivery)
2139
+ });
2140
+ } else {
2141
+ encoded = buildTransactCalldata({
2142
+ ...funded,
2143
+ params: plan.params,
2144
+ deliveries: plan.outputs.map((output) => output.delivery)
2145
+ });
1937
2146
  }
1938
- const estimates = [...groups.values()].map((measurement) => {
1939
- let numerator = 1n;
1940
- let denominator = 1n;
1941
- for (const key of dimensions) {
1942
- const current2 = BigInt(request.shape[key]) + 1n;
1943
- const baseline = BigInt(measurement.shape[key]) + 1n;
1944
- if (current2 * denominator > numerator * baseline) {
1945
- numerator = current2;
1946
- denominator = baseline;
1947
- }
2147
+ return { to: plan.context.protocolAddress, data: encoded.data, value: 0n };
2148
+ }
2149
+
2150
+ // src/transactions/proving/witness.ts
2151
+ import { WitnessBuilder } from "@invisi-labs/protocol";
2152
+ async function witnessFor(protocol, plan, authorization) {
2153
+ const builder = new WitnessBuilder(protocol);
2154
+ const credit = plan.credit;
2155
+ if (credit) {
2156
+ if (!authorization) throw new Error("private Credit wallet authorization required");
2157
+ if (credit.topUp) {
2158
+ return builder.topUpCredit({
2159
+ params: credit.topUp,
2160
+ deliveries: credit.deliveries,
2161
+ funding: plan.funding,
2162
+ inputs: plan.inputs,
2163
+ state: credit.outputs[0].state,
2164
+ ...plan.outputs[0] ? { change: plan.outputs[0].note } : {},
2165
+ previous: credit.inputs[0],
2166
+ authorization
2167
+ });
1948
2168
  }
1949
- let gas = measurement.samples.reduce(
1950
- (max, sample) => BigInt(sample.gasUsed) > max ? BigInt(sample.gasUsed) : max,
1951
- 0n
1952
- );
1953
- gas = (gas * numerator + denominator - 1n) / denominator;
1954
- if (request.shape.funding === "signature" && measurement.shape.funding !== "signature")
1955
- gas *= 2n;
1956
- if (request.shape.permit && !measurement.shape.permit) gas *= 2n;
1957
- return gas;
2169
+ if (credit.creation) {
2170
+ return builder.createCredit({
2171
+ params: credit.creation,
2172
+ deliveries: credit.deliveries,
2173
+ funding: plan.funding,
2174
+ inputs: plan.inputs,
2175
+ state: credit.outputs[0].state,
2176
+ ...plan.outputs[0] ? { change: plan.outputs[0].note } : {},
2177
+ authorization
2178
+ });
2179
+ }
2180
+ if (credit.draw) {
2181
+ return builder.drawCredit({
2182
+ timestamp: plan.preparedAt.timestamp,
2183
+ params: credit.draw,
2184
+ deliveries: credit.deliveries,
2185
+ inputs: credit.inputs,
2186
+ amounts: credit.amounts,
2187
+ authorization
2188
+ });
2189
+ }
2190
+ if (credit.cancel) {
2191
+ return builder.cancelCredit({
2192
+ params: credit.cancel,
2193
+ input: credit.inputs[0],
2194
+ authorization
2195
+ });
2196
+ }
2197
+ throw new Error("unsupported Credit operation");
2198
+ }
2199
+ return builder.transact({
2200
+ params: plan.params,
2201
+ deliveries: plan.outputs.map((output) => output.delivery),
2202
+ funding: plan.funding,
2203
+ inputs: plan.inputs,
2204
+ outputs: plan.outputs.map((output) => ({
2205
+ identity: output.identity,
2206
+ amount: output.amount,
2207
+ ...output.note ? { note: output.note } : {}
2208
+ })),
2209
+ ...authorization ? { authorization } : {},
2210
+ target: plan.statement.target
1958
2211
  });
1959
- return { gasUnits: estimates.reduce((min, gas) => gas < min ? gas : min) };
1960
- }
1961
- function estimateGasReference(plan, protocol, profiles) {
1962
- const reference2 = estimateBenchmarkGas(plan, protocol, profiles);
1963
- if (reference2.available) return { gasUnits: reference2.gasUnits };
1964
- return estimateShapeGas(reference2.request);
1965
2212
  }
1966
2213
 
1967
- // src/session/protocol-events.ts
1968
- import { invisiProtocolAbi } from "@invisi-labs/protocol/contracts";
1969
- import { Interface } from "ethers";
1970
- var protocolInterface = new Interface(invisiProtocolAbi);
1971
- function decodeProtocolEvent(log) {
1972
- const decoded = protocolInterface.parseLog(log);
1973
- if (!decoded) throw new Error("Unknown protocol receipt event");
1974
- const encoded = protocolInterface.encodeEventLog(decoded.fragment, decoded.args);
1975
- if (encoded.data.toLowerCase() !== log.data.toLowerCase() || encoded.topics.length !== log.topics.length || encoded.topics.some(
1976
- (topic, index2) => topic.toLowerCase() !== log.topics[index2].toLowerCase()
1977
- ))
1978
- throw new Error("Noncanonical protocol receipt event");
1979
- const args = decoded.args.toObject(true);
1980
- for (const key of [
1981
- "tokens",
1982
- "commitments",
1983
- "nullifiers",
1984
- "outputCommitments",
1985
- "leafIndices",
1986
- "remainderLeafIndices"
1987
- ]) {
1988
- const values = decoded.args.getValue(key);
1989
- if (values) args[key] = values.toArray(true);
2214
+ // src/transactions/signing/funding.ts
2215
+ import {
2216
+ encodeAggregateFundingTypedData,
2217
+ encodeCreateCreditFundingTypedData,
2218
+ encodeShieldFundingTypedData,
2219
+ encodeTopUpCreditFundingTypedData,
2220
+ encodeTransactFundingTypedData
2221
+ } from "@invisi-labs/protocol";
2222
+ async function fundingTypedData(saved) {
2223
+ const plan = saved.plan;
2224
+ const credit = plan.credit;
2225
+ if (credit?.topUp) {
2226
+ return encodeTopUpCreditFundingTypedData(
2227
+ plan.context,
2228
+ credit.topUp,
2229
+ credit.deliveries,
2230
+ plan.funding
2231
+ );
1990
2232
  }
1991
- for (const key of ["sources", "deliveries"]) {
1992
- const values = decoded.args.getValue(key);
1993
- if (values) args[key] = values.map((value) => value.toObject(true));
2233
+ if (credit?.creation) {
2234
+ return encodeCreateCreditFundingTypedData(
2235
+ plan.context,
2236
+ credit.creation,
2237
+ credit.deliveries,
2238
+ plan.funding
2239
+ );
1994
2240
  }
1995
- const params = decoded.args.getValue("params");
1996
- if (params) {
1997
- const source = params.getValue("creation") ?? params;
1998
- const target = params.getValue("creation") ? args.params.creation : args.params;
1999
- for (const key of ["nullifiers", "remainderCommitments", "expiries", "recoveryAmounts"]) {
2000
- const values = source.getValue(key);
2001
- if (values) target[key] = values.toArray(true);
2002
- }
2003
- const sources = source.getValue("sources");
2004
- if (sources) target.sources = sources.map((value) => value.toObject(true));
2005
- }
2006
- return { eventName: decoded.name, args };
2007
- }
2008
-
2009
- // src/session/receipt-logs.ts
2010
- function readProtocolLogs(receipt, protocolAddress) {
2011
- if (receipt.status !== "success" || !/^0x[0-9a-fA-F]{64}$/.test(receipt.transactionHash) || typeof receipt.block.number !== "bigint" || receipt.block.number < 0n || !/^0x[0-9a-fA-F]{64}$/.test(receipt.block.hash))
2012
- throw new Error("Invalid canonical protocol receipt");
2013
- const seen = /* @__PURE__ */ new Set();
2014
- return receipt.logs.filter((log) => log.address.toLowerCase() === protocolAddress.toLowerCase()).map((log) => {
2015
- if (log.transactionHash.toLowerCase() !== receipt.transactionHash.toLowerCase() || !Number.isSafeInteger(log.logIndex) || log.logIndex < 0 || seen.has(log.logIndex))
2016
- throw new Error("Invalid canonical receipt log identity");
2017
- seen.add(log.logIndex);
2018
- const decoded = decodeProtocolEvent(log);
2019
- return {
2020
- address: log.address,
2021
- eventName: decoded.eventName,
2022
- args: decoded.args,
2023
- blockNumber: receipt.block.number,
2024
- blockHash: receipt.block.hash,
2025
- transactionHash: receipt.transactionHash.toLowerCase(),
2026
- logIndex: log.logIndex,
2027
- removed: false
2028
- };
2029
- }).sort((a, b) => a.logIndex - b.logIndex);
2030
- }
2031
-
2032
- // src/session/discovery.ts
2033
- import { MAX_CIPHERTEXT_BYTES } from "@invisi-labs/core";
2034
- import { isAddress } from "ethers";
2035
- import {
2036
- decodeAddress,
2037
- ReceivingAddress
2038
- } from "@invisi-labs/core";
2039
- import { normalizeOpaqueDelivery } from "@invisi-labs/protocol";
2040
- var EVENTS = /* @__PURE__ */ new Set([
2041
- "CommitmentsInserted",
2042
- "BatchOutputs",
2043
- "Shield",
2044
- "Transact",
2045
- "CrossTreeTransact",
2046
- "CreditCreated",
2047
- "CreditDraw",
2048
- "CreditToppedUp",
2049
- "CreditCancelled"
2050
- ]);
2051
- function check(ok, message) {
2052
- if (!ok) throw new Error(message);
2053
- }
2054
- function object(value) {
2055
- check(value && typeof value === "object" && !Array.isArray(value), "invalid event object");
2056
- return value;
2057
- }
2058
- function array(value) {
2059
- check(Array.isArray(value), "invalid event array");
2060
- check(
2061
- Array.from({ length: value.length }, (_, i) => i in value).every(Boolean),
2062
- "sparse event array"
2063
- );
2064
- return value;
2065
- }
2066
- function integer(value) {
2067
- check(
2068
- typeof value === "bigint" || typeof value === "number" && Number.isSafeInteger(value),
2069
- "invalid event integer"
2070
- );
2071
- const n = BigInt(value);
2072
- check(n >= 0n, "negative event integer");
2073
- return n;
2074
- }
2075
- function index(value) {
2076
- const n = integer(value);
2077
- check(n < 65536n, "invalid event leaf index");
2078
- return Number(n);
2079
- }
2080
- function hash(value) {
2081
- check(typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value), "invalid event hash");
2082
- return value.toLowerCase();
2083
- }
2084
- function contextCheck(context) {
2085
- check(context.chainId >= 0n && context.chainId < 1n << 64n, "invalid discovery chain");
2086
- check(
2087
- isAddress(context.protocolAddress) && BigInt(context.protocolAddress) !== 0n,
2088
- "invalid discovery instance/token"
2089
- );
2090
- }
2091
- var position = (tree, at) => `${tree}:${at}`;
2092
- function extractDeliveryCandidates(logs, context, confirmedThrough) {
2093
- contextCheck(context);
2094
- check(confirmedThrough >= 0n, "invalid confirmed height");
2095
- const relevant = logs.filter(
2096
- (log) => log.address.toLowerCase() === context.protocolAddress.toLowerCase() && EVENTS.has(log.eventName)
2097
- );
2098
- const insertions = /* @__PURE__ */ new Map();
2099
- const metadata = (log) => {
2100
- check(!log.removed, "discovery resync required after reorg");
2101
- check(
2102
- typeof log.blockNumber === "bigint" && log.blockNumber >= 0n && log.blockNumber <= confirmedThrough,
2103
- "event is not confirmed"
2104
- );
2105
- check(Number.isSafeInteger(log.logIndex) && log.logIndex >= 0, "invalid log position");
2106
- return {
2107
- blockNumber: log.blockNumber,
2108
- blockHash: hash(log.blockHash),
2109
- transactionHash: hash(log.transactionHash)
2110
- };
2111
- };
2112
- const insertionKey = (log, tree, at) => `${hash(log.transactionHash)}:${position(tree, at)}`;
2113
- for (const log of relevant) {
2114
- metadata(log);
2115
- if (log.eventName !== "CommitmentsInserted" && log.eventName !== "BatchOutputs") continue;
2116
- const a = object(log.args), tree = integer(a.treeId), start = index(a.startLeafIndex), leaves = array(a.commitments);
2117
- check(tree > 0n && tree < 1n << 64n, "invalid insertion tree");
2118
- check(start + leaves.length <= 65536, "insertion exceeds tree capacity");
2119
- for (const [i, leaf] of leaves.entries()) {
2120
- const key = insertionKey(log, tree, start + i), value = hash(leaf), previous = insertions.get(key);
2121
- check(!previous || previous === value, "conflicting insertion events");
2122
- insertions.set(key, value);
2123
- }
2124
- }
2125
- const results = /* @__PURE__ */ new Map();
2126
- const add = (log, tree, at, commitment, role, wire, token = log.args?.token ?? log.args?.params?.token ?? log.args?.params?.creation?.token) => {
2127
- check(tree > 0n && tree < 1n << 64n, "invalid output tree");
2128
- const inserted = insertions.get(insertionKey(log, tree, at));
2129
- check(inserted, "delivery has no matching canonical insertion");
2130
- if (commitment !== void 0)
2131
- check(hash(commitment) === inserted, "delivery commitment conflicts with insertion");
2132
- const delivery = normalizeOpaqueDelivery(wire, { allowEmpty: role === "funds" });
2133
- check(
2134
- typeof token === "string" && isAddress(token) && BigInt(token) !== 0n,
2135
- "invalid event token"
2136
- );
2137
- const value = {
2138
- tokenAddress: token,
2139
- treeId: tree,
2140
- leafIndex: at,
2141
- commitment: inserted,
2142
- role,
2143
- delivery,
2144
- ...metadata(log)
2145
- };
2146
- const key = `${insertionKey(log, tree, at)}:${role}`, previous = results.get(key);
2147
- check(
2148
- !previous || previous.tokenAddress.toLowerCase() === token.toLowerCase() && previous.delivery.ciphertext === delivery.ciphertext && previous.delivery.clue === delivery.clue && previous.blockHash === value.blockHash,
2149
- "conflicting delivery events"
2150
- );
2151
- results.set(key, value);
2152
- };
2153
- const creation = (log, a, fields) => {
2154
- const tree = integer(a.outputTreeId), leaves = array(a.leafIndices), ds = array(a.deliveries);
2155
- const change = hash(fields.changeCommitment), hasChange = BigInt(change) !== 0n;
2156
- check(
2157
- ds.length === 3 && leaves.length === (hasChange ? 2 : 1),
2158
- "invalid Credit creation delivery slots"
2159
- );
2160
- add(log, tree, index(leaves[0]), fields.creditCommitment, "owner", ds[0]);
2161
- add(log, tree, index(leaves[0]), fields.creditCommitment, "spender", ds[1]);
2162
- if (hasChange) add(log, tree, index(leaves[1]), change, "funds", ds[2]);
2163
- else normalizeOpaqueDelivery(ds[2], { empty: true });
2164
- };
2165
- for (const log of relevant) {
2166
- const a = object(log.args);
2167
- if (log.eventName === "Shield") {
2168
- const ds = array(a.deliveries), tree = integer(a.treeId), start = index(a.startLeafIndex);
2169
- check(start + ds.length <= 65536, "shield exceeds tree capacity");
2170
- ds.forEach((d, i) => add(log, tree, start + i, void 0, "funds", d));
2171
- } else if (log.eventName === "Transact" || log.eventName === "CrossTreeTransact") {
2172
- const cs = array(a.outputCommitments), ds = array(a.deliveries), leaves = array(a.leafIndices), tree = integer(a.outputTreeId);
2173
- check(
2174
- cs.length === ds.length && cs.length === leaves.length,
2175
- "invalid transact delivery slots"
2176
- );
2177
- cs.forEach((c, i) => add(log, tree, index(leaves[i]), c, "funds", ds[i]));
2178
- } else if (log.eventName === "BatchOutputs") {
2179
- const tokens = array(a.tokens);
2180
- const cs = array(a.commitments), tree = integer(a.treeId), start = index(a.startLeafIndex), packed = a.packedDeliveries;
2181
- check(
2182
- typeof packed === "string" && /^0x(?:[0-9a-fA-F]{2})*$/.test(packed),
2183
- "invalid packed deliveries"
2184
- );
2185
- check(tokens.length === cs.length, "batch token count mismatch");
2186
- check(start + cs.length <= 65536, "batch exceeds tree capacity");
2187
- let offset = 2;
2188
- cs.forEach((c, i) => {
2189
- check(offset + 4 <= packed.length, "truncated delivery length prefix");
2190
- const length = Number.parseInt(packed.slice(offset, offset + 4), 16);
2191
- offset += 4;
2192
- check(
2193
- length <= MAX_CIPHERTEXT_BYTES && offset + length * 2 + (length === 0 ? 0 : 136) <= packed.length,
2194
- "invalid packed delivery length"
2195
- );
2196
- const ciphertext = `0x${packed.slice(offset, offset + length * 2)}`;
2197
- offset += length * 2;
2198
- const clue = length === 0 ? "0x" : `0x${packed.slice(offset, offset + 136)}`;
2199
- if (length !== 0) offset += 136;
2200
- add(log, tree, start + i, c, "funds", { ciphertext, clue }, tokens[i]);
2201
- });
2202
- check(offset === packed.length, "trailing packed delivery bytes");
2203
- } else if (log.eventName === "CreditCreated") creation(log, a, a);
2204
- else if (log.eventName === "CreditToppedUp")
2205
- creation(log, a, object(object(a.params).creation));
2206
- else if (log.eventName === "CreditDraw") {
2207
- const p = object(a.params), cs = array(p.remainderCommitments), ds = array(a.deliveries), leaves = array(a.remainderLeafIndices), tree = integer(a.outputTreeId);
2208
- check(
2209
- ds.length === 2 * cs.length && leaves.length === cs.length,
2210
- "invalid draw delivery slots"
2211
- );
2212
- cs.forEach((c, i) => {
2213
- if (BigInt(hash(c)) === 0n) {
2214
- check(integer(leaves[i]) === 0n, "absent remainder has nonzero position");
2215
- normalizeOpaqueDelivery(ds[2 * i], { empty: true });
2216
- normalizeOpaqueDelivery(ds[2 * i + 1], { empty: true });
2217
- } else {
2218
- add(log, tree, index(leaves[i]), c, "owner", ds[2 * i]);
2219
- add(log, tree, index(leaves[i]), c, "spender", ds[2 * i + 1]);
2220
- }
2221
- });
2222
- }
2223
- }
2224
- return [...results.values()];
2225
- }
2226
- async function decryptCandidates(logs, options) {
2227
- contextCheck(options.context);
2228
- decodeAddress(ReceivingAddress.fromIdentity(options.viewing.ownerAddress).toString());
2229
- check(
2230
- !logs.some(
2231
- (log) => log.address.toLowerCase() === options.context.protocolAddress.toLowerCase() && log.removed
2232
- ),
2233
- "discovery resync required after reorg"
2234
- );
2235
- const candidates = extractDeliveryCandidates(logs, options.context, options.confirmedThrough);
2236
- const positions = /* @__PURE__ */ new Map();
2237
- const unique = /* @__PURE__ */ new Map();
2238
- for (const candidate of candidates) {
2239
- const key = position(candidate.treeId, candidate.leafIndex);
2240
- const previous = positions.get(key);
2241
- check(
2242
- !previous || previous.commitment === candidate.commitment && previous.blockHash === candidate.blockHash && previous.transactionHash === candidate.transactionHash && previous.tokenAddress.toLowerCase() === candidate.tokenAddress.toLowerCase(),
2243
- "conflicting canonical position"
2244
- );
2245
- positions.set(key, candidate);
2246
- unique.set(`${key}:${candidate.role}`, candidate);
2247
- }
2248
- const notes = [];
2249
- const credits = [];
2250
- for (const candidate of unique.values()) {
2251
- const { delivery, role, ...positionData } = candidate;
2252
- const context = {
2253
- chainId: options.context.chainId,
2254
- protocolAddress: BigInt(options.context.protocolAddress),
2255
- tokenAddress: BigInt(candidate.tokenAddress),
2256
- outputCommitment: candidate.commitment
2257
- };
2258
- try {
2259
- if (role === "funds") {
2260
- if (delivery.ciphertext === "0x") continue;
2261
- const note = await options.viewing.decryptNote(delivery.ciphertext, context);
2262
- notes.push({ ...positionData, note });
2263
- } else {
2264
- const state = await options.viewing.decryptCredit(delivery.ciphertext, {
2265
- ...context,
2266
- role
2267
- });
2268
- credits.push({ ...positionData, role, state });
2269
- }
2270
- } catch {
2271
- }
2272
- }
2273
- return { notes, credits };
2274
- }
2275
-
2276
- // src/session/DisclosureVerifier.ts
2277
- function check2(value, message) {
2278
- if (!value) throw new Error(message);
2279
- }
2280
- function reference(value) {
2281
- check2(/^0x[0-9a-fA-F]{64}$/.test(value.transactionHash), "invalid disclosure transaction hash");
2282
- check2(
2283
- typeof value.treeId === "bigint" && value.treeId > 0n && value.treeId < 1n << 64n,
2284
- "invalid disclosure tree ID"
2285
- );
2286
- check2(
2287
- Number.isSafeInteger(value.leafIndex) && value.leafIndex >= 0 && value.leafIndex < 65536,
2288
- "invalid disclosure leaf index"
2289
- );
2290
- check2(["funds", "owner", "spender"].includes(value.role), "invalid disclosure reference role");
2291
- return {
2292
- transactionHash: value.transactionHash.toLowerCase(),
2293
- treeId: value.treeId,
2294
- leafIndex: value.leafIndex,
2295
- role: value.role
2296
- };
2297
- }
2298
- var DisclosureVerifier = class {
2299
- #protocol;
2300
- #reader;
2301
- #confirmations;
2302
- constructor(options) {
2303
- const confirmations = options.confirmations ?? 1;
2304
- check2(
2305
- Number.isSafeInteger(confirmations) && confirmations >= 1,
2306
- "invalid disclosure confirmations"
2307
- );
2308
- this.#protocol = options.protocol;
2309
- this.#reader = "provider" in options ? new EthersChainReader(options.provider) : options.reader;
2310
- this.#confirmations = BigInt(confirmations);
2311
- Object.freeze(this);
2312
- }
2313
- async barrier(block) {
2314
- await this.#protocol.validateDeployment(this.#reader);
2315
- const canonical2 = await this.#reader.getBlock(block.number);
2316
- check2(
2317
- canonical2.number === block.number && canonical2.hash.toLowerCase() === block.hash.toLowerCase(),
2318
- "disclosure canonical block changed; reorg verification required"
2319
- );
2320
- const head = await this.#reader.getLatestBlockNumber();
2321
- check2(
2322
- head >= block.number && head - block.number + 1n >= this.#confirmations,
2323
- "insufficient disclosure confirmations"
2324
- );
2325
- check2(
2326
- await this.#reader.getChainId() === this.#protocol.context.chainId,
2327
- "disclosure chain changed during verification"
2328
- );
2329
- return head - block.number + 1n;
2330
- }
2331
- /** Resolve the exact inserted output and its delivery from a successful canonical receipt. */
2332
- async readOutput(value) {
2333
- const selected = reference(value);
2334
- await this.#protocol.validateDeployment(this.#reader);
2335
- const received = await this.#reader.getReceipt(selected.transactionHash);
2336
- check2(received, "disclosure transaction receipt is unavailable");
2337
- const receipt = structuredClone(received);
2338
- check2(receipt.status === "success", "disclosure requires a successful transaction receipt");
2339
- check2(
2340
- receipt.transactionHash.toLowerCase() === selected.transactionHash,
2341
- "disclosure receipt transaction mismatch"
2342
- );
2343
- check2(
2344
- typeof receipt.block.number === "bigint" && receipt.block.number >= 0n && /^0x[0-9a-fA-F]{64}$/.test(receipt.block.hash),
2345
- "invalid disclosure receipt block"
2346
- );
2347
- const { context } = this.#protocol;
2348
- const logs = readProtocolLogs(receipt, context.protocolAddress);
2349
- const candidates = extractDeliveryCandidates(logs, context, receipt.block.number).filter(
2350
- (candidate2) => candidate2.treeId === selected.treeId && candidate2.leafIndex === selected.leafIndex && candidate2.role === selected.role && candidate2.tokenAddress.toLowerCase() === context.tokenAddress.toLowerCase()
2351
- );
2352
- check2(
2353
- candidates.length === 1,
2354
- "disclosure output has no unique canonical insertion and delivery"
2355
- );
2356
- const candidate = candidates[0];
2357
- const confirmations = await this.barrier(receipt.block);
2358
- return Object.freeze({
2359
- reference: Object.freeze(selected),
2360
- block: Object.freeze({ ...receipt.block }),
2361
- confirmations,
2362
- output: Object.freeze({
2363
- chainId: context.chainId,
2364
- protocolAddress: BigInt(context.protocolAddress),
2365
- tokenAddress: BigInt(context.tokenAddress),
2366
- outputCommitment: candidate.commitment,
2367
- role: selected.role,
2368
- ciphertext: candidate.delivery.ciphertext
2369
- })
2370
- });
2371
- }
2372
- async verify(disclosure, value) {
2373
- const evidence = await this.readOutput(value);
2374
- const result = await disclosure.verify(evidence.output);
2375
- const confirmations = await this.barrier(evidence.block);
2376
- return Object.freeze({
2377
- result: Object.freeze(result),
2378
- block: evidence.block,
2379
- confirmations,
2380
- output: Object.freeze({
2381
- ...evidence.reference,
2382
- commitment: evidence.output.outputCommitment
2383
- })
2384
- });
2385
- }
2386
- toJSON() {
2387
- return { disclosureVerifier: true, confirmations: this.#confirmations.toString() };
2388
- }
2389
- [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
2390
- return this.toJSON();
2391
- }
2392
- };
2393
-
2394
- // src/session/store.ts
2395
- import { FIELD_MODULUS, normalizeCreditDeliveryState } from "@invisi-labs/core";
2396
- import { sha256 } from "@noble/hashes/sha256";
2397
- import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils";
2398
- function scopeKey(scope) {
2399
- if (typeof scope.namespace !== "string" || !scope.namespace.length || scope.namespace.length > 256)
2400
- throw new Error("invalid wallet namespace");
2401
- if (typeof scope.chainId !== "bigint" || scope.chainId < 0n || scope.chainId >= 1n << 64n)
2402
- throw new Error("invalid scope chain");
2403
- for (const address2 of [scope.protocolAddress])
2404
- if (!/^0x[0-9a-fA-F]{40}$/.test(address2) || BigInt(address2) === 0n)
2405
- throw new Error("invalid scope deployment");
2406
- if (!/^0x[0-9a-fA-F]{40}$/.test(scope.ownerAddress) || BigInt(scope.ownerAddress) <= 0n || BigInt(scope.ownerAddress) >= 1n << 160n)
2407
- throw new Error("invalid scope wallet");
2408
- return bytesToHex(
2409
- sha256(
2410
- utf8ToBytes(
2411
- "Invisi.WalletScope.v1:" + JSON.stringify([
2412
- scope.namespace,
2413
- scope.chainId.toString(),
2414
- scope.protocolAddress.toLowerCase(),
2415
- scope.ownerAddress.toLowerCase()
2416
- ])
2417
- )
2418
- )
2419
- );
2420
- }
2421
- function initialState(scope) {
2422
- scopeKey(scope);
2423
- return {
2424
- schemaVersion: 1,
2425
- scope: { ...scope },
2426
- revision: 0,
2427
- notes: {},
2428
- credits: {},
2429
- reservations: {},
2430
- operations: {}
2431
- };
2432
- }
2433
- function validateState(state, scope, revision) {
2434
- if (!state || state.schemaVersion !== 1 || scopeKey(state.scope) !== scopeKey(scope) || state.revision !== revision)
2435
- throw new Error("wallet state scope/schema/revision mismatch");
2436
- if (!Number.isSafeInteger(revision) || revision < 0)
2437
- throw new Error("invalid wallet state revision");
2438
- for (const field of [state.notes, state.credits, state.reservations, state.operations])
2439
- if (!field || typeof field !== "object" || Array.isArray(field))
2440
- throw new Error("invalid wallet state records");
2441
- for (const record of Object.values(state.notes)) {
2442
- if (typeof record.note.tokenAddress !== "bigint" || record.note.tokenAddress <= 0n || record.note.tokenAddress >= 1n << 160n || record.note.chainId !== scope.chainId || record.note.protocolAddress !== BigInt(scope.protocolAddress) || record.note.ownerAddress.toLowerCase() !== scope.ownerAddress.toLowerCase() || typeof record.note.amount !== "bigint" || record.note.amount <= 0n || record.note.amount >= 1n << 128n)
2443
- throw new Error("invalid stored note context or amount");
2444
- if (!["unspent", "unknown", "spent"].includes(record.spent))
2445
- throw new Error("invalid spent state");
2446
- }
2447
- for (const record of Object.values(state.credits)) {
2448
- const note = normalizeCreditDeliveryState(record.state).note;
2449
- if (note.chainId !== scope.chainId || note.protocolAddress !== BigInt(scope.protocolAddress) || !["owner", "spender"].includes(record.role) || (record.role === "owner" ? note.ownerWalletIdentity : note.spenderWalletIdentity).toLowerCase() !== scope.ownerAddress.toLowerCase() || !["unspent", "unknown", "spent"].includes(record.spent))
2450
- throw new Error("invalid stored Credit context or role");
2451
- }
2452
- for (const record of [...Object.values(state.notes), ...Object.values(state.credits)]) {
2453
- if (!/^0x[0-9a-fA-F]{64}$/.test(record.commitment) || BigInt(record.commitment) <= 0n || BigInt(record.commitment) >= 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))
2454
- throw new Error("invalid stored receipt position");
2455
- }
2456
- for (const [id, operation] of Object.entries(state.reservations))
2457
- if (!state.operations[operation] || !state.notes[id] && !Object.values(state.credits).some(
2458
- (record) => creditReservationKey(record.commitment) === id
2459
- ) && !["submitted", "uncertain", "reorged"].includes(
2460
- state.operations[operation].status
2461
- ))
2462
- throw new Error("orphan input reservation");
2463
- }
2464
- var TransactionalWalletStore = class {
2465
- persistent;
2466
- #records;
2467
- #codec;
2468
- constructor(records, codec, options) {
2469
- this.#records = records;
2470
- this.#codec = codec;
2471
- this.persistent = options.persistent;
2472
- }
2473
- async readSnapshot(scope) {
2474
- const key = scopeKey(scope), record = await this.#records.read(key);
2475
- if (!record) return initialState(scope);
2476
- const state = await this.#codec.decode(record.data, { key, revision: record.revision });
2477
- validateState(state, scope, record.revision);
2478
- return state;
2479
- }
2480
- async transaction(scope, action) {
2481
- if (action.constructor.name === "AsyncFunction")
2482
- throw new Error("wallet transaction action must be synchronous");
2483
- const copied = { ...scope }, key = scopeKey(copied);
2484
- for (let attempt = 0; attempt < 100; attempt++) {
2485
- const draft = await this.readSnapshot(copied), previous = draft.revision;
2486
- const result = action(draft);
2487
- if (result && typeof result === "object" && "then" in result) {
2488
- Promise.resolve(result).catch(() => {
2489
- });
2490
- throw new Error("wallet transaction action must be synchronous");
2491
- }
2492
- validateState(draft, copied, previous);
2493
- if (!Number.isSafeInteger(previous + 1))
2494
- throw new Error("wallet state revision exhausted");
2495
- draft.revision = previous + 1;
2496
- const data = await this.#codec.encode(draft, { key, revision: draft.revision });
2497
- if (await this.#records.compareAndSwap(key, previous, {
2498
- revision: draft.revision,
2499
- data
2500
- }))
2501
- return result;
2502
- }
2503
- throw new Error("wallet transaction contention limit exceeded");
2504
- }
2505
- toJSON() {
2506
- return { walletStore: true, persistent: this.persistent };
2507
- }
2508
- [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
2509
- return this.toJSON();
2510
- }
2511
- };
2512
- var MemoryAtomicRecords = class {
2513
- #values = /* @__PURE__ */ new Map();
2514
- async read(key) {
2515
- const value = this.#values.get(key);
2516
- return value ? { revision: value.revision, data: Uint8Array.from(value.data) } : void 0;
2241
+ if (plan.kind === "shield") {
2242
+ return encodeShieldFundingTypedData({
2243
+ context: plan.context,
2244
+ params: plan.shieldParams,
2245
+ outputs: plan.shieldOutputs,
2246
+ deliveries: plan.outputs.map((output) => output.delivery),
2247
+ funding: plan.funding
2248
+ });
2517
2249
  }
2518
- async compareAndSwap(key, previousRevision, next) {
2519
- if ((this.#values.get(key)?.revision ?? 0) !== previousRevision) return false;
2520
- this.#values.set(key, { revision: next.revision, data: Uint8Array.from(next.data) });
2521
- return true;
2250
+ if (plan.statement?.target === "noir-recursive") {
2251
+ return encodeAggregateFundingTypedData(plan.context, {
2252
+ params: plan.params,
2253
+ deliveries: plan.outputs.map((output) => output.delivery),
2254
+ funding: plan.funding
2255
+ });
2522
2256
  }
2523
- };
2257
+ return encodeTransactFundingTypedData(
2258
+ plan.context,
2259
+ saved.proof.proofHex,
2260
+ plan.params,
2261
+ plan.outputs.map((output) => output.delivery),
2262
+ plan.funding
2263
+ );
2264
+ }
2524
2265
 
2525
- // src/session/operation-execution.ts
2526
- import { Signature, SigningKey, getBytes as getBytes2, concat, verifyTypedData } from "ethers";
2266
+ // src/transactions/execution/operation-execution.ts
2527
2267
  import {
2528
- computeWalletAuthorizationDigest,
2529
2268
  AuthorizedStatement,
2530
2269
  computeOwnerAddress,
2270
+ computeWalletAuthorizationDigest,
2531
2271
  createSigningRequest,
2532
2272
  walletAuthorizationTypedData
2533
2273
  } from "@invisi-labs/core";
2534
- import { WitnessBuilder } from "@invisi-labs/protocol";
2535
-
2536
- // src/session/proof-integrity.ts
2537
- import { getBytes } from "ethers";
2538
- import { sha256 as sha2562 } from "@noble/hashes/sha256";
2539
- import { bytesToHex as bytesToHex2 } from "@noble/hashes/utils";
2540
- function check3(ok, message) {
2541
- if (!ok) throw new Error(message);
2542
- }
2543
- function sameStatement(proof, expected) {
2544
- check3(
2545
- proof.formatVersion === 1 && proof.bbVersion === "6.0.0" && proof.circuitName === expected.circuitName && proof.target === expected.target && proof.artifactSha256 === expected.artifactSha256 && proof.vkSha256 === expected.vkSha256,
2546
- "proof release/target mismatch"
2547
- );
2548
- check3(
2549
- proof.publicInputs.length === expected.expectedPublicInputs.length && proof.publicInputs.every((word2, i) => word2 === expected.expectedPublicInputs[i]),
2550
- "proof public statement mismatch"
2551
- );
2552
- check3(
2553
- /^0x(?:[a-fA-F0-9]{2})+$/.test(proof.proofHex) && bytesToHex2(sha2562(getBytes(proof.proofHex))) === proof.proofSha256,
2554
- "proof integrity mismatch"
2555
- );
2556
- }
2557
-
2558
- // src/session/operation-execution.ts
2274
+ import { WitnessBuilder as WitnessBuilder2 } from "@invisi-labs/protocol";
2275
+ import { Signature, SigningKey, concat, getBytes as getBytes2, verifyTypedData } from "ethers";
2559
2276
  async function authorizeOperation(plan, provider) {
2560
2277
  if (provider.ownerAddress !== plan.scope.ownerAddress)
2561
2278
  throw new Error("wallet authorization capability mismatch");
@@ -2579,7 +2296,7 @@ async function proveOperation(protocol, plan, authorization, prover, options) {
2579
2296
  throw new Error("aggregate execution requires a 14-slot transfer");
2580
2297
  }
2581
2298
  if (plan.kind === "transfer") {
2582
- plan.statement = WitnessBuilder.statement(
2299
+ plan.statement = WitnessBuilder2.statement(
2583
2300
  frozen.circuitName,
2584
2301
  frozen.expectedPublicInputs,
2585
2302
  execution === "aggregate" ? "noir-recursive" : "evm"
@@ -2658,753 +2375,164 @@ async function fundingSigningRequest(saved, runtime) {
2658
2375
  async function acceptFundingSignature(saved, signature, runtime) {
2659
2376
  if (!/^0x(?:[a-fA-F0-9]{2})+$/.test(signature)) {
2660
2377
  throw new Error("invalid EVM funding signature");
2661
- }
2662
- const request = await fundingSigningRequest(saved, runtime);
2663
- return verifyFundingSignature(
2664
- request,
2665
- saved.plan.funding.payer,
2666
- signature,
2667
- runtime.options.fundingVerifier
2668
- );
2669
- }
2670
- async function verifyFundingSignature(request, payer, signature, verifier) {
2671
- if (!/^0x(?:[a-fA-F0-9]{2})+$/.test(signature)) {
2672
- throw new Error("invalid EVM funding signature");
2673
- }
2674
- const types = Object.fromEntries(
2675
- Object.entries(request.types).map(([name, fields]) => [name, [...fields]])
2676
- );
2677
- let validEOA = false;
2678
- try {
2679
- validEOA = verifyTypedData(request.domain, types, request.message, signature).toLowerCase() === payer.toLowerCase();
2680
- if (validEOA) {
2681
- signature = Signature.from(signature).serialized;
2682
- }
2683
- } catch {
2684
- validEOA = false;
2685
- }
2686
- const valid = verifier ? await verifier.verifyTypedData({
2687
- ...structuredClone(request),
2688
- address: payer,
2689
- signature
2690
- }) : validEOA;
2691
- if (valid !== true) {
2692
- throw new Error("invalid EVM funding signature");
2693
- }
2694
- return signature;
2695
- }
2696
-
2697
- // src/session/operation-preview.ts
2698
- import { normalizeFunding } from "@invisi-labs/protocol";
2699
- function operationPreview(plan, status) {
2700
- const p = plan;
2701
- const fee = p.params ?? p.shieldParams ?? p.credit?.creation ?? p.credit?.cancel;
2702
- const funding = normalizeFunding(p.funding);
2703
- return {
2704
- id: p.id,
2705
- kind: p.kind,
2706
- status,
2707
- context: { ...p.context },
2708
- inputs: inputFacts(p).length,
2709
- inputDetails: [
2710
- ...p.inputs.map((input) => ({
2711
- kind: "notes",
2712
- commitment: input.commitment,
2713
- amount: input.note.amount,
2714
- owner: input.note.ownerAddress,
2715
- treeId: input.treeId,
2716
- leafIndex: input.leafIndex
2717
- })),
2718
- ...(p.credit?.inputs ?? []).map((input) => ({
2719
- kind: "credits",
2720
- commitment: input.commitment,
2721
- amount: input.state.note.amount,
2722
- owner: input.state.note.ownerWalletIdentity,
2723
- spender: input.state.note.spenderWalletIdentity,
2724
- expiry: input.state.note.expiry,
2725
- role: input.role,
2726
- treeId: input.treeId,
2727
- leafIndex: input.leafIndex
2728
- }))
2729
- ],
2730
- funding: {
2731
- payer: funding.payer,
2732
- amount: funding.amount,
2733
- authorization: p.fundingMode,
2734
- sigNonce: funding.sigNonce,
2735
- deadline: funding.deadline,
2736
- permit: { deadline: funding.permit.deadline }
2737
- },
2738
- creditOutputs: p.credit?.outputs.map((output) => ({
2739
- commitment: output.commitment,
2740
- amount: output.state.note.amount,
2741
- expiry: output.state.note.expiry,
2742
- owner: output.state.note.ownerWalletIdentity,
2743
- spender: output.state.note.spenderWalletIdentity,
2744
- ownerPayout: output.state.note.ownerPayout,
2745
- spenderPayout: output.state.note.spenderPayout
2746
- })),
2747
- outputs: p.outputs.map((output) => ({
2748
- commitment: output.commitment,
2749
- to: output.address,
2750
- amount: output.amount,
2751
- change: output.change
2752
- })),
2753
- capacity: p.capacity,
2754
- deadline: p.deadline,
2755
- feeRecipient: fee?.feeRecipient,
2756
- feeAmount: fee?.feeAmount ?? 0n,
2757
- fundingAmount: p.funding.amount ?? 0n,
2758
- fundingPayer: p.funding.payer,
2759
- withdrawalAmount: p.params?.withdrawalAmount ?? p.credit?.draw?.totalAmount ?? (p.credit?.cancel ? p.credit.cancel.amount - (p.credit.cancel.feeAmount ?? 0n) : void 0) ?? 0n,
2760
- withdrawalAddress: p.params?.withdrawalAddress ?? p.credit?.draw?.spender ?? p.credit?.cancel?.owner,
2761
- permissions: {
2762
- walletAuthorization: requiresAuthorization(p),
2763
- proof: p.kind !== "shield",
2764
- fundingAuthorization: p.fundingMode === "signature"
2765
- }
2766
- };
2767
- }
2768
-
2769
- // src/session/Operation.ts
2770
- import { MissingCapabilityError } from "@invisi-labs/core";
2771
- import { normalizeFunding as normalizeFunding2 } from "@invisi-labs/protocol";
2772
-
2773
- // src/session/operation-confirmation.ts
2774
- import { computeCreditNullifier } from "@invisi-labs/core";
2775
-
2776
- // src/session/aggregate-receipt.ts
2777
- import { Interface as Interface2 } from "ethers";
2778
- import { invisiProtocolAbi as invisiProtocolAbi2 } from "@invisi-labs/protocol/contracts";
2779
- import { buildTransactCalldata } from "@invisi-labs/protocol";
2780
- var aggregateAbi = invisiProtocolAbi2.filter(
2781
- (item) => item.type === "function" && item.name === "transact" && item.inputs.length === 2 && item.inputs[1]?.type === "tuple[]"
2782
- );
2783
- function matchesAggregateTransaction(saved, transaction) {
2784
- const plan = saved.plan;
2785
- if (plan.statement?.target !== "noir-recursive" || !plan.params || !saved.externalized || transaction.to.toLowerCase() !== plan.context.protocolAddress.toLowerCase() || transaction.value !== 0n)
2786
- return false;
2787
- try {
2788
- const decoded = new Interface2(aggregateAbi).parseTransaction({ data: transaction.data }), requests = decoded.args[1].map((request) => {
2789
- const member = request.toObject(true);
2790
- member.params.sources = request.params.sources.map(
2791
- (source) => source.toObject(true)
2792
- );
2793
- member.params.nullifiers = request.params.nullifiers.toArray(true);
2794
- member.params.outputCommitments = request.params.outputCommitments.toArray(true);
2795
- member.deliveries = request.deliveries.map(
2796
- (delivery) => delivery.toObject(true)
2797
- );
2798
- member.funding.permit.v = Number(member.funding.permit.v);
2799
- return {
2800
- params: member.params,
2801
- deliveries: member.deliveries,
2802
- funding: member.funding
2803
- };
2804
- });
2805
- if (decoded.name !== "transact" || !Array.isArray(requests) || ![4, 8, 12].includes(requests.length))
2806
- return false;
2807
- const canonical2 = (request) => buildTransactCalldata({
2808
- context: { ...plan.context, tokenAddress: request.params.token },
2809
- proof: "0x00",
2810
- params: request.params,
2811
- deliveries: request.deliveries,
2812
- funding: request.funding,
2813
- caller: transaction.from
2814
- }).data.toLowerCase();
2815
- const expected = buildTransactCalldata({
2816
- context: plan.context,
2817
- proof: "0x00",
2818
- params: plan.params,
2819
- deliveries: plan.outputs.map((output) => output.delivery),
2820
- funding: plan.funding,
2821
- caller: transaction.from
2822
- }).data.toLowerCase();
2823
- return requests.filter((request) => canonical2(request) === expected).length === 1;
2824
- } catch {
2825
- return false;
2826
- }
2827
- }
2828
-
2829
- // src/session/receipts.ts
2830
- function canonical(value) {
2831
- return JSON.stringify(value, (_, item) => {
2832
- if (typeof item === "bigint") {
2833
- return ["bigint", item.toString()];
2834
- }
2835
- if (item instanceof Uint8Array) {
2836
- return Array.from(item);
2837
- }
2838
- if (item && typeof item === "object" && !Array.isArray(item)) {
2839
- return Object.fromEntries(
2840
- Object.keys(item).sort().map((key) => [key, item[key]])
2841
- );
2842
- }
2843
- return item;
2844
- });
2845
- }
2846
- function validateInheritance(state, record) {
2847
- const source = record.receivingKeySource;
2848
- if (!source) return false;
2849
- const parent = state.credits[source.commitment + ":" + record.role];
2850
- if (!parent) {
2851
- throw new Error("Credit receiving inheritance has no authenticated parent");
2852
- }
2853
- const previous = parent.state;
2854
- const successor = record.state;
2855
- const immutableFields = [
2856
- "chainId",
2857
- "protocolAddress",
2858
- "tokenAddress",
2859
- "spenderWalletIdentity",
2860
- "ownerWalletIdentity",
2861
- "spenderPayout",
2862
- "ownerPayout",
2863
- "creditSharedKey"
2864
- ];
2865
- if (immutableFields.some((field) => previous.note[field] !== successor.note[field]) || canonical(previous.ownerReceivingKeys) !== canonical(successor.ownerReceivingKeys) || canonical(previous.spenderReceivingKeys) !== canonical(successor.spenderReceivingKeys)) {
2866
- throw new Error("Credit receiving inheritance changed immutable fields");
2867
- }
2868
- if (source.kind === "draw") {
2869
- if (successor.note.amount >= previous.note.amount || successor.note.expiry !== previous.note.expiry) {
2870
- throw new Error("invalid Credit receiving inheritance");
2871
- }
2872
- } else if (source.kind !== "top-up" || successor.note.amount <= previous.note.amount || successor.note.expiry < previous.note.expiry) {
2873
- throw new Error("invalid Credit receiving inheritance");
2874
- }
2875
- return true;
2876
- }
2877
- function mergeSpentObservation(previous, incoming) {
2878
- const next = incoming.checkedThrough;
2879
- const old = previous.checkedThrough;
2880
- if (!next || incoming.spent === "unknown") {
2881
- return;
2882
- }
2883
- if (old && next.number < old.number) {
2884
- return;
2885
- }
2886
- if (old && next.number === old.number && next.hash !== old.hash) {
2887
- throw new Error("canonical spent checkpoint conflict requires resync");
2888
- }
2889
- if (previous.spent === "spent" && incoming.spent !== "spent") {
2890
- throw new Error("canonical spent observation conflict requires resync");
2891
- }
2892
- previous.spent = incoming.spent;
2893
- previous.checkedThrough = { ...next };
2894
- }
2895
- function mergeReceiptRecords(state, batch) {
2896
- if (state.invalidated) {
2897
- throw new Error("wallet inventory is invalidated");
2898
- }
2899
- const checkpoint = { ...batch.checkpoint };
2900
- for (const record of [...batch.notes, ...batch.credits]) {
2901
- if (record.blockNumber > checkpoint.number) {
2902
- throw new Error("receipt exceeds confirmed checkpoint");
2903
- }
2904
- const atPosition = [...Object.values(state.notes), ...Object.values(state.credits)].filter(
2905
- (existing) => existing.treeId === record.treeId && existing.leafIndex === record.leafIndex
2906
- );
2907
- if (atPosition.some(
2908
- (existing) => existing.commitment !== record.commitment || existing.blockHash !== record.blockHash
2909
- )) {
2910
- throw new Error("canonical position conflict requires resync");
2911
- }
2912
- if ("note" in record) {
2913
- const previous = state.notes[record.commitment];
2914
- if (previous) {
2915
- if (canonical(previous.note) !== canonical(record.note)) {
2916
- throw new Error("canonical note preimage conflict");
2917
- }
2918
- mergeSpentObservation(previous, record);
2919
- continue;
2920
- }
2921
- state.notes[record.commitment] = record;
2922
- } else {
2923
- const key = record.commitment + ":" + record.role;
2924
- const previous = state.credits[key];
2925
- const inherited = validateInheritance(state, record);
2926
- if (previous) {
2927
- if (canonical(previous.state.note) !== canonical(record.state.note)) {
2928
- throw new Error("canonical Credit preimage conflict");
2929
- }
2930
- if (canonical(previous.state) !== canonical(record.state)) {
2931
- if (!inherited) {
2932
- if (previous.receivingKeySource) {
2933
- mergeSpentObservation(previous, record);
2934
- continue;
2935
- }
2936
- throw new Error("Credit receiving metadata conflict");
2937
- }
2938
- previous.state = record.state;
2939
- previous.receivingKeySource = record.receivingKeySource;
2940
- } else if (inherited) {
2941
- previous.receivingKeySource = record.receivingKeySource;
2942
- }
2943
- mergeSpentObservation(previous, record);
2944
- continue;
2945
- }
2946
- state.credits[key] = record;
2947
- }
2948
- }
2949
- }
2950
-
2951
- // src/session/operation-confirmation.ts
2952
- function check4(ok, message) {
2953
- if (!ok) {
2954
- throw new Error(message);
2955
- }
2956
- }
2957
- async function barrier(runtime, state, block) {
2958
- const checkpoints = [
2959
- state.observedThrough,
2960
- ...block ? [{ number: block.number, hash: block.hash }] : []
2961
- ];
2962
- for (const point of checkpoints) {
2963
- if (point && (await runtime.reader.getBlock(point.number)).hash !== point.hash) {
2964
- await runtime.store.transaction(runtime.scope, (draft) => {
2965
- draft.invalidated = "Canonical operation checkpoint changed; full resync required";
2966
- for (const record of [
2967
- ...Object.values(draft.notes),
2968
- ...Object.values(draft.credits)
2969
- ]) {
2970
- record.spent = "unknown";
2971
- }
2972
- for (const operation of Object.values(draft.operations)) {
2973
- if (["failed", "expired"].includes(operation.status) && !operation.payload?.externalized) {
2974
- continue;
2975
- }
2976
- operation.status = "reorged";
2977
- for (const id of operation.inputs) {
2978
- if (!draft.reservations[id]) {
2979
- draft.reservations[id] = operation.id;
2980
- }
2981
- }
2982
- }
2983
- });
2984
- throw new Error("canonical operation checkpoint changed; full resync required");
2985
- }
2986
- }
2987
- }
2988
- function current(state, id, generation) {
2989
- check4(!state.invalidated, "wallet requires canonical resync");
2990
- const record = state.operations[id];
2991
- const saved = record?.payload;
2992
- check4(record && saved.generation === generation && !saved.claim, "stale operation recovery");
2993
- return { record, saved };
2994
- }
2995
- function observe(state, block) {
2996
- if (state.observedThrough?.number === block.number) {
2997
- check4(state.observedThrough.hash === block.hash, "operation checkpoint conflict");
2998
- }
2999
- if (!state.observedThrough || state.observedThrough.number < block.number) {
3000
- state.observedThrough = { number: block.number, hash: block.hash };
3001
- }
3002
- }
3003
- async function confirmOperation(runtime, id, generation, hash2, signal) {
3004
- const { reader, store, scope, options } = runtime;
3005
- const state = await store.readSnapshot(scope);
3006
- const { saved } = current(state, id, generation);
3007
- const plan = saved.plan;
3008
- await options.protocol.validateDeployment(reader);
3009
- await barrier(runtime, state);
3010
- const receipt = await reader.getReceipt(hash2);
3011
- if (!receipt) {
3012
- return void 0;
3013
- }
3014
- check4(receipt.transactionHash === hash2, "receipt transaction identity mismatch");
3015
- await barrier(runtime, state, receipt.block);
3016
- const transaction = await reader.getTransaction(hash2);
3017
- check4(
3018
- transaction && transaction.transactionHash === hash2,
3019
- "confirmed transaction differs from the frozen operation"
3020
- );
3021
- let matchesRequest;
3022
- if (plan.statement?.target === "noir-recursive") {
3023
- matchesRequest = matchesAggregateTransaction(saved, transaction);
3024
- } else {
3025
- matchesRequest = !!saved.call && transaction.to.toLowerCase() === saved.call.to.toLowerCase() && transaction.value === saved.call.value && transaction.data.toLowerCase() === saved.call.data.toLowerCase();
3026
- }
3027
- check4(matchesRequest, "confirmed transaction differs from the frozen operation");
3028
- check4(
3029
- receipt.status === "success" || receipt.status === "reverted",
3030
- "receipt execution status is missing"
3031
- );
3032
- if (receipt.status === "reverted") {
3033
- return void 0;
3034
- }
3035
- const candidates = extractDeliveryCandidates(
3036
- readProtocolLogs(receipt, plan.context.protocolAddress),
3037
- plan.context,
3038
- receipt.block.number
3039
- );
3040
- const latest = await reader.getLatestBlockNumber();
3041
- const height = [latest, receipt.block.number, state.observedThrough?.number ?? 0n].reduce(
3042
- (a, b) => a > b ? a : b
3043
- );
3044
- const spentBlock = await reader.getBlock(height);
3045
- check4(spentBlock.number === height, "spent-state canonical block mismatch");
3046
- const notes = [];
3047
- for (const output of plan.outputs) {
3048
- const candidate = candidates.find(
3049
- (value) => value.role === "funds" && value.commitment === output.commitment
3050
- );
3051
- check4(candidate, "confirmed output is missing from canonical receipt");
3052
- if (!output.note || output.note.ownerAddress !== scope.ownerAddress) {
3053
- continue;
3054
- }
3055
- const input = {
3056
- treeId: candidate.treeId,
3057
- leafIndex: candidate.leafIndex,
3058
- commitment: candidate.commitment
3059
- };
3060
- check4(options.protocolRpc, "membership provider required for confirmed owned output");
3061
- const proof = await options.protocolRpc.getMembership(plan.context, input, receipt.block);
3062
- await verifyMembership(reader, plan.context, receipt.block, input, proof);
3063
- notes.push({
3064
- ...input,
3065
- note: structuredClone(output.note),
3066
- blockNumber: receipt.block.number,
3067
- blockHash: receipt.block.hash,
3068
- transactionHash: hash2,
3069
- spent: "unknown"
3070
- });
3071
- }
3072
- const credits = [];
3073
- for (const output of plan.credit?.outputs ?? []) {
3074
- const n = output.state.note;
3075
- for (const role of ["owner", "spender"]) {
3076
- if ((role === "owner" ? n.ownerWalletIdentity : n.spenderWalletIdentity) !== scope.ownerAddress) {
3077
- continue;
3078
- }
3079
- const candidate = candidates.find(
3080
- (value) => value.role === role && value.commitment === output.commitment
3081
- );
3082
- check4(candidate, "confirmed Credit output is missing from canonical receipt");
3083
- const input = {
3084
- treeId: candidate.treeId,
3085
- leafIndex: candidate.leafIndex,
3086
- commitment: candidate.commitment
3087
- };
3088
- check4(options.protocolRpc, "membership provider required for confirmed Credit");
3089
- const proof = await options.protocolRpc.getMembership(
3090
- plan.context,
3091
- input,
3092
- receipt.block
3093
- );
3094
- await verifyMembership(reader, plan.context, receipt.block, input, proof);
3095
- const index2 = plan.credit?.draw?.remainderCommitments.indexOf(output.commitment);
3096
- const parent = plan.credit?.topUp ? plan.credit.inputs[0] : index2 !== void 0 && index2 >= 0 ? plan.credit?.inputs[index2] : void 0;
3097
- const receivingKeySource = parent && state.credits[parent.commitment + ":" + role] ? {
3098
- kind: plan.credit?.topUp ? "top-up" : "draw",
3099
- commitment: parent.commitment
3100
- } : void 0;
3101
- credits.push({
3102
- ...input,
3103
- state: structuredClone(output.state),
3104
- role,
3105
- blockNumber: receipt.block.number,
3106
- blockHash: receipt.block.hash,
3107
- transactionHash: hash2,
3108
- spent: "unknown",
3109
- ...receivingKeySource ? { receivingKeySource } : {}
3110
- });
3111
- }
3112
- }
3113
- if (credits.length) {
3114
- const nullifiers = await Promise.all(
3115
- credits.map(
3116
- async (record) => computeCreditNullifier(
3117
- plan.context.chainId,
3118
- record.state.note.creditSharedKey,
3119
- await creditLeaf(record.state)
3120
- )
3121
- )
3122
- );
3123
- const values = await reader.isSpent(plan.context, nullifiers, spentBlock);
3124
- check4(
3125
- values.length === credits.length && values.every((value) => typeof value === "boolean"),
3126
- "invalid Credit spent-state response"
3127
- );
3128
- credits.forEach((record, i) => {
3129
- record.spent = values[i] ? "spent" : "unspent";
3130
- record.checkedThrough = { number: spentBlock.number, hash: spentBlock.hash };
3131
- });
3132
- }
3133
- const facts = inputFacts(plan);
3134
- const nfs = facts.map((input) => input.nullifier);
3135
- const spent = nfs.length ? await reader.isSpent(plan.context, nfs, spentBlock) : [];
3136
- check4(
3137
- spent.length === nfs.length && spent.every((value) => value === true),
3138
- "successful operation inputs are not consumed"
3139
- );
3140
- const nullifier = options.wallet.exportNullifier();
3141
- if (notes.length) {
3142
- const values = await reader.isSpent(
3143
- plan.context,
3144
- await Promise.all(
3145
- notes.map(
3146
- (note) => nullifier.compute(plan.context.chainId, note.commitment, note.note.nonce)
3147
- )
3148
- ),
3149
- spentBlock
3150
- );
3151
- check4(
3152
- values.length === notes.length && values.every((value) => typeof value === "boolean"),
3153
- "invalid output spent-state response"
3154
- );
3155
- notes.forEach((note, i) => {
3156
- note.spent = values[i] ? "spent" : "unspent";
3157
- note.checkedThrough = { number: spentBlock.number, hash: spentBlock.hash };
3158
- });
3159
- }
3160
- signal?.throwIfAborted();
3161
- await barrier(runtime, state, receipt.block);
3162
- await barrier(runtime, state, spentBlock);
3163
- return store.transaction(scope, (draft) => {
3164
- signal?.throwIfAborted();
3165
- const { record, saved: active } = current(draft, id, generation);
3166
- for (const point of [draft.observedThrough]) {
3167
- check4(
3168
- !point || point.number < spentBlock.number || point.number === spentBlock.number && point.hash === spentBlock.hash,
3169
- "wallet advanced or changed during confirmation; retry"
3170
- );
3171
- }
3172
- mergeReceiptRecords(draft, {
3173
- notes,
3174
- credits,
3175
- checkpoint: { number: receipt.block.number, hash: receipt.block.hash }
3176
- });
3177
- for (const fact of facts) {
3178
- const input = draft[fact.kind][fact.key];
3179
- if (input) {
3180
- mergeSpentObservation(input, {
3181
- spent: "spent",
3182
- checkedThrough: { number: spentBlock.number, hash: spentBlock.hash }
3183
- });
3184
- }
3185
- if (fact.kind === "credits" && input) {
3186
- for (const other of Object.values(draft.credits)) {
3187
- if (other.commitment === input.commitment) {
3188
- mergeSpentObservation(other, {
3189
- spent: "spent",
3190
- checkedThrough: { number: spentBlock.number, hash: spentBlock.hash }
3191
- });
3192
- }
3193
- }
3194
- }
3195
- if (draft.reservations[fact.reservationKey] === id) {
3196
- delete draft.reservations[fact.reservationKey];
3197
- }
3198
- }
3199
- observe(draft, spentBlock);
3200
- record.status = "confirmed";
3201
- record.transactionHash = hash2;
3202
- active.generation++;
3203
- return { status: record.status, saved: structuredClone(active) };
3204
- });
2378
+ }
2379
+ const request = await fundingSigningRequest(saved, runtime);
2380
+ return verifyFundingSignature(
2381
+ request,
2382
+ saved.plan.funding.payer,
2383
+ signature,
2384
+ runtime.options.fundingVerifier
2385
+ );
3205
2386
  }
3206
- async function reconcileOperation(runtime, id, generation, signal) {
3207
- const { reader, store, scope, options } = runtime;
3208
- const state = await store.readSnapshot(scope);
3209
- const { record, saved } = current(state, id, generation);
3210
- const plan = saved.plan;
3211
- await options.protocol.validateDeployment(reader);
3212
- await barrier(runtime, state);
3213
- const block = await reader.getBlock(await reader.getLatestBlockNumber());
3214
- const facts = inputFacts(plan);
3215
- const nfs = facts.map((input) => input.nullifier);
3216
- const spent = nfs.length ? await reader.isSpent(plan.context, nfs, block) : [];
3217
- check4(
3218
- spent.length === nfs.length && spent.every((value) => typeof value === "boolean"),
3219
- "invalid reconciliation spent-state response"
2387
+ async function verifyFundingSignature(request, payer, signature, verifier) {
2388
+ if (!/^0x(?:[a-fA-F0-9]{2})+$/.test(signature)) {
2389
+ throw new Error("invalid EVM funding signature");
2390
+ }
2391
+ const types = Object.fromEntries(
2392
+ Object.entries(request.types).map(([name, fields]) => [name, [...fields]])
3220
2393
  );
3221
- const expired = operationExpired(plan, block.timestamp);
3222
- const impossible = spent.some((value) => value);
3223
- signal?.throwIfAborted();
3224
- await barrier(runtime, state, block);
3225
- return store.transaction(scope, (draft) => {
3226
- signal?.throwIfAborted();
3227
- const { record: active, saved: next } = current(draft, id, generation);
3228
- for (const point of [
3229
- draft.observedThrough,
3230
- ...facts.map((fact) => draft[fact.kind][fact.key]?.checkedThrough)
3231
- ]) {
3232
- check4(
3233
- !point || point.number < block.number || point.number === block.number && point.hash === block.hash,
3234
- "wallet advanced or changed during reconciliation; retry"
3235
- );
2394
+ let validEOA = false;
2395
+ try {
2396
+ validEOA = verifyTypedData(request.domain, types, request.message, signature).toLowerCase() === payer.toLowerCase();
2397
+ if (validEOA) {
2398
+ signature = Signature.from(signature).serialized;
3236
2399
  }
3237
- if (expired || impossible) {
3238
- active.status = expired ? "expired" : "failed";
3239
- facts.forEach((fact, i) => {
3240
- const input = draft[fact.kind][fact.key];
3241
- if (input) {
3242
- mergeSpentObservation(input, {
3243
- spent: spent[i] ? "spent" : "unspent",
3244
- checkedThrough: { number: block.number, hash: block.hash }
3245
- });
3246
- if (fact.kind === "credits") {
3247
- for (const other of Object.values(draft.credits)) {
3248
- if (other.commitment === input.commitment) {
3249
- mergeSpentObservation(other, {
3250
- spent: spent[i] ? "spent" : "unspent",
3251
- checkedThrough: { number: block.number, hash: block.hash }
3252
- });
3253
- }
3254
- }
3255
- }
3256
- }
3257
- if (draft.reservations[fact.reservationKey] === id) {
3258
- delete draft.reservations[fact.reservationKey];
3259
- }
3260
- });
3261
- observe(draft, block);
3262
- } else if (next.externalized || active.status === "reorged") {
3263
- active.status = "uncertain";
2400
+ } catch {
2401
+ validEOA = false;
2402
+ }
2403
+ const valid = verifier ? await verifier.verifyTypedData({
2404
+ ...structuredClone(request),
2405
+ address: payer,
2406
+ signature
2407
+ }) : validEOA;
2408
+ if (valid !== true) {
2409
+ throw new Error("invalid EVM funding signature");
2410
+ }
2411
+ return signature;
2412
+ }
2413
+
2414
+ // src/transactions/execution/operation-preview.ts
2415
+ import { normalizeFunding } from "@invisi-labs/protocol";
2416
+ function operationPreview(plan, status) {
2417
+ const p = plan;
2418
+ const fee = p.params ?? p.shieldParams ?? p.credit?.creation ?? p.credit?.cancel;
2419
+ const funding = normalizeFunding(p.funding);
2420
+ return {
2421
+ id: p.id,
2422
+ kind: p.kind,
2423
+ status,
2424
+ context: { ...p.context },
2425
+ inputs: inputFacts(p).length,
2426
+ inputDetails: [
2427
+ ...p.inputs.map((input) => ({
2428
+ kind: "notes",
2429
+ commitment: input.commitment,
2430
+ amount: input.note.amount,
2431
+ owner: input.note.ownerAddress,
2432
+ treeId: input.treeId,
2433
+ leafIndex: input.leafIndex
2434
+ })),
2435
+ ...(p.credit?.inputs ?? []).map((input) => ({
2436
+ kind: "credits",
2437
+ commitment: input.commitment,
2438
+ amount: input.state.note.amount,
2439
+ owner: input.state.note.ownerWalletIdentity,
2440
+ spender: input.state.note.spenderWalletIdentity,
2441
+ expiry: input.state.note.expiry,
2442
+ role: input.role,
2443
+ treeId: input.treeId,
2444
+ leafIndex: input.leafIndex
2445
+ }))
2446
+ ],
2447
+ funding: {
2448
+ payer: funding.payer,
2449
+ amount: funding.amount,
2450
+ authorization: p.fundingMode,
2451
+ sigNonce: funding.sigNonce,
2452
+ deadline: funding.deadline,
2453
+ permit: { deadline: funding.permit.deadline }
2454
+ },
2455
+ creditOutputs: p.credit?.outputs.map((output) => ({
2456
+ commitment: output.commitment,
2457
+ amount: output.state.note.amount,
2458
+ expiry: output.state.note.expiry,
2459
+ owner: output.state.note.ownerWalletIdentity,
2460
+ spender: output.state.note.spenderWalletIdentity,
2461
+ ownerPayout: output.state.note.ownerPayout,
2462
+ spenderPayout: output.state.note.spenderPayout
2463
+ })),
2464
+ outputs: p.outputs.map((output) => ({
2465
+ commitment: output.commitment,
2466
+ to: output.address,
2467
+ amount: output.amount,
2468
+ change: output.change
2469
+ })),
2470
+ capacity: p.capacity,
2471
+ deadline: p.deadline,
2472
+ feeRecipient: fee?.feeRecipient,
2473
+ feeAmount: fee?.feeAmount ?? 0n,
2474
+ fundingAmount: p.funding.amount ?? 0n,
2475
+ fundingPayer: p.funding.payer,
2476
+ withdrawalAmount: p.params?.withdrawalAmount ?? p.credit?.draw?.totalAmount ?? (p.credit?.cancel ? p.credit.cancel.amount - (p.credit.cancel.feeAmount ?? 0n) : void 0) ?? 0n,
2477
+ withdrawalAddress: p.params?.withdrawalAddress ?? p.credit?.draw?.spender ?? p.credit?.cancel?.owner,
2478
+ permissions: {
2479
+ walletAuthorization: requiresAuthorization(p),
2480
+ proof: p.kind !== "shield",
2481
+ fundingAuthorization: p.fundingMode === "signature"
3264
2482
  }
3265
- next.generation++;
3266
- return { status: active.status, saved: structuredClone(next) };
3267
- });
2483
+ };
3268
2484
  }
3269
2485
 
3270
- // src/session/Operation.ts
3271
- function check5(ok, message) {
3272
- if (!ok) {
3273
- throw new Error(message);
3274
- }
2486
+ // src/transactions/execution/PreparedTransaction.ts
2487
+ import { MissingCapabilityError } from "@invisi-labs/core";
2488
+ import { normalizeFunding as normalizeFunding2 } from "@invisi-labs/protocol";
2489
+ function check2(ok, message) {
2490
+ if (!ok) throw new Error(message);
3275
2491
  }
3276
- var Operation = class _Operation {
2492
+ var PreparedTransaction = class _PreparedTransaction {
3277
2493
  #runtime;
3278
2494
  #saved;
3279
2495
  #authorization;
2496
+ #lifecycle;
3280
2497
  status;
3281
- constructor(runtime, saved, status, authorization) {
2498
+ constructor(runtime, saved, status, lifecycle, authorization) {
3282
2499
  this.#runtime = runtime;
3283
2500
  this.#saved = structuredClone(saved);
3284
- this.status = status;
2501
+ this.#lifecycle = lifecycle;
3285
2502
  this.#authorization = authorization;
2503
+ this.status = status;
3286
2504
  Object.freeze(this);
3287
2505
  }
3288
- static async prepare(runtime, plan) {
3289
- const saved = { plan: structuredClone(plan), generation: 0 };
3290
- await runtime.store.transaction(runtime.scope, (state) => {
3291
- check5(!state.invalidated, "wallet state requires canonical resync");
3292
- check5(!state.operations[plan.id], "operation already exists");
3293
- const facts = inputFacts(plan);
3294
- for (const input of facts) {
3295
- const record = state[input.kind][input.key];
3296
- check5(
3297
- record && record.spent === "unspent" && !state.reservations[input.reservationKey],
3298
- "input unavailable or already reserved"
3299
- );
3300
- }
3301
- state.operations[plan.id] = {
3302
- id: plan.id,
3303
- status: "prepared",
3304
- inputs: facts.map((input) => input.reservationKey),
3305
- deadline: plan.deadline,
3306
- payload: saved
3307
- };
3308
- for (const input of facts) {
3309
- state.reservations[input.reservationKey] = plan.id;
3310
- }
3311
- });
3312
- return new _Operation(runtime, saved, "prepared");
3313
- }
3314
- static async resume(runtime, id) {
3315
- const result = await runtime.store.transaction(runtime.scope, (state) => {
3316
- const record = state.operations[id];
3317
- check5(record, "unknown operation");
3318
- const saved = record.payload;
3319
- check5(
3320
- saved?.plan?.version === 1 && saved.plan.id === id && Number.isSafeInteger(saved.generation) && saved.generation >= 0 && saved.generation < Number.MAX_SAFE_INTEGER,
3321
- "invalid operation payload"
3322
- );
3323
- const context = runtime.options.protocol.context;
3324
- const actual = saved.plan.context;
3325
- check5(
3326
- scopeKey(saved.plan.scope) === scopeKey(runtime.scope) && actual.chainId === context.chainId && actual.protocolAddress.toLowerCase() === context.protocolAddress.toLowerCase() && actual.tokenAddress.toLowerCase() === context.tokenAddress.toLowerCase(),
3327
- "recovered operation scope/deployment context mismatch"
3328
- );
3329
- if (saved.plan.kind !== "shield") {
3330
- statement(saved.plan);
3331
- }
3332
- if (saved.proof) {
3333
- sameStatement(saved.proof, saved.plan.statement);
3334
- }
3335
- saved.generation++;
3336
- delete saved.claim;
3337
- return new _Operation(runtime, saved, record.status);
3338
- });
3339
- if (result.#saved.call) {
3340
- const expected = await result.encode(result.#saved);
3341
- check5(
3342
- expected.to === result.#saved.call.to && expected.value === result.#saved.call.value && expected.data === result.#saved.call.data,
3343
- "recovered transaction differs from frozen operation"
3344
- );
3345
- }
3346
- return result;
3347
- }
3348
- /** Public non-sendable intent for a firm quote before wallet authorization. */
3349
- async exportBroadcasterIntent() {
3350
- check5(
3351
- ["prepared", "authorized"].includes(this.status),
3352
- "quote intent requires a prepared operation"
2506
+ static prepare(runtime, plan) {
2507
+ return new _PreparedTransaction(
2508
+ runtime,
2509
+ { plan: structuredClone(plan), generation: 0 },
2510
+ "prepared",
2511
+ { generation: 0, busy: false }
3353
2512
  );
3354
- return exportBroadcasterIntent(this.#saved.plan);
3355
2513
  }
3356
2514
  broadcasterQuote() {
3357
2515
  return this.#saved.broadcasterQuote ? structuredClone(this.#saved.broadcasterQuote) : void 0;
3358
2516
  }
3359
2517
  async withBroadcaster(client, options) {
3360
- check5(this.status === "prepared", "broadcaster must attach before signing");
3361
- const { prepareIntent, prepareBroadcasterPlan } = await import("./broadcaster-preparation-QVL62KM6.js");
3362
- const runtime = this.#runtime;
3363
- const reprepare = (intent) => prepareIntent(
3364
- {
3365
- options: runtime.options,
3366
- scope: runtime.scope,
3367
- reader: runtime.reader,
3368
- readInventory: async () => {
3369
- const state = await runtime.store.readSnapshot(runtime.scope);
3370
- for (const [key, owner] of Object.entries(state.reservations))
3371
- if (owner === this.id) delete state.reservations[key];
3372
- return state;
3373
- },
3374
- complete: async (plan) => plan
3375
- },
3376
- intent
2518
+ this.current();
2519
+ check2(
2520
+ this.status === "prepared" && this.#runtime.reprepare,
2521
+ "broadcaster must attach to an unsigned prepared transaction"
2522
+ );
2523
+ const { prepareBroadcasterPlan } = await import("./broadcaster-preparation-RMWQFYNL.js");
2524
+ const selected = await prepareBroadcasterPlan(
2525
+ this.#saved.plan,
2526
+ this.#runtime.reprepare,
2527
+ client,
2528
+ options
2529
+ );
2530
+ return new _PreparedTransaction(
2531
+ this.#runtime,
2532
+ { plan: selected.plan, broadcasterQuote: selected.quote, generation: 0 },
2533
+ "prepared",
2534
+ { generation: 0, busy: false }
3377
2535
  );
3378
- const selected = await prepareBroadcasterPlan(this.#saved.plan, reprepare, client, options);
3379
- const saved = await runtime.store.transaction(runtime.scope, (state) => {
3380
- const record = state.operations[this.id];
3381
- const old = record?.payload;
3382
- check5(
3383
- record?.status === "prepared" && old?.generation === this.#saved.generation && !old.claim && !old.externalized,
3384
- "stale operation or active worker"
3385
- );
3386
- check5(!state.invalidated, "wallet state requires canonical resync");
3387
- const nextPlan = { ...selected.plan, id: this.id };
3388
- const facts = inputFacts(nextPlan);
3389
- for (const fact of facts)
3390
- check5(
3391
- state[fact.kind][fact.key]?.spent === "unspent" && (!state.reservations[fact.reservationKey] || state.reservations[fact.reservationKey] === this.id),
3392
- "input unavailable or already reserved"
3393
- );
3394
- for (const [key, owner] of Object.entries(state.reservations))
3395
- if (owner === this.id) delete state.reservations[key];
3396
- for (const fact of facts) state.reservations[fact.reservationKey] = this.id;
3397
- const next = {
3398
- plan: nextPlan,
3399
- broadcasterQuote: selected.quote,
3400
- generation: old.generation + 1
3401
- };
3402
- record.payload = next;
3403
- record.inputs = facts.map((fact) => fact.reservationKey);
3404
- record.deadline = nextPlan.deadline;
3405
- return structuredClone(next);
3406
- });
3407
- return new _Operation(runtime, saved, "prepared");
3408
2536
  }
3409
2537
  get id() {
3410
2538
  return this.#saved.plan.id;
@@ -3419,77 +2547,61 @@ var Operation = class _Operation {
3419
2547
  this.#runtime.options.gasBenchmarks
3420
2548
  );
3421
2549
  }
2550
+ /** Final Note outputs, including change; Credit outputs are not exported as IPN. */
2551
+ getOutputs() {
2552
+ return Object.freeze(
2553
+ this.#saved.plan.outputs.map((output) => new TransactionOutput(output))
2554
+ );
2555
+ }
3422
2556
  preview() {
3423
2557
  return operationPreview(this.#saved.plan, this.status);
3424
2558
  }
3425
- current(record) {
3426
- check5(record, "unknown operation");
3427
- const saved = record.payload;
3428
- check5(
3429
- saved.generation === this.#saved.generation && !saved.claim,
3430
- "stale operation handle or active worker; recover the operation"
2559
+ /** Obtain a firm quote before signing. This intent contains no private inputs or witness. */
2560
+ async exportBroadcasterIntent() {
2561
+ this.current();
2562
+ check2(
2563
+ ["prepared", "authorized"].includes(this.status),
2564
+ "quote intent requires a prepared transaction"
2565
+ );
2566
+ return exportBroadcasterIntent(this.#saved.plan);
2567
+ }
2568
+ current() {
2569
+ check2(
2570
+ !this.#lifecycle.busy && this.#lifecycle.generation === this.#saved.generation,
2571
+ "stale transaction handle or active worker"
3431
2572
  );
3432
- return saved;
3433
2573
  }
3434
2574
  async stage(stage, allowed, work, signal) {
3435
2575
  signal?.throwIfAborted();
3436
- const { store, scope } = this.#runtime;
3437
- const token = crypto.randomUUID();
3438
- const saved = await store.transaction(scope, (state) => {
3439
- check5(!state.invalidated, "wallet requires canonical resync");
3440
- const record = state.operations[this.id];
3441
- const current2 = this.current(record);
3442
- check5(allowed.includes(record.status), `invalid operation stage for ${stage}`);
3443
- current2.generation++;
3444
- current2.claim = { stage, token };
3445
- return structuredClone(current2);
3446
- });
2576
+ this.current();
2577
+ check2(allowed.includes(this.status), `invalid transaction stage for ${stage}`);
2578
+ this.#lifecycle.busy = true;
2579
+ const saved = structuredClone(this.#saved);
3447
2580
  try {
3448
2581
  const next = await work(saved);
3449
2582
  signal?.throwIfAborted();
3450
- await store.transaction(scope, (state) => {
3451
- check5(!state.invalidated, "wallet invalidated during operation");
3452
- const record = state.operations[this.id];
3453
- const current2 = record?.payload;
3454
- check5(
3455
- record && current2?.generation === saved.generation && current2.claim?.token === token && allowed.includes(record.status),
3456
- "stale operation worker after recovery"
3457
- );
3458
- delete saved.claim;
3459
- record.status = next.status;
3460
- record.payload = structuredClone(saved);
3461
- });
3462
- return new _Operation(
2583
+ saved.generation++;
2584
+ this.#lifecycle.generation = saved.generation;
2585
+ return new _PreparedTransaction(
3463
2586
  this.#runtime,
3464
2587
  saved,
3465
2588
  next.status,
2589
+ this.#lifecycle,
3466
2590
  next.authorization ?? this.#authorization
3467
2591
  );
3468
- } catch (error) {
3469
- await store.transaction(scope, (state) => {
3470
- const current2 = state.operations[this.id]?.payload;
3471
- if (current2?.claim?.token === token) {
3472
- delete current2.claim;
3473
- }
3474
- });
3475
- throw error;
2592
+ } finally {
2593
+ this.#lifecycle.busy = false;
3476
2594
  }
3477
2595
  }
3478
- async signingCurrent() {
3479
- await this.#runtime.store.transaction(this.#runtime.scope, (state) => {
3480
- check5(!state.invalidated, "wallet state requires canonical resync");
3481
- this.current(state.operations[this.id]);
3482
- });
3483
- }
3484
2596
  /** Public EIP-712 request; signing transport and key custody belong to the caller. */
3485
2597
  async exportWalletAuthorization(options = {}) {
3486
2598
  options.signal?.throwIfAborted();
3487
- await this.signingCurrent();
3488
- check5(
2599
+ this.current();
2600
+ check2(
3489
2601
  requiresAuthorization(this.#saved.plan),
3490
2602
  "operation does not require wallet authorization"
3491
2603
  );
3492
- check5(
2604
+ check2(
3493
2605
  ["prepared", "authorized"].includes(this.status),
3494
2606
  "operation is not awaiting wallet authorization"
3495
2607
  );
@@ -3498,12 +2610,12 @@ var Operation = class _Operation {
3498
2610
  /** Attach an external EOA signature, retaining its witness only at the private proving boundary. */
3499
2611
  async withWalletSignature(signature, options = {}) {
3500
2612
  options.signal?.throwIfAborted();
3501
- await this.signingCurrent();
3502
- check5(
2613
+ this.current();
2614
+ check2(
3503
2615
  requiresAuthorization(this.#saved.plan),
3504
2616
  "operation does not require wallet authorization"
3505
2617
  );
3506
- check5(
2618
+ check2(
3507
2619
  ["prepared", "authorized"].includes(this.status),
3508
2620
  "operation is not awaiting wallet authorization"
3509
2621
  );
@@ -3518,21 +2630,21 @@ var Operation = class _Operation {
3518
2630
  /** Export only after proving when the funding authorization binds an actual proof hash. */
3519
2631
  async exportFundingAuthorization(options = {}) {
3520
2632
  options.signal?.throwIfAborted();
3521
- await this.signingCurrent();
3522
- check5(
2633
+ this.current();
2634
+ check2(
3523
2635
  this.status === (this.kind === "shield" ? "prepared" : "proved"),
3524
2636
  "operation is not awaiting funding authorization or proof"
3525
2637
  );
3526
2638
  const request = await fundingSigningRequest(this.#saved, this.#runtime);
3527
2639
  options.signal?.throwIfAborted();
3528
- await this.signingCurrent();
2640
+ this.current();
3529
2641
  return request;
3530
2642
  }
3531
2643
  /** Verify an external signature against this handle's own frozen operation. */
3532
2644
  async withFundingSignature(signature, options = {}) {
3533
2645
  options.signal?.throwIfAborted();
3534
- await this.signingCurrent();
3535
- check5(
2646
+ this.current();
2647
+ check2(
3536
2648
  this.status === (this.kind === "shield" ? "prepared" : "proved"),
3537
2649
  "operation is not awaiting funding authorization or proof"
3538
2650
  );
@@ -3550,41 +2662,21 @@ var Operation = class _Operation {
3550
2662
  options.signal
3551
2663
  );
3552
2664
  }
3553
- async authorize(options = {}) {
3554
- check5(
3555
- requiresAuthorization(this.#saved.plan),
3556
- "operation does not require wallet authorization"
3557
- );
3558
- const provider = this.#runtime.authorization();
3559
- check5(
3560
- provider.ownerAddress === this.#runtime.scope.ownerAddress,
3561
- "wallet authorization capability mismatch"
3562
- );
3563
- return this.stage(
3564
- "wallet authorization",
3565
- ["prepared", "authorized"],
3566
- async (saved) => {
3567
- const authorization = await authorizeOperation(saved.plan, provider);
3568
- return { status: "authorized", authorization };
3569
- },
3570
- options.signal
3571
- );
3572
- }
3573
- async prove(suppliedOptions = {}) {
3574
- const options = { ...suppliedOptions };
3575
- check5(
2665
+ async prove(backendOrOptions = {}, suppliedOptions = {}) {
2666
+ const options = { ..."prove" in backendOrOptions ? suppliedOptions : backendOrOptions };
2667
+ check2(
3576
2668
  !this.#saved.broadcasterQuote || options.execution !== "aggregate",
3577
2669
  "a broadcaster quote requires direct execution"
3578
2670
  );
3579
2671
  const plan = this.#saved.plan;
3580
- check5(plan.kind !== "shield", "shield requires no proof");
2672
+ check2(plan.kind !== "shield", "shield requires no proof");
3581
2673
  if (requiresAuthorization(plan)) {
3582
- check5(
2674
+ check2(
3583
2675
  this.#authorization,
3584
2676
  "local wallet authorization required; authorize again after recovery"
3585
2677
  );
3586
2678
  }
3587
- const prover = this.#runtime.options.prover;
2679
+ const prover = "prove" in backendOrOptions ? backendOrOptions : this.#runtime.options.prover;
3588
2680
  if (!prover) {
3589
2681
  throw new MissingCapabilityError("prover");
3590
2682
  }
@@ -3611,1101 +2703,863 @@ var Operation = class _Operation {
3611
2703
  options.signal
3612
2704
  );
3613
2705
  }
3614
- encode(saved) {
3615
- return encodeOperation(saved);
2706
+ encode(saved) {
2707
+ return encodeOperation(saved);
2708
+ }
2709
+ /** Shield caller funding needs no wallet signing or proof; this step only encodes its frozen request. */
2710
+ async ready() {
2711
+ check2(
2712
+ this.kind === "shield" && this.#saved.plan.fundingMode === "caller",
2713
+ "operation needs authorization or proof first"
2714
+ );
2715
+ return this.stage("encoding", ["prepared"], async (saved) => {
2716
+ saved.call = await this.encode(saved);
2717
+ return { status: "ready" };
2718
+ });
2719
+ }
2720
+ async exportTransaction() {
2721
+ this.current();
2722
+ check2(
2723
+ this.#saved.plan.statement?.target !== "noir-recursive",
2724
+ "recursive operations must be exported as aggregate members"
2725
+ );
2726
+ check2(this.status === "ready" && this.#saved.call, "transaction is not ready");
2727
+ return structuredClone(this.#saved.call);
2728
+ }
2729
+ exportProof() {
2730
+ this.current();
2731
+ check2(this.#saved.proof, "transaction has no proof");
2732
+ return publicProof(this.#saved.proof);
2733
+ }
2734
+ exportAggregateMember() {
2735
+ this.current();
2736
+ const saved = this.#saved, plan = saved.plan;
2737
+ check2(
2738
+ this.status === "ready" && plan.params && saved.proof && plan.statement?.target === "noir-recursive" && plan.statement.circuitName === "transact_user_slots14",
2739
+ "ready recursive slots14 transaction required"
2740
+ );
2741
+ sameStatement(saved.proof, statement(plan));
2742
+ check2(
2743
+ saved.proof.proofHex.length === 2 + 458 * 64,
2744
+ "recursive member proof must have 458 fields"
2745
+ );
2746
+ return {
2747
+ bundle: publicProof(saved.proof),
2748
+ statement: structuredClone(plan.statement),
2749
+ context: { ...plan.context },
2750
+ request: {
2751
+ params: structuredClone(plan.params),
2752
+ deliveries: plan.outputs.map((output) => structuredClone(output.delivery)),
2753
+ funding: normalizeFunding2(plan.funding)
2754
+ }
2755
+ };
2756
+ }
2757
+ toJSON() {
2758
+ return { transaction: true, id: this.id, kind: this.kind, status: this.status };
2759
+ }
2760
+ [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
2761
+ return this.toJSON();
2762
+ }
2763
+ };
2764
+
2765
+ // src/client/infrastructure.ts
2766
+ import { MissingCapabilityError as MissingCapabilityError2 } from "@invisi-labs/core";
2767
+ import { ProtocolRpcClient } from "@invisi-labs/rpc";
2768
+ function configureTransactionInfrastructure(options) {
2769
+ const { provider } = options;
2770
+ const reader = options.chainReader ?? (provider ? new EthersChainReader(provider) : void 0);
2771
+ const protocolRpc = options.protocolRpc instanceof ProtocolRpcClient && reader ? options.protocolRpc.withReader(reader) : options.protocolRpc;
2772
+ const fundingVerifier = options.fundingVerifier ?? (provider ? new EthersFundingVerifier(provider, options.protocol.context.chainId) : void 0);
2773
+ return configureRecipientInfrastructure({
2774
+ ...options,
2775
+ ...reader ? { chainReader: reader } : {},
2776
+ ...protocolRpc ? { protocolRpc } : {},
2777
+ ...fundingVerifier ? { fundingVerifier } : {}
2778
+ });
2779
+ }
2780
+ async function validateTransactionInfrastructure(options) {
2781
+ if (!options.chainReader) throw new MissingCapabilityError2("chain reader");
2782
+ if (options.provider && (await options.provider.getNetwork()).chainId !== options.protocol.context.chainId)
2783
+ throw new Error("provider deployment chain mismatch");
2784
+ await options.protocol.validateDeployment(options.chainReader);
2785
+ }
2786
+
2787
+ // src/client/Invisi.ts
2788
+ import { MissingCapabilityError as MissingCapabilityError3, PublicWallet } from "@invisi-labs/core";
2789
+ import { ProtocolRpcClient as ProtocolRpcClient2 } from "@invisi-labs/rpc";
2790
+
2791
+ // src/transactions/preparation/explicit-preparation.ts
2792
+ import { getAddress } from "ethers";
2793
+ function explicitPreparation(options, data, tokenAddress) {
2794
+ if (!options.chainReader) throw new Error("chain reader required");
2795
+ const inputs = structuredClone(data.inputs ?? []);
2796
+ const credits = structuredClone(data.creditInputs ?? []);
2797
+ const inventory = { notes: {}, credits: {}, reservations: {} };
2798
+ const context = options.protocol.context;
2799
+ function position2(input) {
2800
+ if (typeof input.treeId !== "bigint" || input.treeId < 0n || input.treeId >= 1n << 256n || !Number.isInteger(input.leafIndex) || input.leafIndex < 0 || input.leafIndex >= 65536)
2801
+ throw new Error("invalid explicit input position");
2802
+ }
2803
+ for (const input of inputs) {
2804
+ position2(input);
2805
+ positiveAmount(input.note.amount);
2806
+ if (inventory.notes[input.commitment]) throw new Error("duplicate explicit note input");
2807
+ if (input.note.chainId !== context.chainId || input.note.protocolAddress !== BigInt(context.protocolAddress) || input.note.ownerAddress !== options.wallet.ownerAddress)
2808
+ throw new Error("explicit note identity or deployment mismatch");
2809
+ inventory.notes[input.commitment] = { ...input, spent: "unspent" };
2810
+ }
2811
+ for (const input of credits) {
2812
+ position2(input);
2813
+ if (input.role !== "owner" && input.role !== "spender")
2814
+ throw new Error("invalid explicit Credit role");
2815
+ const key = input.commitment + ":" + input.role;
2816
+ if (inventory.credits[key]) throw new Error("duplicate explicit Credit input");
2817
+ inventory.credits[key] = { ...input, spent: "unspent" };
2818
+ }
2819
+ const scope = {
2820
+ ...context,
2821
+ namespace: "explicit",
2822
+ ownerAddress: options.wallet.ownerAddress
2823
+ };
2824
+ const capabilities = options;
2825
+ if (inputs.some((input) => input.note.tokenAddress !== BigInt(getAddress(tokenAddress))))
2826
+ throw new Error("explicit note token mismatch");
2827
+ const runtime = {
2828
+ options: { ...capabilities, protocol: options.protocol.forToken(tokenAddress) },
2829
+ reader: options.chainReader,
2830
+ scope
2831
+ };
2832
+ const reprepare = (intent) => import("./broadcaster-preparation-RMWQFYNL.js").then(
2833
+ ({ prepareIntent }) => prepareIntent(
2834
+ {
2835
+ ...runtime,
2836
+ explicitInputs: true,
2837
+ ...data.changeAddress ? { changeAddress: data.changeAddress } : {},
2838
+ readInventory: async () => structuredClone(inventory),
2839
+ complete: async (plan) => plan
2840
+ },
2841
+ intent
2842
+ )
2843
+ );
2844
+ return new TransactionPreparation({
2845
+ ...runtime,
2846
+ explicitInputs: true,
2847
+ ...data.changeAddress ? { changeAddress: data.changeAddress } : {},
2848
+ readInventory: async () => structuredClone(inventory),
2849
+ complete: async (plan) => PreparedTransaction.prepare({ ...runtime, reprepare }, plan)
2850
+ });
2851
+ }
2852
+
2853
+ // src/client/Invisi.ts
2854
+ var Invisi = class {
2855
+ #options;
2856
+ constructor(options = {}) {
2857
+ if ("addressLookup" in options || "addressResolver" in options)
2858
+ throw new TypeError(
2859
+ "Recipient lookup belongs to each EVM output; caller resolver injection is not supported"
2860
+ );
2861
+ if ("ciphertextCodecs" in options || "envelopeVersion" in options) {
2862
+ throw new Error(
2863
+ "SDK envelope versions are fixed; encode application payload bytes externally"
2864
+ );
2865
+ }
2866
+ if ("signer" in options || "fundingSigner" in options) {
2867
+ throw new Error(
2868
+ "Invisi does not accept signer defaults; export a signing request and attach the external signature"
2869
+ );
2870
+ }
2871
+ for (const key of [
2872
+ "store",
2873
+ "viewing",
2874
+ "authorization",
2875
+ "namespace",
2876
+ "coinSelector",
2877
+ "wallet",
2878
+ "protocol"
2879
+ ]) {
2880
+ if (key in options)
2881
+ throw new TypeError(`Wallet state and permissions belong to WalletSession: ${key}`);
2882
+ }
2883
+ this.#options = {
2884
+ ...options,
2885
+ ...options.gasBenchmarks ? { gasBenchmarks: copyGasBenchmarks(options.gasBenchmarks) } : {}
2886
+ };
2887
+ Object.freeze(this);
2888
+ }
2889
+ /** A detached snapshot of shared infrastructure; wallet permissions and persistence stay external. */
2890
+ getInfrastructure() {
2891
+ return Object.freeze({
2892
+ ...this.#options,
2893
+ ...this.#options.gasBenchmarks ? { gasBenchmarks: copyGasBenchmarks(this.#options.gasBenchmarks) } : {}
2894
+ });
3616
2895
  }
3617
- async authorizeFunding(options = {}) {
3618
- const p = this.#saved.plan;
3619
- check5(p.fundingMode === "signature", "operation does not require a funding signature");
3620
- check5(p.kind === "shield" || this.#saved.proof, "proof must precede funding authorization");
3621
- const signer = this.#runtime.options.fundingSigner;
3622
- if (!signer) {
3623
- throw new MissingCapabilityError("EVM funding signer");
3624
- }
3625
- check5(
3626
- signer.address.toLowerCase() === p.funding.payer.toLowerCase(),
3627
- "funding signer is not the payer"
3628
- );
3629
- return this.stage(
3630
- "funding authorization",
3631
- p.kind === "shield" ? ["prepared"] : ["proved"],
3632
- async (saved) => {
3633
- saved.plan.funding.signature = await signOperationFunding(
3634
- saved,
3635
- signer,
3636
- this.#runtime.options.fundingVerifier
3637
- );
3638
- if (saved.plan.statement?.target !== "noir-recursive") {
3639
- saved.call = await this.encode(saved);
2896
+ async resolve(options) {
2897
+ const runtime = configureTransactionInfrastructure({
2898
+ ...this.getInfrastructure(),
2899
+ ...options
2900
+ });
2901
+ await validateTransactionInfrastructure(runtime);
2902
+ return runtime;
2903
+ }
2904
+ /** Discover the selected chain's registry and prepare an independent signing request. */
2905
+ async prepareRegistration(options) {
2906
+ return prepareWithErrors("prepareRegistration", async () => {
2907
+ for (const key of Object.keys(options)) {
2908
+ if (!["wallet", "update", "deadline"].includes(key)) {
2909
+ throw new Error(`Unknown registration option: ${key}`);
3640
2910
  }
3641
- return { status: "ready" };
3642
- },
3643
- options.signal
3644
- );
3645
- }
3646
- /** Shield caller funding needs no wallet signing or proof; this step only encodes its frozen request. */
3647
- async ready() {
3648
- check5(
3649
- this.kind === "shield" && this.#saved.plan.fundingMode === "caller",
3650
- "operation needs authorization or proof first"
3651
- );
3652
- return this.stage("encoding", ["prepared"], async (saved) => {
3653
- saved.call = await this.encode(saved);
3654
- return { status: "ready" };
2911
+ }
2912
+ this.assertPublicIdentity(options);
2913
+ const selected = { ...options };
2914
+ const { provider, protocolRpc } = this.#options;
2915
+ if (!provider) throw new MissingCapabilityError3("ethers provider");
2916
+ if (!(protocolRpc instanceof ProtocolRpcClient2)) {
2917
+ throw new MissingCapabilityError3("Protocol RPC deployment discovery");
2918
+ }
2919
+ const chainId = (await provider.getNetwork()).chainId;
2920
+ const deployment = await protocolRpc.getDeployment(chainId);
2921
+ if (!deployment.registry) {
2922
+ throw new MissingCapabilityError3("receiving-key registry deployment");
2923
+ }
2924
+ const registry = new AddressRegistry(
2925
+ { chainId, registryAddress: deployment.registry.address },
2926
+ { expectedRuntimeCodeHash: deployment.registry.runtimeCodeHash }
2927
+ );
2928
+ return registry.prepareRegistration({ ...selected, provider });
3655
2929
  });
3656
2930
  }
3657
- async validateExport(signal) {
3658
- signal?.throwIfAborted();
3659
- const { reader, options: runtimeOptions } = this.#runtime;
3660
- await runtimeOptions.protocol.validateDeployment(reader);
3661
- const head = await reader.getBlock(await reader.getLatestBlockNumber());
3662
- const plan = this.#saved.plan;
3663
- check5(!operationExpired(plan, head.timestamp), "operation has expired");
3664
- if (plan.fundingMode === "signature") {
3665
- check5(head.timestamp <= (plan.funding.deadline ?? 0n), "funding authorization expired");
3666
- check5(
3667
- await reader.getFundingNonce(plan.context, plan.funding.payer, head) === plan.funding.sigNonce,
3668
- "funding nonce changed; prepare again"
3669
- );
3670
- }
3671
- for (const source of operationSources(plan)) {
3672
- check5(
3673
- await reader.isKnownRoot(plan.context, source.treeId, source.merkleRoot, head),
3674
- "source root is no longer accepted; prepare again"
2931
+ assertPublicIdentity(options) {
2932
+ const { wallet } = options;
2933
+ if (!(wallet instanceof PublicWallet) || ["getViewer", "getDetector", "exportAuthorization"].some((key) => key in wallet)) {
2934
+ throw new Error(
2935
+ "Independent preparation requires a PublicWallet without private capabilities"
3675
2936
  );
3676
2937
  }
3677
- const nullifiers = inputFacts(plan).map((input) => input.nullifier);
3678
- if (nullifiers.length) {
3679
- const spent = await reader.isSpent(plan.context, nullifiers, head);
3680
- check5(
3681
- spent.length === nullifiers.length && spent.every((value) => value === false),
3682
- "input spent or invalid spent response"
3683
- );
2938
+ for (const key of ["authorization", "signer", "fundingSigner", "viewing"]) {
2939
+ if (key in options)
2940
+ throw new Error(`Independent preparation does not accept capability: ${key}`);
3684
2941
  }
3685
- signal?.throwIfAborted();
3686
2942
  }
3687
- async exportTransaction(options = {}) {
3688
- check5(
3689
- this.#saved.plan.statement?.target !== "noir-recursive",
3690
- "recursive operations must be exported as aggregate members"
3691
- );
3692
- await this.validateExport(options.signal);
3693
- const result = await this.#runtime.store.transaction(this.#runtime.scope, (state) => {
3694
- options.signal?.throwIfAborted();
3695
- const record = state.operations[this.id];
3696
- const saved = this.current(record);
3697
- check5(record.status === "ready" && saved.call, "operation is not ready");
3698
- saved.externalized = true;
3699
- return structuredClone(saved.call);
2943
+ resolveExplicit(options) {
2944
+ this.assertPublicIdentity(options);
2945
+ return this.resolve({ wallet: options.wallet, protocol: options.protocol });
2946
+ }
2947
+ async prepareTransfer(options) {
2948
+ return prepareWithErrors("prepareTransfer", async () => {
2949
+ const { wallet, protocol, ...input } = options;
2950
+ const request = structuredClone(input);
2951
+ const runtime = await this.resolveExplicit(options);
2952
+ const preparation = explicitPreparation(runtime, request, request.tokenAddress);
2953
+ return preparation.prepareTransfer(request);
3700
2954
  });
3701
- return result;
3702
2955
  }
3703
- async exportAggregateMember() {
3704
- return this.#runtime.store.transaction(this.#runtime.scope, (state) => {
3705
- const record = state.operations[this.id];
3706
- const saved = this.current(record);
3707
- const plan = saved.plan;
3708
- check5(
3709
- ["ready", "uncertain"].includes(record.status) && plan.params && saved.proof && plan.statement?.target === "noir-recursive" && plan.statement.circuitName === "transact_user_slots14",
3710
- "operation is not a ready recursive slots14 aggregate member"
3711
- );
3712
- sameStatement(saved.proof, statement(plan));
3713
- check5(
3714
- saved.proof.proofHex.length === 2 + 458 * 64,
3715
- "recursive member proof must have 458 fields"
3716
- );
3717
- saved.externalized = true;
3718
- return {
3719
- bundle: publicProof(saved.proof),
3720
- statement: structuredClone(plan.statement),
3721
- context: { ...plan.context },
3722
- request: {
3723
- params: structuredClone(plan.params),
3724
- deliveries: plan.outputs.map((output) => structuredClone(output.delivery)),
3725
- funding: normalizeFunding2(plan.funding)
3726
- }
3727
- };
2956
+ async prepareShield(options) {
2957
+ return prepareWithErrors("prepareShield", async () => {
2958
+ const { wallet, protocol, ...input } = options;
2959
+ const request = structuredClone(input);
2960
+ const runtime = await this.resolveExplicit(options);
2961
+ const preparation = explicitPreparation(runtime, {}, request.tokenAddress);
2962
+ return preparation.prepareShield(request);
3728
2963
  });
3729
2964
  }
3730
- async abandon() {
3731
- const saved = await this.#runtime.store.transaction(this.#runtime.scope, (state) => {
3732
- const record = state.operations[this.id];
3733
- const current2 = this.current(record);
3734
- check5(
3735
- !current2.externalized,
3736
- "externalized or broadcast operation requires chain reconciliation"
3737
- );
3738
- check5(
3739
- !["confirmed", "reorged"].includes(record.status),
3740
- "operation requires chain reconciliation"
3741
- );
3742
- for (const id of record.inputs) {
3743
- if (state.reservations[id] === this.id) {
3744
- delete state.reservations[id];
3745
- }
3746
- }
3747
- record.status = "failed";
3748
- current2.generation++;
3749
- return structuredClone(current2);
2965
+ async prepareCreateCredit(options) {
2966
+ return prepareWithErrors("prepareCreateCredit", async () => {
2967
+ const { wallet, protocol, ...input } = options;
2968
+ const request = structuredClone(input);
2969
+ const runtime = await this.resolveExplicit(options);
2970
+ const preparation = explicitPreparation(runtime, request, request.tokenAddress);
2971
+ return preparation.prepareCreateCredit(request);
3750
2972
  });
3751
- return new _Operation(this.#runtime, saved, "failed");
3752
2973
  }
3753
- /** Record an application-owned signed/broadcast transaction. This never sends or waits. */
3754
- async trackTransaction(transactionHash) {
3755
- check5(/^0x[a-fA-F0-9]{64}$/.test(transactionHash), "invalid transaction hash");
3756
- const saved = await this.#runtime.store.transaction(this.#runtime.scope, (state) => {
3757
- check5(!state.invalidated, "wallet requires canonical resync");
3758
- const record = state.operations[this.id];
3759
- const current2 = this.current(record);
3760
- check5(
3761
- ["ready", "uncertain", "submitted"].includes(record.status),
3762
- "operation is not ready"
3763
- );
3764
- check5(
3765
- current2.externalized,
3766
- "export the transaction or aggregate member before tracking"
3767
- );
3768
- for (const input of record.inputs) {
3769
- check5(state.reservations[input] === this.id, "operation input reservation lost");
3770
- }
3771
- current2.generation++;
3772
- current2.externalized = true;
3773
- record.transactionHash = transactionHash;
3774
- record.status = "uncertain";
3775
- return structuredClone(current2);
2974
+ async prepareDrawCredit(options) {
2975
+ return prepareWithErrors("prepareDrawCredit", async () => {
2976
+ const { wallet, protocol, ...input } = options;
2977
+ const request = structuredClone(input);
2978
+ const runtime = await this.resolveExplicit(options);
2979
+ const preparation = explicitPreparation(runtime, request, request.tokenAddress);
2980
+ return preparation.prepareDrawCredit(request);
3776
2981
  });
3777
- return new _Operation(this.#runtime, saved, "uncertain");
3778
2982
  }
3779
- async reconcile(options = {}) {
3780
- options.signal?.throwIfAborted();
3781
- const state = await this.#runtime.store.readSnapshot(this.#runtime.scope);
3782
- const record = state.operations[this.id];
3783
- this.current(record);
3784
- check5(!state.invalidated, "wallet requires canonical resync");
3785
- const hash2 = options.transactionHash ?? record.transactionHash;
3786
- let outcome;
3787
- if (hash2) {
3788
- outcome = await confirmOperation(
3789
- this.#runtime,
3790
- this.id,
3791
- this.#saved.generation,
3792
- hash2,
3793
- options.signal
3794
- );
3795
- }
3796
- outcome ??= await reconcileOperation(
3797
- this.#runtime,
3798
- this.id,
3799
- this.#saved.generation,
3800
- options.signal
3801
- );
3802
- return new _Operation(this.#runtime, outcome.saved, outcome.status);
2983
+ async prepareTopUpCredit(options) {
2984
+ return prepareWithErrors("prepareTopUpCredit", async () => {
2985
+ const { wallet, protocol, ...input } = options;
2986
+ const request = structuredClone(input);
2987
+ const runtime = await this.resolveExplicit(options);
2988
+ const preparation = explicitPreparation(runtime, request, request.tokenAddress);
2989
+ return preparation.prepareTopUpCredit(request);
2990
+ });
2991
+ }
2992
+ async prepareCancelCredit(options) {
2993
+ return prepareWithErrors("prepareCancelCredit", async () => {
2994
+ const { wallet, protocol, ...input } = options;
2995
+ const request = structuredClone(input);
2996
+ const runtime = await this.resolveExplicit(options);
2997
+ const preparation = explicitPreparation(runtime, request, request.tokenAddress);
2998
+ return preparation.prepareCancelCredit(request);
2999
+ });
3803
3000
  }
3804
3001
  toJSON() {
3805
- return { id: this.id, kind: this.kind, status: this.status };
3002
+ return { invisi: true };
3806
3003
  }
3807
3004
  [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
3808
3005
  return this.toJSON();
3809
3006
  }
3810
3007
  };
3811
3008
 
3812
- // src/session/WalletSession.ts
3813
- import { Wallet } from "@invisi-labs/core";
3814
-
3815
- // src/session/history.ts
3816
- function walletHistory(state, options = {}) {
3817
- const token = options.tokenAddress?.toLowerCase();
3818
- const outgoing = [];
3819
- for (const record of Object.values(state.operations)) {
3820
- const saved = record.payload;
3821
- if (!saved?.plan || token && saved.plan.context.tokenAddress.toLowerCase() !== token) {
3822
- continue;
3823
- }
3824
- const plan = saved.plan;
3825
- outgoing.push({
3826
- ...operationPreview(plan, record.status),
3827
- preparedAt: { ...plan.preparedAt },
3828
- ...record.transactionHash ? { transactionHash: record.transactionHash } : {},
3829
- outputs: plan.outputs.map((output) => ({
3830
- to: output.address,
3831
- amount: output.amount,
3832
- change: output.change,
3833
- commitment: output.commitment,
3834
- payload: Uint8Array.from(output.note?.payload ?? [])
3835
- }))
3836
- });
3009
+ // src/discovery/protocol-events.ts
3010
+ import { invisiProtocolAbi } from "@invisi-labs/protocol/contracts";
3011
+ import { Interface } from "ethers";
3012
+ var protocolInterface = new Interface(invisiProtocolAbi);
3013
+ function decodeProtocolEvent(log) {
3014
+ const decoded = protocolInterface.parseLog(log);
3015
+ if (!decoded) throw new Error("Unknown protocol receipt event");
3016
+ const encoded = protocolInterface.encodeEventLog(decoded.fragment, decoded.args);
3017
+ if (encoded.data.toLowerCase() !== log.data.toLowerCase() || encoded.topics.length !== log.topics.length || encoded.topics.some(
3018
+ (topic, index2) => topic.toLowerCase() !== log.topics[index2].toLowerCase()
3019
+ ))
3020
+ throw new Error("Noncanonical protocol receipt event");
3021
+ const args = decoded.args.toObject(true);
3022
+ for (const key of [
3023
+ "tokens",
3024
+ "commitments",
3025
+ "nullifiers",
3026
+ "outputCommitments",
3027
+ "leafIndices",
3028
+ "remainderLeafIndices"
3029
+ ]) {
3030
+ const values = decoded.args.getValue(key);
3031
+ if (values) args[key] = values.toArray(true);
3837
3032
  }
3838
- outgoing.sort(
3839
- (a, b) => a.preparedAt.number === b.preparedAt.number ? a.id.localeCompare(b.id) : a.preparedAt.number > b.preparedAt.number ? -1 : 1
3840
- );
3841
- const incoming = [];
3842
- for (const record of Object.values(state.notes)) {
3843
- const tokenAddress = `0x${record.note.tokenAddress.toString(16).padStart(40, "0")}`;
3844
- if (token && tokenAddress !== token) continue;
3845
- incoming.push({
3846
- commitment: record.commitment,
3847
- treeId: record.treeId,
3848
- leafIndex: record.leafIndex,
3849
- blockNumber: record.blockNumber,
3850
- blockHash: record.blockHash,
3851
- ...record.transactionHash ? { transactionHash: record.transactionHash } : {},
3852
- tokenAddress,
3853
- amount: record.note.amount,
3854
- payload: Uint8Array.from(record.note.payload ?? []),
3855
- spent: record.spent
3856
- });
3033
+ for (const key of ["sources", "deliveries"]) {
3034
+ const values = decoded.args.getValue(key);
3035
+ if (values) args[key] = values.map((value) => value.toObject(true));
3857
3036
  }
3858
- incoming.sort(
3859
- (a, b) => a.blockNumber === b.blockNumber ? a.commitment.localeCompare(b.commitment) : a.blockNumber > b.blockNumber ? -1 : 1
3860
- );
3861
- return structuredClone({
3862
- revision: state.revision,
3863
- checkedThrough: state.discoveryThrough ?? null,
3864
- invalidated: !!state.invalidated,
3865
- outgoing,
3866
- incoming
3867
- });
3868
- }
3869
-
3870
- // src/session/WalletSession.ts
3871
- import { getAddress } from "ethers";
3872
- import { MissingCapabilityError as MissingCapabilityError2 } from "@invisi-labs/core";
3873
-
3874
- // src/session/refresh.ts
3875
- import { MAX_UPDATE_BLOCKS } from "@invisi-labs/rpc";
3876
- async function refreshWallet(runtime, options) {
3877
- const { signal } = options;
3878
- try {
3879
- signal?.throwIfAborted();
3880
- if (typeof options.fromBlock !== "bigint" || options.fromBlock < runtime.fromBlock)
3881
- throw new Error("refresh range precedes deployment");
3882
- const pageSize = options.pageSize ?? MAX_UPDATE_BLOCKS;
3883
- if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > MAX_UPDATE_BLOCKS)
3884
- throw new Error(`refresh page size must be 1..${MAX_UPDATE_BLOCKS}`);
3885
- const status = await runtime.provider.getStatus(runtime.context, {
3886
- ...signal ? { signal } : {}
3887
- });
3888
- if (!status.indexedHead) throw new Error("Protocol RPC has not indexed any blocks");
3889
- const toBlock = options.toBlock ?? status.indexedHead.number;
3890
- if (typeof toBlock !== "bigint" || toBlock < options.fromBlock || toBlock > status.indexedHead.number)
3891
- throw new Error("refresh range is outside indexed history");
3892
- const target = await runtime.reader.getBlock(toBlock);
3893
- if (target.number !== toBlock)
3894
- throw new Error("Chain RPC returned the wrong refresh anchor");
3895
- let result;
3896
- let notes = 0;
3897
- let credits = 0;
3898
- for (let from = options.fromBlock; from <= toBlock; from += BigInt(pageSize)) {
3899
- signal?.throwIfAborted();
3900
- const end = from + BigInt(pageSize) - 1n;
3901
- const page = await runtime.provider.getUpdates(runtime.context, {
3902
- fromBlock: from,
3903
- toBlock: end < toBlock ? end : toBlock,
3904
- ...options.detectionKey ? { detectionKey: options.detectionKey } : {},
3905
- ...signal ? { signal } : {}
3906
- });
3907
- if (page.block.number !== (end < toBlock ? end : toBlock))
3908
- throw new Error("Protocol RPC returned the wrong page anchor");
3909
- signal?.throwIfAborted();
3910
- if (page.transactionHashes.length === 0 && (result || from + BigInt(pageSize) <= toBlock))
3911
- continue;
3912
- const firstApplication = result === void 0;
3913
- result = await runtime.apply(
3914
- {
3915
- context: runtime.context,
3916
- transactionHashes: page.transactionHashes,
3917
- block: target
3918
- },
3919
- {
3920
- ...signal ? { signal } : {},
3921
- ...firstApplication && options.reset ? { reset: true } : {}
3922
- }
3923
- );
3924
- notes += result.notes;
3925
- credits += result.credits;
3037
+ const params = decoded.args.getValue("params");
3038
+ if (params) {
3039
+ const source = params.getValue("creation") ?? params;
3040
+ const target = params.getValue("creation") ? args.params.creation : args.params;
3041
+ for (const key of ["nullifiers", "remainderCommitments", "expiries", "recoveryAmounts"]) {
3042
+ const values = source.getValue(key);
3043
+ if (values) target[key] = values.toArray(true);
3926
3044
  }
3927
- return { ...result, notes, credits };
3928
- } finally {
3929
- options.detectionKey?.fill(0);
3045
+ const sources = source.getValue("sources");
3046
+ if (sources) target.sources = sources.map((value) => value.toObject(true));
3930
3047
  }
3048
+ return { eventName: decoded.name, args };
3931
3049
  }
3932
3050
 
3933
- // src/session/WalletUpdater.ts
3934
- import { NullifierCapability } from "@invisi-labs/core";
3051
+ // src/discovery/receipt-logs.ts
3052
+ function readProtocolLogs(receipt, protocolAddress) {
3053
+ if (receipt.status !== "success" || !/^0x[0-9a-fA-F]{64}$/.test(receipt.transactionHash) || typeof receipt.block.number !== "bigint" || receipt.block.number < 0n || !/^0x[0-9a-fA-F]{64}$/.test(receipt.block.hash))
3054
+ throw new Error("Invalid canonical protocol receipt");
3055
+ const seen = /* @__PURE__ */ new Set();
3056
+ return receipt.logs.filter((log) => log.address.toLowerCase() === protocolAddress.toLowerCase()).map((log) => {
3057
+ if (log.transactionHash.toLowerCase() !== receipt.transactionHash.toLowerCase() || !Number.isSafeInteger(log.logIndex) || log.logIndex < 0 || seen.has(log.logIndex))
3058
+ throw new Error("Invalid canonical receipt log identity");
3059
+ seen.add(log.logIndex);
3060
+ const decoded = decodeProtocolEvent(log);
3061
+ return {
3062
+ address: log.address,
3063
+ eventName: decoded.eventName,
3064
+ args: decoded.args,
3065
+ blockNumber: receipt.block.number,
3066
+ blockHash: receipt.block.hash,
3067
+ transactionHash: receipt.transactionHash.toLowerCase(),
3068
+ logIndex: log.logIndex,
3069
+ removed: false
3070
+ };
3071
+ }).sort((a, b) => a.logIndex - b.logIndex);
3072
+ }
3935
3073
 
3936
- // src/session/inventory.ts
3074
+ // src/discovery/discovery.ts
3075
+ import { MAX_CIPHERTEXT_BYTES } from "@invisi-labs/core";
3076
+ import { isAddress } from "ethers";
3937
3077
  import {
3938
- computeCreditCommitment,
3939
- computeCreditNullifier as computeCreditNullifier2,
3940
- computeCreditSharedKeyHash,
3941
- recoverCreditRemainder
3078
+ decodeAddress,
3079
+ ReceivingAddress
3942
3080
  } from "@invisi-labs/core";
3943
- var CanonicalConflict = class extends Error {
3944
- };
3945
- var CreditMetadataMismatch = class extends Error {
3946
- };
3947
- function check6(value, message) {
3948
- if (!value) {
3949
- throw new Error(message);
3950
- }
3081
+ import { normalizeOpaqueDelivery } from "@invisi-labs/protocol";
3082
+ var EVENTS = /* @__PURE__ */ new Set([
3083
+ "CommitmentsInserted",
3084
+ "BatchOutputs",
3085
+ "Shield",
3086
+ "Transact",
3087
+ "CrossTreeTransact",
3088
+ "CreditCreated",
3089
+ "CreditDraw",
3090
+ "CreditToppedUp",
3091
+ "CreditCancelled"
3092
+ ]);
3093
+ function check3(ok, message) {
3094
+ if (!ok) throw new Error(message);
3951
3095
  }
3952
- var clone = (value) => structuredClone(value);
3953
- var InventoryProcessor = class {
3954
- #options;
3955
- constructor(options) {
3956
- this.#options = { ...options, scope: Object.freeze({ ...options.scope }) };
3957
- }
3958
- reorgOperations(state) {
3959
- for (const operation of Object.values(state.operations)) {
3960
- if (["failed", "expired"].includes(operation.status) && !operation.payload?.externalized) {
3961
- continue;
3962
- }
3963
- operation.status = "reorged";
3964
- for (const id of operation.inputs) {
3965
- if (!state.reservations[id]) {
3966
- state.reservations[id] = operation.id;
3967
- }
3968
- }
3969
- }
3970
- }
3971
- async invalidate() {
3972
- const { store, scope } = this.#options;
3973
- await store.transaction(scope, (state) => {
3974
- state.invalidated = "Canonical history changed; rebuild inventory from canonical data";
3975
- for (const record of [...Object.values(state.notes), ...Object.values(state.credits)]) {
3976
- record.spent = "unknown";
3977
- }
3978
- this.reorgOperations(state);
3979
- });
3980
- }
3981
- checkSnapshotKeys(record) {
3982
- const expected = this.#options.receivingKeys;
3983
- const keys = record.role === "owner" ? record.state.ownerReceivingKeys : record.state.spenderReceivingKeys;
3984
- if (expected && !keys.viewingPublicKey.every((byte, i) => byte === expected.viewingPublicKey[i])) {
3985
- throw new CreditMetadataMismatch("Credit receiving key snapshot mismatch");
3986
- }
3987
- const clue = expected?.clueKey;
3988
- if (clue && !keys.clueKey.every((byte, i) => byte === clue[i])) {
3989
- throw new CreditMetadataMismatch("Credit clue key snapshot mismatch");
3990
- }
3991
- }
3992
- async creditNullifier(state) {
3993
- const n = state.note;
3994
- return computeCreditNullifier2(
3995
- n.chainId,
3996
- n.creditSharedKey,
3997
- await computeCreditCommitment(
3998
- n.chainId,
3999
- n.spenderWalletIdentity,
4000
- n.ownerWalletIdentity,
4001
- n.spenderPayout,
4002
- n.ownerPayout,
4003
- n.tokenAddress,
4004
- n.amount,
4005
- n.nonce,
4006
- n.expiry,
4007
- await computeCreditSharedKeyHash(n.creditSharedKey),
4008
- n
4009
- )
4010
- );
4011
- }
4012
- async recoverCredits(base, found, logs, snapshot) {
4013
- const known = /* @__PURE__ */ new Map();
4014
- const byNullifier = /* @__PURE__ */ new Map();
4015
- const cachedNullifiers = /* @__PURE__ */ new Map();
4016
- const recovered = [];
4017
- const foundByKey = new Map(
4018
- found.map((record) => [record.commitment + ":" + record.role, record])
3096
+ function object(value) {
3097
+ check3(value && typeof value === "object" && !Array.isArray(value), "invalid event object");
3098
+ return value;
3099
+ }
3100
+ function array(value) {
3101
+ check3(Array.isArray(value), "invalid event array");
3102
+ check3(
3103
+ Array.from({ length: value.length }, (_, i) => i in value).every(Boolean),
3104
+ "sparse event array"
3105
+ );
3106
+ return value;
3107
+ }
3108
+ function integer(value) {
3109
+ check3(
3110
+ typeof value === "bigint" || typeof value === "number" && Number.isSafeInteger(value),
3111
+ "invalid event integer"
3112
+ );
3113
+ const n = BigInt(value);
3114
+ check3(n >= 0n, "negative event integer");
3115
+ return n;
3116
+ }
3117
+ function index(value) {
3118
+ const n = integer(value);
3119
+ check3(n < 65536n, "invalid event leaf index");
3120
+ return Number(n);
3121
+ }
3122
+ function hash(value) {
3123
+ check3(typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value), "invalid event hash");
3124
+ return value.toLowerCase();
3125
+ }
3126
+ function contextCheck(context) {
3127
+ check3(context.chainId >= 0n && context.chainId < 1n << 64n, "invalid discovery chain");
3128
+ check3(
3129
+ isAddress(context.protocolAddress) && BigInt(context.protocolAddress) !== 0n,
3130
+ "invalid discovery instance/token"
3131
+ );
3132
+ }
3133
+ var position = (tree, at) => `${tree}:${at}`;
3134
+ function extractDeliveryCandidates(logs, context, confirmedThrough) {
3135
+ contextCheck(context);
3136
+ check3(confirmedThrough >= 0n, "invalid confirmed height");
3137
+ const relevant = logs.filter(
3138
+ (log) => log.address.toLowerCase() === context.protocolAddress.toLowerCase() && EVENTS.has(log.eventName)
3139
+ );
3140
+ const insertions = /* @__PURE__ */ new Map();
3141
+ const metadata = (log) => {
3142
+ check3(!log.removed, "discovery resync required after reorg");
3143
+ check3(
3144
+ typeof log.blockNumber === "bigint" && log.blockNumber >= 0n && log.blockNumber <= confirmedThrough,
3145
+ "event is not confirmed"
4019
3146
  );
4020
- const remember = async (record) => {
4021
- const key = record.commitment + ":" + record.role;
4022
- let nullifier = cachedNullifiers.get(record.commitment);
4023
- if (!nullifier) {
4024
- nullifier = await this.creditNullifier(record.state);
4025
- cachedNullifiers.set(record.commitment, nullifier);
4026
- }
4027
- known.set(key, record);
4028
- let members = byNullifier.get(nullifier);
4029
- if (!members) {
4030
- members = /* @__PURE__ */ new Set();
4031
- byNullifier.set(nullifier, members);
4032
- }
4033
- members.add(key);
4034
- };
4035
- const matching = (nullifier) => [...byNullifier.get(nullifier) ?? []].map((key) => known.get(key));
4036
- for (const record of Object.values(base.credits)) {
4037
- await remember(record);
4038
- }
4039
- for (const record of found) {
4040
- try {
4041
- this.checkSnapshotKeys(record);
4042
- await remember({ ...record, spent: "unknown" });
4043
- } catch (error) {
4044
- if (!(error instanceof CreditMetadataMismatch)) {
4045
- throw error;
4046
- }
4047
- }
4048
- }
4049
- const context = {
4050
- chainId: this.#options.scope.chainId,
4051
- protocolAddress: BigInt(this.#options.scope.protocolAddress)
3147
+ check3(Number.isSafeInteger(log.logIndex) && log.logIndex >= 0, "invalid log position");
3148
+ return {
3149
+ blockNumber: log.blockNumber,
3150
+ blockHash: hash(log.blockHash),
3151
+ transactionHash: hash(log.transactionHash)
4052
3152
  };
4053
- for (const event of logs) {
4054
- if (event.eventName === "CreditDraw") {
4055
- const block = await this.#options.reader.getBlock(event.blockNumber);
4056
- if (block.hash !== event.blockHash)
4057
- throw new CanonicalConflict("draw block changed during Credit recovery");
4058
- const args = event.args;
4059
- const params = args.params;
4060
- const indices = args.remainderLeafIndices;
4061
- check6(
4062
- Array.isArray(params.nullifiers) && params.nullifiers.length === params.remainderCommitments.length && params.nullifiers.length === params.recoveryAmounts.length && params.nullifiers.length === params.expiries.length && params.nullifiers.length === indices.length,
4063
- "invalid draw recovery slots"
4064
- );
4065
- for (const [index2, nullifier] of params.nullifiers.entries()) {
4066
- for (const previous of matching(nullifier)) {
4067
- check6(
4068
- previous.state.note.expiry === params.expiries[index2],
4069
- "draw recovery expiry mismatch"
4070
- );
4071
- const remainder = await recoverCreditRemainder(
4072
- context,
4073
- previous.state.note,
4074
- {
4075
- timestamp: block.timestamp,
4076
- creditNullifier: nullifier,
4077
- remainderCommitment: params.remainderCommitments[index2],
4078
- recoveryAmount: BigInt(params.recoveryAmounts[index2])
4079
- }
4080
- );
4081
- if (!remainder) {
4082
- continue;
4083
- }
4084
- const child = foundByKey.get(
4085
- params.remainderCommitments[index2] + ":" + previous.role
4086
- );
4087
- const payload = child?.state.note.payload;
4088
- const record = {
4089
- commitment: params.remainderCommitments[index2],
4090
- treeId: BigInt(args.outputTreeId),
4091
- leafIndex: Number(indices[index2]),
4092
- blockNumber: event.blockNumber,
4093
- blockHash: event.blockHash,
4094
- transactionHash: event.transactionHash,
4095
- role: previous.role,
4096
- state: {
4097
- ...clone(previous.state),
4098
- note: {
4099
- ...remainder,
4100
- ...payload === void 0 ? {} : { payload: clone(payload) }
4101
- }
4102
- },
4103
- receivingKeySource: { kind: "draw", commitment: previous.commitment },
4104
- spent: "unknown"
4105
- };
4106
- check6(
4107
- await snapshot.getLeaf(record.treeId, record.leafIndex) === BigInt(record.commitment),
4108
- "draw recovery canonical leaf mismatch"
4109
- );
4110
- this.checkSnapshotKeys(record);
4111
- await remember(record);
4112
- recovered.push(record);
4113
- }
4114
- }
4115
- } else if (event.eventName === "CreditToppedUp") {
4116
- const params = event.args.params;
4117
- for (const previous of matching(params.creditNullifier)) {
4118
- const child = foundByKey.get(
4119
- params.creation.creditCommitment + ":" + previous.role
4120
- );
4121
- if (!child) {
4122
- continue;
4123
- }
4124
- const old = previous.state.note;
4125
- const next = child.state.note;
4126
- if ([
4127
- "chainId",
4128
- "protocolAddress",
4129
- "tokenAddress",
4130
- "spenderWalletIdentity",
4131
- "ownerWalletIdentity",
4132
- "spenderPayout",
4133
- "ownerPayout",
4134
- "creditSharedKey"
4135
- ].some(
4136
- (field) => old[field] !== next[field]
4137
- ) || next.amount <= old.amount || next.expiry < old.expiry) {
4138
- continue;
4139
- }
4140
- const repaired = {
4141
- ...clone(child),
4142
- state: {
4143
- note: clone(next),
4144
- ownerReceivingKeys: clone(previous.state.ownerReceivingKeys),
4145
- spenderReceivingKeys: clone(previous.state.spenderReceivingKeys)
4146
- },
4147
- receivingKeySource: { kind: "top-up", commitment: previous.commitment }
4148
- };
4149
- this.checkSnapshotKeys(repaired);
4150
- await remember(repaired);
4151
- recovered.push(repaired);
4152
- }
4153
- }
3153
+ };
3154
+ const insertionKey = (log, tree, at) => `${hash(log.transactionHash)}:${position(tree, at)}`;
3155
+ for (const log of relevant) {
3156
+ metadata(log);
3157
+ if (log.eventName !== "CommitmentsInserted" && log.eventName !== "BatchOutputs") continue;
3158
+ const a = object(log.args), tree = integer(a.treeId), start = index(a.startLeafIndex), leaves = array(a.commitments);
3159
+ check3(tree > 0n && tree < 1n << 64n, "invalid insertion tree");
3160
+ check3(start + leaves.length <= 65536, "insertion exceeds tree capacity");
3161
+ for (const [i, leaf] of leaves.entries()) {
3162
+ const key = insertionKey(log, tree, start + i), value = hash(leaf), previous = insertions.get(key);
3163
+ check3(!previous || previous === value, "conflicting insertion events");
3164
+ insertions.set(key, value);
4154
3165
  }
4155
- return recovered;
4156
3166
  }
4157
- };
4158
-
4159
- // src/session/WalletUpdater.ts
4160
- var check7 = (value, message) => {
4161
- if (!value) throw new Error(message);
4162
- };
4163
- var position2 = (treeId, leafIndex) => `${treeId}:${leafIndex}`;
4164
- var WalletUpdater = class extends InventoryProcessor {
4165
- #options;
4166
- constructor(options) {
4167
- super(options);
4168
- this.#options = { ...options };
4169
- check7(
4170
- options.viewing.ownerAddress === options.scope.ownerAddress,
4171
- "viewing capability wallet mismatch"
3167
+ const results = /* @__PURE__ */ new Map();
3168
+ const add = (log, tree, at, commitment, role, wire, token = log.args?.token ?? log.args?.params?.token ?? log.args?.params?.creation?.token) => {
3169
+ check3(tree > 0n && tree < 1n << 64n, "invalid output tree");
3170
+ const inserted = insertions.get(insertionKey(log, tree, at));
3171
+ check3(inserted, "delivery has no matching canonical insertion");
3172
+ if (commitment !== void 0)
3173
+ check3(hash(commitment) === inserted, "delivery commitment conflicts with insertion");
3174
+ const delivery = normalizeOpaqueDelivery(wire, { allowEmpty: role === "funds" });
3175
+ check3(
3176
+ typeof token === "string" && isAddress(token) && BigInt(token) !== 0n,
3177
+ "invalid event token"
4172
3178
  );
4173
- }
4174
- applyUpdates(batch, options = {}) {
4175
- return this.run(structuredClone(batch), { ...options });
4176
- }
4177
- async run(batch, options) {
4178
- const { reader, protocol, scope, store, viewing } = this.#options;
4179
- const nullifier = NullifierCapability.fromIdentity(scope.ownerAddress);
4180
- const { signal, reset } = options;
4181
- signal?.throwIfAborted();
4182
- const context = protocol.context;
4183
- check7(
4184
- batch.context.chainId === context.chainId && batch.context.protocolAddress.toLowerCase() === context.protocolAddress.toLowerCase(),
4185
- "update deployment context mismatch"
3179
+ const value = {
3180
+ tokenAddress: token,
3181
+ treeId: tree,
3182
+ leafIndex: at,
3183
+ commitment: inserted,
3184
+ role,
3185
+ delivery,
3186
+ ...metadata(log)
3187
+ };
3188
+ const key = `${insertionKey(log, tree, at)}:${role}`, previous = results.get(key);
3189
+ check3(
3190
+ !previous || previous.tokenAddress.toLowerCase() === token.toLowerCase() && previous.delivery.ciphertext === delivery.ciphertext && previous.delivery.clue === delivery.clue && previous.blockHash === value.blockHash,
3191
+ "conflicting delivery events"
4186
3192
  );
4187
- check7(
4188
- batch.block.number >= protocol.deployment.protocol.fromBlock,
4189
- "update precedes deployment"
3193
+ results.set(key, value);
3194
+ };
3195
+ const creation = (log, a, fields) => {
3196
+ const tree = integer(a.outputTreeId), leaves = array(a.leafIndices), ds = array(a.deliveries);
3197
+ const change = hash(fields.changeCommitment), hasChange = BigInt(change) !== 0n;
3198
+ check3(
3199
+ ds.length === 3 && leaves.length === (hasChange ? 2 : 1),
3200
+ "invalid Credit creation delivery slots"
4190
3201
  );
4191
- await protocol.validateDeployment(reader);
4192
- const base = await store.readSnapshot(scope);
4193
- check7(reset || !base.invalidated, "wallet state is invalidated; rebuild required");
4194
- try {
4195
- if (!reset) {
4196
- for (const cursor of [base.observedThrough]) {
4197
- if (!cursor) continue;
4198
- if ((await reader.getBlock(cursor.number)).hash !== cursor.hash)
4199
- throw new CanonicalConflict("Canonical history changed after a reorg");
4200
- check7(
4201
- cursor.number <= batch.block.number,
4202
- "update target precedes wallet checkpoint"
4203
- );
4204
- }
4205
- } else {
4206
- base.notes = {};
4207
- base.credits = {};
4208
- }
4209
- const block = await reader.getBlock(batch.block.number);
4210
- check7(
4211
- block.hash === batch.block.hash && block.number === batch.block.number,
4212
- "update target block is not canonical"
4213
- );
4214
- const receipts = [];
4215
- for (const hash2 of new Set(batch.transactionHashes)) {
4216
- signal?.throwIfAborted();
4217
- check7(/^0x[0-9a-fA-F]{64}$/.test(hash2), "invalid candidate transaction hash");
4218
- const receipt = await reader.getReceipt(hash2);
4219
- check7(
4220
- receipt && receipt.status === "success" && receipt.transactionHash.toLowerCase() === hash2.toLowerCase(),
4221
- "invalid or unsuccessful candidate receipt"
4222
- );
4223
- check7(
4224
- receipt.block.number >= protocol.deployment.protocol.fromBlock && receipt.block.number <= block.number,
4225
- "candidate receipt outside update range"
4226
- );
4227
- if ((await reader.getBlock(receipt.block.number)).hash !== receipt.block.hash)
4228
- throw new CanonicalConflict("Candidate receipt disagrees with canonical chain");
4229
- receipts.push(receipt);
4230
- }
4231
- const logs = receipts.flatMap(
4232
- (receipt) => readProtocolLogs(receipt, context.protocolAddress)
4233
- );
4234
- logs.sort(
4235
- (a, b) => a.blockNumber < b.blockNumber ? -1 : a.blockNumber > b.blockNumber ? 1 : a.logIndex - b.logIndex
4236
- );
4237
- const leaves = /* @__PURE__ */ new Map();
4238
- for (const event of logs) {
4239
- if (event.eventName !== "CommitmentsInserted" && event.eventName !== "BatchOutputs")
4240
- continue;
4241
- const { treeId, startLeafIndex, commitments } = event.args;
4242
- for (const [i, commitment] of commitments.entries()) {
4243
- const key = position2(treeId, Number(startLeafIndex) + i);
4244
- check7(
4245
- !leaves.has(key) || leaves.get(key) === BigInt(commitment),
4246
- "conflicting canonical insertion"
4247
- );
4248
- leaves.set(key, BigInt(commitment));
4249
- }
4250
- }
4251
- const getLeaf = (tree, index2) => {
4252
- const leaf = leaves.get(position2(tree, index2));
4253
- check7(leaf !== void 0, "missing canonical insertion");
4254
- return leaf;
4255
- };
4256
- const decrypted = await decryptCandidates(logs, {
4257
- context,
4258
- viewing,
4259
- confirmedThrough: block.number
4260
- });
4261
- const notes = decrypted.notes.map((record) => ({
4262
- ...record,
4263
- spent: "unknown"
4264
- }));
4265
- const incoming = decrypted.credits.map((record) => ({
4266
- ...record,
4267
- spent: "unknown"
4268
- }));
4269
- const recovered = await this.recoverCredits(base, incoming, logs, { getLeaf });
4270
- const creditsByKey = new Map(
4271
- incoming.map((record) => [record.commitment + ":" + record.role, record])
3202
+ add(log, tree, index(leaves[0]), fields.creditCommitment, "owner", ds[0]);
3203
+ add(log, tree, index(leaves[0]), fields.creditCommitment, "spender", ds[1]);
3204
+ if (hasChange) add(log, tree, index(leaves[1]), change, "funds", ds[2]);
3205
+ else normalizeOpaqueDelivery(ds[2], { empty: true });
3206
+ };
3207
+ for (const log of relevant) {
3208
+ const a = object(log.args);
3209
+ if (log.eventName === "Shield") {
3210
+ const ds = array(a.deliveries), tree = integer(a.treeId), start = index(a.startLeafIndex);
3211
+ check3(start + ds.length <= 65536, "shield exceeds tree capacity");
3212
+ ds.forEach((d, i) => add(log, tree, start + i, void 0, "funds", d));
3213
+ } else if (log.eventName === "Transact" || log.eventName === "CrossTreeTransact") {
3214
+ const cs = array(a.outputCommitments), ds = array(a.deliveries), leaves = array(a.leafIndices), tree = integer(a.outputTreeId);
3215
+ check3(
3216
+ cs.length === ds.length && cs.length === leaves.length,
3217
+ "invalid transact delivery slots"
4272
3218
  );
4273
- for (const record of recovered)
4274
- creditsByKey.set(record.commitment + ":" + record.role, record);
4275
- const credits = [...creditsByKey.values()].filter((record) => {
4276
- try {
4277
- this.checkSnapshotKeys(record);
4278
- return true;
4279
- } catch (error) {
4280
- if (error instanceof CreditMetadataMismatch) return false;
4281
- throw error;
4282
- }
4283
- }).sort(
4284
- (a, b) => a.blockNumber < b.blockNumber ? -1 : a.blockNumber > b.blockNumber ? 1 : a.treeId < b.treeId ? -1 : a.treeId > b.treeId ? 1 : a.leafIndex - b.leafIndex
3219
+ cs.forEach((c, i) => add(log, tree, index(leaves[i]), c, "funds", ds[i]));
3220
+ } else if (log.eventName === "BatchOutputs") {
3221
+ const tokens = array(a.tokens);
3222
+ const cs = array(a.commitments), tree = integer(a.treeId), start = index(a.startLeafIndex), packed = a.packedDeliveries;
3223
+ check3(
3224
+ typeof packed === "string" && /^0x(?:[0-9a-fA-F]{2})*$/.test(packed),
3225
+ "invalid packed deliveries"
4285
3226
  );
4286
- for (const record of [...notes, ...credits]) {
4287
- signal?.throwIfAborted();
4288
- const proof = await this.#options.protocolRpc.getMembership(context, record, block);
4289
- const tokenAddress = "note" in record ? record.note.tokenAddress : record.state.note.tokenAddress;
4290
- await verifyMembership(
4291
- reader,
4292
- {
4293
- ...context,
4294
- tokenAddress: `0x${tokenAddress.toString(16).padStart(40, "0")}`
4295
- },
4296
- block,
4297
- record,
4298
- proof
3227
+ check3(tokens.length === cs.length, "batch token count mismatch");
3228
+ check3(start + cs.length <= 65536, "batch exceeds tree capacity");
3229
+ let offset = 2;
3230
+ cs.forEach((c, i) => {
3231
+ check3(offset + 4 <= packed.length, "truncated delivery length prefix");
3232
+ const length = Number.parseInt(packed.slice(offset, offset + 4), 16);
3233
+ offset += 4;
3234
+ check3(
3235
+ length <= MAX_CIPHERTEXT_BYTES && offset + length * 2 + (length === 0 ? 0 : 136) <= packed.length,
3236
+ "invalid packed delivery length"
4299
3237
  );
4300
- }
4301
- const allNotes = {
4302
- ...base.notes,
4303
- ...Object.fromEntries(notes.map((record) => [record.commitment, record]))
4304
- };
4305
- const allCredits = {
4306
- ...base.credits,
4307
- ...Object.fromEntries(
4308
- credits.map((record) => [record.commitment + ":" + record.role, record])
4309
- )
4310
- };
4311
- const observations = [];
4312
- for (const [id, record] of Object.entries(allNotes))
4313
- observations.push({
4314
- kind: "notes",
4315
- id,
4316
- nullifier: await nullifier.compute(
4317
- scope.chainId,
4318
- record.commitment,
4319
- record.note.nonce
4320
- )
4321
- });
4322
- for (const [id, record] of Object.entries(allCredits))
4323
- observations.push({
4324
- kind: "credits",
4325
- id,
4326
- nullifier: await this.creditNullifier(record.state)
4327
- });
4328
- const spent = observations.length ? await reader.isSpent(
4329
- context,
4330
- observations.map((item) => item.nullifier),
4331
- block
4332
- ) : [];
4333
- check7(
4334
- spent.length === observations.length && spent.every((value) => typeof value === "boolean"),
4335
- "invalid spent-state response"
3238
+ const ciphertext = `0x${packed.slice(offset, offset + length * 2)}`;
3239
+ offset += length * 2;
3240
+ const clue = length === 0 ? "0x" : `0x${packed.slice(offset, offset + 136)}`;
3241
+ if (length !== 0) offset += 136;
3242
+ add(log, tree, start + i, c, "funds", { ciphertext, clue }, tokens[i]);
3243
+ });
3244
+ check3(offset === packed.length, "trailing packed delivery bytes");
3245
+ } else if (log.eventName === "CreditCreated") creation(log, a, a);
3246
+ else if (log.eventName === "CreditToppedUp")
3247
+ creation(log, a, object(object(a.params).creation));
3248
+ else if (log.eventName === "CreditDraw") {
3249
+ const p = object(a.params), cs = array(p.remainderCommitments), ds = array(a.deliveries), leaves = array(a.remainderLeafIndices), tree = integer(a.outputTreeId);
3250
+ check3(
3251
+ ds.length === 2 * cs.length && leaves.length === cs.length,
3252
+ "invalid draw delivery slots"
4336
3253
  );
4337
- signal?.throwIfAborted();
4338
- if ((await reader.getBlock(block.number)).hash !== block.hash)
4339
- throw new CanonicalConflict("Canonical block changed during update");
4340
- await store.transaction(scope, (draft) => {
4341
- signal?.throwIfAborted();
4342
- check7(draft.revision === base.revision, "wallet changed during update; retry");
4343
- if (reset) {
4344
- this.reorgOperations(draft);
4345
- draft.notes = {};
4346
- draft.credits = {};
4347
- delete draft.observedThrough;
4348
- delete draft.discoveryThrough;
4349
- delete draft.invalidated;
3254
+ cs.forEach((c, i) => {
3255
+ if (BigInt(hash(c)) === 0n) {
3256
+ check3(integer(leaves[i]) === 0n, "absent remainder has nonzero position");
3257
+ normalizeOpaqueDelivery(ds[2 * i], { empty: true });
3258
+ normalizeOpaqueDelivery(ds[2 * i + 1], { empty: true });
3259
+ } else {
3260
+ add(log, tree, index(leaves[i]), c, "owner", ds[2 * i]);
3261
+ add(log, tree, index(leaves[i]), c, "spender", ds[2 * i + 1]);
4350
3262
  }
4351
- mergeReceiptRecords(draft, { notes, credits, checkpoint: block });
4352
- observations.forEach((item, index2) => {
4353
- const record = draft[item.kind][item.id];
4354
- if (record) {
4355
- mergeSpentObservation(record, {
4356
- spent: spent[index2] ? "spent" : "unspent",
4357
- checkedThrough: { number: block.number, hash: block.hash }
4358
- });
4359
- }
4360
- });
4361
- draft.observedThrough = { number: block.number, hash: block.hash };
4362
3263
  });
4363
- return {
4364
- block,
4365
- notes: notes.length,
4366
- credits: credits.length,
4367
- balances: walletBalances(await store.readSnapshot(scope))
4368
- };
4369
- } catch (error) {
4370
- if (error instanceof CanonicalConflict) await this.invalidate();
4371
- throw error;
4372
3264
  }
4373
3265
  }
4374
- };
4375
-
4376
- // src/session/WalletSession.ts
4377
- var WalletSession = class {
4378
- #options;
4379
- #updater;
4380
- #listeners = /* @__PURE__ */ new Set();
4381
- #sending = /* @__PURE__ */ new Set();
4382
- #refreshQueue = Promise.resolve();
4383
- scope;
4384
- protocol;
4385
- constructor(options) {
4386
- if ("addressLookup" in options || "addressResolver" in options)
4387
- throw new TypeError(
4388
- "Recipient lookup belongs to each EVM output; caller resolver injection is not supported"
4389
- );
4390
- options = configureRecipientInfrastructure(options);
4391
- if ("ciphertextCodecs" in options || "envelopeVersion" in options) {
4392
- throw new Error(
4393
- "SDK envelope versions are fixed; encode application payload bytes externally"
4394
- );
4395
- }
4396
- const store = options.store;
4397
- this.#options = {
4398
- ...options,
4399
- ...store ? {
4400
- store: {
4401
- persistent: store.persistent,
4402
- readSnapshot: (scope) => store.readSnapshot(scope),
4403
- transaction: async (scope, action) => {
4404
- const result = await store.transaction(scope, action);
4405
- if (scopeKey(scope) === scopeKey(this.scope)) {
4406
- for (const listener of [...this.#listeners]) {
4407
- try {
4408
- listener();
4409
- } catch {
4410
- }
4411
- }
4412
- }
4413
- return result;
4414
- }
4415
- }
4416
- } : {},
4417
- ...options.gasBenchmarks ? { gasBenchmarks: copyGasBenchmarks(options.gasBenchmarks) } : {}
4418
- };
4419
- this.protocol = options.protocol;
4420
- this.scope = Object.freeze({
4421
- namespace: options.namespace ?? "default",
4422
- ...options.protocol.context,
4423
- ownerAddress: options.wallet.ownerAddress
4424
- });
4425
- scopeKey(this.scope);
4426
- Object.freeze(this);
3266
+ return [...results.values()];
3267
+ }
3268
+ async function decryptCandidates(logs, options) {
3269
+ contextCheck(options.context);
3270
+ decodeAddress(ReceivingAddress.fromIdentity(options.viewing.ownerAddress).toString());
3271
+ check3(
3272
+ !logs.some(
3273
+ (log) => log.address.toLowerCase() === options.context.protocolAddress.toLowerCase() && log.removed
3274
+ ),
3275
+ "discovery resync required after reorg"
3276
+ );
3277
+ const candidates = extractDeliveryCandidates(logs, options.context, options.confirmedThrough);
3278
+ const positions = /* @__PURE__ */ new Map();
3279
+ const unique = /* @__PURE__ */ new Map();
3280
+ for (const candidate of candidates) {
3281
+ const key = position(candidate.treeId, candidate.leafIndex);
3282
+ const previous = positions.get(key);
3283
+ check3(
3284
+ !previous || previous.commitment === candidate.commitment && previous.blockHash === candidate.blockHash && previous.transactionHash === candidate.transactionHash && previous.tokenAddress.toLowerCase() === candidate.tokenAddress.toLowerCase(),
3285
+ "conflicting canonical position"
3286
+ );
3287
+ positions.set(key, candidate);
3288
+ unique.set(`${key}:${candidate.role}`, candidate);
4427
3289
  }
4428
- /** Delegate sending to ethers; managed operations additionally retain the resulting hash. */
4429
- async sendTransaction(input, options = {}) {
4430
- const signer = this.#options.signer;
4431
- if (!signer) throw new MissingCapabilityError2("ethers signer");
4432
- const operation = input instanceof Operation ? input : void 0;
4433
- if (operation && this.#sending.has(operation.id))
4434
- throw new Error("operation is already being sent by this session");
4435
- if (operation) this.#sending.add(operation.id);
4436
- const overrides = { ...options };
4437
- const plain = operation ? void 0 : { ...input };
3290
+ const notes = [];
3291
+ const credits = [];
3292
+ for (const candidate of unique.values()) {
3293
+ const { delivery, role, ...positionData } = candidate;
3294
+ const context = {
3295
+ chainId: options.context.chainId,
3296
+ protocolAddress: BigInt(options.context.protocolAddress),
3297
+ tokenAddress: BigInt(candidate.tokenAddress),
3298
+ outputCommitment: candidate.commitment
3299
+ };
4438
3300
  try {
4439
- if (!signer.provider) throw new MissingCapabilityError2("connected ethers signer");
4440
- if ((await signer.provider.getNetwork()).chainId !== this.protocol.context.chainId) {
4441
- throw new Error("signer-provider session chain mismatch");
4442
- }
4443
- if (operation) {
4444
- if (!this.#options.store) throw new MissingCapabilityError2("wallet store");
4445
- const state = await this.#options.store.readSnapshot(this.scope);
4446
- if (!state.operations[operation.id])
4447
- throw new Error("operation does not belong to this wallet session");
4448
- }
4449
- const call = operation ? await operation.exportTransaction() : plain;
4450
- const transaction = await signer.sendTransaction({
4451
- ...overrides,
4452
- to: call.to,
4453
- data: call.data,
4454
- value: call.value,
4455
- chainId: this.protocol.context.chainId
4456
- });
4457
- if (operation) {
4458
- try {
4459
- await operation.trackTransaction(transaction.hash);
4460
- } catch (cause) {
4461
- throw new Error(
4462
- `Transaction ${transaction.hash} was sent but its operation association could not be saved; resume and reconcile this hash`,
4463
- { cause }
4464
- );
4465
- }
3301
+ if (role === "funds") {
3302
+ if (delivery.ciphertext === "0x") continue;
3303
+ const note = await options.viewing.decryptNote(delivery.ciphertext, context);
3304
+ notes.push({ ...positionData, note });
3305
+ } else {
3306
+ const state = await options.viewing.decryptCredit(delivery.ciphertext, {
3307
+ ...context,
3308
+ role
3309
+ });
3310
+ credits.push({ ...positionData, role, state });
4466
3311
  }
4467
- return transaction;
4468
- } finally {
4469
- if (operation) this.#sending.delete(operation.id);
4470
- }
4471
- }
4472
- runtime(tokenAddress) {
4473
- const options = this.#options;
4474
- if (!options.store) {
4475
- throw new MissingCapabilityError2("wallet store");
4476
- }
4477
- if (!options.chainReader) {
4478
- throw new MissingCapabilityError2("chain reader");
3312
+ } catch {
4479
3313
  }
4480
- return {
4481
- options: { ...options, protocol: options.protocol.forToken(tokenAddress) },
4482
- scope: this.scope,
4483
- store: options.store,
4484
- reader: options.chainReader,
4485
- authorization: () => {
4486
- if (options.authorization) return options.authorization;
4487
- if (options.wallet instanceof Wallet) return options.wallet.exportAuthorization();
4488
- throw new MissingCapabilityError2("wallet authorization");
4489
- }
4490
- };
4491
3314
  }
4492
- updater() {
4493
- if (this.#updater) return this.#updater;
4494
- const options = this.#options;
4495
- if (!options.store || !options.chainReader || !options.protocolRpc)
4496
- throw new MissingCapabilityError2("wallet store, chain reader and Protocol RPC");
4497
- const viewing = options.viewing ?? (options.wallet instanceof Wallet ? options.wallet.getViewer() : void 0);
4498
- if (!viewing) throw new MissingCapabilityError2("viewing");
4499
- const receivingAddress = options.wallet.getReceivingAddress();
4500
- const receivingKeys = receivingAddress.kind === "long" ? receivingAddress.getReceivingKeys() : void 0;
4501
- this.#updater = new WalletUpdater({
4502
- protocol: options.protocol,
4503
- scope: this.scope,
4504
- store: options.store,
4505
- reader: options.chainReader,
4506
- viewing,
4507
- protocolRpc: options.protocolRpc,
4508
- ...receivingKeys ? { receivingKeys } : {}
3315
+ return { notes, credits };
3316
+ }
3317
+
3318
+ // src/broadcaster/index.ts
3319
+ import { invisiProtocolAbi as invisiProtocolAbi2 } from "@invisi-labs/protocol/contracts";
3320
+ import { AbiCoder, getAddress as getAddress2, Interface as Interface2, keccak256, verifyTypedData as verifyTypedData2 } from "ethers";
3321
+ var broadcasterQuoteTypes = {
3322
+ Quote: [
3323
+ { name: "token", type: "address" },
3324
+ { name: "feeRecipient", type: "address" },
3325
+ { name: "feeAmount", type: "uint256" },
3326
+ { name: "deadline", type: "uint256" },
3327
+ { name: "maxGas", type: "uint256" },
3328
+ { name: "requestHash", type: "bytes32" }
3329
+ ]
3330
+ };
3331
+ function broadcasterQuoteDomain(trust) {
3332
+ return {
3333
+ name: "InvisiBroadcaster",
3334
+ version: "1",
3335
+ chainId: trust.chainId,
3336
+ verifyingContract: getAddress2(trust.protocol)
3337
+ };
3338
+ }
3339
+ var supportedMethods = ["transact", "shield", "createCredit", "topUpCredit", "cancelCredit"];
3340
+ var intentAbi = invisiProtocolAbi2.filter(
3341
+ (item) => item.type === "function" && supportedMethods.includes(item.name) && (item.name !== "transact" || item.inputs[1]?.type === "tuple")
3342
+ );
3343
+ var broadcasterTransactInterface = new Interface2(intentAbi);
3344
+ function decodeBroadcasterIntent(data) {
3345
+ const parsed = broadcasterTransactInterface.parseTransaction({ data });
3346
+ if (!parsed || !supportedMethods.includes(parsed.name))
3347
+ throw new Error("unsupported broadcaster method");
3348
+ const fragment = parsed.fragment;
3349
+ const decoded = parsed.args;
3350
+ if (broadcasterTransactInterface.encodeFunctionData(fragment, decoded).toLowerCase() !== data.toLowerCase())
3351
+ throw new Error("noncanonical broadcaster calldata");
3352
+ const method = parsed.name;
3353
+ const raw = method === "transact" ? decoded[1] : void 0;
3354
+ const params = raw?.params ?? (method === "shield" ? decoded[0] : method === "topUpCredit" ? decoded[1].creation : decoded[1]);
3355
+ const funding = raw?.funding ?? (method === "shield" ? decoded[3] : ["createCredit", "topUpCredit"].includes(method) ? decoded[3] : void 0);
3356
+ const deliveries = raw?.deliveries ?? (method === "shield" ? decoded[2] : ["createCredit", "topUpCredit"].includes(method) ? decoded[2] : []);
3357
+ return {
3358
+ fragment,
3359
+ method,
3360
+ args: decoded,
3361
+ proof: method === "shield" ? "0x" : decoded[0],
3362
+ request: { params, funding, deliveries }
3363
+ };
3364
+ }
3365
+ function broadcasterRequestHash(data) {
3366
+ const { fragment, method, args } = decodeBroadcasterIntent(data);
3367
+ const copy = (value) => Array.isArray(value) ? value.map(copy) : value;
3368
+ const canonical = copy(args);
3369
+ if (method !== "shield") canonical[0] = "0x";
3370
+ if (method === "transact") canonical[1][2][4] = "0x";
3371
+ else if (method !== "cancelCredit") canonical[3][4] = "0x";
3372
+ const calldata = broadcasterTransactInterface.encodeFunctionData(fragment, canonical);
3373
+ return keccak256(
3374
+ AbiCoder.defaultAbiCoder().encode(
3375
+ ["string", "string", "bytes"],
3376
+ ["InvisiBroadcasterIntentV1", method, calldata]
3377
+ )
3378
+ );
3379
+ }
3380
+ function verifyBroadcasterQuote(signed, trust, data, now = BigInt(Math.floor(Date.now() / 1e3))) {
3381
+ const { quote } = signed;
3382
+ const { request } = decodeBroadcasterIntent(data);
3383
+ const p = request.params;
3384
+ if (quote.requestHash !== broadcasterRequestHash(data) || getAddress2(quote.token) !== getAddress2(p.token) || getAddress2(quote.feeRecipient) !== getAddress2(p.feeRecipient) || BigInt(quote.feeAmount) !== p.feeAmount || BigInt(quote.deadline) > p.deadline || BigInt(quote.deadline) < now || BigInt(quote.maxGas) <= 0n || BigInt(quote.feeAmount) <= 0n || BigInt(quote.feeAmount) >= 1n << 128n)
3385
+ throw new Error("quote does not match final intent or bounds");
3386
+ if (getAddress2(
3387
+ verifyTypedData2(
3388
+ broadcasterQuoteDomain(trust),
3389
+ broadcasterQuoteTypes,
3390
+ quote,
3391
+ signed.signature
3392
+ )
3393
+ ) !== getAddress2(trust.signer))
3394
+ throw new Error("untrusted quote signature");
3395
+ return quote;
3396
+ }
3397
+ function checkedStatus(value, id) {
3398
+ const status = value;
3399
+ if (!status || status.id !== id || !["pending", "submitted", "confirmed", "reverted", "uncertain"].includes(status.state) || status.transactionHash !== void 0 && !/^0x[0-9a-fA-F]{64}$/.test(status.transactionHash))
3400
+ throw new Error("invalid broadcaster status");
3401
+ return status;
3402
+ }
3403
+ var BroadcasterClient = class {
3404
+ configuration;
3405
+ url;
3406
+ transport;
3407
+ requestId = 0;
3408
+ constructor(configuration, transport) {
3409
+ this.configuration = Object.freeze({
3410
+ endpoint: configuration.endpoint,
3411
+ quoteSigner: getAddress2(configuration.quoteSigner),
3412
+ feeRecipient: getAddress2(configuration.feeRecipient)
4509
3413
  });
4510
- return this.#updater;
4511
- }
4512
- /** Local session commits only. Host applications own cross-tab/store subscriptions. */
4513
- subscribe(listener) {
4514
- this.#listeners.add(listener);
4515
- return () => {
4516
- this.#listeners.delete(listener);
4517
- };
4518
- }
4519
- /** Detached history for wallet UI; construction records do not imply chain success. */
4520
- async getHistory(options = {}) {
4521
- const tokenAddress = options.tokenAddress ? getAddress(options.tokenAddress) : void 0;
4522
- const store = this.#options.store;
4523
- if (!store) throw new MissingCapabilityError2("wallet store");
4524
- return walletHistory(
4525
- await store.readSnapshot(this.scope),
4526
- tokenAddress ? { tokenAddress } : {}
4527
- );
4528
- }
4529
- async balance(tokenAddress) {
4530
- const store = this.#options.store;
4531
- if (!store) {
4532
- throw new MissingCapabilityError2("wallet store");
4533
- }
4534
- return walletBalance(await store.readSnapshot(this.scope), getAddress(tokenAddress));
4535
- }
4536
- /** Pull explicitly selected indexed pages; no chain scanning or public tree reconstruction. */
4537
- async refresh(options) {
4538
- const provider = this.#options.protocolRpc;
4539
- const reader = this.#options.chainReader;
4540
- if (!reader) throw new MissingCapabilityError2("chain reader");
4541
- if (!provider) throw new MissingCapabilityError2("Protocol RPC");
4542
- const updater = this.updater();
4543
- const request = {
4544
- ...options,
4545
- ...options.detectionKey ? { detectionKey: options.detectionKey.slice() } : {}
4546
- };
4547
- const task = this.#refreshQueue.then(async () => {
4548
- const result = await refreshWallet(
4549
- {
4550
- provider,
4551
- reader,
4552
- context: this.protocol.context,
4553
- fromBlock: this.protocol.deployment.protocol.fromBlock,
4554
- apply: (batch, settings) => updater.applyUpdates(batch, settings)
4555
- },
4556
- request
4557
- );
4558
- await this.#options.store.transaction(this.scope, (state) => {
4559
- request.signal?.throwIfAborted();
4560
- if (state.invalidated) throw new Error("wallet requires canonical resync");
4561
- const previous = state.discoveryThrough;
4562
- const start = previous ? previous.number + 1n : this.protocol.deployment.protocol.fromBlock;
4563
- if (request.fromBlock <= start && (!previous || result.block.number >= previous.number)) {
4564
- state.discoveryThrough = {
4565
- number: result.block.number,
4566
- hash: result.block.hash
4567
- };
4568
- }
3414
+ if (BigInt(this.configuration.quoteSigner) === 0n || BigInt(this.configuration.feeRecipient) === 0n)
3415
+ throw new Error("invalid broadcaster configuration");
3416
+ this.url = configuration.endpoint;
3417
+ const base = new URL(this.url);
3418
+ if (base.protocol !== "https:" || base.username || base.password)
3419
+ throw new Error("broadcaster requires HTTPS");
3420
+ this.transport = transport ?? (async (request) => {
3421
+ const response = await fetch(base, {
3422
+ redirect: "error",
3423
+ method: "POST",
3424
+ body: JSON.stringify(request),
3425
+ headers: { "content-type": "application/json" },
3426
+ signal: AbortSignal.timeout(3e4)
4569
3427
  });
4570
- return result;
4571
- });
4572
- this.#refreshQueue = task.catch(() => {
4573
- });
4574
- return task;
4575
- }
4576
- preparation(tokenAddress) {
4577
- const runtime = this.runtime(tokenAddress);
4578
- return new TransactionPreparation({
4579
- options: runtime.options,
4580
- scope: runtime.scope,
4581
- reader: runtime.reader,
4582
- readInventory: () => runtime.store.readSnapshot(runtime.scope),
4583
- complete: (plan) => Operation.prepare(runtime, plan)
4584
- });
4585
- }
4586
- async prepareTransfer(options) {
4587
- return prepareWithErrors("prepareTransfer", async () => {
4588
- return this.preparation(options.tokenAddress).prepareTransfer(options);
4589
- });
4590
- }
4591
- async prepareShield(options) {
4592
- return prepareWithErrors("prepareShield", async () => {
4593
- return this.preparation(options.tokenAddress).prepareShield(options);
4594
- });
4595
- }
4596
- async prepareCreateCredit(options) {
4597
- return prepareWithErrors("prepareCreateCredit", async () => {
4598
- return this.preparation(options.tokenAddress).prepareCreateCredit(options);
3428
+ if (!response.ok) throw new Error(`Broadcaster HTTP ${response.status}`);
3429
+ if (!response.body) throw new Error("empty broadcaster response");
3430
+ const reader = response.body.getReader();
3431
+ const chunks = [];
3432
+ let size = 0;
3433
+ try {
3434
+ while (true) {
3435
+ const result = await reader.read();
3436
+ if (result.done) break;
3437
+ size += result.value.length;
3438
+ if (size > 65536) throw new Error("broadcaster response too large");
3439
+ chunks.push(result.value);
3440
+ }
3441
+ } finally {
3442
+ await reader.cancel();
3443
+ }
3444
+ const bytes = new Uint8Array(size);
3445
+ let offset = 0;
3446
+ for (const chunk of chunks) {
3447
+ bytes.set(chunk, offset);
3448
+ offset += chunk.length;
3449
+ }
3450
+ return JSON.parse(new TextDecoder().decode(bytes));
4599
3451
  });
4600
3452
  }
4601
- async prepareDrawCredit(options) {
4602
- return prepareWithErrors("prepareDrawCredit", async () => {
4603
- return this.preparation(options.tokenAddress).prepareDrawCredit(options);
3453
+ async call(method, params) {
3454
+ const id = ++this.requestId;
3455
+ const response = await this.transport({
3456
+ jsonrpc: "2.0",
3457
+ id,
3458
+ method,
3459
+ params: [params]
4604
3460
  });
3461
+ if (!response || response.jsonrpc !== "2.0" || response.id !== id || Object.hasOwn(response, "result") === Object.hasOwn(response, "error"))
3462
+ throw new Error("invalid broadcaster JSON-RPC response");
3463
+ if (response.error) {
3464
+ if (!Number.isInteger(response.error.code) || typeof response.error.message !== "string")
3465
+ throw new Error("invalid broadcaster JSON-RPC error");
3466
+ throw new BroadcasterRpcError(response.error.code, response.error.message);
3467
+ }
3468
+ return response.result;
4605
3469
  }
4606
- async prepareTopUpCredit(options) {
4607
- return prepareWithErrors("prepareTopUpCredit", async () => {
4608
- return this.preparation(options.tokenAddress).prepareTopUpCredit(options);
3470
+ async fee(token, data) {
3471
+ const fee = await this.call("broadcaster_getFee", {
3472
+ token: getAddress2(token),
3473
+ ...data ? { data } : {}
4609
3474
  });
3475
+ if (!fee || getAddress2(fee.feeRecipient) !== this.configuration.feeRecipient || !/^\d+$/.test(fee.feeAmount) || BigInt(fee.feeAmount) <= 0n || BigInt(fee.feeAmount) >= 1n << 128n || !/^\d+$/.test(fee.maxGas) || BigInt(fee.maxGas) <= 0n)
3476
+ throw new Error("invalid broadcaster fee");
3477
+ return fee;
4610
3478
  }
4611
- async prepareCancelCredit(options) {
4612
- return prepareWithErrors("prepareCancelCredit", async () => {
4613
- return this.preparation(options.tokenAddress).prepareCancelCredit(options);
3479
+ async quote(data, context) {
3480
+ const signed = await this.call("broadcaster_getQuote", {
3481
+ data
4614
3482
  });
4615
- }
4616
- async resume(operationId) {
4617
- const store = this.#options.store;
4618
- if (!store) throw new MissingCapabilityError2("wallet store");
4619
- const state = await store.readSnapshot(this.scope);
4620
- const saved = state.operations[operationId]?.payload;
4621
- if (!saved?.plan) throw new Error("operation not found");
4622
- return Operation.resume(this.runtime(saved.plan.context.tokenAddress), operationId);
4623
- }
4624
- toJSON() {
4625
- return { walletSession: true, ownerAddress: this.scope.ownerAddress };
4626
- }
4627
- [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
4628
- return this.toJSON();
4629
- }
4630
- };
4631
-
4632
- // src/session/codec.ts
4633
- function encode(value) {
4634
- if (typeof value === "bigint") return ["bigint", value.toString()];
4635
- if (value instanceof Uint8Array) return ["bytes", Array.from(value)];
4636
- if (Array.isArray(value)) return ["array", value.map(encode)];
4637
- if (value && typeof value === "object")
4638
- return ["object", Object.entries(value).map(([key, item]) => [key, encode(item)])];
4639
- if (value === void 0) return ["undefined"];
4640
- if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value))
4641
- return ["value", value];
4642
- throw new Error("unsupported wallet record value");
4643
- }
4644
- function decode(node) {
4645
- if (!Array.isArray(node)) throw new Error("invalid wallet record encoding");
4646
- const [type, value] = node;
4647
- if (type === "bigint" && typeof value === "string" && /^-?(0|[1-9][0-9]*)$/.test(value))
4648
- return BigInt(value);
4649
- if (type === "bytes" && Array.isArray(value) && value.every((n) => Number.isInteger(n) && n >= 0 && n < 256))
4650
- return Uint8Array.from(value);
4651
- if (type === "array" && Array.isArray(value)) return value.map(decode);
4652
- if (type === "object" && Array.isArray(value))
4653
- return Object.fromEntries(
4654
- value.map((pair) => {
4655
- if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string")
4656
- throw new Error("invalid object record");
4657
- return [pair[0], decode(pair[1])];
4658
- })
3483
+ verifyBroadcasterQuote(
3484
+ signed,
3485
+ { ...context, signer: this.configuration.quoteSigner },
3486
+ data
3487
+ );
3488
+ if (getAddress2(signed.quote.feeRecipient) !== this.configuration.feeRecipient)
3489
+ throw new Error("unregistered fee recipient");
3490
+ return { quote: signed.quote, signature: signed.signature, context: { ...context } };
3491
+ }
3492
+ async submit(data, signed, idempotencyKey) {
3493
+ if (!signed.context) throw new Error("missing verified quote context");
3494
+ verifyBroadcasterQuote(
3495
+ signed,
3496
+ { ...signed.context, signer: this.configuration.quoteSigner },
3497
+ data,
3498
+ 0n
3499
+ );
3500
+ if (getAddress2(signed.quote.feeRecipient) !== this.configuration.feeRecipient)
3501
+ throw new Error("unregistered fee recipient");
3502
+ return checkedStatus(
3503
+ await this.call("broadcaster_submitTransaction", {
3504
+ data,
3505
+ quote: signed.quote,
3506
+ signature: signed.signature,
3507
+ idempotencyKey
3508
+ }),
3509
+ idempotencyKey
4659
3510
  );
4660
- if (type === "undefined" && node.length === 1) return void 0;
4661
- if (type === "value" && (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)))
4662
- return value;
4663
- throw new Error("invalid wallet record node");
4664
- }
4665
- function serializeState(state) {
4666
- return new TextEncoder().encode(JSON.stringify(encode(state)));
4667
- }
4668
- function deserializeState(data) {
4669
- return decode(
4670
- JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(data))
4671
- );
4672
- }
4673
- var MemoryRecordCodec = class {
4674
- async encode(state) {
4675
- return serializeState(state);
4676
3511
  }
4677
- async decode(data) {
4678
- return deserializeState(data);
3512
+ async status(id) {
3513
+ return checkedStatus(await this.call("broadcaster_getTransactionStatus", { id }), id);
4679
3514
  }
4680
3515
  };
4681
-
4682
- // src/session/index.ts
4683
- var MemoryWalletStore = class extends TransactionalWalletStore {
4684
- constructor() {
4685
- super(new MemoryAtomicRecords(), new MemoryRecordCodec(), { persistent: false });
3516
+ var BroadcasterRpcError = class extends Error {
3517
+ constructor(code, message) {
3518
+ super(message);
3519
+ this.code = code;
3520
+ this.name = "BroadcasterRpcError";
4686
3521
  }
4687
3522
  };
4688
3523
 
4689
3524
  export {
3525
+ creditReservationKey,
3526
+ inputFacts,
3527
+ operationSources,
4690
3528
  copyGasBenchmarks,
4691
3529
  defineGasBenchmarks,
3530
+ estimateShapeGas,
3531
+ estimateBenchmarkGas,
4692
3532
  estimateGasReference,
3533
+ AddressRegistry,
3534
+ TransactionOutput,
3535
+ publicProof,
4693
3536
  sameStatement,
3537
+ encodeOperation,
3538
+ witnessFor,
3539
+ fundingTypedData,
3540
+ authorizeOperation,
4694
3541
  proveOperation,
3542
+ signOperationFunding,
4695
3543
  walletSigningRequest,
4696
3544
  acceptWalletSignature,
4697
3545
  fundingSigningRequest,
4698
3546
  acceptFundingSignature,
4699
3547
  operationPreview,
4700
- serializeState,
4701
- deserializeState,
3548
+ PreparedTransaction,
3549
+ configureTransactionInfrastructure,
3550
+ validateTransactionInfrastructure,
3551
+ Invisi,
3552
+ decodeProtocolEvent,
4702
3553
  readProtocolLogs,
3554
+ extractDeliveryCandidates,
4703
3555
  decryptCandidates,
4704
- DisclosureVerifier,
4705
- TransactionalWalletStore,
4706
- MemoryAtomicRecords,
4707
- Operation,
4708
- WalletSession,
4709
- MemoryWalletStore
3556
+ broadcasterQuoteTypes,
3557
+ broadcasterQuoteDomain,
3558
+ broadcasterTransactInterface,
3559
+ decodeBroadcasterIntent,
3560
+ broadcasterRequestHash,
3561
+ verifyBroadcasterQuote,
3562
+ BroadcasterClient,
3563
+ BroadcasterRpcError
4710
3564
  };
4711
- //# sourceMappingURL=chunk-4SD3MW6N.js.map
3565
+ //# sourceMappingURL=chunk-EHP5TBC6.js.map