@bosonprotocol/x402-server 0.1.0-alpha-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/LICENSE +201 -0
- package/README.md +65 -0
- package/dist/cjs/challenge/index.d.ts +64 -0
- package/dist/cjs/challenge/index.js +96 -0
- package/dist/cjs/challenge/index.js.map +1 -0
- package/dist/cjs/client-tnrpqVMW.d.ts +36 -0
- package/dist/cjs/config-CBr9qMps.d.ts +419 -0
- package/dist/cjs/facilitator/index.d.ts +26 -0
- package/dist/cjs/facilitator/index.js +136 -0
- package/dist/cjs/facilitator/index.js.map +1 -0
- package/dist/cjs/handlers/index.d.ts +267 -0
- package/dist/cjs/handlers/index.js +1182 -0
- package/dist/cjs/handlers/index.js.map +1 -0
- package/dist/cjs/index.d.ts +72 -0
- package/dist/cjs/index.js +1643 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/onchain/index.d.ts +73 -0
- package/dist/cjs/onchain/index.js +87 -0
- package/dist/cjs/onchain/index.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/validate/index.d.ts +67 -0
- package/dist/cjs/validate/index.js +429 -0
- package/dist/cjs/validate/index.js.map +1 -0
- package/dist/esm/challenge/index.js +3 -0
- package/dist/esm/challenge/index.js.map +1 -0
- package/dist/esm/chunk-4NQ3VKC3.js +84 -0
- package/dist/esm/chunk-4NQ3VKC3.js.map +1 -0
- package/dist/esm/chunk-CGVXQX5V.js +426 -0
- package/dist/esm/chunk-CGVXQX5V.js.map +1 -0
- package/dist/esm/chunk-DJMCSCBE.js +3 -0
- package/dist/esm/chunk-DJMCSCBE.js.map +1 -0
- package/dist/esm/chunk-EAKQ4EFZ.js +124 -0
- package/dist/esm/chunk-EAKQ4EFZ.js.map +1 -0
- package/dist/esm/chunk-GBPU373M.js +93 -0
- package/dist/esm/chunk-GBPU373M.js.map +1 -0
- package/dist/esm/chunk-PU5Y7FZG.js +14 -0
- package/dist/esm/chunk-PU5Y7FZG.js.map +1 -0
- package/dist/esm/chunk-WU7QJ7YP.js +3 -0
- package/dist/esm/chunk-WU7QJ7YP.js.map +1 -0
- package/dist/esm/chunk-Y5HFLCAT.js +657 -0
- package/dist/esm/chunk-Y5HFLCAT.js.map +1 -0
- package/dist/esm/facilitator/index.js +4 -0
- package/dist/esm/facilitator/index.js.map +1 -0
- package/dist/esm/handlers/index.js +6 -0
- package/dist/esm/handlers/index.js.map +1 -0
- package/dist/esm/index.js +261 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/onchain/index.js +4 -0
- package/dist/esm/onchain/index.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/validate/index.js +4 -0
- package/dist/esm/validate/index.js.map +1 -0
- package/package.json +88 -0
|
@@ -0,0 +1,1182 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var x402Actions = require('@bosonprotocol/x402-actions');
|
|
4
|
+
var escrow = require('@bosonprotocol/x402-core/schemes/escrow');
|
|
5
|
+
var eip712 = require('@bosonprotocol/x402-core/eip712');
|
|
6
|
+
var x402Evm = require('@bosonprotocol/x402-evm');
|
|
7
|
+
var stateMachine = require('@bosonprotocol/x402-core/state-machine');
|
|
8
|
+
|
|
9
|
+
// src/handlers/commit-and-redeem.ts
|
|
10
|
+
|
|
11
|
+
// src/internal/facilitator-endpoints.ts
|
|
12
|
+
var COMMIT_ACTION_IDS = /* @__PURE__ */ new Set([
|
|
13
|
+
"boson-createOfferAndCommit",
|
|
14
|
+
"boson-createOfferCommitAndRedeem"
|
|
15
|
+
]);
|
|
16
|
+
function facilitatorEndpointFor(actionId, facilitatorUrl) {
|
|
17
|
+
const base = facilitatorUrl.replace(/\/+$/, "");
|
|
18
|
+
if (COMMIT_ACTION_IDS.has(actionId)) {
|
|
19
|
+
return `${base}/settle`;
|
|
20
|
+
}
|
|
21
|
+
return `${base}/perform-action?action=${encodeURIComponent(actionId)}`;
|
|
22
|
+
}
|
|
23
|
+
function stampFacilitatorEndpoints(next, facilitatorUrl) {
|
|
24
|
+
return next.map((entry) => {
|
|
25
|
+
if (!entry.channels.includes("facilitator")) {
|
|
26
|
+
return entry;
|
|
27
|
+
}
|
|
28
|
+
if (entry.endpoints?.facilitator !== void 0) {
|
|
29
|
+
return entry;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
...entry,
|
|
33
|
+
endpoints: {
|
|
34
|
+
...entry.endpoints,
|
|
35
|
+
facilitator: facilitatorEndpointFor(entry.id, facilitatorUrl)
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/handlers/next-actions.ts
|
|
42
|
+
function emitNextActions(input, registry, facilitatorUrl) {
|
|
43
|
+
const derived = input.exchangeState === x402Actions.ExchangeState.DISPUTED ? x402Actions.deriveNextActions(
|
|
44
|
+
{
|
|
45
|
+
exchangeId: input.exchangeId,
|
|
46
|
+
exchangeState: x402Actions.ExchangeState.DISPUTED,
|
|
47
|
+
disputeState: input.disputeState
|
|
48
|
+
},
|
|
49
|
+
registry
|
|
50
|
+
) : x402Actions.deriveNextActions(
|
|
51
|
+
{ exchangeId: input.exchangeId, exchangeState: input.exchangeState },
|
|
52
|
+
registry
|
|
53
|
+
);
|
|
54
|
+
if (facilitatorUrl === void 0) {
|
|
55
|
+
return derived;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...derived,
|
|
59
|
+
next: stampFacilitatorEndpoints(derived.next, facilitatorUrl)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/handlers/types.ts
|
|
64
|
+
function handlerOk(body) {
|
|
65
|
+
return { ok: true, status: 200, body };
|
|
66
|
+
}
|
|
67
|
+
function plainHandlerOk(body) {
|
|
68
|
+
return { ok: true, status: 200, body };
|
|
69
|
+
}
|
|
70
|
+
function handlerErr(status, code, reason, details) {
|
|
71
|
+
const body = { code, reason };
|
|
72
|
+
if (details !== void 0) body.details = details;
|
|
73
|
+
return { ok: false, status, body };
|
|
74
|
+
}
|
|
75
|
+
function decodeXPaymentHeader(header) {
|
|
76
|
+
if (header === void 0 || header === null || header.length === 0) {
|
|
77
|
+
return { ok: false, code: "MISSING_HEADER", reason: "X-PAYMENT header is missing" };
|
|
78
|
+
}
|
|
79
|
+
const padded = base64UrlToBase64(header);
|
|
80
|
+
let decoded;
|
|
81
|
+
try {
|
|
82
|
+
if (typeof atob === "function") {
|
|
83
|
+
const binary = atob(padded);
|
|
84
|
+
const bytes = new Uint8Array(binary.length);
|
|
85
|
+
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
86
|
+
decoded = new TextDecoder().decode(bytes);
|
|
87
|
+
} else {
|
|
88
|
+
decoded = Buffer.from(padded, "base64").toString("utf8");
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return { ok: false, code: "INVALID_BASE64", reason: e.message };
|
|
92
|
+
}
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(decoded);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return { ok: false, code: "INVALID_JSON", reason: e.message };
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const payload = escrow.parseEscrowPaymentPayload(parsed);
|
|
101
|
+
return { ok: true, payload };
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return { ok: false, code: "INVALID_PAYLOAD", reason: e.message };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function base64UrlToBase64(input) {
|
|
107
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
108
|
+
const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
|
|
109
|
+
return normalized + padding;
|
|
110
|
+
}
|
|
111
|
+
async function validatePaymentPayload(args) {
|
|
112
|
+
const { payload, requirements } = args;
|
|
113
|
+
if (payload.scheme !== "escrow" || requirements.scheme !== "escrow") {
|
|
114
|
+
return failure(1, "SCHEME_MISMATCH", "scheme", "escrow", payload.scheme);
|
|
115
|
+
}
|
|
116
|
+
if (payload.network !== requirements.network) {
|
|
117
|
+
return failure(2, "NETWORK_MISMATCH", "network", requirements.network, payload.network);
|
|
118
|
+
}
|
|
119
|
+
if (!deepEqual(payload.payload.offerRef.fullOffer, requirements.offer.fullOffer)) {
|
|
120
|
+
return failure(
|
|
121
|
+
3,
|
|
122
|
+
"FULL_OFFER_MISMATCH",
|
|
123
|
+
"payload.offerRef.fullOffer",
|
|
124
|
+
"<requirements.offer.fullOffer>",
|
|
125
|
+
"<payload.offerRef.fullOffer>"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (payload.payload.offerRef.sellerSig !== requirements.offer.sellerSig) {
|
|
129
|
+
return failure(
|
|
130
|
+
4,
|
|
131
|
+
"SELLER_SIG_MISMATCH",
|
|
132
|
+
"payload.offerRef.sellerSig",
|
|
133
|
+
requirements.offer.sellerSig,
|
|
134
|
+
payload.payload.offerRef.sellerSig
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const advertisedActions = requirements.actions.next.map((entry) => entry.id);
|
|
138
|
+
if (!advertisedActions.includes(payload.payload.action)) {
|
|
139
|
+
return failure(
|
|
140
|
+
5,
|
|
141
|
+
"ACTION_NOT_IN_REQUIREMENTS",
|
|
142
|
+
"payload.action",
|
|
143
|
+
advertisedActions,
|
|
144
|
+
payload.payload.action
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (!requirements.tokenAuthStrategies.includes(payload.payload.tokenAuthStrategy)) {
|
|
148
|
+
return failure(
|
|
149
|
+
6,
|
|
150
|
+
"TOKEN_AUTH_NOT_IN_REQUIREMENTS",
|
|
151
|
+
"payload.tokenAuthStrategy",
|
|
152
|
+
requirements.tokenAuthStrategies,
|
|
153
|
+
payload.payload.tokenAuthStrategy
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const calldataResult = await checkFunctionSignatureAndCalldataEquality(payload);
|
|
157
|
+
if (calldataResult !== null) return calldataResult;
|
|
158
|
+
if (payload.payload.buyer.toLowerCase() !== payload.payload.metaTx.from.toLowerCase()) {
|
|
159
|
+
return failure(
|
|
160
|
+
8,
|
|
161
|
+
"BAD_META_TX_SIGNATURE",
|
|
162
|
+
"payload.metaTx.from",
|
|
163
|
+
payload.payload.buyer,
|
|
164
|
+
payload.payload.metaTx.from
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
let recovered;
|
|
168
|
+
try {
|
|
169
|
+
recovered = await eip712.recoverMetaTransactionSigner({
|
|
170
|
+
chainId: args.chainId,
|
|
171
|
+
verifyingContract: requirements.escrowAddress,
|
|
172
|
+
message: {
|
|
173
|
+
nonce: BigInt(payload.payload.metaTx.nonce),
|
|
174
|
+
from: payload.payload.metaTx.from,
|
|
175
|
+
contractAddress: requirements.escrowAddress,
|
|
176
|
+
functionName: payload.payload.metaTx.functionName,
|
|
177
|
+
functionSignature: payload.payload.metaTx.functionSignature
|
|
178
|
+
},
|
|
179
|
+
signature: encodeRsv(payload.payload.metaTx.sig)
|
|
180
|
+
});
|
|
181
|
+
} catch (e) {
|
|
182
|
+
return failure(
|
|
183
|
+
8,
|
|
184
|
+
"BAD_META_TX_SIGNATURE",
|
|
185
|
+
"payload.metaTx.sig",
|
|
186
|
+
"recoverable ECDSA signature",
|
|
187
|
+
payload.payload.metaTx.sig,
|
|
188
|
+
e.message
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (recovered.toLowerCase() !== payload.payload.buyer.toLowerCase()) {
|
|
192
|
+
return failure(
|
|
193
|
+
8,
|
|
194
|
+
"BAD_META_TX_SIGNATURE",
|
|
195
|
+
"payload.metaTx.sig",
|
|
196
|
+
payload.payload.buyer,
|
|
197
|
+
recovered
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const strategyResult = checkTokenAuthRules(payload, requirements, args.now);
|
|
201
|
+
if (strategyResult !== null) return strategyResult;
|
|
202
|
+
const fulfillmentResult = checkFulfillment(payload, requirements, args.validateFulfillmentData);
|
|
203
|
+
if (fulfillmentResult !== null) return fulfillmentResult;
|
|
204
|
+
return { ok: true };
|
|
205
|
+
}
|
|
206
|
+
async function checkFunctionSignatureAndCalldataEquality(payload) {
|
|
207
|
+
const action = payload.payload.action;
|
|
208
|
+
const buildCalldata = action === "boson-createOfferAndCommit" ? x402Evm.buildCreateOfferAndCommitCalldata : action === "boson-createOfferCommitAndRedeem" ? x402Evm.buildCreateOfferCommitAndRedeemCalldata : void 0;
|
|
209
|
+
if (!buildCalldata) {
|
|
210
|
+
return failure(
|
|
211
|
+
7,
|
|
212
|
+
"CALLDATA_MISMATCH",
|
|
213
|
+
"payload.action",
|
|
214
|
+
"boson-createOfferAndCommit | boson-createOfferCommitAndRedeem",
|
|
215
|
+
action,
|
|
216
|
+
"rule-7 calldata reconstruction is only defined for commit-time actions"
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const fullOfferWithSig = {
|
|
220
|
+
...payload.payload.offerRef.fullOffer,
|
|
221
|
+
committer: payload.payload.buyer,
|
|
222
|
+
signature: payload.payload.offerRef.sellerSig
|
|
223
|
+
};
|
|
224
|
+
let expected;
|
|
225
|
+
try {
|
|
226
|
+
expected = await buildCalldata({ fullOffer: fullOfferWithSig });
|
|
227
|
+
} catch (e) {
|
|
228
|
+
return failure(
|
|
229
|
+
7,
|
|
230
|
+
"CALLDATA_MISMATCH",
|
|
231
|
+
"payload.metaTx.functionSignature",
|
|
232
|
+
void 0,
|
|
233
|
+
void 0,
|
|
234
|
+
e.message
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (expected.functionName !== payload.payload.metaTx.functionName) {
|
|
238
|
+
return failure(
|
|
239
|
+
7,
|
|
240
|
+
"CALLDATA_MISMATCH",
|
|
241
|
+
"payload.metaTx.functionName",
|
|
242
|
+
expected.functionName,
|
|
243
|
+
payload.payload.metaTx.functionName
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (expected.functionSignature.toLowerCase() !== payload.payload.metaTx.functionSignature.toLowerCase()) {
|
|
247
|
+
return failure(
|
|
248
|
+
7,
|
|
249
|
+
"CALLDATA_MISMATCH",
|
|
250
|
+
"payload.metaTx.functionSignature",
|
|
251
|
+
expected.functionSignature,
|
|
252
|
+
payload.payload.metaTx.functionSignature
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
function checkTokenAuthRules(payload, requirements, nowSec) {
|
|
258
|
+
const strategy = payload.payload.tokenAuthStrategy;
|
|
259
|
+
const tokenAuth = payload.payload.tokenAuth;
|
|
260
|
+
const now = nowSec ?? Math.floor(Date.now() / 1e3);
|
|
261
|
+
const horizon = now + requirements.maxTimeoutSeconds;
|
|
262
|
+
if (strategy === "none") {
|
|
263
|
+
if (tokenAuth !== void 0) {
|
|
264
|
+
return failure(
|
|
265
|
+
9,
|
|
266
|
+
"TOKEN_AUTH_UNEXPECTED",
|
|
267
|
+
"payload.tokenAuth",
|
|
268
|
+
"undefined (strategy=none)",
|
|
269
|
+
tokenAuth.kind
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
if (tokenAuth === void 0) {
|
|
275
|
+
return failure(
|
|
276
|
+
9,
|
|
277
|
+
"TOKEN_AUTH_MISSING",
|
|
278
|
+
"payload.tokenAuth",
|
|
279
|
+
`<${strategy} authorization>`,
|
|
280
|
+
"undefined"
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (tokenAuth.kind !== strategy) {
|
|
284
|
+
return failure(9, "TOKEN_AUTH_MISSING", "payload.tokenAuth.kind", strategy, tokenAuth.kind);
|
|
285
|
+
}
|
|
286
|
+
switch (tokenAuth.kind) {
|
|
287
|
+
case "erc3009": {
|
|
288
|
+
if (tokenAuth.data.value !== requirements.amount) {
|
|
289
|
+
return failure(
|
|
290
|
+
9,
|
|
291
|
+
"TOKEN_AUTH_AMOUNT_MISMATCH",
|
|
292
|
+
"payload.tokenAuth.data.value",
|
|
293
|
+
requirements.amount,
|
|
294
|
+
tokenAuth.data.value
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (tokenAuth.data.to.toLowerCase() !== requirements.escrowAddress.toLowerCase()) {
|
|
298
|
+
return failure(
|
|
299
|
+
9,
|
|
300
|
+
"TOKEN_AUTH_RECIPIENT_MISMATCH",
|
|
301
|
+
"payload.tokenAuth.data.to",
|
|
302
|
+
requirements.escrowAddress,
|
|
303
|
+
tokenAuth.data.to
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
if (tokenAuth.data.validBefore > horizon) {
|
|
307
|
+
return failure(
|
|
308
|
+
9,
|
|
309
|
+
"TOKEN_AUTH_DEADLINE_EXCEEDED",
|
|
310
|
+
"payload.tokenAuth.data.validBefore",
|
|
311
|
+
`<= ${horizon}`,
|
|
312
|
+
tokenAuth.data.validBefore
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
case "permit": {
|
|
318
|
+
if (tokenAuth.data.value !== requirements.amount) {
|
|
319
|
+
return failure(
|
|
320
|
+
10,
|
|
321
|
+
"TOKEN_AUTH_AMOUNT_MISMATCH",
|
|
322
|
+
"payload.tokenAuth.data.value",
|
|
323
|
+
requirements.amount,
|
|
324
|
+
tokenAuth.data.value
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (tokenAuth.data.spender.toLowerCase() !== requirements.escrowAddress.toLowerCase()) {
|
|
328
|
+
return failure(
|
|
329
|
+
10,
|
|
330
|
+
"TOKEN_AUTH_SPENDER_MISMATCH",
|
|
331
|
+
"payload.tokenAuth.data.spender",
|
|
332
|
+
requirements.escrowAddress,
|
|
333
|
+
tokenAuth.data.spender
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
if (tokenAuth.data.deadline > horizon) {
|
|
337
|
+
return failure(
|
|
338
|
+
10,
|
|
339
|
+
"TOKEN_AUTH_DEADLINE_EXCEEDED",
|
|
340
|
+
"payload.tokenAuth.data.deadline",
|
|
341
|
+
`<= ${horizon}`,
|
|
342
|
+
tokenAuth.data.deadline
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
case "permit2": {
|
|
348
|
+
if (tokenAuth.data.permitted.amount !== requirements.amount) {
|
|
349
|
+
return failure(
|
|
350
|
+
11,
|
|
351
|
+
"TOKEN_AUTH_AMOUNT_MISMATCH",
|
|
352
|
+
"payload.tokenAuth.data.permitted.amount",
|
|
353
|
+
requirements.amount,
|
|
354
|
+
tokenAuth.data.permitted.amount
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
if (tokenAuth.data.permitted.token.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
358
|
+
return failure(
|
|
359
|
+
11,
|
|
360
|
+
"TOKEN_AUTH_TOKEN_MISMATCH",
|
|
361
|
+
"payload.tokenAuth.data.permitted.token",
|
|
362
|
+
requirements.asset,
|
|
363
|
+
tokenAuth.data.permitted.token
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
if (tokenAuth.data.spender.toLowerCase() !== requirements.escrowAddress.toLowerCase()) {
|
|
367
|
+
return failure(
|
|
368
|
+
11,
|
|
369
|
+
"TOKEN_AUTH_SPENDER_MISMATCH",
|
|
370
|
+
"payload.tokenAuth.data.spender",
|
|
371
|
+
requirements.escrowAddress,
|
|
372
|
+
tokenAuth.data.spender
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
if (tokenAuth.data.deadline > horizon) {
|
|
376
|
+
return failure(
|
|
377
|
+
11,
|
|
378
|
+
"TOKEN_AUTH_DEADLINE_EXCEEDED",
|
|
379
|
+
"payload.tokenAuth.data.deadline",
|
|
380
|
+
`<= ${horizon}`,
|
|
381
|
+
tokenAuth.data.deadline
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
default:
|
|
387
|
+
return failure(
|
|
388
|
+
9,
|
|
389
|
+
"TOKEN_AUTH_UNKNOWN_STRATEGY",
|
|
390
|
+
"payload.tokenAuth.kind",
|
|
391
|
+
"erc3009 | permit | permit2",
|
|
392
|
+
tokenAuth.kind
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function checkFulfillment(payload, requirements, validator) {
|
|
397
|
+
const required = requirements.fulfillment?.required === true;
|
|
398
|
+
const carried = payload.fulfillment;
|
|
399
|
+
if (carried === void 0) {
|
|
400
|
+
return required ? failure(13, "FULFILLMENT_REQUIRED", "payload.fulfillment", "<option>", "undefined") : null;
|
|
401
|
+
}
|
|
402
|
+
const advertised = requirements.fulfillment?.options ?? [];
|
|
403
|
+
const matched = advertised.find((option) => option.id === carried.option);
|
|
404
|
+
if (!matched) {
|
|
405
|
+
return failure(
|
|
406
|
+
13,
|
|
407
|
+
"FULFILLMENT_OPTION_NOT_ADVERTISED",
|
|
408
|
+
"payload.fulfillment.option",
|
|
409
|
+
advertised.map((o) => o.id),
|
|
410
|
+
carried.option
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const action = payload.payload.action;
|
|
414
|
+
if (action === "boson-createOfferCommitAndRedeem") {
|
|
415
|
+
if (carried.data === void 0) {
|
|
416
|
+
return failure(
|
|
417
|
+
13,
|
|
418
|
+
"FULFILLMENT_DATA_REQUIRED",
|
|
419
|
+
"payload.fulfillment.data",
|
|
420
|
+
matched.schema ?? "null",
|
|
421
|
+
"undefined",
|
|
422
|
+
"atomic createOfferCommitAndRedeem requires fulfillment.data at commit time"
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
if (validator !== void 0) {
|
|
426
|
+
const result = validator(matched.id, carried.data);
|
|
427
|
+
if (!result.ok) {
|
|
428
|
+
return failure(
|
|
429
|
+
13,
|
|
430
|
+
"FULFILLMENT_DATA_INVALID",
|
|
431
|
+
"payload.fulfillment.data",
|
|
432
|
+
matched.schema,
|
|
433
|
+
carried.data,
|
|
434
|
+
result.reason
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
if (carried.data !== void 0) {
|
|
441
|
+
return failure(
|
|
442
|
+
13,
|
|
443
|
+
"FULFILLMENT_DATA_UNEXPECTED",
|
|
444
|
+
"payload.fulfillment.data",
|
|
445
|
+
"undefined",
|
|
446
|
+
carried.data,
|
|
447
|
+
"two-step boson-createOfferAndCommit defers fulfillment.data to the redeem POST body"
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
function deepEqual(a, b) {
|
|
453
|
+
if (a === b) return true;
|
|
454
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
455
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
456
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
457
|
+
if (a.length !== b.length) return false;
|
|
458
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
459
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
460
|
+
}
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
const ao = a;
|
|
464
|
+
const bo = b;
|
|
465
|
+
const aKeys = Object.keys(ao);
|
|
466
|
+
const bKeys = Object.keys(bo);
|
|
467
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
468
|
+
for (const k of aKeys) {
|
|
469
|
+
if (!Object.prototype.hasOwnProperty.call(bo, k)) return false;
|
|
470
|
+
if (!deepEqual(ao[k], bo[k])) return false;
|
|
471
|
+
}
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
function encodeRsv(sig) {
|
|
475
|
+
if (sig.v !== 0 && sig.v !== 1 && sig.v !== 27 && sig.v !== 28) {
|
|
476
|
+
throw new Error("signature v must be 0, 1, 27, or 28");
|
|
477
|
+
}
|
|
478
|
+
const r = sig.r.replace(/^0x/, "").padStart(64, "0");
|
|
479
|
+
const s = sig.s.replace(/^0x/, "").padStart(64, "0");
|
|
480
|
+
const normalizedV = sig.v === 0 || sig.v === 1 ? sig.v + 27 : sig.v;
|
|
481
|
+
const v = normalizedV.toString(16).padStart(2, "0");
|
|
482
|
+
return `0x${r}${s}${v}`;
|
|
483
|
+
}
|
|
484
|
+
function failure(rule, code, field, expected, got, reason) {
|
|
485
|
+
const out = { ok: false, rule, code };
|
|
486
|
+
if (field !== void 0) out.field = field;
|
|
487
|
+
if (expected !== void 0) out.expected = expected;
|
|
488
|
+
if (got !== void 0) out.got = got;
|
|
489
|
+
if (reason !== void 0) out.reason = reason;
|
|
490
|
+
return out;
|
|
491
|
+
}
|
|
492
|
+
function verifyExchangeSnapshot(snapshot, expected) {
|
|
493
|
+
if (snapshot === null) {
|
|
494
|
+
return { ok: false, code: "EXCHANGE_NOT_FOUND" };
|
|
495
|
+
}
|
|
496
|
+
if (snapshot.state !== expected.state) {
|
|
497
|
+
return {
|
|
498
|
+
ok: false,
|
|
499
|
+
code: "STATE_MISMATCH",
|
|
500
|
+
field: "state",
|
|
501
|
+
expected: expected.state,
|
|
502
|
+
got: snapshot.state
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
if (expected.state === x402Actions.ExchangeState.DISPUTED) {
|
|
506
|
+
if (snapshot.disputeState !== expected.disputeState) {
|
|
507
|
+
return {
|
|
508
|
+
ok: false,
|
|
509
|
+
code: "DISPUTE_STATE_MISMATCH",
|
|
510
|
+
field: "disputeState",
|
|
511
|
+
expected: expected.disputeState,
|
|
512
|
+
got: snapshot.disputeState
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (snapshot.seller.toLowerCase() !== expected.seller.toLowerCase()) {
|
|
517
|
+
return {
|
|
518
|
+
ok: false,
|
|
519
|
+
code: "SELLER_MISMATCH",
|
|
520
|
+
field: "seller",
|
|
521
|
+
expected: expected.seller,
|
|
522
|
+
got: snapshot.seller
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
if (snapshot.exchangeToken.toLowerCase() !== expected.exchangeToken.toLowerCase()) {
|
|
526
|
+
return {
|
|
527
|
+
ok: false,
|
|
528
|
+
code: "TOKEN_MISMATCH",
|
|
529
|
+
field: "exchangeToken",
|
|
530
|
+
expected: expected.exchangeToken,
|
|
531
|
+
got: snapshot.exchangeToken
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
if (snapshot.price !== expected.price) {
|
|
535
|
+
return {
|
|
536
|
+
ok: false,
|
|
537
|
+
code: "PRICE_MISMATCH",
|
|
538
|
+
field: "price",
|
|
539
|
+
expected: expected.price,
|
|
540
|
+
got: snapshot.price
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
return { ok: true };
|
|
544
|
+
}
|
|
545
|
+
async function verifyExchange(reader, exchangeId, expected, options = {}) {
|
|
546
|
+
const attempts = Math.max(1, Math.floor(options.attempts ?? 3));
|
|
547
|
+
const delayMs = Math.max(0, Math.floor(options.delayMs ?? 50));
|
|
548
|
+
let result = { ok: false, code: "EXCHANGE_NOT_FOUND" };
|
|
549
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
550
|
+
const snapshot = await reader.read(exchangeId);
|
|
551
|
+
result = verifyExchangeSnapshot(snapshot, expected);
|
|
552
|
+
if (result.ok || !isRetryableVerifyResult(result) || attempt === attempts) {
|
|
553
|
+
return result;
|
|
554
|
+
}
|
|
555
|
+
if (delayMs > 0) {
|
|
556
|
+
await delay(delayMs);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
function isRetryableVerifyResult(result) {
|
|
562
|
+
return !result.ok && (result.code === "EXCHANGE_NOT_FOUND" || result.code === "STATE_MISMATCH" || result.code === "DISPUTE_STATE_MISMATCH");
|
|
563
|
+
}
|
|
564
|
+
async function delay(ms) {
|
|
565
|
+
await new Promise((resolve) => {
|
|
566
|
+
setTimeout(resolve, ms);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/facilitator/errors.ts
|
|
571
|
+
var FacilitatorHttpError = class extends Error {
|
|
572
|
+
constructor(message, init) {
|
|
573
|
+
super(message, init.cause !== void 0 ? { cause: init.cause } : void 0);
|
|
574
|
+
this.name = "FacilitatorHttpError";
|
|
575
|
+
this.code = init.code;
|
|
576
|
+
if (init.status !== void 0) this.status = init.status;
|
|
577
|
+
if (init.facilitatorCode !== void 0) this.facilitatorCode = init.facilitatorCode;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// src/handlers/commit-and-redeem.ts
|
|
582
|
+
async function handleCommit(input, ctx) {
|
|
583
|
+
return await handleCommitImpl(input, ctx, {
|
|
584
|
+
expectedAction: "boson-createOfferAndCommit",
|
|
585
|
+
expectedState: x402Actions.ExchangeState.COMMITTED
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
async function handleCommitAndRedeem(input, ctx) {
|
|
589
|
+
return await handleCommitImpl(input, ctx, {
|
|
590
|
+
expectedAction: "boson-createOfferCommitAndRedeem",
|
|
591
|
+
expectedState: x402Actions.ExchangeState.REDEEMED
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
async function handleCommitImpl(input, ctx, expected) {
|
|
595
|
+
const decoded = decodeXPaymentHeader(input.paymentHeader);
|
|
596
|
+
if (!decoded.ok) {
|
|
597
|
+
const status = decoded.code === "MISSING_HEADER" ? 402 : 400;
|
|
598
|
+
return handlerErr(status, decoded.code, decoded.reason);
|
|
599
|
+
}
|
|
600
|
+
if (decoded.payload.payload.action !== expected.expectedAction) {
|
|
601
|
+
return handlerErr(
|
|
602
|
+
400,
|
|
603
|
+
"ACTION_ROUTE_MISMATCH",
|
|
604
|
+
`handler expected action ${expected.expectedAction}, got ${decoded.payload.payload.action}`,
|
|
605
|
+
{
|
|
606
|
+
expected: expected.expectedAction,
|
|
607
|
+
got: decoded.payload.payload.action
|
|
608
|
+
}
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
const channels = ctx.config.fulfillmentChannels ?? [];
|
|
612
|
+
const channelById = new Map(channels.map((c) => [c.id, c]));
|
|
613
|
+
const validation = await validatePaymentPayload({
|
|
614
|
+
payload: decoded.payload,
|
|
615
|
+
requirements: input.requirements,
|
|
616
|
+
chainId: ctx.config.chainId,
|
|
617
|
+
validateFulfillmentData: (option, data) => {
|
|
618
|
+
const channel = channelById.get(option);
|
|
619
|
+
if (channel === void 0) {
|
|
620
|
+
return {
|
|
621
|
+
ok: false,
|
|
622
|
+
reason: `fulfillment.option '${option}' has no registered channel adapter on this server`
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
return channel.validate(data);
|
|
627
|
+
} catch (e) {
|
|
628
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
if (!validation.ok) {
|
|
633
|
+
return handlerErr(400, validation.code, validation.reason ?? `rule ${validation.rule} failed`, {
|
|
634
|
+
rule: validation.rule,
|
|
635
|
+
field: validation.field,
|
|
636
|
+
expected: validation.expected,
|
|
637
|
+
got: validation.got
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
let settleResult;
|
|
641
|
+
try {
|
|
642
|
+
settleResult = await ctx.facilitator.settle({
|
|
643
|
+
scheme: "escrow",
|
|
644
|
+
network: input.requirements.network,
|
|
645
|
+
payload: decoded.payload,
|
|
646
|
+
requirements: input.requirements
|
|
647
|
+
});
|
|
648
|
+
} catch (e) {
|
|
649
|
+
if (e instanceof FacilitatorHttpError) {
|
|
650
|
+
return handlerErr(502, "FACILITATOR_UNREACHABLE", e.message, {
|
|
651
|
+
code: e.code,
|
|
652
|
+
status: e.status,
|
|
653
|
+
facilitatorCode: e.facilitatorCode
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
throw e;
|
|
657
|
+
}
|
|
658
|
+
if (!settleResult.ok) {
|
|
659
|
+
return handlerErr(502, "FACILITATOR_REJECTED", settleResult.reason, {
|
|
660
|
+
facilitatorCode: settleResult.code
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
const verifyResult = await verifyExchange(
|
|
664
|
+
ctx.exchangeReader,
|
|
665
|
+
settleResult.exchangeId,
|
|
666
|
+
buildExpectedFromRequirements(input.requirements, expected.expectedState)
|
|
667
|
+
);
|
|
668
|
+
if (!verifyResult.ok) {
|
|
669
|
+
return handlerErr(
|
|
670
|
+
502,
|
|
671
|
+
`STATE_VERIFY_${verifyResult.code}`,
|
|
672
|
+
"post-settle state verification failed",
|
|
673
|
+
{
|
|
674
|
+
exchangeId: settleResult.exchangeId,
|
|
675
|
+
txHash: settleResult.txHash,
|
|
676
|
+
field: verifyResult.field,
|
|
677
|
+
expected: verifyResult.expected,
|
|
678
|
+
got: verifyResult.got
|
|
679
|
+
}
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
if (expected.expectedState === x402Actions.ExchangeState.COMMITTED) {
|
|
683
|
+
ctx.exchangeFulfillmentOptionStore.set(
|
|
684
|
+
settleResult.exchangeId,
|
|
685
|
+
input.requirements.fulfillment?.options.map((option) => option.id) ?? []
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
const warnings = [];
|
|
689
|
+
if (expected.expectedState === x402Actions.ExchangeState.REDEEMED && decoded.payload.fulfillment !== void 0 && decoded.payload.fulfillment.data !== void 0) {
|
|
690
|
+
const pending = {
|
|
691
|
+
exchangeId: settleResult.exchangeId,
|
|
692
|
+
option: decoded.payload.fulfillment.option,
|
|
693
|
+
data: decoded.payload.fulfillment.data,
|
|
694
|
+
redeemer: decoded.payload.payload.buyer,
|
|
695
|
+
recordedAt: Date.now()
|
|
696
|
+
};
|
|
697
|
+
ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, pending);
|
|
698
|
+
const channel = channelById.get(decoded.payload.fulfillment.option);
|
|
699
|
+
if (channel === void 0) {
|
|
700
|
+
const reason = "no channel adapter is registered";
|
|
701
|
+
ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, {
|
|
702
|
+
...pending,
|
|
703
|
+
error: reason
|
|
704
|
+
});
|
|
705
|
+
warnings.push({
|
|
706
|
+
code: "FULFILLMENT_COMMIT_DEFERRED",
|
|
707
|
+
reason: "atomic redeem succeeded on-chain, but no channel adapter is registered",
|
|
708
|
+
details: {
|
|
709
|
+
exchangeId: settleResult.exchangeId,
|
|
710
|
+
option: decoded.payload.fulfillment.option,
|
|
711
|
+
error: reason
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
} else {
|
|
715
|
+
try {
|
|
716
|
+
await channel.onCommit(settleResult.exchangeId, decoded.payload.fulfillment.data);
|
|
717
|
+
ctx.fulfillmentRecoveryStore.delete(settleResult.exchangeId);
|
|
718
|
+
} catch (e) {
|
|
719
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
720
|
+
ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, {
|
|
721
|
+
...pending,
|
|
722
|
+
error: reason
|
|
723
|
+
});
|
|
724
|
+
warnings.push({
|
|
725
|
+
code: "FULFILLMENT_COMMIT_DEFERRED",
|
|
726
|
+
reason: "atomic redeem succeeded on-chain, but the channel adapter rejected the data",
|
|
727
|
+
details: {
|
|
728
|
+
exchangeId: settleResult.exchangeId,
|
|
729
|
+
option: decoded.payload.fulfillment.option,
|
|
730
|
+
error: reason
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const nextActions = emitNextActions(
|
|
737
|
+
{
|
|
738
|
+
exchangeId: settleResult.exchangeId,
|
|
739
|
+
exchangeState: expected.expectedState
|
|
740
|
+
},
|
|
741
|
+
ctx.config.channelRegistry,
|
|
742
|
+
ctx.config.facilitator.url
|
|
743
|
+
);
|
|
744
|
+
return handlerOk({
|
|
745
|
+
exchangeId: settleResult.exchangeId,
|
|
746
|
+
txHash: settleResult.txHash,
|
|
747
|
+
nextActions,
|
|
748
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
function buildExpectedFromRequirements(requirements, state) {
|
|
752
|
+
return {
|
|
753
|
+
state,
|
|
754
|
+
seller: requirements.offer.creator,
|
|
755
|
+
exchangeToken: requirements.asset,
|
|
756
|
+
price: requirements.amount
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
async function handlePerformAction(action, input, ctx) {
|
|
760
|
+
const reference = await ctx.exchangeReader.read(input.exchangeId);
|
|
761
|
+
if (reference === null) {
|
|
762
|
+
return handlerErr(
|
|
763
|
+
502,
|
|
764
|
+
"STATE_VERIFY_EXCHANGE_NOT_FOUND",
|
|
765
|
+
"pre-action exchange reference could not be read",
|
|
766
|
+
{ action, exchangeId: input.exchangeId }
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
let result;
|
|
770
|
+
try {
|
|
771
|
+
result = await ctx.facilitator.performAction({
|
|
772
|
+
network: ctx.config.network,
|
|
773
|
+
escrowAddress: ctx.config.escrow,
|
|
774
|
+
exchangeId: input.exchangeId,
|
|
775
|
+
action,
|
|
776
|
+
signedPayload: input.signedPayload
|
|
777
|
+
});
|
|
778
|
+
} catch (e) {
|
|
779
|
+
if (e instanceof FacilitatorHttpError) {
|
|
780
|
+
return handlerErr(502, "FACILITATOR_UNREACHABLE", e.message, {
|
|
781
|
+
code: e.code,
|
|
782
|
+
status: e.status,
|
|
783
|
+
facilitatorCode: e.facilitatorCode
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
throw e;
|
|
787
|
+
}
|
|
788
|
+
if (!result.ok) {
|
|
789
|
+
return handlerErr(502, "FACILITATOR_REJECTED", result.reason, {
|
|
790
|
+
facilitatorCode: result.code
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
const postState = stateMachine.ACTION_POST_STATE[action];
|
|
794
|
+
const expected = {
|
|
795
|
+
state: postState.exchange,
|
|
796
|
+
...postState.dispute !== void 0 ? { disputeState: postState.dispute } : {},
|
|
797
|
+
seller: reference.seller,
|
|
798
|
+
exchangeToken: reference.exchangeToken,
|
|
799
|
+
price: reference.price
|
|
800
|
+
};
|
|
801
|
+
const verifyResult = await verifyExchange(ctx.exchangeReader, input.exchangeId, expected);
|
|
802
|
+
if (!verifyResult.ok) {
|
|
803
|
+
return handlerErr(
|
|
804
|
+
502,
|
|
805
|
+
`STATE_VERIFY_${verifyResult.code}`,
|
|
806
|
+
"post-action state verification failed",
|
|
807
|
+
{
|
|
808
|
+
action,
|
|
809
|
+
exchangeId: input.exchangeId,
|
|
810
|
+
txHash: result.txHash,
|
|
811
|
+
field: verifyResult.field,
|
|
812
|
+
expected: verifyResult.expected,
|
|
813
|
+
got: verifyResult.got
|
|
814
|
+
}
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
const nextActions = postState.exchange === x402Actions.ExchangeState.DISPUTED && postState.dispute !== void 0 ? emitNextActions(
|
|
818
|
+
{
|
|
819
|
+
exchangeId: input.exchangeId,
|
|
820
|
+
exchangeState: x402Actions.ExchangeState.DISPUTED,
|
|
821
|
+
disputeState: postState.dispute
|
|
822
|
+
},
|
|
823
|
+
ctx.config.channelRegistry,
|
|
824
|
+
ctx.config.facilitator.url
|
|
825
|
+
) : emitNextActions(
|
|
826
|
+
{
|
|
827
|
+
exchangeId: input.exchangeId,
|
|
828
|
+
exchangeState: postState.exchange
|
|
829
|
+
},
|
|
830
|
+
ctx.config.channelRegistry,
|
|
831
|
+
ctx.config.facilitator.url
|
|
832
|
+
);
|
|
833
|
+
return handlerOk({ txHash: result.txHash, nextActions });
|
|
834
|
+
}
|
|
835
|
+
async function handleRedeem(input, ctx) {
|
|
836
|
+
let redeemer;
|
|
837
|
+
try {
|
|
838
|
+
redeemer = x402Evm.decodeSignedPayload(input.signedPayload).from;
|
|
839
|
+
} catch (e) {
|
|
840
|
+
return handlerErr(
|
|
841
|
+
400,
|
|
842
|
+
"SIGNED_PAYLOAD_DECODE_FAILED",
|
|
843
|
+
e instanceof Error ? e.message : "signedPayload could not be decoded"
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
let resolvedChannel;
|
|
847
|
+
if (input.fulfillment !== void 0) {
|
|
848
|
+
const advertisedOptions = ctx.exchangeFulfillmentOptionStore.get(input.exchangeId);
|
|
849
|
+
if (advertisedOptions !== void 0 && !advertisedOptions.includes(input.fulfillment.option)) {
|
|
850
|
+
return handlerErr(
|
|
851
|
+
400,
|
|
852
|
+
"FULFILLMENT_OPTION_NOT_ADVERTISED",
|
|
853
|
+
`fulfillment.option '${input.fulfillment.option}' was not advertised for this exchange`,
|
|
854
|
+
{ option: input.fulfillment.option, advertised: advertisedOptions }
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
const channels = ctx.config.fulfillmentChannels;
|
|
858
|
+
if (channels === void 0) {
|
|
859
|
+
return handlerErr(
|
|
860
|
+
400,
|
|
861
|
+
"FULFILLMENT_CHANNELS_NOT_CONFIGURED",
|
|
862
|
+
"server received redeem-time fulfillment data but has no fulfillmentChannels registered"
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
const channel = channels.find((c) => c.id === input.fulfillment.option);
|
|
866
|
+
if (channel === void 0) {
|
|
867
|
+
return handlerErr(
|
|
868
|
+
400,
|
|
869
|
+
"FULFILLMENT_OPTION_UNKNOWN",
|
|
870
|
+
`fulfillment.option '${input.fulfillment.option}' is not registered with the server`,
|
|
871
|
+
{ option: input.fulfillment.option, registered: channels.map((c) => c.id) }
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
let validation;
|
|
875
|
+
try {
|
|
876
|
+
validation = channel.validate(input.fulfillment.data);
|
|
877
|
+
} catch (e) {
|
|
878
|
+
return handlerErr(400, "FULFILLMENT_DATA_INVALID", errorMessage(e), {
|
|
879
|
+
option: input.fulfillment.option
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
if (!validation.ok) {
|
|
883
|
+
return handlerErr(400, "FULFILLMENT_DATA_INVALID", validation.reason, {
|
|
884
|
+
option: input.fulfillment.option
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
resolvedChannel = channel;
|
|
888
|
+
}
|
|
889
|
+
const result = await handlePerformAction("boson-redeem", input, ctx);
|
|
890
|
+
if (!result.ok) return result;
|
|
891
|
+
let warning;
|
|
892
|
+
if (resolvedChannel !== void 0 && input.fulfillment !== void 0) {
|
|
893
|
+
const pending = {
|
|
894
|
+
exchangeId: input.exchangeId,
|
|
895
|
+
option: input.fulfillment.option,
|
|
896
|
+
data: input.fulfillment.data,
|
|
897
|
+
redeemer,
|
|
898
|
+
recordedAt: Date.now()
|
|
899
|
+
};
|
|
900
|
+
ctx.fulfillmentRecoveryStore.set(input.exchangeId, pending);
|
|
901
|
+
try {
|
|
902
|
+
await resolvedChannel.onCommit(input.exchangeId, input.fulfillment.data);
|
|
903
|
+
ctx.fulfillmentRecoveryStore.delete(input.exchangeId);
|
|
904
|
+
} catch (e) {
|
|
905
|
+
const reason = errorMessage(e);
|
|
906
|
+
ctx.fulfillmentRecoveryStore.set(input.exchangeId, { ...pending, error: reason });
|
|
907
|
+
warning = {
|
|
908
|
+
code: "FULFILLMENT_UPDATE_DEFERRED",
|
|
909
|
+
reason: "redeem succeeded on-chain, but the server could not persist the fulfillment update",
|
|
910
|
+
details: {
|
|
911
|
+
exchangeId: input.exchangeId,
|
|
912
|
+
option: input.fulfillment.option,
|
|
913
|
+
error: reason
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
ctx.exchangeFulfillmentOptionStore.delete(input.exchangeId);
|
|
919
|
+
if (warning !== void 0) {
|
|
920
|
+
return {
|
|
921
|
+
...result,
|
|
922
|
+
body: {
|
|
923
|
+
...result.body,
|
|
924
|
+
warnings: [...result.body.warnings ?? [], warning]
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
return result;
|
|
929
|
+
}
|
|
930
|
+
function errorMessage(e) {
|
|
931
|
+
return e instanceof Error ? e.message : String(e);
|
|
932
|
+
}
|
|
933
|
+
var handleComplete = (input, ctx) => handlePerformAction("boson-completeExchange", input, ctx);
|
|
934
|
+
var handleDisputeRaise = (input, ctx) => handlePerformAction("boson-raiseDispute", input, ctx);
|
|
935
|
+
var handleDisputeResolve = (input, ctx) => handlePerformAction("boson-resolveDispute", input, ctx);
|
|
936
|
+
var handleDisputeRetract = (input, ctx) => handlePerformAction("boson-retractDispute", input, ctx);
|
|
937
|
+
var handleDisputeEscalate = (input, ctx) => handlePerformAction("boson-escalateDispute", input, ctx);
|
|
938
|
+
var DECIMAL_UINT_RE = escrow.DECIMAL_UINT;
|
|
939
|
+
var ADDRESS_RE = escrow.ADDRESS;
|
|
940
|
+
var HEX_BYTES_RE = /^0x([0-9a-fA-F]{2})*$/;
|
|
941
|
+
|
|
942
|
+
// src/handlers/resolve-entity.ts
|
|
943
|
+
async function resolveEntityId(coreSdk, input) {
|
|
944
|
+
const address = input.address.toLowerCase();
|
|
945
|
+
let sellers = [];
|
|
946
|
+
let buyers = [];
|
|
947
|
+
try {
|
|
948
|
+
[sellers, buyers] = await Promise.all([
|
|
949
|
+
input.role === "buyer" ? Promise.resolve([]) : coreSdk.getSellersByAddress(address),
|
|
950
|
+
input.role === "seller" ? Promise.resolve([]) : coreSdk.getBuyers({ buyersFilter: { wallet: address } })
|
|
951
|
+
]);
|
|
952
|
+
} catch (e) {
|
|
953
|
+
return {
|
|
954
|
+
ok: false,
|
|
955
|
+
code: "SUBGRAPH_FAILURE",
|
|
956
|
+
reason: e instanceof Error ? e.message : "subgraph lookup failed"
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
const sellerIds = sellers.map((s) => s.id);
|
|
960
|
+
const buyerIds = buyers.map((b) => b.id);
|
|
961
|
+
if (input.role === "seller") {
|
|
962
|
+
if (sellerIds.length === 0) {
|
|
963
|
+
return {
|
|
964
|
+
ok: false,
|
|
965
|
+
code: "NOT_FOUND",
|
|
966
|
+
reason: `no seller entity found for address ${address}`
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
if (sellerIds.length > 1) {
|
|
970
|
+
return {
|
|
971
|
+
ok: false,
|
|
972
|
+
code: "AMBIGUOUS",
|
|
973
|
+
reason: `address ${address} resolves to multiple seller entities (ids ${sellerIds.join(", ")}); pass entityId to disambiguate`,
|
|
974
|
+
sellerIds
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
return { ok: true, entityId: sellerIds[0], role: "seller" };
|
|
978
|
+
}
|
|
979
|
+
if (input.role === "buyer") {
|
|
980
|
+
if (buyerIds.length === 0) {
|
|
981
|
+
return {
|
|
982
|
+
ok: false,
|
|
983
|
+
code: "NOT_FOUND",
|
|
984
|
+
reason: `no buyer entity found for address ${address}`
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
if (buyerIds.length > 1) {
|
|
988
|
+
return {
|
|
989
|
+
ok: false,
|
|
990
|
+
code: "AMBIGUOUS",
|
|
991
|
+
reason: `address ${address} resolves to multiple buyer entities (ids ${buyerIds.join(", ")}); pass entityId to disambiguate`,
|
|
992
|
+
buyerIds
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
return { ok: true, entityId: buyerIds[0], role: "buyer" };
|
|
996
|
+
}
|
|
997
|
+
const totalMatches = sellerIds.length + buyerIds.length;
|
|
998
|
+
if (totalMatches === 0) {
|
|
999
|
+
return {
|
|
1000
|
+
ok: false,
|
|
1001
|
+
code: "NOT_FOUND",
|
|
1002
|
+
reason: `no seller or buyer entity found for address ${address}`
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
if (totalMatches > 1) {
|
|
1006
|
+
return {
|
|
1007
|
+
ok: false,
|
|
1008
|
+
code: "AMBIGUOUS",
|
|
1009
|
+
reason: `address ${address} resolves to ${describeMatches(sellerIds, buyerIds)}; pass role and/or entityId to disambiguate`,
|
|
1010
|
+
...sellerIds.length > 0 ? { sellerIds } : {},
|
|
1011
|
+
...buyerIds.length > 0 ? { buyerIds } : {}
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
if (sellerIds.length === 1) return { ok: true, entityId: sellerIds[0], role: "seller" };
|
|
1015
|
+
return { ok: true, entityId: buyerIds[0], role: "buyer" };
|
|
1016
|
+
}
|
|
1017
|
+
function describeMatches(sellerIds, buyerIds) {
|
|
1018
|
+
const parts = [];
|
|
1019
|
+
if (sellerIds.length > 0) {
|
|
1020
|
+
parts.push(
|
|
1021
|
+
sellerIds.length === 1 ? `seller (id ${sellerIds[0]})` : `${sellerIds.length} sellers (ids ${sellerIds.join(", ")})`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
if (buyerIds.length > 0) {
|
|
1025
|
+
parts.push(
|
|
1026
|
+
buyerIds.length === 1 ? `buyer (id ${buyerIds[0]})` : `${buyerIds.length} buyers (ids ${buyerIds.join(", ")})`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
return parts.join(" and ");
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/handlers/withdraw-funds.ts
|
|
1033
|
+
async function handleWithdrawFunds(input, ctx) {
|
|
1034
|
+
let entityId;
|
|
1035
|
+
let role;
|
|
1036
|
+
if ("entityId" in input) {
|
|
1037
|
+
if (!DECIMAL_UINT_RE.test(input.entityId)) {
|
|
1038
|
+
return handlerErr(
|
|
1039
|
+
400,
|
|
1040
|
+
"INVALID_ENTITY_ID",
|
|
1041
|
+
`entityId must be a decimal uint256 string, got "${input.entityId}"`
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
entityId = input.entityId;
|
|
1045
|
+
} else {
|
|
1046
|
+
if (!ADDRESS_RE.test(input.address)) {
|
|
1047
|
+
return handlerErr(
|
|
1048
|
+
400,
|
|
1049
|
+
"INVALID_ADDRESS",
|
|
1050
|
+
`address must be a 20-byte 0x-prefixed hex string, got "${input.address}"`
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
const resolved = await resolveEntityId(ctx.coreSdkRead, {
|
|
1054
|
+
address: input.address,
|
|
1055
|
+
...input.role !== void 0 ? { role: input.role } : {}
|
|
1056
|
+
});
|
|
1057
|
+
if (!resolved.ok) {
|
|
1058
|
+
const status = resolved.code === "NOT_FOUND" ? 404 : resolved.code === "AMBIGUOUS" ? 409 : 502;
|
|
1059
|
+
const details = resolved.code === "AMBIGUOUS" ? {
|
|
1060
|
+
...resolved.sellerIds !== void 0 ? { sellerIds: resolved.sellerIds } : {},
|
|
1061
|
+
...resolved.buyerIds !== void 0 ? { buyerIds: resolved.buyerIds } : {}
|
|
1062
|
+
} : void 0;
|
|
1063
|
+
return handlerErr(status, resolved.code, resolved.reason, details);
|
|
1064
|
+
}
|
|
1065
|
+
entityId = resolved.entityId;
|
|
1066
|
+
role = resolved.role;
|
|
1067
|
+
}
|
|
1068
|
+
let result;
|
|
1069
|
+
try {
|
|
1070
|
+
result = await ctx.facilitator.performAction({
|
|
1071
|
+
network: ctx.config.network,
|
|
1072
|
+
escrowAddress: ctx.config.escrow,
|
|
1073
|
+
entityId,
|
|
1074
|
+
action: "boson-withdrawFunds",
|
|
1075
|
+
signedPayload: input.signedPayload
|
|
1076
|
+
});
|
|
1077
|
+
} catch (e) {
|
|
1078
|
+
if (e instanceof FacilitatorHttpError) {
|
|
1079
|
+
return handlerErr(502, "FACILITATOR_UNREACHABLE", e.message, {
|
|
1080
|
+
code: e.code,
|
|
1081
|
+
status: e.status,
|
|
1082
|
+
facilitatorCode: e.facilitatorCode
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
throw e;
|
|
1086
|
+
}
|
|
1087
|
+
if (!result.ok) {
|
|
1088
|
+
return handlerErr(502, "FACILITATOR_REJECTED", result.reason, {
|
|
1089
|
+
facilitatorCode: result.code
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
return plainHandlerOk({
|
|
1093
|
+
txHash: result.txHash,
|
|
1094
|
+
entityId,
|
|
1095
|
+
...role !== void 0 ? { role } : {}
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// src/handlers/available-funds.ts
|
|
1100
|
+
async function handleGetAvailableFunds(query, ctx) {
|
|
1101
|
+
let entityId;
|
|
1102
|
+
let role;
|
|
1103
|
+
if ("entityId" in query) {
|
|
1104
|
+
if (!DECIMAL_UINT_RE.test(query.entityId)) {
|
|
1105
|
+
return handlerErr(
|
|
1106
|
+
400,
|
|
1107
|
+
"INVALID_ENTITY_ID",
|
|
1108
|
+
`entityId must be a decimal uint256 string, got "${query.entityId}"`
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
entityId = query.entityId;
|
|
1112
|
+
} else {
|
|
1113
|
+
if (!ADDRESS_RE.test(query.address)) {
|
|
1114
|
+
return handlerErr(
|
|
1115
|
+
400,
|
|
1116
|
+
"INVALID_ADDRESS",
|
|
1117
|
+
`address must be a 20-byte 0x-prefixed hex string, got "${query.address}"`
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
const resolved = await resolveEntityId(ctx.coreSdkRead, {
|
|
1121
|
+
address: query.address,
|
|
1122
|
+
...query.role !== void 0 ? { role: query.role } : {}
|
|
1123
|
+
});
|
|
1124
|
+
if (!resolved.ok) {
|
|
1125
|
+
const status = resolved.code === "NOT_FOUND" ? 404 : resolved.code === "AMBIGUOUS" ? 409 : 502;
|
|
1126
|
+
return handlerErr(status, resolved.code, resolved.reason, omitErrorMetadata(resolved));
|
|
1127
|
+
}
|
|
1128
|
+
entityId = resolved.entityId;
|
|
1129
|
+
role = resolved.role;
|
|
1130
|
+
}
|
|
1131
|
+
let raw;
|
|
1132
|
+
try {
|
|
1133
|
+
raw = await ctx.coreSdkRead.getFunds({ fundsFilter: { accountId: entityId } });
|
|
1134
|
+
} catch (e) {
|
|
1135
|
+
return handlerErr(
|
|
1136
|
+
502,
|
|
1137
|
+
"SUBGRAPH_FAILURE",
|
|
1138
|
+
e instanceof Error ? e.message : "subgraph getFunds lookup failed"
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
const funds = raw.map((entry) => ({
|
|
1142
|
+
tokenAddress: entry.token.address,
|
|
1143
|
+
tokenSymbol: entry.token.symbol,
|
|
1144
|
+
tokenName: entry.token.name,
|
|
1145
|
+
decimals: Number(entry.token.decimals),
|
|
1146
|
+
availableAmount: entry.availableAmount
|
|
1147
|
+
}));
|
|
1148
|
+
return plainHandlerOk({
|
|
1149
|
+
entityId,
|
|
1150
|
+
...role !== void 0 ? { role } : {},
|
|
1151
|
+
funds
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
function omitErrorMetadata(resolved) {
|
|
1155
|
+
if (resolved.code !== "AMBIGUOUS") return void 0;
|
|
1156
|
+
const details = {};
|
|
1157
|
+
if (resolved.sellerIds !== void 0) details.sellerIds = resolved.sellerIds;
|
|
1158
|
+
if (resolved.buyerIds !== void 0) details.buyerIds = resolved.buyerIds;
|
|
1159
|
+
return Object.keys(details).length > 0 ? details : void 0;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
exports.ADDRESS_RE = ADDRESS_RE;
|
|
1163
|
+
exports.DECIMAL_UINT_RE = DECIMAL_UINT_RE;
|
|
1164
|
+
exports.HEX_BYTES_RE = HEX_BYTES_RE;
|
|
1165
|
+
exports.emitNextActions = emitNextActions;
|
|
1166
|
+
exports.handleCommit = handleCommit;
|
|
1167
|
+
exports.handleCommitAndRedeem = handleCommitAndRedeem;
|
|
1168
|
+
exports.handleComplete = handleComplete;
|
|
1169
|
+
exports.handleDisputeEscalate = handleDisputeEscalate;
|
|
1170
|
+
exports.handleDisputeRaise = handleDisputeRaise;
|
|
1171
|
+
exports.handleDisputeResolve = handleDisputeResolve;
|
|
1172
|
+
exports.handleDisputeRetract = handleDisputeRetract;
|
|
1173
|
+
exports.handleGetAvailableFunds = handleGetAvailableFunds;
|
|
1174
|
+
exports.handlePerformAction = handlePerformAction;
|
|
1175
|
+
exports.handleRedeem = handleRedeem;
|
|
1176
|
+
exports.handleWithdrawFunds = handleWithdrawFunds;
|
|
1177
|
+
exports.handlerErr = handlerErr;
|
|
1178
|
+
exports.handlerOk = handlerOk;
|
|
1179
|
+
exports.plainHandlerOk = plainHandlerOk;
|
|
1180
|
+
exports.resolveEntityId = resolveEntityId;
|
|
1181
|
+
//# sourceMappingURL=index.js.map
|
|
1182
|
+
//# sourceMappingURL=index.js.map
|