@invisi-labs/protocol 1.1.6 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contracts.d.cts +1 -1
- package/dist/contracts.d.ts +1 -1
- package/dist/index.cjs +6566 -6496
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +397 -398
- package/dist/index.d.ts +397 -398
- package/dist/index.js +2228 -2153
- package/dist/index.js.map +1 -1
- package/dist/{types-BmUlasOg.d.cts → types-Bvakbfeb.d.cts} +1 -1
- package/dist/{types-BmUlasOg.d.ts → types-Bvakbfeb.d.ts} +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -4,623 +4,145 @@ import {
|
|
|
4
4
|
invisiProtocolAbi
|
|
5
5
|
} from "./chunk-BYJ4LXCJ.js";
|
|
6
6
|
|
|
7
|
-
// src/
|
|
8
|
-
import { getAddress as getAddress2, keccak256 } from "ethers";
|
|
7
|
+
// src/authorization/WitnessContext.ts
|
|
9
8
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
computeCreditCommitment,
|
|
10
|
+
computeCreditSharedKeyHash,
|
|
11
|
+
computeNoteCommitment,
|
|
12
|
+
computeNoteIdentity,
|
|
13
|
+
computeNoteNullifier,
|
|
14
|
+
computeWalletAuthorizationDigest,
|
|
15
|
+
poseidonHash,
|
|
16
|
+
verifyWalletAuthorization
|
|
12
17
|
} from "@invisi-labs/core";
|
|
13
18
|
|
|
14
|
-
// src/
|
|
15
|
-
import {
|
|
16
|
-
|
|
19
|
+
// src/authorization/common.ts
|
|
20
|
+
import {
|
|
21
|
+
normalizeCreditPolicy
|
|
22
|
+
} from "@invisi-labs/core";
|
|
23
|
+
import {
|
|
24
|
+
LocalWitness
|
|
25
|
+
} from "@invisi-labs/prover";
|
|
17
26
|
|
|
18
|
-
// src/
|
|
27
|
+
// src/validation.ts
|
|
28
|
+
import { MAX_CIPHERTEXT_BYTES } from "@invisi-labs/core";
|
|
19
29
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
AbiCoder,
|
|
31
|
+
dataLength,
|
|
22
32
|
getAddress,
|
|
23
|
-
getBytes,
|
|
24
|
-
hexlify,
|
|
25
33
|
isHexString,
|
|
26
|
-
|
|
34
|
+
keccak256,
|
|
35
|
+
ParamType,
|
|
27
36
|
toBeHex
|
|
28
37
|
} from "ethers";
|
|
29
|
-
|
|
30
|
-
var
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
var FIELD_MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
39
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
40
|
+
var ZERO_WORD = toBeHex(0n, 32);
|
|
41
|
+
var EMPTY_PERMIT = {
|
|
42
|
+
deadline: 0n,
|
|
43
|
+
v: 0,
|
|
44
|
+
r: ZERO_WORD,
|
|
45
|
+
s: ZERO_WORD
|
|
46
|
+
};
|
|
47
|
+
var EMPTY_FUNDING = {
|
|
48
|
+
payer: ZERO_ADDRESS,
|
|
49
|
+
amount: 0n,
|
|
50
|
+
sigNonce: 0n,
|
|
51
|
+
deadline: 0n,
|
|
52
|
+
signature: "0x",
|
|
53
|
+
permit: EMPTY_PERMIT
|
|
54
|
+
};
|
|
55
|
+
function uint(v, bits, name = "value") {
|
|
56
|
+
if (typeof v !== "bigint" && typeof v !== "number" && typeof v !== "string" || typeof v === "number" && !Number.isSafeInteger(v))
|
|
57
|
+
throw new Error(`${name} must be an integer`);
|
|
58
|
+
const n = BigInt(v);
|
|
59
|
+
if (n < 0n || n >= 1n << BigInt(bits)) throw new Error(`${name} exceeds u${bits}`);
|
|
60
|
+
return n;
|
|
35
61
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return
|
|
62
|
+
function field(v, name = "field") {
|
|
63
|
+
const n = uint(v, 256, name);
|
|
64
|
+
if (n >= FIELD_MODULUS) throw new Error(`${name} is noncanonical field`);
|
|
65
|
+
return n;
|
|
40
66
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
);
|
|
49
|
-
if (decoded.kind !== "long") throw new Error("Expected full receiving address");
|
|
50
|
-
return {
|
|
51
|
-
viewingPublicKey: hexlify(decoded.viewingPublicKey),
|
|
52
|
-
clueKey: hexlify(decoded.clueKey)
|
|
53
|
-
};
|
|
67
|
+
var word = (v) => toBeHex(typeof v === "bigint" ? v : BigInt(v), 32);
|
|
68
|
+
function dense(v, name) {
|
|
69
|
+
if (!Array.isArray(v)) throw new Error(`${name} must be array`);
|
|
70
|
+
for (let i = 0; i < v.length; i++)
|
|
71
|
+
if (!Object.hasOwn(v, i) || v[i] === void 0 || v[i] === null)
|
|
72
|
+
throw new Error(`${name} has missing or sparse entries`);
|
|
73
|
+
return v;
|
|
54
74
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (BigInt(registryAddress) === 0n) throw new Error("Registry address must be nonzero");
|
|
59
|
-
if (!/^0x(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$/.test(registration.ownerAddress))
|
|
60
|
-
throw new Error("Wallet identity must be an EVM owner or padded bytes32");
|
|
61
|
-
const ownerAddress = decodeAddress(
|
|
62
|
-
ReceivingAddress.fromIdentity(
|
|
63
|
-
toBeHex(BigInt(registration.ownerAddress), 20)
|
|
64
|
-
).toString()
|
|
65
|
-
).ownerAddress;
|
|
66
|
-
const keys = normalizeKeys(ownerAddress, registration.keys);
|
|
67
|
-
const nonce = uint64(registration.nonce, "nonce"), deadline = uint64(registration.deadline, "deadline");
|
|
68
|
-
if (nonce === MAX_NONCE) throw new Error("Registry nonce exhausted");
|
|
69
|
-
if (options.update !== void 0 && typeof options.update !== "boolean")
|
|
70
|
-
throw new TypeError("update must be boolean");
|
|
71
|
-
if (!options.update && nonce !== 0n)
|
|
72
|
-
throw new Error("Initial registration requires nonce zero");
|
|
73
|
-
if (options.expectedNonce !== void 0 && uint64(options.expectedNonce, "expectedNonce") !== nonce)
|
|
74
|
-
throw new Error("Registry nonce mismatch");
|
|
75
|
-
if (context.now !== void 0 && deadline < uint64(context.now, "now"))
|
|
76
|
-
throw new Error("Registration expired");
|
|
77
|
-
return { registryAddress, registration: { ownerAddress, keys, nonce, deadline } };
|
|
75
|
+
function known(v, keys, name = "params") {
|
|
76
|
+
if (!v || typeof v !== "object" || Array.isArray(v) || Object.keys(v).some((k) => !keys.includes(k)))
|
|
77
|
+
throw new Error(`${name} has unsupported fields`);
|
|
78
78
|
}
|
|
79
|
-
function
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
name: "InvisiAddressRegistry",
|
|
85
|
-
version: "1",
|
|
86
|
-
chainId: context.chainId,
|
|
87
|
-
verifyingContract: registryAddress
|
|
88
|
-
},
|
|
89
|
-
primaryType,
|
|
90
|
-
types: {
|
|
91
|
-
[primaryType]: [
|
|
92
|
-
{ name: "walletIdentity", type: "bytes32" },
|
|
93
|
-
{ name: "viewingPublicKey", type: "bytes" },
|
|
94
|
-
{ name: "clueKey", type: "bytes32" },
|
|
95
|
-
{ name: "nonce", type: "uint64" },
|
|
96
|
-
{ name: "deadline", type: "uint64" }
|
|
97
|
-
]
|
|
98
|
-
},
|
|
99
|
-
message: {
|
|
100
|
-
walletIdentity: toBeHex(BigInt(r.ownerAddress), 32),
|
|
101
|
-
...r.keys,
|
|
102
|
-
nonce: r.nonce,
|
|
103
|
-
deadline: r.deadline
|
|
104
|
-
}
|
|
105
|
-
};
|
|
79
|
+
function notes(v, name) {
|
|
80
|
+
const r = dense(v, name).map((v2) => word(field(v2, name)));
|
|
81
|
+
if (r.some((v2) => BigInt(v2) === 0n) || new Set(r).size !== r.length)
|
|
82
|
+
throw new Error(`duplicate or zero ${name}`);
|
|
83
|
+
return r;
|
|
106
84
|
}
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
85
|
+
function envelope(v, length, version, maximum = length) {
|
|
86
|
+
if (!isHexString(v) || !/^0x(?:[a-fA-F0-9]{2})*$/.test(v) || dataLength(v) < length || dataLength(v) > maximum)
|
|
87
|
+
throw new Error(`ciphertext must be ${length} bytes`);
|
|
88
|
+
if (version !== void 0 && Number.parseInt(v.slice(2, 4), 16) !== version)
|
|
89
|
+
throw new Error("unsupported ciphertext envelope version");
|
|
90
|
+
return v;
|
|
110
91
|
}
|
|
111
|
-
function
|
|
112
|
-
const normalized = normalize(context, registration, options);
|
|
113
|
-
if (typeof signature !== "string" || !isHexString(signature) || signature.length % 2 !== 0)
|
|
114
|
-
throw new Error("Registration signature must be hexadecimal bytes");
|
|
92
|
+
function normalizeContext(c) {
|
|
115
93
|
return {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
[
|
|
121
|
-
{
|
|
122
|
-
// The registry wire schema retains its deployed EIP-712/ABI field name.
|
|
123
|
-
walletIdentity: toBeHex(BigInt(normalized.registration.ownerAddress), 32),
|
|
124
|
-
keys: normalized.registration.keys,
|
|
125
|
-
nonce: normalized.registration.nonce,
|
|
126
|
-
deadline: normalized.registration.deadline
|
|
127
|
-
},
|
|
128
|
-
signature
|
|
129
|
-
]
|
|
130
|
-
)
|
|
94
|
+
...c,
|
|
95
|
+
chainId: uint(c.chainId, 64, "chainId"),
|
|
96
|
+
protocolAddress: getAddress(c.protocolAddress),
|
|
97
|
+
tokenAddress: getAddress(c.tokenAddress)
|
|
131
98
|
};
|
|
132
99
|
}
|
|
133
|
-
function
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
136
|
-
return
|
|
137
|
-
const identity = decodeAddress(
|
|
138
|
-
ReceivingAddress.fromIdentity(ownerAddress).toString()
|
|
139
|
-
).ownerAddress;
|
|
140
|
-
const [keys, nonce] = await new Contract(
|
|
141
|
-
address,
|
|
142
|
-
invisiAddressRegistryAbi,
|
|
143
|
-
client
|
|
144
|
-
).getFunction("receivingKeysOf")(toBeHex(BigInt(identity), 32));
|
|
145
|
-
uint64(nonce, "registry nonce");
|
|
146
|
-
const normalized = normalizeKeys(identity, keys);
|
|
147
|
-
return {
|
|
148
|
-
viewingPublicKey: getBytes(normalized.viewingPublicKey),
|
|
149
|
-
clueKey: getBytes(normalized.clueKey)
|
|
150
|
-
};
|
|
151
|
-
};
|
|
100
|
+
function checkDeadline(c, value) {
|
|
101
|
+
const n = uint(value, 64, "deadline");
|
|
102
|
+
if (c.now !== void 0 && n < uint(c.now, 64, "now")) throw new Error("deadline has expired");
|
|
103
|
+
return n;
|
|
152
104
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
105
|
+
function normalizeSources(sources, inputCount, capacity) {
|
|
106
|
+
dense(sources, "sources");
|
|
107
|
+
if (inputCount < 0 || inputCount > capacity || sources.length > inputCount || (inputCount === 0 ? sources.length !== 0 : sources.length === 0))
|
|
108
|
+
throw new Error("source count must be 1..inputCount, or empty for zero inputs");
|
|
109
|
+
let previous = 0n;
|
|
110
|
+
return sources.map((s) => {
|
|
111
|
+
known(s, ["treeId", "merkleRoot"], "source");
|
|
112
|
+
const treeId = uint(s.treeId, 64, "treeId"), merkleRoot = word(field(s.merkleRoot, "merkleRoot"));
|
|
113
|
+
if (treeId === 0n || treeId <= previous || BigInt(merkleRoot) === 0n)
|
|
114
|
+
throw new Error("sources must have positive roots and strictly ascending tree IDs");
|
|
115
|
+
previous = treeId;
|
|
116
|
+
return { treeId, merkleRoot };
|
|
117
|
+
});
|
|
159
118
|
}
|
|
160
|
-
var
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
this.#runtime = { ...runtime };
|
|
171
|
-
this.#plan = structuredClone(plan);
|
|
172
|
-
this.status = status;
|
|
173
|
-
this.#signature = signature;
|
|
174
|
-
this.#hash = hash;
|
|
175
|
-
this.#anchor = anchor ? { ...anchor } : void 0;
|
|
176
|
-
Object.freeze(this);
|
|
119
|
+
var pad = (v, n) => [
|
|
120
|
+
...v,
|
|
121
|
+
...Array(n - v.length).fill(0n)
|
|
122
|
+
];
|
|
123
|
+
function normalizeOpaqueDelivery(value, options = {}) {
|
|
124
|
+
known(value, ["ciphertext", "clue"], "delivery");
|
|
125
|
+
if (options.empty) {
|
|
126
|
+
if (value.ciphertext !== "0x" || value.clue !== "0x")
|
|
127
|
+
throw new Error("absent output requires an empty delivery");
|
|
128
|
+
return { ciphertext: "0x", clue: "0x" };
|
|
177
129
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
check(
|
|
195
|
-
update ? nonce > 0n : nonce === 0n,
|
|
196
|
-
update ? "wallet is not registered" : "wallet is already registered; prepare an update"
|
|
197
|
-
);
|
|
198
|
-
const address = ReceivingAddress2.parse(await registry.resolve(runtime.wallet.getAddress()));
|
|
199
|
-
const keys = address.getReceivingKeys();
|
|
200
|
-
const registration = {
|
|
201
|
-
ownerAddress: runtime.wallet.ownerAddress,
|
|
202
|
-
keys: {
|
|
203
|
-
viewingPublicKey: hexlify2(keys.viewingPublicKey),
|
|
204
|
-
clueKey: hexlify2(keys.clueKey)
|
|
205
|
-
},
|
|
206
|
-
nonce,
|
|
207
|
-
deadline: options.deadline ?? block.timestamp + 300n
|
|
208
|
-
};
|
|
209
|
-
registrationTypedData(context, registration, { update, expectedNonce: nonce });
|
|
210
|
-
check(
|
|
211
|
-
(await runtime.reader.getBlock(block.number)).hash === block.hash,
|
|
212
|
-
"canonical registry block changed while preparing"
|
|
213
|
-
);
|
|
214
|
-
return new _RegistrationOperation(
|
|
215
|
-
registry,
|
|
216
|
-
runtime,
|
|
217
|
-
{ id: crypto.randomUUID(), context, registration, update },
|
|
218
|
-
"prepared"
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
get id() {
|
|
222
|
-
return this.#plan.id;
|
|
223
|
-
}
|
|
224
|
-
preview() {
|
|
225
|
-
return {
|
|
226
|
-
id: this.id,
|
|
227
|
-
status: this.status,
|
|
228
|
-
chainId: this.#plan.context.chainId,
|
|
229
|
-
registryAddress: this.#plan.context.registryAddress,
|
|
230
|
-
...structuredClone(this.#plan.registration),
|
|
231
|
-
update: this.#plan.update
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
/** Export portable EIP-712 data for external signing. */
|
|
235
|
-
async exportRegistrationAuthorization(options = {}) {
|
|
236
|
-
check(this.status === "prepared", "registry operation is not awaiting authorization");
|
|
237
|
-
options.signal?.throwIfAborted();
|
|
238
|
-
await this.validateFreshness();
|
|
239
|
-
options.signal?.throwIfAborted();
|
|
240
|
-
return createSigningRequest(this.typedData());
|
|
241
|
-
}
|
|
242
|
-
/** Validate and attach an independently obtained EOA or ERC-1271 signature. */
|
|
243
|
-
async withSignature(signature, options = {}) {
|
|
244
|
-
check(this.status === "prepared", "registry operation is not awaiting authorization");
|
|
245
|
-
options.signal?.throwIfAborted();
|
|
246
|
-
await this.validateFreshness();
|
|
247
|
-
options.signal?.throwIfAborted();
|
|
248
|
-
check(
|
|
249
|
-
/^0x(?:[a-fA-F0-9]{2})+$/.test(signature),
|
|
250
|
-
"registry signature must be nonempty hexadecimal bytes"
|
|
251
|
-
);
|
|
252
|
-
const p = this.#plan;
|
|
253
|
-
const owner = p.registration.ownerAddress;
|
|
254
|
-
let valid = false;
|
|
255
|
-
try {
|
|
256
|
-
const bytes = getBytes2(signature);
|
|
257
|
-
const typed = this.typedData();
|
|
258
|
-
valid = bytes.length === 65 && (bytes[64] === 27 || bytes[64] === 28) && BigInt(verifyTypedData(typed.domain, typed.types, typed.message, signature)) === BigInt(owner);
|
|
259
|
-
} catch {
|
|
260
|
-
valid = false;
|
|
261
|
-
}
|
|
262
|
-
if (!valid) {
|
|
263
|
-
const code = await this.#runtime.reader.getCode(owner);
|
|
264
|
-
check(/^0x(?:[a-fA-F0-9]{2})*$/.test(code), "invalid owner contract code");
|
|
265
|
-
check(code !== "0x", "invalid EOA registry signature");
|
|
266
|
-
const verify = this.#runtime.reader.isValidRegistrationSignature;
|
|
267
|
-
check(verify, "ERC-1271 registry signature verification capability is missing");
|
|
268
|
-
check(
|
|
269
|
-
await verify.call(
|
|
270
|
-
this.#runtime.reader,
|
|
271
|
-
owner,
|
|
272
|
-
registrationDigest(p.context, p.registration, { update: p.update }),
|
|
273
|
-
signature
|
|
274
|
-
),
|
|
275
|
-
"invalid ERC-1271 registry signature"
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
options.signal?.throwIfAborted();
|
|
279
|
-
return new _RegistrationOperation(
|
|
280
|
-
this.#registry,
|
|
281
|
-
this.#runtime,
|
|
282
|
-
this.#plan,
|
|
283
|
-
"ready",
|
|
284
|
-
signature
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
/** Empty signatures require the transaction sender to be the wallet owner. */
|
|
288
|
-
async authorizeDirect() {
|
|
289
|
-
check(this.status === "prepared", "registry operation is not awaiting authorization");
|
|
290
|
-
await this.validateFreshness();
|
|
291
|
-
return new _RegistrationOperation(this.#registry, this.#runtime, this.#plan, "ready", "0x");
|
|
292
|
-
}
|
|
293
|
-
typedData() {
|
|
294
|
-
const p = this.#plan;
|
|
295
|
-
return registrationTypedData(p.context, p.registration, {
|
|
296
|
-
update: p.update,
|
|
297
|
-
expectedNonce: p.registration.nonce
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
call() {
|
|
301
|
-
check(this.#signature !== void 0, "registry signature is missing");
|
|
302
|
-
const p = this.#plan;
|
|
303
|
-
const result = buildRegistrationCalldata(p.context, p.registration, this.#signature, {
|
|
304
|
-
update: p.update,
|
|
305
|
-
expectedNonce: p.registration.nonce
|
|
306
|
-
});
|
|
307
|
-
return { to: result.to, data: result.callData, value: result.value };
|
|
308
|
-
}
|
|
309
|
-
async exportTransaction() {
|
|
310
|
-
check(
|
|
311
|
-
this.#signature !== void 0 && this.status !== "prepared",
|
|
312
|
-
"registry operation is not ready"
|
|
313
|
-
);
|
|
314
|
-
await this.validateFreshness();
|
|
315
|
-
return this.call();
|
|
316
|
-
}
|
|
317
|
-
async validateFreshness() {
|
|
318
|
-
await this.#registry.validateDeployment(this.#runtime.reader);
|
|
319
|
-
const block = await this.#runtime.reader.getBlock(
|
|
320
|
-
await this.#runtime.reader.getLatestBlockNumber()
|
|
321
|
-
);
|
|
322
|
-
const p = this.#plan;
|
|
323
|
-
check(block.timestamp <= p.registration.deadline, "registry operation has expired");
|
|
324
|
-
check(
|
|
325
|
-
await this.#runtime.reader.getRegistryNonce(
|
|
326
|
-
p.context,
|
|
327
|
-
p.registration.ownerAddress,
|
|
328
|
-
block
|
|
329
|
-
) === p.registration.nonce,
|
|
330
|
-
"registry nonce changed; prepare again"
|
|
331
|
-
);
|
|
332
|
-
check(
|
|
333
|
-
(await this.#runtime.reader.getBlock(block.number)).hash === block.hash,
|
|
334
|
-
"canonical registry block changed during authorization"
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
/** Associate a signed/broadcast transaction; ethers owns sending and receipt waiting. */
|
|
338
|
-
trackTransaction(transactionHash) {
|
|
339
|
-
check(/^0x[a-fA-F0-9]{64}$/.test(transactionHash), "invalid registry transaction hash");
|
|
340
|
-
check(
|
|
341
|
-
this.#signature !== void 0 && ["ready", "uncertain"].includes(this.status),
|
|
342
|
-
"registry operation is not ready"
|
|
343
|
-
);
|
|
344
|
-
return new _RegistrationOperation(
|
|
345
|
-
this.#registry,
|
|
346
|
-
this.#runtime,
|
|
347
|
-
this.#plan,
|
|
348
|
-
"uncertain",
|
|
349
|
-
this.#signature,
|
|
350
|
-
transactionHash
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
async reconcile(options = {}) {
|
|
354
|
-
options.signal?.throwIfAborted();
|
|
355
|
-
const { reader } = this.#runtime;
|
|
356
|
-
const p = this.#plan;
|
|
357
|
-
await this.#registry.validateDeployment(reader);
|
|
358
|
-
if (this.#anchor && (await reader.getBlock(this.#anchor.number)).hash !== this.#anchor.hash) {
|
|
359
|
-
return new _RegistrationOperation(
|
|
360
|
-
this.#registry,
|
|
361
|
-
this.#runtime,
|
|
362
|
-
p,
|
|
363
|
-
"reorged",
|
|
364
|
-
this.#signature,
|
|
365
|
-
this.#hash
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
const hash = options.transactionHash ?? this.#hash;
|
|
369
|
-
const receipt = hash ? await reader.getReceipt(hash) : null;
|
|
370
|
-
if (receipt) {
|
|
371
|
-
check(
|
|
372
|
-
receipt.transactionHash === hash && (await reader.getBlock(receipt.block.number)).hash === receipt.block.hash,
|
|
373
|
-
"registry receipt is not canonical"
|
|
374
|
-
);
|
|
375
|
-
const transaction = await reader.getTransaction(hash);
|
|
376
|
-
const call = this.call();
|
|
377
|
-
check(
|
|
378
|
-
transaction && transaction.transactionHash === hash && transaction.to.toLowerCase() === call.to.toLowerCase() && transaction.data.toLowerCase() === call.data.toLowerCase() && transaction.value === 0n && (this.#signature !== "0x" || BigInt(transaction.from) === BigInt(p.registration.ownerAddress)),
|
|
379
|
-
"registry transaction differs from frozen request"
|
|
380
|
-
);
|
|
381
|
-
check(
|
|
382
|
-
receipt.status === "success" || receipt.status === "reverted",
|
|
383
|
-
"registry execution status is missing"
|
|
384
|
-
);
|
|
385
|
-
if (receipt.status === "success") {
|
|
386
|
-
const found = receipt.logs.some((log) => {
|
|
387
|
-
if (log.address.toLowerCase() !== p.context.registryAddress.toLowerCase() || log.transactionHash !== hash) {
|
|
388
|
-
return false;
|
|
389
|
-
}
|
|
390
|
-
try {
|
|
391
|
-
const event = ((decoded) => ({
|
|
392
|
-
eventName: decoded.name,
|
|
393
|
-
args: decoded.args.toObject()
|
|
394
|
-
}))(
|
|
395
|
-
new Interface2(invisiAddressRegistryAbi).parseLog({
|
|
396
|
-
data: log.data,
|
|
397
|
-
topics: log.topics
|
|
398
|
-
})
|
|
399
|
-
);
|
|
400
|
-
return event.eventName === "ReceivingKeysSet" && BigInt(event.args.walletIdentity) === BigInt(p.registration.ownerAddress) && event.args.viewingPublicKey.toLowerCase() === p.registration.keys.viewingPublicKey.toLowerCase() && event.args.clueKey.toLowerCase() === p.registration.keys.clueKey.toLowerCase() && event.args.nextNonce === p.registration.nonce + 1n;
|
|
401
|
-
} catch {
|
|
402
|
-
return false;
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
check(found, "registry receipt has no matching key update event");
|
|
406
|
-
check(
|
|
407
|
-
(await reader.getBlock(receipt.block.number)).hash === receipt.block.hash,
|
|
408
|
-
"registry block changed during confirmation"
|
|
409
|
-
);
|
|
410
|
-
options.signal?.throwIfAborted();
|
|
411
|
-
return new _RegistrationOperation(
|
|
412
|
-
this.#registry,
|
|
413
|
-
this.#runtime,
|
|
414
|
-
p,
|
|
415
|
-
"confirmed",
|
|
416
|
-
this.#signature,
|
|
417
|
-
hash,
|
|
418
|
-
receipt.block
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
const head = await reader.getBlock(await reader.getLatestBlockNumber());
|
|
423
|
-
const nonce = await reader.getRegistryNonce(p.context, p.registration.ownerAddress, head);
|
|
424
|
-
check(
|
|
425
|
-
(await reader.getBlock(head.number)).hash === head.hash,
|
|
426
|
-
"canonical registry block changed during reconciliation"
|
|
427
|
-
);
|
|
428
|
-
return new _RegistrationOperation(
|
|
429
|
-
this.#registry,
|
|
430
|
-
this.#runtime,
|
|
431
|
-
p,
|
|
432
|
-
head.timestamp > p.registration.deadline ? "expired" : nonce !== p.registration.nonce ? "failed" : "uncertain",
|
|
433
|
-
this.#signature,
|
|
434
|
-
hash
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
toJSON() {
|
|
438
|
-
return {
|
|
439
|
-
id: this.id,
|
|
440
|
-
kind: this.#plan.update ? "update-receiving-keys" : "register-receiving-keys",
|
|
441
|
-
status: this.status
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
445
|
-
return this.toJSON();
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
// src/AddressRegistry.ts
|
|
450
|
-
var AddressRegistry = class {
|
|
451
|
-
context;
|
|
452
|
-
#lookup;
|
|
453
|
-
expectedRuntimeCodeHash;
|
|
454
|
-
constructor(context, options = {}) {
|
|
455
|
-
if (typeof context.chainId !== "bigint" || context.chainId < 0n || context.chainId >= 1n << 64n || BigInt(getAddress2(context.registryAddress)) === 0n) {
|
|
456
|
-
throw new Error("invalid registry context");
|
|
457
|
-
}
|
|
458
|
-
this.context = Object.freeze({
|
|
459
|
-
...context,
|
|
460
|
-
registryAddress: getAddress2(context.registryAddress)
|
|
461
|
-
});
|
|
462
|
-
this.#lookup = options.lookup;
|
|
463
|
-
this.expectedRuntimeCodeHash = options.expectedRuntimeCodeHash?.toLowerCase();
|
|
464
|
-
if (this.expectedRuntimeCodeHash && !/^0x[0-9a-f]{64}$/.test(this.expectedRuntimeCodeHash)) {
|
|
465
|
-
throw new Error("invalid registry runtime profile");
|
|
466
|
-
}
|
|
467
|
-
Object.freeze(this);
|
|
468
|
-
}
|
|
469
|
-
async validateDeployment(reader) {
|
|
470
|
-
if (!this.expectedRuntimeCodeHash) {
|
|
471
|
-
throw new MissingCapabilityError("trusted registry deployment profile");
|
|
472
|
-
}
|
|
473
|
-
if (await reader.getChainId() !== this.context.chainId) {
|
|
474
|
-
throw new Error("registry chain mismatch");
|
|
475
|
-
}
|
|
476
|
-
const code = await reader.getCode(this.context.registryAddress);
|
|
477
|
-
if (!/^0x(?:[0-9a-fA-F]{2})+$/.test(code) || keccak256(code) !== this.expectedRuntimeCodeHash) {
|
|
478
|
-
throw new Error("registry runtime profile mismatch");
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
prepareRegistration(options) {
|
|
482
|
-
return RegistrationOperation.prepare(this, options);
|
|
483
|
-
}
|
|
484
|
-
async resolve(address) {
|
|
485
|
-
const receiving = ReceivingAddress3.parse(address);
|
|
486
|
-
if (receiving.kind === "short" && !this.#lookup) {
|
|
487
|
-
throw new MissingCapabilityError("registry reader");
|
|
488
|
-
}
|
|
489
|
-
return (await receiving.resolve(this.#lookup)).toString();
|
|
490
|
-
}
|
|
491
|
-
typedData(registration, options) {
|
|
492
|
-
return registrationTypedData(this.context, registration, options);
|
|
493
|
-
}
|
|
494
|
-
digest(registration, options) {
|
|
495
|
-
return registrationDigest(this.context, registration, options);
|
|
496
|
-
}
|
|
497
|
-
buildCall(registration, signature, options) {
|
|
498
|
-
return buildRegistrationCalldata(this.context, registration, signature, options);
|
|
499
|
-
}
|
|
500
|
-
};
|
|
501
|
-
|
|
502
|
-
// src/fee.ts
|
|
503
|
-
import { getAddress as getAddress4 } from "ethers";
|
|
504
|
-
|
|
505
|
-
// src/validation.ts
|
|
506
|
-
import { MAX_CIPHERTEXT_BYTES } from "@invisi-labs/core";
|
|
507
|
-
import {
|
|
508
|
-
AbiCoder,
|
|
509
|
-
dataLength,
|
|
510
|
-
getAddress as getAddress3,
|
|
511
|
-
isHexString as isHexString2,
|
|
512
|
-
keccak256 as keccak2562,
|
|
513
|
-
ParamType,
|
|
514
|
-
toBeHex as toBeHex2
|
|
515
|
-
} from "ethers";
|
|
516
|
-
var FIELD_MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
517
|
-
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
518
|
-
var ZERO_WORD = toBeHex2(0n, 32);
|
|
519
|
-
var EMPTY_PERMIT = {
|
|
520
|
-
deadline: 0n,
|
|
521
|
-
v: 0,
|
|
522
|
-
r: ZERO_WORD,
|
|
523
|
-
s: ZERO_WORD
|
|
524
|
-
};
|
|
525
|
-
var EMPTY_FUNDING = {
|
|
526
|
-
payer: ZERO_ADDRESS,
|
|
527
|
-
amount: 0n,
|
|
528
|
-
sigNonce: 0n,
|
|
529
|
-
deadline: 0n,
|
|
530
|
-
signature: "0x",
|
|
531
|
-
permit: EMPTY_PERMIT
|
|
532
|
-
};
|
|
533
|
-
function uint(v, bits, name = "value") {
|
|
534
|
-
if (typeof v !== "bigint" && typeof v !== "number" && typeof v !== "string" || typeof v === "number" && !Number.isSafeInteger(v))
|
|
535
|
-
throw new Error(`${name} must be an integer`);
|
|
536
|
-
const n = BigInt(v);
|
|
537
|
-
if (n < 0n || n >= 1n << BigInt(bits)) throw new Error(`${name} exceeds u${bits}`);
|
|
538
|
-
return n;
|
|
539
|
-
}
|
|
540
|
-
function field(v, name = "field") {
|
|
541
|
-
const n = uint(v, 256, name);
|
|
542
|
-
if (n >= FIELD_MODULUS) throw new Error(`${name} is noncanonical field`);
|
|
543
|
-
return n;
|
|
544
|
-
}
|
|
545
|
-
var word = (v) => toBeHex2(typeof v === "bigint" ? v : BigInt(v), 32);
|
|
546
|
-
function dense(v, name) {
|
|
547
|
-
if (!Array.isArray(v)) throw new Error(`${name} must be array`);
|
|
548
|
-
for (let i = 0; i < v.length; i++)
|
|
549
|
-
if (!Object.hasOwn(v, i) || v[i] === void 0 || v[i] === null)
|
|
550
|
-
throw new Error(`${name} has missing or sparse entries`);
|
|
551
|
-
return v;
|
|
552
|
-
}
|
|
553
|
-
function known(v, keys, name = "params") {
|
|
554
|
-
if (!v || typeof v !== "object" || Array.isArray(v) || Object.keys(v).some((k) => !keys.includes(k)))
|
|
555
|
-
throw new Error(`${name} has unsupported fields`);
|
|
556
|
-
}
|
|
557
|
-
function notes(v, name) {
|
|
558
|
-
const r = dense(v, name).map((v2) => word(field(v2, name)));
|
|
559
|
-
if (r.some((v2) => BigInt(v2) === 0n) || new Set(r).size !== r.length)
|
|
560
|
-
throw new Error(`duplicate or zero ${name}`);
|
|
561
|
-
return r;
|
|
562
|
-
}
|
|
563
|
-
function envelope(v, length, version, maximum = length) {
|
|
564
|
-
if (!isHexString2(v) || !/^0x(?:[a-fA-F0-9]{2})*$/.test(v) || dataLength(v) < length || dataLength(v) > maximum)
|
|
565
|
-
throw new Error(`ciphertext must be ${length} bytes`);
|
|
566
|
-
if (version !== void 0 && Number.parseInt(v.slice(2, 4), 16) !== version)
|
|
567
|
-
throw new Error("unsupported ciphertext envelope version");
|
|
568
|
-
return v;
|
|
569
|
-
}
|
|
570
|
-
function normalizeContext(c) {
|
|
571
|
-
return {
|
|
572
|
-
...c,
|
|
573
|
-
chainId: uint(c.chainId, 64, "chainId"),
|
|
574
|
-
protocolAddress: getAddress3(c.protocolAddress),
|
|
575
|
-
tokenAddress: getAddress3(c.tokenAddress)
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
function checkDeadline(c, value) {
|
|
579
|
-
const n = uint(value, 64, "deadline");
|
|
580
|
-
if (c.now !== void 0 && n < uint(c.now, 64, "now")) throw new Error("deadline has expired");
|
|
581
|
-
return n;
|
|
582
|
-
}
|
|
583
|
-
function normalizeSources(sources, inputCount, capacity) {
|
|
584
|
-
dense(sources, "sources");
|
|
585
|
-
if (inputCount < 0 || inputCount > capacity || sources.length > inputCount || (inputCount === 0 ? sources.length !== 0 : sources.length === 0))
|
|
586
|
-
throw new Error("source count must be 1..inputCount, or empty for zero inputs");
|
|
587
|
-
let previous = 0n;
|
|
588
|
-
return sources.map((s) => {
|
|
589
|
-
known(s, ["treeId", "merkleRoot"], "source");
|
|
590
|
-
const treeId = uint(s.treeId, 64, "treeId"), merkleRoot = word(field(s.merkleRoot, "merkleRoot"));
|
|
591
|
-
if (treeId === 0n || treeId <= previous || BigInt(merkleRoot) === 0n)
|
|
592
|
-
throw new Error("sources must have positive roots and strictly ascending tree IDs");
|
|
593
|
-
previous = treeId;
|
|
594
|
-
return { treeId, merkleRoot };
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
var pad = (v, n) => [
|
|
598
|
-
...v,
|
|
599
|
-
...Array(n - v.length).fill(0n)
|
|
600
|
-
];
|
|
601
|
-
function normalizeOpaqueDelivery(value, options = {}) {
|
|
602
|
-
known(value, ["ciphertext", "clue"], "delivery");
|
|
603
|
-
if (options.empty) {
|
|
604
|
-
if (value.ciphertext !== "0x" || value.clue !== "0x")
|
|
605
|
-
throw new Error("absent output requires an empty delivery");
|
|
606
|
-
return { ciphertext: "0x", clue: "0x" };
|
|
607
|
-
}
|
|
608
|
-
if (options.allowEmpty && value.ciphertext === "0x" && value.clue === "0x")
|
|
609
|
-
return { ciphertext: "0x", clue: "0x" };
|
|
610
|
-
const ciphertext = envelope(value.ciphertext, 1, void 0, MAX_CIPHERTEXT_BYTES);
|
|
611
|
-
const clue = envelope(value.clue, 68);
|
|
612
|
-
if (clue.slice(130, 132).toLowerCase() !== "18") throw new Error("clue precision must be 24");
|
|
613
|
-
return { ciphertext, clue };
|
|
614
|
-
}
|
|
615
|
-
var deliveriesHash = (v) => keccak2562(
|
|
616
|
-
AbiCoder.defaultAbiCoder().encode(
|
|
617
|
-
[
|
|
618
|
-
{
|
|
619
|
-
type: "tuple[]",
|
|
620
|
-
components: [
|
|
621
|
-
{ name: "ciphertext", type: "bytes" },
|
|
622
|
-
{ name: "clue", type: "bytes" }
|
|
623
|
-
]
|
|
130
|
+
if (options.allowEmpty && value.ciphertext === "0x" && value.clue === "0x")
|
|
131
|
+
return { ciphertext: "0x", clue: "0x" };
|
|
132
|
+
const ciphertext = envelope(value.ciphertext, 1, void 0, MAX_CIPHERTEXT_BYTES);
|
|
133
|
+
const clue = envelope(value.clue, 68);
|
|
134
|
+
if (clue.slice(130, 132).toLowerCase() !== "18") throw new Error("clue precision must be 24");
|
|
135
|
+
return { ciphertext, clue };
|
|
136
|
+
}
|
|
137
|
+
var deliveriesHash = (v) => keccak256(
|
|
138
|
+
AbiCoder.defaultAbiCoder().encode(
|
|
139
|
+
[
|
|
140
|
+
{
|
|
141
|
+
type: "tuple[]",
|
|
142
|
+
components: [
|
|
143
|
+
{ name: "ciphertext", type: "bytes" },
|
|
144
|
+
{ name: "clue", type: "bytes" }
|
|
145
|
+
]
|
|
624
146
|
}
|
|
625
147
|
].map((parameter) => ParamType.from(parameter)),
|
|
626
148
|
[dense(v, "deliveries")]
|
|
@@ -628,34 +150,34 @@ var deliveriesHash = (v) => keccak2562(
|
|
|
628
150
|
);
|
|
629
151
|
function normalizeFunding(input = {}) {
|
|
630
152
|
const f = { ...EMPTY_FUNDING, ...input };
|
|
631
|
-
|
|
153
|
+
getAddress(f.payer);
|
|
632
154
|
uint(f.amount, 128, "depositAmount");
|
|
633
155
|
uint(f.sigNonce, 256, "sigNonce");
|
|
634
156
|
uint(f.deadline, 256, "funding deadline");
|
|
635
|
-
if (!
|
|
157
|
+
if (!isHexString(f.signature) || !/^0x(?:[a-fA-F0-9]{2})*$/.test(f.signature))
|
|
636
158
|
throw new Error("invalid signature bytes");
|
|
637
159
|
const p = f.permit;
|
|
638
160
|
uint(p.deadline, 256);
|
|
639
161
|
uint(p.v, 8);
|
|
640
162
|
for (const v of [p.r, p.s])
|
|
641
|
-
if (!
|
|
163
|
+
if (!isHexString(v) || dataLength(v) !== 32) throw new Error("permit r/s must be bytes32");
|
|
642
164
|
if (f.amount === 0n !== (BigInt(f.payer) === 0n))
|
|
643
165
|
throw new Error("deposit amount and payer must both be zero or positive");
|
|
644
166
|
if (f.amount === 0n && (f.sigNonce !== 0n || f.deadline !== 0n || f.signature !== "0x" || p.deadline !== 0n || p.v !== 0 || BigInt(p.r) !== 0n || BigInt(p.s) !== 0n))
|
|
645
167
|
throw new Error("zero funding must be entirely empty");
|
|
646
|
-
return { ...f, payer:
|
|
168
|
+
return { ...f, payer: getAddress(f.payer) };
|
|
647
169
|
}
|
|
648
170
|
function validateFundingSubmission(f, caller, now) {
|
|
649
171
|
if (f.signature !== "0x" && now !== void 0 && uint(now, 64, "now") > f.deadline)
|
|
650
172
|
throw new Error("funding authorization has expired");
|
|
651
|
-
if (f.signature === "0x" && (f.sigNonce !== 0n || f.deadline !== 0n || caller !== void 0 && f.amount > 0n &&
|
|
173
|
+
if (f.signature === "0x" && (f.sigNonce !== 0n || f.deadline !== 0n || caller !== void 0 && f.amount > 0n && getAddress(caller) !== f.payer))
|
|
652
174
|
throw new Error("direct funding requires payer caller and zero nonce/deadline");
|
|
653
175
|
}
|
|
654
176
|
var getEIP712Domain = (c) => ({
|
|
655
177
|
name: "InvisibleProtocol",
|
|
656
178
|
version: "1",
|
|
657
179
|
chainId: uint(c.chainId, 64),
|
|
658
|
-
verifyingContract:
|
|
180
|
+
verifyingContract: getAddress(c.protocolAddress)
|
|
659
181
|
});
|
|
660
182
|
var fundingFields = [
|
|
661
183
|
{ name: "requestHash", type: "bytes32" },
|
|
@@ -665,1075 +187,6 @@ var fundingFields = [
|
|
|
665
187
|
{ name: "deadline", type: "uint256" }
|
|
666
188
|
];
|
|
667
189
|
|
|
668
|
-
// src/fee.ts
|
|
669
|
-
function normalizeFee(p) {
|
|
670
|
-
const feeRecipient = getAddress4(p.feeRecipient ?? ZERO_ADDRESS);
|
|
671
|
-
const feeAmount = uint(p.feeAmount ?? 0n, 128, "feeAmount");
|
|
672
|
-
if (BigInt(feeRecipient) === 0n !== (feeAmount === 0n))
|
|
673
|
-
throw new Error("invalid fee context");
|
|
674
|
-
return { feeRecipient, feeAmount };
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// src/credit-create.ts
|
|
678
|
-
import {
|
|
679
|
-
AbiCoder as AbiCoder2,
|
|
680
|
-
dataLength as dataLength2,
|
|
681
|
-
getAddress as getAddress5,
|
|
682
|
-
Interface as Interface3,
|
|
683
|
-
isHexString as isHexString3,
|
|
684
|
-
keccak256 as keccak2563,
|
|
685
|
-
ParamType as ParamType2
|
|
686
|
-
} from "ethers";
|
|
687
|
-
import { computeWalletAuthorizationDigest, walletAuthorizationTypedData } from "@invisi-labs/core";
|
|
688
|
-
var creationTuple = [
|
|
689
|
-
"(address token,(uint256 treeId,bytes32 merkleRoot)[] sources,bytes32[] nullifiers,bytes32 creditCommitment,bytes32 changeCommitment,address owner,uint256 expiry,uint256 deadline,address feeRecipient,uint256 feeAmount)"
|
|
690
|
-
][0];
|
|
691
|
-
var CREATE_CREDIT_FIELDS = [
|
|
692
|
-
"token",
|
|
693
|
-
"sources",
|
|
694
|
-
"nullifiers",
|
|
695
|
-
"creditCommitment",
|
|
696
|
-
"changeCommitment",
|
|
697
|
-
"owner",
|
|
698
|
-
"expiry",
|
|
699
|
-
"deadline",
|
|
700
|
-
"feeRecipient",
|
|
701
|
-
"feeAmount"
|
|
702
|
-
];
|
|
703
|
-
function validateCreateCreditRequest(c, p, cs, funding = {}) {
|
|
704
|
-
known(p, CREATE_CREDIT_FIELDS);
|
|
705
|
-
if (getAddress5(p.token) !== getAddress5(c.tokenAddress))
|
|
706
|
-
throw new Error("request token mismatch");
|
|
707
|
-
const ctx = normalizeContext(c), ns = notes(p.nullifiers, "nullifiers"), sources = normalizeSources(p.sources, ns.length, 10), creditCommitment = word(field(p.creditCommitment)), changeCommitment = word(field(p.changeCommitment)), owner = getAddress5(p.owner), expiry = uint(p.expiry, 64, "expiry"), deadline = checkDeadline(c, p.deadline), f = normalizeFunding(funding);
|
|
708
|
-
if (BigInt(creditCommitment) === 0n || creditCommitment === changeCommitment)
|
|
709
|
-
throw new Error("zero or duplicate commitment");
|
|
710
|
-
if (BigInt(owner) === 0n || expiry === 0n || c.now !== void 0 && expiry <= c.now)
|
|
711
|
-
throw new Error("owner must be nonzero and expiry future");
|
|
712
|
-
if (dense(cs, "deliveries").length !== 3) throw new Error("deliveries count mismatch");
|
|
713
|
-
const deliveries = cs.map(
|
|
714
|
-
(v, i) => normalizeOpaqueDelivery(v, {
|
|
715
|
-
empty: i === 2 && BigInt(changeCommitment) === 0n,
|
|
716
|
-
allowEmpty: i === 2
|
|
717
|
-
})
|
|
718
|
-
);
|
|
719
|
-
if (ns.length === 0 && f.amount === 0n)
|
|
720
|
-
throw new Error("empty inputs require positive funding");
|
|
721
|
-
return {
|
|
722
|
-
...ctx,
|
|
723
|
-
...normalizeFee(p),
|
|
724
|
-
sources,
|
|
725
|
-
nullifiers: ns,
|
|
726
|
-
actualCount: ns.length,
|
|
727
|
-
sourceCount: sources.length,
|
|
728
|
-
creditCommitment,
|
|
729
|
-
changeCommitment,
|
|
730
|
-
owner,
|
|
731
|
-
expiry,
|
|
732
|
-
deadline,
|
|
733
|
-
deliveries,
|
|
734
|
-
funding: f
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
function createCreditBaseInputs(c, p, cs, f = {}) {
|
|
738
|
-
const r = validateCreateCreditRequest(c, p, cs, f), h = BigInt(deliveriesHash(cs));
|
|
739
|
-
return [
|
|
740
|
-
r.chainId,
|
|
741
|
-
BigInt(r.protocolAddress),
|
|
742
|
-
BigInt(r.tokenAddress),
|
|
743
|
-
BigInt(r.owner),
|
|
744
|
-
BigInt(r.actualCount),
|
|
745
|
-
BigInt(r.sourceCount),
|
|
746
|
-
...pad(
|
|
747
|
-
r.sources.map((s) => s.treeId),
|
|
748
|
-
10
|
|
749
|
-
),
|
|
750
|
-
...pad(
|
|
751
|
-
r.sources.map((s) => BigInt(s.merkleRoot)),
|
|
752
|
-
10
|
|
753
|
-
),
|
|
754
|
-
...pad(r.nullifiers.map(BigInt), 10),
|
|
755
|
-
BigInt(r.creditCommitment),
|
|
756
|
-
BigInt(r.changeCommitment),
|
|
757
|
-
h >> 128n,
|
|
758
|
-
h & (1n << 128n) - 1n,
|
|
759
|
-
r.expiry,
|
|
760
|
-
r.deadline,
|
|
761
|
-
r.funding.amount,
|
|
762
|
-
BigInt(r.funding.payer),
|
|
763
|
-
BigInt(r.feeRecipient),
|
|
764
|
-
r.feeAmount
|
|
765
|
-
].map(word);
|
|
766
|
-
}
|
|
767
|
-
function createCreditPublicInputs(c, p, deliveries, funding = {}) {
|
|
768
|
-
const base = createCreditBaseInputs(c, p, deliveries, funding);
|
|
769
|
-
const digest = BigInt(
|
|
770
|
-
computeWalletAuthorizationDigest({
|
|
771
|
-
context: { chainId: c.chainId, protocolAddress: BigInt(c.protocolAddress) },
|
|
772
|
-
operation: "create-credit",
|
|
773
|
-
capacity: 10,
|
|
774
|
-
words: base.map(BigInt)
|
|
775
|
-
})
|
|
776
|
-
);
|
|
777
|
-
return [...base, word(digest >> 128n), word(digest & (1n << 128n) - 1n)];
|
|
778
|
-
}
|
|
779
|
-
var hashContext = {
|
|
780
|
-
chainId: 0n,
|
|
781
|
-
protocolAddress: ZERO_ADDRESS,
|
|
782
|
-
tokenAddress: ZERO_ADDRESS
|
|
783
|
-
};
|
|
784
|
-
function createCreditRequestHash(p, cs, f = {}) {
|
|
785
|
-
const r = validateCreateCreditRequest({ ...hashContext, tokenAddress: p.token }, p, cs, f);
|
|
786
|
-
return keccak2563(
|
|
787
|
-
AbiCoder2.defaultAbiCoder().encode(
|
|
788
|
-
[
|
|
789
|
-
{ type: "bytes32" },
|
|
790
|
-
creationTuple,
|
|
791
|
-
{ type: "bytes32" },
|
|
792
|
-
{ type: "address" },
|
|
793
|
-
{ type: "uint256" }
|
|
794
|
-
].map((parameter) => ParamType2.from(parameter)),
|
|
795
|
-
[
|
|
796
|
-
keccak2563(new TextEncoder().encode("InvisiCreateCredit")),
|
|
797
|
-
{ ...p, ...normalizeFee(p) },
|
|
798
|
-
deliveriesHash(cs),
|
|
799
|
-
r.funding.payer,
|
|
800
|
-
r.funding.amount
|
|
801
|
-
]
|
|
802
|
-
)
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
function encodeCreateCreditTypedData(c, p, deliveries, funding = {}) {
|
|
806
|
-
return walletAuthorizationTypedData({
|
|
807
|
-
context: { chainId: c.chainId, protocolAddress: BigInt(c.protocolAddress) },
|
|
808
|
-
operation: "create-credit",
|
|
809
|
-
capacity: 10,
|
|
810
|
-
words: createCreditBaseInputs(c, p, deliveries, funding).map(BigInt)
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
function encodeCreateCreditFundingTypedData(c, p, cs, f) {
|
|
814
|
-
const r = validateCreateCreditRequest(c, p, cs, f);
|
|
815
|
-
if (r.funding.amount === 0n) throw new Error("funding authorization requires positive deposit");
|
|
816
|
-
return {
|
|
817
|
-
domain: getEIP712Domain(c),
|
|
818
|
-
primaryType: "FundTransact",
|
|
819
|
-
types: { FundTransact: fundingFields },
|
|
820
|
-
message: {
|
|
821
|
-
requestHash: createCreditRequestHash(p, cs, f),
|
|
822
|
-
payer: r.funding.payer,
|
|
823
|
-
amount: r.funding.amount,
|
|
824
|
-
sigNonce: r.funding.sigNonce,
|
|
825
|
-
deadline: r.funding.deadline
|
|
826
|
-
}
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
function buildCreateCreditCalldata(p) {
|
|
830
|
-
const r = validateCreateCreditRequest(p.context, p.params, p.deliveries, p.funding);
|
|
831
|
-
validateFundingSubmission(r.funding, p.caller, p.context.now);
|
|
832
|
-
return {
|
|
833
|
-
data: new Interface3(invisiProtocolAbi).encodeFunctionData("createCredit", [
|
|
834
|
-
encodeCreditProof(p.proof),
|
|
835
|
-
{ ...p.params, ...normalizeFee(p.params) },
|
|
836
|
-
p.deliveries,
|
|
837
|
-
r.funding
|
|
838
|
-
]),
|
|
839
|
-
approveAmount: r.funding.amount
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
function encodeCreditProof(proof) {
|
|
843
|
-
if (!isHexString3(proof) || !/^0x(?:[a-fA-F0-9]{2})*$/.test(proof) || dataLength2(proof) === 0)
|
|
844
|
-
throw new Error("empty or malformed Credit proof");
|
|
845
|
-
return AbiCoder2.defaultAbiCoder().encode(
|
|
846
|
-
["uint8", "bytes"].map((parameter) => ParamType2.from(parameter)),
|
|
847
|
-
[0, proof]
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
// src/credit-lifecycle.ts
|
|
852
|
-
import { AbiCoder as AbiCoder3, getAddress as getAddress6, Interface as Interface4, keccak256 as keccak2564, ParamType as ParamType3 } from "ethers";
|
|
853
|
-
import {
|
|
854
|
-
creditPeriods,
|
|
855
|
-
computeWalletAuthorizationDigest as computeWalletAuthorizationDigest2,
|
|
856
|
-
walletAuthorizationTypedData as walletAuthorizationTypedData2
|
|
857
|
-
} from "@invisi-labs/core";
|
|
858
|
-
var CREDIT_DRAW_CAPACITIES = [1, 12, 50, 120];
|
|
859
|
-
function selectCreditDrawCircuit(count) {
|
|
860
|
-
if (!Number.isInteger(count) || count < 1 || count > 120)
|
|
861
|
-
throw new Error("draw requires 1..120 credits");
|
|
862
|
-
const maxSize = CREDIT_DRAW_CAPACITIES.find((n) => count <= n);
|
|
863
|
-
return {
|
|
864
|
-
name: `credit_draw_slots${maxSize}`,
|
|
865
|
-
maxSize,
|
|
866
|
-
publicInputCount: 6 * maxSize + 14,
|
|
867
|
-
totalBits: 126 + Math.ceil(Math.log2(maxSize))
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
function validateCreditDrawRequest(c, p, cs) {
|
|
871
|
-
known(p, [
|
|
872
|
-
"token",
|
|
873
|
-
"sources",
|
|
874
|
-
"nullifiers",
|
|
875
|
-
"remainderCommitments",
|
|
876
|
-
"expiries",
|
|
877
|
-
"recoveryAmounts",
|
|
878
|
-
"spender",
|
|
879
|
-
"totalAmount",
|
|
880
|
-
"deadline"
|
|
881
|
-
]);
|
|
882
|
-
if (getAddress6(p.token) !== getAddress6(c.tokenAddress))
|
|
883
|
-
throw new Error("request token mismatch");
|
|
884
|
-
const ctx = normalizeContext(c), ns = notes(p.nullifiers, "nullifiers"), profile = selectCreditDrawCircuit(ns.length), sources = normalizeSources(p.sources, ns.length, profile.maxSize);
|
|
885
|
-
for (const [key, v] of Object.entries({
|
|
886
|
-
remainderCommitments: p.remainderCommitments,
|
|
887
|
-
expiries: p.expiries,
|
|
888
|
-
recoveryAmounts: p.recoveryAmounts
|
|
889
|
-
}))
|
|
890
|
-
if (dense(v, key).length !== ns.length) throw new Error(`${key} count mismatch`);
|
|
891
|
-
const remainderCommitments = p.remainderCommitments.map((v) => word(field(v))), recoveryAmounts = p.recoveryAmounts.map((v) => word(field(v))), expiries = p.expiries.map((v) => uint(v, 64, "expiry")), positives = remainderCommitments.filter((v) => BigInt(v) !== 0n);
|
|
892
|
-
if (new Set(positives).size !== positives.length)
|
|
893
|
-
throw new Error("duplicate remainder commitment");
|
|
894
|
-
for (let i = 0; i < ns.length; i++) {
|
|
895
|
-
if (expiries[i] === 0n || c.now !== void 0 && expiries[i] <= c.now)
|
|
896
|
-
throw new Error("expiry must be future");
|
|
897
|
-
if (BigInt(remainderCommitments[i]) === 0n && BigInt(recoveryAmounts[i]) !== 0n)
|
|
898
|
-
throw new Error("full draw requires zero recovery amount");
|
|
899
|
-
}
|
|
900
|
-
const spender = getAddress6(p.spender), totalAmount = uint(p.totalAmount, profile.totalBits, "totalAmount");
|
|
901
|
-
if (BigInt(spender) === 0n || totalAmount === 0n)
|
|
902
|
-
throw new Error("spender and total amount must be positive");
|
|
903
|
-
if (dense(cs, "deliveries").length !== 2 * ns.length)
|
|
904
|
-
throw new Error("deliveries count mismatch");
|
|
905
|
-
const deliveries = cs.map(
|
|
906
|
-
(v, i) => BigInt(remainderCommitments[Math.floor(i / 2)]) === 0n ? normalizeOpaqueDelivery(v, { empty: true }) : normalizeOpaqueDelivery(v, {})
|
|
907
|
-
);
|
|
908
|
-
return {
|
|
909
|
-
...ctx,
|
|
910
|
-
sources,
|
|
911
|
-
nullifiers: ns,
|
|
912
|
-
remainderCommitments,
|
|
913
|
-
recoveryAmounts,
|
|
914
|
-
expiries,
|
|
915
|
-
spender,
|
|
916
|
-
totalAmount,
|
|
917
|
-
deadline: checkDeadline(c, p.deadline),
|
|
918
|
-
deliveries,
|
|
919
|
-
profile,
|
|
920
|
-
periods: creditPeriods(c.timestamp)
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
function creditDrawBaseInputs(c, p, cs) {
|
|
924
|
-
const r = validateCreditDrawRequest(c, p, cs), n = r.profile.maxSize, h = BigInt(deliveriesHash(cs));
|
|
925
|
-
return [
|
|
926
|
-
r.chainId,
|
|
927
|
-
BigInt(r.protocolAddress),
|
|
928
|
-
BigInt(r.tokenAddress),
|
|
929
|
-
BigInt(r.spender),
|
|
930
|
-
BigInt(r.nullifiers.length),
|
|
931
|
-
BigInt(r.sources.length),
|
|
932
|
-
...pad(
|
|
933
|
-
r.sources.map((s) => s.treeId),
|
|
934
|
-
n
|
|
935
|
-
),
|
|
936
|
-
...pad(
|
|
937
|
-
r.sources.map((s) => BigInt(s.merkleRoot)),
|
|
938
|
-
n
|
|
939
|
-
),
|
|
940
|
-
...pad(r.nullifiers.map(BigInt), n),
|
|
941
|
-
...pad(r.remainderCommitments.map(BigInt), n),
|
|
942
|
-
...pad(r.expiries, n),
|
|
943
|
-
r.totalAmount,
|
|
944
|
-
r.deadline,
|
|
945
|
-
h >> 128n,
|
|
946
|
-
h & (1n << 128n) - 1n,
|
|
947
|
-
...pad(r.recoveryAmounts.map(BigInt), n),
|
|
948
|
-
r.periods.currentDay,
|
|
949
|
-
r.periods.currentMonth
|
|
950
|
-
].map(word);
|
|
951
|
-
}
|
|
952
|
-
function validateCancelCreditRequest(c, p) {
|
|
953
|
-
known(p, [
|
|
954
|
-
"token",
|
|
955
|
-
"source",
|
|
956
|
-
"nullifier",
|
|
957
|
-
"owner",
|
|
958
|
-
"amount",
|
|
959
|
-
"deadline",
|
|
960
|
-
"feeRecipient",
|
|
961
|
-
"feeAmount"
|
|
962
|
-
]);
|
|
963
|
-
if (getAddress6(p.token) !== getAddress6(c.tokenAddress))
|
|
964
|
-
throw new Error("request token mismatch");
|
|
965
|
-
const ctx = normalizeContext(c), source = normalizeSources([p.source], 1, 1)[0], nullifier = notes([p.nullifier], "nullifier")[0], owner = getAddress6(p.owner), amount = uint(p.amount, 126, "amount");
|
|
966
|
-
if (BigInt(owner) === 0n || amount === 0n) throw new Error("owner and amount must be positive");
|
|
967
|
-
const fee = normalizeFee(p);
|
|
968
|
-
if (fee.feeAmount >= amount) throw new Error("fee must leave a positive owner refund");
|
|
969
|
-
return {
|
|
970
|
-
...ctx,
|
|
971
|
-
...fee,
|
|
972
|
-
source,
|
|
973
|
-
nullifier,
|
|
974
|
-
owner,
|
|
975
|
-
amount,
|
|
976
|
-
deadline: checkDeadline(c, p.deadline)
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
function cancelCreditBaseInputs(c, p) {
|
|
980
|
-
const r = validateCancelCreditRequest(c, p);
|
|
981
|
-
return [
|
|
982
|
-
r.chainId,
|
|
983
|
-
BigInt(r.protocolAddress),
|
|
984
|
-
BigInt(r.tokenAddress),
|
|
985
|
-
BigInt(r.owner),
|
|
986
|
-
r.source.treeId,
|
|
987
|
-
BigInt(r.source.merkleRoot),
|
|
988
|
-
BigInt(r.nullifier),
|
|
989
|
-
r.amount,
|
|
990
|
-
r.deadline,
|
|
991
|
-
BigInt(r.feeRecipient),
|
|
992
|
-
r.feeAmount
|
|
993
|
-
].map(word);
|
|
994
|
-
}
|
|
995
|
-
function validateTopUpCreditRequest(c, p, cs, funding = {}) {
|
|
996
|
-
known(p, ["token", "creation", "creditSource", "creditNullifier", "previousExpiry"]);
|
|
997
|
-
const creation = validateCreateCreditRequest(c, p.creation, cs, funding), creditSource = normalizeSources([p.creditSource], 1, 1)[0], creditNullifier = notes([p.creditNullifier], "creditNullifier")[0], previousExpiry = uint(p.previousExpiry, 64, "previousExpiry");
|
|
998
|
-
if (previousExpiry === 0n || c.now !== void 0 && previousExpiry <= c.now)
|
|
999
|
-
throw new Error("previous Credit has expired");
|
|
1000
|
-
if (creation.expiry < previousExpiry) throw new Error("expiry must not decrease");
|
|
1001
|
-
if (creation.nullifiers.includes(creditNullifier))
|
|
1002
|
-
throw new Error("duplicate credit and ordinary nullifier");
|
|
1003
|
-
return { creation, creditSource, creditNullifier, previousExpiry };
|
|
1004
|
-
}
|
|
1005
|
-
function topUpCreditBaseInputs(c, p, cs, f = {}) {
|
|
1006
|
-
const r = validateTopUpCreditRequest(c, p, cs, f);
|
|
1007
|
-
return [
|
|
1008
|
-
...createCreditBaseInputs(c, p.creation, cs, f),
|
|
1009
|
-
word(r.creditSource.treeId),
|
|
1010
|
-
r.creditSource.merkleRoot,
|
|
1011
|
-
r.creditNullifier,
|
|
1012
|
-
word(r.previousExpiry)
|
|
1013
|
-
];
|
|
1014
|
-
}
|
|
1015
|
-
var cancelTuple = [
|
|
1016
|
-
"(address token,(uint256 treeId,bytes32 merkleRoot) source,bytes32 nullifier,address owner,uint256 amount,uint256 deadline,address feeRecipient,uint256 feeAmount)"
|
|
1017
|
-
][0];
|
|
1018
|
-
var topTuple = [
|
|
1019
|
-
"((address token,(uint256 treeId,bytes32 merkleRoot)[] sources,bytes32[] nullifiers,bytes32 creditCommitment,bytes32 changeCommitment,address owner,uint256 expiry,uint256 deadline,address feeRecipient,uint256 feeAmount) creation,(uint256 treeId,bytes32 merkleRoot) creditSource,bytes32 creditNullifier,uint256 previousExpiry)"
|
|
1020
|
-
][0];
|
|
1021
|
-
var hashContext2 = {
|
|
1022
|
-
chainId: 0n,
|
|
1023
|
-
protocolAddress: ZERO_ADDRESS,
|
|
1024
|
-
tokenAddress: ZERO_ADDRESS
|
|
1025
|
-
};
|
|
1026
|
-
function cancelCreditRequestHash(p) {
|
|
1027
|
-
validateCancelCreditRequest({ ...hashContext2, tokenAddress: p.token }, p);
|
|
1028
|
-
return keccak2564(
|
|
1029
|
-
AbiCoder3.defaultAbiCoder().encode(
|
|
1030
|
-
[{ type: "bytes32" }, cancelTuple].map((parameter) => ParamType3.from(parameter)),
|
|
1031
|
-
[
|
|
1032
|
-
keccak2564(new TextEncoder().encode("InvisiCancelCredit")),
|
|
1033
|
-
{ ...p, ...normalizeFee(p) }
|
|
1034
|
-
]
|
|
1035
|
-
)
|
|
1036
|
-
);
|
|
1037
|
-
}
|
|
1038
|
-
function topUpCreditRequestHash(p, cs, f = {}) {
|
|
1039
|
-
const r = validateTopUpCreditRequest(
|
|
1040
|
-
{ ...hashContext2, tokenAddress: p.creation.token },
|
|
1041
|
-
p,
|
|
1042
|
-
cs,
|
|
1043
|
-
f
|
|
1044
|
-
);
|
|
1045
|
-
return keccak2564(
|
|
1046
|
-
AbiCoder3.defaultAbiCoder().encode(
|
|
1047
|
-
[
|
|
1048
|
-
{ type: "bytes32" },
|
|
1049
|
-
topTuple,
|
|
1050
|
-
{ type: "bytes32" },
|
|
1051
|
-
{ type: "address" },
|
|
1052
|
-
{ type: "uint256" }
|
|
1053
|
-
].map((parameter) => ParamType3.from(parameter)),
|
|
1054
|
-
[
|
|
1055
|
-
keccak2564(new TextEncoder().encode("InvisiTopUpCredit")),
|
|
1056
|
-
{ ...p, creation: { ...p.creation, ...normalizeFee(p.creation) } },
|
|
1057
|
-
deliveriesHash(cs),
|
|
1058
|
-
r.creation.funding.payer,
|
|
1059
|
-
r.creation.funding.amount
|
|
1060
|
-
]
|
|
1061
|
-
)
|
|
1062
|
-
);
|
|
1063
|
-
}
|
|
1064
|
-
function authorize(c, operation, capacity, words) {
|
|
1065
|
-
return walletAuthorizationTypedData2({
|
|
1066
|
-
context: { chainId: c.chainId, protocolAddress: BigInt(c.protocolAddress) },
|
|
1067
|
-
operation,
|
|
1068
|
-
capacity,
|
|
1069
|
-
words: words.map(BigInt)
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
function appendDigest(c, operation, capacity, words) {
|
|
1073
|
-
const digest = BigInt(
|
|
1074
|
-
computeWalletAuthorizationDigest2({
|
|
1075
|
-
context: { chainId: c.chainId, protocolAddress: BigInt(c.protocolAddress) },
|
|
1076
|
-
operation,
|
|
1077
|
-
capacity,
|
|
1078
|
-
words: words.map(BigInt)
|
|
1079
|
-
})
|
|
1080
|
-
);
|
|
1081
|
-
return [...words, word(digest >> 128n), word(digest & (1n << 128n) - 1n)];
|
|
1082
|
-
}
|
|
1083
|
-
function encodeCancelCreditTypedData(c, p) {
|
|
1084
|
-
return authorize(c, "cancel-credit", 1, cancelCreditBaseInputs(c, p));
|
|
1085
|
-
}
|
|
1086
|
-
function encodeTopUpCreditTypedData(c, p, deliveries, funding = {}) {
|
|
1087
|
-
return authorize(c, "top-up-credit", 10, topUpCreditBaseInputs(c, p, deliveries, funding));
|
|
1088
|
-
}
|
|
1089
|
-
function encodeCreditDrawTypedData(c, p, deliveries) {
|
|
1090
|
-
return authorize(
|
|
1091
|
-
c,
|
|
1092
|
-
"credit-draw",
|
|
1093
|
-
selectCreditDrawCircuit(p.nullifiers.length).maxSize,
|
|
1094
|
-
creditDrawBaseInputs(c, p, deliveries)
|
|
1095
|
-
);
|
|
1096
|
-
}
|
|
1097
|
-
function creditDrawPublicInputs(c, p, deliveries) {
|
|
1098
|
-
return appendDigest(
|
|
1099
|
-
c,
|
|
1100
|
-
"credit-draw",
|
|
1101
|
-
selectCreditDrawCircuit(p.nullifiers.length).maxSize,
|
|
1102
|
-
creditDrawBaseInputs(c, p, deliveries)
|
|
1103
|
-
);
|
|
1104
|
-
}
|
|
1105
|
-
function cancelCreditPublicInputs(c, p) {
|
|
1106
|
-
return appendDigest(c, "cancel-credit", 1, cancelCreditBaseInputs(c, p));
|
|
1107
|
-
}
|
|
1108
|
-
function topUpCreditPublicInputs(c, p, deliveries, funding = {}) {
|
|
1109
|
-
return appendDigest(c, "top-up-credit", 10, topUpCreditBaseInputs(c, p, deliveries, funding));
|
|
1110
|
-
}
|
|
1111
|
-
function encodeTopUpCreditFundingTypedData(c, p, cs, f) {
|
|
1112
|
-
const r = validateTopUpCreditRequest(c, p, cs, f), funding = r.creation.funding;
|
|
1113
|
-
if (funding.amount === 0n) throw new Error("funding authorization requires positive deposit");
|
|
1114
|
-
return {
|
|
1115
|
-
domain: getEIP712Domain(c),
|
|
1116
|
-
primaryType: "FundTransact",
|
|
1117
|
-
types: { FundTransact: fundingFields },
|
|
1118
|
-
message: {
|
|
1119
|
-
requestHash: topUpCreditRequestHash(p, cs, f),
|
|
1120
|
-
payer: funding.payer,
|
|
1121
|
-
amount: funding.amount,
|
|
1122
|
-
sigNonce: funding.sigNonce,
|
|
1123
|
-
deadline: funding.deadline
|
|
1124
|
-
}
|
|
1125
|
-
};
|
|
1126
|
-
}
|
|
1127
|
-
function buildCreditDrawCalldata(p) {
|
|
1128
|
-
validateCreditDrawRequest(p.context, p.params, p.deliveries);
|
|
1129
|
-
return {
|
|
1130
|
-
data: new Interface4(invisiProtocolAbi).encodeFunctionData("creditDraw", [
|
|
1131
|
-
encodeCreditProof(p.proof),
|
|
1132
|
-
p.params,
|
|
1133
|
-
p.deliveries
|
|
1134
|
-
]),
|
|
1135
|
-
nullifiers: p.params.nullifiers,
|
|
1136
|
-
totalAmount: p.params.totalAmount
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
function buildCancelCreditCalldata(p) {
|
|
1140
|
-
validateCancelCreditRequest(p.context, p.params);
|
|
1141
|
-
return {
|
|
1142
|
-
data: new Interface4(invisiProtocolAbi).encodeFunctionData("cancelCredit", [
|
|
1143
|
-
encodeCreditProof(p.proof),
|
|
1144
|
-
{ ...p.params, ...normalizeFee(p.params) }
|
|
1145
|
-
])
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
function buildTopUpCreditCalldata(p) {
|
|
1149
|
-
const r = validateTopUpCreditRequest(p.context, p.params, p.deliveries, p.funding);
|
|
1150
|
-
validateFundingSubmission(r.creation.funding, p.caller, p.context.now);
|
|
1151
|
-
return {
|
|
1152
|
-
data: new Interface4(invisiProtocolAbi).encodeFunctionData("topUpCredit", [
|
|
1153
|
-
encodeCreditProof(p.proof),
|
|
1154
|
-
{ ...p.params, creation: { ...p.params.creation, ...normalizeFee(p.params.creation) } },
|
|
1155
|
-
p.deliveries,
|
|
1156
|
-
r.creation.funding
|
|
1157
|
-
]),
|
|
1158
|
-
approveAmount: r.creation.funding.amount
|
|
1159
|
-
};
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// src/funds.ts
|
|
1163
|
-
import { AbiCoder as AbiCoder4, getAddress as getAddress7, Interface as Interface5, keccak256 as keccak2565, ParamType as ParamType4, TypedDataEncoder as TypedDataEncoder2 } from "ethers";
|
|
1164
|
-
import { computeNoteCommitment, walletAuthorizationTypedData as walletAuthorizationTypedData3 } from "@invisi-labs/core";
|
|
1165
|
-
var TRANSACT_CAPACITIES = [14, 56, 112, 168];
|
|
1166
|
-
function selectTransactCapacity(inputs, outputs) {
|
|
1167
|
-
if (!Number.isInteger(inputs) || !Number.isInteger(outputs) || inputs < 0 || outputs < 0 || inputs + outputs < 1 || inputs === 0 && outputs < 2 || inputs + outputs > 168)
|
|
1168
|
-
throw new Error("unsupported transact shape");
|
|
1169
|
-
return TRANSACT_CAPACITIES.find((n) => inputs + outputs <= n);
|
|
1170
|
-
}
|
|
1171
|
-
function normalizeTransactRequest(c, p, cs, funding = {}) {
|
|
1172
|
-
const ctx = normalizeContext(c);
|
|
1173
|
-
if (getAddress7(p.token) !== ctx.tokenAddress)
|
|
1174
|
-
throw new Error("request token mismatch");
|
|
1175
|
-
known(p, [
|
|
1176
|
-
"token",
|
|
1177
|
-
"sources",
|
|
1178
|
-
"nullifiers",
|
|
1179
|
-
"outputCommitments",
|
|
1180
|
-
"withdrawalAddress",
|
|
1181
|
-
"withdrawalAmount",
|
|
1182
|
-
"deadline",
|
|
1183
|
-
"feeRecipient",
|
|
1184
|
-
"feeAmount"
|
|
1185
|
-
]);
|
|
1186
|
-
const ns = notes(p.nullifiers, "nullifiers"), os = notes(p.outputCommitments, "outputCommitments"), capacity = selectTransactCapacity(ns.length, os.length);
|
|
1187
|
-
const sources = normalizeSources(p.sources, ns.length, capacity), deliveries = dense(cs, "deliveries").map(
|
|
1188
|
-
(v) => normalizeOpaqueDelivery(v, { allowEmpty: true })
|
|
1189
|
-
);
|
|
1190
|
-
if (deliveries.length !== os.length) throw new Error("deliveries count mismatch");
|
|
1191
|
-
const withdrawalAmount = uint(p.withdrawalAmount, 128, "withdrawalAmount"), withdrawalAddress = getAddress7(p.withdrawalAddress), f = normalizeFunding(funding);
|
|
1192
|
-
const feeAmount = uint(p.feeAmount ?? 0n, 128, "feeAmount");
|
|
1193
|
-
const feeRecipient = getAddress7(
|
|
1194
|
-
p.feeRecipient ?? "0x0000000000000000000000000000000000000000"
|
|
1195
|
-
);
|
|
1196
|
-
if (feeAmount === 0n !== (BigInt(feeRecipient) === 0n)) throw new Error("invalid fee");
|
|
1197
|
-
if (withdrawalAmount === 0n !== (BigInt(withdrawalAddress) === 0n) || os.length === 0 && withdrawalAmount === 0n)
|
|
1198
|
-
throw new Error("invalid withdrawal");
|
|
1199
|
-
if (ns.length === 0 && (f.amount === 0n || withdrawalAmount !== 0n))
|
|
1200
|
-
throw new Error("zero inputs require positive funding and zero withdrawal");
|
|
1201
|
-
return {
|
|
1202
|
-
...ctx,
|
|
1203
|
-
capacity,
|
|
1204
|
-
sources,
|
|
1205
|
-
nullifiers: ns,
|
|
1206
|
-
outputCommitments: os,
|
|
1207
|
-
inputCount: ns.length,
|
|
1208
|
-
outputCount: os.length,
|
|
1209
|
-
sourceCount: sources.length,
|
|
1210
|
-
inputTreeIds: pad(
|
|
1211
|
-
sources.map((s) => s.treeId),
|
|
1212
|
-
capacity
|
|
1213
|
-
),
|
|
1214
|
-
merkleRoots: pad(
|
|
1215
|
-
sources.map((s) => BigInt(s.merkleRoot)),
|
|
1216
|
-
capacity
|
|
1217
|
-
),
|
|
1218
|
-
slotValues: pad([...ns, ...os].map(BigInt), capacity),
|
|
1219
|
-
deliveries,
|
|
1220
|
-
deliveriesHash: deliveriesHash(deliveries),
|
|
1221
|
-
withdrawalAddress,
|
|
1222
|
-
withdrawalAmount,
|
|
1223
|
-
feeRecipient,
|
|
1224
|
-
feeAmount,
|
|
1225
|
-
deadline: checkDeadline(c, p.deadline),
|
|
1226
|
-
depositAmount: f.amount,
|
|
1227
|
-
depositPayer: f.payer,
|
|
1228
|
-
funding: f
|
|
1229
|
-
};
|
|
1230
|
-
}
|
|
1231
|
-
function transactPublicInputs(c, p, cs, f = {}) {
|
|
1232
|
-
const r = normalizeTransactRequest(c, p, cs, f), h = BigInt(r.deliveriesHash);
|
|
1233
|
-
return [
|
|
1234
|
-
r.chainId,
|
|
1235
|
-
BigInt(r.protocolAddress),
|
|
1236
|
-
BigInt(r.tokenAddress),
|
|
1237
|
-
BigInt(r.inputCount),
|
|
1238
|
-
BigInt(r.outputCount),
|
|
1239
|
-
BigInt(r.sourceCount),
|
|
1240
|
-
...r.inputTreeIds,
|
|
1241
|
-
...r.merkleRoots,
|
|
1242
|
-
...r.slotValues,
|
|
1243
|
-
h >> 128n,
|
|
1244
|
-
h & (1n << 128n) - 1n,
|
|
1245
|
-
BigInt(r.withdrawalAddress),
|
|
1246
|
-
r.withdrawalAmount,
|
|
1247
|
-
r.deadline,
|
|
1248
|
-
r.depositAmount,
|
|
1249
|
-
BigInt(r.depositPayer),
|
|
1250
|
-
BigInt(r.feeRecipient),
|
|
1251
|
-
r.feeAmount
|
|
1252
|
-
].map(word);
|
|
1253
|
-
}
|
|
1254
|
-
function encodeTransactTypedData(c, p, deliveries, f = {}) {
|
|
1255
|
-
const r = normalizeTransactRequest(c, p, deliveries, f);
|
|
1256
|
-
return walletAuthorizationTypedData3({
|
|
1257
|
-
context: { chainId: r.chainId, protocolAddress: BigInt(r.protocolAddress) },
|
|
1258
|
-
operation: "transact",
|
|
1259
|
-
capacity: r.capacity,
|
|
1260
|
-
words: transactPublicInputs(c, p, deliveries, f).map(BigInt)
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
|
-
var singleTuple = [
|
|
1264
|
-
"(address token,bytes32 merkleRoot,bytes32[] nullifiers,bytes32[] outputCommitments,uint256 inputTreeId,address withdrawalAddress,uint256 withdrawalAmount,uint256 deadline,address feeRecipient,uint256 feeAmount)"
|
|
1265
|
-
][0];
|
|
1266
|
-
var crossTuple = [
|
|
1267
|
-
"(address token,(uint256 treeId,bytes32 merkleRoot)[] sources,bytes32[] nullifiers,bytes32[] outputCommitments,address withdrawalAddress,uint256 withdrawalAmount,uint256 deadline,address feeRecipient,uint256 feeAmount)"
|
|
1268
|
-
][0];
|
|
1269
|
-
function transactFundingRequestHash(proof, p, cs) {
|
|
1270
|
-
const single = {
|
|
1271
|
-
token: p.token,
|
|
1272
|
-
merkleRoot: p.sources[0]?.merkleRoot ?? word(0n),
|
|
1273
|
-
nullifiers: p.nullifiers,
|
|
1274
|
-
outputCommitments: p.outputCommitments,
|
|
1275
|
-
inputTreeId: p.sources[0]?.treeId ?? 0n,
|
|
1276
|
-
withdrawalAddress: p.withdrawalAddress,
|
|
1277
|
-
withdrawalAmount: p.withdrawalAmount,
|
|
1278
|
-
deadline: p.deadline,
|
|
1279
|
-
feeRecipient: p.feeRecipient ?? "0x0000000000000000000000000000000000000000",
|
|
1280
|
-
feeAmount: p.feeAmount ?? 0n
|
|
1281
|
-
};
|
|
1282
|
-
return p.sources.length > 1 ? keccak2565(
|
|
1283
|
-
AbiCoder4.defaultAbiCoder().encode(
|
|
1284
|
-
[{ type: "bytes32" }, { type: "bytes32" }, crossTuple, { type: "bytes32" }].map(
|
|
1285
|
-
(parameter) => ParamType4.from(parameter)
|
|
1286
|
-
),
|
|
1287
|
-
[
|
|
1288
|
-
keccak2565(new TextEncoder().encode("InvisiCrossTreeTransact")),
|
|
1289
|
-
keccak2565(proof),
|
|
1290
|
-
{ ...p, feeRecipient: single.feeRecipient, feeAmount: single.feeAmount },
|
|
1291
|
-
deliveriesHash(cs)
|
|
1292
|
-
]
|
|
1293
|
-
)
|
|
1294
|
-
) : keccak2565(
|
|
1295
|
-
AbiCoder4.defaultAbiCoder().encode(
|
|
1296
|
-
[{ type: "bytes32" }, singleTuple, { type: "bytes32" }].map(
|
|
1297
|
-
(parameter) => ParamType4.from(parameter)
|
|
1298
|
-
),
|
|
1299
|
-
[keccak2565(proof), single, deliveriesHash(cs)]
|
|
1300
|
-
)
|
|
1301
|
-
);
|
|
1302
|
-
}
|
|
1303
|
-
function encodeTransactFundingTypedData(c, proof, p, cs, funding) {
|
|
1304
|
-
normalizeTransactRequest(c, p, cs, funding);
|
|
1305
|
-
const f = normalizeFunding(funding);
|
|
1306
|
-
if (f.amount === 0n) throw new Error("funding authorization requires positive deposit");
|
|
1307
|
-
return {
|
|
1308
|
-
domain: getEIP712Domain(c),
|
|
1309
|
-
primaryType: "FundTransact",
|
|
1310
|
-
types: { FundTransact: fundingFields },
|
|
1311
|
-
message: {
|
|
1312
|
-
requestHash: transactFundingRequestHash(proof, p, cs),
|
|
1313
|
-
payer: f.payer,
|
|
1314
|
-
amount: f.amount,
|
|
1315
|
-
sigNonce: f.sigNonce,
|
|
1316
|
-
deadline: f.deadline
|
|
1317
|
-
}
|
|
1318
|
-
};
|
|
1319
|
-
}
|
|
1320
|
-
function buildTransactCalldata(p) {
|
|
1321
|
-
const r = normalizeTransactRequest(p.context, p.params, p.deliveries, p.funding);
|
|
1322
|
-
validateFundingSubmission(r.funding, p.caller, p.context.now);
|
|
1323
|
-
const request = {
|
|
1324
|
-
params: {
|
|
1325
|
-
token: r.tokenAddress,
|
|
1326
|
-
sources: r.sources,
|
|
1327
|
-
nullifiers: r.nullifiers,
|
|
1328
|
-
outputCommitments: r.outputCommitments,
|
|
1329
|
-
withdrawalAddress: r.withdrawalAddress,
|
|
1330
|
-
withdrawalAmount: r.withdrawalAmount,
|
|
1331
|
-
deadline: r.deadline,
|
|
1332
|
-
feeRecipient: r.feeRecipient,
|
|
1333
|
-
feeAmount: r.feeAmount
|
|
1334
|
-
},
|
|
1335
|
-
deliveries: p.deliveries,
|
|
1336
|
-
funding: r.funding
|
|
1337
|
-
};
|
|
1338
|
-
const abi = invisiProtocolAbi.filter(
|
|
1339
|
-
(item) => item.type === "function" && item.name === "transact" && item.inputs.length === 2 && item.inputs[1]?.type === "tuple"
|
|
1340
|
-
);
|
|
1341
|
-
const data = new Interface5(abi).encodeFunctionData("transact", [p.proof, request]);
|
|
1342
|
-
return { data, capacity: r.capacity, approveAmount: r.depositAmount };
|
|
1343
|
-
}
|
|
1344
|
-
function buildTransactAggregateCalldata(p) {
|
|
1345
|
-
if (![4, 8, 12].includes(dense(p.requests, "requests").length))
|
|
1346
|
-
throw new Error("aggregate requires 4, 8 or 12 requests");
|
|
1347
|
-
const requests = p.requests.map((request) => {
|
|
1348
|
-
const r = normalizeTransactRequest(
|
|
1349
|
-
{ ...p.context, tokenAddress: request.params.token },
|
|
1350
|
-
request.params,
|
|
1351
|
-
request.deliveries,
|
|
1352
|
-
request.funding
|
|
1353
|
-
);
|
|
1354
|
-
if (r.capacity !== 14) throw new Error("aggregate supports only capacity 14");
|
|
1355
|
-
validateFundingSubmission(r.funding, p.caller, p.context.now);
|
|
1356
|
-
return {
|
|
1357
|
-
...request,
|
|
1358
|
-
params: { ...request.params, feeRecipient: r.feeRecipient, feeAmount: r.feeAmount }
|
|
1359
|
-
};
|
|
1360
|
-
});
|
|
1361
|
-
return {
|
|
1362
|
-
data: new Interface5(
|
|
1363
|
-
invisiProtocolAbi.filter(
|
|
1364
|
-
(item) => item.type === "function" && item.name === "transact" && item.inputs[1]?.type === "tuple[]"
|
|
1365
|
-
)
|
|
1366
|
-
).encodeFunctionData("transact", [p.proof, requests]),
|
|
1367
|
-
capacity: 14
|
|
1368
|
-
};
|
|
1369
|
-
}
|
|
1370
|
-
async function validateShieldRequest(p) {
|
|
1371
|
-
normalizeContext(p.context);
|
|
1372
|
-
if (getAddress7(p.params.token) !== getAddress7(p.context.tokenAddress))
|
|
1373
|
-
throw new Error("request token mismatch");
|
|
1374
|
-
known(p.params, ["token", "outputCommitments", "deadline", "feeRecipient", "feeAmount"]);
|
|
1375
|
-
checkDeadline(p.context, p.params.deadline);
|
|
1376
|
-
const outputs = dense(p.outputs, "outputs"), commitments = notes(p.params.outputCommitments, "outputCommitments"), cs = dense(p.deliveries, "deliveries"), f = normalizeFunding(p.funding);
|
|
1377
|
-
if (outputs.length < 1 || outputs.length > 10 || commitments.length !== outputs.length || cs.length !== outputs.length)
|
|
1378
|
-
throw new Error("shield requires 1..10 outputs and matching deliveries");
|
|
1379
|
-
let total = 0n;
|
|
1380
|
-
for (const [i, o] of outputs.entries()) {
|
|
1381
|
-
known(o, ["identity", "amount"], "output");
|
|
1382
|
-
uint(o.amount, 128, "amount");
|
|
1383
|
-
if (o.amount === 0n) throw new Error("amount must be positive");
|
|
1384
|
-
field(o.identity);
|
|
1385
|
-
normalizeOpaqueDelivery(cs[i], { allowEmpty: true });
|
|
1386
|
-
if (await computeNoteCommitment(
|
|
1387
|
-
p.context.chainId,
|
|
1388
|
-
o.identity,
|
|
1389
|
-
BigInt(p.context.tokenAddress),
|
|
1390
|
-
o.amount
|
|
1391
|
-
) !== commitments[i])
|
|
1392
|
-
throw new Error("output commitment mismatch");
|
|
1393
|
-
total += o.amount;
|
|
1394
|
-
}
|
|
1395
|
-
if (total + normalizeFee(p.params).feeAmount !== f.amount)
|
|
1396
|
-
throw new Error("funding must equal output total plus fee");
|
|
1397
|
-
return f;
|
|
1398
|
-
}
|
|
1399
|
-
function shieldFundingRequestHash(params, outputs, cs) {
|
|
1400
|
-
const outputHash = keccak2565(
|
|
1401
|
-
AbiCoder4.defaultAbiCoder().encode(
|
|
1402
|
-
["(bytes32 identity,uint256 amount)[]"].map((parameter) => ParamType4.from(parameter)),
|
|
1403
|
-
[outputs]
|
|
1404
|
-
)
|
|
1405
|
-
);
|
|
1406
|
-
return keccak2565(
|
|
1407
|
-
AbiCoder4.defaultAbiCoder().encode(
|
|
1408
|
-
[
|
|
1409
|
-
"bytes32",
|
|
1410
|
-
"(address token,bytes32[] outputCommitments,uint256 deadline,address feeRecipient,uint256 feeAmount)",
|
|
1411
|
-
"bytes32",
|
|
1412
|
-
"bytes32"
|
|
1413
|
-
].map((parameter) => ParamType4.from(parameter)),
|
|
1414
|
-
[
|
|
1415
|
-
keccak2565(new TextEncoder().encode("InvisiShield")),
|
|
1416
|
-
{ ...params, ...normalizeFee(params) },
|
|
1417
|
-
outputHash,
|
|
1418
|
-
deliveriesHash(cs)
|
|
1419
|
-
]
|
|
1420
|
-
)
|
|
1421
|
-
);
|
|
1422
|
-
}
|
|
1423
|
-
async function encodeShieldFundingTypedData(p) {
|
|
1424
|
-
const f = await validateShieldRequest(p);
|
|
1425
|
-
return {
|
|
1426
|
-
domain: getEIP712Domain(p.context),
|
|
1427
|
-
primaryType: "FundShield",
|
|
1428
|
-
types: { FundShield: fundingFields },
|
|
1429
|
-
message: {
|
|
1430
|
-
requestHash: shieldFundingRequestHash(p.params, p.outputs, p.deliveries),
|
|
1431
|
-
payer: f.payer,
|
|
1432
|
-
amount: f.amount,
|
|
1433
|
-
sigNonce: f.sigNonce,
|
|
1434
|
-
deadline: f.deadline
|
|
1435
|
-
}
|
|
1436
|
-
};
|
|
1437
|
-
}
|
|
1438
|
-
async function buildShieldCalldata(p) {
|
|
1439
|
-
const f = await validateShieldRequest(p);
|
|
1440
|
-
validateFundingSubmission(f, p.caller, p.context.now);
|
|
1441
|
-
return {
|
|
1442
|
-
data: new Interface5(invisiProtocolAbi).encodeFunctionData("shield", [
|
|
1443
|
-
{ ...p.params, ...normalizeFee(p.params) },
|
|
1444
|
-
p.outputs,
|
|
1445
|
-
p.deliveries,
|
|
1446
|
-
f
|
|
1447
|
-
]),
|
|
1448
|
-
approveAmount: f.amount
|
|
1449
|
-
};
|
|
1450
|
-
}
|
|
1451
|
-
function transactUserPublicInputs(c, p, cs, f = {}) {
|
|
1452
|
-
const words = transactPublicInputs(c, p, cs, f), digest = BigInt(
|
|
1453
|
-
((typed) => TypedDataEncoder2.hash(
|
|
1454
|
-
typed.domain,
|
|
1455
|
-
Object.fromEntries(
|
|
1456
|
-
Object.entries(typed.types).map(([name, fields]) => [
|
|
1457
|
-
name,
|
|
1458
|
-
[...fields]
|
|
1459
|
-
])
|
|
1460
|
-
),
|
|
1461
|
-
typed.message
|
|
1462
|
-
))(encodeTransactTypedData(c, p, cs, f))
|
|
1463
|
-
);
|
|
1464
|
-
return [...words, word(digest >> 128n), word(digest & (1n << 128n) - 1n)];
|
|
1465
|
-
}
|
|
1466
|
-
function aggregateMemberRequestHash(c, r) {
|
|
1467
|
-
const capacity = selectTransactCapacity(
|
|
1468
|
-
r.params.nullifiers.length,
|
|
1469
|
-
r.params.outputCommitments.length
|
|
1470
|
-
);
|
|
1471
|
-
if (capacity !== 14) throw new Error("aggregate supports only capacity 14");
|
|
1472
|
-
return keccak2565(
|
|
1473
|
-
AbiCoder4.defaultAbiCoder().encode(
|
|
1474
|
-
["bytes32", "uint256", "bytes32[]"].map((parameter) => ParamType4.from(parameter)),
|
|
1475
|
-
[
|
|
1476
|
-
keccak2565(new TextEncoder().encode("InvisiAtomicBatchMemberV1")),
|
|
1477
|
-
BigInt(capacity),
|
|
1478
|
-
transactPublicInputs(c, r.params, r.deliveries, r.funding)
|
|
1479
|
-
]
|
|
1480
|
-
)
|
|
1481
|
-
);
|
|
1482
|
-
}
|
|
1483
|
-
function aggregatePublicInputs(c, requests) {
|
|
1484
|
-
if (![4, 8, 12].includes(dense(requests, "requests").length))
|
|
1485
|
-
throw new Error("aggregate requires 4, 8 or 12 requests");
|
|
1486
|
-
return requests.flatMap((r) => {
|
|
1487
|
-
const context = { ...c, tokenAddress: r.params.token };
|
|
1488
|
-
aggregateMemberRequestHash(context, r);
|
|
1489
|
-
return [word(14n), ...transactPublicInputs(context, r.params, r.deliveries, r.funding)];
|
|
1490
|
-
});
|
|
1491
|
-
}
|
|
1492
|
-
function encodeAggregateFundingTypedData(c, r) {
|
|
1493
|
-
const f = normalizeFunding(r.funding);
|
|
1494
|
-
if (f.amount === 0n) throw new Error("funding authorization requires positive deposit");
|
|
1495
|
-
return {
|
|
1496
|
-
domain: getEIP712Domain(c),
|
|
1497
|
-
primaryType: "FundTransact",
|
|
1498
|
-
types: { FundTransact: fundingFields },
|
|
1499
|
-
message: {
|
|
1500
|
-
requestHash: aggregateMemberRequestHash(c, r),
|
|
1501
|
-
payer: f.payer,
|
|
1502
|
-
amount: f.amount,
|
|
1503
|
-
sigNonce: f.sigNonce,
|
|
1504
|
-
deadline: f.deadline
|
|
1505
|
-
}
|
|
1506
|
-
};
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
// src/Protocol.ts
|
|
1510
|
-
import { Contract as Contract2, getAddress as getAddress8, keccak256 as keccak2567 } from "ethers";
|
|
1511
|
-
import {
|
|
1512
|
-
defineDeployment
|
|
1513
|
-
} from "@invisi-labs/rpc";
|
|
1514
|
-
|
|
1515
|
-
// src/TokenProtocol.ts
|
|
1516
|
-
import { keccak256 as keccak2566 } from "ethers";
|
|
1517
|
-
import { MissingCapabilityError as MissingCapabilityError2 } from "@invisi-labs/core";
|
|
1518
|
-
var TokenProtocol = class {
|
|
1519
|
-
context;
|
|
1520
|
-
expectedRuntimeCodeHash;
|
|
1521
|
-
constructor(context, options = {}) {
|
|
1522
|
-
const normalized = normalizeContext(context);
|
|
1523
|
-
if (BigInt(normalized.protocolAddress) === 0n || BigInt(normalized.tokenAddress) === 0n) {
|
|
1524
|
-
throw new Error("deployment addresses must be nonzero");
|
|
1525
|
-
}
|
|
1526
|
-
if (options.expectedRuntimeCodeHash && !/^0x[0-9a-fA-F]{64}$/.test(options.expectedRuntimeCodeHash)) {
|
|
1527
|
-
throw new Error("invalid runtime code hash");
|
|
1528
|
-
}
|
|
1529
|
-
this.expectedRuntimeCodeHash = options.expectedRuntimeCodeHash?.toLowerCase();
|
|
1530
|
-
this.context = Object.freeze(normalized);
|
|
1531
|
-
Object.freeze(this);
|
|
1532
|
-
}
|
|
1533
|
-
async validateDeployment(reader, options = {}) {
|
|
1534
|
-
if ((options.requireRuntimeProfile ?? true) && !this.expectedRuntimeCodeHash) {
|
|
1535
|
-
throw new MissingCapabilityError2("trusted deployment profile");
|
|
1536
|
-
}
|
|
1537
|
-
if (await reader.getChainId() !== this.context.chainId) {
|
|
1538
|
-
throw new Error("deployment chain mismatch");
|
|
1539
|
-
}
|
|
1540
|
-
const code = await reader.getCode(this.context.protocolAddress);
|
|
1541
|
-
if (!/^0x(?:[0-9a-fA-F]{2})+$/.test(code) || this.expectedRuntimeCodeHash && keccak2566(code) !== this.expectedRuntimeCodeHash) {
|
|
1542
|
-
throw new Error("deployment runtime code profile mismatch");
|
|
1543
|
-
}
|
|
1544
|
-
const tokenCode = await reader.getCode(this.context.tokenAddress);
|
|
1545
|
-
if (!/^0x(?:[0-9a-fA-F]{2})+$/.test(tokenCode)) {
|
|
1546
|
-
throw new Error("token address must have contract code");
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
transact(request) {
|
|
1550
|
-
return {
|
|
1551
|
-
to: this.context.protocolAddress,
|
|
1552
|
-
value: 0n,
|
|
1553
|
-
...buildTransactCalldata({ ...request, context: this.context })
|
|
1554
|
-
};
|
|
1555
|
-
}
|
|
1556
|
-
aggregate(request) {
|
|
1557
|
-
return {
|
|
1558
|
-
to: this.context.protocolAddress,
|
|
1559
|
-
value: 0n,
|
|
1560
|
-
...buildTransactAggregateCalldata({ ...request, context: this.context })
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
async shield(request) {
|
|
1564
|
-
return {
|
|
1565
|
-
to: this.context.protocolAddress,
|
|
1566
|
-
value: 0n,
|
|
1567
|
-
...await buildShieldCalldata({ ...request, context: this.context })
|
|
1568
|
-
};
|
|
1569
|
-
}
|
|
1570
|
-
createCredit(request) {
|
|
1571
|
-
return {
|
|
1572
|
-
to: this.context.protocolAddress,
|
|
1573
|
-
value: 0n,
|
|
1574
|
-
...buildCreateCreditCalldata({ ...request, context: this.context })
|
|
1575
|
-
};
|
|
1576
|
-
}
|
|
1577
|
-
drawCredit(request) {
|
|
1578
|
-
return {
|
|
1579
|
-
to: this.context.protocolAddress,
|
|
1580
|
-
value: 0n,
|
|
1581
|
-
...buildCreditDrawCalldata({ ...request, context: this.context })
|
|
1582
|
-
};
|
|
1583
|
-
}
|
|
1584
|
-
cancelCredit(request) {
|
|
1585
|
-
return {
|
|
1586
|
-
to: this.context.protocolAddress,
|
|
1587
|
-
value: 0n,
|
|
1588
|
-
...buildCancelCreditCalldata({ ...request, context: this.context })
|
|
1589
|
-
};
|
|
1590
|
-
}
|
|
1591
|
-
topUpCredit(request) {
|
|
1592
|
-
return {
|
|
1593
|
-
to: this.context.protocolAddress,
|
|
1594
|
-
value: 0n,
|
|
1595
|
-
...buildTopUpCreditCalldata({ ...request, context: this.context })
|
|
1596
|
-
};
|
|
1597
|
-
}
|
|
1598
|
-
};
|
|
1599
|
-
|
|
1600
|
-
// src/Protocol.ts
|
|
1601
|
-
var Protocol = class _Protocol {
|
|
1602
|
-
deployment;
|
|
1603
|
-
context;
|
|
1604
|
-
constructor(options) {
|
|
1605
|
-
known(options, ["deployment"], "protocol options");
|
|
1606
|
-
this.deployment = defineDeployment(options.deployment);
|
|
1607
|
-
this.context = Object.freeze({
|
|
1608
|
-
chainId: this.deployment.chainId,
|
|
1609
|
-
protocolAddress: this.deployment.protocol.address
|
|
1610
|
-
});
|
|
1611
|
-
Object.freeze(this);
|
|
1612
|
-
}
|
|
1613
|
-
/** Load the selected deployment and verify its contract against Chain RPC before returning. */
|
|
1614
|
-
static async fromRpc(rpc, provider, options = {}) {
|
|
1615
|
-
const chainId = (await provider.getNetwork()).chainId;
|
|
1616
|
-
if (typeof chainId !== "bigint" || chainId <= 0n || chainId >= 1n << 64n)
|
|
1617
|
-
throw new Error("invalid protocol chainId");
|
|
1618
|
-
const deployment = await rpc.getDeployment(
|
|
1619
|
-
chainId,
|
|
1620
|
-
options.signal ? { signal: options.signal } : {}
|
|
1621
|
-
);
|
|
1622
|
-
const protocol = new _Protocol({ deployment });
|
|
1623
|
-
if (protocol.context.chainId !== chainId) throw new Error("RPC deployment chain mismatch");
|
|
1624
|
-
await protocol.validateDeployment(provider);
|
|
1625
|
-
return protocol;
|
|
1626
|
-
}
|
|
1627
|
-
async validateDeployment(connection) {
|
|
1628
|
-
const reader = "getNetwork" in connection ? {
|
|
1629
|
-
getChainId: async () => (await connection.getNetwork()).chainId,
|
|
1630
|
-
getCode: async (address) => await connection.getCode(address),
|
|
1631
|
-
getProtocolAddressRegistry: async (address) => getAddress8(
|
|
1632
|
-
await new Contract2(
|
|
1633
|
-
address,
|
|
1634
|
-
["function addressRegistry() view returns (address)"],
|
|
1635
|
-
connection
|
|
1636
|
-
).getFunction("addressRegistry")()
|
|
1637
|
-
),
|
|
1638
|
-
getProtocolVersion: async (address) => BigInt(
|
|
1639
|
-
await new Contract2(
|
|
1640
|
-
address,
|
|
1641
|
-
["function VERSION() view returns (uint256)"],
|
|
1642
|
-
connection
|
|
1643
|
-
).getFunction("VERSION")()
|
|
1644
|
-
)
|
|
1645
|
-
} : connection;
|
|
1646
|
-
if (await reader.getChainId() !== this.context.chainId)
|
|
1647
|
-
throw new Error("deployment chain mismatch");
|
|
1648
|
-
const code = await reader.getCode(this.context.protocolAddress);
|
|
1649
|
-
if (!/^0x(?:[0-9a-fA-F]{2})+$/.test(code) || keccak2567(code) !== this.deployment.protocol.runtimeCodeHash)
|
|
1650
|
-
throw new Error("protocol runtime code profile mismatch");
|
|
1651
|
-
if (reader.getProtocolVersion && await reader.getProtocolVersion(this.context.protocolAddress) !== 1n)
|
|
1652
|
-
throw new Error("unsupported onchain protocol version");
|
|
1653
|
-
const registry = this.deployment.registry;
|
|
1654
|
-
if (registry) {
|
|
1655
|
-
if (!reader.getProtocolAddressRegistry)
|
|
1656
|
-
throw new Error("registry binding reader capability is missing");
|
|
1657
|
-
if (getAddress8(
|
|
1658
|
-
await reader.getProtocolAddressRegistry(this.context.protocolAddress)
|
|
1659
|
-
) !== getAddress8(registry.address))
|
|
1660
|
-
throw new Error("protocol registry binding mismatch");
|
|
1661
|
-
const registryCode = await reader.getCode(registry.address);
|
|
1662
|
-
if (!/^0x(?:[0-9a-fA-F]{2})+$/.test(registryCode) || keccak2567(registryCode) !== registry.runtimeCodeHash)
|
|
1663
|
-
throw new Error("registry runtime code profile mismatch");
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
aggregate(request) {
|
|
1667
|
-
return {
|
|
1668
|
-
to: this.context.protocolAddress,
|
|
1669
|
-
value: 0n,
|
|
1670
|
-
...buildTransactAggregateCalldata({ ...request, context: this.context })
|
|
1671
|
-
};
|
|
1672
|
-
}
|
|
1673
|
-
/** Build an operation context for one asset within this same deployment. */
|
|
1674
|
-
forToken(tokenAddress) {
|
|
1675
|
-
return new TokenProtocol(
|
|
1676
|
-
{ ...this.context, tokenAddress: getAddress8(tokenAddress) },
|
|
1677
|
-
{ expectedRuntimeCodeHash: this.deployment.protocol.runtimeCodeHash }
|
|
1678
|
-
);
|
|
1679
|
-
}
|
|
1680
|
-
};
|
|
1681
|
-
|
|
1682
|
-
// src/verify.ts
|
|
1683
|
-
import {
|
|
1684
|
-
computeCreditCommitment,
|
|
1685
|
-
computeCreditSharedKeyHash,
|
|
1686
|
-
computeNoteCommitment as computeNoteCommitment2,
|
|
1687
|
-
computeNoteIdentity,
|
|
1688
|
-
FIELD_MODULUS as FIELD_MODULUS2
|
|
1689
|
-
} from "@invisi-labs/core";
|
|
1690
|
-
async function verifyNoteCommitment(params) {
|
|
1691
|
-
const expected = await computeNoteCommitment2(
|
|
1692
|
-
params.chainId,
|
|
1693
|
-
await computeNoteIdentity(BigInt(params.ownerAddress), BigInt(params.nonce)),
|
|
1694
|
-
BigInt(params.tokenAddress),
|
|
1695
|
-
params.amount
|
|
1696
|
-
);
|
|
1697
|
-
return BigInt(expected) === BigInt(params.noteCommitment);
|
|
1698
|
-
}
|
|
1699
|
-
async function verifyCreditCommitment(params) {
|
|
1700
|
-
if (params.creditSharedKey <= 0n || params.creditSharedKey >= FIELD_MODULUS2)
|
|
1701
|
-
throw new RangeError("creditSharedKey must be a nonzero canonical Field");
|
|
1702
|
-
const expected = await computeCreditCommitment(
|
|
1703
|
-
params.chainId,
|
|
1704
|
-
params.spenderWalletIdentity,
|
|
1705
|
-
params.ownerWalletIdentity,
|
|
1706
|
-
BigInt(params.spenderPayout),
|
|
1707
|
-
BigInt(params.ownerPayout),
|
|
1708
|
-
BigInt(params.tokenAddress),
|
|
1709
|
-
params.amount,
|
|
1710
|
-
params.nonce,
|
|
1711
|
-
params.expiry,
|
|
1712
|
-
await computeCreditSharedKeyHash(params.creditSharedKey),
|
|
1713
|
-
params
|
|
1714
|
-
);
|
|
1715
|
-
return BigInt(expected) === BigInt(params.creditCommitment);
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
// src/witness.ts
|
|
1719
|
-
import {
|
|
1720
|
-
computeCreditCommitment as computeCreditCommitment2,
|
|
1721
|
-
normalizeCreditPolicy,
|
|
1722
|
-
applyCreditDrawUsage,
|
|
1723
|
-
computeCreditNullifier,
|
|
1724
|
-
computeCreditSharedKeyHash as computeCreditSharedKeyHash2,
|
|
1725
|
-
computeNoteCommitment as computeNoteCommitment3,
|
|
1726
|
-
computeNoteIdentity as computeNoteIdentity2,
|
|
1727
|
-
computeNoteNullifier,
|
|
1728
|
-
computeWalletAuthorizationDigest as computeWalletAuthorizationDigest3,
|
|
1729
|
-
normalizeCreditDeliveryState,
|
|
1730
|
-
poseidonHash,
|
|
1731
|
-
verifyWalletAuthorization
|
|
1732
|
-
} from "@invisi-labs/core";
|
|
1733
|
-
import {
|
|
1734
|
-
LocalWitness
|
|
1735
|
-
} from "@invisi-labs/prover";
|
|
1736
|
-
|
|
1737
190
|
// src/witness-layouts.json
|
|
1738
191
|
var witness_layouts_default = {
|
|
1739
192
|
transact_user_slots14: {
|
|
@@ -3178,541 +1631,2163 @@ var witness_layouts_default = {
|
|
|
3178
1631
|
count: 1,
|
|
3179
1632
|
array: false
|
|
3180
1633
|
}
|
|
3181
|
-
],
|
|
3182
|
-
private: [
|
|
3183
|
-
"input_amounts",
|
|
3184
|
-
"input_nonces",
|
|
3185
|
-
"leaf_indices",
|
|
3186
|
-
"merkle_paths",
|
|
3187
|
-
"input_source_indices",
|
|
3188
|
-
"public_key_x",
|
|
3189
|
-
"public_key_y",
|
|
3190
|
-
"signature",
|
|
3191
|
-
"spender_payout",
|
|
3192
|
-
"spender_wallet_identity",
|
|
3193
|
-
"owner_wallet_identity",
|
|
3194
|
-
"credit_amount",
|
|
3195
|
-
"credit_nonce",
|
|
3196
|
-
"credit_shared_key",
|
|
3197
|
-
"single_limit",
|
|
3198
|
-
"daily_limit",
|
|
3199
|
-
"monthly_limit",
|
|
3200
|
-
"previous_single_limit",
|
|
3201
|
-
"previous_daily_limit",
|
|
3202
|
-
"previous_monthly_limit",
|
|
3203
|
-
"last_day",
|
|
3204
|
-
"daily_spent",
|
|
3205
|
-
"last_month",
|
|
3206
|
-
"monthly_spent",
|
|
3207
|
-
"change_amount",
|
|
3208
|
-
"change_nonce",
|
|
3209
|
-
"previous_credit_amount",
|
|
3210
|
-
"previous_credit_nonce",
|
|
3211
|
-
"previous_credit_leaf_index",
|
|
3212
|
-
"previous_credit_merkle_path"
|
|
3213
|
-
]
|
|
1634
|
+
],
|
|
1635
|
+
private: [
|
|
1636
|
+
"input_amounts",
|
|
1637
|
+
"input_nonces",
|
|
1638
|
+
"leaf_indices",
|
|
1639
|
+
"merkle_paths",
|
|
1640
|
+
"input_source_indices",
|
|
1641
|
+
"public_key_x",
|
|
1642
|
+
"public_key_y",
|
|
1643
|
+
"signature",
|
|
1644
|
+
"spender_payout",
|
|
1645
|
+
"spender_wallet_identity",
|
|
1646
|
+
"owner_wallet_identity",
|
|
1647
|
+
"credit_amount",
|
|
1648
|
+
"credit_nonce",
|
|
1649
|
+
"credit_shared_key",
|
|
1650
|
+
"single_limit",
|
|
1651
|
+
"daily_limit",
|
|
1652
|
+
"monthly_limit",
|
|
1653
|
+
"previous_single_limit",
|
|
1654
|
+
"previous_daily_limit",
|
|
1655
|
+
"previous_monthly_limit",
|
|
1656
|
+
"last_day",
|
|
1657
|
+
"daily_spent",
|
|
1658
|
+
"last_month",
|
|
1659
|
+
"monthly_spent",
|
|
1660
|
+
"change_amount",
|
|
1661
|
+
"change_nonce",
|
|
1662
|
+
"previous_credit_amount",
|
|
1663
|
+
"previous_credit_nonce",
|
|
1664
|
+
"previous_credit_leaf_index",
|
|
1665
|
+
"previous_credit_merkle_path"
|
|
1666
|
+
]
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
|
|
1670
|
+
// src/authorization/common.ts
|
|
1671
|
+
var pad2 = (values, count, zero) => {
|
|
1672
|
+
if (values.length > count) throw new Error("witness capacity exceeded");
|
|
1673
|
+
return [...values, ...Array.from({ length: count - values.length }, zero)];
|
|
1674
|
+
};
|
|
1675
|
+
var policyWitness = (note, usage = true) => {
|
|
1676
|
+
const p = normalizeCreditPolicy(note);
|
|
1677
|
+
return {
|
|
1678
|
+
single_limit: p.singleLimit,
|
|
1679
|
+
daily_limit: p.dailyLimit,
|
|
1680
|
+
monthly_limit: p.monthlyLimit,
|
|
1681
|
+
...usage ? {
|
|
1682
|
+
last_day: p.lastDay,
|
|
1683
|
+
daily_spent: p.dailySpent,
|
|
1684
|
+
last_month: p.lastMonth,
|
|
1685
|
+
monthly_spent: p.monthlySpent
|
|
1686
|
+
} : {}
|
|
1687
|
+
};
|
|
1688
|
+
};
|
|
1689
|
+
var zeros = () => Array(16).fill(0n);
|
|
1690
|
+
var zeroAuthority = () => ({
|
|
1691
|
+
public_key_x: Array(32).fill(0),
|
|
1692
|
+
public_key_y: Array(32).fill(0),
|
|
1693
|
+
signature: Array(64).fill(0)
|
|
1694
|
+
});
|
|
1695
|
+
function check(ok, message) {
|
|
1696
|
+
if (!ok) throw new Error(message);
|
|
1697
|
+
}
|
|
1698
|
+
function provingRequest(circuitName, privateInputs, expectedPublicInputs, target = "evm") {
|
|
1699
|
+
const schema = witness_layouts_default[circuitName], key = schema.keys[target];
|
|
1700
|
+
check(typeof key === "string", "unpublished proof target");
|
|
1701
|
+
check(
|
|
1702
|
+
expectedPublicInputs.length === schema.publicInputCount,
|
|
1703
|
+
"public input order/width mismatch"
|
|
1704
|
+
);
|
|
1705
|
+
check(
|
|
1706
|
+
Object.keys(privateInputs).length === schema.private.length && schema.private.every((name) => Object.hasOwn(privateInputs, name)),
|
|
1707
|
+
"private witness schema mismatch"
|
|
1708
|
+
);
|
|
1709
|
+
let offset = 0;
|
|
1710
|
+
const publicInputs = {};
|
|
1711
|
+
for (const entry of schema.public) {
|
|
1712
|
+
const values = expectedPublicInputs.slice(offset, offset + entry.count).map((value) => {
|
|
1713
|
+
const n = BigInt(value);
|
|
1714
|
+
check(n >= 0n && n < FIELD_MODULUS, "noncanonical public input");
|
|
1715
|
+
return n;
|
|
1716
|
+
});
|
|
1717
|
+
publicInputs[entry.name] = entry.array ? values : values[0];
|
|
1718
|
+
offset += entry.count;
|
|
1719
|
+
}
|
|
1720
|
+
check(offset === expectedPublicInputs.length, "public input layout mismatch");
|
|
1721
|
+
return {
|
|
1722
|
+
circuitName,
|
|
1723
|
+
target,
|
|
1724
|
+
artifactSha256: schema.artifactSha256,
|
|
1725
|
+
vkSha256: key,
|
|
1726
|
+
expectedPublicInputs: [...expectedPublicInputs],
|
|
1727
|
+
witness: new LocalWitness({ ...privateInputs, ...publicInputs })
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
function witnessStatement(circuitName, expectedPublicInputs, target = "evm") {
|
|
1731
|
+
const schema = witness_layouts_default[circuitName], key = schema.keys[target];
|
|
1732
|
+
check(
|
|
1733
|
+
typeof key === "string" && expectedPublicInputs.length === schema.publicInputCount,
|
|
1734
|
+
"unpublished target or statement width mismatch"
|
|
1735
|
+
);
|
|
1736
|
+
return {
|
|
1737
|
+
circuitName,
|
|
1738
|
+
target,
|
|
1739
|
+
artifactSha256: schema.artifactSha256,
|
|
1740
|
+
vkSha256: key,
|
|
1741
|
+
expectedPublicInputs: [...expectedPublicInputs]
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// src/authorization/WitnessContext.ts
|
|
1746
|
+
var WitnessContext = class {
|
|
1747
|
+
constructor(protocol) {
|
|
1748
|
+
this.protocol = protocol;
|
|
1749
|
+
}
|
|
1750
|
+
context(note) {
|
|
1751
|
+
const c = this.protocol.context;
|
|
1752
|
+
check(
|
|
1753
|
+
note.chainId === c.chainId && note.protocolAddress === BigInt(c.protocolAddress) && note.tokenAddress === BigInt(c.tokenAddress),
|
|
1754
|
+
"note context mismatch"
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
async authority(operation, capacity, words, signed, identity) {
|
|
1758
|
+
check(!!signed, "private wallet authorization required");
|
|
1759
|
+
const c = this.protocol.context, request = {
|
|
1760
|
+
context: { chainId: c.chainId, protocolAddress: BigInt(c.protocolAddress) },
|
|
1761
|
+
operation,
|
|
1762
|
+
capacity,
|
|
1763
|
+
words: words.slice(0, -2).map(BigInt)
|
|
1764
|
+
};
|
|
1765
|
+
check(
|
|
1766
|
+
signed.ownerAddress === identity && signed.digest === computeWalletAuthorizationDigest(request),
|
|
1767
|
+
"authorization statement mismatch"
|
|
1768
|
+
);
|
|
1769
|
+
const witness = signed.withWitness((value) => value);
|
|
1770
|
+
check(
|
|
1771
|
+
await verifyWalletAuthorization(
|
|
1772
|
+
identity,
|
|
1773
|
+
Uint8Array.from([4, ...witness.public_key_x, ...witness.public_key_y]),
|
|
1774
|
+
Uint8Array.from(witness.signature),
|
|
1775
|
+
request
|
|
1776
|
+
),
|
|
1777
|
+
"invalid wallet authorization"
|
|
1778
|
+
);
|
|
1779
|
+
return witness;
|
|
1780
|
+
}
|
|
1781
|
+
async membership(leaf, input, sources) {
|
|
1782
|
+
const source = sources.findIndex((value) => value.treeId === input.treeId);
|
|
1783
|
+
check(
|
|
1784
|
+
source >= 0 && Number.isInteger(input.leafIndex) && input.leafIndex >= 0 && input.leafIndex < 65536 && input.path.length === 16,
|
|
1785
|
+
"invalid membership position"
|
|
1786
|
+
);
|
|
1787
|
+
let root = BigInt(leaf);
|
|
1788
|
+
for (let level = 0; level < 16; level++)
|
|
1789
|
+
root = BigInt(
|
|
1790
|
+
await poseidonHash(
|
|
1791
|
+
input.leafIndex >> level & 1 ? [input.path[level], root] : [root, input.path[level]]
|
|
1792
|
+
)
|
|
1793
|
+
);
|
|
1794
|
+
check(root === BigInt(sources[source].merkleRoot), "membership root mismatch");
|
|
1795
|
+
return source;
|
|
1796
|
+
}
|
|
1797
|
+
async noteLeaf(note) {
|
|
1798
|
+
this.context(note);
|
|
1799
|
+
return computeNoteCommitment(
|
|
1800
|
+
note.chainId,
|
|
1801
|
+
await computeNoteIdentity(BigInt(note.ownerAddress), note.nonce),
|
|
1802
|
+
note.tokenAddress,
|
|
1803
|
+
note.amount
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
async creditLeaf(note) {
|
|
1807
|
+
this.context(note);
|
|
1808
|
+
return computeCreditCommitment(
|
|
1809
|
+
note.chainId,
|
|
1810
|
+
note.spenderWalletIdentity,
|
|
1811
|
+
note.ownerWalletIdentity,
|
|
1812
|
+
note.spenderPayout,
|
|
1813
|
+
note.ownerPayout,
|
|
1814
|
+
note.tokenAddress,
|
|
1815
|
+
note.amount,
|
|
1816
|
+
note.nonce,
|
|
1817
|
+
note.expiry,
|
|
1818
|
+
await computeCreditSharedKeyHash(note.creditSharedKey),
|
|
1819
|
+
note
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
async noteInputs(inputs, sources, nullifiers, identity, authority) {
|
|
1823
|
+
check(inputs.length === nullifiers.length, "input count mismatch");
|
|
1824
|
+
return Promise.all(
|
|
1825
|
+
inputs.map(async (input, i) => {
|
|
1826
|
+
check(input.note.ownerAddress === identity, "input wallet mismatch");
|
|
1827
|
+
const leaf = await this.noteLeaf(input.note);
|
|
1828
|
+
check(
|
|
1829
|
+
await computeNoteNullifier(input.note.chainId, input.note.nonce, leaf) === nullifiers[i],
|
|
1830
|
+
"input nullifier mismatch"
|
|
1831
|
+
);
|
|
1832
|
+
return BigInt(await this.membership(leaf, input, sources));
|
|
1833
|
+
})
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
|
|
1838
|
+
// src/authorization/credit-cancel.ts
|
|
1839
|
+
import { computeCreditNullifier, normalizeCreditDeliveryState } from "@invisi-labs/core";
|
|
1840
|
+
|
|
1841
|
+
// src/credit/credit-lifecycle.ts
|
|
1842
|
+
import { AbiCoder as AbiCoder3, getAddress as getAddress4, Interface as Interface2, keccak256 as keccak2563, ParamType as ParamType3 } from "ethers";
|
|
1843
|
+
|
|
1844
|
+
// src/fees/fee.ts
|
|
1845
|
+
import { getAddress as getAddress2 } from "ethers";
|
|
1846
|
+
function normalizeFee(p) {
|
|
1847
|
+
const feeRecipient = getAddress2(p.feeRecipient ?? ZERO_ADDRESS);
|
|
1848
|
+
const feeAmount = uint(p.feeAmount ?? 0n, 128, "feeAmount");
|
|
1849
|
+
if (BigInt(feeRecipient) === 0n !== (feeAmount === 0n))
|
|
1850
|
+
throw new Error("invalid fee context");
|
|
1851
|
+
return { feeRecipient, feeAmount };
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/credit/credit-lifecycle.ts
|
|
1855
|
+
import {
|
|
1856
|
+
computeWalletAuthorizationDigest as computeWalletAuthorizationDigest3,
|
|
1857
|
+
creditPeriods,
|
|
1858
|
+
walletAuthorizationTypedData as walletAuthorizationTypedData2
|
|
1859
|
+
} from "@invisi-labs/core";
|
|
1860
|
+
|
|
1861
|
+
// src/credit/credit-create.ts
|
|
1862
|
+
import {
|
|
1863
|
+
AbiCoder as AbiCoder2,
|
|
1864
|
+
dataLength as dataLength2,
|
|
1865
|
+
getAddress as getAddress3,
|
|
1866
|
+
Interface,
|
|
1867
|
+
isHexString as isHexString2,
|
|
1868
|
+
keccak256 as keccak2562,
|
|
1869
|
+
ParamType as ParamType2
|
|
1870
|
+
} from "ethers";
|
|
1871
|
+
import { computeWalletAuthorizationDigest as computeWalletAuthorizationDigest2, walletAuthorizationTypedData } from "@invisi-labs/core";
|
|
1872
|
+
var creationTuple = [
|
|
1873
|
+
"(address token,(uint256 treeId,bytes32 merkleRoot)[] sources,bytes32[] nullifiers,bytes32 creditCommitment,bytes32 changeCommitment,address owner,uint256 expiry,uint256 deadline,address feeRecipient,uint256 feeAmount)"
|
|
1874
|
+
][0];
|
|
1875
|
+
var CREATE_CREDIT_FIELDS = [
|
|
1876
|
+
"token",
|
|
1877
|
+
"sources",
|
|
1878
|
+
"nullifiers",
|
|
1879
|
+
"creditCommitment",
|
|
1880
|
+
"changeCommitment",
|
|
1881
|
+
"owner",
|
|
1882
|
+
"expiry",
|
|
1883
|
+
"deadline",
|
|
1884
|
+
"feeRecipient",
|
|
1885
|
+
"feeAmount"
|
|
1886
|
+
];
|
|
1887
|
+
function validateCreateCreditRequest(c, p, cs, funding = {}) {
|
|
1888
|
+
known(p, CREATE_CREDIT_FIELDS);
|
|
1889
|
+
if (getAddress3(p.token) !== getAddress3(c.tokenAddress))
|
|
1890
|
+
throw new Error("request token mismatch");
|
|
1891
|
+
const ctx = normalizeContext(c), ns = notes(p.nullifiers, "nullifiers"), sources = normalizeSources(p.sources, ns.length, 10), creditCommitment = word(field(p.creditCommitment)), changeCommitment = word(field(p.changeCommitment)), owner = getAddress3(p.owner), expiry = uint(p.expiry, 64, "expiry"), deadline = checkDeadline(c, p.deadline), f = normalizeFunding(funding);
|
|
1892
|
+
if (BigInt(creditCommitment) === 0n || creditCommitment === changeCommitment)
|
|
1893
|
+
throw new Error("zero or duplicate commitment");
|
|
1894
|
+
if (BigInt(owner) === 0n || expiry === 0n || c.now !== void 0 && expiry <= c.now)
|
|
1895
|
+
throw new Error("owner must be nonzero and expiry future");
|
|
1896
|
+
if (dense(cs, "deliveries").length !== 3) throw new Error("deliveries count mismatch");
|
|
1897
|
+
const deliveries = cs.map(
|
|
1898
|
+
(v, i) => normalizeOpaqueDelivery(v, {
|
|
1899
|
+
empty: i === 2 && BigInt(changeCommitment) === 0n,
|
|
1900
|
+
allowEmpty: i === 2
|
|
1901
|
+
})
|
|
1902
|
+
);
|
|
1903
|
+
if (ns.length === 0 && f.amount === 0n)
|
|
1904
|
+
throw new Error("empty inputs require positive funding");
|
|
1905
|
+
return {
|
|
1906
|
+
...ctx,
|
|
1907
|
+
...normalizeFee(p),
|
|
1908
|
+
sources,
|
|
1909
|
+
nullifiers: ns,
|
|
1910
|
+
actualCount: ns.length,
|
|
1911
|
+
sourceCount: sources.length,
|
|
1912
|
+
creditCommitment,
|
|
1913
|
+
changeCommitment,
|
|
1914
|
+
owner,
|
|
1915
|
+
expiry,
|
|
1916
|
+
deadline,
|
|
1917
|
+
deliveries,
|
|
1918
|
+
funding: f
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
function createCreditBaseInputs(c, p, cs, f = {}) {
|
|
1922
|
+
const r = validateCreateCreditRequest(c, p, cs, f), h = BigInt(deliveriesHash(cs));
|
|
1923
|
+
return [
|
|
1924
|
+
r.chainId,
|
|
1925
|
+
BigInt(r.protocolAddress),
|
|
1926
|
+
BigInt(r.tokenAddress),
|
|
1927
|
+
BigInt(r.owner),
|
|
1928
|
+
BigInt(r.actualCount),
|
|
1929
|
+
BigInt(r.sourceCount),
|
|
1930
|
+
...pad(
|
|
1931
|
+
r.sources.map((s) => s.treeId),
|
|
1932
|
+
10
|
|
1933
|
+
),
|
|
1934
|
+
...pad(
|
|
1935
|
+
r.sources.map((s) => BigInt(s.merkleRoot)),
|
|
1936
|
+
10
|
|
1937
|
+
),
|
|
1938
|
+
...pad(r.nullifiers.map(BigInt), 10),
|
|
1939
|
+
BigInt(r.creditCommitment),
|
|
1940
|
+
BigInt(r.changeCommitment),
|
|
1941
|
+
h >> 128n,
|
|
1942
|
+
h & (1n << 128n) - 1n,
|
|
1943
|
+
r.expiry,
|
|
1944
|
+
r.deadline,
|
|
1945
|
+
r.funding.amount,
|
|
1946
|
+
BigInt(r.funding.payer),
|
|
1947
|
+
BigInt(r.feeRecipient),
|
|
1948
|
+
r.feeAmount
|
|
1949
|
+
].map(word);
|
|
1950
|
+
}
|
|
1951
|
+
function createCreditPublicInputs(c, p, deliveries, funding = {}) {
|
|
1952
|
+
const base = createCreditBaseInputs(c, p, deliveries, funding);
|
|
1953
|
+
const digest = BigInt(
|
|
1954
|
+
computeWalletAuthorizationDigest2({
|
|
1955
|
+
context: { chainId: c.chainId, protocolAddress: BigInt(c.protocolAddress) },
|
|
1956
|
+
operation: "create-credit",
|
|
1957
|
+
capacity: 10,
|
|
1958
|
+
words: base.map(BigInt)
|
|
1959
|
+
})
|
|
1960
|
+
);
|
|
1961
|
+
return [...base, word(digest >> 128n), word(digest & (1n << 128n) - 1n)];
|
|
1962
|
+
}
|
|
1963
|
+
var hashContext = {
|
|
1964
|
+
chainId: 0n,
|
|
1965
|
+
protocolAddress: ZERO_ADDRESS,
|
|
1966
|
+
tokenAddress: ZERO_ADDRESS
|
|
1967
|
+
};
|
|
1968
|
+
function createCreditRequestHash(p, cs, f = {}) {
|
|
1969
|
+
const r = validateCreateCreditRequest({ ...hashContext, tokenAddress: p.token }, p, cs, f);
|
|
1970
|
+
return keccak2562(
|
|
1971
|
+
AbiCoder2.defaultAbiCoder().encode(
|
|
1972
|
+
[
|
|
1973
|
+
{ type: "bytes32" },
|
|
1974
|
+
creationTuple,
|
|
1975
|
+
{ type: "bytes32" },
|
|
1976
|
+
{ type: "address" },
|
|
1977
|
+
{ type: "uint256" }
|
|
1978
|
+
].map((parameter) => ParamType2.from(parameter)),
|
|
1979
|
+
[
|
|
1980
|
+
keccak2562(new TextEncoder().encode("InvisiCreateCredit")),
|
|
1981
|
+
{ ...p, ...normalizeFee(p) },
|
|
1982
|
+
deliveriesHash(cs),
|
|
1983
|
+
r.funding.payer,
|
|
1984
|
+
r.funding.amount
|
|
1985
|
+
]
|
|
1986
|
+
)
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
function encodeCreateCreditTypedData(c, p, deliveries, funding = {}) {
|
|
1990
|
+
return walletAuthorizationTypedData({
|
|
1991
|
+
context: { chainId: c.chainId, protocolAddress: BigInt(c.protocolAddress) },
|
|
1992
|
+
operation: "create-credit",
|
|
1993
|
+
capacity: 10,
|
|
1994
|
+
words: createCreditBaseInputs(c, p, deliveries, funding).map(BigInt)
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
function encodeCreateCreditFundingTypedData(c, p, cs, f) {
|
|
1998
|
+
const r = validateCreateCreditRequest(c, p, cs, f);
|
|
1999
|
+
if (r.funding.amount === 0n) throw new Error("funding authorization requires positive deposit");
|
|
2000
|
+
return {
|
|
2001
|
+
domain: getEIP712Domain(c),
|
|
2002
|
+
primaryType: "FundTransact",
|
|
2003
|
+
types: { FundTransact: fundingFields },
|
|
2004
|
+
message: {
|
|
2005
|
+
requestHash: createCreditRequestHash(p, cs, f),
|
|
2006
|
+
payer: r.funding.payer,
|
|
2007
|
+
amount: r.funding.amount,
|
|
2008
|
+
sigNonce: r.funding.sigNonce,
|
|
2009
|
+
deadline: r.funding.deadline
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
function buildCreateCreditCalldata(p) {
|
|
2014
|
+
const r = validateCreateCreditRequest(p.context, p.params, p.deliveries, p.funding);
|
|
2015
|
+
validateFundingSubmission(r.funding, p.caller, p.context.now);
|
|
2016
|
+
return {
|
|
2017
|
+
data: new Interface(invisiProtocolAbi).encodeFunctionData("createCredit", [
|
|
2018
|
+
encodeCreditProof(p.proof),
|
|
2019
|
+
{ ...p.params, ...normalizeFee(p.params) },
|
|
2020
|
+
p.deliveries,
|
|
2021
|
+
r.funding
|
|
2022
|
+
]),
|
|
2023
|
+
approveAmount: r.funding.amount
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
function encodeCreditProof(proof) {
|
|
2027
|
+
if (!isHexString2(proof) || !/^0x(?:[a-fA-F0-9]{2})*$/.test(proof) || dataLength2(proof) === 0)
|
|
2028
|
+
throw new Error("empty or malformed Credit proof");
|
|
2029
|
+
return AbiCoder2.defaultAbiCoder().encode(
|
|
2030
|
+
["uint8", "bytes"].map((parameter) => ParamType2.from(parameter)),
|
|
2031
|
+
[0, proof]
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// src/credit/credit-lifecycle.ts
|
|
2036
|
+
var CREDIT_DRAW_CAPACITIES = [1, 12, 50, 120];
|
|
2037
|
+
function selectCreditDrawCircuit(count) {
|
|
2038
|
+
if (!Number.isInteger(count) || count < 1 || count > 120)
|
|
2039
|
+
throw new Error("draw requires 1..120 credits");
|
|
2040
|
+
const maxSize = CREDIT_DRAW_CAPACITIES.find((n) => count <= n);
|
|
2041
|
+
return {
|
|
2042
|
+
name: `credit_draw_slots${maxSize}`,
|
|
2043
|
+
maxSize,
|
|
2044
|
+
publicInputCount: 6 * maxSize + 14,
|
|
2045
|
+
totalBits: 126 + Math.ceil(Math.log2(maxSize))
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
function validateCreditDrawRequest(c, p, cs) {
|
|
2049
|
+
known(p, [
|
|
2050
|
+
"token",
|
|
2051
|
+
"sources",
|
|
2052
|
+
"nullifiers",
|
|
2053
|
+
"remainderCommitments",
|
|
2054
|
+
"expiries",
|
|
2055
|
+
"recoveryAmounts",
|
|
2056
|
+
"spender",
|
|
2057
|
+
"totalAmount",
|
|
2058
|
+
"deadline"
|
|
2059
|
+
]);
|
|
2060
|
+
if (getAddress4(p.token) !== getAddress4(c.tokenAddress))
|
|
2061
|
+
throw new Error("request token mismatch");
|
|
2062
|
+
const ctx = normalizeContext(c), ns = notes(p.nullifiers, "nullifiers"), profile = selectCreditDrawCircuit(ns.length), sources = normalizeSources(p.sources, ns.length, profile.maxSize);
|
|
2063
|
+
for (const [key, v] of Object.entries({
|
|
2064
|
+
remainderCommitments: p.remainderCommitments,
|
|
2065
|
+
expiries: p.expiries,
|
|
2066
|
+
recoveryAmounts: p.recoveryAmounts
|
|
2067
|
+
}))
|
|
2068
|
+
if (dense(v, key).length !== ns.length) throw new Error(`${key} count mismatch`);
|
|
2069
|
+
const remainderCommitments = p.remainderCommitments.map((v) => word(field(v))), recoveryAmounts = p.recoveryAmounts.map((v) => word(field(v))), expiries = p.expiries.map((v) => uint(v, 64, "expiry")), positives = remainderCommitments.filter((v) => BigInt(v) !== 0n);
|
|
2070
|
+
if (new Set(positives).size !== positives.length)
|
|
2071
|
+
throw new Error("duplicate remainder commitment");
|
|
2072
|
+
for (let i = 0; i < ns.length; i++) {
|
|
2073
|
+
if (expiries[i] === 0n || c.now !== void 0 && expiries[i] <= c.now)
|
|
2074
|
+
throw new Error("expiry must be future");
|
|
2075
|
+
if (BigInt(remainderCommitments[i]) === 0n && BigInt(recoveryAmounts[i]) !== 0n)
|
|
2076
|
+
throw new Error("full draw requires zero recovery amount");
|
|
2077
|
+
}
|
|
2078
|
+
const spender = getAddress4(p.spender), totalAmount = uint(p.totalAmount, profile.totalBits, "totalAmount");
|
|
2079
|
+
if (BigInt(spender) === 0n || totalAmount === 0n)
|
|
2080
|
+
throw new Error("spender and total amount must be positive");
|
|
2081
|
+
if (dense(cs, "deliveries").length !== 2 * ns.length)
|
|
2082
|
+
throw new Error("deliveries count mismatch");
|
|
2083
|
+
const deliveries = cs.map(
|
|
2084
|
+
(v, i) => BigInt(remainderCommitments[Math.floor(i / 2)]) === 0n ? normalizeOpaqueDelivery(v, { empty: true }) : normalizeOpaqueDelivery(v, {})
|
|
2085
|
+
);
|
|
2086
|
+
return {
|
|
2087
|
+
...ctx,
|
|
2088
|
+
sources,
|
|
2089
|
+
nullifiers: ns,
|
|
2090
|
+
remainderCommitments,
|
|
2091
|
+
recoveryAmounts,
|
|
2092
|
+
expiries,
|
|
2093
|
+
spender,
|
|
2094
|
+
totalAmount,
|
|
2095
|
+
deadline: checkDeadline(c, p.deadline),
|
|
2096
|
+
deliveries,
|
|
2097
|
+
profile,
|
|
2098
|
+
periods: creditPeriods(c.timestamp)
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
function creditDrawBaseInputs(c, p, cs) {
|
|
2102
|
+
const r = validateCreditDrawRequest(c, p, cs), n = r.profile.maxSize, h = BigInt(deliveriesHash(cs));
|
|
2103
|
+
return [
|
|
2104
|
+
r.chainId,
|
|
2105
|
+
BigInt(r.protocolAddress),
|
|
2106
|
+
BigInt(r.tokenAddress),
|
|
2107
|
+
BigInt(r.spender),
|
|
2108
|
+
BigInt(r.nullifiers.length),
|
|
2109
|
+
BigInt(r.sources.length),
|
|
2110
|
+
...pad(
|
|
2111
|
+
r.sources.map((s) => s.treeId),
|
|
2112
|
+
n
|
|
2113
|
+
),
|
|
2114
|
+
...pad(
|
|
2115
|
+
r.sources.map((s) => BigInt(s.merkleRoot)),
|
|
2116
|
+
n
|
|
2117
|
+
),
|
|
2118
|
+
...pad(r.nullifiers.map(BigInt), n),
|
|
2119
|
+
...pad(r.remainderCommitments.map(BigInt), n),
|
|
2120
|
+
...pad(r.expiries, n),
|
|
2121
|
+
r.totalAmount,
|
|
2122
|
+
r.deadline,
|
|
2123
|
+
h >> 128n,
|
|
2124
|
+
h & (1n << 128n) - 1n,
|
|
2125
|
+
...pad(r.recoveryAmounts.map(BigInt), n),
|
|
2126
|
+
r.periods.currentDay,
|
|
2127
|
+
r.periods.currentMonth
|
|
2128
|
+
].map(word);
|
|
2129
|
+
}
|
|
2130
|
+
function validateCancelCreditRequest(c, p) {
|
|
2131
|
+
known(p, [
|
|
2132
|
+
"token",
|
|
2133
|
+
"source",
|
|
2134
|
+
"nullifier",
|
|
2135
|
+
"owner",
|
|
2136
|
+
"amount",
|
|
2137
|
+
"deadline",
|
|
2138
|
+
"feeRecipient",
|
|
2139
|
+
"feeAmount"
|
|
2140
|
+
]);
|
|
2141
|
+
if (getAddress4(p.token) !== getAddress4(c.tokenAddress))
|
|
2142
|
+
throw new Error("request token mismatch");
|
|
2143
|
+
const ctx = normalizeContext(c), source = normalizeSources([p.source], 1, 1)[0], nullifier = notes([p.nullifier], "nullifier")[0], owner = getAddress4(p.owner), amount = uint(p.amount, 126, "amount");
|
|
2144
|
+
if (BigInt(owner) === 0n || amount === 0n) throw new Error("owner and amount must be positive");
|
|
2145
|
+
const fee = normalizeFee(p);
|
|
2146
|
+
if (fee.feeAmount >= amount) throw new Error("fee must leave a positive owner refund");
|
|
2147
|
+
return {
|
|
2148
|
+
...ctx,
|
|
2149
|
+
...fee,
|
|
2150
|
+
source,
|
|
2151
|
+
nullifier,
|
|
2152
|
+
owner,
|
|
2153
|
+
amount,
|
|
2154
|
+
deadline: checkDeadline(c, p.deadline)
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
function cancelCreditBaseInputs(c, p) {
|
|
2158
|
+
const r = validateCancelCreditRequest(c, p);
|
|
2159
|
+
return [
|
|
2160
|
+
r.chainId,
|
|
2161
|
+
BigInt(r.protocolAddress),
|
|
2162
|
+
BigInt(r.tokenAddress),
|
|
2163
|
+
BigInt(r.owner),
|
|
2164
|
+
r.source.treeId,
|
|
2165
|
+
BigInt(r.source.merkleRoot),
|
|
2166
|
+
BigInt(r.nullifier),
|
|
2167
|
+
r.amount,
|
|
2168
|
+
r.deadline,
|
|
2169
|
+
BigInt(r.feeRecipient),
|
|
2170
|
+
r.feeAmount
|
|
2171
|
+
].map(word);
|
|
2172
|
+
}
|
|
2173
|
+
function validateTopUpCreditRequest(c, p, cs, funding = {}) {
|
|
2174
|
+
known(p, ["token", "creation", "creditSource", "creditNullifier", "previousExpiry"]);
|
|
2175
|
+
const creation2 = validateCreateCreditRequest(c, p.creation, cs, funding), creditSource = normalizeSources([p.creditSource], 1, 1)[0], creditNullifier = notes([p.creditNullifier], "creditNullifier")[0], previousExpiry = uint(p.previousExpiry, 64, "previousExpiry");
|
|
2176
|
+
if (previousExpiry === 0n || c.now !== void 0 && previousExpiry <= c.now)
|
|
2177
|
+
throw new Error("previous Credit has expired");
|
|
2178
|
+
if (creation2.expiry < previousExpiry) throw new Error("expiry must not decrease");
|
|
2179
|
+
if (creation2.nullifiers.includes(creditNullifier))
|
|
2180
|
+
throw new Error("duplicate credit and ordinary nullifier");
|
|
2181
|
+
return { creation: creation2, creditSource, creditNullifier, previousExpiry };
|
|
2182
|
+
}
|
|
2183
|
+
function topUpCreditBaseInputs(c, p, cs, f = {}) {
|
|
2184
|
+
const r = validateTopUpCreditRequest(c, p, cs, f);
|
|
2185
|
+
return [
|
|
2186
|
+
...createCreditBaseInputs(c, p.creation, cs, f),
|
|
2187
|
+
word(r.creditSource.treeId),
|
|
2188
|
+
r.creditSource.merkleRoot,
|
|
2189
|
+
r.creditNullifier,
|
|
2190
|
+
word(r.previousExpiry)
|
|
2191
|
+
];
|
|
2192
|
+
}
|
|
2193
|
+
var cancelTuple = [
|
|
2194
|
+
"(address token,(uint256 treeId,bytes32 merkleRoot) source,bytes32 nullifier,address owner,uint256 amount,uint256 deadline,address feeRecipient,uint256 feeAmount)"
|
|
2195
|
+
][0];
|
|
2196
|
+
var topTuple = [
|
|
2197
|
+
"((address token,(uint256 treeId,bytes32 merkleRoot)[] sources,bytes32[] nullifiers,bytes32 creditCommitment,bytes32 changeCommitment,address owner,uint256 expiry,uint256 deadline,address feeRecipient,uint256 feeAmount) creation,(uint256 treeId,bytes32 merkleRoot) creditSource,bytes32 creditNullifier,uint256 previousExpiry)"
|
|
2198
|
+
][0];
|
|
2199
|
+
var hashContext2 = {
|
|
2200
|
+
chainId: 0n,
|
|
2201
|
+
protocolAddress: ZERO_ADDRESS,
|
|
2202
|
+
tokenAddress: ZERO_ADDRESS
|
|
2203
|
+
};
|
|
2204
|
+
function cancelCreditRequestHash(p) {
|
|
2205
|
+
validateCancelCreditRequest({ ...hashContext2, tokenAddress: p.token }, p);
|
|
2206
|
+
return keccak2563(
|
|
2207
|
+
AbiCoder3.defaultAbiCoder().encode(
|
|
2208
|
+
[{ type: "bytes32" }, cancelTuple].map((parameter) => ParamType3.from(parameter)),
|
|
2209
|
+
[
|
|
2210
|
+
keccak2563(new TextEncoder().encode("InvisiCancelCredit")),
|
|
2211
|
+
{ ...p, ...normalizeFee(p) }
|
|
2212
|
+
]
|
|
2213
|
+
)
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
function topUpCreditRequestHash(p, cs, f = {}) {
|
|
2217
|
+
const r = validateTopUpCreditRequest(
|
|
2218
|
+
{ ...hashContext2, tokenAddress: p.creation.token },
|
|
2219
|
+
p,
|
|
2220
|
+
cs,
|
|
2221
|
+
f
|
|
2222
|
+
);
|
|
2223
|
+
return keccak2563(
|
|
2224
|
+
AbiCoder3.defaultAbiCoder().encode(
|
|
2225
|
+
[
|
|
2226
|
+
{ type: "bytes32" },
|
|
2227
|
+
topTuple,
|
|
2228
|
+
{ type: "bytes32" },
|
|
2229
|
+
{ type: "address" },
|
|
2230
|
+
{ type: "uint256" }
|
|
2231
|
+
].map((parameter) => ParamType3.from(parameter)),
|
|
2232
|
+
[
|
|
2233
|
+
keccak2563(new TextEncoder().encode("InvisiTopUpCredit")),
|
|
2234
|
+
{ ...p, creation: { ...p.creation, ...normalizeFee(p.creation) } },
|
|
2235
|
+
deliveriesHash(cs),
|
|
2236
|
+
r.creation.funding.payer,
|
|
2237
|
+
r.creation.funding.amount
|
|
2238
|
+
]
|
|
2239
|
+
)
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
function authorize(c, operation, capacity, words) {
|
|
2243
|
+
return walletAuthorizationTypedData2({
|
|
2244
|
+
context: { chainId: c.chainId, protocolAddress: BigInt(c.protocolAddress) },
|
|
2245
|
+
operation,
|
|
2246
|
+
capacity,
|
|
2247
|
+
words: words.map(BigInt)
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
function appendDigest(c, operation, capacity, words) {
|
|
2251
|
+
const digest = BigInt(
|
|
2252
|
+
computeWalletAuthorizationDigest3({
|
|
2253
|
+
context: { chainId: c.chainId, protocolAddress: BigInt(c.protocolAddress) },
|
|
2254
|
+
operation,
|
|
2255
|
+
capacity,
|
|
2256
|
+
words: words.map(BigInt)
|
|
2257
|
+
})
|
|
2258
|
+
);
|
|
2259
|
+
return [...words, word(digest >> 128n), word(digest & (1n << 128n) - 1n)];
|
|
2260
|
+
}
|
|
2261
|
+
function encodeCancelCreditTypedData(c, p) {
|
|
2262
|
+
return authorize(c, "cancel-credit", 1, cancelCreditBaseInputs(c, p));
|
|
2263
|
+
}
|
|
2264
|
+
function encodeTopUpCreditTypedData(c, p, deliveries, funding = {}) {
|
|
2265
|
+
return authorize(c, "top-up-credit", 10, topUpCreditBaseInputs(c, p, deliveries, funding));
|
|
2266
|
+
}
|
|
2267
|
+
function encodeCreditDrawTypedData(c, p, deliveries) {
|
|
2268
|
+
return authorize(
|
|
2269
|
+
c,
|
|
2270
|
+
"credit-draw",
|
|
2271
|
+
selectCreditDrawCircuit(p.nullifiers.length).maxSize,
|
|
2272
|
+
creditDrawBaseInputs(c, p, deliveries)
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
function creditDrawPublicInputs(c, p, deliveries) {
|
|
2276
|
+
return appendDigest(
|
|
2277
|
+
c,
|
|
2278
|
+
"credit-draw",
|
|
2279
|
+
selectCreditDrawCircuit(p.nullifiers.length).maxSize,
|
|
2280
|
+
creditDrawBaseInputs(c, p, deliveries)
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
function cancelCreditPublicInputs(c, p) {
|
|
2284
|
+
return appendDigest(c, "cancel-credit", 1, cancelCreditBaseInputs(c, p));
|
|
2285
|
+
}
|
|
2286
|
+
function topUpCreditPublicInputs(c, p, deliveries, funding = {}) {
|
|
2287
|
+
return appendDigest(c, "top-up-credit", 10, topUpCreditBaseInputs(c, p, deliveries, funding));
|
|
2288
|
+
}
|
|
2289
|
+
function encodeTopUpCreditFundingTypedData(c, p, cs, f) {
|
|
2290
|
+
const r = validateTopUpCreditRequest(c, p, cs, f), funding = r.creation.funding;
|
|
2291
|
+
if (funding.amount === 0n) throw new Error("funding authorization requires positive deposit");
|
|
2292
|
+
return {
|
|
2293
|
+
domain: getEIP712Domain(c),
|
|
2294
|
+
primaryType: "FundTransact",
|
|
2295
|
+
types: { FundTransact: fundingFields },
|
|
2296
|
+
message: {
|
|
2297
|
+
requestHash: topUpCreditRequestHash(p, cs, f),
|
|
2298
|
+
payer: funding.payer,
|
|
2299
|
+
amount: funding.amount,
|
|
2300
|
+
sigNonce: funding.sigNonce,
|
|
2301
|
+
deadline: funding.deadline
|
|
2302
|
+
}
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
function buildCreditDrawCalldata(p) {
|
|
2306
|
+
validateCreditDrawRequest(p.context, p.params, p.deliveries);
|
|
2307
|
+
return {
|
|
2308
|
+
data: new Interface2(invisiProtocolAbi).encodeFunctionData("creditDraw", [
|
|
2309
|
+
encodeCreditProof(p.proof),
|
|
2310
|
+
p.params,
|
|
2311
|
+
p.deliveries
|
|
2312
|
+
]),
|
|
2313
|
+
nullifiers: p.params.nullifiers,
|
|
2314
|
+
totalAmount: p.params.totalAmount
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
function buildCancelCreditCalldata(p) {
|
|
2318
|
+
validateCancelCreditRequest(p.context, p.params);
|
|
2319
|
+
return {
|
|
2320
|
+
data: new Interface2(invisiProtocolAbi).encodeFunctionData("cancelCredit", [
|
|
2321
|
+
encodeCreditProof(p.proof),
|
|
2322
|
+
{ ...p.params, ...normalizeFee(p.params) }
|
|
2323
|
+
])
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
function buildTopUpCreditCalldata(p) {
|
|
2327
|
+
const r = validateTopUpCreditRequest(p.context, p.params, p.deliveries, p.funding);
|
|
2328
|
+
validateFundingSubmission(r.creation.funding, p.caller, p.context.now);
|
|
2329
|
+
return {
|
|
2330
|
+
data: new Interface2(invisiProtocolAbi).encodeFunctionData("topUpCredit", [
|
|
2331
|
+
encodeCreditProof(p.proof),
|
|
2332
|
+
{ ...p.params, creation: { ...p.params.creation, ...normalizeFee(p.params.creation) } },
|
|
2333
|
+
p.deliveries,
|
|
2334
|
+
r.creation.funding
|
|
2335
|
+
]),
|
|
2336
|
+
approveAmount: r.creation.funding.amount
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
// src/authorization/credit-cancel.ts
|
|
2341
|
+
async function cancelCredit(witness, p) {
|
|
2342
|
+
const note = normalizeCreditDeliveryState(p.input.state).note, leaf = await witness.creditLeaf(note), c = witness.protocol.context;
|
|
2343
|
+
check(
|
|
2344
|
+
await computeCreditNullifier(c.chainId, note.creditSharedKey, leaf) === p.params.nullifier && note.amount === p.params.amount && note.ownerPayout === BigInt(p.params.owner),
|
|
2345
|
+
"Credit cancellation mismatch"
|
|
2346
|
+
);
|
|
2347
|
+
await witness.membership(leaf, p.input, [p.params.source]);
|
|
2348
|
+
const words = cancelCreditPublicInputs(c, p.params), authority = await witness.authority(
|
|
2349
|
+
"cancel-credit",
|
|
2350
|
+
1,
|
|
2351
|
+
words,
|
|
2352
|
+
p.authorization,
|
|
2353
|
+
note.ownerWalletIdentity
|
|
2354
|
+
);
|
|
2355
|
+
return provingRequest(
|
|
2356
|
+
"credit_cancel",
|
|
2357
|
+
{
|
|
2358
|
+
...authority,
|
|
2359
|
+
spender_payout: note.spenderPayout,
|
|
2360
|
+
spender_wallet_identity: BigInt(note.spenderWalletIdentity),
|
|
2361
|
+
owner_wallet_identity: BigInt(note.ownerWalletIdentity),
|
|
2362
|
+
...policyWitness(note),
|
|
2363
|
+
input_nonce: note.nonce,
|
|
2364
|
+
expiry: note.expiry,
|
|
2365
|
+
credit_shared_key: note.creditSharedKey,
|
|
2366
|
+
leaf_index: BigInt(p.input.leafIndex),
|
|
2367
|
+
merkle_path: [...p.input.path]
|
|
2368
|
+
},
|
|
2369
|
+
words
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// src/authorization/credit-creation.ts
|
|
2374
|
+
import { normalizeCreditDeliveryState as normalizeCreditDeliveryState2 } from "@invisi-labs/core";
|
|
2375
|
+
async function creation(witness, p, operation, words) {
|
|
2376
|
+
const state = normalizeCreditDeliveryState2(p.state), note = state.note;
|
|
2377
|
+
check(
|
|
2378
|
+
await witness.creditLeaf(note) === p.params.creditCommitment && note.ownerPayout === BigInt(p.params.owner) && note.expiry === p.params.expiry,
|
|
2379
|
+
"Credit creation mismatch"
|
|
2380
|
+
);
|
|
2381
|
+
if (operation === "create-credit" && [note.lastDay, note.dailySpent, note.lastMonth, note.monthlySpent].some((v) => v !== 0n))
|
|
2382
|
+
throw new Error("new Credit usage must be zero");
|
|
2383
|
+
const authority = await witness.authority(
|
|
2384
|
+
operation,
|
|
2385
|
+
10,
|
|
2386
|
+
words,
|
|
2387
|
+
p.authorization,
|
|
2388
|
+
note.ownerWalletIdentity
|
|
2389
|
+
);
|
|
2390
|
+
const sources = await witness.noteInputs(
|
|
2391
|
+
p.inputs,
|
|
2392
|
+
p.params.sources,
|
|
2393
|
+
p.params.nullifiers,
|
|
2394
|
+
note.ownerWalletIdentity,
|
|
2395
|
+
authority
|
|
2396
|
+
);
|
|
2397
|
+
if (p.change)
|
|
2398
|
+
check(
|
|
2399
|
+
p.change.ownerAddress === note.ownerWalletIdentity && await witness.noteLeaf(p.change) === p.params.changeCommitment,
|
|
2400
|
+
"change commitment mismatch"
|
|
2401
|
+
);
|
|
2402
|
+
else check(BigInt(p.params.changeCommitment) === 0n, "missing change note");
|
|
2403
|
+
return {
|
|
2404
|
+
...authority,
|
|
2405
|
+
input_amounts: pad2(
|
|
2406
|
+
p.inputs.map((input) => input.note.amount),
|
|
2407
|
+
10,
|
|
2408
|
+
() => 0n
|
|
2409
|
+
),
|
|
2410
|
+
input_nonces: pad2(
|
|
2411
|
+
p.inputs.map((input) => input.note.nonce),
|
|
2412
|
+
10,
|
|
2413
|
+
() => 0n
|
|
2414
|
+
),
|
|
2415
|
+
leaf_indices: pad2(
|
|
2416
|
+
p.inputs.map((input) => BigInt(input.leafIndex)),
|
|
2417
|
+
10,
|
|
2418
|
+
() => 0n
|
|
2419
|
+
),
|
|
2420
|
+
merkle_paths: pad2(
|
|
2421
|
+
p.inputs.map((input) => [...input.path]),
|
|
2422
|
+
10,
|
|
2423
|
+
zeros
|
|
2424
|
+
),
|
|
2425
|
+
input_source_indices: pad2(sources, 10, () => 0n),
|
|
2426
|
+
spender_payout: note.spenderPayout,
|
|
2427
|
+
spender_wallet_identity: BigInt(note.spenderWalletIdentity),
|
|
2428
|
+
owner_wallet_identity: BigInt(note.ownerWalletIdentity),
|
|
2429
|
+
...policyWitness(note, operation === "top-up-credit"),
|
|
2430
|
+
credit_amount: note.amount,
|
|
2431
|
+
credit_nonce: note.nonce,
|
|
2432
|
+
credit_shared_key: note.creditSharedKey,
|
|
2433
|
+
change_amount: p.change?.amount ?? 0n,
|
|
2434
|
+
change_nonce: p.change?.nonce ?? 0n
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// src/authorization/credit-create.ts
|
|
2439
|
+
async function createCredit(witness, p) {
|
|
2440
|
+
const words = createCreditPublicInputs(
|
|
2441
|
+
witness.protocol.context,
|
|
2442
|
+
p.params,
|
|
2443
|
+
p.deliveries,
|
|
2444
|
+
p.funding
|
|
2445
|
+
);
|
|
2446
|
+
return provingRequest(
|
|
2447
|
+
"create_credit",
|
|
2448
|
+
await creation(witness, p, "create-credit", words),
|
|
2449
|
+
words
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// src/authorization/credit-draw.ts
|
|
2454
|
+
import {
|
|
2455
|
+
applyCreditDrawUsage,
|
|
2456
|
+
computeCreditNullifier as computeCreditNullifier2,
|
|
2457
|
+
normalizeCreditDeliveryState as normalizeCreditDeliveryState3,
|
|
2458
|
+
normalizeCreditPolicy as normalizeCreditPolicy2
|
|
2459
|
+
} from "@invisi-labs/core";
|
|
2460
|
+
async function drawCredit(witness, p) {
|
|
2461
|
+
const c = { ...witness.protocol.context, timestamp: p.timestamp }, profile = selectCreditDrawCircuit(p.inputs.length), capacity = profile.maxSize;
|
|
2462
|
+
check(
|
|
2463
|
+
p.inputs.length === p.amounts.length && p.inputs.length === p.params.nullifiers.length,
|
|
2464
|
+
"draw input count mismatch"
|
|
2465
|
+
);
|
|
2466
|
+
const notes2 = p.inputs.map((input) => normalizeCreditDeliveryState3(input.state).note), identity = notes2[0].spenderWalletIdentity;
|
|
2467
|
+
const words = creditDrawPublicInputs(c, p.params, p.deliveries), authority = await witness.authority(
|
|
2468
|
+
"credit-draw",
|
|
2469
|
+
capacity,
|
|
2470
|
+
words,
|
|
2471
|
+
p.authorization,
|
|
2472
|
+
identity
|
|
2473
|
+
);
|
|
2474
|
+
const sourceIndices = await Promise.all(
|
|
2475
|
+
notes2.map(async (note, i) => {
|
|
2476
|
+
applyCreditDrawUsage(note, p.amounts[i], p.timestamp);
|
|
2477
|
+
const leaf = await witness.creditLeaf(note);
|
|
2478
|
+
check(
|
|
2479
|
+
note.spenderWalletIdentity === identity && note.spenderPayout === BigInt(p.params.spender) && note.expiry === p.params.expiries[i] && p.amounts[i] > 0n && p.amounts[i] <= note.amount && await computeCreditNullifier2(c.chainId, note.creditSharedKey, leaf) === p.params.nullifiers[i],
|
|
2480
|
+
"Credit draw mismatch"
|
|
2481
|
+
);
|
|
2482
|
+
return BigInt(await witness.membership(leaf, p.inputs[i], p.params.sources));
|
|
2483
|
+
})
|
|
2484
|
+
);
|
|
2485
|
+
return provingRequest(
|
|
2486
|
+
profile.name,
|
|
2487
|
+
{
|
|
2488
|
+
...authority,
|
|
2489
|
+
...Object.fromEntries(
|
|
2490
|
+
Object.entries({
|
|
2491
|
+
singleLimit: "single_limits",
|
|
2492
|
+
dailyLimit: "daily_limits",
|
|
2493
|
+
monthlyLimit: "monthly_limits",
|
|
2494
|
+
lastDay: "last_days",
|
|
2495
|
+
dailySpent: "daily_spents",
|
|
2496
|
+
lastMonth: "last_months",
|
|
2497
|
+
monthlySpent: "monthly_spents"
|
|
2498
|
+
}).map(([key, name]) => [
|
|
2499
|
+
name,
|
|
2500
|
+
pad2(
|
|
2501
|
+
notes2.map(
|
|
2502
|
+
(note) => normalizeCreditPolicy2(note)[key]
|
|
2503
|
+
),
|
|
2504
|
+
capacity,
|
|
2505
|
+
() => 0n
|
|
2506
|
+
)
|
|
2507
|
+
])
|
|
2508
|
+
),
|
|
2509
|
+
spender_wallet_identity: BigInt(identity),
|
|
2510
|
+
owner_payouts: pad2(
|
|
2511
|
+
notes2.map((note) => note.ownerPayout),
|
|
2512
|
+
capacity,
|
|
2513
|
+
() => 0n
|
|
2514
|
+
),
|
|
2515
|
+
owner_wallet_identities: pad2(
|
|
2516
|
+
notes2.map((note) => BigInt(note.ownerWalletIdentity)),
|
|
2517
|
+
capacity,
|
|
2518
|
+
() => 0n
|
|
2519
|
+
),
|
|
2520
|
+
input_amounts: pad2(
|
|
2521
|
+
notes2.map((note) => note.amount),
|
|
2522
|
+
capacity,
|
|
2523
|
+
() => 0n
|
|
2524
|
+
),
|
|
2525
|
+
input_nonces: pad2(
|
|
2526
|
+
notes2.map((note) => note.nonce),
|
|
2527
|
+
capacity,
|
|
2528
|
+
() => 0n
|
|
2529
|
+
),
|
|
2530
|
+
credit_shared_keys: pad2(
|
|
2531
|
+
notes2.map((note) => note.creditSharedKey),
|
|
2532
|
+
capacity,
|
|
2533
|
+
() => 0n
|
|
2534
|
+
),
|
|
2535
|
+
leaf_indices: pad2(
|
|
2536
|
+
p.inputs.map((input) => BigInt(input.leafIndex)),
|
|
2537
|
+
capacity,
|
|
2538
|
+
() => 0n
|
|
2539
|
+
),
|
|
2540
|
+
merkle_paths: pad2(
|
|
2541
|
+
p.inputs.map((input) => [...input.path]),
|
|
2542
|
+
capacity,
|
|
2543
|
+
zeros
|
|
2544
|
+
),
|
|
2545
|
+
input_source_indices: pad2(sourceIndices, capacity, () => 0n),
|
|
2546
|
+
draw_amounts: pad2(p.amounts, capacity, () => 0n)
|
|
2547
|
+
},
|
|
2548
|
+
words
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
// src/authorization/credit-top-up.ts
|
|
2553
|
+
import { computeCreditNullifier as computeCreditNullifier3, normalizeCreditDeliveryState as normalizeCreditDeliveryState4 } from "@invisi-labs/core";
|
|
2554
|
+
async function topUpCredit(witness, p) {
|
|
2555
|
+
const c = witness.protocol.context, previous = normalizeCreditDeliveryState4(p.previous.state), next = normalizeCreditDeliveryState4(p.state), old = previous.note, note = next.note, leaf = await witness.creditLeaf(old);
|
|
2556
|
+
for (const field2 of [
|
|
2557
|
+
"spenderWalletIdentity",
|
|
2558
|
+
"ownerWalletIdentity",
|
|
2559
|
+
"spenderPayout",
|
|
2560
|
+
"ownerPayout",
|
|
2561
|
+
"creditSharedKey",
|
|
2562
|
+
"lastDay",
|
|
2563
|
+
"dailySpent",
|
|
2564
|
+
"lastMonth",
|
|
2565
|
+
"monthlySpent"
|
|
2566
|
+
])
|
|
2567
|
+
check(old[field2] === note[field2], "Credit role or payout changed");
|
|
2568
|
+
check(
|
|
2569
|
+
JSON.stringify(previous.ownerReceivingKeys) === JSON.stringify(next.ownerReceivingKeys) && JSON.stringify(previous.spenderReceivingKeys) === JSON.stringify(next.spenderReceivingKeys),
|
|
2570
|
+
"Credit receiving snapshots changed"
|
|
2571
|
+
);
|
|
2572
|
+
check(
|
|
2573
|
+
old.amount < note.amount && old.expiry <= note.expiry && old.expiry === p.params.previousExpiry && (c.now === void 0 || old.expiry > c.now),
|
|
2574
|
+
"invalid Credit top-up"
|
|
2575
|
+
);
|
|
2576
|
+
check(
|
|
2577
|
+
await computeCreditNullifier3(c.chainId, old.creditSharedKey, leaf) === p.params.creditNullifier,
|
|
2578
|
+
"previous Credit nullifier mismatch"
|
|
2579
|
+
);
|
|
2580
|
+
await witness.membership(leaf, p.previous, [p.params.creditSource]);
|
|
2581
|
+
const words = topUpCreditPublicInputs(c, p.params, p.deliveries, p.funding), base = await creation(witness, { ...p, params: p.params.creation }, "top-up-credit", words);
|
|
2582
|
+
return provingRequest(
|
|
2583
|
+
"credit_top_up",
|
|
2584
|
+
{
|
|
2585
|
+
...base,
|
|
2586
|
+
previous_single_limit: old.singleLimit,
|
|
2587
|
+
previous_daily_limit: old.dailyLimit,
|
|
2588
|
+
previous_monthly_limit: old.monthlyLimit,
|
|
2589
|
+
previous_credit_amount: old.amount,
|
|
2590
|
+
previous_credit_nonce: old.nonce,
|
|
2591
|
+
previous_credit_leaf_index: BigInt(p.previous.leafIndex),
|
|
2592
|
+
previous_credit_merkle_path: [...p.previous.path]
|
|
2593
|
+
},
|
|
2594
|
+
words
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
// src/authorization/transact.ts
|
|
2599
|
+
import { computeNoteCommitment as computeNoteCommitment2, computeNoteIdentity as computeNoteIdentity2 } from "@invisi-labs/core";
|
|
2600
|
+
|
|
2601
|
+
// src/funds/public-inputs.ts
|
|
2602
|
+
import { TypedDataEncoder } from "ethers";
|
|
2603
|
+
|
|
2604
|
+
// src/funds/funding.ts
|
|
2605
|
+
import { walletAuthorizationTypedData as walletAuthorizationTypedData3 } from "@invisi-labs/core";
|
|
2606
|
+
import { AbiCoder as AbiCoder4, keccak256 as keccak2564, ParamType as ParamType4 } from "ethers";
|
|
2607
|
+
|
|
2608
|
+
// src/funds/abi-tuples.ts
|
|
2609
|
+
var singleTuple = [
|
|
2610
|
+
"(address token,bytes32 merkleRoot,bytes32[] nullifiers,bytes32[] outputCommitments,uint256 inputTreeId,address withdrawalAddress,uint256 withdrawalAmount,uint256 deadline,address feeRecipient,uint256 feeAmount)"
|
|
2611
|
+
][0];
|
|
2612
|
+
var crossTuple = [
|
|
2613
|
+
"(address token,(uint256 treeId,bytes32 merkleRoot)[] sources,bytes32[] nullifiers,bytes32[] outputCommitments,address withdrawalAddress,uint256 withdrawalAmount,uint256 deadline,address feeRecipient,uint256 feeAmount)"
|
|
2614
|
+
][0];
|
|
2615
|
+
|
|
2616
|
+
// src/funds/request.ts
|
|
2617
|
+
import { getAddress as getAddress5 } from "ethers";
|
|
2618
|
+
var TRANSACT_CAPACITIES = [14, 56, 112, 168];
|
|
2619
|
+
function selectTransactCapacity(inputs, outputs) {
|
|
2620
|
+
if (!Number.isInteger(inputs) || !Number.isInteger(outputs) || inputs < 0 || outputs < 0 || inputs + outputs < 1 || inputs === 0 && outputs < 2 || inputs + outputs > 168)
|
|
2621
|
+
throw new Error("unsupported transact shape");
|
|
2622
|
+
return TRANSACT_CAPACITIES.find((n) => inputs + outputs <= n);
|
|
2623
|
+
}
|
|
2624
|
+
function normalizeTransactRequest(c, p, cs, funding = {}) {
|
|
2625
|
+
const ctx = normalizeContext(c);
|
|
2626
|
+
if (getAddress5(p.token) !== ctx.tokenAddress)
|
|
2627
|
+
throw new Error("request token mismatch");
|
|
2628
|
+
known(p, [
|
|
2629
|
+
"token",
|
|
2630
|
+
"sources",
|
|
2631
|
+
"nullifiers",
|
|
2632
|
+
"outputCommitments",
|
|
2633
|
+
"withdrawalAddress",
|
|
2634
|
+
"withdrawalAmount",
|
|
2635
|
+
"deadline",
|
|
2636
|
+
"feeRecipient",
|
|
2637
|
+
"feeAmount"
|
|
2638
|
+
]);
|
|
2639
|
+
const ns = notes(p.nullifiers, "nullifiers"), os = notes(p.outputCommitments, "outputCommitments"), capacity = selectTransactCapacity(ns.length, os.length);
|
|
2640
|
+
const sources = normalizeSources(p.sources, ns.length, capacity), deliveries = dense(cs, "deliveries").map(
|
|
2641
|
+
(v) => normalizeOpaqueDelivery(v, { allowEmpty: true })
|
|
2642
|
+
);
|
|
2643
|
+
if (deliveries.length !== os.length) throw new Error("deliveries count mismatch");
|
|
2644
|
+
const withdrawalAmount = uint(p.withdrawalAmount, 128, "withdrawalAmount"), withdrawalAddress = getAddress5(p.withdrawalAddress), f = normalizeFunding(funding);
|
|
2645
|
+
const feeAmount = uint(p.feeAmount ?? 0n, 128, "feeAmount");
|
|
2646
|
+
const feeRecipient = getAddress5(
|
|
2647
|
+
p.feeRecipient ?? "0x0000000000000000000000000000000000000000"
|
|
2648
|
+
);
|
|
2649
|
+
if (feeAmount === 0n !== (BigInt(feeRecipient) === 0n)) throw new Error("invalid fee");
|
|
2650
|
+
if (withdrawalAmount === 0n !== (BigInt(withdrawalAddress) === 0n) || os.length === 0 && withdrawalAmount === 0n)
|
|
2651
|
+
throw new Error("invalid withdrawal");
|
|
2652
|
+
if (ns.length === 0 && (f.amount === 0n || withdrawalAmount !== 0n))
|
|
2653
|
+
throw new Error("zero inputs require positive funding and zero withdrawal");
|
|
2654
|
+
return {
|
|
2655
|
+
...ctx,
|
|
2656
|
+
capacity,
|
|
2657
|
+
sources,
|
|
2658
|
+
nullifiers: ns,
|
|
2659
|
+
outputCommitments: os,
|
|
2660
|
+
inputCount: ns.length,
|
|
2661
|
+
outputCount: os.length,
|
|
2662
|
+
sourceCount: sources.length,
|
|
2663
|
+
inputTreeIds: pad(
|
|
2664
|
+
sources.map((s) => s.treeId),
|
|
2665
|
+
capacity
|
|
2666
|
+
),
|
|
2667
|
+
merkleRoots: pad(
|
|
2668
|
+
sources.map((s) => BigInt(s.merkleRoot)),
|
|
2669
|
+
capacity
|
|
2670
|
+
),
|
|
2671
|
+
slotValues: pad([...ns, ...os].map(BigInt), capacity),
|
|
2672
|
+
deliveries,
|
|
2673
|
+
deliveriesHash: deliveriesHash(deliveries),
|
|
2674
|
+
withdrawalAddress,
|
|
2675
|
+
withdrawalAmount,
|
|
2676
|
+
feeRecipient,
|
|
2677
|
+
feeAmount,
|
|
2678
|
+
deadline: checkDeadline(c, p.deadline),
|
|
2679
|
+
depositAmount: f.amount,
|
|
2680
|
+
depositPayer: f.payer,
|
|
2681
|
+
funding: f
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
function transactPublicInputs(c, p, cs, f = {}) {
|
|
2685
|
+
const r = normalizeTransactRequest(c, p, cs, f), h = BigInt(r.deliveriesHash);
|
|
2686
|
+
return [
|
|
2687
|
+
r.chainId,
|
|
2688
|
+
BigInt(r.protocolAddress),
|
|
2689
|
+
BigInt(r.tokenAddress),
|
|
2690
|
+
BigInt(r.inputCount),
|
|
2691
|
+
BigInt(r.outputCount),
|
|
2692
|
+
BigInt(r.sourceCount),
|
|
2693
|
+
...r.inputTreeIds,
|
|
2694
|
+
...r.merkleRoots,
|
|
2695
|
+
...r.slotValues,
|
|
2696
|
+
h >> 128n,
|
|
2697
|
+
h & (1n << 128n) - 1n,
|
|
2698
|
+
BigInt(r.withdrawalAddress),
|
|
2699
|
+
r.withdrawalAmount,
|
|
2700
|
+
r.deadline,
|
|
2701
|
+
r.depositAmount,
|
|
2702
|
+
BigInt(r.depositPayer),
|
|
2703
|
+
BigInt(r.feeRecipient),
|
|
2704
|
+
r.feeAmount
|
|
2705
|
+
].map(word);
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// src/funds/funding.ts
|
|
2709
|
+
function encodeTransactTypedData(c, p, deliveries, f = {}) {
|
|
2710
|
+
const r = normalizeTransactRequest(c, p, deliveries, f);
|
|
2711
|
+
return walletAuthorizationTypedData3({
|
|
2712
|
+
context: { chainId: r.chainId, protocolAddress: BigInt(r.protocolAddress) },
|
|
2713
|
+
operation: "transact",
|
|
2714
|
+
capacity: r.capacity,
|
|
2715
|
+
words: transactPublicInputs(c, p, deliveries, f).map(BigInt)
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
function transactFundingRequestHash(proof, p, cs) {
|
|
2719
|
+
const single = {
|
|
2720
|
+
token: p.token,
|
|
2721
|
+
merkleRoot: p.sources[0]?.merkleRoot ?? word(0n),
|
|
2722
|
+
nullifiers: p.nullifiers,
|
|
2723
|
+
outputCommitments: p.outputCommitments,
|
|
2724
|
+
inputTreeId: p.sources[0]?.treeId ?? 0n,
|
|
2725
|
+
withdrawalAddress: p.withdrawalAddress,
|
|
2726
|
+
withdrawalAmount: p.withdrawalAmount,
|
|
2727
|
+
deadline: p.deadline,
|
|
2728
|
+
feeRecipient: p.feeRecipient ?? "0x0000000000000000000000000000000000000000",
|
|
2729
|
+
feeAmount: p.feeAmount ?? 0n
|
|
2730
|
+
};
|
|
2731
|
+
return p.sources.length > 1 ? keccak2564(
|
|
2732
|
+
AbiCoder4.defaultAbiCoder().encode(
|
|
2733
|
+
[{ type: "bytes32" }, { type: "bytes32" }, crossTuple, { type: "bytes32" }].map(
|
|
2734
|
+
(parameter) => ParamType4.from(parameter)
|
|
2735
|
+
),
|
|
2736
|
+
[
|
|
2737
|
+
keccak2564(new TextEncoder().encode("InvisiCrossTreeTransact")),
|
|
2738
|
+
keccak2564(proof),
|
|
2739
|
+
{ ...p, feeRecipient: single.feeRecipient, feeAmount: single.feeAmount },
|
|
2740
|
+
deliveriesHash(cs)
|
|
2741
|
+
]
|
|
2742
|
+
)
|
|
2743
|
+
) : keccak2564(
|
|
2744
|
+
AbiCoder4.defaultAbiCoder().encode(
|
|
2745
|
+
[{ type: "bytes32" }, singleTuple, { type: "bytes32" }].map(
|
|
2746
|
+
(parameter) => ParamType4.from(parameter)
|
|
2747
|
+
),
|
|
2748
|
+
[keccak2564(proof), single, deliveriesHash(cs)]
|
|
2749
|
+
)
|
|
2750
|
+
);
|
|
2751
|
+
}
|
|
2752
|
+
function encodeTransactFundingTypedData(c, proof, p, cs, funding) {
|
|
2753
|
+
normalizeTransactRequest(c, p, cs, funding);
|
|
2754
|
+
const f = normalizeFunding(funding);
|
|
2755
|
+
if (f.amount === 0n) throw new Error("funding authorization requires positive deposit");
|
|
2756
|
+
return {
|
|
2757
|
+
domain: getEIP712Domain(c),
|
|
2758
|
+
primaryType: "FundTransact",
|
|
2759
|
+
types: { FundTransact: fundingFields },
|
|
2760
|
+
message: {
|
|
2761
|
+
requestHash: transactFundingRequestHash(proof, p, cs),
|
|
2762
|
+
payer: f.payer,
|
|
2763
|
+
amount: f.amount,
|
|
2764
|
+
sigNonce: f.sigNonce,
|
|
2765
|
+
deadline: f.deadline
|
|
2766
|
+
}
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// src/funds/public-inputs.ts
|
|
2771
|
+
function transactUserPublicInputs(c, p, cs, f = {}) {
|
|
2772
|
+
const words = transactPublicInputs(c, p, cs, f), digest = BigInt(
|
|
2773
|
+
((typed) => TypedDataEncoder.hash(
|
|
2774
|
+
typed.domain,
|
|
2775
|
+
Object.fromEntries(
|
|
2776
|
+
Object.entries(typed.types).map(([name, fields]) => [
|
|
2777
|
+
name,
|
|
2778
|
+
[...fields]
|
|
2779
|
+
])
|
|
2780
|
+
),
|
|
2781
|
+
typed.message
|
|
2782
|
+
))(encodeTransactTypedData(c, p, cs, f))
|
|
2783
|
+
);
|
|
2784
|
+
return [...words, word(digest >> 128n), word(digest & (1n << 128n) - 1n)];
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
// src/authorization/transact.ts
|
|
2788
|
+
async function transact(witness, p) {
|
|
2789
|
+
const c = witness.protocol.context, n = normalizeTransactRequest(c, p.params, p.deliveries, p.funding), words = transactUserPublicInputs(c, p.params, p.deliveries, p.funding);
|
|
2790
|
+
check(
|
|
2791
|
+
p.inputs.length === n.inputCount && p.outputs.length === n.outputCount,
|
|
2792
|
+
"note count mismatch"
|
|
2793
|
+
);
|
|
2794
|
+
const identity = p.inputs[0]?.note.ownerAddress;
|
|
2795
|
+
const unsignedSweep = !!identity && !p.authorization && n.outputCount === 0 && n.depositAmount === 0n && n.feeAmount === 0n && BigInt(p.params.withdrawalAddress) === BigInt(identity);
|
|
2796
|
+
const authority = identity && !unsignedSweep ? await witness.authority("transact", n.capacity, words, p.authorization, identity) : zeroAuthority();
|
|
2797
|
+
const sources = await witness.noteInputs(
|
|
2798
|
+
p.inputs,
|
|
2799
|
+
p.params.sources,
|
|
2800
|
+
p.params.nullifiers,
|
|
2801
|
+
identity ?? "0x" + "00".repeat(32),
|
|
2802
|
+
authority
|
|
2803
|
+
);
|
|
2804
|
+
const outputs = await Promise.all(
|
|
2805
|
+
p.outputs.map(async (output, i) => {
|
|
2806
|
+
const opening = "identity" in output ? output.note : output;
|
|
2807
|
+
const identity2 = "identity" in output ? field(output.identity) : BigInt(await computeNoteIdentity2(BigInt(output.ownerAddress), output.nonce));
|
|
2808
|
+
const amount = uint(output.amount, 128, "output amount");
|
|
2809
|
+
check(identity2 !== 0n && amount !== 0n, "zero output identity or amount");
|
|
2810
|
+
if (opening) {
|
|
2811
|
+
witness.context(opening);
|
|
2812
|
+
check(
|
|
2813
|
+
opening.amount === amount && BigInt(
|
|
2814
|
+
await computeNoteIdentity2(BigInt(opening.ownerAddress), opening.nonce)
|
|
2815
|
+
) === identity2,
|
|
2816
|
+
"output opening mismatch"
|
|
2817
|
+
);
|
|
2818
|
+
}
|
|
2819
|
+
const commitment = await computeNoteCommitment2(
|
|
2820
|
+
c.chainId,
|
|
2821
|
+
word(identity2),
|
|
2822
|
+
BigInt(c.tokenAddress),
|
|
2823
|
+
amount
|
|
2824
|
+
);
|
|
2825
|
+
check(commitment === p.params.outputCommitments[i], "output commitment mismatch");
|
|
2826
|
+
return { identity: identity2, amount };
|
|
2827
|
+
})
|
|
2828
|
+
);
|
|
2829
|
+
const all = [...p.inputs.map((input) => input.note), ...outputs];
|
|
2830
|
+
return provingRequest(
|
|
2831
|
+
`transact_user_slots${n.capacity}`,
|
|
2832
|
+
{
|
|
2833
|
+
input_owner_address: identity ? BigInt(identity) : 0n,
|
|
2834
|
+
...authority,
|
|
2835
|
+
output_identities: pad2(
|
|
2836
|
+
[...p.inputs.map(() => 0n), ...outputs.map((output) => output.identity)],
|
|
2837
|
+
n.capacity,
|
|
2838
|
+
() => 0n
|
|
2839
|
+
),
|
|
2840
|
+
amounts: pad2(
|
|
2841
|
+
all.map((note) => note.amount),
|
|
2842
|
+
n.capacity,
|
|
2843
|
+
() => 0n
|
|
2844
|
+
),
|
|
2845
|
+
nonces: pad2(
|
|
2846
|
+
[...p.inputs.map((input) => input.note.nonce), ...outputs.map(() => 0n)],
|
|
2847
|
+
n.capacity,
|
|
2848
|
+
() => 0n
|
|
2849
|
+
),
|
|
2850
|
+
leaf_indices: pad2(
|
|
2851
|
+
p.inputs.map((input) => BigInt(input.leafIndex)),
|
|
2852
|
+
n.capacity,
|
|
2853
|
+
() => 0n
|
|
2854
|
+
),
|
|
2855
|
+
merkle_paths: pad2(
|
|
2856
|
+
p.inputs.map((input) => [...input.path]),
|
|
2857
|
+
n.capacity,
|
|
2858
|
+
zeros
|
|
2859
|
+
),
|
|
2860
|
+
input_source_indices: pad2(sources, n.capacity, () => 0n)
|
|
2861
|
+
},
|
|
2862
|
+
words,
|
|
2863
|
+
p.target
|
|
2864
|
+
);
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
// src/authorization/witness.ts
|
|
2868
|
+
var WitnessBuilder = class {
|
|
2869
|
+
#witness;
|
|
2870
|
+
constructor(protocol) {
|
|
2871
|
+
this.#witness = new WitnessContext(protocol);
|
|
2872
|
+
}
|
|
2873
|
+
static statement(circuitName, expectedPublicInputs, target = "evm") {
|
|
2874
|
+
return witnessStatement(circuitName, expectedPublicInputs, target);
|
|
2875
|
+
}
|
|
2876
|
+
async transact(p) {
|
|
2877
|
+
return transact(this.#witness, p);
|
|
2878
|
+
}
|
|
2879
|
+
async createCredit(p) {
|
|
2880
|
+
return createCredit(this.#witness, p);
|
|
2881
|
+
}
|
|
2882
|
+
async cancelCredit(p) {
|
|
2883
|
+
return cancelCredit(this.#witness, p);
|
|
2884
|
+
}
|
|
2885
|
+
async drawCredit(p) {
|
|
2886
|
+
return drawCredit(this.#witness, p);
|
|
2887
|
+
}
|
|
2888
|
+
async topUpCredit(p) {
|
|
2889
|
+
return topUpCredit(this.#witness, p);
|
|
2890
|
+
}
|
|
2891
|
+
};
|
|
2892
|
+
|
|
2893
|
+
// src/client/Protocol.ts
|
|
2894
|
+
import { Contract, getAddress as getAddress7, keccak256 as keccak2568 } from "ethers";
|
|
2895
|
+
import {
|
|
2896
|
+
defineDeployment
|
|
2897
|
+
} from "@invisi-labs/rpc";
|
|
2898
|
+
|
|
2899
|
+
// src/funds/aggregate.ts
|
|
2900
|
+
import { AbiCoder as AbiCoder5, Interface as Interface3, keccak256 as keccak2565, ParamType as ParamType5 } from "ethers";
|
|
2901
|
+
function buildTransactAggregateCalldata(p) {
|
|
2902
|
+
if (![4, 8, 12].includes(dense(p.requests, "requests").length))
|
|
2903
|
+
throw new Error("aggregate requires 4, 8 or 12 requests");
|
|
2904
|
+
const requests = p.requests.map((request) => {
|
|
2905
|
+
const r = normalizeTransactRequest(
|
|
2906
|
+
{ ...p.context, tokenAddress: request.params.token },
|
|
2907
|
+
request.params,
|
|
2908
|
+
request.deliveries,
|
|
2909
|
+
request.funding
|
|
2910
|
+
);
|
|
2911
|
+
if (r.capacity !== 14) throw new Error("aggregate supports only capacity 14");
|
|
2912
|
+
validateFundingSubmission(r.funding, p.caller, p.context.now);
|
|
2913
|
+
return {
|
|
2914
|
+
...request,
|
|
2915
|
+
params: { ...request.params, feeRecipient: r.feeRecipient, feeAmount: r.feeAmount }
|
|
2916
|
+
};
|
|
2917
|
+
});
|
|
2918
|
+
return {
|
|
2919
|
+
data: new Interface3(
|
|
2920
|
+
invisiProtocolAbi.filter(
|
|
2921
|
+
(item) => item.type === "function" && item.name === "transact" && item.inputs[1]?.type === "tuple[]"
|
|
2922
|
+
)
|
|
2923
|
+
).encodeFunctionData("transact", [p.proof, requests]),
|
|
2924
|
+
capacity: 14
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
function aggregateMemberRequestHash(c, r) {
|
|
2928
|
+
const capacity = selectTransactCapacity(
|
|
2929
|
+
r.params.nullifiers.length,
|
|
2930
|
+
r.params.outputCommitments.length
|
|
2931
|
+
);
|
|
2932
|
+
if (capacity !== 14) throw new Error("aggregate supports only capacity 14");
|
|
2933
|
+
return keccak2565(
|
|
2934
|
+
AbiCoder5.defaultAbiCoder().encode(
|
|
2935
|
+
["bytes32", "uint256", "bytes32[]"].map((parameter) => ParamType5.from(parameter)),
|
|
2936
|
+
[
|
|
2937
|
+
keccak2565(new TextEncoder().encode("InvisiAtomicBatchMemberV1")),
|
|
2938
|
+
BigInt(capacity),
|
|
2939
|
+
transactPublicInputs(c, r.params, r.deliveries, r.funding)
|
|
2940
|
+
]
|
|
2941
|
+
)
|
|
2942
|
+
);
|
|
2943
|
+
}
|
|
2944
|
+
function aggregatePublicInputs(c, requests) {
|
|
2945
|
+
if (![4, 8, 12].includes(dense(requests, "requests").length))
|
|
2946
|
+
throw new Error("aggregate requires 4, 8 or 12 requests");
|
|
2947
|
+
return requests.flatMap((r) => {
|
|
2948
|
+
const context = { ...c, tokenAddress: r.params.token };
|
|
2949
|
+
aggregateMemberRequestHash(context, r);
|
|
2950
|
+
return [word(14n), ...transactPublicInputs(context, r.params, r.deliveries, r.funding)];
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
function encodeAggregateFundingTypedData(c, r) {
|
|
2954
|
+
const f = normalizeFunding(r.funding);
|
|
2955
|
+
if (f.amount === 0n) throw new Error("funding authorization requires positive deposit");
|
|
2956
|
+
return {
|
|
2957
|
+
domain: getEIP712Domain(c),
|
|
2958
|
+
primaryType: "FundTransact",
|
|
2959
|
+
types: { FundTransact: fundingFields },
|
|
2960
|
+
message: {
|
|
2961
|
+
requestHash: aggregateMemberRequestHash(c, r),
|
|
2962
|
+
payer: f.payer,
|
|
2963
|
+
amount: f.amount,
|
|
2964
|
+
sigNonce: f.sigNonce,
|
|
2965
|
+
deadline: f.deadline
|
|
2966
|
+
}
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
// src/client/TokenProtocol.ts
|
|
2971
|
+
import { keccak256 as keccak2567 } from "ethers";
|
|
2972
|
+
import { MissingCapabilityError } from "@invisi-labs/core";
|
|
2973
|
+
|
|
2974
|
+
// src/funds/calldata.ts
|
|
2975
|
+
import { Interface as Interface4 } from "ethers";
|
|
2976
|
+
function buildTransactCalldata(p) {
|
|
2977
|
+
const r = normalizeTransactRequest(p.context, p.params, p.deliveries, p.funding);
|
|
2978
|
+
validateFundingSubmission(r.funding, p.caller, p.context.now);
|
|
2979
|
+
const request = {
|
|
2980
|
+
params: {
|
|
2981
|
+
token: r.tokenAddress,
|
|
2982
|
+
sources: r.sources,
|
|
2983
|
+
nullifiers: r.nullifiers,
|
|
2984
|
+
outputCommitments: r.outputCommitments,
|
|
2985
|
+
withdrawalAddress: r.withdrawalAddress,
|
|
2986
|
+
withdrawalAmount: r.withdrawalAmount,
|
|
2987
|
+
deadline: r.deadline,
|
|
2988
|
+
feeRecipient: r.feeRecipient,
|
|
2989
|
+
feeAmount: r.feeAmount
|
|
2990
|
+
},
|
|
2991
|
+
deliveries: p.deliveries,
|
|
2992
|
+
funding: r.funding
|
|
2993
|
+
};
|
|
2994
|
+
const abi = invisiProtocolAbi.filter(
|
|
2995
|
+
(item) => item.type === "function" && item.name === "transact" && item.inputs.length === 2 && item.inputs[1]?.type === "tuple"
|
|
2996
|
+
);
|
|
2997
|
+
const data = new Interface4(abi).encodeFunctionData("transact", [p.proof, request]);
|
|
2998
|
+
return { data, capacity: r.capacity, approveAmount: r.depositAmount };
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
// src/funds/shield.ts
|
|
3002
|
+
import { computeNoteCommitment as computeNoteCommitment3 } from "@invisi-labs/core";
|
|
3003
|
+
import { AbiCoder as AbiCoder6, getAddress as getAddress6, Interface as Interface5, keccak256 as keccak2566, ParamType as ParamType6 } from "ethers";
|
|
3004
|
+
async function validateShieldRequest(p) {
|
|
3005
|
+
normalizeContext(p.context);
|
|
3006
|
+
if (getAddress6(p.params.token) !== getAddress6(p.context.tokenAddress))
|
|
3007
|
+
throw new Error("request token mismatch");
|
|
3008
|
+
known(p.params, ["token", "outputCommitments", "deadline", "feeRecipient", "feeAmount"]);
|
|
3009
|
+
checkDeadline(p.context, p.params.deadline);
|
|
3010
|
+
const outputs = dense(p.outputs, "outputs"), commitments = notes(p.params.outputCommitments, "outputCommitments"), cs = dense(p.deliveries, "deliveries"), f = normalizeFunding(p.funding);
|
|
3011
|
+
if (outputs.length < 1 || outputs.length > 10 || commitments.length !== outputs.length || cs.length !== outputs.length)
|
|
3012
|
+
throw new Error("shield requires 1..10 outputs and matching deliveries");
|
|
3013
|
+
let total = 0n;
|
|
3014
|
+
for (const [i, o] of outputs.entries()) {
|
|
3015
|
+
known(o, ["identity", "amount"], "output");
|
|
3016
|
+
uint(o.amount, 128, "amount");
|
|
3017
|
+
if (o.amount === 0n) throw new Error("amount must be positive");
|
|
3018
|
+
field(o.identity);
|
|
3019
|
+
normalizeOpaqueDelivery(cs[i], { allowEmpty: true });
|
|
3020
|
+
if (await computeNoteCommitment3(
|
|
3021
|
+
p.context.chainId,
|
|
3022
|
+
o.identity,
|
|
3023
|
+
BigInt(p.context.tokenAddress),
|
|
3024
|
+
o.amount
|
|
3025
|
+
) !== commitments[i])
|
|
3026
|
+
throw new Error("output commitment mismatch");
|
|
3027
|
+
total += o.amount;
|
|
3028
|
+
}
|
|
3029
|
+
if (total + normalizeFee(p.params).feeAmount !== f.amount)
|
|
3030
|
+
throw new Error("funding must equal output total plus fee");
|
|
3031
|
+
return f;
|
|
3032
|
+
}
|
|
3033
|
+
async function encodeShieldFundingTypedData(p) {
|
|
3034
|
+
const f = await validateShieldRequest(p);
|
|
3035
|
+
return {
|
|
3036
|
+
domain: getEIP712Domain(p.context),
|
|
3037
|
+
primaryType: "FundShield",
|
|
3038
|
+
types: { FundShield: fundingFields },
|
|
3039
|
+
message: {
|
|
3040
|
+
requestHash: shieldFundingRequestHash(p.params, p.outputs, p.deliveries),
|
|
3041
|
+
payer: f.payer,
|
|
3042
|
+
amount: f.amount,
|
|
3043
|
+
sigNonce: f.sigNonce,
|
|
3044
|
+
deadline: f.deadline
|
|
3045
|
+
}
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
async function buildShieldCalldata(p) {
|
|
3049
|
+
const f = await validateShieldRequest(p);
|
|
3050
|
+
validateFundingSubmission(f, p.caller, p.context.now);
|
|
3051
|
+
return {
|
|
3052
|
+
data: new Interface5(invisiProtocolAbi).encodeFunctionData("shield", [
|
|
3053
|
+
{ ...p.params, ...normalizeFee(p.params) },
|
|
3054
|
+
p.outputs,
|
|
3055
|
+
p.deliveries,
|
|
3056
|
+
f
|
|
3057
|
+
]),
|
|
3058
|
+
approveAmount: f.amount
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
function shieldFundingRequestHash(params, outputs, cs) {
|
|
3062
|
+
const outputHash = keccak2566(
|
|
3063
|
+
AbiCoder6.defaultAbiCoder().encode(
|
|
3064
|
+
["(bytes32 identity,uint256 amount)[]"].map((parameter) => ParamType6.from(parameter)),
|
|
3065
|
+
[outputs]
|
|
3066
|
+
)
|
|
3067
|
+
);
|
|
3068
|
+
return keccak2566(
|
|
3069
|
+
AbiCoder6.defaultAbiCoder().encode(
|
|
3070
|
+
[
|
|
3071
|
+
"bytes32",
|
|
3072
|
+
"(address token,bytes32[] outputCommitments,uint256 deadline,address feeRecipient,uint256 feeAmount)",
|
|
3073
|
+
"bytes32",
|
|
3074
|
+
"bytes32"
|
|
3075
|
+
].map((parameter) => ParamType6.from(parameter)),
|
|
3076
|
+
[
|
|
3077
|
+
keccak2566(new TextEncoder().encode("InvisiShield")),
|
|
3078
|
+
{ ...params, ...normalizeFee(params) },
|
|
3079
|
+
outputHash,
|
|
3080
|
+
deliveriesHash(cs)
|
|
3081
|
+
]
|
|
3082
|
+
)
|
|
3083
|
+
);
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
// src/client/TokenProtocol.ts
|
|
3087
|
+
var TokenProtocol = class {
|
|
3088
|
+
context;
|
|
3089
|
+
expectedRuntimeCodeHash;
|
|
3090
|
+
constructor(context, options = {}) {
|
|
3091
|
+
const normalized = normalizeContext(context);
|
|
3092
|
+
if (BigInt(normalized.protocolAddress) === 0n || BigInt(normalized.tokenAddress) === 0n) {
|
|
3093
|
+
throw new Error("deployment addresses must be nonzero");
|
|
3094
|
+
}
|
|
3095
|
+
if (options.expectedRuntimeCodeHash && !/^0x[0-9a-fA-F]{64}$/.test(options.expectedRuntimeCodeHash)) {
|
|
3096
|
+
throw new Error("invalid runtime code hash");
|
|
3097
|
+
}
|
|
3098
|
+
this.expectedRuntimeCodeHash = options.expectedRuntimeCodeHash?.toLowerCase();
|
|
3099
|
+
this.context = Object.freeze(normalized);
|
|
3100
|
+
Object.freeze(this);
|
|
3101
|
+
}
|
|
3102
|
+
async validateDeployment(reader, options = {}) {
|
|
3103
|
+
if ((options.requireRuntimeProfile ?? true) && !this.expectedRuntimeCodeHash) {
|
|
3104
|
+
throw new MissingCapabilityError("trusted deployment profile");
|
|
3105
|
+
}
|
|
3106
|
+
if (await reader.getChainId() !== this.context.chainId) {
|
|
3107
|
+
throw new Error("deployment chain mismatch");
|
|
3108
|
+
}
|
|
3109
|
+
const code = await reader.getCode(this.context.protocolAddress);
|
|
3110
|
+
if (!/^0x(?:[0-9a-fA-F]{2})+$/.test(code) || this.expectedRuntimeCodeHash && keccak2567(code) !== this.expectedRuntimeCodeHash) {
|
|
3111
|
+
throw new Error("deployment runtime code profile mismatch");
|
|
3112
|
+
}
|
|
3113
|
+
const tokenCode = await reader.getCode(this.context.tokenAddress);
|
|
3114
|
+
if (!/^0x(?:[0-9a-fA-F]{2})+$/.test(tokenCode)) {
|
|
3115
|
+
throw new Error("token address must have contract code");
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
transact(request) {
|
|
3119
|
+
return {
|
|
3120
|
+
to: this.context.protocolAddress,
|
|
3121
|
+
value: 0n,
|
|
3122
|
+
...buildTransactCalldata({ ...request, context: this.context })
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
aggregate(request) {
|
|
3126
|
+
return {
|
|
3127
|
+
to: this.context.protocolAddress,
|
|
3128
|
+
value: 0n,
|
|
3129
|
+
...buildTransactAggregateCalldata({ ...request, context: this.context })
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
async shield(request) {
|
|
3133
|
+
return {
|
|
3134
|
+
to: this.context.protocolAddress,
|
|
3135
|
+
value: 0n,
|
|
3136
|
+
...await buildShieldCalldata({ ...request, context: this.context })
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
createCredit(request) {
|
|
3140
|
+
return {
|
|
3141
|
+
to: this.context.protocolAddress,
|
|
3142
|
+
value: 0n,
|
|
3143
|
+
...buildCreateCreditCalldata({ ...request, context: this.context })
|
|
3144
|
+
};
|
|
3145
|
+
}
|
|
3146
|
+
drawCredit(request) {
|
|
3147
|
+
return {
|
|
3148
|
+
to: this.context.protocolAddress,
|
|
3149
|
+
value: 0n,
|
|
3150
|
+
...buildCreditDrawCalldata({ ...request, context: this.context })
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
cancelCredit(request) {
|
|
3154
|
+
return {
|
|
3155
|
+
to: this.context.protocolAddress,
|
|
3156
|
+
value: 0n,
|
|
3157
|
+
...buildCancelCreditCalldata({ ...request, context: this.context })
|
|
3158
|
+
};
|
|
3159
|
+
}
|
|
3160
|
+
topUpCredit(request) {
|
|
3161
|
+
return {
|
|
3162
|
+
to: this.context.protocolAddress,
|
|
3163
|
+
value: 0n,
|
|
3164
|
+
...buildTopUpCreditCalldata({ ...request, context: this.context })
|
|
3165
|
+
};
|
|
3214
3166
|
}
|
|
3215
3167
|
};
|
|
3216
3168
|
|
|
3217
|
-
// src/
|
|
3218
|
-
var
|
|
3219
|
-
|
|
3220
|
-
|
|
3169
|
+
// src/client/Protocol.ts
|
|
3170
|
+
var Protocol = class _Protocol {
|
|
3171
|
+
deployment;
|
|
3172
|
+
context;
|
|
3173
|
+
constructor(options) {
|
|
3174
|
+
known(options, ["deployment"], "protocol options");
|
|
3175
|
+
this.deployment = defineDeployment(options.deployment);
|
|
3176
|
+
this.context = Object.freeze({
|
|
3177
|
+
chainId: this.deployment.chainId,
|
|
3178
|
+
protocolAddress: this.deployment.protocol.address
|
|
3179
|
+
});
|
|
3180
|
+
Object.freeze(this);
|
|
3181
|
+
}
|
|
3182
|
+
/** Load the selected deployment and verify its contract against Chain RPC before returning. */
|
|
3183
|
+
static async fromRpc(rpc, provider, options = {}) {
|
|
3184
|
+
const chainId = (await provider.getNetwork()).chainId;
|
|
3185
|
+
if (typeof chainId !== "bigint" || chainId <= 0n || chainId >= 1n << 64n)
|
|
3186
|
+
throw new Error("invalid protocol chainId");
|
|
3187
|
+
const deployment = await rpc.getDeployment(
|
|
3188
|
+
chainId,
|
|
3189
|
+
options.signal ? { signal: options.signal } : {}
|
|
3190
|
+
);
|
|
3191
|
+
const protocol = new _Protocol({ deployment });
|
|
3192
|
+
if (protocol.context.chainId !== chainId) throw new Error("RPC deployment chain mismatch");
|
|
3193
|
+
await protocol.validateDeployment(provider);
|
|
3194
|
+
return protocol;
|
|
3195
|
+
}
|
|
3196
|
+
async validateDeployment(connection) {
|
|
3197
|
+
const reader = "getNetwork" in connection ? {
|
|
3198
|
+
getChainId: async () => (await connection.getNetwork()).chainId,
|
|
3199
|
+
getCode: async (address) => await connection.getCode(address),
|
|
3200
|
+
getProtocolAddressRegistry: async (address) => getAddress7(
|
|
3201
|
+
await new Contract(
|
|
3202
|
+
address,
|
|
3203
|
+
["function addressRegistry() view returns (address)"],
|
|
3204
|
+
connection
|
|
3205
|
+
).getFunction("addressRegistry")()
|
|
3206
|
+
),
|
|
3207
|
+
getProtocolVersion: async (address) => BigInt(
|
|
3208
|
+
await new Contract(
|
|
3209
|
+
address,
|
|
3210
|
+
["function VERSION() view returns (uint256)"],
|
|
3211
|
+
connection
|
|
3212
|
+
).getFunction("VERSION")()
|
|
3213
|
+
)
|
|
3214
|
+
} : connection;
|
|
3215
|
+
if (await reader.getChainId() !== this.context.chainId)
|
|
3216
|
+
throw new Error("deployment chain mismatch");
|
|
3217
|
+
const code = await reader.getCode(this.context.protocolAddress);
|
|
3218
|
+
if (!/^0x(?:[0-9a-fA-F]{2})+$/.test(code) || keccak2568(code) !== this.deployment.protocol.runtimeCodeHash)
|
|
3219
|
+
throw new Error("protocol runtime code profile mismatch");
|
|
3220
|
+
if (reader.getProtocolVersion && await reader.getProtocolVersion(this.context.protocolAddress) !== 1n)
|
|
3221
|
+
throw new Error("unsupported onchain protocol version");
|
|
3222
|
+
const registry = this.deployment.registry;
|
|
3223
|
+
if (registry) {
|
|
3224
|
+
if (!reader.getProtocolAddressRegistry)
|
|
3225
|
+
throw new Error("registry binding reader capability is missing");
|
|
3226
|
+
if (getAddress7(
|
|
3227
|
+
await reader.getProtocolAddressRegistry(this.context.protocolAddress)
|
|
3228
|
+
) !== getAddress7(registry.address))
|
|
3229
|
+
throw new Error("protocol registry binding mismatch");
|
|
3230
|
+
const registryCode = await reader.getCode(registry.address);
|
|
3231
|
+
if (!/^0x(?:[0-9a-fA-F]{2})+$/.test(registryCode) || keccak2568(registryCode) !== registry.runtimeCodeHash)
|
|
3232
|
+
throw new Error("registry runtime code profile mismatch");
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
aggregate(request) {
|
|
3236
|
+
return {
|
|
3237
|
+
to: this.context.protocolAddress,
|
|
3238
|
+
value: 0n,
|
|
3239
|
+
...buildTransactAggregateCalldata({ ...request, context: this.context })
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
/** Build an operation context for one asset within this same deployment. */
|
|
3243
|
+
forToken(tokenAddress) {
|
|
3244
|
+
return new TokenProtocol(
|
|
3245
|
+
{ ...this.context, tokenAddress: getAddress7(tokenAddress) },
|
|
3246
|
+
{ expectedRuntimeCodeHash: this.deployment.protocol.runtimeCodeHash }
|
|
3247
|
+
);
|
|
3248
|
+
}
|
|
3221
3249
|
};
|
|
3222
|
-
|
|
3223
|
-
|
|
3250
|
+
|
|
3251
|
+
// src/registry/AddressRegistry.ts
|
|
3252
|
+
import { getAddress as getAddress9, keccak256 as keccak2569 } from "ethers";
|
|
3253
|
+
import {
|
|
3254
|
+
MissingCapabilityError as MissingCapabilityError2,
|
|
3255
|
+
ReceivingAddress as ReceivingAddress3
|
|
3256
|
+
} from "@invisi-labs/core";
|
|
3257
|
+
|
|
3258
|
+
// src/registry/RegistrationOperation.ts
|
|
3259
|
+
import { Interface as Interface7, getBytes as getBytes2, hexlify as hexlify2, verifyTypedData } from "ethers";
|
|
3260
|
+
import { ReceivingAddress as ReceivingAddress2, createSigningRequest } from "@invisi-labs/core";
|
|
3261
|
+
|
|
3262
|
+
// src/registry/registry.ts
|
|
3263
|
+
import {
|
|
3264
|
+
Contract as Contract2,
|
|
3265
|
+
Interface as Interface6,
|
|
3266
|
+
TypedDataEncoder as TypedDataEncoder2,
|
|
3267
|
+
getAddress as getAddress8,
|
|
3268
|
+
getBytes,
|
|
3269
|
+
hexlify,
|
|
3270
|
+
isHexString as isHexString3,
|
|
3271
|
+
toBeHex as toBeHex2
|
|
3272
|
+
} from "ethers";
|
|
3273
|
+
import { ReceivingAddress, decodeAddress } from "@invisi-labs/core";
|
|
3274
|
+
var MAX_NONCE = (1n << 64n) - 1n;
|
|
3275
|
+
function uint64(value, name) {
|
|
3276
|
+
if (typeof value !== "bigint" || value < 0n || value > MAX_NONCE)
|
|
3277
|
+
throw new RangeError(`${name} must be uint64`);
|
|
3278
|
+
return value;
|
|
3279
|
+
}
|
|
3280
|
+
function hexBytes(value, length) {
|
|
3281
|
+
if (typeof value !== "string" || !isHexString3(value) || value.length !== 2 + length * 2)
|
|
3282
|
+
throw new Error(`Expected ${length} hexadecimal bytes`);
|
|
3283
|
+
return getBytes(value);
|
|
3284
|
+
}
|
|
3285
|
+
function normalizeKeys(ownerAddress, keys) {
|
|
3286
|
+
const decoded = decodeAddress(
|
|
3287
|
+
ReceivingAddress.from({
|
|
3288
|
+
ownerAddress,
|
|
3289
|
+
viewingPublicKey: hexBytes(keys.viewingPublicKey, 33),
|
|
3290
|
+
clueKey: hexBytes(keys.clueKey, 32)
|
|
3291
|
+
}).toString()
|
|
3292
|
+
);
|
|
3293
|
+
if (decoded.kind !== "long") throw new Error("Expected full receiving address");
|
|
3224
3294
|
return {
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
monthly_limit: p.monthlyLimit,
|
|
3228
|
-
...usage ? {
|
|
3229
|
-
last_day: p.lastDay,
|
|
3230
|
-
daily_spent: p.dailySpent,
|
|
3231
|
-
last_month: p.lastMonth,
|
|
3232
|
-
monthly_spent: p.monthlySpent
|
|
3233
|
-
} : {}
|
|
3295
|
+
viewingPublicKey: hexlify(decoded.viewingPublicKey),
|
|
3296
|
+
clueKey: hexlify(decoded.clueKey)
|
|
3234
3297
|
};
|
|
3235
|
-
};
|
|
3236
|
-
var zeros = () => Array(16).fill(0n);
|
|
3237
|
-
var zeroAuthority = () => ({
|
|
3238
|
-
public_key_x: Array(32).fill(0),
|
|
3239
|
-
public_key_y: Array(32).fill(0),
|
|
3240
|
-
signature: Array(64).fill(0)
|
|
3241
|
-
});
|
|
3242
|
-
function check2(ok, message) {
|
|
3243
|
-
if (!ok) throw new Error(message);
|
|
3244
3298
|
}
|
|
3245
|
-
function
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
"
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
);
|
|
3256
|
-
|
|
3257
|
-
const
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3299
|
+
function normalize(context, registration, options) {
|
|
3300
|
+
uint64(context.chainId, "chainId");
|
|
3301
|
+
const registryAddress = getAddress8(context.registryAddress);
|
|
3302
|
+
if (BigInt(registryAddress) === 0n) throw new Error("Registry address must be nonzero");
|
|
3303
|
+
if (!/^0x(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$/.test(registration.ownerAddress))
|
|
3304
|
+
throw new Error("Wallet identity must be an EVM owner or padded bytes32");
|
|
3305
|
+
const ownerAddress = decodeAddress(
|
|
3306
|
+
ReceivingAddress.fromIdentity(
|
|
3307
|
+
toBeHex2(BigInt(registration.ownerAddress), 20)
|
|
3308
|
+
).toString()
|
|
3309
|
+
).ownerAddress;
|
|
3310
|
+
const keys = normalizeKeys(ownerAddress, registration.keys);
|
|
3311
|
+
const nonce = uint64(registration.nonce, "nonce"), deadline = uint64(registration.deadline, "deadline");
|
|
3312
|
+
if (nonce === MAX_NONCE) throw new Error("Registry nonce exhausted");
|
|
3313
|
+
if (options.update !== void 0 && typeof options.update !== "boolean")
|
|
3314
|
+
throw new TypeError("update must be boolean");
|
|
3315
|
+
if (!options.update && nonce !== 0n)
|
|
3316
|
+
throw new Error("Initial registration requires nonce zero");
|
|
3317
|
+
if (options.expectedNonce !== void 0 && uint64(options.expectedNonce, "expectedNonce") !== nonce)
|
|
3318
|
+
throw new Error("Registry nonce mismatch");
|
|
3319
|
+
if (context.now !== void 0 && deadline < uint64(context.now, "now"))
|
|
3320
|
+
throw new Error("Registration expired");
|
|
3321
|
+
return { registryAddress, registration: { ownerAddress, keys, nonce, deadline } };
|
|
3322
|
+
}
|
|
3323
|
+
function registrationTypedData(context, registration, options = {}) {
|
|
3324
|
+
const { registryAddress, registration: r } = normalize(context, registration, options);
|
|
3325
|
+
const primaryType = options.update ? "UpdateReceivingKeys" : "RegisterReceivingKeys";
|
|
3326
|
+
return {
|
|
3327
|
+
domain: {
|
|
3328
|
+
name: "InvisiAddressRegistry",
|
|
3329
|
+
version: "1",
|
|
3330
|
+
chainId: context.chainId,
|
|
3331
|
+
verifyingContract: registryAddress
|
|
3332
|
+
},
|
|
3333
|
+
primaryType,
|
|
3334
|
+
types: {
|
|
3335
|
+
[primaryType]: [
|
|
3336
|
+
{ name: "walletIdentity", type: "bytes32" },
|
|
3337
|
+
{ name: "viewingPublicKey", type: "bytes" },
|
|
3338
|
+
{ name: "clueKey", type: "bytes32" },
|
|
3339
|
+
{ name: "nonce", type: "uint64" },
|
|
3340
|
+
{ name: "deadline", type: "uint64" }
|
|
3341
|
+
]
|
|
3342
|
+
},
|
|
3343
|
+
message: {
|
|
3344
|
+
walletIdentity: toBeHex2(BigInt(r.ownerAddress), 32),
|
|
3345
|
+
...r.keys,
|
|
3346
|
+
nonce: r.nonce,
|
|
3347
|
+
deadline: r.deadline
|
|
3348
|
+
}
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3351
|
+
function registrationDigest(context, registration, options = {}) {
|
|
3352
|
+
const typed = registrationTypedData(context, registration, options);
|
|
3353
|
+
return TypedDataEncoder2.hash(typed.domain, typed.types, typed.message);
|
|
3354
|
+
}
|
|
3355
|
+
function buildRegistrationCalldata(context, registration, signature, options = {}) {
|
|
3356
|
+
const normalized = normalize(context, registration, options);
|
|
3357
|
+
if (typeof signature !== "string" || !isHexString3(signature) || signature.length % 2 !== 0)
|
|
3358
|
+
throw new Error("Registration signature must be hexadecimal bytes");
|
|
3268
3359
|
return {
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3360
|
+
to: normalized.registryAddress,
|
|
3361
|
+
value: 0n,
|
|
3362
|
+
callData: new Interface6(invisiAddressRegistryAbi).encodeFunctionData(
|
|
3363
|
+
options.update ? "updateKeys" : "registerKeys",
|
|
3364
|
+
[
|
|
3365
|
+
{
|
|
3366
|
+
// The registry wire schema retains its deployed EIP-712/ABI field name.
|
|
3367
|
+
walletIdentity: toBeHex2(BigInt(normalized.registration.ownerAddress), 32),
|
|
3368
|
+
keys: normalized.registration.keys,
|
|
3369
|
+
nonce: normalized.registration.nonce,
|
|
3370
|
+
deadline: normalized.registration.deadline
|
|
3371
|
+
},
|
|
3372
|
+
signature
|
|
3373
|
+
]
|
|
3374
|
+
)
|
|
3275
3375
|
};
|
|
3276
3376
|
}
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3377
|
+
function createRegistryReceivingKeyLookup(client, registryAddress) {
|
|
3378
|
+
const address = getAddress8(registryAddress);
|
|
3379
|
+
if (BigInt(address) === 0n) throw new Error("Registry address must be nonzero");
|
|
3380
|
+
return async (ownerAddress) => {
|
|
3381
|
+
const identity = decodeAddress(
|
|
3382
|
+
ReceivingAddress.fromIdentity(ownerAddress).toString()
|
|
3383
|
+
).ownerAddress;
|
|
3384
|
+
const [keys, nonce] = await new Contract2(
|
|
3385
|
+
address,
|
|
3386
|
+
invisiAddressRegistryAbi,
|
|
3387
|
+
client
|
|
3388
|
+
).getFunction("receivingKeysOf")(toBeHex2(BigInt(identity), 32));
|
|
3389
|
+
uint64(nonce, "registry nonce");
|
|
3390
|
+
const normalized = normalizeKeys(identity, keys);
|
|
3288
3391
|
return {
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
artifactSha256: schema.artifactSha256,
|
|
3292
|
-
vkSha256: key,
|
|
3293
|
-
expectedPublicInputs: [...expectedPublicInputs]
|
|
3392
|
+
viewingPublicKey: getBytes(normalized.viewingPublicKey),
|
|
3393
|
+
clueKey: getBytes(normalized.clueKey)
|
|
3294
3394
|
};
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
// src/registry/RegistrationOperation.ts
|
|
3399
|
+
function check2(ok, message) {
|
|
3400
|
+
if (!ok) {
|
|
3401
|
+
throw new Error(message);
|
|
3295
3402
|
}
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3403
|
+
}
|
|
3404
|
+
var RegistrationOperation = class _RegistrationOperation {
|
|
3405
|
+
#registry;
|
|
3406
|
+
#runtime;
|
|
3407
|
+
#plan;
|
|
3408
|
+
#signature;
|
|
3409
|
+
#hash;
|
|
3410
|
+
#anchor;
|
|
3411
|
+
status;
|
|
3412
|
+
constructor(registry, runtime, plan, status, signature, hash, anchor) {
|
|
3413
|
+
this.#registry = registry;
|
|
3414
|
+
this.#runtime = { ...runtime };
|
|
3415
|
+
this.#plan = structuredClone(plan);
|
|
3416
|
+
this.status = status;
|
|
3417
|
+
this.#signature = signature;
|
|
3418
|
+
this.#hash = hash;
|
|
3419
|
+
this.#anchor = anchor ? { ...anchor } : void 0;
|
|
3420
|
+
Object.freeze(this);
|
|
3302
3421
|
}
|
|
3303
|
-
async
|
|
3304
|
-
|
|
3305
|
-
const
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3422
|
+
static async prepare(registry, options) {
|
|
3423
|
+
const runtime = { ...options };
|
|
3424
|
+
const update = options.update ?? false;
|
|
3425
|
+
check2(typeof update === "boolean", "update must be boolean");
|
|
3426
|
+
await registry.validateDeployment(runtime.reader);
|
|
3427
|
+
const block = await runtime.reader.getBlock(await runtime.reader.getLatestBlockNumber());
|
|
3428
|
+
const context = { ...registry.context, now: block.timestamp };
|
|
3429
|
+
const nonce = await runtime.reader.getRegistryNonce(
|
|
3430
|
+
context,
|
|
3431
|
+
runtime.wallet.ownerAddress,
|
|
3432
|
+
block
|
|
3433
|
+
);
|
|
3311
3434
|
check2(
|
|
3312
|
-
|
|
3313
|
-
"
|
|
3435
|
+
typeof nonce === "bigint" && nonce >= 0n && nonce < (1n << 64n) - 1n,
|
|
3436
|
+
"invalid or exhausted registry nonce"
|
|
3314
3437
|
);
|
|
3315
|
-
const witness = signed.withWitness((value) => value);
|
|
3316
3438
|
check2(
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
Uint8Array.from([4, ...witness.public_key_x, ...witness.public_key_y]),
|
|
3320
|
-
Uint8Array.from(witness.signature),
|
|
3321
|
-
request
|
|
3322
|
-
),
|
|
3323
|
-
"invalid wallet authorization"
|
|
3439
|
+
update ? nonce > 0n : nonce === 0n,
|
|
3440
|
+
update ? "wallet is not registered" : "wallet is already registered; prepare an update"
|
|
3324
3441
|
);
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3442
|
+
const address = ReceivingAddress2.parse(await registry.resolve(runtime.wallet.getAddress()));
|
|
3443
|
+
const keys = address.getReceivingKeys();
|
|
3444
|
+
const registration = {
|
|
3445
|
+
ownerAddress: runtime.wallet.ownerAddress,
|
|
3446
|
+
keys: {
|
|
3447
|
+
viewingPublicKey: hexlify2(keys.viewingPublicKey),
|
|
3448
|
+
clueKey: hexlify2(keys.clueKey)
|
|
3449
|
+
},
|
|
3450
|
+
nonce,
|
|
3451
|
+
deadline: options.deadline ?? block.timestamp + 300n
|
|
3452
|
+
};
|
|
3453
|
+
registrationTypedData(context, registration, { update, expectedNonce: nonce });
|
|
3329
3454
|
check2(
|
|
3330
|
-
|
|
3331
|
-
"
|
|
3455
|
+
(await runtime.reader.getBlock(block.number)).hash === block.hash,
|
|
3456
|
+
"canonical registry block changed while preparing"
|
|
3332
3457
|
);
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
)
|
|
3339
|
-
);
|
|
3340
|
-
check2(root === BigInt(sources[source].merkleRoot), "membership root mismatch");
|
|
3341
|
-
return source;
|
|
3342
|
-
}
|
|
3343
|
-
async noteLeaf(note) {
|
|
3344
|
-
this.context(note);
|
|
3345
|
-
return computeNoteCommitment3(
|
|
3346
|
-
note.chainId,
|
|
3347
|
-
await computeNoteIdentity2(BigInt(note.ownerAddress), note.nonce),
|
|
3348
|
-
note.tokenAddress,
|
|
3349
|
-
note.amount
|
|
3458
|
+
return new _RegistrationOperation(
|
|
3459
|
+
registry,
|
|
3460
|
+
runtime,
|
|
3461
|
+
{ id: crypto.randomUUID(), context, registration, update },
|
|
3462
|
+
"prepared"
|
|
3350
3463
|
);
|
|
3351
3464
|
}
|
|
3352
|
-
|
|
3353
|
-
this.
|
|
3354
|
-
return computeCreditCommitment2(
|
|
3355
|
-
note.chainId,
|
|
3356
|
-
note.spenderWalletIdentity,
|
|
3357
|
-
note.ownerWalletIdentity,
|
|
3358
|
-
note.spenderPayout,
|
|
3359
|
-
note.ownerPayout,
|
|
3360
|
-
note.tokenAddress,
|
|
3361
|
-
note.amount,
|
|
3362
|
-
note.nonce,
|
|
3363
|
-
note.expiry,
|
|
3364
|
-
await computeCreditSharedKeyHash2(note.creditSharedKey),
|
|
3365
|
-
note
|
|
3366
|
-
);
|
|
3465
|
+
get id() {
|
|
3466
|
+
return this.#plan.id;
|
|
3367
3467
|
}
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
);
|
|
3378
|
-
return BigInt(await this.membership(leaf, input, sources));
|
|
3379
|
-
})
|
|
3380
|
-
);
|
|
3468
|
+
preview() {
|
|
3469
|
+
return {
|
|
3470
|
+
id: this.id,
|
|
3471
|
+
status: this.status,
|
|
3472
|
+
chainId: this.#plan.context.chainId,
|
|
3473
|
+
registryAddress: this.#plan.context.registryAddress,
|
|
3474
|
+
...structuredClone(this.#plan.registration),
|
|
3475
|
+
update: this.#plan.update
|
|
3476
|
+
};
|
|
3381
3477
|
}
|
|
3382
|
-
|
|
3383
|
-
|
|
3478
|
+
/** Export portable EIP-712 data for external signing. */
|
|
3479
|
+
async exportRegistrationAuthorization(options = {}) {
|
|
3480
|
+
check2(this.status === "prepared", "registry operation is not awaiting authorization");
|
|
3481
|
+
options.signal?.throwIfAborted();
|
|
3482
|
+
await this.validateFreshness();
|
|
3483
|
+
options.signal?.throwIfAborted();
|
|
3484
|
+
return createSigningRequest(this.typedData());
|
|
3485
|
+
}
|
|
3486
|
+
/** Validate and attach an independently obtained EOA or ERC-1271 signature. */
|
|
3487
|
+
async withSignature(signature, options = {}) {
|
|
3488
|
+
check2(this.status === "prepared", "registry operation is not awaiting authorization");
|
|
3489
|
+
options.signal?.throwIfAborted();
|
|
3490
|
+
await this.validateFreshness();
|
|
3491
|
+
options.signal?.throwIfAborted();
|
|
3384
3492
|
check2(
|
|
3385
|
-
|
|
3386
|
-
"
|
|
3387
|
-
);
|
|
3388
|
-
const identity = p.inputs[0]?.note.ownerAddress;
|
|
3389
|
-
const unsignedSweep = !!identity && !p.authorization && n.outputCount === 0 && n.depositAmount === 0n && n.feeAmount === 0n && BigInt(p.params.withdrawalAddress) === BigInt(identity);
|
|
3390
|
-
const authority = identity && !unsignedSweep ? await this.authority("transact", n.capacity, words, p.authorization, identity) : zeroAuthority();
|
|
3391
|
-
const sources = await this.noteInputs(
|
|
3392
|
-
p.inputs,
|
|
3393
|
-
p.params.sources,
|
|
3394
|
-
p.params.nullifiers,
|
|
3395
|
-
identity ?? "0x" + "00".repeat(32),
|
|
3396
|
-
authority
|
|
3397
|
-
);
|
|
3398
|
-
const outputs = await Promise.all(
|
|
3399
|
-
p.outputs.map(async (output, i) => {
|
|
3400
|
-
const opening = "identity" in output ? output.note : output;
|
|
3401
|
-
const identity2 = "identity" in output ? field(output.identity) : BigInt(
|
|
3402
|
-
await computeNoteIdentity2(BigInt(output.ownerAddress), output.nonce)
|
|
3403
|
-
);
|
|
3404
|
-
const amount = uint(output.amount, 128, "output amount");
|
|
3405
|
-
check2(identity2 !== 0n && amount !== 0n, "zero output identity or amount");
|
|
3406
|
-
if (opening) {
|
|
3407
|
-
this.context(opening);
|
|
3408
|
-
check2(
|
|
3409
|
-
opening.amount === amount && BigInt(
|
|
3410
|
-
await computeNoteIdentity2(
|
|
3411
|
-
BigInt(opening.ownerAddress),
|
|
3412
|
-
opening.nonce
|
|
3413
|
-
)
|
|
3414
|
-
) === identity2,
|
|
3415
|
-
"output opening mismatch"
|
|
3416
|
-
);
|
|
3417
|
-
}
|
|
3418
|
-
const commitment = await computeNoteCommitment3(
|
|
3419
|
-
c.chainId,
|
|
3420
|
-
word(identity2),
|
|
3421
|
-
BigInt(c.tokenAddress),
|
|
3422
|
-
amount
|
|
3423
|
-
);
|
|
3424
|
-
check2(commitment === p.params.outputCommitments[i], "output commitment mismatch");
|
|
3425
|
-
return { identity: identity2, amount };
|
|
3426
|
-
})
|
|
3493
|
+
/^0x(?:[a-fA-F0-9]{2})+$/.test(signature),
|
|
3494
|
+
"registry signature must be nonempty hexadecimal bytes"
|
|
3427
3495
|
);
|
|
3428
|
-
const
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
p.inputs.map((input) => BigInt(input.leafIndex)),
|
|
3451
|
-
n.capacity,
|
|
3452
|
-
() => 0n
|
|
3453
|
-
),
|
|
3454
|
-
merkle_paths: pad2(
|
|
3455
|
-
p.inputs.map((input) => [...input.path]),
|
|
3456
|
-
n.capacity,
|
|
3457
|
-
zeros
|
|
3496
|
+
const p = this.#plan;
|
|
3497
|
+
const owner = p.registration.ownerAddress;
|
|
3498
|
+
let valid = false;
|
|
3499
|
+
try {
|
|
3500
|
+
const bytes = getBytes2(signature);
|
|
3501
|
+
const typed = this.typedData();
|
|
3502
|
+
valid = bytes.length === 65 && (bytes[64] === 27 || bytes[64] === 28) && BigInt(verifyTypedData(typed.domain, typed.types, typed.message, signature)) === BigInt(owner);
|
|
3503
|
+
} catch {
|
|
3504
|
+
valid = false;
|
|
3505
|
+
}
|
|
3506
|
+
if (!valid) {
|
|
3507
|
+
const code = await this.#runtime.reader.getCode(owner);
|
|
3508
|
+
check2(/^0x(?:[a-fA-F0-9]{2})*$/.test(code), "invalid owner contract code");
|
|
3509
|
+
check2(code !== "0x", "invalid EOA registry signature");
|
|
3510
|
+
const verify = this.#runtime.reader.isValidRegistrationSignature;
|
|
3511
|
+
check2(verify, "ERC-1271 registry signature verification capability is missing");
|
|
3512
|
+
check2(
|
|
3513
|
+
await verify.call(
|
|
3514
|
+
this.#runtime.reader,
|
|
3515
|
+
owner,
|
|
3516
|
+
registrationDigest(p.context, p.registration, { update: p.update }),
|
|
3517
|
+
signature
|
|
3458
3518
|
),
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3519
|
+
"invalid ERC-1271 registry signature"
|
|
3520
|
+
);
|
|
3521
|
+
}
|
|
3522
|
+
options.signal?.throwIfAborted();
|
|
3523
|
+
return new _RegistrationOperation(
|
|
3524
|
+
this.#registry,
|
|
3525
|
+
this.#runtime,
|
|
3526
|
+
this.#plan,
|
|
3527
|
+
"ready",
|
|
3528
|
+
signature
|
|
3463
3529
|
);
|
|
3464
3530
|
}
|
|
3465
|
-
|
|
3466
|
-
|
|
3531
|
+
/** Empty signatures require the transaction sender to be the wallet owner. */
|
|
3532
|
+
async authorizeDirect() {
|
|
3533
|
+
check2(this.status === "prepared", "registry operation is not awaiting authorization");
|
|
3534
|
+
await this.validateFreshness();
|
|
3535
|
+
return new _RegistrationOperation(this.#registry, this.#runtime, this.#plan, "ready", "0x");
|
|
3536
|
+
}
|
|
3537
|
+
typedData() {
|
|
3538
|
+
const p = this.#plan;
|
|
3539
|
+
return registrationTypedData(p.context, p.registration, {
|
|
3540
|
+
update: p.update,
|
|
3541
|
+
expectedNonce: p.registration.nonce
|
|
3542
|
+
});
|
|
3543
|
+
}
|
|
3544
|
+
call() {
|
|
3545
|
+
check2(this.#signature !== void 0, "registry signature is missing");
|
|
3546
|
+
const p = this.#plan;
|
|
3547
|
+
const result = buildRegistrationCalldata(p.context, p.registration, this.#signature, {
|
|
3548
|
+
update: p.update,
|
|
3549
|
+
expectedNonce: p.registration.nonce
|
|
3550
|
+
});
|
|
3551
|
+
return { to: result.to, data: result.callData, value: result.value };
|
|
3552
|
+
}
|
|
3553
|
+
async exportTransaction() {
|
|
3467
3554
|
check2(
|
|
3468
|
-
|
|
3469
|
-
"
|
|
3470
|
-
);
|
|
3471
|
-
if (operation === "create-credit" && [note.lastDay, note.dailySpent, note.lastMonth, note.monthlySpent].some((v) => v !== 0n))
|
|
3472
|
-
throw new Error("new Credit usage must be zero");
|
|
3473
|
-
const authority = await this.authority(
|
|
3474
|
-
operation,
|
|
3475
|
-
10,
|
|
3476
|
-
words,
|
|
3477
|
-
p.authorization,
|
|
3478
|
-
note.ownerWalletIdentity
|
|
3479
|
-
);
|
|
3480
|
-
const sources = await this.noteInputs(
|
|
3481
|
-
p.inputs,
|
|
3482
|
-
p.params.sources,
|
|
3483
|
-
p.params.nullifiers,
|
|
3484
|
-
note.ownerWalletIdentity,
|
|
3485
|
-
authority
|
|
3555
|
+
this.#signature !== void 0 && this.status !== "prepared",
|
|
3556
|
+
"registry operation is not ready"
|
|
3486
3557
|
);
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
p.change.ownerAddress === note.ownerWalletIdentity && await this.noteLeaf(p.change) === p.params.changeCommitment,
|
|
3490
|
-
"change commitment mismatch"
|
|
3491
|
-
);
|
|
3492
|
-
else check2(BigInt(p.params.changeCommitment) === 0n, "missing change note");
|
|
3493
|
-
return {
|
|
3494
|
-
...authority,
|
|
3495
|
-
input_amounts: pad2(
|
|
3496
|
-
p.inputs.map((input) => input.note.amount),
|
|
3497
|
-
10,
|
|
3498
|
-
() => 0n
|
|
3499
|
-
),
|
|
3500
|
-
input_nonces: pad2(
|
|
3501
|
-
p.inputs.map((input) => input.note.nonce),
|
|
3502
|
-
10,
|
|
3503
|
-
() => 0n
|
|
3504
|
-
),
|
|
3505
|
-
leaf_indices: pad2(
|
|
3506
|
-
p.inputs.map((input) => BigInt(input.leafIndex)),
|
|
3507
|
-
10,
|
|
3508
|
-
() => 0n
|
|
3509
|
-
),
|
|
3510
|
-
merkle_paths: pad2(
|
|
3511
|
-
p.inputs.map((input) => [...input.path]),
|
|
3512
|
-
10,
|
|
3513
|
-
zeros
|
|
3514
|
-
),
|
|
3515
|
-
input_source_indices: pad2(sources, 10, () => 0n),
|
|
3516
|
-
spender_payout: note.spenderPayout,
|
|
3517
|
-
spender_wallet_identity: BigInt(note.spenderWalletIdentity),
|
|
3518
|
-
owner_wallet_identity: BigInt(note.ownerWalletIdentity),
|
|
3519
|
-
...policyWitness(note, operation === "top-up-credit"),
|
|
3520
|
-
credit_amount: note.amount,
|
|
3521
|
-
credit_nonce: note.nonce,
|
|
3522
|
-
credit_shared_key: note.creditSharedKey,
|
|
3523
|
-
change_amount: p.change?.amount ?? 0n,
|
|
3524
|
-
change_nonce: p.change?.nonce ?? 0n
|
|
3525
|
-
};
|
|
3558
|
+
await this.validateFreshness();
|
|
3559
|
+
return this.call();
|
|
3526
3560
|
}
|
|
3527
|
-
async
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
p.deliveries,
|
|
3532
|
-
p.funding
|
|
3533
|
-
);
|
|
3534
|
-
return provingRequest(
|
|
3535
|
-
"create_credit",
|
|
3536
|
-
await this.creation(p, "create-credit", words),
|
|
3537
|
-
words
|
|
3561
|
+
async validateFreshness() {
|
|
3562
|
+
await this.#registry.validateDeployment(this.#runtime.reader);
|
|
3563
|
+
const block = await this.#runtime.reader.getBlock(
|
|
3564
|
+
await this.#runtime.reader.getLatestBlockNumber()
|
|
3538
3565
|
);
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
const note = normalizeCreditDeliveryState(p.input.state).note, leaf = await this.creditLeaf(note), c = this.#protocol.context;
|
|
3566
|
+
const p = this.#plan;
|
|
3567
|
+
check2(block.timestamp <= p.registration.deadline, "registry operation has expired");
|
|
3542
3568
|
check2(
|
|
3543
|
-
await
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
"
|
|
3549
|
-
1,
|
|
3550
|
-
words,
|
|
3551
|
-
p.authorization,
|
|
3552
|
-
note.ownerWalletIdentity
|
|
3569
|
+
await this.#runtime.reader.getRegistryNonce(
|
|
3570
|
+
p.context,
|
|
3571
|
+
p.registration.ownerAddress,
|
|
3572
|
+
block
|
|
3573
|
+
) === p.registration.nonce,
|
|
3574
|
+
"registry nonce changed; prepare again"
|
|
3553
3575
|
);
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
...authority,
|
|
3558
|
-
spender_payout: note.spenderPayout,
|
|
3559
|
-
spender_wallet_identity: BigInt(note.spenderWalletIdentity),
|
|
3560
|
-
owner_wallet_identity: BigInt(note.ownerWalletIdentity),
|
|
3561
|
-
...policyWitness(note),
|
|
3562
|
-
input_nonce: note.nonce,
|
|
3563
|
-
expiry: note.expiry,
|
|
3564
|
-
credit_shared_key: note.creditSharedKey,
|
|
3565
|
-
leaf_index: BigInt(p.input.leafIndex),
|
|
3566
|
-
merkle_path: [...p.input.path]
|
|
3567
|
-
},
|
|
3568
|
-
words
|
|
3576
|
+
check2(
|
|
3577
|
+
(await this.#runtime.reader.getBlock(block.number)).hash === block.hash,
|
|
3578
|
+
"canonical registry block changed during authorization"
|
|
3569
3579
|
);
|
|
3570
3580
|
}
|
|
3571
|
-
|
|
3572
|
-
|
|
3581
|
+
/** Associate a signed/broadcast transaction; ethers owns sending and receipt waiting. */
|
|
3582
|
+
trackTransaction(transactionHash) {
|
|
3583
|
+
check2(/^0x[a-fA-F0-9]{64}$/.test(transactionHash), "invalid registry transaction hash");
|
|
3573
3584
|
check2(
|
|
3574
|
-
|
|
3575
|
-
"
|
|
3585
|
+
this.#signature !== void 0 && ["ready", "uncertain"].includes(this.status),
|
|
3586
|
+
"registry operation is not ready"
|
|
3576
3587
|
);
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3588
|
+
return new _RegistrationOperation(
|
|
3589
|
+
this.#registry,
|
|
3590
|
+
this.#runtime,
|
|
3591
|
+
this.#plan,
|
|
3592
|
+
"uncertain",
|
|
3593
|
+
this.#signature,
|
|
3594
|
+
transactionHash
|
|
3584
3595
|
);
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3596
|
+
}
|
|
3597
|
+
async reconcile(options = {}) {
|
|
3598
|
+
options.signal?.throwIfAborted();
|
|
3599
|
+
const { reader } = this.#runtime;
|
|
3600
|
+
const p = this.#plan;
|
|
3601
|
+
await this.#registry.validateDeployment(reader);
|
|
3602
|
+
if (this.#anchor && (await reader.getBlock(this.#anchor.number)).hash !== this.#anchor.hash) {
|
|
3603
|
+
return new _RegistrationOperation(
|
|
3604
|
+
this.#registry,
|
|
3605
|
+
this.#runtime,
|
|
3606
|
+
p,
|
|
3607
|
+
"reorged",
|
|
3608
|
+
this.#signature,
|
|
3609
|
+
this.#hash
|
|
3610
|
+
);
|
|
3611
|
+
}
|
|
3612
|
+
const hash = options.transactionHash ?? this.#hash;
|
|
3613
|
+
const receipt = hash ? await reader.getReceipt(hash) : null;
|
|
3614
|
+
if (receipt) {
|
|
3615
|
+
check2(
|
|
3616
|
+
receipt.transactionHash === hash && (await reader.getBlock(receipt.block.number)).hash === receipt.block.hash,
|
|
3617
|
+
"registry receipt is not canonical"
|
|
3618
|
+
);
|
|
3619
|
+
const transaction = await reader.getTransaction(hash);
|
|
3620
|
+
const call = this.call();
|
|
3621
|
+
check2(
|
|
3622
|
+
transaction && transaction.transactionHash === hash && transaction.to.toLowerCase() === call.to.toLowerCase() && transaction.data.toLowerCase() === call.data.toLowerCase() && transaction.value === 0n && (this.#signature !== "0x" || BigInt(transaction.from) === BigInt(p.registration.ownerAddress)),
|
|
3623
|
+
"registry transaction differs from frozen request"
|
|
3624
|
+
);
|
|
3625
|
+
check2(
|
|
3626
|
+
receipt.status === "success" || receipt.status === "reverted",
|
|
3627
|
+
"registry execution status is missing"
|
|
3628
|
+
);
|
|
3629
|
+
if (receipt.status === "success") {
|
|
3630
|
+
const found = receipt.logs.some((log) => {
|
|
3631
|
+
if (log.address.toLowerCase() !== p.context.registryAddress.toLowerCase() || log.transactionHash !== hash) {
|
|
3632
|
+
return false;
|
|
3633
|
+
}
|
|
3634
|
+
try {
|
|
3635
|
+
const event = ((decoded) => ({
|
|
3636
|
+
eventName: decoded.name,
|
|
3637
|
+
args: decoded.args.toObject()
|
|
3638
|
+
}))(
|
|
3639
|
+
new Interface7(invisiAddressRegistryAbi).parseLog({
|
|
3640
|
+
data: log.data,
|
|
3641
|
+
topics: log.topics
|
|
3642
|
+
})
|
|
3643
|
+
);
|
|
3644
|
+
return event.eventName === "ReceivingKeysSet" && BigInt(event.args.walletIdentity) === BigInt(p.registration.ownerAddress) && event.args.viewingPublicKey.toLowerCase() === p.registration.keys.viewingPublicKey.toLowerCase() && event.args.clueKey.toLowerCase() === p.registration.keys.clueKey.toLowerCase() && event.args.nextNonce === p.registration.nonce + 1n;
|
|
3645
|
+
} catch {
|
|
3646
|
+
return false;
|
|
3647
|
+
}
|
|
3648
|
+
});
|
|
3649
|
+
check2(found, "registry receipt has no matching key update event");
|
|
3589
3650
|
check2(
|
|
3590
|
-
|
|
3591
|
-
"
|
|
3651
|
+
(await reader.getBlock(receipt.block.number)).hash === receipt.block.hash,
|
|
3652
|
+
"registry block changed during confirmation"
|
|
3592
3653
|
);
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
lastMonth: "last_months",
|
|
3608
|
-
monthlySpent: "monthly_spents"
|
|
3609
|
-
}).map(([key, name]) => [
|
|
3610
|
-
name,
|
|
3611
|
-
pad2(
|
|
3612
|
-
notes2.map(
|
|
3613
|
-
(note) => normalizeCreditPolicy(note)[key]
|
|
3614
|
-
),
|
|
3615
|
-
capacity,
|
|
3616
|
-
() => 0n
|
|
3617
|
-
)
|
|
3618
|
-
])
|
|
3619
|
-
),
|
|
3620
|
-
spender_wallet_identity: BigInt(identity),
|
|
3621
|
-
owner_payouts: pad2(
|
|
3622
|
-
notes2.map((note) => note.ownerPayout),
|
|
3623
|
-
capacity,
|
|
3624
|
-
() => 0n
|
|
3625
|
-
),
|
|
3626
|
-
owner_wallet_identities: pad2(
|
|
3627
|
-
notes2.map((note) => BigInt(note.ownerWalletIdentity)),
|
|
3628
|
-
capacity,
|
|
3629
|
-
() => 0n
|
|
3630
|
-
),
|
|
3631
|
-
input_amounts: pad2(
|
|
3632
|
-
notes2.map((note) => note.amount),
|
|
3633
|
-
capacity,
|
|
3634
|
-
() => 0n
|
|
3635
|
-
),
|
|
3636
|
-
input_nonces: pad2(
|
|
3637
|
-
notes2.map((note) => note.nonce),
|
|
3638
|
-
capacity,
|
|
3639
|
-
() => 0n
|
|
3640
|
-
),
|
|
3641
|
-
credit_shared_keys: pad2(
|
|
3642
|
-
notes2.map((note) => note.creditSharedKey),
|
|
3643
|
-
capacity,
|
|
3644
|
-
() => 0n
|
|
3645
|
-
),
|
|
3646
|
-
leaf_indices: pad2(
|
|
3647
|
-
p.inputs.map((input) => BigInt(input.leafIndex)),
|
|
3648
|
-
capacity,
|
|
3649
|
-
() => 0n
|
|
3650
|
-
),
|
|
3651
|
-
merkle_paths: pad2(
|
|
3652
|
-
p.inputs.map((input) => [...input.path]),
|
|
3653
|
-
capacity,
|
|
3654
|
-
zeros
|
|
3655
|
-
),
|
|
3656
|
-
input_source_indices: pad2(sourceIndices, capacity, () => 0n),
|
|
3657
|
-
draw_amounts: pad2(p.amounts, capacity, () => 0n)
|
|
3658
|
-
},
|
|
3659
|
-
words
|
|
3660
|
-
);
|
|
3661
|
-
}
|
|
3662
|
-
async topUpCredit(p) {
|
|
3663
|
-
const c = this.#protocol.context, previous = normalizeCreditDeliveryState(p.previous.state), next = normalizeCreditDeliveryState(p.state), old = previous.note, note = next.note, leaf = await this.creditLeaf(old);
|
|
3664
|
-
for (const field2 of [
|
|
3665
|
-
"spenderWalletIdentity",
|
|
3666
|
-
"ownerWalletIdentity",
|
|
3667
|
-
"spenderPayout",
|
|
3668
|
-
"ownerPayout",
|
|
3669
|
-
"creditSharedKey",
|
|
3670
|
-
"lastDay",
|
|
3671
|
-
"dailySpent",
|
|
3672
|
-
"lastMonth",
|
|
3673
|
-
"monthlySpent"
|
|
3674
|
-
])
|
|
3675
|
-
check2(old[field2] === note[field2], "Credit role or payout changed");
|
|
3676
|
-
check2(
|
|
3677
|
-
JSON.stringify(previous.ownerReceivingKeys) === JSON.stringify(next.ownerReceivingKeys) && JSON.stringify(previous.spenderReceivingKeys) === JSON.stringify(next.spenderReceivingKeys),
|
|
3678
|
-
"Credit receiving snapshots changed"
|
|
3679
|
-
);
|
|
3680
|
-
check2(
|
|
3681
|
-
old.amount < note.amount && old.expiry <= note.expiry && old.expiry === p.params.previousExpiry && (c.now === void 0 || old.expiry > c.now),
|
|
3682
|
-
"invalid Credit top-up"
|
|
3683
|
-
);
|
|
3654
|
+
options.signal?.throwIfAborted();
|
|
3655
|
+
return new _RegistrationOperation(
|
|
3656
|
+
this.#registry,
|
|
3657
|
+
this.#runtime,
|
|
3658
|
+
p,
|
|
3659
|
+
"confirmed",
|
|
3660
|
+
this.#signature,
|
|
3661
|
+
hash,
|
|
3662
|
+
receipt.block
|
|
3663
|
+
);
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
const head = await reader.getBlock(await reader.getLatestBlockNumber());
|
|
3667
|
+
const nonce = await reader.getRegistryNonce(p.context, p.registration.ownerAddress, head);
|
|
3684
3668
|
check2(
|
|
3685
|
-
await
|
|
3686
|
-
"
|
|
3669
|
+
(await reader.getBlock(head.number)).hash === head.hash,
|
|
3670
|
+
"canonical registry block changed during reconciliation"
|
|
3687
3671
|
);
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
previous_daily_limit: old.dailyLimit,
|
|
3696
|
-
previous_monthly_limit: old.monthlyLimit,
|
|
3697
|
-
previous_credit_amount: old.amount,
|
|
3698
|
-
previous_credit_nonce: old.nonce,
|
|
3699
|
-
previous_credit_leaf_index: BigInt(p.previous.leafIndex),
|
|
3700
|
-
previous_credit_merkle_path: [...p.previous.path]
|
|
3701
|
-
},
|
|
3702
|
-
words
|
|
3672
|
+
return new _RegistrationOperation(
|
|
3673
|
+
this.#registry,
|
|
3674
|
+
this.#runtime,
|
|
3675
|
+
p,
|
|
3676
|
+
head.timestamp > p.registration.deadline ? "expired" : nonce !== p.registration.nonce ? "failed" : "uncertain",
|
|
3677
|
+
this.#signature,
|
|
3678
|
+
hash
|
|
3703
3679
|
);
|
|
3704
3680
|
}
|
|
3681
|
+
toJSON() {
|
|
3682
|
+
return {
|
|
3683
|
+
id: this.id,
|
|
3684
|
+
kind: this.#plan.update ? "update-receiving-keys" : "register-receiving-keys",
|
|
3685
|
+
status: this.status
|
|
3686
|
+
};
|
|
3687
|
+
}
|
|
3688
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
3689
|
+
return this.toJSON();
|
|
3690
|
+
}
|
|
3691
|
+
};
|
|
3692
|
+
|
|
3693
|
+
// src/registry/AddressRegistry.ts
|
|
3694
|
+
var AddressRegistry = class {
|
|
3695
|
+
context;
|
|
3696
|
+
#lookup;
|
|
3697
|
+
expectedRuntimeCodeHash;
|
|
3698
|
+
constructor(context, options = {}) {
|
|
3699
|
+
if (typeof context.chainId !== "bigint" || context.chainId < 0n || context.chainId >= 1n << 64n || BigInt(getAddress9(context.registryAddress)) === 0n) {
|
|
3700
|
+
throw new Error("invalid registry context");
|
|
3701
|
+
}
|
|
3702
|
+
this.context = Object.freeze({
|
|
3703
|
+
...context,
|
|
3704
|
+
registryAddress: getAddress9(context.registryAddress)
|
|
3705
|
+
});
|
|
3706
|
+
this.#lookup = options.lookup;
|
|
3707
|
+
this.expectedRuntimeCodeHash = options.expectedRuntimeCodeHash?.toLowerCase();
|
|
3708
|
+
if (this.expectedRuntimeCodeHash && !/^0x[0-9a-f]{64}$/.test(this.expectedRuntimeCodeHash)) {
|
|
3709
|
+
throw new Error("invalid registry runtime profile");
|
|
3710
|
+
}
|
|
3711
|
+
Object.freeze(this);
|
|
3712
|
+
}
|
|
3713
|
+
async validateDeployment(reader) {
|
|
3714
|
+
if (!this.expectedRuntimeCodeHash) {
|
|
3715
|
+
throw new MissingCapabilityError2("trusted registry deployment profile");
|
|
3716
|
+
}
|
|
3717
|
+
if (await reader.getChainId() !== this.context.chainId) {
|
|
3718
|
+
throw new Error("registry chain mismatch");
|
|
3719
|
+
}
|
|
3720
|
+
const code = await reader.getCode(this.context.registryAddress);
|
|
3721
|
+
if (!/^0x(?:[0-9a-fA-F]{2})+$/.test(code) || keccak2569(code) !== this.expectedRuntimeCodeHash) {
|
|
3722
|
+
throw new Error("registry runtime profile mismatch");
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
prepareRegistration(options) {
|
|
3726
|
+
return RegistrationOperation.prepare(this, options);
|
|
3727
|
+
}
|
|
3728
|
+
async resolve(address) {
|
|
3729
|
+
const receiving = ReceivingAddress3.parse(address);
|
|
3730
|
+
if (receiving.kind === "short" && !this.#lookup) {
|
|
3731
|
+
throw new MissingCapabilityError2("registry reader");
|
|
3732
|
+
}
|
|
3733
|
+
return (await receiving.resolve(this.#lookup)).toString();
|
|
3734
|
+
}
|
|
3735
|
+
typedData(registration, options) {
|
|
3736
|
+
return registrationTypedData(this.context, registration, options);
|
|
3737
|
+
}
|
|
3738
|
+
digest(registration, options) {
|
|
3739
|
+
return registrationDigest(this.context, registration, options);
|
|
3740
|
+
}
|
|
3741
|
+
buildCall(registration, signature, options) {
|
|
3742
|
+
return buildRegistrationCalldata(this.context, registration, signature, options);
|
|
3743
|
+
}
|
|
3705
3744
|
};
|
|
3706
3745
|
|
|
3707
|
-
// src/
|
|
3708
|
-
import {
|
|
3746
|
+
// src/verification/verify.ts
|
|
3747
|
+
import {
|
|
3748
|
+
computeCreditCommitment as computeCreditCommitment2,
|
|
3749
|
+
computeCreditSharedKeyHash as computeCreditSharedKeyHash2,
|
|
3750
|
+
computeNoteCommitment as computeNoteCommitment4,
|
|
3751
|
+
computeNoteIdentity as computeNoteIdentity3,
|
|
3752
|
+
FIELD_MODULUS as FIELD_MODULUS2
|
|
3753
|
+
} from "@invisi-labs/core";
|
|
3754
|
+
async function verifyNoteCommitment(params) {
|
|
3755
|
+
const expected = await computeNoteCommitment4(
|
|
3756
|
+
params.chainId,
|
|
3757
|
+
await computeNoteIdentity3(BigInt(params.ownerAddress), BigInt(params.nonce)),
|
|
3758
|
+
BigInt(params.tokenAddress),
|
|
3759
|
+
params.amount
|
|
3760
|
+
);
|
|
3761
|
+
return BigInt(expected) === BigInt(params.noteCommitment);
|
|
3762
|
+
}
|
|
3763
|
+
async function verifyCreditCommitment(params) {
|
|
3764
|
+
if (params.creditSharedKey <= 0n || params.creditSharedKey >= FIELD_MODULUS2)
|
|
3765
|
+
throw new RangeError("creditSharedKey must be a nonzero canonical Field");
|
|
3766
|
+
const expected = await computeCreditCommitment2(
|
|
3767
|
+
params.chainId,
|
|
3768
|
+
params.spenderWalletIdentity,
|
|
3769
|
+
params.ownerWalletIdentity,
|
|
3770
|
+
BigInt(params.spenderPayout),
|
|
3771
|
+
BigInt(params.ownerPayout),
|
|
3772
|
+
BigInt(params.tokenAddress),
|
|
3773
|
+
params.amount,
|
|
3774
|
+
params.nonce,
|
|
3775
|
+
params.expiry,
|
|
3776
|
+
await computeCreditSharedKeyHash2(params.creditSharedKey),
|
|
3777
|
+
params
|
|
3778
|
+
);
|
|
3779
|
+
return BigInt(expected) === BigInt(params.creditCommitment);
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
// src/factory/factory.ts
|
|
3783
|
+
import { Contract as Contract3, getAddress as getAddress10 } from "ethers";
|
|
3709
3784
|
async function resolveInstanceAddress(client, factoryAddress, salt) {
|
|
3710
3785
|
const instance = await new Contract3(
|
|
3711
|
-
|
|
3786
|
+
getAddress10(factoryAddress),
|
|
3712
3787
|
invisiFactoryAbi,
|
|
3713
3788
|
client
|
|
3714
3789
|
).getFunction("getInstanceAddress")(salt);
|
|
3715
|
-
return
|
|
3790
|
+
return getAddress10(instance);
|
|
3716
3791
|
}
|
|
3717
3792
|
export {
|
|
3718
3793
|
AddressRegistry,
|