@bananapus/nana-sdk-core 6.3.1 → 6.5.0
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/cjs/review/decode.d.ts.map +1 -1
- package/dist/cjs/review/decode.js +10 -3
- package/dist/cjs/review/decode.js.map +1 -1
- package/dist/cjs/review/safeSend.d.ts +7 -3
- package/dist/cjs/review/safeSend.d.ts.map +1 -1
- package/dist/cjs/review/safeSend.js +17 -6
- package/dist/cjs/review/safeSend.js.map +1 -1
- package/dist/cjs/safeService.d.ts +6 -0
- package/dist/cjs/safeService.d.ts.map +1 -1
- package/dist/cjs/safeService.js +13 -0
- package/dist/cjs/safeService.js.map +1 -1
- package/dist/cjs/v6/index.d.ts +1 -0
- package/dist/cjs/v6/index.d.ts.map +1 -1
- package/dist/cjs/v6/index.js +1 -0
- package/dist/cjs/v6/index.js.map +1 -1
- package/dist/cjs/v6/operationEvidence.d.ts +38 -0
- package/dist/cjs/v6/operationEvidence.d.ts.map +1 -0
- package/dist/cjs/v6/operationEvidence.js +860 -0
- package/dist/cjs/v6/operationEvidence.js.map +1 -0
- package/dist/esm/review/decode.d.ts.map +1 -1
- package/dist/esm/review/decode.js +10 -3
- package/dist/esm/review/decode.js.map +1 -1
- package/dist/esm/review/safeSend.d.ts +7 -3
- package/dist/esm/review/safeSend.d.ts.map +1 -1
- package/dist/esm/review/safeSend.js +18 -7
- package/dist/esm/review/safeSend.js.map +1 -1
- package/dist/esm/safeService.d.ts +6 -0
- package/dist/esm/safeService.d.ts.map +1 -1
- package/dist/esm/safeService.js +12 -0
- package/dist/esm/safeService.js.map +1 -1
- package/dist/esm/v6/index.d.ts +1 -0
- package/dist/esm/v6/index.d.ts.map +1 -1
- package/dist/esm/v6/index.js +1 -0
- package/dist/esm/v6/index.js.map +1 -1
- package/dist/esm/v6/operationEvidence.d.ts +38 -0
- package/dist/esm/v6/operationEvidence.d.ts.map +1 -0
- package/dist/esm/v6/operationEvidence.js +857 -0
- package/dist/esm/v6/operationEvidence.js.map +1 -0
- package/package.json +6 -1
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
import { decodeEventLog, decodeFunctionData, erc20Abi, getAddress, isAddress, toFunctionSelector, zeroAddress, } from "viem";
|
|
2
|
+
import { jb721TiersHookAbi, jb721TiersHookDeployerAbi, jb721TiersHookProjectDeployerAbi, jbBuybackHookAbi, jbBuybackHookRegistryAbi, jbControllerAbi, jbMultiTerminalAbi, jbPermissionsAbi, jbOmnichainDeployerAbi, jbTokensAbi, jbProjectsAbi, jbRouterTerminalRegistryAbi, jbSuckerRegistryAbi, revDeployerAbi, revLoansAbi, revOwnerAbi, } from "../generated/juicebox.js";
|
|
3
|
+
import { NATIVE_TOKEN } from "../constants.js";
|
|
4
|
+
import { uniswapV4PoolId } from "./uniswapV4.js";
|
|
5
|
+
import { jbSuckerV6Abi } from "./suckers.js";
|
|
6
|
+
import { v6Address, v6Deployments } from "./types.js";
|
|
7
|
+
const PAY_SELECTOR = /* @__PURE__ */ toFunctionSelector("pay(uint256,address,uint256,address,uint256,string,bytes)");
|
|
8
|
+
const same = (left, right) => typeof left === "string" &&
|
|
9
|
+
typeof right === "string" &&
|
|
10
|
+
left.toLowerCase() === right.toLowerCase();
|
|
11
|
+
const jsonSafe = (value) => JSON.parse(JSON.stringify(value, (_key, item) => typeof item === "bigint" ? item.toString() : item));
|
|
12
|
+
function canonicalJson(value) {
|
|
13
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
14
|
+
return JSON.stringify(value);
|
|
15
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
16
|
+
return JSON.stringify(value);
|
|
17
|
+
if (typeof value === "bigint")
|
|
18
|
+
return JSON.stringify(value.toString());
|
|
19
|
+
if (Array.isArray(value))
|
|
20
|
+
return "[" + value.map(canonicalJson).join(",") + "]";
|
|
21
|
+
if (typeof value === "object" && value !== null)
|
|
22
|
+
return ("{" +
|
|
23
|
+
Object.entries(value)
|
|
24
|
+
.filter(([, v]) => v !== undefined)
|
|
25
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
26
|
+
.map(([k, v]) => JSON.stringify(k) + ":" + canonicalJson(v))
|
|
27
|
+
.join(",") +
|
|
28
|
+
"}");
|
|
29
|
+
throw new Error("Value cannot be represented as canonical JSON.");
|
|
30
|
+
}
|
|
31
|
+
const equalData = (left, right) => canonicalJson(left) === canonicalJson(right);
|
|
32
|
+
const suckerReceiptAbi = [
|
|
33
|
+
{
|
|
34
|
+
type: "event",
|
|
35
|
+
name: "Claimed",
|
|
36
|
+
inputs: [
|
|
37
|
+
{
|
|
38
|
+
name: "beneficiary",
|
|
39
|
+
type: "bytes32",
|
|
40
|
+
indexed: false,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "token",
|
|
44
|
+
type: "address",
|
|
45
|
+
indexed: false,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "projectTokenCount",
|
|
49
|
+
type: "uint256",
|
|
50
|
+
indexed: false,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "terminalTokenAmount",
|
|
54
|
+
type: "uint256",
|
|
55
|
+
indexed: false,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "index",
|
|
59
|
+
type: "uint256",
|
|
60
|
+
indexed: false,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "metadata",
|
|
64
|
+
type: "bytes32",
|
|
65
|
+
indexed: false,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "caller",
|
|
69
|
+
type: "address",
|
|
70
|
+
indexed: false,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
anonymous: false,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: "event",
|
|
77
|
+
name: "AccountingDataSynced",
|
|
78
|
+
inputs: [
|
|
79
|
+
{
|
|
80
|
+
name: "sourceTimestamp",
|
|
81
|
+
type: "uint256",
|
|
82
|
+
indexed: false,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "caller",
|
|
86
|
+
type: "address",
|
|
87
|
+
indexed: false,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
anonymous: false,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
/** The producer's strict pool-key schema, without a service/schema dependency. */
|
|
94
|
+
function reviewedPoolKey(input) {
|
|
95
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
96
|
+
return undefined;
|
|
97
|
+
const key = input;
|
|
98
|
+
const fields = ["currency0", "currency1", "fee", "tickSpacing", "hooks"];
|
|
99
|
+
if (Object.keys(key).some((field) => !fields.includes(field)))
|
|
100
|
+
return undefined;
|
|
101
|
+
if (typeof key.fee !== "number" ||
|
|
102
|
+
!Number.isInteger(key.fee) ||
|
|
103
|
+
key.fee < 0 ||
|
|
104
|
+
key.fee > 16777215 ||
|
|
105
|
+
typeof key.tickSpacing !== "number" ||
|
|
106
|
+
!Number.isInteger(key.tickSpacing) ||
|
|
107
|
+
key.tickSpacing < -8388608 ||
|
|
108
|
+
key.tickSpacing > 8388607)
|
|
109
|
+
return undefined;
|
|
110
|
+
if (typeof key.currency0 !== "string" ||
|
|
111
|
+
!isAddress(key.currency0, { strict: true }) ||
|
|
112
|
+
typeof key.currency1 !== "string" ||
|
|
113
|
+
!isAddress(key.currency1, { strict: true }) ||
|
|
114
|
+
typeof key.hooks !== "string" ||
|
|
115
|
+
!isAddress(key.hooks, { strict: true }))
|
|
116
|
+
return undefined;
|
|
117
|
+
return {
|
|
118
|
+
currency0: getAddress(key.currency0),
|
|
119
|
+
currency1: getAddress(key.currency1),
|
|
120
|
+
fee: key.fee,
|
|
121
|
+
tickSpacing: key.tickSpacing,
|
|
122
|
+
hooks: getAddress(key.hooks),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Interpret modeled V6 effects in a receipt. No RPC, signing, secrets or storage.
|
|
127
|
+
* The host must first validate the draft and independently prove exact execution,
|
|
128
|
+
* receipt identity and canonicality. A successful receipt alone is insufficient.
|
|
129
|
+
*/
|
|
130
|
+
export function verifyV6OperationEvidence(draft, step, receipt) {
|
|
131
|
+
if (!Number.isSafeInteger(step) || step < 0 || step >= draft.calls.length)
|
|
132
|
+
throw new Error("The semantic verification step is outside the validated plan.");
|
|
133
|
+
if (receipt.status !== "success")
|
|
134
|
+
return {
|
|
135
|
+
verified: false,
|
|
136
|
+
events: [],
|
|
137
|
+
reason: "A reverted or unrecognized receipt cannot establish the planned outcome.",
|
|
138
|
+
};
|
|
139
|
+
return operationEvidence({ draft }, draft.calls[step], receipt);
|
|
140
|
+
}
|
|
141
|
+
function operationEvidence(plan, call, receipt) {
|
|
142
|
+
const events = [];
|
|
143
|
+
const sourceLogs = receipt.logs.filter((log) => !log.removed &&
|
|
144
|
+
same(log.transactionHash, receipt.transactionHash) &&
|
|
145
|
+
same(log.blockHash, receipt.blockHash) &&
|
|
146
|
+
log.blockNumber === receipt.blockNumber &&
|
|
147
|
+
log.logIndex !== null);
|
|
148
|
+
const append = (log, event, args) => events.push({
|
|
149
|
+
emitter: log.address,
|
|
150
|
+
event,
|
|
151
|
+
logIndex: log.logIndex,
|
|
152
|
+
args: jsonSafe(args),
|
|
153
|
+
});
|
|
154
|
+
const unknown = (reason) => ({ verified: false, events, reason });
|
|
155
|
+
try {
|
|
156
|
+
// Canonical deployments matter: arbitrary contracts can copy a Juicebox event signature.
|
|
157
|
+
const terminalEmitter = v6Address("JBMultiTerminal", call.chainId);
|
|
158
|
+
const directTerminal = same(call.to, terminalEmitter);
|
|
159
|
+
const routers = v6Deployments("JBRouterTerminal", call.chainId);
|
|
160
|
+
const gateways = v6Deployments("JBRouterTerminalGateway", call.chainId);
|
|
161
|
+
const buybacks = v6Deployments("JBBuybackHook", call.chainId);
|
|
162
|
+
const routerRegistry = v6Address("JBRouterTerminalRegistry", call.chainId);
|
|
163
|
+
if (directTerminal ||
|
|
164
|
+
([...routers, ...gateways, routerRegistry].some((address) => same(call.to, address)) &&
|
|
165
|
+
same(call.data.slice(0, 10), PAY_SELECTOR))) {
|
|
166
|
+
const decoded = decodeFunctionData({
|
|
167
|
+
abi: jbMultiTerminalAbi,
|
|
168
|
+
data: call.data,
|
|
169
|
+
});
|
|
170
|
+
const args = decoded.args;
|
|
171
|
+
const projectId = decoded.functionName === "cashOutTokensOf" ? args[1] : args[0];
|
|
172
|
+
if (!plan.draft.project ||
|
|
173
|
+
plan.draft.project.chainId !== call.chainId ||
|
|
174
|
+
projectId !== BigInt(plan.draft.project.projectId))
|
|
175
|
+
return unknown("The call does not match the signed project identity.");
|
|
176
|
+
const eventName = decoded.functionName === "pay"
|
|
177
|
+
? "Pay"
|
|
178
|
+
: decoded.functionName === "cashOutTokensOf"
|
|
179
|
+
? "CashOutTokens"
|
|
180
|
+
: decoded.functionName === "sendPayoutsOf"
|
|
181
|
+
? "SendPayouts"
|
|
182
|
+
: undefined;
|
|
183
|
+
if (!eventName)
|
|
184
|
+
return unknown("Semantic verification is not implemented for this terminal function.");
|
|
185
|
+
let payoutFailed = false;
|
|
186
|
+
for (const log of sourceLogs) {
|
|
187
|
+
if (!same(log.address, terminalEmitter) || log.removed)
|
|
188
|
+
continue;
|
|
189
|
+
try {
|
|
190
|
+
const event = decodeEventLog({
|
|
191
|
+
abi: jbMultiTerminalAbi,
|
|
192
|
+
data: log.data,
|
|
193
|
+
topics: log.topics,
|
|
194
|
+
strict: true,
|
|
195
|
+
});
|
|
196
|
+
const fields = event.args;
|
|
197
|
+
if (fields.projectId !== projectId)
|
|
198
|
+
continue;
|
|
199
|
+
if (eventName === "SendPayouts" &&
|
|
200
|
+
["PayoutReverted", "PayoutTransferReverted"].includes(event.eventName)) {
|
|
201
|
+
payoutFailed = true;
|
|
202
|
+
append(log, event.eventName, fields);
|
|
203
|
+
}
|
|
204
|
+
const callerMatches = directTerminal
|
|
205
|
+
? same(fields.caller, plan.draft.account)
|
|
206
|
+
: [...routers, routerRegistry].some((address) => same(fields.caller, address));
|
|
207
|
+
if (event.eventName !== eventName || !callerMatches)
|
|
208
|
+
continue;
|
|
209
|
+
// Pay.newlyIssuedTokenCount is newly minted tokens only. The exact successful call
|
|
210
|
+
// enforces minReturnedTokens against the beneficiary's complete balance delta, including hooks.
|
|
211
|
+
if (eventName === "Pay" &&
|
|
212
|
+
(!same(fields.payer, directTerminal ? plan.draft.account : fields.caller) ||
|
|
213
|
+
!same(fields.beneficiary, args[3])))
|
|
214
|
+
continue;
|
|
215
|
+
if (eventName === "CashOutTokens" &&
|
|
216
|
+
(!same(fields.holder, args[0]) ||
|
|
217
|
+
!same(fields.beneficiary, args[5]) ||
|
|
218
|
+
fields.cashOutCount !== args[2] ||
|
|
219
|
+
typeof fields.reclaimAmount !== "bigint" ||
|
|
220
|
+
fields.reclaimAmount < args[4]))
|
|
221
|
+
continue;
|
|
222
|
+
append(log, event.eventName, fields);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
/* Unrelated or malformed events are not outcome evidence. */
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (payoutFailed)
|
|
229
|
+
return unknown("The transaction confirmed, but at least one payout transfer or hook reverted. Inspect the payout events for partial completion.");
|
|
230
|
+
let sellFailed = false;
|
|
231
|
+
if (eventName === "Pay" || eventName === "CashOutTokens") {
|
|
232
|
+
for (const log of sourceLogs) {
|
|
233
|
+
if (!buybacks.some((address) => same(log.address, address)))
|
|
234
|
+
continue;
|
|
235
|
+
try {
|
|
236
|
+
const event = decodeEventLog({
|
|
237
|
+
abi: jbBuybackHookAbi,
|
|
238
|
+
data: log.data,
|
|
239
|
+
topics: log.topics,
|
|
240
|
+
strict: true,
|
|
241
|
+
});
|
|
242
|
+
const fields = event.args;
|
|
243
|
+
if (fields.projectId !== projectId ||
|
|
244
|
+
!same(fields.caller, terminalEmitter))
|
|
245
|
+
continue;
|
|
246
|
+
if (eventName === "Pay" &&
|
|
247
|
+
["Swap", "Mint"].includes(event.eventName))
|
|
248
|
+
append(log, event.eventName, fields);
|
|
249
|
+
if (eventName === "CashOutTokens" &&
|
|
250
|
+
["CashOutSwap", "SellSwapReverted"].includes(event.eventName)) {
|
|
251
|
+
append(log, event.eventName, fields);
|
|
252
|
+
if (event.eventName === "SellSwapReverted")
|
|
253
|
+
sellFailed = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
/* Hook legs are separate observed amounts; never add them to minted amounts blindly. */
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (sellFailed)
|
|
262
|
+
return unknown("The exact transaction confirmed, but the sell-side swap failed and project tokens were returned. The requested terminal-token cash-out outcome is not verified.");
|
|
263
|
+
if (eventName === "Pay" &&
|
|
264
|
+
plan.draft.operation === "721_pay" &&
|
|
265
|
+
events.some((event) => event.event === "Pay")) {
|
|
266
|
+
const nft = nftPaymentEvidence(plan, sourceLogs, terminalEmitter);
|
|
267
|
+
return { ...nft, events: [...events, ...nft.events] };
|
|
268
|
+
}
|
|
269
|
+
return events.some((event) => event.event === eventName)
|
|
270
|
+
? { verified: true, events }
|
|
271
|
+
: unknown("The expected canonical project event is absent or does not match the planned beneficiary, holder, or minimum output.");
|
|
272
|
+
}
|
|
273
|
+
if (same(call.to, v6Address("JBController", call.chainId))) {
|
|
274
|
+
const decoded = decodeFunctionData({
|
|
275
|
+
abi: jbControllerAbi,
|
|
276
|
+
data: call.data,
|
|
277
|
+
});
|
|
278
|
+
if (decoded.functionName === "queueRulesetsOf") {
|
|
279
|
+
const [projectId, , memo] = decoded.args;
|
|
280
|
+
if (!plan.draft.project ||
|
|
281
|
+
plan.draft.project.chainId !== call.chainId ||
|
|
282
|
+
projectId !== BigInt(plan.draft.project.projectId))
|
|
283
|
+
return unknown("The queued rulesets do not match the signed project identity.");
|
|
284
|
+
for (const log of sourceLogs) {
|
|
285
|
+
if (!same(log.address, call.to))
|
|
286
|
+
continue;
|
|
287
|
+
try {
|
|
288
|
+
const event = decodeEventLog({
|
|
289
|
+
abi: jbControllerAbi,
|
|
290
|
+
data: log.data,
|
|
291
|
+
topics: log.topics,
|
|
292
|
+
strict: true,
|
|
293
|
+
});
|
|
294
|
+
if (event.eventName === "QueueRulesets" &&
|
|
295
|
+
event.args.projectId === projectId &&
|
|
296
|
+
same(event.args.caller, plan.draft.account) &&
|
|
297
|
+
event.args.memo === memo)
|
|
298
|
+
append(log, event.eventName, event.args);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
/* Only a canonical queue event proves that rulesets were queued. */
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return events.length
|
|
305
|
+
? {
|
|
306
|
+
verified: true,
|
|
307
|
+
events,
|
|
308
|
+
reason: "Rulesets were queued. This does not assert approval or activation; inspect their current status and effective dates separately.",
|
|
309
|
+
}
|
|
310
|
+
: unknown("The canonical QueueRulesets event is absent or does not match the project/caller/memo.");
|
|
311
|
+
}
|
|
312
|
+
if (decoded.functionName === "deployERC20For" ||
|
|
313
|
+
decoded.functionName === "setUriOf")
|
|
314
|
+
return extensionEvidence(plan, call, sourceLogs);
|
|
315
|
+
if (decoded.functionName !== "launchProjectFor")
|
|
316
|
+
return unknown("Semantic verification is not implemented for this controller function.");
|
|
317
|
+
const [owner, projectUri] = decoded.args;
|
|
318
|
+
const creates = new Set();
|
|
319
|
+
const launches = new Set();
|
|
320
|
+
for (const log of sourceLogs) {
|
|
321
|
+
if (log.removed)
|
|
322
|
+
continue;
|
|
323
|
+
try {
|
|
324
|
+
if (same(log.address, v6Address("JBProjects", call.chainId))) {
|
|
325
|
+
const event = decodeEventLog({
|
|
326
|
+
abi: jbProjectsAbi,
|
|
327
|
+
eventName: "Create",
|
|
328
|
+
data: log.data,
|
|
329
|
+
topics: log.topics,
|
|
330
|
+
strict: true,
|
|
331
|
+
});
|
|
332
|
+
if (event.eventName !== "Create" ||
|
|
333
|
+
!same(event.args.owner, owner) ||
|
|
334
|
+
!same(event.args.caller, call.to))
|
|
335
|
+
continue;
|
|
336
|
+
creates.add(event.args.projectId.toString());
|
|
337
|
+
append(log, event.eventName, event.args);
|
|
338
|
+
}
|
|
339
|
+
else if (same(log.address, call.to)) {
|
|
340
|
+
const event = decodeEventLog({
|
|
341
|
+
abi: jbControllerAbi,
|
|
342
|
+
eventName: "LaunchProject",
|
|
343
|
+
data: log.data,
|
|
344
|
+
topics: log.topics,
|
|
345
|
+
strict: true,
|
|
346
|
+
});
|
|
347
|
+
if (event.eventName !== "LaunchProject" ||
|
|
348
|
+
!same(event.args.caller, plan.draft.account) ||
|
|
349
|
+
event.args.projectUri !== projectUri)
|
|
350
|
+
continue;
|
|
351
|
+
launches.add(event.args.projectId.toString());
|
|
352
|
+
append(log, event.eventName, event.args);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
/* Require both canonical emitter events for the same created ID. */
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return [...creates].some((id) => launches.has(id))
|
|
360
|
+
? { verified: true, events }
|
|
361
|
+
: unknown("A launch requires matching JBProjects.Create and JBController.LaunchProject logs from canonical deployments, with the requested owner and URI.");
|
|
362
|
+
}
|
|
363
|
+
const extension = extensionEvidence(plan, call, sourceLogs);
|
|
364
|
+
if (extension)
|
|
365
|
+
return extension;
|
|
366
|
+
const approval = decodeFunctionData({ abi: erc20Abi, data: call.data });
|
|
367
|
+
if (approval.functionName === "approve") {
|
|
368
|
+
for (const log of sourceLogs) {
|
|
369
|
+
if (!same(log.address, call.to) || log.removed)
|
|
370
|
+
continue;
|
|
371
|
+
try {
|
|
372
|
+
const event = decodeEventLog({
|
|
373
|
+
abi: erc20Abi,
|
|
374
|
+
eventName: "Approval",
|
|
375
|
+
data: log.data,
|
|
376
|
+
topics: log.topics,
|
|
377
|
+
strict: true,
|
|
378
|
+
});
|
|
379
|
+
if (event.eventName === "Approval" &&
|
|
380
|
+
same(event.args.owner, plan.draft.account) &&
|
|
381
|
+
same(event.args.spender, approval.args[0]) &&
|
|
382
|
+
event.args.value === approval.args[1])
|
|
383
|
+
append(log, event.eventName, event.args);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
/* Tokens without matching Approval logs remain semantically unverified. */
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return events.length > 0
|
|
390
|
+
? {
|
|
391
|
+
verified: true,
|
|
392
|
+
events,
|
|
393
|
+
reason: "The token emitted the exact Approval event. Re-simulate the dependent call against current chain state; this event does not prove a persistent allowance.",
|
|
394
|
+
}
|
|
395
|
+
: unknown("No exact Approval event was found; use the next step’s real-state simulation to check allowance.");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
return unknown("The confirmed transaction uses calldata or a deployment without a supported semantic verifier.");
|
|
400
|
+
}
|
|
401
|
+
return unknown("The direct transaction confirmed, but semantic outcome verification is not implemented for this operation.");
|
|
402
|
+
}
|
|
403
|
+
function extensionEvidence(plan, call, logs) {
|
|
404
|
+
const events = [];
|
|
405
|
+
const projectMatches = (projectId) => !!plan.draft.project &&
|
|
406
|
+
plan.draft.project.chainId === call.chainId &&
|
|
407
|
+
projectId === BigInt(plan.draft.project.projectId);
|
|
408
|
+
const matching = (abi, eventName, emitter, predicate) => {
|
|
409
|
+
const matches = [];
|
|
410
|
+
for (const log of logs) {
|
|
411
|
+
if (!same(log.address, emitter))
|
|
412
|
+
continue;
|
|
413
|
+
try {
|
|
414
|
+
const event = decodeEventLog({
|
|
415
|
+
abi,
|
|
416
|
+
data: log.data,
|
|
417
|
+
topics: log.topics,
|
|
418
|
+
strict: true,
|
|
419
|
+
});
|
|
420
|
+
const args = event.args;
|
|
421
|
+
// `eventName` is only a TypeScript inference hint to viem, never a runtime filter.
|
|
422
|
+
if (event.eventName === eventName && predicate(args))
|
|
423
|
+
matches.push({
|
|
424
|
+
emitter: log.address,
|
|
425
|
+
event: eventName,
|
|
426
|
+
logIndex: log.logIndex,
|
|
427
|
+
args: jsonSafe(args),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
/* No outcome evidence from malformed or unrelated logs. */
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
events.push(...matches);
|
|
435
|
+
return matches;
|
|
436
|
+
};
|
|
437
|
+
const done = (verified, reason) => ({
|
|
438
|
+
verified,
|
|
439
|
+
events,
|
|
440
|
+
reason: reason ??
|
|
441
|
+
(verified
|
|
442
|
+
? undefined
|
|
443
|
+
: "Canonical operation events are absent or do not exactly match the signed call."),
|
|
444
|
+
});
|
|
445
|
+
const fromCaller = (args) => same(args.caller, plan.draft.account);
|
|
446
|
+
if (same(call.to, v6Address("JBController", call.chainId))) {
|
|
447
|
+
const decoded = decodeFunctionData({
|
|
448
|
+
abi: jbControllerAbi,
|
|
449
|
+
data: call.data,
|
|
450
|
+
});
|
|
451
|
+
if (decoded.functionName === "deployERC20For") {
|
|
452
|
+
const [projectId, name, symbol, salt] = decoded.args;
|
|
453
|
+
const deployed = projectMatches(projectId) &&
|
|
454
|
+
matching(jbControllerAbi, "DeployERC20", call.to, (args) => fromCaller(args) &&
|
|
455
|
+
args.projectId === projectId &&
|
|
456
|
+
args.salt === salt).length === 1 &&
|
|
457
|
+
matching(jbTokensAbi, "DeployERC20", v6Address("JBTokens", call.chainId), (args) => args.projectId === projectId &&
|
|
458
|
+
args.name === name &&
|
|
459
|
+
args.symbol === symbol &&
|
|
460
|
+
same(args.caller, call.to)).length === 1;
|
|
461
|
+
return done(deployed, deployed ? "The project ERC-20 was deployed." : undefined);
|
|
462
|
+
}
|
|
463
|
+
if (decoded.functionName === "setUriOf") {
|
|
464
|
+
const [projectId, uri] = decoded.args;
|
|
465
|
+
return done(projectMatches(projectId) &&
|
|
466
|
+
matching(jbControllerAbi, "SetUri", call.to, (args) => fromCaller(args) &&
|
|
467
|
+
args.projectId === projectId &&
|
|
468
|
+
args.uri === uri).length === 1);
|
|
469
|
+
}
|
|
470
|
+
return done(false);
|
|
471
|
+
}
|
|
472
|
+
if (same(call.to, v6Address("JBOmnichainDeployer", call.chainId))) {
|
|
473
|
+
const decoded = decodeFunctionData({
|
|
474
|
+
abi: jbOmnichainDeployerAbi,
|
|
475
|
+
data: call.data,
|
|
476
|
+
});
|
|
477
|
+
if (decoded.functionName === "queueRulesetsOf") {
|
|
478
|
+
const args = decoded.args;
|
|
479
|
+
const projectId = args[0];
|
|
480
|
+
const memo = args[args.length - 1];
|
|
481
|
+
const queued = projectMatches(projectId) &&
|
|
482
|
+
matching(jbControllerAbi, "QueueRulesets", v6Address("JBController", call.chainId), (event) => event.projectId === projectId &&
|
|
483
|
+
same(event.caller, call.to) &&
|
|
484
|
+
event.memo === memo).length === 1;
|
|
485
|
+
return done(queued, queued
|
|
486
|
+
? "Rulesets were queued through JBOmnichainDeployer. This does not assert approval or activation; inspect their current status and effective dates separately."
|
|
487
|
+
: "The canonical QueueRulesets event from JBController with JBOmnichainDeployer as caller is absent or does not match the project/memo.");
|
|
488
|
+
}
|
|
489
|
+
if (decoded.functionName !== "launchProjectFor")
|
|
490
|
+
return done(false);
|
|
491
|
+
const args = decoded.args;
|
|
492
|
+
const owner = args[0];
|
|
493
|
+
const projectUri = args[1];
|
|
494
|
+
const memo = args[args.length - 2];
|
|
495
|
+
const expectedSuckers = args[args.length - 1].deployerConfigurations;
|
|
496
|
+
const created = matching(jbProjectsAbi, "Create", v6Address("JBProjects", call.chainId), (event) => same(event.owner, call.to) && same(event.caller, call.to));
|
|
497
|
+
const ids = new Set(created.map((event) => event.args.projectId));
|
|
498
|
+
const hooks = matching(jb721TiersHookDeployerAbi, "HookDeployed", v6Address("JB721TiersHookDeployer", call.chainId), (event) => ids.has(String(event.projectId)) && same(event.caller, call.to));
|
|
499
|
+
const rulesets = matching(jbControllerAbi, "LaunchRulesets", v6Address("JBController", call.chainId), (event) => ids.has(String(event.projectId)) &&
|
|
500
|
+
same(event.caller, call.to) &&
|
|
501
|
+
event.projectUri === projectUri &&
|
|
502
|
+
event.memo === memo);
|
|
503
|
+
const suckers = matching(jbSuckerRegistryAbi, "SuckerDeployedFor", v6Address("JBSuckerRegistry", call.chainId), (event) => ids.has(String(event.projectId)) && same(event.caller, call.to));
|
|
504
|
+
const ownership = matching(jbProjectsAbi, "Transfer", v6Address("JBProjects", call.chainId), (event) => ids.has(String(event.tokenId)) &&
|
|
505
|
+
same(event.from, call.to) &&
|
|
506
|
+
same(event.to, owner));
|
|
507
|
+
const actual = suckers
|
|
508
|
+
.map((event) => canonicalJson(event.args.configuration))
|
|
509
|
+
.sort();
|
|
510
|
+
const expected = expectedSuckers
|
|
511
|
+
.map((item) => canonicalJson(jsonSafe(item)))
|
|
512
|
+
.sort();
|
|
513
|
+
const verified = created.length === 1 &&
|
|
514
|
+
hooks.length === 1 &&
|
|
515
|
+
rulesets.length === 1 &&
|
|
516
|
+
ownership.length === 1 &&
|
|
517
|
+
equalData(actual, expected);
|
|
518
|
+
return done(verified, verified
|
|
519
|
+
? "The project, its NFT collection, its rulesets and the requested local suckers were created on this chain, and the owner received the project. The remote chains need their own verification."
|
|
520
|
+
: "Project creation, NFT collection, rulesets, local suckers or the owner transfer were not all verified.");
|
|
521
|
+
}
|
|
522
|
+
if (same(call.to, v6Address("JBPermissions", call.chainId))) {
|
|
523
|
+
const decoded = decodeFunctionData({
|
|
524
|
+
abi: jbPermissionsAbi,
|
|
525
|
+
data: call.data,
|
|
526
|
+
});
|
|
527
|
+
if (decoded.functionName !== "setPermissionsFor")
|
|
528
|
+
return done(false);
|
|
529
|
+
const [account, permission] = decoded.args;
|
|
530
|
+
const packed = permission.permissionIds.reduce((bits, id) => bits | (1n << BigInt(id)), 0n);
|
|
531
|
+
return done(matching(jbPermissionsAbi, "OperatorPermissionsSet", call.to, (args) => fromCaller(args) &&
|
|
532
|
+
same(args.account, account) &&
|
|
533
|
+
same(args.operator, permission.operator) &&
|
|
534
|
+
args.projectId === permission.projectId &&
|
|
535
|
+
equalData(args.permissionIds, permission.permissionIds) &&
|
|
536
|
+
args.packed === packed).length > 0);
|
|
537
|
+
}
|
|
538
|
+
if (same(call.to, v6Address("REVLoans", call.chainId))) {
|
|
539
|
+
const decoded = decodeFunctionData({ abi: revLoansAbi, data: call.data });
|
|
540
|
+
if (decoded.functionName === "borrowFrom") {
|
|
541
|
+
const [projectId, token, minimumBorrow, collateral, beneficiary, prepaidFee, holder,] = decoded.args;
|
|
542
|
+
if (!projectMatches(projectId))
|
|
543
|
+
return done(false);
|
|
544
|
+
const borrows = matching(revLoansAbi, "Borrow", call.to, (args) => {
|
|
545
|
+
const loan = args.loan;
|
|
546
|
+
return (fromCaller(args) &&
|
|
547
|
+
args.revnetId === projectId &&
|
|
548
|
+
same(args.token, token) &&
|
|
549
|
+
args.collateralCount === collateral &&
|
|
550
|
+
same(args.beneficiary, beneficiary) &&
|
|
551
|
+
typeof args.borrowAmount === "bigint" &&
|
|
552
|
+
args.borrowAmount >= minimumBorrow &&
|
|
553
|
+
loan.collateral === collateral &&
|
|
554
|
+
BigInt(loan.prepaidFeePercent) === prepaidFee &&
|
|
555
|
+
same(loan.sourceToken, token));
|
|
556
|
+
});
|
|
557
|
+
const ids = new Set(borrows.map((event) => event.args.loanId));
|
|
558
|
+
const ownership = matching(revLoansAbi, "Transfer", call.to, (args) => same(args.from, zeroAddress) &&
|
|
559
|
+
same(args.to, holder) &&
|
|
560
|
+
ids.has(String(args.tokenId)));
|
|
561
|
+
return done(borrows.length > 0 && ownership.length > 0);
|
|
562
|
+
}
|
|
563
|
+
if (decoded.functionName === "repayLoan") {
|
|
564
|
+
const [loanId, maximumRepay, collateralReturned, beneficiary] = decoded.args;
|
|
565
|
+
return done(matching(revLoansAbi, "RepayLoan", call.to, (args) => {
|
|
566
|
+
const loan = args.loan;
|
|
567
|
+
const paidOff = args.paidOffLoan;
|
|
568
|
+
// The contract returns all collateral if the remaining debt rounds down to zero.
|
|
569
|
+
const returned = args.collateralCountToReturn === collateralReturned ||
|
|
570
|
+
(paidOff.amount === 0n &&
|
|
571
|
+
args.collateralCountToReturn === loan.collateral);
|
|
572
|
+
return (fromCaller(args) &&
|
|
573
|
+
args.loanId === loanId &&
|
|
574
|
+
projectMatches(args.revnetId) &&
|
|
575
|
+
returned &&
|
|
576
|
+
same(args.beneficiary, beneficiary) &&
|
|
577
|
+
typeof args.repayBorrowAmount === "bigint" &&
|
|
578
|
+
args.repayBorrowAmount <= maximumRepay);
|
|
579
|
+
}).length > 0);
|
|
580
|
+
}
|
|
581
|
+
return done(false);
|
|
582
|
+
}
|
|
583
|
+
if (same(call.to, v6Address("REVOwner", call.chainId))) {
|
|
584
|
+
const decoded = decodeFunctionData({ abi: revOwnerAbi, data: call.data });
|
|
585
|
+
if (decoded.functionName !== "autoIssueFor")
|
|
586
|
+
return done(false);
|
|
587
|
+
const [projectId, stageId, beneficiary] = decoded.args;
|
|
588
|
+
return done(projectMatches(projectId) &&
|
|
589
|
+
matching(revOwnerAbi, "AutoIssue", call.to, (args) => fromCaller(args) &&
|
|
590
|
+
args.revnetId === projectId &&
|
|
591
|
+
args.stageId === stageId &&
|
|
592
|
+
same(args.beneficiary, beneficiary) &&
|
|
593
|
+
typeof args.count === "bigint" &&
|
|
594
|
+
args.count > 0n).length > 0);
|
|
595
|
+
}
|
|
596
|
+
if (same(call.to, v6Address("JB721TiersHookProjectDeployer", call.chainId))) {
|
|
597
|
+
const decoded = decodeFunctionData({
|
|
598
|
+
abi: jb721TiersHookProjectDeployerAbi,
|
|
599
|
+
data: call.data,
|
|
600
|
+
});
|
|
601
|
+
if (decoded.functionName !== "launchProjectFor")
|
|
602
|
+
return done(false);
|
|
603
|
+
const [owner, , configuration, controller] = decoded.args;
|
|
604
|
+
if (!same(controller, v6Address("JBController", call.chainId)))
|
|
605
|
+
return done(false, "Only canonical-controller NFT project launches have a supported verifier.");
|
|
606
|
+
const created = matching(jbProjectsAbi, "Create", v6Address("JBProjects", call.chainId), (args) => same(args.owner, call.to) && same(args.caller, call.to));
|
|
607
|
+
const ids = new Set(created.map((event) => event.args.projectId));
|
|
608
|
+
const hooks = matching(jb721TiersHookDeployerAbi, "HookDeployed", v6Address("JB721TiersHookDeployer", call.chainId), (args) => ids.has(String(args.projectId)) &&
|
|
609
|
+
same(args.caller, call.to) &&
|
|
610
|
+
typeof args.hook === "string" &&
|
|
611
|
+
!same(args.hook, zeroAddress));
|
|
612
|
+
const rulesets = matching(jbControllerAbi, "LaunchRulesets", controller, (args) => ids.has(String(args.projectId)) &&
|
|
613
|
+
same(args.caller, call.to) &&
|
|
614
|
+
args.projectUri === configuration.projectUri &&
|
|
615
|
+
args.memo === configuration.memo);
|
|
616
|
+
const ownership = matching(jbProjectsAbi, "Transfer", v6Address("JBProjects", call.chainId), (args) => ids.has(String(args.tokenId)) &&
|
|
617
|
+
same(args.from, call.to) &&
|
|
618
|
+
same(args.to, owner));
|
|
619
|
+
const verified = created.length === 1 &&
|
|
620
|
+
hooks.length === 1 &&
|
|
621
|
+
rulesets.length === 1 &&
|
|
622
|
+
ownership.length === 1;
|
|
623
|
+
return done(verified, verified
|
|
624
|
+
? "The project and its NFT hook were created on this chain, initial rulesets were launched, and the project NFT was delivered to the requested owner."
|
|
625
|
+
: "The canonical project creation, hook association, launched rulesets and requested ownership transfer were not all verified.");
|
|
626
|
+
}
|
|
627
|
+
if (same(call.to, v6Address("REVDeployer", call.chainId))) {
|
|
628
|
+
const decoded = decodeFunctionData({
|
|
629
|
+
abi: revDeployerAbi,
|
|
630
|
+
data: call.data,
|
|
631
|
+
});
|
|
632
|
+
if (decoded.functionName !== "deployFor" || decoded.args[0] !== 0n)
|
|
633
|
+
return done(false, "Only new canonical revnet deployment receipts have a supported deployment verifier.");
|
|
634
|
+
const [, configuration, , suckerConfiguration] = decoded.args;
|
|
635
|
+
const deployments = matching(revDeployerAbi, "DeployRevnet", call.to, (args) => fromCaller(args) &&
|
|
636
|
+
equalData(args.configuration, configuration) &&
|
|
637
|
+
equalData(args.suckerDeploymentConfiguration, suckerConfiguration));
|
|
638
|
+
const ids = new Set(deployments.map((event) => event.args.revnetId));
|
|
639
|
+
const created = matching(jbProjectsAbi, "Create", v6Address("JBProjects", call.chainId), (args) => ids.has(String(args.projectId)) &&
|
|
640
|
+
same(args.owner, call.to) &&
|
|
641
|
+
same(args.caller, call.to));
|
|
642
|
+
const initialized = matching(revOwnerAbi, "InitializeRevnet", v6Address("REVOwner", call.chainId), (args) => ids.has(String(args.revnetId)) && same(args.caller, call.to));
|
|
643
|
+
const ownership = matching(jbProjectsAbi, "Transfer", v6Address("JBProjects", call.chainId), (args) => ids.has(String(args.tokenId)) &&
|
|
644
|
+
same(args.from, call.to) &&
|
|
645
|
+
same(args.to, v6Address("REVOwner", call.chainId)));
|
|
646
|
+
const expectedSuckers = suckerConfiguration.deployerConfigurations;
|
|
647
|
+
const suckers = matching(jbSuckerRegistryAbi, "SuckerDeployedFor", v6Address("JBSuckerRegistry", call.chainId), (args) => ids.has(String(args.projectId)) &&
|
|
648
|
+
same(args.caller, call.to) &&
|
|
649
|
+
expectedSuckers.some((configuration) => equalData(args.configuration, configuration)));
|
|
650
|
+
const actualConfigurations = suckers
|
|
651
|
+
.map((event) => canonicalJson(event.args.configuration))
|
|
652
|
+
.sort();
|
|
653
|
+
const expectedConfigurations = expectedSuckers
|
|
654
|
+
.map((configuration) => canonicalJson(jsonSafe(configuration)))
|
|
655
|
+
.sort();
|
|
656
|
+
const verified = deployments.length === 1 &&
|
|
657
|
+
created.length === 1 &&
|
|
658
|
+
initialized.length === 1 &&
|
|
659
|
+
ownership.length === 1 &&
|
|
660
|
+
equalData(actualConfigurations, expectedConfigurations);
|
|
661
|
+
return done(verified, verified
|
|
662
|
+
? "The revnet, project ownership, and requested local suckers were created. Peer deployments, transport delivery, and cross-chain connectivity require separate verification."
|
|
663
|
+
: "Required revnet creation, ownership, initialization, or requested local sucker-deployment events are missing.");
|
|
664
|
+
}
|
|
665
|
+
if (["bridge_claim", "sync_accounting"].includes(plan.draft.operation)) {
|
|
666
|
+
// Plan producers prove canonical-registry membership + projectId before sealing a sucker call.
|
|
667
|
+
// Restrict this instance-address verifier to those producer operations and the signed project chain.
|
|
668
|
+
if (!plan.draft.project || plan.draft.project.chainId !== call.chainId)
|
|
669
|
+
return done(false);
|
|
670
|
+
const decoded = decodeFunctionData({ abi: jbSuckerV6Abi, data: call.data });
|
|
671
|
+
if (plan.draft.operation === "bridge_claim" &&
|
|
672
|
+
decoded.functionName === "claim") {
|
|
673
|
+
const [claim] = decoded.args;
|
|
674
|
+
return done(matching(suckerReceiptAbi, "Claimed", call.to, (args) => fromCaller(args) &&
|
|
675
|
+
same(args.token, claim.token) &&
|
|
676
|
+
same(args.beneficiary, claim.leaf.beneficiary) &&
|
|
677
|
+
args.projectTokenCount === claim.leaf.projectTokenCount &&
|
|
678
|
+
args.terminalTokenAmount === claim.leaf.terminalTokenAmount &&
|
|
679
|
+
args.index === claim.leaf.index &&
|
|
680
|
+
same(args.metadata, claim.leaf.metadata)).length > 0);
|
|
681
|
+
}
|
|
682
|
+
if (plan.draft.operation === "sync_accounting" &&
|
|
683
|
+
decoded.functionName === "syncAccountingData") {
|
|
684
|
+
const synced = matching(suckerReceiptAbi, "AccountingDataSynced", call.to, (args) => fromCaller(args) &&
|
|
685
|
+
typeof args.sourceTimestamp === "bigint" &&
|
|
686
|
+
args.sourceTimestamp > 0n);
|
|
687
|
+
return done(synced.length > 0, synced.length > 0
|
|
688
|
+
? "The source sucker sent accounting data. Destination delivery and freshness are not verified by this source receipt."
|
|
689
|
+
: undefined);
|
|
690
|
+
}
|
|
691
|
+
return done(false);
|
|
692
|
+
}
|
|
693
|
+
const summary = plan.draft.summary;
|
|
694
|
+
if (summary?.operation === "721_adjust_tiers" &&
|
|
695
|
+
same(summary.hook, call.to)) {
|
|
696
|
+
// The producer authenticates this exact Solady clone, immutable implementation, store and project.
|
|
697
|
+
if (!plan.draft.project || !equalData(summary.project, plan.draft.project))
|
|
698
|
+
return done(false);
|
|
699
|
+
const decoded = decodeFunctionData({
|
|
700
|
+
abi: jb721TiersHookAbi,
|
|
701
|
+
data: call.data,
|
|
702
|
+
});
|
|
703
|
+
if (decoded.functionName !== "adjustTiers")
|
|
704
|
+
return done(false);
|
|
705
|
+
const [tiers, removals] = decoded.args;
|
|
706
|
+
const added = matching(jb721TiersHookAbi, "AddTier", call.to, (args) => fromCaller(args) && tiers.some((tier) => equalData(args.tier, tier)));
|
|
707
|
+
const removed = matching(jb721TiersHookAbi, "RemoveTier", call.to, (args) => fromCaller(args) && removals.includes(args.tierId));
|
|
708
|
+
const actualConfigurations = added
|
|
709
|
+
.map((event) => canonicalJson(event.args.tier))
|
|
710
|
+
.sort();
|
|
711
|
+
const expectedConfigurations = tiers
|
|
712
|
+
.map((tier) => canonicalJson(jsonSafe(tier)))
|
|
713
|
+
.sort();
|
|
714
|
+
return done(equalData(actualConfigurations, expectedConfigurations) &&
|
|
715
|
+
new Set(removed.map((event) => event.args.tierId)).size === removals.length);
|
|
716
|
+
}
|
|
717
|
+
if (same(call.to, v6Address("JBBuybackHookRegistry", call.chainId))) {
|
|
718
|
+
const decoded = decodeFunctionData({
|
|
719
|
+
abi: jbBuybackHookRegistryAbi,
|
|
720
|
+
data: call.data,
|
|
721
|
+
});
|
|
722
|
+
if (decoded.functionName !== "setHookFor")
|
|
723
|
+
return done(false);
|
|
724
|
+
const [projectId, hook] = decoded.args;
|
|
725
|
+
return done(projectMatches(projectId) &&
|
|
726
|
+
matching(jbBuybackHookRegistryAbi, "JBBuybackHookRegistry_SetHook", call.to, (args) => fromCaller(args) &&
|
|
727
|
+
args.projectId === projectId &&
|
|
728
|
+
same(args.hook, hook)).length > 0);
|
|
729
|
+
}
|
|
730
|
+
if (same(call.to, v6Address("JBRouterTerminalRegistry", call.chainId))) {
|
|
731
|
+
const decoded = decodeFunctionData({
|
|
732
|
+
abi: jbRouterTerminalRegistryAbi,
|
|
733
|
+
data: call.data,
|
|
734
|
+
});
|
|
735
|
+
if (decoded.functionName !== "setTerminalFor")
|
|
736
|
+
return done(false);
|
|
737
|
+
const [projectId, terminal] = decoded.args;
|
|
738
|
+
return done(projectMatches(projectId) &&
|
|
739
|
+
matching(jbRouterTerminalRegistryAbi, "JBRouterTerminalRegistry_SetTerminal", call.to, (args) => fromCaller(args) &&
|
|
740
|
+
args.projectId === projectId &&
|
|
741
|
+
same(args.terminal, terminal)).length > 0);
|
|
742
|
+
}
|
|
743
|
+
if (v6Deployments("JBBuybackHook", call.chainId).some((address) => same(call.to, address))) {
|
|
744
|
+
const decoded = decodeFunctionData({
|
|
745
|
+
abi: jbBuybackHookAbi,
|
|
746
|
+
data: call.data,
|
|
747
|
+
});
|
|
748
|
+
if (decoded.functionName === "setTwapWindowOf") {
|
|
749
|
+
const [projectId, terminalToken, window] = decoded.args;
|
|
750
|
+
const normalized = same(terminalToken, NATIVE_TOKEN)
|
|
751
|
+
? zeroAddress
|
|
752
|
+
: terminalToken;
|
|
753
|
+
return done(projectMatches(projectId) &&
|
|
754
|
+
matching(jbBuybackHookAbi, "TwapWindowChanged", call.to, (args) => fromCaller(args) &&
|
|
755
|
+
args.projectId === projectId &&
|
|
756
|
+
same(args.terminalToken, normalized) &&
|
|
757
|
+
args.newWindow === window).length > 0);
|
|
758
|
+
}
|
|
759
|
+
if (decoded.functionName === "setPoolFor") {
|
|
760
|
+
const projectId = decoded.args[0];
|
|
761
|
+
const window = decoded.args.length === 4 ? decoded.args[2] : decoded.args[3];
|
|
762
|
+
const terminalToken = decoded.args.length === 4 ? decoded.args[3] : decoded.args[4];
|
|
763
|
+
const key = reviewedPoolKey(decoded.args.length === 4 ? decoded.args[1] : summary?.key);
|
|
764
|
+
if (!key)
|
|
765
|
+
return done(false, "The reviewed pool key is unavailable or invalid.");
|
|
766
|
+
const normalized = same(terminalToken, NATIVE_TOKEN)
|
|
767
|
+
? zeroAddress
|
|
768
|
+
: terminalToken;
|
|
769
|
+
if (decoded.args.length === 5 &&
|
|
770
|
+
(key.fee !== decoded.args[1] ||
|
|
771
|
+
key.tickSpacing !== decoded.args[2] ||
|
|
772
|
+
(!same(key.currency0, normalized) &&
|
|
773
|
+
!same(key.currency1, normalized))))
|
|
774
|
+
return done(false, "The reviewed pool key does not match the signed fee, tick spacing, or terminal token.");
|
|
775
|
+
const poolId = uniswapV4PoolId(key);
|
|
776
|
+
const pools = matching(jbBuybackHookAbi, "PoolAdded", call.to, (args) => fromCaller(args) &&
|
|
777
|
+
projectMatches(args.projectId) &&
|
|
778
|
+
args.projectId === projectId &&
|
|
779
|
+
same(args.terminalToken, normalized) &&
|
|
780
|
+
same(args.poolId, poolId));
|
|
781
|
+
// The rollout hook remaps the registration sentinel; retired 1.1.1/v1 store the literal window.
|
|
782
|
+
const expectedWindow = window === 172800n &&
|
|
783
|
+
same(call.to, v6Address("JBBuybackHook", call.chainId))
|
|
784
|
+
? 1800n
|
|
785
|
+
: window;
|
|
786
|
+
const windows = matching(jbBuybackHookAbi, "TwapWindowChanged", call.to, (args) => fromCaller(args) &&
|
|
787
|
+
args.projectId === projectId &&
|
|
788
|
+
same(args.terminalToken, normalized) &&
|
|
789
|
+
args.newWindow === expectedWindow);
|
|
790
|
+
return done(pools.length > 0 && windows.length > 0);
|
|
791
|
+
}
|
|
792
|
+
return done(false);
|
|
793
|
+
}
|
|
794
|
+
return undefined;
|
|
795
|
+
}
|
|
796
|
+
function nftPaymentEvidence(plan, logs, terminal) {
|
|
797
|
+
const summary = plan.draft.summary;
|
|
798
|
+
const events = [];
|
|
799
|
+
if (!summary ||
|
|
800
|
+
!same(summary.operation, "721_pay") ||
|
|
801
|
+
!plan.draft.project ||
|
|
802
|
+
!equalData(summary.project, plan.draft.project) ||
|
|
803
|
+
typeof summary.hook !== "string" ||
|
|
804
|
+
!Array.isArray(summary.tierIds))
|
|
805
|
+
return {
|
|
806
|
+
verified: false,
|
|
807
|
+
events,
|
|
808
|
+
reason: "The signed NFT delivery specification is unavailable.",
|
|
809
|
+
};
|
|
810
|
+
const minted = [];
|
|
811
|
+
const transferred = new Set();
|
|
812
|
+
const tokenIds = new Set();
|
|
813
|
+
for (const log of logs) {
|
|
814
|
+
if (!same(log.address, summary.hook))
|
|
815
|
+
continue;
|
|
816
|
+
try {
|
|
817
|
+
const event = decodeEventLog({
|
|
818
|
+
abi: jb721TiersHookAbi,
|
|
819
|
+
data: log.data,
|
|
820
|
+
topics: log.topics,
|
|
821
|
+
strict: true,
|
|
822
|
+
});
|
|
823
|
+
if (event.eventName === "Mint" &&
|
|
824
|
+
same(event.args.beneficiary, summary.beneficiary) &&
|
|
825
|
+
same(event.args.caller, terminal)) {
|
|
826
|
+
minted.push(event.args.tierId.toString());
|
|
827
|
+
tokenIds.add(event.args.tokenId.toString());
|
|
828
|
+
events.push({
|
|
829
|
+
emitter: log.address,
|
|
830
|
+
event: event.eventName,
|
|
831
|
+
logIndex: log.logIndex,
|
|
832
|
+
args: jsonSafe(event.args),
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
if (event.eventName === "Transfer" &&
|
|
836
|
+
same(event.args.from, zeroAddress) &&
|
|
837
|
+
same(event.args.to, summary.beneficiary))
|
|
838
|
+
transferred.add(event.args.tokenId.toString());
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
/* Mint + ERC721 Transfer are both required from the authenticated clone. */
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const verified = equalData(minted.sort(), summary.tierIds.map(String).sort()) &&
|
|
845
|
+
minted.length === tokenIds.size &&
|
|
846
|
+
[...tokenIds].every((id) => transferred.has(id));
|
|
847
|
+
return {
|
|
848
|
+
verified,
|
|
849
|
+
events,
|
|
850
|
+
...(verified
|
|
851
|
+
? {}
|
|
852
|
+
: {
|
|
853
|
+
reason: "Payment confirmed, but the expected NFT tier Mint and ownership Transfer events were not all verified for the planned beneficiary.",
|
|
854
|
+
}),
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
//# sourceMappingURL=operationEvidence.js.map
|