@invisi-labs/sdk 1.1.5 → 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.
- package/dist/broadcaster-preparation-DMxxZ_1Q.d.cts +587 -0
- package/dist/broadcaster-preparation-DMxxZ_1Q.d.ts +587 -0
- package/dist/broadcaster-preparation-RMWQFYNL.js +11 -0
- package/dist/broadcaster-preparation-UGOTO4SX.cjs +11 -0
- package/dist/broadcaster-preparation-UGOTO4SX.cjs.map +1 -0
- package/dist/chunk-7F7ZZW3L.cjs +374 -0
- package/dist/chunk-7F7ZZW3L.cjs.map +1 -0
- package/dist/chunk-EHP5TBC6.js +3565 -0
- package/dist/chunk-EHP5TBC6.js.map +1 -0
- package/dist/chunk-GTPUYPQL.js +128 -0
- package/dist/chunk-GTPUYPQL.js.map +1 -0
- package/dist/chunk-ITTGBIG2.js +1877 -0
- package/dist/chunk-ITTGBIG2.js.map +1 -0
- package/dist/chunk-J2JH6HIG.cjs +1877 -0
- package/dist/chunk-J2JH6HIG.cjs.map +1 -0
- package/dist/chunk-LOU7AP2B.cjs +128 -0
- package/dist/chunk-LOU7AP2B.cjs.map +1 -0
- package/dist/chunk-RHNIHI4P.cjs +3565 -0
- package/dist/chunk-RHNIHI4P.cjs.map +1 -0
- package/dist/{chunk-4HMM7ZKY.js → chunk-XHKQ2BO2.js} +19 -19
- package/dist/chunk-XHKQ2BO2.js.map +1 -0
- package/dist/ethers.cjs +11 -385
- package/dist/ethers.cjs.map +1 -1
- package/dist/ethers.d.cts +4 -5
- package/dist/ethers.d.ts +4 -5
- package/dist/ethers.js +1 -1
- package/dist/index.cjs +441 -6217
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +109 -491
- package/dist/index.d.ts +109 -491
- package/dist/index.js +518 -599
- package/dist/index.js.map +1 -1
- package/dist/receipt-logs-CvC951p7.d.cts +318 -0
- package/dist/receipt-logs-m0-bhaxW.d.ts +318 -0
- package/dist/session-support.cjs +168 -0
- package/dist/session-support.cjs.map +1 -0
- package/dist/session-support.d.cts +252 -0
- package/dist/session-support.d.ts +252 -0
- package/dist/session-support.js +168 -0
- package/dist/session-support.js.map +1 -0
- package/package.json +10 -20
- package/dist/chunk-4HMM7ZKY.js.map +0 -1
- package/dist/chunk-BE52B32Z.js +0 -5490
- package/dist/chunk-BE52B32Z.js.map +0 -1
- package/dist/indexeddb.cjs +0 -5636
- package/dist/indexeddb.cjs.map +0 -1
- package/dist/indexeddb.d.cts +0 -20
- package/dist/indexeddb.d.ts +0 -20
- package/dist/indexeddb.js +0 -70
- package/dist/indexeddb.js.map +0 -1
- package/dist/memory.cjs +0 -5644
- package/dist/memory.cjs.map +0 -1
- package/dist/memory.d.cts +0 -12
- package/dist/memory.d.ts +0 -12
- package/dist/memory.js +0 -10
- package/dist/operation-types-D7I405FX.d.ts +0 -275
- package/dist/operation-types-DIazgS2d.d.cts +0 -275
- package/dist/sqlite.cjs +0 -5620
- package/dist/sqlite.cjs.map +0 -1
- package/dist/sqlite.d.cts +0 -28
- package/dist/sqlite.d.ts +0 -28
- package/dist/sqlite.js +0 -54
- package/dist/sqlite.js.map +0 -1
- package/dist/state-AUnz7TG7.d.cts +0 -78
- package/dist/state-AUnz7TG7.d.ts +0 -78
- package/dist/store-BMN8kd0c.d.ts +0 -42
- package/dist/store-Cz75STRl.d.cts +0 -42
- /package/dist/{memory.js.map → broadcaster-preparation-RMWQFYNL.js.map} +0 -0
|
@@ -0,0 +1,1877 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EthersAddressResolver
|
|
3
|
+
} from "./chunk-XHKQ2BO2.js";
|
|
4
|
+
|
|
5
|
+
// src/errors.ts
|
|
6
|
+
import { MissingCapabilityError } from "@invisi-labs/core";
|
|
7
|
+
var InvisiError = class extends Error {
|
|
8
|
+
code;
|
|
9
|
+
operation;
|
|
10
|
+
address;
|
|
11
|
+
outputIndex;
|
|
12
|
+
field;
|
|
13
|
+
capability;
|
|
14
|
+
constructor(code, message, details = {}) {
|
|
15
|
+
super(message, { cause: details.cause });
|
|
16
|
+
this.name = "InvisiError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.operation = details.operation;
|
|
19
|
+
this.address = details.address;
|
|
20
|
+
this.outputIndex = details.outputIndex;
|
|
21
|
+
this.field = details.field;
|
|
22
|
+
this.capability = details.capability;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
async function prepareWithErrors(operation, prepare) {
|
|
26
|
+
try {
|
|
27
|
+
return await prepare();
|
|
28
|
+
} catch (cause) {
|
|
29
|
+
if (cause instanceof InvisiError) {
|
|
30
|
+
if (cause.operation) throw cause;
|
|
31
|
+
throw new InvisiError(cause.code, cause.message, {
|
|
32
|
+
operation,
|
|
33
|
+
...cause.address === void 0 ? {} : { address: cause.address },
|
|
34
|
+
...cause.outputIndex === void 0 ? {} : { outputIndex: cause.outputIndex },
|
|
35
|
+
...cause.field === void 0 ? {} : { field: cause.field },
|
|
36
|
+
...cause.capability === void 0 ? {} : { capability: cause.capability },
|
|
37
|
+
cause: cause.cause
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
throw new InvisiError(
|
|
41
|
+
cause instanceof MissingCapabilityError ? "MISSING_CAPABILITY" : "PREPARATION_FAILED",
|
|
42
|
+
cause instanceof Error ? cause.message : "Transaction preparation failed",
|
|
43
|
+
{
|
|
44
|
+
operation,
|
|
45
|
+
cause,
|
|
46
|
+
...cause instanceof MissingCapabilityError ? { capability: cause.capability } : {}
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/payment-note/codec.ts
|
|
53
|
+
import { FIELD_MODULUS } from "@invisi-labs/core";
|
|
54
|
+
import { decodeBase64, encodeBase64, getAddress, getBytes, hexlify, concat } from "ethers";
|
|
55
|
+
function uint(value, bytes) {
|
|
56
|
+
if (typeof value !== "bigint" || value < 0n || value >= 1n << BigInt(bytes * 8))
|
|
57
|
+
throw new RangeError(`Invalid uint${bytes * 8} payment note field`);
|
|
58
|
+
return getBytes(`0x${value.toString(16).padStart(bytes * 2, "0")}`);
|
|
59
|
+
}
|
|
60
|
+
function validate(value) {
|
|
61
|
+
const keys = [
|
|
62
|
+
"chainId",
|
|
63
|
+
"protocolAddress",
|
|
64
|
+
"tokenAddress",
|
|
65
|
+
"amount",
|
|
66
|
+
"ownerAddress",
|
|
67
|
+
"nonce",
|
|
68
|
+
"commitment"
|
|
69
|
+
];
|
|
70
|
+
if (!value || typeof value !== "object" || Object.keys(value).some((key) => !keys.includes(key)))
|
|
71
|
+
throw new TypeError("Invalid payment note fields");
|
|
72
|
+
uint(value.chainId, 8);
|
|
73
|
+
uint(value.amount, 16);
|
|
74
|
+
if (value.chainId === 0n || value.amount === 0n)
|
|
75
|
+
throw new RangeError("Positive chainId and amount required");
|
|
76
|
+
for (const key of ["protocolAddress", "ownerAddress", "tokenAddress"]) {
|
|
77
|
+
getAddress(value[key]);
|
|
78
|
+
if (key !== "tokenAddress" && BigInt(value[key]) === 0n)
|
|
79
|
+
throw new RangeError(`Invalid ${key}`);
|
|
80
|
+
}
|
|
81
|
+
uint(value.nonce, 32);
|
|
82
|
+
if (value.nonce >= FIELD_MODULUS) throw new RangeError("Invalid payment note nonce");
|
|
83
|
+
if (!/^0x[\da-f]{64}$/i.test(value.commitment) || BigInt(value.commitment) <= 0n || BigInt(value.commitment) >= FIELD_MODULUS)
|
|
84
|
+
throw new RangeError("Invalid payment note commitment");
|
|
85
|
+
}
|
|
86
|
+
function encodePaymentNote(value) {
|
|
87
|
+
validate(value);
|
|
88
|
+
const bytes = concat([
|
|
89
|
+
new Uint8Array([1]),
|
|
90
|
+
uint(value.chainId, 8),
|
|
91
|
+
getBytes(value.protocolAddress),
|
|
92
|
+
getBytes(value.tokenAddress),
|
|
93
|
+
uint(value.amount, 16),
|
|
94
|
+
getBytes(value.ownerAddress),
|
|
95
|
+
uint(value.nonce, 32),
|
|
96
|
+
getBytes(value.commitment)
|
|
97
|
+
]);
|
|
98
|
+
return `ipn:${encodeBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
|
|
99
|
+
}
|
|
100
|
+
function decodePaymentNote(encoded) {
|
|
101
|
+
if (typeof encoded !== "string" || encoded.length !== 203 || !/^ipn:[A-Za-z0-9_-]+$/.test(encoded))
|
|
102
|
+
throw new TypeError("Invalid payment note encoding");
|
|
103
|
+
const bytes = decodeBase64(encoded.slice(4).replace(/-/g, "+").replace(/_/g, "/") + "=");
|
|
104
|
+
if (bytes.length !== 149 || bytes[0] !== 1)
|
|
105
|
+
throw new Error("Unsupported payment note version or length");
|
|
106
|
+
let offset = 1;
|
|
107
|
+
const take = (size) => {
|
|
108
|
+
const value2 = hexlify(bytes.slice(offset, offset += size));
|
|
109
|
+
return value2;
|
|
110
|
+
};
|
|
111
|
+
const value = {
|
|
112
|
+
chainId: BigInt(take(8)),
|
|
113
|
+
protocolAddress: getAddress(take(20)),
|
|
114
|
+
tokenAddress: getAddress(take(20)),
|
|
115
|
+
amount: BigInt(take(16)),
|
|
116
|
+
ownerAddress: getAddress(take(20)).toLowerCase(),
|
|
117
|
+
nonce: BigInt(take(32)),
|
|
118
|
+
commitment: take(32)
|
|
119
|
+
};
|
|
120
|
+
if (encodePaymentNote(value) !== encoded) throw new Error("Noncanonical payment note encoding");
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/transactions/signing/statement.ts
|
|
125
|
+
import { creditPeriods } from "@invisi-labs/core";
|
|
126
|
+
import {
|
|
127
|
+
WitnessBuilder,
|
|
128
|
+
cancelCreditPublicInputs,
|
|
129
|
+
createCreditPublicInputs,
|
|
130
|
+
creditDrawPublicInputs,
|
|
131
|
+
topUpCreditPublicInputs,
|
|
132
|
+
transactUserPublicInputs
|
|
133
|
+
} from "@invisi-labs/protocol";
|
|
134
|
+
function requiresAuthorization(plan) {
|
|
135
|
+
const ownerOnlyFullWithdrawal = !plan.credit && plan.inputs.length > 0 && plan.outputs.length === 0 && plan.params !== void 0 && plan.params.outputCommitments.length === 0 && (plan.params.feeAmount ?? 0n) === 0n && BigInt(plan.params.feeRecipient ?? 0n) === 0n && (plan.funding.amount ?? 0n) === 0n && plan.fundingMode === "none" && BigInt(plan.params.withdrawalAddress) === BigInt(plan.scope.ownerAddress) && plan.params.withdrawalAmount === plan.inputs.reduce((sum, input) => sum + input.note.amount, 0n) && plan.inputs.every((input) => input.note.ownerAddress === plan.scope.ownerAddress);
|
|
136
|
+
if (ownerOnlyFullWithdrawal) return false;
|
|
137
|
+
return !!plan.credit || plan.inputs.length > 0;
|
|
138
|
+
}
|
|
139
|
+
function statement(plan) {
|
|
140
|
+
if (!plan.statement) throw new Error("operation has no proof statement");
|
|
141
|
+
const credit = plan.credit;
|
|
142
|
+
let words;
|
|
143
|
+
if (credit?.topUp) {
|
|
144
|
+
words = topUpCreditPublicInputs(
|
|
145
|
+
plan.context,
|
|
146
|
+
credit.topUp,
|
|
147
|
+
credit.deliveries,
|
|
148
|
+
plan.funding
|
|
149
|
+
);
|
|
150
|
+
} else if (credit?.creation) {
|
|
151
|
+
words = createCreditPublicInputs(
|
|
152
|
+
plan.context,
|
|
153
|
+
credit.creation,
|
|
154
|
+
credit.deliveries,
|
|
155
|
+
plan.funding
|
|
156
|
+
);
|
|
157
|
+
} else if (credit?.draw) {
|
|
158
|
+
words = creditDrawPublicInputs(plan.context, credit.draw, credit.deliveries);
|
|
159
|
+
} else if (credit?.cancel) {
|
|
160
|
+
words = cancelCreditPublicInputs(plan.context, credit.cancel);
|
|
161
|
+
} else {
|
|
162
|
+
words = transactUserPublicInputs(
|
|
163
|
+
plan.context,
|
|
164
|
+
plan.params,
|
|
165
|
+
plan.outputs.map((output) => output.delivery),
|
|
166
|
+
plan.funding
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const result = WitnessBuilder.statement(
|
|
170
|
+
plan.statement.circuitName,
|
|
171
|
+
words,
|
|
172
|
+
plan.statement.target
|
|
173
|
+
);
|
|
174
|
+
if (JSON.stringify(result) !== JSON.stringify(plan.statement)) {
|
|
175
|
+
throw new Error("frozen operation statement changed");
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
function authorizationRequest(plan) {
|
|
180
|
+
let operation = "transact";
|
|
181
|
+
if (plan.kind === "draw-credit") {
|
|
182
|
+
operation = "credit-draw";
|
|
183
|
+
} else if (plan.kind === "create-credit" || plan.kind === "top-up-credit" || plan.kind === "cancel-credit") {
|
|
184
|
+
operation = plan.kind;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
context: {
|
|
188
|
+
chainId: plan.context.chainId,
|
|
189
|
+
protocolAddress: BigInt(plan.context.protocolAddress)
|
|
190
|
+
},
|
|
191
|
+
operation,
|
|
192
|
+
capacity: plan.capacity,
|
|
193
|
+
words: statement(plan).expectedPublicInputs.slice(0, -2).map(BigInt)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function operationExpired(plan, now) {
|
|
197
|
+
if (now > plan.deadline) return true;
|
|
198
|
+
const credit = plan.credit;
|
|
199
|
+
if (credit?.topUp && now >= credit.topUp.previousExpiry) return true;
|
|
200
|
+
if (credit?.creation && now >= credit.creation.expiry) return true;
|
|
201
|
+
if (credit?.draw) {
|
|
202
|
+
if (credit.draw.expiries.some((expiry) => now >= expiry)) return true;
|
|
203
|
+
const prepared = creditPeriods(plan.preparedAt.timestamp), current = creditPeriods(now);
|
|
204
|
+
if (prepared.currentDay !== current.currentDay || prepared.currentMonth !== current.currentMonth)
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/broadcaster/intent.ts
|
|
211
|
+
import { normalizeFunding } from "@invisi-labs/protocol";
|
|
212
|
+
import { invisiProtocolAbi } from "@invisi-labs/protocol/contracts";
|
|
213
|
+
import { Interface } from "ethers";
|
|
214
|
+
function exportBroadcasterIntent(plan) {
|
|
215
|
+
if (plan.kind === "draw-credit" || plan.statement && plan.statement.target !== "evm")
|
|
216
|
+
throw new Error("unsupported broadcaster operation");
|
|
217
|
+
if (plan.statement) statement(plan);
|
|
218
|
+
const feeDefaults = (params) => ({
|
|
219
|
+
...params,
|
|
220
|
+
feeRecipient: params.feeRecipient ?? "0x0000000000000000000000000000000000000000",
|
|
221
|
+
feeAmount: params.feeAmount ?? 0n
|
|
222
|
+
});
|
|
223
|
+
const funding = { ...normalizeFunding(plan.funding), signature: "0x" };
|
|
224
|
+
let method;
|
|
225
|
+
let args;
|
|
226
|
+
if (plan.params) {
|
|
227
|
+
method = "transact";
|
|
228
|
+
args = [
|
|
229
|
+
"0x",
|
|
230
|
+
{
|
|
231
|
+
params: feeDefaults(plan.params),
|
|
232
|
+
deliveries: plan.outputs.map((o) => o.delivery),
|
|
233
|
+
funding
|
|
234
|
+
}
|
|
235
|
+
];
|
|
236
|
+
} else if (plan.shieldParams) {
|
|
237
|
+
method = "shield";
|
|
238
|
+
args = [
|
|
239
|
+
feeDefaults(plan.shieldParams),
|
|
240
|
+
plan.shieldOutputs,
|
|
241
|
+
plan.outputs.map((o) => o.delivery),
|
|
242
|
+
funding
|
|
243
|
+
];
|
|
244
|
+
} else if (plan.credit?.topUp) {
|
|
245
|
+
method = "topUpCredit";
|
|
246
|
+
args = [
|
|
247
|
+
"0x",
|
|
248
|
+
{ ...plan.credit.topUp, creation: feeDefaults(plan.credit.topUp.creation) },
|
|
249
|
+
plan.credit.deliveries,
|
|
250
|
+
funding
|
|
251
|
+
];
|
|
252
|
+
} else if (plan.credit?.creation) {
|
|
253
|
+
method = "createCredit";
|
|
254
|
+
args = ["0x", feeDefaults(plan.credit.creation), plan.credit.deliveries, funding];
|
|
255
|
+
} else if (plan.credit?.cancel) {
|
|
256
|
+
method = "cancelCredit";
|
|
257
|
+
args = ["0x", feeDefaults(plan.credit.cancel)];
|
|
258
|
+
} else throw new Error("unsupported broadcaster operation");
|
|
259
|
+
const abi = invisiProtocolAbi.filter(
|
|
260
|
+
(item) => item.type === "function" && item.name === method && (method !== "transact" || item.inputs[1]?.type === "tuple")
|
|
261
|
+
);
|
|
262
|
+
return {
|
|
263
|
+
sendable: false,
|
|
264
|
+
data: new Interface(abi).encodeFunctionData(method, args)
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/payment-request/codec.ts
|
|
269
|
+
import { ReceivingAddress as ReceivingAddress2 } from "@invisi-labs/core";
|
|
270
|
+
import { decodeBase64 as decodeBase642, encodeBase64 as encodeBase642, getBytes as getBytes2, hexlify as hexlify2 } from "ethers";
|
|
271
|
+
|
|
272
|
+
// src/payment-request/binary.ts
|
|
273
|
+
function uint2(value, size, name) {
|
|
274
|
+
if (typeof value !== "bigint" || value < 0n || value >= 1n << BigInt(size * 8))
|
|
275
|
+
throw new RangeError(`${name} must be uint${size * 8}`);
|
|
276
|
+
const result = new Uint8Array(size);
|
|
277
|
+
for (let i = size - 1; i >= 0; i--) {
|
|
278
|
+
result[i] = Number(value & 255n);
|
|
279
|
+
value >>= 8n;
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
function concat2(parts) {
|
|
284
|
+
const result = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
285
|
+
let offset = 0;
|
|
286
|
+
for (const part of parts) {
|
|
287
|
+
result.set(part, offset);
|
|
288
|
+
offset += part.length;
|
|
289
|
+
}
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
var Reader = class {
|
|
293
|
+
constructor(data) {
|
|
294
|
+
this.data = data;
|
|
295
|
+
}
|
|
296
|
+
#offset = 0;
|
|
297
|
+
take(size) {
|
|
298
|
+
if (this.#offset + size > this.data.length) throw new Error("Truncated payment request");
|
|
299
|
+
const result = this.data.slice(this.#offset, this.#offset + size);
|
|
300
|
+
this.#offset += size;
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
integer(size) {
|
|
304
|
+
return this.take(size).reduce((n, b) => n << 8n | BigInt(b), 0n);
|
|
305
|
+
}
|
|
306
|
+
done() {
|
|
307
|
+
if (this.#offset !== this.data.length) throw new Error("Trailing payment request bytes");
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// src/payment-request/validation.ts
|
|
312
|
+
import { FIELD_MODULUS as FIELD_MODULUS2, MAX_CIPHERTEXT_BYTES, ReceivingAddress } from "@invisi-labs/core";
|
|
313
|
+
import { getAddress as getAddress2 } from "ethers";
|
|
314
|
+
var MAX_REQUEST_PAYLOAD_BYTES = MAX_CIPHERTEXT_BYTES - 160;
|
|
315
|
+
function fields(value, allowed, name) {
|
|
316
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
317
|
+
throw new TypeError(`Invalid ${name}`);
|
|
318
|
+
for (const key of Object.keys(value))
|
|
319
|
+
if (!allowed.includes(key)) throw new TypeError(`Unsupported ${name} field ${key}`);
|
|
320
|
+
}
|
|
321
|
+
function nonce(value) {
|
|
322
|
+
uint2(value, 32, "nonce");
|
|
323
|
+
if (value >= FIELD_MODULUS2)
|
|
324
|
+
throw new RangeError("nonce must be a canonical BN254 field element");
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
function validatePaymentRequest(request) {
|
|
328
|
+
fields(
|
|
329
|
+
request,
|
|
330
|
+
["chainId", "protocolAddress", "tokenAddress", "amount", "recipient", "payload"],
|
|
331
|
+
"payment request"
|
|
332
|
+
);
|
|
333
|
+
uint2(request.chainId, 8, "chainId");
|
|
334
|
+
if (request.chainId === 0n) throw new RangeError("chainId must be positive");
|
|
335
|
+
const protocol = getAddress2(request.protocolAddress);
|
|
336
|
+
getAddress2(request.tokenAddress);
|
|
337
|
+
if (BigInt(protocol) === 0n) throw new RangeError("protocol address must be nonzero");
|
|
338
|
+
uint2(request.amount, 16, "amount");
|
|
339
|
+
if (request.amount === 0n) throw new RangeError("amount must be positive");
|
|
340
|
+
const recipient = request.recipient;
|
|
341
|
+
fields(
|
|
342
|
+
recipient,
|
|
343
|
+
["type", "address", "nonce", "identity", "envelope", "addressLookup"],
|
|
344
|
+
"recipient"
|
|
345
|
+
);
|
|
346
|
+
if (recipient.type === "identity") {
|
|
347
|
+
fields(recipient, ["type", "identity"], "identity recipient");
|
|
348
|
+
if (typeof recipient.identity !== "string" || !/^0x[\da-f]{64}$/i.test(recipient.identity) || BigInt(recipient.identity) <= 0n || BigInt(recipient.identity) >= FIELD_MODULUS2)
|
|
349
|
+
throw new RangeError("identity must be a nonzero canonical BN254 field element");
|
|
350
|
+
if (request.payload !== void 0)
|
|
351
|
+
throw new TypeError("payload requires an encrypted envelope");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (recipient.type !== "evm" && recipient.type !== "invisi")
|
|
355
|
+
throw new TypeError("Invalid recipient type");
|
|
356
|
+
fields(
|
|
357
|
+
recipient,
|
|
358
|
+
recipient.type === "evm" ? ["type", "address", "nonce", "envelope", "addressLookup"] : ["type", "address", "nonce"],
|
|
359
|
+
"recipient"
|
|
360
|
+
);
|
|
361
|
+
const parsed = ReceivingAddress.parse(recipient.address);
|
|
362
|
+
if (recipient.type === "evm" !== (parsed.kind === "short"))
|
|
363
|
+
throw new TypeError("recipient address type mismatch");
|
|
364
|
+
if (BigInt(parsed.ownerAddress) === 0n) throw new RangeError("owner address must be nonzero");
|
|
365
|
+
if (recipient.nonce !== void 0) nonce(recipient.nonce);
|
|
366
|
+
if (recipient.type === "evm") {
|
|
367
|
+
if (recipient.envelope !== void 0 && typeof recipient.envelope !== "boolean")
|
|
368
|
+
throw new TypeError("evm envelope must be boolean");
|
|
369
|
+
if (recipient.addressLookup !== void 0 && !["chain", "protocol-rpc"].includes(recipient.addressLookup))
|
|
370
|
+
throw new TypeError("Invalid recipient addressLookup");
|
|
371
|
+
if (recipient.envelope === false && request.payload !== void 0)
|
|
372
|
+
throw new TypeError("payload requires an encrypted envelope");
|
|
373
|
+
}
|
|
374
|
+
if (request.payload !== void 0 && !(request.payload instanceof Uint8Array))
|
|
375
|
+
throw new TypeError("payload must be Uint8Array");
|
|
376
|
+
if ((request.payload?.length ?? 0) > MAX_REQUEST_PAYLOAD_BYTES)
|
|
377
|
+
throw new RangeError("payload exceeds funds envelope limit");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/payment-request/codec.ts
|
|
381
|
+
var PREFIX = "ipr:";
|
|
382
|
+
var VERSION = 1;
|
|
383
|
+
var MAX_BINARY_BYTES = 186 + MAX_REQUEST_PAYLOAD_BYTES;
|
|
384
|
+
function base64url(data) {
|
|
385
|
+
return encodeBase642(data).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
386
|
+
}
|
|
387
|
+
function encodePaymentRequest(request) {
|
|
388
|
+
validatePaymentRequest(request);
|
|
389
|
+
const recipient = request.recipient;
|
|
390
|
+
const parts = [
|
|
391
|
+
new Uint8Array([
|
|
392
|
+
VERSION,
|
|
393
|
+
recipient.type === "evm" ? 1 : recipient.type === "invisi" ? 2 : 3
|
|
394
|
+
]),
|
|
395
|
+
uint2(request.chainId, 8, "chainId"),
|
|
396
|
+
getBytes2(request.protocolAddress),
|
|
397
|
+
getBytes2(request.tokenAddress),
|
|
398
|
+
uint2(request.amount, 16, "amount")
|
|
399
|
+
];
|
|
400
|
+
if (recipient.type === "identity") parts.push(getBytes2(recipient.identity));
|
|
401
|
+
else {
|
|
402
|
+
const parsed = ReceivingAddress2.parse(recipient.address);
|
|
403
|
+
parts.push(getBytes2(parsed.ownerAddress));
|
|
404
|
+
if (recipient.type === "invisi") {
|
|
405
|
+
const keys = parsed.getReceivingKeys();
|
|
406
|
+
parts.push(keys.viewingPublicKey, keys.clueKey);
|
|
407
|
+
} else parts.push(new Uint8Array([recipient.envelope === false ? 0 : 1]));
|
|
408
|
+
parts.push(new Uint8Array([recipient.nonce === void 0 ? 0 : 1]));
|
|
409
|
+
if (recipient.nonce !== void 0) parts.push(uint2(recipient.nonce, 32, "nonce"));
|
|
410
|
+
if (recipient.type === "invisi" || recipient.envelope !== false) {
|
|
411
|
+
const payload = request.payload ?? new Uint8Array();
|
|
412
|
+
parts.push(uint2(BigInt(payload.length), 2, "payloadLength"), payload);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return PREFIX + base64url(concat2(parts));
|
|
416
|
+
}
|
|
417
|
+
function decodePaymentRequest(encoded) {
|
|
418
|
+
if (typeof encoded !== "string" || !encoded.startsWith(PREFIX) || encoded.length > 4 + Math.ceil(MAX_BINARY_BYTES * 8 / 6))
|
|
419
|
+
throw new Error("Invalid payment request encoding");
|
|
420
|
+
const text = encoded.slice(PREFIX.length);
|
|
421
|
+
if (!/^[A-Za-z0-9_-]+$/.test(text) || text.length % 4 === 1)
|
|
422
|
+
throw new Error("Invalid payment request Base64URL");
|
|
423
|
+
const data = decodeBase642(
|
|
424
|
+
text.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - text.length % 4) % 4)
|
|
425
|
+
);
|
|
426
|
+
if (base64url(data) !== text) throw new Error("Noncanonical payment request Base64URL");
|
|
427
|
+
const reader = new Reader(data);
|
|
428
|
+
if (reader.integer(1) !== BigInt(VERSION))
|
|
429
|
+
throw new Error("Unsupported payment request version");
|
|
430
|
+
const type = reader.integer(1);
|
|
431
|
+
if (type < 1n || type > 3n) throw new Error("Unsupported payment request recipient type");
|
|
432
|
+
const context = {
|
|
433
|
+
chainId: reader.integer(8),
|
|
434
|
+
protocolAddress: hexlify2(reader.take(20)),
|
|
435
|
+
tokenAddress: hexlify2(reader.take(20)),
|
|
436
|
+
amount: reader.integer(16)
|
|
437
|
+
};
|
|
438
|
+
let recipient;
|
|
439
|
+
let payload;
|
|
440
|
+
if (type === 3n)
|
|
441
|
+
recipient = { type: "identity", identity: hexlify2(reader.take(32)) };
|
|
442
|
+
else {
|
|
443
|
+
const ownerAddress = hexlify2(reader.take(20));
|
|
444
|
+
let address;
|
|
445
|
+
let envelope = true;
|
|
446
|
+
if (type === 2n)
|
|
447
|
+
address = ReceivingAddress2.from({
|
|
448
|
+
ownerAddress,
|
|
449
|
+
viewingPublicKey: reader.take(33),
|
|
450
|
+
clueKey: reader.take(32)
|
|
451
|
+
}).toString();
|
|
452
|
+
else {
|
|
453
|
+
address = ReceivingAddress2.fromIdentity(ownerAddress).toString();
|
|
454
|
+
const value = reader.integer(1);
|
|
455
|
+
if (value > 1n) throw new Error("Invalid envelope flag");
|
|
456
|
+
envelope = value === 1n;
|
|
457
|
+
}
|
|
458
|
+
const present = reader.integer(1);
|
|
459
|
+
if (present > 1n) throw new Error("Invalid nonce presence flag");
|
|
460
|
+
const nonce2 = present === 1n ? reader.integer(32) : void 0;
|
|
461
|
+
recipient = type === 2n ? { type: "invisi", address, ...nonce2 === void 0 ? {} : { nonce: nonce2 } } : { type: "evm", address, envelope, ...nonce2 === void 0 ? {} : { nonce: nonce2 } };
|
|
462
|
+
if (envelope) {
|
|
463
|
+
const length = Number(reader.integer(2));
|
|
464
|
+
if (length > MAX_REQUEST_PAYLOAD_BYTES)
|
|
465
|
+
throw new RangeError("payload exceeds funds envelope limit");
|
|
466
|
+
if (length !== 0) payload = reader.take(length);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
reader.done();
|
|
470
|
+
const request = {
|
|
471
|
+
...context,
|
|
472
|
+
recipient,
|
|
473
|
+
...payload === void 0 ? {} : { payload }
|
|
474
|
+
};
|
|
475
|
+
validatePaymentRequest(request);
|
|
476
|
+
if (encodePaymentRequest(request) !== encoded) throw new Error("Noncanonical payment request");
|
|
477
|
+
return request;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/payment-note/export.ts
|
|
481
|
+
function exportPaymentNote(output) {
|
|
482
|
+
const note = output.note;
|
|
483
|
+
if (output.recipient.type === "identity" || !note)
|
|
484
|
+
throw new Error("Identity outputs cannot export an IPN");
|
|
485
|
+
return encodePaymentNote({
|
|
486
|
+
chainId: note.chainId,
|
|
487
|
+
protocolAddress: output.context.protocolAddress,
|
|
488
|
+
tokenAddress: output.context.tokenAddress,
|
|
489
|
+
amount: note.amount,
|
|
490
|
+
ownerAddress: note.ownerAddress,
|
|
491
|
+
nonce: note.nonce,
|
|
492
|
+
commitment: output.commitment
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/notes/NoteOutput.ts
|
|
497
|
+
import {
|
|
498
|
+
FIELD_MODULUS as FIELD_MODULUS3,
|
|
499
|
+
ReceivingAddress as ReceivingAddress5,
|
|
500
|
+
computeNoteCommitment,
|
|
501
|
+
computeNoteIdentity,
|
|
502
|
+
createClue,
|
|
503
|
+
encryptPrivateTransferNote,
|
|
504
|
+
generateNonce
|
|
505
|
+
} from "@invisi-labs/core";
|
|
506
|
+
import { AbiCoder, getAddress as getAddress3, hexlify as hexlify4, keccak256 } from "ethers";
|
|
507
|
+
|
|
508
|
+
// src/notes/resolve-recipient.ts
|
|
509
|
+
import {
|
|
510
|
+
MissingCapabilityError as MissingCapabilityError2,
|
|
511
|
+
ReceivingAddress as ReceivingAddress3
|
|
512
|
+
} from "@invisi-labs/core";
|
|
513
|
+
async function resolveRecipient(address, lookup, details = {}) {
|
|
514
|
+
const context = { ...details, address };
|
|
515
|
+
let recipient;
|
|
516
|
+
try {
|
|
517
|
+
recipient = ReceivingAddress3.parse(address);
|
|
518
|
+
} catch (cause) {
|
|
519
|
+
throw new InvisiError("INVALID_RECIPIENT", "Invalid recipient address", {
|
|
520
|
+
...context,
|
|
521
|
+
cause
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
if (recipient.kind === "long") return recipient;
|
|
525
|
+
if (!lookup) {
|
|
526
|
+
throw new InvisiError("MISSING_CAPABILITY", "Missing registry lookup capability", {
|
|
527
|
+
...context,
|
|
528
|
+
capability: "registry lookup"
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
let keys;
|
|
532
|
+
try {
|
|
533
|
+
keys = await lookup(recipient.ownerAddress);
|
|
534
|
+
} catch (cause) {
|
|
535
|
+
if (cause instanceof MissingCapabilityError2) {
|
|
536
|
+
throw new InvisiError("MISSING_CAPABILITY", cause.message, {
|
|
537
|
+
...context,
|
|
538
|
+
cause,
|
|
539
|
+
capability: cause.capability
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
throw new InvisiError("RECIPIENT_LOOKUP_FAILED", "Recipient registry lookup failed", {
|
|
543
|
+
...context,
|
|
544
|
+
cause
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
if (keys == null) {
|
|
548
|
+
throw new InvisiError(
|
|
549
|
+
"EVM_ADDRESS_NOT_REGISTERED",
|
|
550
|
+
"EVM address has no registered receiving keys",
|
|
551
|
+
context
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
return ReceivingAddress3.from({
|
|
556
|
+
ownerAddress: recipient.ownerAddress,
|
|
557
|
+
viewingPublicKey: keys.viewingPublicKey,
|
|
558
|
+
clueKey: keys.clueKey
|
|
559
|
+
});
|
|
560
|
+
} catch (cause) {
|
|
561
|
+
throw new InvisiError(
|
|
562
|
+
"RECIPIENT_LOOKUP_FAILED",
|
|
563
|
+
"Registry returned invalid receiving keys",
|
|
564
|
+
{ ...context, cause }
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/payment-request/match-output.ts
|
|
570
|
+
import { ReceivingAddress as ReceivingAddress4 } from "@invisi-labs/core";
|
|
571
|
+
import { hexlify as hexlify3 } from "ethers";
|
|
572
|
+
function assertRequestMatchesOutput(ipp, output) {
|
|
573
|
+
const request = decodePaymentRequest(ipp);
|
|
574
|
+
const expected = request.recipient;
|
|
575
|
+
const actual = output.recipient;
|
|
576
|
+
if (request.chainId !== output.context.chainId || BigInt(request.protocolAddress) !== BigInt(output.context.protocolAddress) || BigInt(request.tokenAddress) !== BigInt(output.context.tokenAddress) || request.amount !== output.amount || expected.type !== actual.type)
|
|
577
|
+
throw new Error("IPR does not match output context or recipient");
|
|
578
|
+
if (expected.type === "identity") {
|
|
579
|
+
if (actual.type !== "identity" || BigInt(expected.identity) !== BigInt(actual.identity))
|
|
580
|
+
throw new Error("IPR identity mismatch");
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (actual.type === "identity") throw new Error("IPR recipient type mismatch");
|
|
584
|
+
const parsed = ReceivingAddress4.parse(expected.address);
|
|
585
|
+
const resolved = ReceivingAddress4.parse(actual.address);
|
|
586
|
+
if (parsed.ownerAddress !== resolved.ownerAddress || expected.nonce !== void 0 && expected.nonce !== output.note?.nonce || hexlify3(request.payload ?? new Uint8Array()) !== hexlify3(output.note?.payload ?? new Uint8Array()))
|
|
587
|
+
throw new Error("IPR does not match output opening");
|
|
588
|
+
if (expected.type === "evm" && actual.type === "evm" && expected.envelope !== false !== (actual.envelope !== false))
|
|
589
|
+
throw new Error("IPR envelope mismatch");
|
|
590
|
+
if (expected.type === "invisi") {
|
|
591
|
+
const a = parsed.getReceivingKeys(), b = resolved.getReceivingKeys();
|
|
592
|
+
if (hexlify3(a.viewingPublicKey) !== hexlify3(b.viewingPublicKey) || hexlify3(a.clueKey) !== hexlify3(b.clueKey))
|
|
593
|
+
throw new Error("IPR receiving keys mismatch");
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/payment-request/execution.ts
|
|
598
|
+
function applyExecutionOptions(request, execution) {
|
|
599
|
+
fields(execution, ["nonce", "addressLookup"], "execution options");
|
|
600
|
+
const copy = structuredClone(request);
|
|
601
|
+
const recipient = copy.recipient;
|
|
602
|
+
if (execution.nonce !== void 0) {
|
|
603
|
+
if (recipient.type === "identity")
|
|
604
|
+
throw new TypeError("identity recipient cannot specify nonce");
|
|
605
|
+
nonce(execution.nonce);
|
|
606
|
+
if (recipient.nonce !== void 0 && recipient.nonce !== execution.nonce)
|
|
607
|
+
throw new Error("nonce conflicts with the fixed payment request");
|
|
608
|
+
recipient.nonce = execution.nonce;
|
|
609
|
+
}
|
|
610
|
+
if (execution.addressLookup !== void 0) {
|
|
611
|
+
if (recipient.type !== "evm" || recipient.envelope === false)
|
|
612
|
+
throw new TypeError("addressLookup requires an encrypted EVM recipient");
|
|
613
|
+
if (!["chain", "protocol-rpc"].includes(execution.addressLookup))
|
|
614
|
+
throw new TypeError("Invalid addressLookup");
|
|
615
|
+
recipient.addressLookup = execution.addressLookup;
|
|
616
|
+
}
|
|
617
|
+
return copy;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/notes/NoteOutput.ts
|
|
621
|
+
function nonceValue(value) {
|
|
622
|
+
if (typeof value !== "bigint" || value < 0n || value >= FIELD_MODULUS3)
|
|
623
|
+
throw new RangeError("nonce must be a canonical BN254 field element");
|
|
624
|
+
return value;
|
|
625
|
+
}
|
|
626
|
+
function identityValue(value) {
|
|
627
|
+
if (typeof value !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(value) || BigInt(value) <= 0n || BigInt(value) >= FIELD_MODULUS3)
|
|
628
|
+
throw new RangeError("identity must be a nonzero canonical BN254 field element");
|
|
629
|
+
return value.toLowerCase();
|
|
630
|
+
}
|
|
631
|
+
function payloadCopy(value) {
|
|
632
|
+
if (value === void 0) return new Uint8Array();
|
|
633
|
+
if (!(value instanceof Uint8Array)) throw new TypeError("payload must be Uint8Array");
|
|
634
|
+
return Uint8Array.from(value);
|
|
635
|
+
}
|
|
636
|
+
function fingerprint(delivery) {
|
|
637
|
+
return keccak256(
|
|
638
|
+
AbiCoder.defaultAbiCoder().encode(["bytes", "bytes"], [delivery.ciphertext, delivery.clue])
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
function checkedContext(context) {
|
|
642
|
+
if (typeof context.chainId !== "bigint" || context.chainId <= 0n)
|
|
643
|
+
throw new RangeError("chainId must be positive");
|
|
644
|
+
const protocolAddress = getAddress3(context.protocolAddress);
|
|
645
|
+
const tokenAddress = getAddress3(context.tokenAddress);
|
|
646
|
+
if (BigInt(protocolAddress) === 0n) throw new RangeError("protocol address must be nonzero");
|
|
647
|
+
return { chainId: context.chainId, protocolAddress, tokenAddress };
|
|
648
|
+
}
|
|
649
|
+
function encrypted(recipient) {
|
|
650
|
+
return recipient.type === "invisi" || recipient.type === "evm" && recipient.envelope !== false;
|
|
651
|
+
}
|
|
652
|
+
function rejectLegacy(value) {
|
|
653
|
+
for (const key of [
|
|
654
|
+
"expectedCommitment",
|
|
655
|
+
"nonce",
|
|
656
|
+
"envelope",
|
|
657
|
+
"context",
|
|
658
|
+
"to",
|
|
659
|
+
"addressResolver",
|
|
660
|
+
"addressLookup"
|
|
661
|
+
])
|
|
662
|
+
if (key in value) throw new TypeError(`${key} is not a NoteOutput create option`);
|
|
663
|
+
}
|
|
664
|
+
var outputCreation = /* @__PURE__ */ Symbol("NoteOutput.create");
|
|
665
|
+
var NoteOutput = class _NoteOutput {
|
|
666
|
+
to;
|
|
667
|
+
ipp;
|
|
668
|
+
amount;
|
|
669
|
+
identity;
|
|
670
|
+
commitment;
|
|
671
|
+
deliveryFingerprint;
|
|
672
|
+
/** Encrypted EVM delivery is resolved by preparation; the original offline intent stays pending. */
|
|
673
|
+
deliveryPending;
|
|
674
|
+
constructor(output, token, ipp) {
|
|
675
|
+
if (token !== outputCreation)
|
|
676
|
+
throw new TypeError("outputs must be created with NoteOutput.create");
|
|
677
|
+
const saved = structuredClone(output);
|
|
678
|
+
this.ipp = ipp;
|
|
679
|
+
this.to = saved.address;
|
|
680
|
+
this.amount = saved.amount;
|
|
681
|
+
this.identity = saved.identity;
|
|
682
|
+
this.commitment = saved.commitment;
|
|
683
|
+
this.deliveryFingerprint = fingerprint(saved.delivery);
|
|
684
|
+
this.deliveryPending = saved.deliveryPending === true;
|
|
685
|
+
Object.defineProperties(this, {
|
|
686
|
+
context: { enumerable: true, get: () => structuredClone(saved.context) },
|
|
687
|
+
recipient: { enumerable: true, get: () => structuredClone(saved.recipient) },
|
|
688
|
+
nonce: { enumerable: true, get: () => saved.note?.nonce },
|
|
689
|
+
payload: { enumerable: true, get: () => payloadCopy(saved.note?.payload) },
|
|
690
|
+
preparedOutput: { enumerable: true, get: () => structuredClone(saved) }
|
|
691
|
+
});
|
|
692
|
+
Object.freeze(this);
|
|
693
|
+
}
|
|
694
|
+
/** Private offline delivery data; does not assert that the output has been mined. */
|
|
695
|
+
exportPaymentNote() {
|
|
696
|
+
return exportPaymentNote(this.preparedOutput);
|
|
697
|
+
}
|
|
698
|
+
static async create(input, execution = {}) {
|
|
699
|
+
if (typeof input !== "string") rejectLegacy(input);
|
|
700
|
+
const request = typeof input === "string" ? decodePaymentRequest(input) : structuredClone(input);
|
|
701
|
+
rejectLegacy(request);
|
|
702
|
+
const options = applyExecutionOptions(request, execution);
|
|
703
|
+
if (typeof options.amount !== "bigint" || options.amount <= 0n || options.amount >= 1n << 128n)
|
|
704
|
+
throw new RangeError("amount must be a positive uint128");
|
|
705
|
+
const context = checkedContext(options);
|
|
706
|
+
const recipient = structuredClone(options.recipient);
|
|
707
|
+
if (!recipient || !["evm", "invisi", "identity"].includes(recipient.type))
|
|
708
|
+
throw new TypeError("invalid recipient type");
|
|
709
|
+
if ("expectedCommitment" in recipient)
|
|
710
|
+
throw new TypeError("expectedCommitment is not supported");
|
|
711
|
+
const envelope = encrypted(recipient);
|
|
712
|
+
if (!envelope && options.payload !== void 0)
|
|
713
|
+
throw new TypeError("payload requires an encrypted envelope");
|
|
714
|
+
let identity;
|
|
715
|
+
let note;
|
|
716
|
+
let resolved;
|
|
717
|
+
let address;
|
|
718
|
+
if (recipient.type === "identity") {
|
|
719
|
+
if ("nonce" in recipient || "envelope" in recipient || "addressLookup" in recipient)
|
|
720
|
+
throw new TypeError("identity recipient cannot specify nonce or envelope");
|
|
721
|
+
identity = identityValue(recipient.identity);
|
|
722
|
+
address = identity;
|
|
723
|
+
} else {
|
|
724
|
+
if (recipient.type === "evm" && recipient.envelope !== void 0 && typeof recipient.envelope !== "boolean")
|
|
725
|
+
throw new TypeError("evm envelope must be boolean");
|
|
726
|
+
if (recipient.type === "invisi" && "envelope" in recipient)
|
|
727
|
+
throw new TypeError("invisi recipient is always encrypted");
|
|
728
|
+
let parsed;
|
|
729
|
+
try {
|
|
730
|
+
parsed = ReceivingAddress5.parse(recipient.address);
|
|
731
|
+
} catch (cause) {
|
|
732
|
+
throw new InvisiError("INVALID_RECIPIENT", "Invalid recipient address", {
|
|
733
|
+
address: recipient.address,
|
|
734
|
+
field: "to",
|
|
735
|
+
cause
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
if (recipient.type === "evm" !== (parsed.kind === "short"))
|
|
739
|
+
throw new TypeError("recipient address type mismatch");
|
|
740
|
+
resolved = parsed;
|
|
741
|
+
if (recipient.type === "evm") {
|
|
742
|
+
if (recipient.addressLookup !== void 0 && recipient.addressLookup !== "chain" && recipient.addressLookup !== "protocol-rpc")
|
|
743
|
+
throw new TypeError("invalid recipient addressLookup");
|
|
744
|
+
recipient.addressLookup ??= "protocol-rpc";
|
|
745
|
+
} else if ("addressLookup" in recipient)
|
|
746
|
+
throw new TypeError("addressLookup only applies to EVM recipients");
|
|
747
|
+
const nonce2 = nonceValue(
|
|
748
|
+
recipient.nonce === void 0 ? generateNonce() : recipient.nonce
|
|
749
|
+
);
|
|
750
|
+
recipient.nonce = nonce2;
|
|
751
|
+
identity = await computeNoteIdentity(BigInt(resolved.ownerAddress), nonce2);
|
|
752
|
+
address = recipient.address;
|
|
753
|
+
note = {
|
|
754
|
+
chainId: context.chainId,
|
|
755
|
+
protocolAddress: BigInt(context.protocolAddress),
|
|
756
|
+
tokenAddress: BigInt(context.tokenAddress),
|
|
757
|
+
ownerAddress: resolved.ownerAddress,
|
|
758
|
+
amount: options.amount,
|
|
759
|
+
nonce: nonce2,
|
|
760
|
+
payload: payloadCopy(options.payload)
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
const commitment = await computeNoteCommitment(
|
|
764
|
+
context.chainId,
|
|
765
|
+
identity,
|
|
766
|
+
BigInt(context.tokenAddress),
|
|
767
|
+
options.amount
|
|
768
|
+
);
|
|
769
|
+
let delivery = { ciphertext: "0x", clue: "0x" };
|
|
770
|
+
if (envelope && recipient.type === "invisi") {
|
|
771
|
+
const keys = resolved.getReceivingKeys();
|
|
772
|
+
delivery = {
|
|
773
|
+
ciphertext: hexlify4(
|
|
774
|
+
await encryptPrivateTransferNote(keys.viewingPublicKey, note, {
|
|
775
|
+
chainId: context.chainId,
|
|
776
|
+
protocolAddress: BigInt(context.protocolAddress),
|
|
777
|
+
outputCommitment: commitment
|
|
778
|
+
})
|
|
779
|
+
),
|
|
780
|
+
clue: hexlify4(createClue(keys.clueKey))
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
const ipp = typeof input === "string" ? input : encodePaymentRequest(request);
|
|
784
|
+
return new _NoteOutput(
|
|
785
|
+
{
|
|
786
|
+
context,
|
|
787
|
+
recipient,
|
|
788
|
+
amount: options.amount,
|
|
789
|
+
...note ? { note } : {},
|
|
790
|
+
identity,
|
|
791
|
+
commitment,
|
|
792
|
+
delivery,
|
|
793
|
+
...envelope && recipient.type === "evm" ? { deliveryPending: true } : {},
|
|
794
|
+
address,
|
|
795
|
+
change: false
|
|
796
|
+
},
|
|
797
|
+
outputCreation,
|
|
798
|
+
ipp
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
assertCommitment(expectedHex) {
|
|
802
|
+
if (typeof expectedHex !== "string" || expectedHex.toLowerCase() !== this.commitment.toLowerCase())
|
|
803
|
+
throw new Error("note output commitment mismatch");
|
|
804
|
+
}
|
|
805
|
+
get note() {
|
|
806
|
+
return this.preparedOutput.note;
|
|
807
|
+
}
|
|
808
|
+
get delivery() {
|
|
809
|
+
return this.preparedOutput.delivery;
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
async function materializeNoteOutput(payment, context, lookup) {
|
|
813
|
+
if ("expectedCommitment" in payment) throw new TypeError("expectedCommitment is not supported");
|
|
814
|
+
const cached = payment.preparedOutput;
|
|
815
|
+
if (cached === void 0) throw new TypeError("outputs must be created with NoteOutput.create");
|
|
816
|
+
const output = structuredClone(cached);
|
|
817
|
+
const expectedContext = checkedContext(context);
|
|
818
|
+
const copied = payment;
|
|
819
|
+
for (const actual of [output.context, copied.context]) {
|
|
820
|
+
if (!actual || actual.chainId !== expectedContext.chainId || BigInt(actual.protocolAddress) !== BigInt(expectedContext.protocolAddress) || BigInt(actual.tokenAddress) !== BigInt(expectedContext.tokenAddress))
|
|
821
|
+
throw new Error("note output chain, protocol or token mismatch");
|
|
822
|
+
}
|
|
823
|
+
if (output.amount !== payment.amount || output.address !== payment.to || output.identity !== copied.identity || output.commitment !== copied.commitment || JSON.stringify(output.recipient, (_, v) => typeof v === "bigint" ? v.toString() : v) !== JSON.stringify(copied.recipient, (_, v) => typeof v === "bigint" ? v.toString() : v))
|
|
824
|
+
throw new Error("note output material does not match payment intent");
|
|
825
|
+
if (fingerprint(output.delivery) !== copied.deliveryFingerprint)
|
|
826
|
+
throw new Error("note output delivery fingerprint mismatch");
|
|
827
|
+
const hasCiphertext = output.delivery.ciphertext !== "0x";
|
|
828
|
+
const hasClue = output.delivery.clue !== "0x";
|
|
829
|
+
const pending = output.deliveryPending === true;
|
|
830
|
+
if (copied.deliveryPending !== pending || "deliveryPending" in output && output.deliveryPending !== true)
|
|
831
|
+
throw new Error("note output pending delivery mismatch");
|
|
832
|
+
if (hasCiphertext !== hasClue || (pending ? hasCiphertext || output.recipient.type !== "evm" || !encrypted(output.recipient) : hasCiphertext !== encrypted(output.recipient)))
|
|
833
|
+
throw new Error("note output delivery mode mismatch");
|
|
834
|
+
const recipient = output.recipient;
|
|
835
|
+
if ("expectedCommitment" in recipient) throw new Error("expectedCommitment is not supported");
|
|
836
|
+
if (!encrypted(recipient) && payloadCopy(payment.payload).length !== 0)
|
|
837
|
+
throw new Error("payload requires an encrypted envelope");
|
|
838
|
+
if (recipient.type === "identity") {
|
|
839
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(output.identity) || output.address !== output.identity || output.note || "nonce" in recipient || "envelope" in recipient || "addressLookup" in recipient || recipient.identity.toLowerCase() !== output.identity.toLowerCase())
|
|
840
|
+
throw new Error("opaque identity output material mismatch");
|
|
841
|
+
identityValue(output.identity);
|
|
842
|
+
} else {
|
|
843
|
+
const note = output.note;
|
|
844
|
+
const parsed = ReceivingAddress5.parse(recipient.address);
|
|
845
|
+
if (recipient.type === "evm" && recipient.addressLookup !== void 0 && recipient.addressLookup !== "chain" && recipient.addressLookup !== "protocol-rpc")
|
|
846
|
+
throw new Error("invalid recipient addressLookup");
|
|
847
|
+
if (recipient.type === "invisi" && "addressLookup" in recipient)
|
|
848
|
+
throw new Error("addressLookup only applies to EVM recipients");
|
|
849
|
+
if (output.address !== recipient.address || recipient.type === "evm" && recipient.envelope !== void 0 && typeof recipient.envelope !== "boolean" || recipient.type === "invisi" && "envelope" in recipient)
|
|
850
|
+
throw new Error("recipient material mismatch");
|
|
851
|
+
if (!note || note.chainId !== context.chainId || note.protocolAddress !== BigInt(context.protocolAddress) || note.tokenAddress !== BigInt(context.tokenAddress) || note.ownerAddress !== parsed.ownerAddress || note.amount !== output.amount || note.nonce !== recipient.nonce || note.nonce !== payment.nonce || hexlify4(payloadCopy(note.payload)) !== hexlify4(payloadCopy(payment.payload)))
|
|
852
|
+
throw new Error("note output opening does not match payment intent");
|
|
853
|
+
if (recipient.type === "evm" !== (parsed.kind === "short"))
|
|
854
|
+
throw new Error("recipient type mismatch");
|
|
855
|
+
if (await computeNoteIdentity(BigInt(note.ownerAddress), nonceValue(note.nonce)) !== output.identity)
|
|
856
|
+
throw new Error("note output identity mismatch");
|
|
857
|
+
}
|
|
858
|
+
if (typeof output.amount !== "bigint" || output.amount <= 0n || output.amount >= 1n << 128n || await computeNoteCommitment(
|
|
859
|
+
context.chainId,
|
|
860
|
+
output.identity,
|
|
861
|
+
BigInt(context.tokenAddress),
|
|
862
|
+
output.amount
|
|
863
|
+
) !== output.commitment)
|
|
864
|
+
throw new Error("note output commitment mismatch");
|
|
865
|
+
assertRequestMatchesOutput(payment.ipp, output);
|
|
866
|
+
if (pending) {
|
|
867
|
+
if (recipient.type !== "evm") throw new Error("only EVM outputs may have pending delivery");
|
|
868
|
+
const resolved = await resolveRecipient(
|
|
869
|
+
recipient.address,
|
|
870
|
+
lookup?.(recipient.addressLookup ?? "protocol-rpc"),
|
|
871
|
+
{ field: "to" }
|
|
872
|
+
);
|
|
873
|
+
const keys = resolved.getReceivingKeys();
|
|
874
|
+
output.delivery = {
|
|
875
|
+
ciphertext: hexlify4(
|
|
876
|
+
await encryptPrivateTransferNote(keys.viewingPublicKey, output.note, {
|
|
877
|
+
chainId: context.chainId,
|
|
878
|
+
protocolAddress: BigInt(context.protocolAddress),
|
|
879
|
+
outputCommitment: output.commitment
|
|
880
|
+
})
|
|
881
|
+
),
|
|
882
|
+
clue: hexlify4(createClue(keys.clueKey))
|
|
883
|
+
};
|
|
884
|
+
delete output.deliveryPending;
|
|
885
|
+
}
|
|
886
|
+
return output;
|
|
887
|
+
}
|
|
888
|
+
function resolvedOutputSnapshot(payment, output) {
|
|
889
|
+
const copy = structuredClone(payment);
|
|
890
|
+
Object.assign(copy, {
|
|
891
|
+
preparedOutput: structuredClone(output),
|
|
892
|
+
deliveryFingerprint: fingerprint(output.delivery),
|
|
893
|
+
deliveryPending: false
|
|
894
|
+
});
|
|
895
|
+
return copy;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/notes/recipient-infrastructure.ts
|
|
899
|
+
import {
|
|
900
|
+
MissingCapabilityError as MissingCapabilityError3,
|
|
901
|
+
ReceivingAddress as ReceivingAddress6
|
|
902
|
+
} from "@invisi-labs/core";
|
|
903
|
+
import { ProtocolRpcClient } from "@invisi-labs/rpc";
|
|
904
|
+
var recipientInfrastructure = /* @__PURE__ */ Symbol("SDK recipient infrastructure");
|
|
905
|
+
function configureRecipientInfrastructure(options) {
|
|
906
|
+
const { provider, protocolRpc, chainReader } = options;
|
|
907
|
+
const registry = options.protocol.deployment.registry;
|
|
908
|
+
const deployment = options.protocol.deployment;
|
|
909
|
+
const lookup = (source) => async (ownerAddress) => {
|
|
910
|
+
if (!registry) throw new MissingCapabilityError3("receiving-key registry deployment");
|
|
911
|
+
if (source === "chain") {
|
|
912
|
+
if (!provider)
|
|
913
|
+
throw new MissingCapabilityError3("ethers provider for registry lookup");
|
|
914
|
+
return new EthersAddressResolver(
|
|
915
|
+
provider,
|
|
916
|
+
{
|
|
917
|
+
chainId: deployment.chainId,
|
|
918
|
+
registryAddress: registry.address
|
|
919
|
+
},
|
|
920
|
+
registry.runtimeCodeHash
|
|
921
|
+
).lookup(ownerAddress);
|
|
922
|
+
}
|
|
923
|
+
if (!(protocolRpc instanceof ProtocolRpcClient))
|
|
924
|
+
throw new MissingCapabilityError3("Protocol RPC recipient discovery");
|
|
925
|
+
const rpc = chainReader ? protocolRpc.withReader(chainReader) : protocolRpc;
|
|
926
|
+
const discovered = await rpc.getDeployment(deployment.chainId);
|
|
927
|
+
if (discovered.protocol.address !== deployment.protocol.address || discovered.protocol.runtimeCodeHash !== deployment.protocol.runtimeCodeHash || discovered.protocol.fromBlock !== deployment.protocol.fromBlock || discovered.registry?.address !== registry.address || discovered.registry.runtimeCodeHash !== registry.runtimeCodeHash || discovered.registry.fromBlock !== registry.fromBlock)
|
|
928
|
+
throw new Error("Recipient registry deployment pins mismatch");
|
|
929
|
+
const registration = await rpc.getReceivingAddress(ownerAddress);
|
|
930
|
+
return registration ? ReceivingAddress6.parse(registration.receivingAddress).getReceivingKeys() : null;
|
|
931
|
+
};
|
|
932
|
+
return { ...options, [recipientInfrastructure]: lookup };
|
|
933
|
+
}
|
|
934
|
+
function recipientLookup(options, source = "protocol-rpc") {
|
|
935
|
+
return async (ownerAddress) => {
|
|
936
|
+
const lookup = options[recipientInfrastructure];
|
|
937
|
+
if (!lookup) throw new MissingCapabilityError3("recipient infrastructure");
|
|
938
|
+
return lookup(source)(ownerAddress);
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/discovery/membership.ts
|
|
943
|
+
import { FIELD_MODULUS as FIELD_MODULUS4, poseidonHash } from "@invisi-labs/core";
|
|
944
|
+
async function verifyMembership(reader, context, block, input, proof) {
|
|
945
|
+
if (proof.block.hash !== block.hash || proof.block.number !== block.number || proof.leaf !== input.commitment || proof.treeId !== input.treeId || proof.leafIndex !== input.leafIndex || proof.path.length !== 16 || !Number.isInteger(input.leafIndex) || input.leafIndex < 0 || input.leafIndex >= 65536 || !await reader.isKnownRoot(context, input.treeId, proof.root, block))
|
|
946
|
+
throw new Error("membership context/root mismatch");
|
|
947
|
+
let root = BigInt(input.commitment);
|
|
948
|
+
for (let i = 0; i < 16; i++) {
|
|
949
|
+
const sibling = proof.path[i];
|
|
950
|
+
if (typeof sibling !== "bigint" || sibling < 0n || sibling >= FIELD_MODULUS4)
|
|
951
|
+
throw new Error("noncanonical membership path");
|
|
952
|
+
root = BigInt(
|
|
953
|
+
await poseidonHash(input.leafIndex >> i & 1 ? [sibling, root] : [root, sibling])
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
if (root !== BigInt(proof.root))
|
|
957
|
+
throw new Error("membership path does not produce canonical root");
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/transactions/preparation/credit-values.ts
|
|
961
|
+
import {
|
|
962
|
+
computeCreditCommitment,
|
|
963
|
+
computeCreditSharedKeyHash
|
|
964
|
+
} from "@invisi-labs/core";
|
|
965
|
+
async function creditLeaf(state) {
|
|
966
|
+
const n = state.note;
|
|
967
|
+
return computeCreditCommitment(
|
|
968
|
+
n.chainId,
|
|
969
|
+
n.spenderWalletIdentity,
|
|
970
|
+
n.ownerWalletIdentity,
|
|
971
|
+
n.spenderPayout,
|
|
972
|
+
n.ownerPayout,
|
|
973
|
+
n.tokenAddress,
|
|
974
|
+
n.amount,
|
|
975
|
+
n.nonce,
|
|
976
|
+
n.expiry,
|
|
977
|
+
await computeCreditSharedKeyHash(n.creditSharedKey),
|
|
978
|
+
n
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// src/transactions/preparation/transaction-preparation.ts
|
|
983
|
+
import {
|
|
984
|
+
normalizeTransactRequest,
|
|
985
|
+
transactUserPublicInputs as transactUserPublicInputs2,
|
|
986
|
+
WitnessBuilder as WitnessBuilder3,
|
|
987
|
+
ZERO_ADDRESS
|
|
988
|
+
} from "@invisi-labs/protocol";
|
|
989
|
+
import { getAddress as getAddress6 } from "ethers";
|
|
990
|
+
|
|
991
|
+
// src/transactions/preparation/prepare-common.ts
|
|
992
|
+
import { getAddress as getAddress4 } from "ethers";
|
|
993
|
+
import {
|
|
994
|
+
MissingCapabilityError as MissingCapabilityError4,
|
|
995
|
+
computeNoteCommitment as computeNoteCommitment2,
|
|
996
|
+
computeNoteIdentity as computeNoteIdentity2
|
|
997
|
+
} from "@invisi-labs/core";
|
|
998
|
+
import {
|
|
999
|
+
normalizeFunding as normalizeFunding2
|
|
1000
|
+
} from "@invisi-labs/protocol";
|
|
1001
|
+
|
|
1002
|
+
// src/transactions/preparation/selection.ts
|
|
1003
|
+
function selectNotes(notes, target, reserved) {
|
|
1004
|
+
if (typeof target !== "bigint" || target < 0n)
|
|
1005
|
+
throw new Error("selection target must be a nonnegative amount");
|
|
1006
|
+
if (target === 0n) return [];
|
|
1007
|
+
const eligible = notes.filter(
|
|
1008
|
+
(note) => note.spent === "unspent" && !reserved.has(note.commitment)
|
|
1009
|
+
);
|
|
1010
|
+
for (const record of eligible)
|
|
1011
|
+
if (typeof record.note.amount !== "bigint" || record.note.amount <= 0n || record.note.amount >= 1n << 128n)
|
|
1012
|
+
throw new Error("invalid verified note amount");
|
|
1013
|
+
eligible.sort(
|
|
1014
|
+
(a, b) => a.note.amount > b.note.amount ? -1 : a.note.amount < b.note.amount ? 1 : a.treeId < b.treeId ? -1 : a.treeId > b.treeId ? 1 : a.leafIndex - b.leafIndex || a.commitment.localeCompare(b.commitment)
|
|
1015
|
+
);
|
|
1016
|
+
const selected = [];
|
|
1017
|
+
let total = 0n;
|
|
1018
|
+
for (const record of eligible) {
|
|
1019
|
+
selected.push(structuredClone(record));
|
|
1020
|
+
total += record.note.amount;
|
|
1021
|
+
if (total >= target) return selected;
|
|
1022
|
+
}
|
|
1023
|
+
throw new Error("insufficient verified available funds");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// src/transactions/preparation/prepare-common.ts
|
|
1027
|
+
function positiveAmount(value, bits = 128) {
|
|
1028
|
+
if (typeof value !== "bigint" || value <= 0n || value >= 1n << BigInt(bits)) {
|
|
1029
|
+
throw new Error(`amount must be a positive uint${bits}`);
|
|
1030
|
+
}
|
|
1031
|
+
return value;
|
|
1032
|
+
}
|
|
1033
|
+
function copyApplicationPayload(value) {
|
|
1034
|
+
if (value === void 0) return new Uint8Array();
|
|
1035
|
+
if (!(value instanceof Uint8Array)) throw new TypeError("payload must be Uint8Array");
|
|
1036
|
+
return Uint8Array.from(value);
|
|
1037
|
+
}
|
|
1038
|
+
function positiveOutputAmount(payment, outputIndex) {
|
|
1039
|
+
try {
|
|
1040
|
+
return positiveAmount(payment.amount);
|
|
1041
|
+
} catch (cause) {
|
|
1042
|
+
throw new InvisiError(
|
|
1043
|
+
"PREPARATION_FAILED",
|
|
1044
|
+
cause instanceof Error ? cause.message : "Invalid output amount",
|
|
1045
|
+
{
|
|
1046
|
+
address: payment.to,
|
|
1047
|
+
field: "amount",
|
|
1048
|
+
...outputIndex === void 0 ? {} : { outputIndex },
|
|
1049
|
+
cause
|
|
1050
|
+
}
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
var Preparation = class {
|
|
1055
|
+
constructor(runtime) {
|
|
1056
|
+
this.runtime = runtime;
|
|
1057
|
+
}
|
|
1058
|
+
#roots = /* @__PURE__ */ new Map();
|
|
1059
|
+
async frame(requested) {
|
|
1060
|
+
const { reader, options } = this.runtime;
|
|
1061
|
+
await options.protocol.validateDeployment(reader);
|
|
1062
|
+
const block = await reader.getBlock(await reader.getLatestBlockNumber());
|
|
1063
|
+
const deadline = requested ?? block.timestamp + (options.transactionTTL ?? 300n);
|
|
1064
|
+
if (typeof deadline !== "bigint" || deadline < block.timestamp || deadline >= 1n << 64n) {
|
|
1065
|
+
throw new Error("invalid transaction deadline");
|
|
1066
|
+
}
|
|
1067
|
+
const state = await this.runtime.readInventory();
|
|
1068
|
+
if (state.invalidated) {
|
|
1069
|
+
throw new Error("wallet requires canonical resync");
|
|
1070
|
+
}
|
|
1071
|
+
for (const point of [state.observedThrough]) {
|
|
1072
|
+
if (point && (await reader.getBlock(point.number)).hash !== point.hash) {
|
|
1073
|
+
throw new Error("canonical wallet checkpoint changed; resync required");
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return {
|
|
1077
|
+
context: {
|
|
1078
|
+
...options.protocol.context,
|
|
1079
|
+
now: block.timestamp,
|
|
1080
|
+
timestamp: block.timestamp
|
|
1081
|
+
},
|
|
1082
|
+
block,
|
|
1083
|
+
deadline
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
async assertBlock(frame) {
|
|
1087
|
+
if ((await this.runtime.reader.getBlock(frame.block.number)).hash !== frame.block.hash) {
|
|
1088
|
+
throw new Error("canonical block changed while preparing");
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
async funding(source, frame) {
|
|
1092
|
+
if (!source) {
|
|
1093
|
+
return { funding: {}, fundingMode: "none" };
|
|
1094
|
+
}
|
|
1095
|
+
positiveAmount(source.amount);
|
|
1096
|
+
const payer = getAddress4(source.payer);
|
|
1097
|
+
const mode = source.authorization ?? "signature";
|
|
1098
|
+
if (BigInt(payer) === 0n) {
|
|
1099
|
+
throw new Error("funding payer must be nonzero");
|
|
1100
|
+
}
|
|
1101
|
+
if (mode !== "signature" && mode !== "caller") {
|
|
1102
|
+
throw new Error("invalid funding authorization mode");
|
|
1103
|
+
}
|
|
1104
|
+
if (mode === "caller" && (source.sigNonce || source.deadline)) {
|
|
1105
|
+
throw new Error("caller funding uses zero nonce/deadline");
|
|
1106
|
+
}
|
|
1107
|
+
const funding = normalizeFunding2({
|
|
1108
|
+
payer,
|
|
1109
|
+
amount: source.amount,
|
|
1110
|
+
sigNonce: mode === "caller" ? 0n : source.sigNonce ?? await this.runtime.reader.getFundingNonce(
|
|
1111
|
+
frame.context,
|
|
1112
|
+
payer,
|
|
1113
|
+
frame.block
|
|
1114
|
+
),
|
|
1115
|
+
deadline: mode === "caller" ? 0n : source.deadline ?? frame.deadline,
|
|
1116
|
+
...source.permit ? { permit: structuredClone(source.permit) } : {}
|
|
1117
|
+
});
|
|
1118
|
+
if (mode === "signature" && funding.deadline < frame.block.timestamp) {
|
|
1119
|
+
throw new Error("funding authorization deadline has expired");
|
|
1120
|
+
}
|
|
1121
|
+
return { funding, fundingMode: mode };
|
|
1122
|
+
}
|
|
1123
|
+
async change(amount, context) {
|
|
1124
|
+
const address = this.runtime.changeAddress ?? this.runtime.options.wallet.getReceivingAddress().toString();
|
|
1125
|
+
try {
|
|
1126
|
+
const output = await NoteOutput.create({
|
|
1127
|
+
chainId: context.chainId,
|
|
1128
|
+
protocolAddress: context.protocolAddress,
|
|
1129
|
+
tokenAddress: context.tokenAddress,
|
|
1130
|
+
amount,
|
|
1131
|
+
recipient: address.startsWith("0i") ? { type: "invisi", address } : { type: "evm", address }
|
|
1132
|
+
});
|
|
1133
|
+
return await this.output(output, context, true);
|
|
1134
|
+
} catch (cause) {
|
|
1135
|
+
if (cause instanceof InvisiError)
|
|
1136
|
+
throw new InvisiError(cause.code, cause.message, {
|
|
1137
|
+
address,
|
|
1138
|
+
field: "changeAddress",
|
|
1139
|
+
...cause.capability ? { capability: cause.capability } : {},
|
|
1140
|
+
cause: cause.cause ?? cause
|
|
1141
|
+
});
|
|
1142
|
+
throw new InvisiError(
|
|
1143
|
+
"PREPARATION_FAILED",
|
|
1144
|
+
cause instanceof Error ? cause.message : "Change output preparation failed",
|
|
1145
|
+
{ address, field: "changeAddress", cause }
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
async output(payment, context, change = false, outputIndex) {
|
|
1150
|
+
try {
|
|
1151
|
+
positiveOutputAmount(payment, outputIndex);
|
|
1152
|
+
const output = await materializeNoteOutput(
|
|
1153
|
+
payment,
|
|
1154
|
+
context,
|
|
1155
|
+
(source) => recipientLookup(this.runtime.options, source)
|
|
1156
|
+
);
|
|
1157
|
+
return { ...output, change };
|
|
1158
|
+
} catch (cause) {
|
|
1159
|
+
if (cause instanceof InvisiError) {
|
|
1160
|
+
throw new InvisiError(cause.code, cause.message, {
|
|
1161
|
+
address: cause.address ?? payment.to,
|
|
1162
|
+
field: change ? "changeAddress" : cause.field ?? "to",
|
|
1163
|
+
...outputIndex === void 0 ? {} : { outputIndex },
|
|
1164
|
+
...cause.capability ? { capability: cause.capability } : {},
|
|
1165
|
+
cause: cause.cause ?? cause
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
throw new InvisiError(
|
|
1169
|
+
"PREPARATION_FAILED",
|
|
1170
|
+
cause instanceof Error ? cause.message : "Output preparation failed",
|
|
1171
|
+
{
|
|
1172
|
+
address: payment.to,
|
|
1173
|
+
field: change ? "changeAddress" : "to",
|
|
1174
|
+
...outputIndex === void 0 ? {} : { outputIndex },
|
|
1175
|
+
cause
|
|
1176
|
+
}
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
async membership(input, frame) {
|
|
1181
|
+
const { options, reader } = this.runtime;
|
|
1182
|
+
if (!options.protocolRpc)
|
|
1183
|
+
throw new MissingCapabilityError4("Protocol RPC membership provider");
|
|
1184
|
+
const root = this.#roots.get(input.treeId);
|
|
1185
|
+
const proof = await options.protocolRpc.getMembership(
|
|
1186
|
+
frame.context,
|
|
1187
|
+
{
|
|
1188
|
+
...input,
|
|
1189
|
+
...root ? { root } : {}
|
|
1190
|
+
},
|
|
1191
|
+
frame.block
|
|
1192
|
+
);
|
|
1193
|
+
if (root && proof.root !== root)
|
|
1194
|
+
throw new Error("membership provider changed the pinned source root");
|
|
1195
|
+
await verifyMembership(reader, frame.context, frame.block, input, proof);
|
|
1196
|
+
this.#roots.set(input.treeId, proof.root);
|
|
1197
|
+
return { ...proof, path: [...proof.path] };
|
|
1198
|
+
}
|
|
1199
|
+
async selectFunds(target, frame, maxInputs = 168) {
|
|
1200
|
+
const { scope, options, reader } = this.runtime;
|
|
1201
|
+
const state = await this.runtime.readInventory();
|
|
1202
|
+
if (state.invalidated) {
|
|
1203
|
+
throw new Error("wallet requires canonical resync");
|
|
1204
|
+
}
|
|
1205
|
+
const need = target > 0n ? target : 0n;
|
|
1206
|
+
const nullifier = options.wallet.exportNullifier();
|
|
1207
|
+
const reserved = new Set(Object.keys(state.reservations));
|
|
1208
|
+
const eligible = Object.values(state.notes).filter(
|
|
1209
|
+
(note) => note.note.tokenAddress === BigInt(frame.context.tokenAddress) && note.spent === "unspent" && !reserved.has(note.commitment)
|
|
1210
|
+
);
|
|
1211
|
+
const byId = new Map(eligible.map((note) => [note.commitment, note]));
|
|
1212
|
+
const choices = this.runtime.inputSelection ? [...this.runtime.inputSelection] : this.runtime.explicitInputs ? eligible.map((note) => note.commitment) : need > 0n && options.coinSelector ? await options.coinSelector.select(storedCandidates(eligible), need) : selectNotes(eligible, need, reserved).map((note) => note.commitment);
|
|
1213
|
+
if (!Array.isArray(choices) || new Set(choices).size !== choices.length || choices.some((id) => !byId.has(id))) {
|
|
1214
|
+
throw new Error("invalid or duplicate coin selection");
|
|
1215
|
+
}
|
|
1216
|
+
const selected = choices.map((id) => structuredClone(byId.get(id)));
|
|
1217
|
+
const amount = selected.reduce((sum, input) => sum + input.note.amount, 0n);
|
|
1218
|
+
if (amount < need) {
|
|
1219
|
+
throw new Error("coin selection has insufficient verified funds");
|
|
1220
|
+
}
|
|
1221
|
+
if (selected.length > maxInputs || amount >= 1n << 135n) {
|
|
1222
|
+
throw new Error("coin selection exceeds transaction capacity");
|
|
1223
|
+
}
|
|
1224
|
+
const inputs = [];
|
|
1225
|
+
const sources = /* @__PURE__ */ new Map();
|
|
1226
|
+
const nullifiers = [];
|
|
1227
|
+
for (const input of selected) {
|
|
1228
|
+
const note = input.note;
|
|
1229
|
+
const c = frame.context;
|
|
1230
|
+
if (note.ownerAddress !== scope.ownerAddress || note.chainId !== c.chainId || note.protocolAddress !== BigInt(c.protocolAddress) || note.tokenAddress !== BigInt(c.tokenAddress) || await computeNoteCommitment2(
|
|
1231
|
+
c.chainId,
|
|
1232
|
+
await computeNoteIdentity2(BigInt(note.ownerAddress), note.nonce),
|
|
1233
|
+
note.tokenAddress,
|
|
1234
|
+
note.amount
|
|
1235
|
+
) !== input.commitment) {
|
|
1236
|
+
throw new Error("selected input preimage/context mismatch");
|
|
1237
|
+
}
|
|
1238
|
+
const proof = await this.membership(input, frame);
|
|
1239
|
+
const path = [...proof.path];
|
|
1240
|
+
const root = proof.root;
|
|
1241
|
+
if (sources.has(input.treeId) && sources.get(input.treeId) !== root) {
|
|
1242
|
+
throw new Error("source roots do not share a canonical snapshot");
|
|
1243
|
+
}
|
|
1244
|
+
sources.set(input.treeId, root);
|
|
1245
|
+
inputs.push({
|
|
1246
|
+
note,
|
|
1247
|
+
commitment: input.commitment,
|
|
1248
|
+
treeId: input.treeId,
|
|
1249
|
+
leafIndex: input.leafIndex,
|
|
1250
|
+
path
|
|
1251
|
+
});
|
|
1252
|
+
nullifiers.push(
|
|
1253
|
+
await nullifier.compute(c.chainId, input.commitment, input.note.nonce)
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
await this.assertUnspent(nullifiers, frame);
|
|
1257
|
+
return {
|
|
1258
|
+
inputs,
|
|
1259
|
+
sources: [...sources].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([treeId, merkleRoot]) => ({ treeId, merkleRoot })),
|
|
1260
|
+
nullifiers,
|
|
1261
|
+
amount
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
async assertUnspent(nullifiers, frame) {
|
|
1265
|
+
if (nullifiers.length) {
|
|
1266
|
+
const spent = await this.runtime.reader.isSpent(frame.context, nullifiers, frame.block);
|
|
1267
|
+
if (spent.length !== nullifiers.length || spent.some((value) => value !== false)) {
|
|
1268
|
+
throw new Error("selected input spent or invalid spent-state response");
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
function storedCandidates(inputs) {
|
|
1274
|
+
const records = inputs.filter(
|
|
1275
|
+
(input) => "blockNumber" in input && typeof input.blockNumber === "bigint" && "blockHash" in input && typeof input.blockHash === "string"
|
|
1276
|
+
);
|
|
1277
|
+
if (records.length !== inputs.length)
|
|
1278
|
+
throw new Error("custom coin selection requires stored receipt metadata");
|
|
1279
|
+
return structuredClone(records);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// src/transactions/preparation/prepare-credit.ts
|
|
1283
|
+
import { getAddress as getAddress5, hexlify as hexlify5, toBeHex } from "ethers";
|
|
1284
|
+
import {
|
|
1285
|
+
FIELD_MODULUS as FIELD_MODULUS5,
|
|
1286
|
+
applyCreditDrawUsage,
|
|
1287
|
+
computeCreditNullifier,
|
|
1288
|
+
computeCreditRecoveryPad,
|
|
1289
|
+
createClue as createClue2,
|
|
1290
|
+
deriveRemainderNonce,
|
|
1291
|
+
encryptCreditNote,
|
|
1292
|
+
generateCreditSharedKey,
|
|
1293
|
+
generateNonce as generateNonce2,
|
|
1294
|
+
normalizeCreditDeliveryState,
|
|
1295
|
+
normalizeCreditPolicy
|
|
1296
|
+
} from "@invisi-labs/core";
|
|
1297
|
+
import {
|
|
1298
|
+
WitnessBuilder as WitnessBuilder2,
|
|
1299
|
+
cancelCreditPublicInputs as cancelCreditPublicInputs2,
|
|
1300
|
+
createCreditPublicInputs as createCreditPublicInputs2,
|
|
1301
|
+
creditDrawPublicInputs as creditDrawPublicInputs2,
|
|
1302
|
+
selectCreditDrawCircuit,
|
|
1303
|
+
topUpCreditPublicInputs as topUpCreditPublicInputs2,
|
|
1304
|
+
word
|
|
1305
|
+
} from "@invisi-labs/protocol";
|
|
1306
|
+
var EMPTY = { ciphertext: "0x", clue: "0x" };
|
|
1307
|
+
var CreditPreparation = class {
|
|
1308
|
+
constructor(runtime) {
|
|
1309
|
+
this.runtime = runtime;
|
|
1310
|
+
this.#prepare = new Preparation(runtime);
|
|
1311
|
+
}
|
|
1312
|
+
#prepare;
|
|
1313
|
+
expiry(value, frame) {
|
|
1314
|
+
if (typeof value !== "bigint" || value <= frame.block.timestamp || value >= 1n << 64n) {
|
|
1315
|
+
throw new Error("Credit expiry must be a future uint64 timestamp");
|
|
1316
|
+
}
|
|
1317
|
+
return value;
|
|
1318
|
+
}
|
|
1319
|
+
async seal(input, frame) {
|
|
1320
|
+
const state = normalizeCreditDeliveryState(input);
|
|
1321
|
+
const commitment = await creditLeaf(state);
|
|
1322
|
+
const context = {
|
|
1323
|
+
chainId: frame.context.chainId,
|
|
1324
|
+
protocolAddress: BigInt(frame.context.protocolAddress),
|
|
1325
|
+
outputCommitment: commitment
|
|
1326
|
+
};
|
|
1327
|
+
const deliveries = await Promise.all(
|
|
1328
|
+
["owner", "spender"].map(async (role) => {
|
|
1329
|
+
const keys = role === "owner" ? state.ownerReceivingKeys : state.spenderReceivingKeys;
|
|
1330
|
+
return {
|
|
1331
|
+
ciphertext: hexlify5(
|
|
1332
|
+
await encryptCreditNote(keys.viewingPublicKey, state, { ...context, role })
|
|
1333
|
+
),
|
|
1334
|
+
clue: hexlify5(createClue2(keys.clueKey))
|
|
1335
|
+
};
|
|
1336
|
+
})
|
|
1337
|
+
);
|
|
1338
|
+
return { state, commitment, deliveries };
|
|
1339
|
+
}
|
|
1340
|
+
async input(commitment, role, frame) {
|
|
1341
|
+
const stored = (await this.runtime.readInventory()).credits[commitment + ":" + role];
|
|
1342
|
+
if (!stored || stored.spent !== "unspent" || stored.role !== role) {
|
|
1343
|
+
throw new Error(`confirmed unspent ${role} Credit is unavailable`);
|
|
1344
|
+
}
|
|
1345
|
+
const state = normalizeCreditDeliveryState(stored.state);
|
|
1346
|
+
const n = state.note;
|
|
1347
|
+
if ((role === "owner" ? n.ownerWalletIdentity : n.spenderWalletIdentity) !== this.runtime.scope.ownerAddress || n.chainId !== frame.context.chainId || n.protocolAddress !== BigInt(frame.context.protocolAddress) || n.tokenAddress !== BigInt(frame.context.tokenAddress) || await creditLeaf(state) !== commitment) {
|
|
1348
|
+
throw new Error("Credit role, commitment or deployment mismatch");
|
|
1349
|
+
}
|
|
1350
|
+
const proof = await this.#prepare.membership(stored, frame);
|
|
1351
|
+
const path = [...proof.path];
|
|
1352
|
+
const nullifier = await computeCreditNullifier(n.chainId, n.creditSharedKey, commitment);
|
|
1353
|
+
await this.#prepare.assertUnspent([nullifier], frame);
|
|
1354
|
+
return {
|
|
1355
|
+
input: {
|
|
1356
|
+
state,
|
|
1357
|
+
commitment,
|
|
1358
|
+
role,
|
|
1359
|
+
treeId: stored.treeId,
|
|
1360
|
+
leafIndex: stored.leafIndex,
|
|
1361
|
+
path
|
|
1362
|
+
},
|
|
1363
|
+
nullifier,
|
|
1364
|
+
source: {
|
|
1365
|
+
treeId: stored.treeId,
|
|
1366
|
+
merkleRoot: proof.root
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
base(frame, kind) {
|
|
1371
|
+
return {
|
|
1372
|
+
version: 1,
|
|
1373
|
+
id: crypto.randomUUID(),
|
|
1374
|
+
kind,
|
|
1375
|
+
context: frame.context,
|
|
1376
|
+
scope: this.runtime.scope,
|
|
1377
|
+
preparedAt: frame.block,
|
|
1378
|
+
deadline: frame.deadline,
|
|
1379
|
+
outputs: [],
|
|
1380
|
+
inputs: [],
|
|
1381
|
+
funding: {},
|
|
1382
|
+
fundingMode: "none"
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
async create(options) {
|
|
1386
|
+
positiveAmount(options.amount, 126);
|
|
1387
|
+
const frame = await this.#prepare.frame(options.deadline);
|
|
1388
|
+
const expiry = this.expiry(options.expiry, frame);
|
|
1389
|
+
const spender = await resolveRecipient(
|
|
1390
|
+
options.spender,
|
|
1391
|
+
recipientLookup(this.runtime.options),
|
|
1392
|
+
{ field: "spender" }
|
|
1393
|
+
);
|
|
1394
|
+
const owner = await resolveRecipient(
|
|
1395
|
+
this.runtime.options.wallet.getReceivingAddress().toString(),
|
|
1396
|
+
recipientLookup(this.runtime.options),
|
|
1397
|
+
{ field: "wallet" }
|
|
1398
|
+
);
|
|
1399
|
+
const ownerPayout = getAddress5(owner.ownerAddress);
|
|
1400
|
+
const spenderPayout = getAddress5(spender.ownerAddress);
|
|
1401
|
+
if (BigInt(ownerPayout) === 0n || BigInt(spenderPayout) === 0n) {
|
|
1402
|
+
throw new Error("Credit payouts must be nonzero");
|
|
1403
|
+
}
|
|
1404
|
+
const output = await this.seal(
|
|
1405
|
+
{
|
|
1406
|
+
note: {
|
|
1407
|
+
chainId: frame.context.chainId,
|
|
1408
|
+
protocolAddress: BigInt(frame.context.protocolAddress),
|
|
1409
|
+
tokenAddress: BigInt(frame.context.tokenAddress),
|
|
1410
|
+
ownerWalletIdentity: this.runtime.scope.ownerAddress,
|
|
1411
|
+
spenderWalletIdentity: spender.ownerAddress,
|
|
1412
|
+
ownerPayout: BigInt(ownerPayout),
|
|
1413
|
+
spenderPayout: BigInt(spenderPayout),
|
|
1414
|
+
...normalizeCreditPolicy({
|
|
1415
|
+
singleLimit: options.singleLimit ?? 0n,
|
|
1416
|
+
dailyLimit: options.dailyLimit ?? 0n,
|
|
1417
|
+
monthlyLimit: options.monthlyLimit ?? 0n
|
|
1418
|
+
}),
|
|
1419
|
+
amount: options.amount,
|
|
1420
|
+
payload: copyApplicationPayload(options.payload),
|
|
1421
|
+
expiry,
|
|
1422
|
+
nonce: generateNonce2(),
|
|
1423
|
+
creditSharedKey: generateCreditSharedKey()
|
|
1424
|
+
},
|
|
1425
|
+
ownerReceivingKeys: owner.getReceivingKeys(),
|
|
1426
|
+
spenderReceivingKeys: spender.getReceivingKeys()
|
|
1427
|
+
},
|
|
1428
|
+
frame
|
|
1429
|
+
);
|
|
1430
|
+
return this.creation(
|
|
1431
|
+
this.base(frame, "create-credit"),
|
|
1432
|
+
output,
|
|
1433
|
+
options.amount,
|
|
1434
|
+
options.funding,
|
|
1435
|
+
options.fee,
|
|
1436
|
+
frame
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
async creation(plan, output, delta, fundingSource, fee, frame, previous) {
|
|
1440
|
+
const funding = await this.#prepare.funding(fundingSource, frame);
|
|
1441
|
+
const selected = await this.#prepare.selectFunds(
|
|
1442
|
+
delta + (fee?.amount ?? 0n) - (funding.funding.amount ?? 0n),
|
|
1443
|
+
frame,
|
|
1444
|
+
10
|
|
1445
|
+
);
|
|
1446
|
+
const change = selected.amount + (funding.funding.amount ?? 0n) - delta - (fee?.amount ?? 0n);
|
|
1447
|
+
plan.inputs = selected.inputs;
|
|
1448
|
+
Object.assign(plan, funding);
|
|
1449
|
+
if (change > 0n) {
|
|
1450
|
+
plan.outputs.push(await this.#prepare.change(change, frame.context));
|
|
1451
|
+
}
|
|
1452
|
+
const creation = {
|
|
1453
|
+
token: frame.context.tokenAddress,
|
|
1454
|
+
feeRecipient: fee?.recipient ?? "0x0000000000000000000000000000000000000000",
|
|
1455
|
+
feeAmount: fee?.amount ?? 0n,
|
|
1456
|
+
sources: selected.sources,
|
|
1457
|
+
nullifiers: selected.nullifiers,
|
|
1458
|
+
creditCommitment: output.commitment,
|
|
1459
|
+
changeCommitment: plan.outputs[0]?.commitment ?? word(0n),
|
|
1460
|
+
owner: getAddress5(toBeHex(output.state.note.ownerPayout, 20)),
|
|
1461
|
+
expiry: output.state.note.expiry,
|
|
1462
|
+
deadline: frame.deadline
|
|
1463
|
+
};
|
|
1464
|
+
const deliveries = [...output.deliveries, plan.outputs[0]?.delivery ?? { ...EMPTY }];
|
|
1465
|
+
plan.capacity = 10;
|
|
1466
|
+
plan.credit = {
|
|
1467
|
+
inputs: previous ? [previous.input] : [],
|
|
1468
|
+
outputs: [output],
|
|
1469
|
+
deliveries,
|
|
1470
|
+
creation
|
|
1471
|
+
};
|
|
1472
|
+
if (previous) {
|
|
1473
|
+
plan.credit.topUp = {
|
|
1474
|
+
creation,
|
|
1475
|
+
creditSource: previous.source,
|
|
1476
|
+
creditNullifier: previous.nullifier,
|
|
1477
|
+
previousExpiry: previous.input.state.note.expiry
|
|
1478
|
+
};
|
|
1479
|
+
plan.statement = WitnessBuilder2.statement(
|
|
1480
|
+
"credit_top_up",
|
|
1481
|
+
topUpCreditPublicInputs2(frame.context, plan.credit.topUp, deliveries, plan.funding)
|
|
1482
|
+
);
|
|
1483
|
+
} else {
|
|
1484
|
+
plan.statement = WitnessBuilder2.statement(
|
|
1485
|
+
"create_credit",
|
|
1486
|
+
createCreditPublicInputs2(frame.context, creation, deliveries, plan.funding)
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
await this.#prepare.assertBlock(frame);
|
|
1490
|
+
return this.runtime.complete(plan);
|
|
1491
|
+
}
|
|
1492
|
+
async topUp(options) {
|
|
1493
|
+
positiveAmount(options.additionalAmount, 126);
|
|
1494
|
+
const frame = await this.#prepare.frame(options.deadline);
|
|
1495
|
+
const previous = await this.input(options.credit, "owner", frame);
|
|
1496
|
+
const old = previous.input.state;
|
|
1497
|
+
if (old.note.expiry <= frame.block.timestamp) {
|
|
1498
|
+
throw new Error("previous Credit has expired");
|
|
1499
|
+
}
|
|
1500
|
+
const expiry = this.expiry(options.expiry ?? old.note.expiry, frame);
|
|
1501
|
+
if (expiry < old.note.expiry) {
|
|
1502
|
+
throw new Error("Credit expiry must not decrease");
|
|
1503
|
+
}
|
|
1504
|
+
const amount = positiveAmount(old.note.amount + options.additionalAmount, 126);
|
|
1505
|
+
const output = await this.seal(
|
|
1506
|
+
{
|
|
1507
|
+
...old,
|
|
1508
|
+
note: {
|
|
1509
|
+
...old.note,
|
|
1510
|
+
singleLimit: options.singleLimit ?? old.note.singleLimit ?? 0n,
|
|
1511
|
+
dailyLimit: options.dailyLimit ?? old.note.dailyLimit ?? 0n,
|
|
1512
|
+
monthlyLimit: options.monthlyLimit ?? old.note.monthlyLimit ?? 0n,
|
|
1513
|
+
amount,
|
|
1514
|
+
expiry,
|
|
1515
|
+
nonce: generateNonce2(),
|
|
1516
|
+
payload: copyApplicationPayload(
|
|
1517
|
+
options.payload === void 0 ? old.note.payload : options.payload
|
|
1518
|
+
)
|
|
1519
|
+
}
|
|
1520
|
+
},
|
|
1521
|
+
frame
|
|
1522
|
+
);
|
|
1523
|
+
return this.creation(
|
|
1524
|
+
this.base(frame, "top-up-credit"),
|
|
1525
|
+
output,
|
|
1526
|
+
options.additionalAmount,
|
|
1527
|
+
options.funding,
|
|
1528
|
+
options.fee,
|
|
1529
|
+
frame,
|
|
1530
|
+
previous
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
async draw(options) {
|
|
1534
|
+
const frame = await this.#prepare.frame(options.deadline);
|
|
1535
|
+
const profile = selectCreditDrawCircuit(options.credits.length);
|
|
1536
|
+
if (new Set(options.credits.map((value) => value.credit)).size !== options.credits.length) {
|
|
1537
|
+
throw new Error("duplicate Credit input");
|
|
1538
|
+
}
|
|
1539
|
+
const members = await Promise.all(
|
|
1540
|
+
options.credits.map((value) => this.input(value.credit, "spender", frame))
|
|
1541
|
+
);
|
|
1542
|
+
const payout = members[0].input.state.note.spenderPayout;
|
|
1543
|
+
const sources = /* @__PURE__ */ new Map();
|
|
1544
|
+
const outputs = [];
|
|
1545
|
+
const deliveries = [];
|
|
1546
|
+
const recoveryAmounts = [];
|
|
1547
|
+
const remainders = [];
|
|
1548
|
+
const amounts = [];
|
|
1549
|
+
for (const [i, member] of members.entries()) {
|
|
1550
|
+
const state = member.input.state;
|
|
1551
|
+
const n = state.note;
|
|
1552
|
+
const amount = positiveAmount(options.credits[i].amount, 126);
|
|
1553
|
+
if (n.spenderPayout !== payout) {
|
|
1554
|
+
throw new Error(
|
|
1555
|
+
"Credit draw requires separate groups for different public payouts; one call must be atomic"
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
this.expiry(n.expiry, frame);
|
|
1559
|
+
if (amount > n.amount) {
|
|
1560
|
+
throw new Error("Credit draw exceeds remaining amount");
|
|
1561
|
+
}
|
|
1562
|
+
if (sources.has(member.source.treeId) && sources.get(member.source.treeId) !== member.source.merkleRoot) {
|
|
1563
|
+
throw new Error("Credit source root mismatch");
|
|
1564
|
+
}
|
|
1565
|
+
sources.set(member.source.treeId, member.source.merkleRoot);
|
|
1566
|
+
amounts.push(amount);
|
|
1567
|
+
const usage = applyCreditDrawUsage(n, amount, frame.block.timestamp);
|
|
1568
|
+
const remaining = n.amount - amount;
|
|
1569
|
+
if (remaining === 0n) {
|
|
1570
|
+
remainders.push(word(0n));
|
|
1571
|
+
recoveryAmounts.push(word(0n));
|
|
1572
|
+
deliveries.push({ ...EMPTY }, { ...EMPTY });
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
const args = [
|
|
1576
|
+
n.chainId,
|
|
1577
|
+
n.protocolAddress,
|
|
1578
|
+
n.tokenAddress,
|
|
1579
|
+
BigInt(member.nullifier),
|
|
1580
|
+
n.creditSharedKey
|
|
1581
|
+
];
|
|
1582
|
+
const output = await this.seal(
|
|
1583
|
+
{
|
|
1584
|
+
...state,
|
|
1585
|
+
note: {
|
|
1586
|
+
...n,
|
|
1587
|
+
...usage,
|
|
1588
|
+
amount: remaining,
|
|
1589
|
+
payload: copyApplicationPayload(
|
|
1590
|
+
options.credits[i].payload === void 0 ? n.payload : options.credits[i].payload
|
|
1591
|
+
),
|
|
1592
|
+
nonce: await deriveRemainderNonce(...args)
|
|
1593
|
+
}
|
|
1594
|
+
},
|
|
1595
|
+
frame
|
|
1596
|
+
);
|
|
1597
|
+
outputs.push(output);
|
|
1598
|
+
remainders.push(output.commitment);
|
|
1599
|
+
recoveryAmounts.push(
|
|
1600
|
+
word((remaining + await computeCreditRecoveryPad(...args)) % FIELD_MODULUS5)
|
|
1601
|
+
);
|
|
1602
|
+
deliveries.push(...output.deliveries);
|
|
1603
|
+
}
|
|
1604
|
+
const plan = this.base(frame, "draw-credit");
|
|
1605
|
+
const params = {
|
|
1606
|
+
token: frame.context.tokenAddress,
|
|
1607
|
+
sources: [...sources].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([treeId, merkleRoot]) => ({ treeId, merkleRoot })),
|
|
1608
|
+
nullifiers: members.map((member) => member.nullifier),
|
|
1609
|
+
remainderCommitments: remainders,
|
|
1610
|
+
expiries: members.map((member) => member.input.state.note.expiry),
|
|
1611
|
+
recoveryAmounts,
|
|
1612
|
+
spender: getAddress5(toBeHex(payout, 20)),
|
|
1613
|
+
totalAmount: amounts.reduce((sum, amount) => sum + amount, 0n),
|
|
1614
|
+
deadline: frame.deadline
|
|
1615
|
+
};
|
|
1616
|
+
plan.capacity = profile.maxSize;
|
|
1617
|
+
plan.credit = {
|
|
1618
|
+
inputs: members.map((member) => member.input),
|
|
1619
|
+
outputs,
|
|
1620
|
+
deliveries,
|
|
1621
|
+
draw: params,
|
|
1622
|
+
amounts
|
|
1623
|
+
};
|
|
1624
|
+
plan.statement = WitnessBuilder2.statement(
|
|
1625
|
+
profile.name,
|
|
1626
|
+
creditDrawPublicInputs2(frame.context, params, deliveries)
|
|
1627
|
+
);
|
|
1628
|
+
await this.#prepare.assertBlock(frame);
|
|
1629
|
+
return this.runtime.complete(plan);
|
|
1630
|
+
}
|
|
1631
|
+
async cancel(options) {
|
|
1632
|
+
const frame = await this.#prepare.frame(options.deadline);
|
|
1633
|
+
const previous = await this.input(options.credit, "owner", frame);
|
|
1634
|
+
const n = previous.input.state.note;
|
|
1635
|
+
const plan = this.base(frame, "cancel-credit");
|
|
1636
|
+
if ((options.fee?.amount ?? 0n) >= n.amount)
|
|
1637
|
+
throw new Error("fee must be less than the cancellation refund");
|
|
1638
|
+
const params = {
|
|
1639
|
+
feeRecipient: options.fee?.recipient ?? "0x0000000000000000000000000000000000000000",
|
|
1640
|
+
feeAmount: options.fee?.amount ?? 0n,
|
|
1641
|
+
token: frame.context.tokenAddress,
|
|
1642
|
+
source: previous.source,
|
|
1643
|
+
nullifier: previous.nullifier,
|
|
1644
|
+
owner: getAddress5(toBeHex(n.ownerPayout, 20)),
|
|
1645
|
+
amount: n.amount,
|
|
1646
|
+
deadline: frame.deadline
|
|
1647
|
+
};
|
|
1648
|
+
plan.capacity = 1;
|
|
1649
|
+
plan.credit = { inputs: [previous.input], outputs: [], deliveries: [], cancel: params };
|
|
1650
|
+
plan.statement = WitnessBuilder2.statement(
|
|
1651
|
+
"credit_cancel",
|
|
1652
|
+
cancelCreditPublicInputs2(frame.context, params)
|
|
1653
|
+
);
|
|
1654
|
+
await this.#prepare.assertBlock(frame);
|
|
1655
|
+
return this.runtime.complete(plan);
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
// src/transactions/preparation/transaction-preparation.ts
|
|
1660
|
+
var TransactionPreparation = class {
|
|
1661
|
+
constructor(context) {
|
|
1662
|
+
this.context = context;
|
|
1663
|
+
const complete = context.complete;
|
|
1664
|
+
this.context = {
|
|
1665
|
+
...context,
|
|
1666
|
+
complete: (plan) => {
|
|
1667
|
+
const intent = this.#intent ? structuredClone(this.#intent) : void 0;
|
|
1668
|
+
if (intent && (intent.kind === "transfer" || intent.kind === "shield")) {
|
|
1669
|
+
const businessOutputs = plan.outputs.filter((output) => !output.change);
|
|
1670
|
+
intent.options.outputs = intent.options.outputs.map(
|
|
1671
|
+
(payment, index) => resolvedOutputSnapshot(payment, businessOutputs[index])
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
return complete({ ...plan, ...intent ? { intent } : {} });
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
#intent;
|
|
1679
|
+
runtime(tokenAddress) {
|
|
1680
|
+
if (getAddress6(tokenAddress) !== getAddress6(this.context.options.protocol.context.tokenAddress))
|
|
1681
|
+
throw new Error("preparation token mismatch");
|
|
1682
|
+
return this.context;
|
|
1683
|
+
}
|
|
1684
|
+
get scope() {
|
|
1685
|
+
return this.context.scope;
|
|
1686
|
+
}
|
|
1687
|
+
async prepareTransfer(options) {
|
|
1688
|
+
this.#intent = { kind: "transfer", options: structuredClone(options) };
|
|
1689
|
+
if (Object.hasOwn(options, "target") || Object.hasOwn(options, "execution")) {
|
|
1690
|
+
throw new Error(
|
|
1691
|
+
"prepareTransfer accepts no target or execution; choose execution in prove()"
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
const copied = structuredClone(options);
|
|
1695
|
+
const runtime = this.runtime(copied.tokenAddress);
|
|
1696
|
+
if (copied.commitments !== void 0 && copied.commitments.length === 0)
|
|
1697
|
+
throw new Error("empty explicit input selection");
|
|
1698
|
+
const preparation = new Preparation({
|
|
1699
|
+
...runtime,
|
|
1700
|
+
...copied.commitments !== void 0 ? { inputSelection: copied.commitments } : {}
|
|
1701
|
+
});
|
|
1702
|
+
const frame = await preparation.frame(copied.deadline);
|
|
1703
|
+
if (copied.outputs.length > 168) throw new Error("transfer supports at most 168 outputs");
|
|
1704
|
+
let withdrawalAddress = ZERO_ADDRESS;
|
|
1705
|
+
let withdrawalAmount = 0n;
|
|
1706
|
+
if (copied.withdrawal !== void 0) {
|
|
1707
|
+
withdrawalAddress = getAddress6(copied.withdrawal.to);
|
|
1708
|
+
positiveAmount(copied.withdrawal.amount);
|
|
1709
|
+
if (BigInt(withdrawalAddress) === 0n)
|
|
1710
|
+
throw new Error("withdrawal destination must be nonzero");
|
|
1711
|
+
withdrawalAmount = copied.withdrawal.amount;
|
|
1712
|
+
}
|
|
1713
|
+
if (copied.outputs.length === 0 && withdrawalAmount === 0n)
|
|
1714
|
+
throw new Error("transfer requires a private output or public withdrawal");
|
|
1715
|
+
const feeRecipient = copied.fee ? getAddress6(copied.fee.recipient) : ZERO_ADDRESS;
|
|
1716
|
+
const feeAmount = copied.fee?.amount ?? 0n;
|
|
1717
|
+
if (copied.fee) {
|
|
1718
|
+
positiveAmount(feeAmount);
|
|
1719
|
+
if (BigInt(feeRecipient) === 0n) throw new Error("fee recipient must be nonzero");
|
|
1720
|
+
}
|
|
1721
|
+
const total = copied.outputs.reduce(
|
|
1722
|
+
(sum, output, index) => sum + positiveOutputAmount(output, index),
|
|
1723
|
+
withdrawalAmount + feeAmount
|
|
1724
|
+
);
|
|
1725
|
+
const funding = await preparation.funding(copied.funding, frame);
|
|
1726
|
+
return this.funds(
|
|
1727
|
+
copied.outputs,
|
|
1728
|
+
total,
|
|
1729
|
+
withdrawalAddress,
|
|
1730
|
+
withdrawalAmount,
|
|
1731
|
+
{ recipient: feeRecipient, amount: feeAmount },
|
|
1732
|
+
frame,
|
|
1733
|
+
funding,
|
|
1734
|
+
preparation
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
async funds(payments, total, withdrawalAddress, withdrawalAmount, fee, frame, funding, preparation) {
|
|
1738
|
+
if (total >= 1n << 135n) {
|
|
1739
|
+
throw new Error("transaction sum exceeds uint135");
|
|
1740
|
+
}
|
|
1741
|
+
const { context, block, deadline } = frame;
|
|
1742
|
+
const selection = await preparation.selectFunds(
|
|
1743
|
+
total - (funding.funding.amount ?? 0n),
|
|
1744
|
+
frame
|
|
1745
|
+
);
|
|
1746
|
+
const outputs = await Promise.all(
|
|
1747
|
+
payments.map((payment, index) => preparation.output(payment, context, false, index))
|
|
1748
|
+
);
|
|
1749
|
+
const change = selection.amount + (funding.funding.amount ?? 0n) - total;
|
|
1750
|
+
if (change > 0n) {
|
|
1751
|
+
outputs.push(await preparation.change(change, context));
|
|
1752
|
+
}
|
|
1753
|
+
const params = {
|
|
1754
|
+
token: context.tokenAddress,
|
|
1755
|
+
sources: selection.sources,
|
|
1756
|
+
nullifiers: selection.nullifiers,
|
|
1757
|
+
outputCommitments: outputs.map((output) => output.commitment),
|
|
1758
|
+
withdrawalAddress,
|
|
1759
|
+
withdrawalAmount,
|
|
1760
|
+
deadline,
|
|
1761
|
+
feeRecipient: fee.recipient,
|
|
1762
|
+
feeAmount: fee.amount
|
|
1763
|
+
};
|
|
1764
|
+
const deliveries = outputs.map((output) => output.delivery);
|
|
1765
|
+
const normalized = normalizeTransactRequest(context, params, deliveries, funding.funding);
|
|
1766
|
+
const statement2 = WitnessBuilder3.statement(
|
|
1767
|
+
`transact_user_slots${normalized.capacity}`,
|
|
1768
|
+
transactUserPublicInputs2(context, params, deliveries, funding.funding),
|
|
1769
|
+
"evm"
|
|
1770
|
+
);
|
|
1771
|
+
await preparation.assertBlock(frame);
|
|
1772
|
+
return preparation.runtime.complete({
|
|
1773
|
+
version: 1,
|
|
1774
|
+
id: crypto.randomUUID(),
|
|
1775
|
+
kind: "transfer",
|
|
1776
|
+
context,
|
|
1777
|
+
scope: this.scope,
|
|
1778
|
+
preparedAt: block,
|
|
1779
|
+
deadline,
|
|
1780
|
+
outputs,
|
|
1781
|
+
inputs: selection.inputs,
|
|
1782
|
+
...funding,
|
|
1783
|
+
params,
|
|
1784
|
+
capacity: normalized.capacity,
|
|
1785
|
+
statement: statement2
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
async prepareShield(options) {
|
|
1789
|
+
this.#intent = { kind: "shield", options: structuredClone(options) };
|
|
1790
|
+
const copied = structuredClone(options);
|
|
1791
|
+
const preparation = new Preparation(this.runtime(copied.tokenAddress));
|
|
1792
|
+
const frame = await preparation.frame(copied.deadline);
|
|
1793
|
+
const funding = await preparation.funding(copied.funding, frame);
|
|
1794
|
+
if (copied.outputs.length < 1 || copied.outputs.length > 10) {
|
|
1795
|
+
throw new Error("shield requires 1..10 outputs");
|
|
1796
|
+
}
|
|
1797
|
+
const outputs = await Promise.all(
|
|
1798
|
+
copied.outputs.map(
|
|
1799
|
+
(output, index) => preparation.output(output, frame.context, false, index)
|
|
1800
|
+
)
|
|
1801
|
+
);
|
|
1802
|
+
if (outputs.reduce((sum, output) => sum + output.amount, 0n) !== (funding.funding.amount ?? 0n) - (copied.fee?.amount ?? 0n)) {
|
|
1803
|
+
throw new Error("shield funding must equal output total");
|
|
1804
|
+
}
|
|
1805
|
+
await preparation.assertBlock(frame);
|
|
1806
|
+
return preparation.runtime.complete({
|
|
1807
|
+
version: 1,
|
|
1808
|
+
id: crypto.randomUUID(),
|
|
1809
|
+
kind: "shield",
|
|
1810
|
+
context: frame.context,
|
|
1811
|
+
scope: this.scope,
|
|
1812
|
+
preparedAt: frame.block,
|
|
1813
|
+
deadline: frame.deadline,
|
|
1814
|
+
outputs,
|
|
1815
|
+
inputs: [],
|
|
1816
|
+
...funding,
|
|
1817
|
+
shieldParams: {
|
|
1818
|
+
token: frame.context.tokenAddress,
|
|
1819
|
+
feeRecipient: copied.fee?.recipient ?? ZERO_ADDRESS,
|
|
1820
|
+
feeAmount: copied.fee?.amount ?? 0n,
|
|
1821
|
+
outputCommitments: outputs.map((output) => output.commitment),
|
|
1822
|
+
deadline: frame.deadline
|
|
1823
|
+
},
|
|
1824
|
+
shieldOutputs: outputs.map((output) => ({
|
|
1825
|
+
identity: output.identity,
|
|
1826
|
+
amount: output.amount
|
|
1827
|
+
}))
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
prepareCreateCredit(options) {
|
|
1831
|
+
this.#intent = { kind: "create-credit", options: structuredClone(options) };
|
|
1832
|
+
return new CreditPreparation(this.runtime(options.tokenAddress)).create(
|
|
1833
|
+
structuredClone(options)
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
prepareDrawCredit(options) {
|
|
1837
|
+
this.#intent = void 0;
|
|
1838
|
+
return new CreditPreparation(this.runtime(options.tokenAddress)).draw(
|
|
1839
|
+
structuredClone(options)
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1842
|
+
prepareTopUpCredit(options) {
|
|
1843
|
+
this.#intent = { kind: "top-up-credit", options: structuredClone(options) };
|
|
1844
|
+
return new CreditPreparation(this.runtime(options.tokenAddress)).topUp(
|
|
1845
|
+
structuredClone(options)
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
prepareCancelCredit(options) {
|
|
1849
|
+
this.#intent = { kind: "cancel-credit", options: structuredClone(options) };
|
|
1850
|
+
return new CreditPreparation(this.runtime(options.tokenAddress)).cancel(
|
|
1851
|
+
structuredClone(options)
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
|
|
1856
|
+
export {
|
|
1857
|
+
InvisiError,
|
|
1858
|
+
prepareWithErrors,
|
|
1859
|
+
encodePaymentNote,
|
|
1860
|
+
decodePaymentNote,
|
|
1861
|
+
exportPaymentNote,
|
|
1862
|
+
requiresAuthorization,
|
|
1863
|
+
statement,
|
|
1864
|
+
authorizationRequest,
|
|
1865
|
+
operationExpired,
|
|
1866
|
+
exportBroadcasterIntent,
|
|
1867
|
+
encodePaymentRequest,
|
|
1868
|
+
decodePaymentRequest,
|
|
1869
|
+
NoteOutput,
|
|
1870
|
+
configureRecipientInfrastructure,
|
|
1871
|
+
recipientLookup,
|
|
1872
|
+
verifyMembership,
|
|
1873
|
+
positiveAmount,
|
|
1874
|
+
creditLeaf,
|
|
1875
|
+
TransactionPreparation
|
|
1876
|
+
};
|
|
1877
|
+
//# sourceMappingURL=chunk-ITTGBIG2.js.map
|