@perkos/scheme-deferred 1.0.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/index.d.mts +344 -0
- package/dist/index.d.ts +344 -0
- package/dist/index.js +376 -0
- package/dist/index.mjs +347 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DEFERRED_ESCROW_ABI: () => DEFERRED_ESCROW_ABI,
|
|
24
|
+
DEFERRED_ESCROW_CLAIM_VOUCHER_ABI: () => DEFERRED_ESCROW_CLAIM_VOUCHER_ABI,
|
|
25
|
+
DEFERRED_ESCROW_GET_BALANCE_ABI: () => DEFERRED_ESCROW_GET_BALANCE_ABI,
|
|
26
|
+
DEFERRED_ESCROW_VOUCHER_CLAIMED_ABI: () => DEFERRED_ESCROW_VOUCHER_CLAIMED_ABI,
|
|
27
|
+
DeferredSchemeVerifier: () => DeferredSchemeVerifier,
|
|
28
|
+
ERC20_BALANCE_ABI: () => ERC20_BALANCE_ABI,
|
|
29
|
+
VOUCHER_TYPES: () => VOUCHER_TYPES,
|
|
30
|
+
VOUCHER_TYPE_DEF: () => VOUCHER_TYPE_DEF,
|
|
31
|
+
createEIP712Domain: () => createEIP712Domain,
|
|
32
|
+
createVoucherMessage: () => createVoucherMessage,
|
|
33
|
+
createVoucherTuple: () => createVoucherTuple,
|
|
34
|
+
generateVoucherId: () => generateVoucherId,
|
|
35
|
+
parseSignature: () => parseSignature
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
var import_viem = require("viem");
|
|
39
|
+
var import_util_chains = require("@perkos/util-chains");
|
|
40
|
+
var VOUCHER_TYPE_DEF = [
|
|
41
|
+
{ name: "id", type: "bytes32" },
|
|
42
|
+
{ name: "buyer", type: "address" },
|
|
43
|
+
{ name: "seller", type: "address" },
|
|
44
|
+
{ name: "valueAggregate", type: "uint256" },
|
|
45
|
+
{ name: "asset", type: "address" },
|
|
46
|
+
{ name: "timestamp", type: "uint64" },
|
|
47
|
+
{ name: "nonce", type: "uint256" },
|
|
48
|
+
{ name: "escrow", type: "address" },
|
|
49
|
+
{ name: "chainId", type: "uint256" }
|
|
50
|
+
];
|
|
51
|
+
var VOUCHER_TYPES = {
|
|
52
|
+
Voucher: VOUCHER_TYPE_DEF
|
|
53
|
+
};
|
|
54
|
+
var ERC20_BALANCE_ABI = [
|
|
55
|
+
{
|
|
56
|
+
name: "balanceOf",
|
|
57
|
+
type: "function",
|
|
58
|
+
stateMutability: "view",
|
|
59
|
+
inputs: [{ name: "account", type: "address" }],
|
|
60
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
61
|
+
}
|
|
62
|
+
];
|
|
63
|
+
var DEFERRED_ESCROW_GET_BALANCE_ABI = [
|
|
64
|
+
{
|
|
65
|
+
name: "getAvailableBalance",
|
|
66
|
+
type: "function",
|
|
67
|
+
stateMutability: "view",
|
|
68
|
+
inputs: [
|
|
69
|
+
{ name: "buyer", type: "address" },
|
|
70
|
+
{ name: "seller", type: "address" },
|
|
71
|
+
{ name: "asset", type: "address" }
|
|
72
|
+
],
|
|
73
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
var DEFERRED_ESCROW_VOUCHER_CLAIMED_ABI = [
|
|
77
|
+
{
|
|
78
|
+
name: "voucherClaimed",
|
|
79
|
+
type: "function",
|
|
80
|
+
stateMutability: "view",
|
|
81
|
+
inputs: [
|
|
82
|
+
{ name: "voucherId", type: "bytes32" },
|
|
83
|
+
{ name: "nonce", type: "uint256" }
|
|
84
|
+
],
|
|
85
|
+
outputs: [{ name: "", type: "bool" }]
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
var DEFERRED_ESCROW_CLAIM_VOUCHER_ABI = [
|
|
89
|
+
{
|
|
90
|
+
name: "claimVoucher",
|
|
91
|
+
type: "function",
|
|
92
|
+
stateMutability: "nonpayable",
|
|
93
|
+
inputs: [
|
|
94
|
+
{
|
|
95
|
+
name: "voucher",
|
|
96
|
+
type: "tuple",
|
|
97
|
+
components: [
|
|
98
|
+
{ name: "id", type: "bytes32" },
|
|
99
|
+
{ name: "buyer", type: "address" },
|
|
100
|
+
{ name: "seller", type: "address" },
|
|
101
|
+
{ name: "valueAggregate", type: "uint256" },
|
|
102
|
+
{ name: "asset", type: "address" },
|
|
103
|
+
{ name: "timestamp", type: "uint64" },
|
|
104
|
+
{ name: "nonce", type: "uint256" },
|
|
105
|
+
{ name: "escrow", type: "address" },
|
|
106
|
+
{ name: "chainId", type: "uint256" }
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
{ name: "signature", type: "bytes" }
|
|
110
|
+
],
|
|
111
|
+
outputs: []
|
|
112
|
+
}
|
|
113
|
+
];
|
|
114
|
+
var DEFERRED_ESCROW_ABI = [
|
|
115
|
+
...DEFERRED_ESCROW_GET_BALANCE_ABI,
|
|
116
|
+
...DEFERRED_ESCROW_VOUCHER_CLAIMED_ABI,
|
|
117
|
+
...DEFERRED_ESCROW_CLAIM_VOUCHER_ABI
|
|
118
|
+
];
|
|
119
|
+
var DeferredSchemeVerifier = class {
|
|
120
|
+
constructor(config) {
|
|
121
|
+
this.network = config.network;
|
|
122
|
+
this.chainId = (0, import_util_chains.getChainIdFromNetwork)(config.network) || 1;
|
|
123
|
+
this.escrowAddress = config.escrowAddress;
|
|
124
|
+
this.domainName = config.domainName || "X402DeferredEscrow";
|
|
125
|
+
this.domainVersion = config.domainVersion || "1";
|
|
126
|
+
const chain = (0, import_util_chains.getChainById)(this.chainId);
|
|
127
|
+
const rpcUrl = config.rpcUrl || (0, import_util_chains.getRpcUrl)(this.chainId);
|
|
128
|
+
if (!chain || !rpcUrl) {
|
|
129
|
+
throw new Error(`Unsupported network: ${config.network}`);
|
|
130
|
+
}
|
|
131
|
+
this.publicClient = (0, import_viem.createPublicClient)({
|
|
132
|
+
chain,
|
|
133
|
+
transport: (0, import_viem.http)(rpcUrl)
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Verify a deferred scheme payment voucher
|
|
138
|
+
*/
|
|
139
|
+
async verify(payload, requirements) {
|
|
140
|
+
try {
|
|
141
|
+
const { voucher, signature } = payload;
|
|
142
|
+
if (!this.validateVoucher(voucher, requirements)) {
|
|
143
|
+
return {
|
|
144
|
+
isValid: false,
|
|
145
|
+
invalidReason: "Voucher fields invalid",
|
|
146
|
+
payer: null
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const signer = await this.recoverSigner(voucher, signature);
|
|
150
|
+
if (!signer) {
|
|
151
|
+
return {
|
|
152
|
+
isValid: false,
|
|
153
|
+
invalidReason: "Invalid signature",
|
|
154
|
+
payer: null
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (signer.toLowerCase() !== voucher.buyer.toLowerCase()) {
|
|
158
|
+
return {
|
|
159
|
+
isValid: false,
|
|
160
|
+
invalidReason: `Signer does not match buyer. Recovered: ${signer}, Expected: ${voucher.buyer}`,
|
|
161
|
+
payer: null
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const claimed = await this.isVoucherClaimed(
|
|
165
|
+
voucher.id,
|
|
166
|
+
BigInt(voucher.nonce)
|
|
167
|
+
);
|
|
168
|
+
if (claimed) {
|
|
169
|
+
return {
|
|
170
|
+
isValid: false,
|
|
171
|
+
invalidReason: "Voucher already claimed",
|
|
172
|
+
payer: null
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const balance = await this.getEscrowBalance(
|
|
176
|
+
voucher.buyer,
|
|
177
|
+
voucher.seller,
|
|
178
|
+
voucher.asset
|
|
179
|
+
);
|
|
180
|
+
const valueAggregate = BigInt(voucher.valueAggregate);
|
|
181
|
+
if (balance < valueAggregate) {
|
|
182
|
+
return {
|
|
183
|
+
isValid: false,
|
|
184
|
+
invalidReason: "Insufficient escrow balance",
|
|
185
|
+
payer: null
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
isValid: true,
|
|
190
|
+
invalidReason: null,
|
|
191
|
+
payer: voucher.buyer
|
|
192
|
+
};
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return {
|
|
195
|
+
isValid: false,
|
|
196
|
+
invalidReason: error instanceof Error ? error.message : "Verification failed",
|
|
197
|
+
payer: null
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Validate voucher fields against requirements
|
|
203
|
+
*/
|
|
204
|
+
validateVoucher(voucher, requirements) {
|
|
205
|
+
if (voucher.escrow.toLowerCase() !== this.escrowAddress.toLowerCase()) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
if (BigInt(voucher.chainId) !== BigInt(this.chainId)) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
if (voucher.seller.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const valueAggregate = BigInt(voucher.valueAggregate);
|
|
215
|
+
const maxAmount = BigInt(requirements.maxAmountRequired);
|
|
216
|
+
if (valueAggregate > maxAmount) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
if (voucher.asset.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Recover signer from EIP-712 typed data signature
|
|
226
|
+
*/
|
|
227
|
+
async recoverSigner(voucher, signature) {
|
|
228
|
+
try {
|
|
229
|
+
const domain = this.getEIP712Domain();
|
|
230
|
+
const message = {
|
|
231
|
+
id: voucher.id,
|
|
232
|
+
buyer: voucher.buyer,
|
|
233
|
+
seller: voucher.seller,
|
|
234
|
+
valueAggregate: BigInt(voucher.valueAggregate),
|
|
235
|
+
asset: voucher.asset,
|
|
236
|
+
timestamp: BigInt(voucher.timestamp),
|
|
237
|
+
nonce: BigInt(voucher.nonce),
|
|
238
|
+
escrow: voucher.escrow,
|
|
239
|
+
chainId: BigInt(voucher.chainId)
|
|
240
|
+
};
|
|
241
|
+
const recoveredAddress = await (0, import_viem.recoverTypedDataAddress)({
|
|
242
|
+
domain,
|
|
243
|
+
types: VOUCHER_TYPES,
|
|
244
|
+
primaryType: "Voucher",
|
|
245
|
+
message,
|
|
246
|
+
signature
|
|
247
|
+
});
|
|
248
|
+
return recoveredAddress;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Get EIP-712 domain for escrow contract
|
|
255
|
+
*/
|
|
256
|
+
getEIP712Domain() {
|
|
257
|
+
return {
|
|
258
|
+
name: this.domainName,
|
|
259
|
+
version: this.domainVersion,
|
|
260
|
+
chainId: this.chainId,
|
|
261
|
+
verifyingContract: this.escrowAddress
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Check if voucher has been claimed
|
|
266
|
+
*/
|
|
267
|
+
async isVoucherClaimed(voucherId, nonce) {
|
|
268
|
+
try {
|
|
269
|
+
const claimed = await this.publicClient.readContract({
|
|
270
|
+
address: this.escrowAddress,
|
|
271
|
+
abi: DEFERRED_ESCROW_VOUCHER_CLAIMED_ABI,
|
|
272
|
+
functionName: "voucherClaimed",
|
|
273
|
+
args: [voucherId, nonce]
|
|
274
|
+
});
|
|
275
|
+
return claimed;
|
|
276
|
+
} catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Get available escrow balance
|
|
282
|
+
*/
|
|
283
|
+
async getEscrowBalance(buyer, seller, asset) {
|
|
284
|
+
try {
|
|
285
|
+
const balance = await this.publicClient.readContract({
|
|
286
|
+
address: this.escrowAddress,
|
|
287
|
+
abi: DEFERRED_ESCROW_GET_BALANCE_ABI,
|
|
288
|
+
functionName: "getAvailableBalance",
|
|
289
|
+
args: [buyer, seller, asset]
|
|
290
|
+
});
|
|
291
|
+
return balance;
|
|
292
|
+
} catch {
|
|
293
|
+
return 0n;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Get network name
|
|
298
|
+
*/
|
|
299
|
+
getNetwork() {
|
|
300
|
+
return this.network;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Get chain ID
|
|
304
|
+
*/
|
|
305
|
+
getChainId() {
|
|
306
|
+
return this.chainId;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Get escrow address
|
|
310
|
+
*/
|
|
311
|
+
getEscrowAddress() {
|
|
312
|
+
return this.escrowAddress;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
function parseSignature(signature) {
|
|
316
|
+
const sig = signature.slice(2);
|
|
317
|
+
const r = `0x${sig.slice(0, 64)}`;
|
|
318
|
+
const s = `0x${sig.slice(64, 128)}`;
|
|
319
|
+
const v = parseInt(sig.slice(128, 130), 16);
|
|
320
|
+
return { v, r, s };
|
|
321
|
+
}
|
|
322
|
+
function createEIP712Domain(chainId, escrowAddress, domainName, domainVersion) {
|
|
323
|
+
return {
|
|
324
|
+
name: domainName || "X402DeferredEscrow",
|
|
325
|
+
version: domainVersion || "1",
|
|
326
|
+
chainId,
|
|
327
|
+
verifyingContract: escrowAddress
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function generateVoucherId() {
|
|
331
|
+
const bytes = new Uint8Array(32);
|
|
332
|
+
crypto.getRandomValues(bytes);
|
|
333
|
+
return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
334
|
+
}
|
|
335
|
+
function createVoucherMessage(id, buyer, seller, valueAggregate, asset, timestamp, nonce, escrow, chainId) {
|
|
336
|
+
return {
|
|
337
|
+
id,
|
|
338
|
+
buyer,
|
|
339
|
+
seller,
|
|
340
|
+
valueAggregate: BigInt(valueAggregate),
|
|
341
|
+
asset,
|
|
342
|
+
timestamp: BigInt(timestamp),
|
|
343
|
+
nonce: BigInt(nonce),
|
|
344
|
+
escrow,
|
|
345
|
+
chainId: BigInt(chainId)
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function createVoucherTuple(voucher) {
|
|
349
|
+
return {
|
|
350
|
+
id: voucher.id,
|
|
351
|
+
buyer: voucher.buyer,
|
|
352
|
+
seller: voucher.seller,
|
|
353
|
+
valueAggregate: BigInt(voucher.valueAggregate),
|
|
354
|
+
asset: voucher.asset,
|
|
355
|
+
timestamp: BigInt(voucher.timestamp),
|
|
356
|
+
nonce: BigInt(voucher.nonce),
|
|
357
|
+
escrow: voucher.escrow,
|
|
358
|
+
chainId: BigInt(voucher.chainId)
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
362
|
+
0 && (module.exports = {
|
|
363
|
+
DEFERRED_ESCROW_ABI,
|
|
364
|
+
DEFERRED_ESCROW_CLAIM_VOUCHER_ABI,
|
|
365
|
+
DEFERRED_ESCROW_GET_BALANCE_ABI,
|
|
366
|
+
DEFERRED_ESCROW_VOUCHER_CLAIMED_ABI,
|
|
367
|
+
DeferredSchemeVerifier,
|
|
368
|
+
ERC20_BALANCE_ABI,
|
|
369
|
+
VOUCHER_TYPES,
|
|
370
|
+
VOUCHER_TYPE_DEF,
|
|
371
|
+
createEIP712Domain,
|
|
372
|
+
createVoucherMessage,
|
|
373
|
+
createVoucherTuple,
|
|
374
|
+
generateVoucherId,
|
|
375
|
+
parseSignature
|
|
376
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
createPublicClient,
|
|
4
|
+
http,
|
|
5
|
+
recoverTypedDataAddress
|
|
6
|
+
} from "viem";
|
|
7
|
+
import {
|
|
8
|
+
getChainById,
|
|
9
|
+
getChainIdFromNetwork,
|
|
10
|
+
getRpcUrl
|
|
11
|
+
} from "@perkos/util-chains";
|
|
12
|
+
var VOUCHER_TYPE_DEF = [
|
|
13
|
+
{ name: "id", type: "bytes32" },
|
|
14
|
+
{ name: "buyer", type: "address" },
|
|
15
|
+
{ name: "seller", type: "address" },
|
|
16
|
+
{ name: "valueAggregate", type: "uint256" },
|
|
17
|
+
{ name: "asset", type: "address" },
|
|
18
|
+
{ name: "timestamp", type: "uint64" },
|
|
19
|
+
{ name: "nonce", type: "uint256" },
|
|
20
|
+
{ name: "escrow", type: "address" },
|
|
21
|
+
{ name: "chainId", type: "uint256" }
|
|
22
|
+
];
|
|
23
|
+
var VOUCHER_TYPES = {
|
|
24
|
+
Voucher: VOUCHER_TYPE_DEF
|
|
25
|
+
};
|
|
26
|
+
var ERC20_BALANCE_ABI = [
|
|
27
|
+
{
|
|
28
|
+
name: "balanceOf",
|
|
29
|
+
type: "function",
|
|
30
|
+
stateMutability: "view",
|
|
31
|
+
inputs: [{ name: "account", type: "address" }],
|
|
32
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
var DEFERRED_ESCROW_GET_BALANCE_ABI = [
|
|
36
|
+
{
|
|
37
|
+
name: "getAvailableBalance",
|
|
38
|
+
type: "function",
|
|
39
|
+
stateMutability: "view",
|
|
40
|
+
inputs: [
|
|
41
|
+
{ name: "buyer", type: "address" },
|
|
42
|
+
{ name: "seller", type: "address" },
|
|
43
|
+
{ name: "asset", type: "address" }
|
|
44
|
+
],
|
|
45
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
46
|
+
}
|
|
47
|
+
];
|
|
48
|
+
var DEFERRED_ESCROW_VOUCHER_CLAIMED_ABI = [
|
|
49
|
+
{
|
|
50
|
+
name: "voucherClaimed",
|
|
51
|
+
type: "function",
|
|
52
|
+
stateMutability: "view",
|
|
53
|
+
inputs: [
|
|
54
|
+
{ name: "voucherId", type: "bytes32" },
|
|
55
|
+
{ name: "nonce", type: "uint256" }
|
|
56
|
+
],
|
|
57
|
+
outputs: [{ name: "", type: "bool" }]
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
var DEFERRED_ESCROW_CLAIM_VOUCHER_ABI = [
|
|
61
|
+
{
|
|
62
|
+
name: "claimVoucher",
|
|
63
|
+
type: "function",
|
|
64
|
+
stateMutability: "nonpayable",
|
|
65
|
+
inputs: [
|
|
66
|
+
{
|
|
67
|
+
name: "voucher",
|
|
68
|
+
type: "tuple",
|
|
69
|
+
components: [
|
|
70
|
+
{ name: "id", type: "bytes32" },
|
|
71
|
+
{ name: "buyer", type: "address" },
|
|
72
|
+
{ name: "seller", type: "address" },
|
|
73
|
+
{ name: "valueAggregate", type: "uint256" },
|
|
74
|
+
{ name: "asset", type: "address" },
|
|
75
|
+
{ name: "timestamp", type: "uint64" },
|
|
76
|
+
{ name: "nonce", type: "uint256" },
|
|
77
|
+
{ name: "escrow", type: "address" },
|
|
78
|
+
{ name: "chainId", type: "uint256" }
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{ name: "signature", type: "bytes" }
|
|
82
|
+
],
|
|
83
|
+
outputs: []
|
|
84
|
+
}
|
|
85
|
+
];
|
|
86
|
+
var DEFERRED_ESCROW_ABI = [
|
|
87
|
+
...DEFERRED_ESCROW_GET_BALANCE_ABI,
|
|
88
|
+
...DEFERRED_ESCROW_VOUCHER_CLAIMED_ABI,
|
|
89
|
+
...DEFERRED_ESCROW_CLAIM_VOUCHER_ABI
|
|
90
|
+
];
|
|
91
|
+
var DeferredSchemeVerifier = class {
|
|
92
|
+
constructor(config) {
|
|
93
|
+
this.network = config.network;
|
|
94
|
+
this.chainId = getChainIdFromNetwork(config.network) || 1;
|
|
95
|
+
this.escrowAddress = config.escrowAddress;
|
|
96
|
+
this.domainName = config.domainName || "X402DeferredEscrow";
|
|
97
|
+
this.domainVersion = config.domainVersion || "1";
|
|
98
|
+
const chain = getChainById(this.chainId);
|
|
99
|
+
const rpcUrl = config.rpcUrl || getRpcUrl(this.chainId);
|
|
100
|
+
if (!chain || !rpcUrl) {
|
|
101
|
+
throw new Error(`Unsupported network: ${config.network}`);
|
|
102
|
+
}
|
|
103
|
+
this.publicClient = createPublicClient({
|
|
104
|
+
chain,
|
|
105
|
+
transport: http(rpcUrl)
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Verify a deferred scheme payment voucher
|
|
110
|
+
*/
|
|
111
|
+
async verify(payload, requirements) {
|
|
112
|
+
try {
|
|
113
|
+
const { voucher, signature } = payload;
|
|
114
|
+
if (!this.validateVoucher(voucher, requirements)) {
|
|
115
|
+
return {
|
|
116
|
+
isValid: false,
|
|
117
|
+
invalidReason: "Voucher fields invalid",
|
|
118
|
+
payer: null
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const signer = await this.recoverSigner(voucher, signature);
|
|
122
|
+
if (!signer) {
|
|
123
|
+
return {
|
|
124
|
+
isValid: false,
|
|
125
|
+
invalidReason: "Invalid signature",
|
|
126
|
+
payer: null
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (signer.toLowerCase() !== voucher.buyer.toLowerCase()) {
|
|
130
|
+
return {
|
|
131
|
+
isValid: false,
|
|
132
|
+
invalidReason: `Signer does not match buyer. Recovered: ${signer}, Expected: ${voucher.buyer}`,
|
|
133
|
+
payer: null
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const claimed = await this.isVoucherClaimed(
|
|
137
|
+
voucher.id,
|
|
138
|
+
BigInt(voucher.nonce)
|
|
139
|
+
);
|
|
140
|
+
if (claimed) {
|
|
141
|
+
return {
|
|
142
|
+
isValid: false,
|
|
143
|
+
invalidReason: "Voucher already claimed",
|
|
144
|
+
payer: null
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const balance = await this.getEscrowBalance(
|
|
148
|
+
voucher.buyer,
|
|
149
|
+
voucher.seller,
|
|
150
|
+
voucher.asset
|
|
151
|
+
);
|
|
152
|
+
const valueAggregate = BigInt(voucher.valueAggregate);
|
|
153
|
+
if (balance < valueAggregate) {
|
|
154
|
+
return {
|
|
155
|
+
isValid: false,
|
|
156
|
+
invalidReason: "Insufficient escrow balance",
|
|
157
|
+
payer: null
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
isValid: true,
|
|
162
|
+
invalidReason: null,
|
|
163
|
+
payer: voucher.buyer
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return {
|
|
167
|
+
isValid: false,
|
|
168
|
+
invalidReason: error instanceof Error ? error.message : "Verification failed",
|
|
169
|
+
payer: null
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Validate voucher fields against requirements
|
|
175
|
+
*/
|
|
176
|
+
validateVoucher(voucher, requirements) {
|
|
177
|
+
if (voucher.escrow.toLowerCase() !== this.escrowAddress.toLowerCase()) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
if (BigInt(voucher.chainId) !== BigInt(this.chainId)) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
if (voucher.seller.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const valueAggregate = BigInt(voucher.valueAggregate);
|
|
187
|
+
const maxAmount = BigInt(requirements.maxAmountRequired);
|
|
188
|
+
if (valueAggregate > maxAmount) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
if (voucher.asset.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Recover signer from EIP-712 typed data signature
|
|
198
|
+
*/
|
|
199
|
+
async recoverSigner(voucher, signature) {
|
|
200
|
+
try {
|
|
201
|
+
const domain = this.getEIP712Domain();
|
|
202
|
+
const message = {
|
|
203
|
+
id: voucher.id,
|
|
204
|
+
buyer: voucher.buyer,
|
|
205
|
+
seller: voucher.seller,
|
|
206
|
+
valueAggregate: BigInt(voucher.valueAggregate),
|
|
207
|
+
asset: voucher.asset,
|
|
208
|
+
timestamp: BigInt(voucher.timestamp),
|
|
209
|
+
nonce: BigInt(voucher.nonce),
|
|
210
|
+
escrow: voucher.escrow,
|
|
211
|
+
chainId: BigInt(voucher.chainId)
|
|
212
|
+
};
|
|
213
|
+
const recoveredAddress = await recoverTypedDataAddress({
|
|
214
|
+
domain,
|
|
215
|
+
types: VOUCHER_TYPES,
|
|
216
|
+
primaryType: "Voucher",
|
|
217
|
+
message,
|
|
218
|
+
signature
|
|
219
|
+
});
|
|
220
|
+
return recoveredAddress;
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get EIP-712 domain for escrow contract
|
|
227
|
+
*/
|
|
228
|
+
getEIP712Domain() {
|
|
229
|
+
return {
|
|
230
|
+
name: this.domainName,
|
|
231
|
+
version: this.domainVersion,
|
|
232
|
+
chainId: this.chainId,
|
|
233
|
+
verifyingContract: this.escrowAddress
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Check if voucher has been claimed
|
|
238
|
+
*/
|
|
239
|
+
async isVoucherClaimed(voucherId, nonce) {
|
|
240
|
+
try {
|
|
241
|
+
const claimed = await this.publicClient.readContract({
|
|
242
|
+
address: this.escrowAddress,
|
|
243
|
+
abi: DEFERRED_ESCROW_VOUCHER_CLAIMED_ABI,
|
|
244
|
+
functionName: "voucherClaimed",
|
|
245
|
+
args: [voucherId, nonce]
|
|
246
|
+
});
|
|
247
|
+
return claimed;
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Get available escrow balance
|
|
254
|
+
*/
|
|
255
|
+
async getEscrowBalance(buyer, seller, asset) {
|
|
256
|
+
try {
|
|
257
|
+
const balance = await this.publicClient.readContract({
|
|
258
|
+
address: this.escrowAddress,
|
|
259
|
+
abi: DEFERRED_ESCROW_GET_BALANCE_ABI,
|
|
260
|
+
functionName: "getAvailableBalance",
|
|
261
|
+
args: [buyer, seller, asset]
|
|
262
|
+
});
|
|
263
|
+
return balance;
|
|
264
|
+
} catch {
|
|
265
|
+
return 0n;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get network name
|
|
270
|
+
*/
|
|
271
|
+
getNetwork() {
|
|
272
|
+
return this.network;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get chain ID
|
|
276
|
+
*/
|
|
277
|
+
getChainId() {
|
|
278
|
+
return this.chainId;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Get escrow address
|
|
282
|
+
*/
|
|
283
|
+
getEscrowAddress() {
|
|
284
|
+
return this.escrowAddress;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
function parseSignature(signature) {
|
|
288
|
+
const sig = signature.slice(2);
|
|
289
|
+
const r = `0x${sig.slice(0, 64)}`;
|
|
290
|
+
const s = `0x${sig.slice(64, 128)}`;
|
|
291
|
+
const v = parseInt(sig.slice(128, 130), 16);
|
|
292
|
+
return { v, r, s };
|
|
293
|
+
}
|
|
294
|
+
function createEIP712Domain(chainId, escrowAddress, domainName, domainVersion) {
|
|
295
|
+
return {
|
|
296
|
+
name: domainName || "X402DeferredEscrow",
|
|
297
|
+
version: domainVersion || "1",
|
|
298
|
+
chainId,
|
|
299
|
+
verifyingContract: escrowAddress
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function generateVoucherId() {
|
|
303
|
+
const bytes = new Uint8Array(32);
|
|
304
|
+
crypto.getRandomValues(bytes);
|
|
305
|
+
return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
306
|
+
}
|
|
307
|
+
function createVoucherMessage(id, buyer, seller, valueAggregate, asset, timestamp, nonce, escrow, chainId) {
|
|
308
|
+
return {
|
|
309
|
+
id,
|
|
310
|
+
buyer,
|
|
311
|
+
seller,
|
|
312
|
+
valueAggregate: BigInt(valueAggregate),
|
|
313
|
+
asset,
|
|
314
|
+
timestamp: BigInt(timestamp),
|
|
315
|
+
nonce: BigInt(nonce),
|
|
316
|
+
escrow,
|
|
317
|
+
chainId: BigInt(chainId)
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function createVoucherTuple(voucher) {
|
|
321
|
+
return {
|
|
322
|
+
id: voucher.id,
|
|
323
|
+
buyer: voucher.buyer,
|
|
324
|
+
seller: voucher.seller,
|
|
325
|
+
valueAggregate: BigInt(voucher.valueAggregate),
|
|
326
|
+
asset: voucher.asset,
|
|
327
|
+
timestamp: BigInt(voucher.timestamp),
|
|
328
|
+
nonce: BigInt(voucher.nonce),
|
|
329
|
+
escrow: voucher.escrow,
|
|
330
|
+
chainId: BigInt(voucher.chainId)
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
export {
|
|
334
|
+
DEFERRED_ESCROW_ABI,
|
|
335
|
+
DEFERRED_ESCROW_CLAIM_VOUCHER_ABI,
|
|
336
|
+
DEFERRED_ESCROW_GET_BALANCE_ABI,
|
|
337
|
+
DEFERRED_ESCROW_VOUCHER_CLAIMED_ABI,
|
|
338
|
+
DeferredSchemeVerifier,
|
|
339
|
+
ERC20_BALANCE_ABI,
|
|
340
|
+
VOUCHER_TYPES,
|
|
341
|
+
VOUCHER_TYPE_DEF,
|
|
342
|
+
createEIP712Domain,
|
|
343
|
+
createVoucherMessage,
|
|
344
|
+
createVoucherTuple,
|
|
345
|
+
generateVoucherId,
|
|
346
|
+
parseSignature
|
|
347
|
+
};
|