@x402r/evm 0.0.1 → 0.0.2
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/README.md +21 -8
- package/dist/escrow/client/index.d.ts +29 -13
- package/dist/escrow/client/index.d.ts.map +1 -1
- package/dist/escrow/client/index.js +92 -33
- package/dist/escrow/client/index.js.map +1 -1
- package/dist/escrow/facilitator/index.d.ts +1 -1
- package/dist/escrow/facilitator/index.d.ts.map +1 -1
- package/dist/escrow/facilitator/index.js +163 -9
- package/dist/escrow/facilitator/index.js.map +1 -1
- package/dist/escrow/server/index.d.ts +41 -37
- package/dist/escrow/server/index.d.ts.map +1 -1
- package/dist/escrow/server/index.js +159 -25
- package/dist/escrow/server/index.js.map +1 -1
- package/dist/shared/constants.d.ts +14 -0
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +12 -0
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/nonce.d.ts +2 -2
- package/dist/shared/nonce.d.ts.map +1 -1
- package/dist/shared/nonce.js +3 -4
- package/dist/shared/nonce.js.map +1 -1
- package/dist/shared/types.d.ts +8 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +20 -1
- package/dist/shared/types.js.map +1 -1
- package/package.json +1 -1
- package/src/escrow/client/index.ts +140 -67
- package/src/escrow/facilitator/index.ts +196 -12
- package/src/escrow/server/index.ts +189 -61
- package/src/shared/constants.ts +15 -0
- package/src/shared/nonce.ts +6 -5
- package/src/shared/types.ts +26 -0
|
@@ -1,88 +1,161 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Escrow Scheme - Client
|
|
3
|
-
* Creates payment payloads for escrow payments
|
|
3
|
+
* Creates payment payloads for escrow payments.
|
|
4
|
+
*
|
|
5
|
+
* Implements x402's SchemeNetworkClient interface so it can be registered
|
|
6
|
+
* on an x402Client via client.register('eip155:84532', new EscrowEvmScheme(signer)).
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
Network,
|
|
11
|
+
PaymentPayload,
|
|
12
|
+
PaymentRequirements,
|
|
13
|
+
SchemeNetworkClient,
|
|
14
|
+
} from "@x402/core/types";
|
|
15
|
+
import type { ClientEvmSigner } from "@x402/evm";
|
|
16
|
+
import { x402Client } from "@x402/core/client";
|
|
7
17
|
import {
|
|
8
18
|
computeEscrowNonce,
|
|
9
19
|
signERC3009,
|
|
10
20
|
generateSalt,
|
|
11
21
|
} from "../../shared/nonce.js";
|
|
12
22
|
import { MAX_UINT48 } from "../../shared/constants.js";
|
|
13
|
-
import type { EscrowExtra
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
import type { EscrowExtra } from "../../shared/types.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse chainId from CAIP-2 network identifier
|
|
27
|
+
*/
|
|
28
|
+
function parseChainId(network: string): number {
|
|
29
|
+
const parts = network.split(":");
|
|
30
|
+
if (parts.length !== 2 || parts[0] !== "eip155") {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Invalid network format: ${network}. Expected 'eip155:<chainId>'`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const chainId = parseInt(parts[1], 10);
|
|
36
|
+
if (isNaN(chainId)) {
|
|
37
|
+
throw new Error(`Invalid chainId in network: ${network}`);
|
|
38
|
+
}
|
|
39
|
+
return chainId;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Escrow Client Scheme - implements x402's SchemeNetworkClient
|
|
44
|
+
*/
|
|
45
|
+
export class EscrowEvmScheme implements SchemeNetworkClient {
|
|
46
|
+
readonly scheme = "escrow";
|
|
47
|
+
|
|
48
|
+
constructor(private readonly signer: ClientEvmSigner) {}
|
|
49
|
+
|
|
50
|
+
async createPaymentPayload(
|
|
51
|
+
x402Version: number,
|
|
52
|
+
requirements: PaymentRequirements,
|
|
53
|
+
): Promise<Pick<PaymentPayload, "x402Version" | "payload">> {
|
|
54
|
+
if (x402Version !== 2) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Unsupported x402Version: ${x402Version}. Only version 2 is supported.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const extra = requirements.extra as unknown as EscrowExtra;
|
|
61
|
+
|
|
62
|
+
// Validate required EIP-712 domain parameters (M3, M10)
|
|
63
|
+
if (!extra.name) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`EIP-712 domain parameter 'name' is required in payment requirements for asset ${requirements.asset}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (!extra.version) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`EIP-712 domain parameter 'version' is required in payment requirements for asset ${requirements.asset}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const {
|
|
75
|
+
escrowAddress,
|
|
76
|
+
operatorAddress,
|
|
77
|
+
tokenCollector,
|
|
78
|
+
minFeeBps = 0,
|
|
79
|
+
maxFeeBps = 0,
|
|
80
|
+
feeReceiver,
|
|
81
|
+
preApprovalExpirySeconds,
|
|
82
|
+
refundExpirySeconds,
|
|
83
|
+
authorizationExpirySeconds,
|
|
84
|
+
} = extra;
|
|
85
|
+
|
|
86
|
+
const chainId = parseChainId(requirements.network);
|
|
87
|
+
const maxAmount = requirements.amount;
|
|
88
|
+
|
|
89
|
+
const paymentInfo = {
|
|
90
|
+
operator: operatorAddress,
|
|
91
|
+
receiver: requirements.payTo as `0x${string}`,
|
|
92
|
+
token: requirements.asset as `0x${string}`,
|
|
93
|
+
maxAmount,
|
|
94
|
+
preApprovalExpiry: preApprovalExpirySeconds ?? MAX_UINT48,
|
|
95
|
+
authorizationExpiry: authorizationExpirySeconds ?? MAX_UINT48,
|
|
96
|
+
refundExpiry: refundExpirySeconds ?? MAX_UINT48,
|
|
97
|
+
minFeeBps,
|
|
98
|
+
maxFeeBps,
|
|
99
|
+
feeReceiver: feeReceiver ?? operatorAddress,
|
|
100
|
+
salt: generateSalt(),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const nonce = computeEscrowNonce(chainId, escrowAddress, paymentInfo);
|
|
104
|
+
|
|
105
|
+
// ERC-3009 authorization - validBefore MUST match what contract passes to receiveWithAuthorization
|
|
106
|
+
// The contract uses paymentInfo.preApprovalExpiry as validBefore
|
|
107
|
+
const authorization = {
|
|
108
|
+
from: this.signer.address,
|
|
109
|
+
to: tokenCollector,
|
|
110
|
+
value: maxAmount,
|
|
111
|
+
validAfter: "0",
|
|
112
|
+
validBefore: String(paymentInfo.preApprovalExpiry),
|
|
113
|
+
nonce,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const signature = await signERC3009(
|
|
117
|
+
this.signer,
|
|
118
|
+
authorization,
|
|
119
|
+
extra,
|
|
120
|
+
requirements.asset as `0x${string}`,
|
|
121
|
+
chainId,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
x402Version,
|
|
126
|
+
payload: { authorization, signature, paymentInfo },
|
|
127
|
+
};
|
|
128
|
+
}
|
|
22
129
|
}
|
|
23
130
|
|
|
24
131
|
/**
|
|
25
|
-
*
|
|
132
|
+
* Register escrow client scheme with x402Client
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* const client = new x402Client();
|
|
137
|
+
* registerEscrowScheme(client, { signer, networks: "eip155:84532" });
|
|
138
|
+
* ```
|
|
26
139
|
*/
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
):
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
refundExpirySeconds,
|
|
40
|
-
authorizationExpirySeconds,
|
|
41
|
-
} = requirements.extra;
|
|
42
|
-
|
|
43
|
-
const chainId = await wallet.getChainId();
|
|
44
|
-
const maxAmount = requirements.amount;
|
|
45
|
-
|
|
46
|
-
const paymentInfo = {
|
|
47
|
-
operator: operatorAddress,
|
|
48
|
-
receiver: requirements.payTo,
|
|
49
|
-
token: requirements.asset,
|
|
50
|
-
maxAmount,
|
|
51
|
-
preApprovalExpiry: preApprovalExpirySeconds ?? MAX_UINT48,
|
|
52
|
-
authorizationExpiry: authorizationExpirySeconds ?? MAX_UINT48,
|
|
53
|
-
refundExpiry: refundExpirySeconds ?? MAX_UINT48,
|
|
54
|
-
minFeeBps,
|
|
55
|
-
maxFeeBps,
|
|
56
|
-
feeReceiver: feeReceiver ?? operatorAddress,
|
|
57
|
-
salt: generateSalt(),
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const nonce = computeEscrowNonce(chainId, escrowAddress, paymentInfo);
|
|
61
|
-
|
|
62
|
-
// ERC-3009 authorization - validBefore MUST match what contract passes to receiveWithAuthorization
|
|
63
|
-
// The contract uses paymentInfo.preApprovalExpiry as validBefore
|
|
64
|
-
const authorization = {
|
|
65
|
-
from: wallet.account!.address,
|
|
66
|
-
to: tokenCollector,
|
|
67
|
-
value: maxAmount,
|
|
68
|
-
validAfter: "0",
|
|
69
|
-
validBefore: String(paymentInfo.preApprovalExpiry),
|
|
70
|
-
nonce,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const signature = await signERC3009(
|
|
74
|
-
wallet,
|
|
75
|
-
authorization,
|
|
76
|
-
requirements.extra,
|
|
77
|
-
requirements.asset,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
return { authorization, signature, paymentInfo };
|
|
140
|
+
export function registerEscrowScheme(
|
|
141
|
+
client: x402Client,
|
|
142
|
+
config: { signer: ClientEvmSigner; networks: Network | Network[] },
|
|
143
|
+
): x402Client {
|
|
144
|
+
const scheme = new EscrowEvmScheme(config.signer);
|
|
145
|
+
const networks = Array.isArray(config.networks)
|
|
146
|
+
? config.networks
|
|
147
|
+
: [config.networks];
|
|
148
|
+
for (const network of networks) {
|
|
149
|
+
client.register(network, scheme);
|
|
150
|
+
}
|
|
151
|
+
return client;
|
|
81
152
|
}
|
|
82
153
|
|
|
154
|
+
/**
|
|
155
|
+
* @deprecated Use `new EscrowEvmScheme(signer)` directly
|
|
156
|
+
*/
|
|
83
157
|
export const EscrowScheme = {
|
|
84
158
|
scheme: "escrow" as const,
|
|
85
|
-
createPaymentPayload,
|
|
86
159
|
};
|
|
87
160
|
|
|
88
161
|
export type { EscrowExtra, EscrowPayload } from "../../shared/types.js";
|
|
@@ -16,8 +16,16 @@ import type {
|
|
|
16
16
|
} from "@x402/core/types";
|
|
17
17
|
import type { FacilitatorEvmSigner } from "@x402/evm";
|
|
18
18
|
import { x402Facilitator } from "@x402/core/facilitator";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
OPERATOR_ABI,
|
|
21
|
+
ERC20_BALANCE_OF_ABI,
|
|
22
|
+
ERC6492_MAGIC_VALUE,
|
|
23
|
+
} from "../../shared/constants.js";
|
|
20
24
|
import { verifyERC3009Signature } from "../../shared/nonce.js";
|
|
25
|
+
import {
|
|
26
|
+
isEscrowPayload,
|
|
27
|
+
isEscrowExtra,
|
|
28
|
+
} from "../../shared/types.js";
|
|
21
29
|
import type { EscrowExtra, EscrowPayload } from "../../shared/types.js";
|
|
22
30
|
|
|
23
31
|
/**
|
|
@@ -39,6 +47,46 @@ function parseChainId(network: string): number {
|
|
|
39
47
|
return chainId;
|
|
40
48
|
}
|
|
41
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Extract inner signature from an EIP-6492 wrapped signature.
|
|
52
|
+
* If the signature is not EIP-6492 wrapped, returns it unchanged.
|
|
53
|
+
*
|
|
54
|
+
* EIP-6492 format: abi.encode(address, bytes, bytes) ++ MAGIC_VALUE
|
|
55
|
+
* The inner signature is the third ABI-encoded bytes field.
|
|
56
|
+
*/
|
|
57
|
+
function unwrapERC6492Signature(signature: `0x${string}`): `0x${string}` {
|
|
58
|
+
// EIP-6492 magic is 32 bytes (64 hex chars) at the end
|
|
59
|
+
if (signature.length <= 66) return signature; // Too short to be wrapped
|
|
60
|
+
|
|
61
|
+
const magicSuffix = `0x${signature.slice(-64)}`;
|
|
62
|
+
if (magicSuffix !== ERC6492_MAGIC_VALUE) return signature; // Not wrapped
|
|
63
|
+
|
|
64
|
+
// Strip the magic suffix and ABI-decode: (address prepareTarget, bytes prepareData, bytes innerSignature)
|
|
65
|
+
// The wrapped data (without magic) is: 0x + ABI-encoded (address, bytes, bytes)
|
|
66
|
+
const wrappedHex = signature.slice(2, -64); // hex without 0x prefix and magic
|
|
67
|
+
|
|
68
|
+
// ABI layout for (address, bytes, bytes):
|
|
69
|
+
// word 0 (0-64): address (padded to 32 bytes)
|
|
70
|
+
// word 1 (64-128): offset to prepareData bytes
|
|
71
|
+
// word 2 (128-192): offset to innerSignature bytes
|
|
72
|
+
// Then the dynamic data follows
|
|
73
|
+
|
|
74
|
+
if (wrappedHex.length < 192) return signature; // Malformed
|
|
75
|
+
|
|
76
|
+
const innerSigOffset = parseInt(wrappedHex.slice(128, 192), 16) * 2; // byte offset → hex offset
|
|
77
|
+
if (innerSigOffset + 64 > wrappedHex.length) return signature; // Malformed
|
|
78
|
+
|
|
79
|
+
const innerSigLength = parseInt(
|
|
80
|
+
wrappedHex.slice(innerSigOffset, innerSigOffset + 64),
|
|
81
|
+
16,
|
|
82
|
+
) * 2; // bytes → hex chars
|
|
83
|
+
const innerSigStart = innerSigOffset + 64;
|
|
84
|
+
|
|
85
|
+
if (innerSigStart + innerSigLength > wrappedHex.length) return signature; // Malformed
|
|
86
|
+
|
|
87
|
+
return `0x${wrappedHex.slice(innerSigStart, innerSigStart + innerSigLength)}` as `0x${string}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
42
90
|
/**
|
|
43
91
|
* Escrow Facilitator Scheme - implements x402's SchemeNetworkFacilitator
|
|
44
92
|
*
|
|
@@ -56,29 +104,97 @@ export class EscrowFacilitatorScheme implements SchemeNetworkFacilitator {
|
|
|
56
104
|
return [...this.signer.getAddresses()];
|
|
57
105
|
}
|
|
58
106
|
|
|
59
|
-
|
|
60
|
-
|
|
107
|
+
// C4: name/version now come from server's parsePrice() via AssetAmount.extra.
|
|
108
|
+
// The facilitator should not hardcode token-specific metadata.
|
|
109
|
+
getExtra(_network: string): Record<string, unknown> | undefined {
|
|
110
|
+
return undefined;
|
|
61
111
|
}
|
|
62
112
|
|
|
63
113
|
async verify(
|
|
64
114
|
payload: PaymentPayload,
|
|
65
115
|
requirements: PaymentRequirements,
|
|
66
116
|
): Promise<VerifyResponse> {
|
|
67
|
-
|
|
68
|
-
|
|
117
|
+
// M5: Type guard instead of double cast
|
|
118
|
+
if (!isEscrowPayload(payload.payload)) {
|
|
119
|
+
return {
|
|
120
|
+
isValid: false,
|
|
121
|
+
invalidReason: "invalid_payload_format",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const escrowPayload = payload.payload as EscrowPayload;
|
|
125
|
+
const payer = escrowPayload.authorization.from;
|
|
126
|
+
|
|
127
|
+
// Validate scheme
|
|
128
|
+
if (requirements.scheme !== "escrow") {
|
|
129
|
+
return {
|
|
130
|
+
isValid: false,
|
|
131
|
+
invalidReason: "unsupported_scheme",
|
|
132
|
+
payer,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Validate network format
|
|
137
|
+
const networkParts = requirements.network.split(":");
|
|
138
|
+
if (networkParts.length !== 2 || networkParts[0] !== "eip155") {
|
|
139
|
+
return {
|
|
140
|
+
isValid: false,
|
|
141
|
+
invalidReason: "invalid_network",
|
|
142
|
+
payer,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// M5: Type guard for extra
|
|
147
|
+
if (!isEscrowExtra(requirements.extra)) {
|
|
148
|
+
return {
|
|
149
|
+
isValid: false,
|
|
150
|
+
invalidReason: "invalid_escrow_extra",
|
|
151
|
+
payer,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const extra = requirements.extra as EscrowExtra;
|
|
69
155
|
const chainId = parseChainId(requirements.network);
|
|
70
156
|
|
|
157
|
+
// Time window validation
|
|
158
|
+
const now = Math.floor(Date.now() / 1000);
|
|
159
|
+
const validBefore = Number(escrowPayload.authorization.validBefore);
|
|
160
|
+
const validAfter = Number(escrowPayload.authorization.validAfter);
|
|
161
|
+
|
|
162
|
+
if (validBefore <= now + 6) {
|
|
163
|
+
return {
|
|
164
|
+
isValid: false,
|
|
165
|
+
invalidReason: "authorization_expired",
|
|
166
|
+
payer,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (validAfter > now) {
|
|
171
|
+
return {
|
|
172
|
+
isValid: false,
|
|
173
|
+
invalidReason: "authorization_not_yet_valid",
|
|
174
|
+
payer,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// M4: Extract inner signature for verification if EIP-6492 wrapped.
|
|
179
|
+
// The contract's ERC6492SignatureHandler handles deployment; the facilitator
|
|
180
|
+
// only needs the inner ECDSA signature for ecrecover verification.
|
|
181
|
+
const signatureForVerify = unwrapERC6492Signature(escrowPayload.signature);
|
|
182
|
+
|
|
71
183
|
// Verify ERC-3009 signature
|
|
72
184
|
const isValidSignature = await verifyERC3009Signature(
|
|
73
185
|
this.signer,
|
|
74
186
|
escrowPayload.authorization,
|
|
75
|
-
|
|
187
|
+
signatureForVerify,
|
|
76
188
|
{ ...extra, chainId },
|
|
77
189
|
requirements.asset as `0x${string}`,
|
|
78
190
|
);
|
|
79
191
|
|
|
80
192
|
if (!isValidSignature) {
|
|
81
|
-
return {
|
|
193
|
+
return {
|
|
194
|
+
isValid: false,
|
|
195
|
+
invalidReason: "invalid_escrow_signature",
|
|
196
|
+
payer,
|
|
197
|
+
};
|
|
82
198
|
}
|
|
83
199
|
|
|
84
200
|
// Verify amount meets requirements
|
|
@@ -86,7 +202,11 @@ export class EscrowFacilitatorScheme implements SchemeNetworkFacilitator {
|
|
|
86
202
|
BigInt(escrowPayload.authorization.value) <
|
|
87
203
|
BigInt(requirements.amount)
|
|
88
204
|
) {
|
|
89
|
-
return {
|
|
205
|
+
return {
|
|
206
|
+
isValid: false,
|
|
207
|
+
invalidReason: "insufficient_amount",
|
|
208
|
+
payer,
|
|
209
|
+
};
|
|
90
210
|
}
|
|
91
211
|
|
|
92
212
|
// Verify token matches
|
|
@@ -94,7 +214,11 @@ export class EscrowFacilitatorScheme implements SchemeNetworkFacilitator {
|
|
|
94
214
|
escrowPayload.paymentInfo.token.toLowerCase() !==
|
|
95
215
|
requirements.asset.toLowerCase()
|
|
96
216
|
) {
|
|
97
|
-
return {
|
|
217
|
+
return {
|
|
218
|
+
isValid: false,
|
|
219
|
+
invalidReason: "token_mismatch",
|
|
220
|
+
payer,
|
|
221
|
+
};
|
|
98
222
|
}
|
|
99
223
|
|
|
100
224
|
// Verify receiver matches
|
|
@@ -102,12 +226,37 @@ export class EscrowFacilitatorScheme implements SchemeNetworkFacilitator {
|
|
|
102
226
|
escrowPayload.paymentInfo.receiver.toLowerCase() !==
|
|
103
227
|
requirements.payTo.toLowerCase()
|
|
104
228
|
) {
|
|
105
|
-
return {
|
|
229
|
+
return {
|
|
230
|
+
isValid: false,
|
|
231
|
+
invalidReason: "receiver_mismatch",
|
|
232
|
+
payer,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// H4: Balance check — verify payer has sufficient token balance
|
|
237
|
+
try {
|
|
238
|
+
const balance = await this.signer.readContract({
|
|
239
|
+
address: requirements.asset as `0x${string}`,
|
|
240
|
+
abi: ERC20_BALANCE_OF_ABI,
|
|
241
|
+
functionName: "balanceOf",
|
|
242
|
+
args: [payer],
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (BigInt(balance as string) < BigInt(requirements.amount)) {
|
|
246
|
+
return {
|
|
247
|
+
isValid: false,
|
|
248
|
+
invalidReason: "insufficient_balance",
|
|
249
|
+
payer,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
// If balance check fails (e.g., non-standard token), skip it.
|
|
254
|
+
// The on-chain transaction will fail anyway if balance is insufficient.
|
|
106
255
|
}
|
|
107
256
|
|
|
108
257
|
return {
|
|
109
258
|
isValid: true,
|
|
110
|
-
payer
|
|
259
|
+
payer,
|
|
111
260
|
};
|
|
112
261
|
}
|
|
113
262
|
|
|
@@ -115,6 +264,18 @@ export class EscrowFacilitatorScheme implements SchemeNetworkFacilitator {
|
|
|
115
264
|
payload: PaymentPayload,
|
|
116
265
|
requirements: PaymentRequirements,
|
|
117
266
|
): Promise<SettleResponse> {
|
|
267
|
+
// H2: Re-verify before settling to catch expired/invalid payloads
|
|
268
|
+
const verification = await this.verify(payload, requirements);
|
|
269
|
+
if (!verification.isValid) {
|
|
270
|
+
return {
|
|
271
|
+
success: false,
|
|
272
|
+
errorReason: verification.invalidReason ?? "verification_failed",
|
|
273
|
+
transaction: "",
|
|
274
|
+
network: requirements.network,
|
|
275
|
+
payer: verification.payer,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
118
279
|
const escrowPayload = payload.payload as unknown as EscrowPayload;
|
|
119
280
|
const extra = requirements.extra as unknown as EscrowExtra;
|
|
120
281
|
const { authorizeAddress, operatorAddress, tokenCollector } = extra;
|
|
@@ -134,7 +295,8 @@ export class EscrowFacilitatorScheme implements SchemeNetworkFacilitator {
|
|
|
134
295
|
salt: BigInt(escrowPayload.paymentInfo.salt),
|
|
135
296
|
};
|
|
136
297
|
|
|
137
|
-
// Pass raw signature
|
|
298
|
+
// Pass raw signature — ERC3009PaymentCollector/ERC6492SignatureHandler
|
|
299
|
+
// handles EIP-6492 unwrapping and wallet deployment on-chain
|
|
138
300
|
const collectorData = escrowPayload.signature;
|
|
139
301
|
|
|
140
302
|
const target = authorizeAddress ?? operatorAddress;
|
|
@@ -152,6 +314,28 @@ export class EscrowFacilitatorScheme implements SchemeNetworkFacilitator {
|
|
|
152
314
|
],
|
|
153
315
|
});
|
|
154
316
|
|
|
317
|
+
// Wait for transaction confirmation with 60s timeout to avoid hanging on stuck txs
|
|
318
|
+
const receiptPromise = this.signer.waitForTransactionReceipt({
|
|
319
|
+
hash: txHash,
|
|
320
|
+
});
|
|
321
|
+
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
322
|
+
setTimeout(
|
|
323
|
+
() => reject(new Error("Transaction receipt timeout after 60s")),
|
|
324
|
+
60_000,
|
|
325
|
+
),
|
|
326
|
+
);
|
|
327
|
+
const receipt = await Promise.race([receiptPromise, timeoutPromise]);
|
|
328
|
+
|
|
329
|
+
if (receipt.status !== "success") {
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
errorReason: "transaction_reverted",
|
|
333
|
+
transaction: txHash,
|
|
334
|
+
network: requirements.network,
|
|
335
|
+
payer: escrowPayload.authorization.from,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
155
339
|
return {
|
|
156
340
|
success: true,
|
|
157
341
|
transaction: txHash,
|