@atumlabs/mppx-atum-escrow 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -2
- package/README.md +113 -4
- package/dist/{chunk-EWIOTGMO.js → chunk-5IQDNGVU.js} +78 -28
- package/dist/chunk-F3H73WI3.js +8358 -0
- package/dist/{chunk-IGTT7XSG.js → chunk-NVPGDBFS.js} +6671 -1261
- package/dist/client.d.ts +187 -4
- package/dist/client.js +12 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +13 -3
- package/dist/{internal-BZBJJfaL.d.ts → internal-DwRZPk7k.d.ts} +4 -18
- package/dist/server.d.ts +52 -22
- package/dist/server.js +2 -2
- package/package.json +4 -4
- package/dist/chunk-LAWFMGYD.js +0 -3707
package/CHANGELOG.md
CHANGED
|
@@ -7,16 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-10-09
|
|
11
|
+
|
|
10
12
|
### Changed
|
|
11
|
-
-
|
|
13
|
+
- The challenge's `source.amount` is validated before it is signed as the Permit2 spend cap: it must be a positive base-10 integer in the source token's smallest units. `0` or a leading zero, which used to be signed, is now refused on the client, and a decimal is refused with a clear error rather than failing inside the signing step. Canonical amounts are unaffected.
|
|
14
|
+
- EVM and Tron payments are always built with the four-member deposit witness, and building a Solana payment fails for a signature domain version below 4. Every deployed escrow is at version 4 or later.
|
|
12
15
|
|
|
13
16
|
### Security
|
|
14
|
-
- The
|
|
17
|
+
- The server refuses a credential whose payee or `request_id` differs from the one the payer signed, on EVM, Tron and Solana. The signed deposit carries both, and they must match the payment request's own `destination` and `request_id`. A deposit that commits no destination is refused.
|
|
18
|
+
- Each value a verifier reads has one accepted spelling: `message` is `0x`-prefixed hex on every chain, Tron included; a signature is `0x` followed by 65 bytes of hex (recovery id 0, 1, 27 or 28) on EVM and Tron and `0x` followed by 64 bytes on Solana; and `max_source_amount` is a decimal integer with no sign, prefix, padding or leading zero.
|
|
19
|
+
- Payers must be key accounts: the signer is recovered off chain, so a contract account (ERC-1271, such as a Safe), whose signature comes from an owner key, is refused.
|
|
20
|
+
- New `onCommitmentResult` option on `registerServer`: it receives the result of every payee check, admissions and refusals alike, with log fields named as in the Go and TypeScript SDKs (`sender_auth_result`, `signed_destination`, `request_destination`, ...). Count payments by `sender_auth_result` and log every refusal.
|
|
21
|
+
- The server checks the payment request, not only the deposit: it refuses a repeated member name, including names that differ only in letter case, where the request is read (any top-level name, or inside `source`, `destination`, `request_id` or `sender_auth`), and a `message_scheme` other than the one it verifies for the source chain (`EVM_PERMIT2` on EVM and Tron, `SOLANA` on Solana).
|
|
22
|
+
- **Breaking:** the client refuses to sign a corridor the payer does not trust. It used to sign whatever escrow, reserver and releaser a merchant's challenge named, so a merchant could name its own contract as the escrow, or itself as reserver and releaser, and take up to the cap. Before anything is signed, `extra.escrow`, `extra.reserver` and `extra.releaser` are now compared with the source chain's trusted escrow, quote selector and fulfillment verifier account, and `extra.fulfillmentProxy` with the destination chain's trusted proxy; a mismatch throws `AtumEscrowTrustError` (`UNTRUSTED_ESCROW`, `UNTRUSTED_RESERVER`, `UNTRUSTED_RELEASER`, `UNTRUSTED_FULFILLMENT_PROXY`). The trust source is the new optional `trust` option on `registerClient`; without it the client asks `GET /v1/defaults` on Atum's production gateways (`ATUM_DEFAULT_TRUST_GATEWAYS`, via `atumGatewayTrust`), and a chain they do not serve, or an answer they cannot give, refuses (`CHAIN_NOT_TRUSTED`, `TRUST_SOURCE_UNAVAILABLE`). Building the signed request itself still makes no network call. **Payers on a devnet, a staging deployment or another operator must now pass `trust`**, or every payment is refused. A trust source's own refusal keeps its code, including an `AtumEscrowTrustError` thrown by another Atum package's resolver (matched by `name` and `code`, since each package bundles its own class), and an answer that is not well-formed roles — a role that is not a non-empty string, or a missing or malformed `tokens` list — is refused as `TRUST_SOURCE_UNAVAILABLE`. Payers on 0.4.x must upgrade to this release to get the check: it is not in a 0.4.x patch.
|
|
23
|
+
- **Breaking:** the client bounds the spend cap. The source and destination tokens must be listed by the trust source (`UNTRUSTED_ASSET`) and be the same money — the same symbol, or in one of `pegGroups` (default `DEFAULT_PEG_GROUPS`) — or the payment is refused (`ASSETS_NOT_COMPARABLE`); `source.amount` may exceed `fulfillmentAmount` by at most `maxMarkupBps` (default `DEFAULT_MAX_MARKUP_BPS`, 500) after both are put in one unit (`MARKUP_EXCEEDED`). The same names, codes and defaults as `@atumlabs/x402-atum-escrow`'s client. **Merchants whose corridor `markupBps` is above 500 will be refused by payers on the default bound.**
|
|
24
|
+
- The client refuses a challenge whose `fulfillmentAmount` is not a base-10 integer before anything is signed. It is not signed, and the cap bound is computed from it, so a hex, binary or padded value, which JavaScript's `BigInt` would also read, could otherwise inflate the bound, and an empty one would be read as 0.
|
|
15
25
|
|
|
16
26
|
## [0.4.1] - 2026-09-18
|
|
17
27
|
|
|
18
28
|
### Changed
|
|
19
29
|
- Published under Apache-2.0 with `publishConfig.access: public`. npm will not overwrite 0.4.0, so this version exists to ship the LICENSE, NOTICE, and public visibility that 0.4.0 cannot take.
|
|
30
|
+
- **Breaking: `PaymentSettlementStatus` now reads `'pending' | 'finalizing' | 'completed' | 'failed'`.** `cancelled` is removed — the gateway could not produce it, and a payment that ends undelivered is reported as `failed`. `finalizing` is added: the transaction is on chain but has not yet reached the confirmations its chain requires, so it is **not** terminal and a payment in that state is still waited on rather than written off. Code doing an exhaustive `switch` on this type will not compile until a `finalizing` branch is added. A status this package does not recognise is treated as pending rather than terminal, which is the safe direction: it raises `SettlementPendingError` (retry this purchase) instead of `SettlementFailedError` (start a new one), so a payment still in flight is never answered with an instruction to charge again.
|
|
31
|
+
|
|
32
|
+
### Security
|
|
33
|
+
- The axios copy bundled from the payment-gateway client is now 1.20.0, which patches the 1.13.x advisories. The notices file lists the new MIT transitives (`https-proxy-agent`, `agent-base`) that ride along.
|
|
20
34
|
|
|
21
35
|
## [0.4.0] - 2026-09-16
|
|
22
36
|
|
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ for the gateway connection and corridor defaults.
|
|
|
90
90
|
| **Corridor** | The merchant's payment configuration: what it receives (destination), and which source chains/tokens it accepts. You define it once. |
|
|
91
91
|
| **Source option** | A source chain (with its escrow and role addresses) and one or more tokens (`assets`) a payer may pay from on it. A corridor may list several; each challenge offers one `(chain, token)`. |
|
|
92
92
|
| **`fulfillmentAmount`** | The exact amount, in the destination token's atomic units, the merchant will receive. USDC/USDT use 6 decimals, so `"10000000"` = 10 USDC. Set per charge. |
|
|
93
|
-
| **Source cap** | The most the payer can spend on the source side: `fulfillmentAmount` + a markup (`markupBps`) to cover the cross-chain spread. The payer signs this cap. |
|
|
93
|
+
| **Source cap** | The most the payer can spend on the source side: `fulfillmentAmount` + a markup (`markupBps`) to cover the cross-chain spread. The payer signs this cap. This package's payer client refuses a cap more than 500 bps above `fulfillmentAmount` unless the payer raises its `maxMarkupBps`, so keep `markupBps` at or below 500. |
|
|
94
94
|
| **Escrow** | The source-chain contract the payer's funds lock into until the merchant is paid. |
|
|
95
95
|
| **Deadlines** | Two budgets in seconds: `quoteDeadlineSeconds` (how long the auction runs) and `fulfillmentDeadlineSeconds` (how long settlement may take). Required order: `now < quote < fulfillment`. |
|
|
96
96
|
| **`PaymentSubmitter`** | A small adapter *you* provide that hands a signed payment request to the Atum Payment Gateway and returns the result. |
|
|
@@ -224,6 +224,111 @@ const resource = await res.text();
|
|
|
224
224
|
> corridor whose settlement can genuinely take minutes — see
|
|
225
225
|
> [Settlement outcomes](#settlement-outcomes) for the retry pattern that handles it.
|
|
226
226
|
|
|
227
|
+
### Trusted corridors and the spend cap
|
|
228
|
+
|
|
229
|
+
**Who the escrow protects, and against whom.** The escrow protects the payer against a settler
|
|
230
|
+
that does not pay: it holds the payer's funds until the fulfillment verifier confirms that the
|
|
231
|
+
settler paid the merchant on the destination chain. (It does not cover the merchant failing to
|
|
232
|
+
deliver the HTTP resource.) That protection only holds if the escrow, the reserver (who picks the
|
|
233
|
+
settler and how much of the cap it is paid) and the releaser (who opens the escrow) are ones the
|
|
234
|
+
payer trusts. The merchant writes all of them into the
|
|
235
|
+
challenge, so a merchant that named its own contract as the escrow, or itself as reserver or
|
|
236
|
+
releaser, could take up to the cap without delivering anything. The client therefore protects the
|
|
237
|
+
**payer** against the **merchant's** choice of corridor: before anything is signed, it checks the
|
|
238
|
+
challenge against a source the payer controls, the way a browser checks a site's certificate
|
|
239
|
+
against the authorities it trusts. The check cannot be turned off; only the trust source is
|
|
240
|
+
configurable, and it never comes from the challenge. The merchant's `verify()` checks the opposite
|
|
241
|
+
direction — that the credential matches the merchant's own terms — so it offers the payer nothing
|
|
242
|
+
here.
|
|
243
|
+
|
|
244
|
+
What is checked:
|
|
245
|
+
|
|
246
|
+
- on the source chain: `extra.escrow` against the trusted escrow, `extra.reserver` against the
|
|
247
|
+
trusted quote selector, and `extra.releaser` against the trusted fulfillment verifier account;
|
|
248
|
+
- on the destination chain: `extra.fulfillmentProxy` against the trusted fulfillment proxy;
|
|
249
|
+
- both tokens must be ones the trust source lists for their chain, and must be the same money:
|
|
250
|
+
the same symbol, or in one peg group;
|
|
251
|
+
- the cap (`source.amount`) may exceed `fulfillmentAmount` by at most `maxMarkupBps`, after both
|
|
252
|
+
are put in one unit using the tokens' decimals.
|
|
253
|
+
|
|
254
|
+
**The trust source.** By default the client asks `GET /v1/defaults` on Atum's production
|
|
255
|
+
gateways (`ATUM_DEFAULT_TRUST_GATEWAYS`: production mainnet and production testnet) and caches a
|
|
256
|
+
successful answer per chain for five minutes. Building the signed request itself makes no network
|
|
257
|
+
call; this lookup does, and so does a Turnkey signer when it signs. Pass `trust` to use another
|
|
258
|
+
source:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import {
|
|
262
|
+
AtumEscrowTrustError,
|
|
263
|
+
atumGatewayTrust,
|
|
264
|
+
registerClient,
|
|
265
|
+
type TrustedRoles,
|
|
266
|
+
} from "@atumlabs/mppx-atum-escrow/client";
|
|
267
|
+
|
|
268
|
+
// Another deployment's gateway (a staging environment, a local devnet, another operator)…
|
|
269
|
+
const method = registerClient({
|
|
270
|
+
signer: { privateKey: process.env.PAYER_PRIVATE_KEY! },
|
|
271
|
+
account: process.env.PAYER_ADDRESS!,
|
|
272
|
+
trust: atumGatewayTrust({ gateways: ["http://localhost:8080"] }),
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// …or a fixed set of addresses, with no network call. Each entry carries escrowContract,
|
|
276
|
+
// quoteSelector, fulfillmentVerifierAccount, fulfillmentProxy and the payable tokens.
|
|
277
|
+
const TRUSTED_CORRIDORS: Record<string, TrustedRoles> = {
|
|
278
|
+
/* "eip155:8453": { ... } */
|
|
279
|
+
};
|
|
280
|
+
const pinned = registerClient({
|
|
281
|
+
signer: { privateKey: process.env.PAYER_PRIVATE_KEY! },
|
|
282
|
+
account: process.env.PAYER_ADDRESS!,
|
|
283
|
+
trust: async (network) => {
|
|
284
|
+
const roles = TRUSTED_CORRIDORS[network];
|
|
285
|
+
if (!roles) {
|
|
286
|
+
throw new AtumEscrowTrustError({
|
|
287
|
+
code: "CHAIN_NOT_TRUSTED",
|
|
288
|
+
network,
|
|
289
|
+
message: `no trusted corridor on ${network}`,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return roles;
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
> **Devnet and staging must pass `trust`.** A local devnet forks mainnet and keeps mainnet chain
|
|
298
|
+
> ids, and staging runs its own escrow and roles, so the default would answer with production's
|
|
299
|
+
> addresses and every payment would be refused. A devnet whose tokens are test stand-ins (for
|
|
300
|
+
> example `TUSDC`, `TUSDT`) must also pass `pegGroups` naming them, or the client cannot price one
|
|
301
|
+
> against the other and refuses with `ASSETS_NOT_COMPARABLE`:
|
|
302
|
+
> `pegGroups: [[...DEFAULT_PEG_GROUPS[0], "TUSDC", "TUSDT"]]`.
|
|
303
|
+
|
|
304
|
+
**The cap.** `maxMarkupBps` (default `DEFAULT_MAX_MARKUP_BPS`, 500) is the most the cap may exceed
|
|
305
|
+
`fulfillmentAmount`, in basis points. `pegGroups` (default `DEFAULT_PEG_GROUPS`, the US-dollar
|
|
306
|
+
stablecoins on the network's allowlists) lists the token symbols the payer treats as
|
|
307
|
+
interchangeable 1:1; passing it replaces the default. Tokens that are neither the same symbol nor
|
|
308
|
+
in one group are not priced against each other at all.
|
|
309
|
+
|
|
310
|
+
**Refusals.** Every refusal is an `AtumEscrowTrustError`, and nothing is signed. Branch on `code`
|
|
311
|
+
(or `name`), not on `instanceof`: each published Atum package bundles its own copy of this class,
|
|
312
|
+
so an error from one package is not an instance of another package's class.
|
|
313
|
+
|
|
314
|
+
| `code` | Meaning |
|
|
315
|
+
| ----------------------------- | ---------------------------------------------------------------------------- |
|
|
316
|
+
| `UNTRUSTED_ESCROW` | `extra.escrow` is not the trusted escrow |
|
|
317
|
+
| `UNTRUSTED_RESERVER` | `extra.reserver` is not the trusted quote selector |
|
|
318
|
+
| `UNTRUSTED_RELEASER` | `extra.releaser` is not the trusted fulfillment verifier |
|
|
319
|
+
| `UNTRUSTED_FULFILLMENT_PROXY` | the fulfillment proxy is not the destination chain's trusted proxy |
|
|
320
|
+
| `UNTRUSTED_ASSET` | a token is not one the trust source lists for its chain; `field` says which (`source.asset` or `extra.destination.asset`) |
|
|
321
|
+
| `ASSETS_NOT_COMPARABLE` | the two tokens are not the same money |
|
|
322
|
+
| `MARKUP_EXCEEDED` | `source.amount` is above the bound; `expected` is the largest amount allowed |
|
|
323
|
+
| `CHAIN_NOT_TRUSTED` | the trust source serves nothing on the chain |
|
|
324
|
+
| `TRUST_SOURCE_UNAVAILABLE` | the trust source could not answer, or answered with something other than roles; retry only if the failure was transient |
|
|
325
|
+
|
|
326
|
+
For the `UNTRUSTED_*` codes and `MARKUP_EXCEEDED`, `field`, `expected` (the trusted value or
|
|
327
|
+
bound) and `actual` (what the challenge asked for) say exactly what failed.
|
|
328
|
+
|
|
329
|
+
Before any of these checks, a challenge whose `fulfillmentAmount` is not a base-10 integer is
|
|
330
|
+
refused with a plain `Error`: it is a malformed challenge, not a trust verdict.
|
|
331
|
+
|
|
227
332
|
### Approving the source token
|
|
228
333
|
|
|
229
334
|
On EVM and Tron, the payer must approve the token-transfer contract (Permit2) to move the source
|
|
@@ -534,15 +639,18 @@ Import everything from the root, or from the role-specific entry points (which e
|
|
|
534
639
|
each side needs):
|
|
535
640
|
|
|
536
641
|
- `@atumlabs/mppx-atum-escrow` — everything below
|
|
537
|
-
- `@atumlabs/mppx-atum-escrow/client` — `registerClient`, `ensureSourceApproval`, `needsSourceApproval
|
|
642
|
+
- `@atumlabs/mppx-atum-escrow/client` — `registerClient`, `ensureSourceApproval`, `needsSourceApproval`, the trust API + payer types
|
|
538
643
|
- `@atumlabs/mppx-atum-escrow/server` — `registerServer`, `buildChargeChallenge`,
|
|
539
644
|
`buildChargeRequest`, `validateCorridor`, `corridorFromDefaults`, the settlement errors +
|
|
540
645
|
merchant types
|
|
541
646
|
|
|
542
647
|
| Export | Description |
|
|
543
648
|
| --- | --- |
|
|
544
|
-
| `registerClient(config)` | Payer-side method. `config`: `{ signer, account, now?, solanaClockReader? }`. |
|
|
545
|
-
| `
|
|
649
|
+
| `registerClient(config)` | Payer-side method. `config`: `{ signer, account, now?, solanaClockReader?, trust?, maxMarkupBps?, pegGroups? }`. See [Trusted corridors and the spend cap](#trusted-corridors-and-the-spend-cap). |
|
|
650
|
+
| `atumGatewayTrust(options?)` | A trust source backed by gateways' `GET /v1/defaults` (default: `ATUM_DEFAULT_TRUST_GATEWAYS`), cached per chain. The default `trust`. |
|
|
651
|
+
| `AtumEscrowTrustError` | Why the client refused to sign a challenge; branch on `code`. |
|
|
652
|
+
| `DEFAULT_MAX_MARKUP_BPS`, `DEFAULT_PEG_GROUPS` | The client's defaults for `maxMarkupBps` (500) and `pegGroups`. |
|
|
653
|
+
| `registerServer(config)` | Merchant-side method. `config`: `{ submitter, now?, onCommitmentResult? }`; `onCommitmentResult(result, logFields)` receives every payee-check result, so you can count payments by `logFields.sender_auth_result` and log refusals. Returns an `AtumEscrowServer`, whose `verify()` resolves with an `AtumEscrowReceipt`. |
|
|
546
654
|
| `buildChargeChallenge(corridor, select, fulfillmentAmount, { intentId, issuedAt? })` | Builds a `charge` challenge — the payment terms plus the per-purchase identifier that makes a retry safe. **Use this.** Returns `{ request, meta }`. |
|
|
547
655
|
| `buildChargeRequest(corridor, select, fulfillmentAmount, options?)` | The payment terms alone, without the identifier. For supplying challenge metadata by hand. |
|
|
548
656
|
| `validateCorridor(corridor)` | Validates a corridor's shape and per-source addresses (run automatically by both builders). |
|
|
@@ -558,6 +666,7 @@ each side needs):
|
|
|
558
666
|
|
|
559
667
|
Key types: `AtumEscrowCorridor`, `AtumEscrowSource`, `SenderSigner`, `SenderSignerOptions`,
|
|
560
668
|
`PaymentSubmitter`, `PaymentSubmitResult`, `PaymentSettlementStatus`, `AtumEscrowClientConfig`,
|
|
669
|
+
`TrustResolver`, `TrustedRoles`, `TrustedToken`, `AtumEscrowTrustErrorCode`, `AtumGatewayTrustOptions`,
|
|
561
670
|
`AtumEscrowServer`, `AtumEscrowServerConfig`, `ChainDefaultsSource`, `EnsureApprovalResult`,
|
|
562
671
|
`AtumEscrowChallenge`, `AtumEscrowCredential`, `AtumEscrowReceipt`, `ChargeChallenge`, `ChargeRequest`,
|
|
563
672
|
`SettlementErrorDetails`, `PaymentRequest`, `FulfillmentConfirmation`.
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
require_dist2 as require_dist,
|
|
14
14
|
sameAddress,
|
|
15
15
|
sameAssetIdentifier
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-F3H73WI3.js";
|
|
17
17
|
|
|
18
18
|
// src/server.ts
|
|
19
19
|
var import_payment_request_sender_auth2 = __toESM(require_dist(), 1);
|
|
@@ -99,6 +99,32 @@ var PaymentRejectedError = class extends Errors.BadRequestError {
|
|
|
99
99
|
// src/server.ts
|
|
100
100
|
var DEADLINE_SKEW_TOLERANCE_MS = 6e4;
|
|
101
101
|
var SOLANA_MAX_FULFILLMENT_DEADLINE_SECONDS = import_payment_request_sender_auth2.REPLAY_HORIZON_SECS - 10;
|
|
102
|
+
function checkCommitment(config, check) {
|
|
103
|
+
let checked;
|
|
104
|
+
try {
|
|
105
|
+
checked = check();
|
|
106
|
+
} catch (e) {
|
|
107
|
+
const refused = (0, import_payment_request_sender_auth2.commitmentResultOf)(e);
|
|
108
|
+
if (refused !== void 0) {
|
|
109
|
+
try {
|
|
110
|
+
config.onCommitmentResult?.(refused, (0, import_payment_request_sender_auth2.commitmentLogFields)(refused));
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
config.onCommitmentResult?.(checked.result, (0, import_payment_request_sender_auth2.commitmentLogFields)(checked.result));
|
|
118
|
+
} catch {
|
|
119
|
+
throw new Error("atum-escrow: the payment could not be recorded, so it is refused");
|
|
120
|
+
}
|
|
121
|
+
return checked.value;
|
|
122
|
+
}
|
|
123
|
+
var SCHEME_BY_NAMESPACE = /* @__PURE__ */ new Map([
|
|
124
|
+
["eip155", "EVM_PERMIT2"],
|
|
125
|
+
["tron", "EVM_PERMIT2"],
|
|
126
|
+
["solana", "SOLANA"]
|
|
127
|
+
]);
|
|
102
128
|
function registerServer(config) {
|
|
103
129
|
const now = config.now ?? (() => Date.now());
|
|
104
130
|
return Method.toServer(atumEscrowChargeMethod, {
|
|
@@ -110,6 +136,11 @@ function registerServer(config) {
|
|
|
110
136
|
if (!senderAuth || senderAuth.signed_messages.length === 0) {
|
|
111
137
|
throw new Error("atum-escrow: credential is missing sender_auth");
|
|
112
138
|
}
|
|
139
|
+
if (senderAuth.signed_messages.length !== 1) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"atum-escrow: sender_auth must carry exactly one signed message, the deposit"
|
|
142
|
+
);
|
|
143
|
+
}
|
|
113
144
|
const signed = senderAuth.signed_messages[0];
|
|
114
145
|
if (!signed.message_prehash) {
|
|
115
146
|
throw new Error("atum-escrow: sender_auth is missing message_prehash");
|
|
@@ -117,38 +148,19 @@ function registerServer(config) {
|
|
|
117
148
|
if (!pr.source) {
|
|
118
149
|
throw new Error("atum-escrow: credential is missing a source");
|
|
119
150
|
}
|
|
151
|
+
if (senderAuth.message_scheme !== SCHEME_BY_NAMESPACE.get(namespace)) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`atum-escrow: unsupported message_scheme for ${source.network}: ${senderAuth.message_scheme}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
120
156
|
const maxSourceStr = pr.max_source_amount ?? "0";
|
|
157
|
+
if (!/^(?:0|[1-9][0-9]*)$/.test(maxSourceStr)) {
|
|
158
|
+
throw new Error("atum-escrow: max_source_amount is not a decimal integer string");
|
|
159
|
+
}
|
|
121
160
|
const cap = BigInt(source.amount);
|
|
122
161
|
if (BigInt(maxSourceStr) > cap) {
|
|
123
162
|
throw new Error("atum-escrow: max_source_amount exceeds the source cap");
|
|
124
163
|
}
|
|
125
|
-
const authTerms = (0, import_payment_request_sender_auth2.getChainAdapter)(source.network).recoverAndVerify(
|
|
126
|
-
signed,
|
|
127
|
-
{ network: source.network, asset: source.asset, account: pr.source.account },
|
|
128
|
-
{
|
|
129
|
-
maxSourceAmount: maxSourceStr,
|
|
130
|
-
escrowContract: extra.escrow,
|
|
131
|
-
reserver: extra.reserver,
|
|
132
|
-
releaser: extra.releaser,
|
|
133
|
-
svmSignatureClusterId: extra.svmSignatureClusterId,
|
|
134
|
-
svmSignatureDomainVersion: extra.svmSignatureDomainVersion,
|
|
135
|
-
// Check permit freshness against the merchant's own clock (the same `now` used for
|
|
136
|
-
// deadline ordering/budgets), not the SDK's wall clock — so freshness is consistent
|
|
137
|
-
// with the rest of verify and deterministic under an injected clock.
|
|
138
|
-
nowSec: Math.floor(now() / 1e3)
|
|
139
|
-
}
|
|
140
|
-
);
|
|
141
|
-
const intentId = intentIdOf(credential.challenge);
|
|
142
|
-
if (intentId === void 0) {
|
|
143
|
-
throw new Error(
|
|
144
|
-
`atum-escrow: the challenge carries no '${INTENT_ID_META_KEY}' metadata, so this payment cannot be de-duplicated and is refused`
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
if (pr.request_id !== chargeRequestId(intentId, pr.source.account)) {
|
|
148
|
-
throw new Error(
|
|
149
|
-
"atum-escrow: request_id is not derived from the challenge's purchase identifier, so a retry of this payment would not de-duplicate"
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
164
|
const dest = pr.destination?.[0];
|
|
153
165
|
if (!dest) {
|
|
154
166
|
throw new Error("atum-escrow: credential is missing a destination");
|
|
@@ -163,6 +175,44 @@ function registerServer(config) {
|
|
|
163
175
|
if (pr.fulfillment_amount !== extra.fulfillmentAmount) {
|
|
164
176
|
throw new Error("atum-escrow: fulfillment_amount does not match the challenge");
|
|
165
177
|
}
|
|
178
|
+
const authTerms = checkCommitment(config, () => {
|
|
179
|
+
const verified = (0, import_payment_request_sender_auth2.getChainAdapter)(source.network).recoverAndVerify(
|
|
180
|
+
signed,
|
|
181
|
+
{ network: source.network, asset: source.asset, account: pr.source.account },
|
|
182
|
+
{ destinations: pr.destination, requestId: pr.request_id ?? "" },
|
|
183
|
+
{
|
|
184
|
+
maxSourceAmount: maxSourceStr,
|
|
185
|
+
escrowContract: extra.escrow,
|
|
186
|
+
reserver: extra.reserver,
|
|
187
|
+
releaser: extra.releaser,
|
|
188
|
+
svmSignatureClusterId: extra.svmSignatureClusterId,
|
|
189
|
+
svmSignatureDomainVersion: extra.svmSignatureDomainVersion,
|
|
190
|
+
// Use the same clock as the deadline checks below, so an injected `now` applies
|
|
191
|
+
// throughout.
|
|
192
|
+
nowSec: Math.floor(now() / 1e3)
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
const result2 = (0, import_payment_request_sender_auth2.verifyRequestCommitment)(
|
|
196
|
+
JSON.stringify(pr),
|
|
197
|
+
extra.svmSignatureDomainVersion == null ? void 0 : {
|
|
198
|
+
escrowProgramId: extra.escrow,
|
|
199
|
+
clusterId: extra.svmSignatureClusterId ?? "",
|
|
200
|
+
domainVersion: extra.svmSignatureDomainVersion
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
return { value: verified, result: result2 };
|
|
204
|
+
});
|
|
205
|
+
const intentId = intentIdOf(credential.challenge);
|
|
206
|
+
if (intentId === void 0) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`atum-escrow: the challenge carries no '${INTENT_ID_META_KEY}' metadata, so this payment cannot be de-duplicated and is refused`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (pr.request_id !== chargeRequestId(intentId, pr.source.account)) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
"atum-escrow: request_id is not derived from the challenge's purchase identifier, so a retry of this payment would not de-duplicate"
|
|
214
|
+
);
|
|
215
|
+
}
|
|
166
216
|
const overrides = contractOverridesFromExtra(source.network, extra);
|
|
167
217
|
const expectedQuoteSelector = overrides?.quote_selector ?? extra.reserver;
|
|
168
218
|
const expectedVerifier = overrides?.fulfillment_verifier ?? {
|