@atumlabs/mppx-atum-escrow 0.4.0 → 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/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # @atumlabs/mppx-atum-escrow
2
2
 
3
- > **Proprietary software - not open source.**
3
+ > Copyright 2026 Atum Labs, Inc.
4
4
  >
5
- > Copyright (c) 2026 Atum Labs, Inc. All rights reserved. Access to and use of `@atumlabs/mppx-atum-escrow` are limited to entities expressly authorized by Atum and are governed by the Atum MPP Escrow SDK Proprietary License Agreement in the LICENSE file included with this package. Do not redistribute, publish, mirror, sublicense, or provide this package to any unauthorized person. By accessing, installing, copying, or using the package, you agree to the license terms.
5
+ > Licensed under the Apache License, Version 2.0. See the `LICENSE` file
6
+ > distributed with this package.
6
7
 
7
8
  An [`mppx`](https://www.npmjs.com/package/mppx) payment method that lets a merchant accept
8
9
  **cross-chain stablecoin payments** through Atum, over the Machine Payments Protocol (MPP).
@@ -89,7 +90,7 @@ for the gateway connection and corridor defaults.
89
90
  | **Corridor** | The merchant's payment configuration: what it receives (destination), and which source chains/tokens it accepts. You define it once. |
90
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)`. |
91
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. |
92
- | **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. |
93
94
  | **Escrow** | The source-chain contract the payer's funds lock into until the merchant is paid. |
94
95
  | **Deadlines** | Two budgets in seconds: `quoteDeadlineSeconds` (how long the auction runs) and `fulfillmentDeadlineSeconds` (how long settlement may take). Required order: `now < quote < fulfillment`. |
95
96
  | **`PaymentSubmitter`** | A small adapter *you* provide that hands a signed payment request to the Atum Payment Gateway and returns the result. |
@@ -223,6 +224,111 @@ const resource = await res.text();
223
224
  > corridor whose settlement can genuinely take minutes — see
224
225
  > [Settlement outcomes](#settlement-outcomes) for the retry pattern that handles it.
225
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
+
226
332
  ### Approving the source token
227
333
 
228
334
  On EVM and Tron, the payer must approve the token-transfer contract (Permit2) to move the source
@@ -533,15 +639,18 @@ Import everything from the root, or from the role-specific entry points (which e
533
639
  each side needs):
534
640
 
535
641
  - `@atumlabs/mppx-atum-escrow` — everything below
536
- - `@atumlabs/mppx-atum-escrow/client` — `registerClient`, `ensureSourceApproval`, `needsSourceApproval` + payer types
642
+ - `@atumlabs/mppx-atum-escrow/client` — `registerClient`, `ensureSourceApproval`, `needsSourceApproval`, the trust API + payer types
537
643
  - `@atumlabs/mppx-atum-escrow/server` — `registerServer`, `buildChargeChallenge`,
538
644
  `buildChargeRequest`, `validateCorridor`, `corridorFromDefaults`, the settlement errors +
539
645
  merchant types
540
646
 
541
647
  | Export | Description |
542
648
  | --- | --- |
543
- | `registerClient(config)` | Payer-side method. `config`: `{ signer, account, now?, solanaClockReader? }`. |
544
- | `registerServer(config)` | Merchant-side method. `config`: `{ submitter, now? }`. Returns an `AtumEscrowServer`, whose `verify()` resolves with an `AtumEscrowReceipt`. |
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`. |
545
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 }`. |
546
655
  | `buildChargeRequest(corridor, select, fulfillmentAmount, options?)` | The payment terms alone, without the identifier. For supplying challenge metadata by hand. |
547
656
  | `validateCorridor(corridor)` | Validates a corridor's shape and per-source addresses (run automatically by both builders). |
@@ -557,6 +666,7 @@ each side needs):
557
666
 
558
667
  Key types: `AtumEscrowCorridor`, `AtumEscrowSource`, `SenderSigner`, `SenderSignerOptions`,
559
668
  `PaymentSubmitter`, `PaymentSubmitResult`, `PaymentSettlementStatus`, `AtumEscrowClientConfig`,
669
+ `TrustResolver`, `TrustedRoles`, `TrustedToken`, `AtumEscrowTrustErrorCode`, `AtumGatewayTrustOptions`,
560
670
  `AtumEscrowServer`, `AtumEscrowServerConfig`, `ChainDefaultsSource`, `EnsureApprovalResult`,
561
671
  `AtumEscrowChallenge`, `AtumEscrowCredential`, `AtumEscrowReceipt`, `ChargeChallenge`, `ChargeRequest`,
562
672
  `SettlementErrorDetails`, `PaymentRequest`, `FulfillmentConfirmation`.
@@ -2,12 +2,11 @@ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
2
2
  ==============================================================================
3
3
  @atumlabs/mppx-atum-escrow
4
4
 
5
- @atumlabs/mppx-atum-escrow is proprietary software, governed by the license
6
- agreement in the package-root LICENSE file. THIS file is not that license. This
5
+ @atumlabs/mppx-atum-escrow is licensed under the Apache License, Version 2.0, as stated
6
+ in the package-root LICENSE file. THIS file is not that license. This
7
7
  package includes the third-party software identified below; the licenses and
8
8
  notices reproduced here apply only to those third-party components. They do not
9
- modify the proprietary license governing this package or any Atum-authored code,
10
- and they do not make it open source.
9
+ modify the Apache-2.0 license governing this package or any Atum-authored code.
11
10
 
12
11
  The published artifact of this package (the compiled code under dist/) statically
13
12
  bundles the third-party open-source components listed below; their source is
@@ -18,9 +17,9 @@ as (peer)dependencies of this package (for example @solana/web3.js, ethers, bs58
18
17
  tweetnacl, zod, and mppx) — those ship under their own licenses and are not
19
18
  redistributed here.
20
19
 
21
- LICENSE SUMMARY (27 components)
20
+ LICENSE SUMMARY (28 components)
22
21
  ------------------------------------------------------------------------------
23
- 27 MIT
22
+ 28 MIT
24
23
  ==============================================================================
25
24
 
26
25
  ------------------------------------------------------------------------------
@@ -50,6 +49,13 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
50
49
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
51
50
  THE SOFTWARE.
52
51
 
52
+ ------------------------------------------------------------------------------
53
+ agent-base 6.0.2 (MIT)
54
+ Author: Nathan Rajlich <nathan@tootallnate.net> (http://n8.io/)
55
+ ------------------------------------------------------------------------------
56
+
57
+ [No license file is distributed inside this package. Declared license: MIT; author: Nathan Rajlich <nathan@tootallnate.net> (http://n8.io/).]
58
+
53
59
  ------------------------------------------------------------------------------
54
60
  asynckit 0.4.0 (MIT)
55
61
  Author: Alex Indigo <iam@alexindigo.com>
@@ -78,7 +84,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
78
84
  SOFTWARE.
79
85
 
80
86
  ------------------------------------------------------------------------------
81
- axios 1.13.2 (MIT)
87
+ axios 1.20.0 (MIT)
82
88
  Author: Matt Zabriskie
83
89
  ------------------------------------------------------------------------------
84
90
 
@@ -563,6 +569,13 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
563
569
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
564
570
  SOFTWARE.
565
571
 
572
+ ------------------------------------------------------------------------------
573
+ https-proxy-agent 5.0.1 (MIT)
574
+ Author: Nathan Rajlich <nathan@tootallnate.net> (http://n8.io/)
575
+ ------------------------------------------------------------------------------
576
+
577
+ [No license file is distributed inside this package. Declared license: MIT; author: Nathan Rajlich <nathan@tootallnate.net> (http://n8.io/).]
578
+
566
579
  ------------------------------------------------------------------------------
567
580
  math-intrinsics 1.1.0 (MIT)
568
581
  Author: Jordan Harband <ljharb@gmail.com>
@@ -672,32 +685,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
672
685
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
673
686
  SOFTWARE.
674
687
 
675
- ------------------------------------------------------------------------------
676
- proxy-from-env 1.1.0 (MIT)
677
- Author: Rob Wu <rob@robwu.nl> (https://robwu.nl/)
678
- ------------------------------------------------------------------------------
679
-
680
- The MIT License
681
-
682
- Copyright (C) 2016-2018 Rob Wu <rob@robwu.nl>
683
-
684
- Permission is hereby granted, free of charge, to any person obtaining a copy of
685
- this software and associated documentation files (the "Software"), to deal in
686
- the Software without restriction, including without limitation the rights to
687
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
688
- of the Software, and to permit persons to whom the Software is furnished to do
689
- so, subject to the following conditions:
690
-
691
- The above copyright notice and this permission notice shall be included in all
692
- copies or substantial portions of the Software.
693
-
694
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
695
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
696
- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
697
- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
698
- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
699
- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
700
-
701
688
  ------------------------------------------------------------------------------
702
689
  supports-color 10.2.2 (MIT)
703
690
  Author: Sindre Sorhus <sindresorhus@gmail.com>
@@ -13,7 +13,7 @@ import {
13
13
  require_dist2 as require_dist,
14
14
  sameAddress,
15
15
  sameAssetIdentifier
16
- } from "./chunk-OEEC5P3E.js";
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 ?? {
@@ -211,7 +261,7 @@ function registerServer(config) {
211
261
  const result = await config.submitter.submit(pr);
212
262
  const fc = result.fulfillment_confirmation;
213
263
  if (!fc) {
214
- const terminal = result.status === "failed" || result.status === "cancelled";
264
+ const terminal = result.status === "failed";
215
265
  if (terminal) {
216
266
  throw new SettlementFailedError({ paymentId: result.payment_id, state: result.status });
217
267
  }