@atumlabs/mppx-atum-escrow 0.1.1 → 0.3.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 +50 -0
- package/README.md +212 -47
- package/dist/chunk-4YD6566T.js +371 -0
- package/dist/chunk-Z3AUNEU5.js +2645 -0
- package/dist/{chunk-2MWWLU75.js → chunk-ZFA5SSUP.js} +1691 -1653
- package/dist/client.d.ts +21 -16
- package/dist/client.js +4 -2
- package/dist/index.d.ts +19 -18
- package/dist/index.js +19 -3
- package/dist/internal-9tB7y-A7.d.ts +365 -0
- package/dist/server.d.ts +234 -17
- package/dist/server.js +18 -2
- package/package.json +7 -4
- package/dist/chunk-L62WG2VU.js +0 -131
- package/dist/chunk-W6D2D767.js +0 -1410
- package/dist/internal-CjcEyEsm.d.ts +0 -842
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-07-30
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `buildChargeChallenge(corridor, select, fulfillmentAmount, { intentId })` — the merchant-side entry point for building a charge challenge. It pairs the payment terms with a per-purchase identifier (your order or invoice id), which is what makes a retry of that purchase resolve to the original payment instead of becoming a second charge. It travels in challenge metadata under `INTENT_ID_META_KEY`.
|
|
12
|
+
- `SettlementPendingError`, `SettlementFailedError`, and `PaymentRejectedError`, with `isSettlementPending` / `isSettlementFailed` / `isPaymentRejected` guards. A payment that has not settled is no longer reported as a single undifferentiated failure: pending means retrying the same purchase is safe and is how the result is collected, while a terminal failure means that purchase can never settle and recovery requires a new one. Both settlement errors carry the gateway's `paymentId` — as its own field on `toProblemDetails()` (and `SettlementFailedError` also carries `state`), not only interpolated into the message text, so a payer reading the wire response can reconcile the attempt without parsing a sentence.
|
|
13
|
+
- `PaymentSubmitResult.status`, so a submitter can pass the gateway's lifecycle state through. An absent status is read as pending.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- The payer's `signer` option now accepts Turnkey provider config (`{ provider: "turnkey", ... }`) as documented by its type, instead of throwing and directing you to construct a `TurnkeySenderSigner` yourself and pass it as a ready-made signer. Both paths now behave the same way; the ready-made-signer path still works unchanged.
|
|
17
|
+
- A credential presented after its quote window has closed now fails with a typed `PaymentActionRequiredError` (the same type `SettlementPendingError` carries) instead of a plain `Error`. mppx's request-routing layer replaces any thrown value that is not a recognized payment error with a generic "Payment verification failed." before it reaches the wire, which was silently erasing the "re-attempt with a fresh challenge" guidance for every real HTTP payer — only a merchant calling `verify()` directly in-process ever saw it. The message is unchanged; only its wire-visible type is fixed, so a payer's retry loop can now correctly recognize this as the same "fetch a fresh challenge and continue" outcome as a pending settlement.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **Breaking:** the payment's identity is now derived from the challenge's per-purchase identifier rather than from the challenge id. The challenge id is an HMAC over the challenge's own contents, so it moved whenever the expiry or the quote moved: under a rolling `expires` every retry derived a fresh identity and was charged as a new payment, and with no `expires` or metadata at all two distinct purchases with identical terms collapsed onto one payment, leaving the second unpaid. Merchants now stamp `intentId` (via `buildChargeChallenge`); a challenge without one is refused by the payer's client rather than paid unsafely.
|
|
21
|
+
- **If your integration sets `challenge.opaque` yourself** (the pre-0.3.0 way to carry your own correlation data), note that it silently overrides the `meta` this relies on for `intentId` — the mppx framework only serializes `meta` into `opaque` when nothing has already set `opaque`. Move your own data onto extra keys of the same `meta` object `buildChargeChallenge` returns instead. See the README's "Quick start — merchant" section.
|
|
22
|
+
- A `PaymentSubmitter` should submit and return the gateway's response rather than polling for completion — the payer's retry is what collects a pending result. Polling holds the merchant's request open for the whole settlement window and hides the pending state that makes the retry safe.
|
|
23
|
+
- **Re-attempting a purchase is now spelled out**, in `SettlementPendingError`'s own message and in the README: request a fresh challenge and sign it again, keeping the same `intentId`. "Retry the same purchase" was ambiguous, and the reading it invited does not work — `quote_deadline` and `fulfillment_deadline` are absolute timestamps fixed when the challenge is built, so a credential that has already been signed cannot be presented a second time once its quote window has closed. On Solana the deposit authorization additionally carries its own on-chain replay window, which expires with them.
|
|
24
|
+
- An authorization whose quote window has closed is now reported separately from deadlines that are in the wrong order. They were one message, and they need opposite responses: the first is fixed by re-running the purchase, the second is a malformed request that no passage of time will make valid. The stale case now names the remedy, including which field must not change.
|
|
25
|
+
|
|
26
|
+
## [0.2.2] - 2026-07-31
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- README: the proprietary-license notice now references the bundled `LICENSE` file by name instead of linking to it, so it no longer points at a repository that is not publicly shared.
|
|
30
|
+
|
|
31
|
+
## [0.2.1] - 2026-07-27
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- Building and signing the payer's deposit authorization now runs entirely through the shared `@atum-labs/payment-request-sender-auth` library (via the updated `@atum-labs/payment-gateway-client`): the deposit nonce is derived deterministically from the charge identity (retry-safe) and the EVM/Tron permit deadline tracks the settlement horizon. No public API changes; the idempotency anchor is byte-identical.
|
|
35
|
+
- Deposit-freshness verification (permit deadline) now uses the merchant's configured clock, consistent with the existing deadline-ordering and budget checks, instead of the wall clock.
|
|
36
|
+
|
|
37
|
+
## [0.2.0] - 2026-07-24
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- Verifying the payer's deposit authorization (`sender_auth`) now delegates to the shared `@atum-labs/payment-request-sender-auth` library instead of an embedded copy of the recover-and-verify logic, keeping the escrow method byte-compatible with the rest of the Atum payment stack.
|
|
41
|
+
|
|
42
|
+
## [0.1.1] - 2026-07-22
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
- Packaging for standalone publication: externalized `@solana/web3.js` so the LGPL-licensed `rpc-websockets` (pulled in transitively) is no longer bundled into the published package. Added `THIRD-PARTY-NOTICES.txt` and license materials. No API changes.
|
|
46
|
+
|
|
47
|
+
## [0.1.0] - 2026-07-21
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- Initial release: `mppx-atum-escrow`, Atum's cross-chain escrow payment method for the MPP (x402-style) payment flow — build, sign, and verify a source-chain deposit authorization across EVM, Tron, and Solana.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Proprietary software - not open source.**
|
|
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
|
|
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.
|
|
6
6
|
|
|
7
7
|
An [`mppx`](https://www.npmjs.com/package/mppx) payment method that lets a merchant accept
|
|
8
8
|
**cross-chain stablecoin payments** through Atum, over the Machine Payments Protocol (MPP).
|
|
@@ -48,12 +48,15 @@ Payer (client) Merchant (server) Atum Payment Gateway
|
|
|
48
48
|
|
|
49
49
|
1. The merchant answers a request with a `402` **challenge** describing what it receives
|
|
50
50
|
(destination asset, chain, exact amount) and the source option a payer may fund from. You build
|
|
51
|
-
this challenge from a **corridor** with `
|
|
51
|
+
this challenge from a **corridor** with `buildChargeChallenge`.
|
|
52
52
|
2. The payer's `registerClient` reads the challenge, builds an Atum payment request, signs the
|
|
53
53
|
source-chain deposit authorization for that chain, and returns it as an MPP **credential**.
|
|
54
54
|
3. The merchant's `registerServer` **verifies** the credential (recovers the signature, matches
|
|
55
|
-
every term against the challenge) and **submits** it to the Atum Payment Gateway
|
|
56
|
-
|
|
55
|
+
every term against the challenge) and **submits** it to the Atum Payment Gateway. Once the
|
|
56
|
+
payment settles it returns a **receipt** carrying the settlement confirmation. A payment that
|
|
57
|
+
settles more slowly than the gateway's synchronous window reports as *pending*, and the payer's
|
|
58
|
+
retry of the same purchase collects the result — see
|
|
59
|
+
[Settlement outcomes](#settlement-outcomes).
|
|
57
60
|
|
|
58
61
|
The source-chain authorization is built and signed through the Atum
|
|
59
62
|
[`@atumlabs/payment-gateway-client`](https://www.npmjs.com/package/@atumlabs/payment-gateway-client),
|
|
@@ -92,6 +95,7 @@ for the gateway connection and corridor defaults.
|
|
|
92
95
|
| **Escrow** | The source-chain contract the payer's funds lock into until the merchant is paid. |
|
|
93
96
|
| **Deadlines** | Two budgets in seconds: `quoteDeadlineSeconds` (how long the auction runs) and `fulfillmentDeadlineSeconds` (how long settlement may take). Required order: `now < quote < fulfillment`. |
|
|
94
97
|
| **`PaymentSubmitter`** | A small adapter *you* provide that hands a signed payment request to the Atum Payment Gateway and returns the result. |
|
|
98
|
+
| **`intentId`** | Identifies the *purchase* (your order or invoice id): one value per purchase, reused on every retry of it. The payment's identity derives from it, so it is what makes a retry safe. |
|
|
95
99
|
| **Receipt** | What `verify()` returns on success: the MPP receipt plus the full settlement confirmation. |
|
|
96
100
|
|
|
97
101
|
All addresses and amounts are strings; all amounts are **atomic units** (never floats). Addresses
|
|
@@ -104,7 +108,7 @@ import { Mppx } from "mppx/server";
|
|
|
104
108
|
import { PaymentGatewayClient } from "@atumlabs/payment-gateway-client";
|
|
105
109
|
import {
|
|
106
110
|
registerServer,
|
|
107
|
-
|
|
111
|
+
buildChargeChallenge,
|
|
108
112
|
corridorFromDefaults,
|
|
109
113
|
type AtumEscrowCorridor,
|
|
110
114
|
type PaymentSubmitter,
|
|
@@ -141,11 +145,14 @@ const corridor: AtumEscrowCorridor = await corridorFromDefaults(gateway, {
|
|
|
141
145
|
// You can also build a corridor by hand if you already hold the addresses — see AtumEscrowCorridor.
|
|
142
146
|
|
|
143
147
|
// 2. Provide the gateway adapter. The gateway response maps 1:1 onto what the method needs.
|
|
148
|
+
// Pass `status` through: it is what distinguishes a payment still settling from one that
|
|
149
|
+
// failed. Submit and return — do not poll for completion (see "Settlement outcomes").
|
|
144
150
|
const submitter: PaymentSubmitter = {
|
|
145
151
|
async submit(request) {
|
|
146
152
|
const res = await gateway.payments.submitPayment({ requestBody: request });
|
|
147
153
|
return {
|
|
148
154
|
payment_id: res.payment_id,
|
|
155
|
+
status: res.status,
|
|
149
156
|
fulfillment_confirmation: res.fulfillment_confirmation,
|
|
150
157
|
};
|
|
151
158
|
},
|
|
@@ -160,20 +167,27 @@ const mppx = Mppx.create({
|
|
|
160
167
|
methods: [method],
|
|
161
168
|
});
|
|
162
169
|
|
|
163
|
-
// 4. Guard a route.
|
|
170
|
+
// 4. Guard a route. buildChargeChallenge turns your corridor + a chosen source + the price into
|
|
164
171
|
// the challenge. Set the price per charge, so one corridor serves any amount.
|
|
165
172
|
app.get("/paid-resource", async (req) => {
|
|
166
|
-
const request =
|
|
173
|
+
const { request, meta } = buildChargeChallenge(
|
|
167
174
|
corridor,
|
|
168
175
|
{ network: "eip155:8453", asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" }, // offer Base USDC
|
|
169
176
|
"10000000", // receive exactly 10 USDC
|
|
177
|
+
// Identifies the purchase, NOT the attempt: the same value every time this order is
|
|
178
|
+
// re-offered or retried, a different one for a new order. See "Retries and idempotency".
|
|
179
|
+
{ intentId: orderIdFor(req) },
|
|
170
180
|
);
|
|
171
|
-
const result = await mppx.compose(["atum-escrow/charge", request])(req);
|
|
181
|
+
const result = await mppx.compose(["atum-escrow/charge", { ...request, meta }])(req);
|
|
172
182
|
if (result.status === 402) return result.challenge; // not paid yet — ask for payment
|
|
173
183
|
return result.withReceipt(new Response("here is your resource")); // paid & settled
|
|
174
184
|
});
|
|
175
185
|
```
|
|
176
186
|
|
|
187
|
+
> Before production: `mppx.fetch`/a single `verify()` call is enough for this quickstart, but not
|
|
188
|
+
> for a corridor whose settlement can genuinely take minutes — see
|
|
189
|
+
> [Settlement outcomes](#settlement-outcomes) for handling that without polling.
|
|
190
|
+
|
|
177
191
|
## Quick start — payer (client)
|
|
178
192
|
|
|
179
193
|
```ts
|
|
@@ -182,6 +196,7 @@ import { registerClient } from "@atumlabs/mppx-atum-escrow/client";
|
|
|
182
196
|
|
|
183
197
|
// The signer is chain-agnostic: pass private-key options (bound to the challenge's source chain
|
|
184
198
|
// automatically) or a ready-made SenderSigner. `account` is your source-chain address.
|
|
199
|
+
// Use a TESTNET-ONLY wallet for PAYER_PRIVATE_KEY while trying this out — never one holding real funds.
|
|
185
200
|
const method = registerClient({
|
|
186
201
|
signer: { privateKey: process.env.PAYER_PRIVATE_KEY! },
|
|
187
202
|
account: process.env.PAYER_ADDRESS!,
|
|
@@ -194,6 +209,10 @@ const res = await mppx.fetch("https://api.example.com/paid-resource");
|
|
|
194
209
|
const resource = await res.text();
|
|
195
210
|
```
|
|
196
211
|
|
|
212
|
+
> Before production: `mppx.fetch` retries only a few times with no delay, which isn't enough for a
|
|
213
|
+
> corridor whose settlement can genuinely take minutes — see
|
|
214
|
+
> [Settlement outcomes](#settlement-outcomes) for the retry pattern that handles it.
|
|
215
|
+
|
|
197
216
|
### Approving the source token
|
|
198
217
|
|
|
199
218
|
On EVM and Tron, the payer must approve the token-transfer contract (Permit2) to move the source
|
|
@@ -257,42 +276,182 @@ On success you get an `AtumEscrowReceipt` — the MPP receipt plus the full sett
|
|
|
257
276
|
|
|
258
277
|
## Retries and idempotency
|
|
259
278
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
279
|
+
A retry must never become a second charge, and two different purchases must never collapse into
|
|
280
|
+
one. Both come down to a single value: the **per-purchase identifier** the merchant stamps into the
|
|
281
|
+
challenge.
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
const { request, meta } = buildChargeChallenge(corridor, source, amount, {
|
|
285
|
+
intentId: order.id, // ← identifies the purchase
|
|
286
|
+
})
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The payer derives the payment's `request_id` from `intentId`, and the gateway de-duplicates on that
|
|
290
|
+
`request_id`. So the identifier's lifetime *is* the definition of "the same payment":
|
|
291
|
+
|
|
292
|
+
- **one value per purchase** — a value shared by two purchases makes the second resolve onto the
|
|
293
|
+
first and go unpaid (see the merchant-side defence below);
|
|
294
|
+
- **the same value on every retry** of that purchase, including every re-issued `402` for it — a
|
|
295
|
+
value that changes per attempt makes each retry a fresh charge;
|
|
296
|
+
- **never derived from the terms** — two orders for the same item have identical terms and must
|
|
297
|
+
still be two payments.
|
|
298
|
+
|
|
299
|
+
Use the merchant's own order, invoice, or cart id. Notably it is *not* the challenge id: that is an
|
|
300
|
+
HMAC over the challenge's own contents, so it moves whenever the expiry or the quote moves, neither
|
|
301
|
+
of which tracks the purchase.
|
|
302
|
+
|
|
303
|
+
A challenge carrying no identifier is **refused** by the payer's client rather than paid. There is
|
|
304
|
+
no fallback, deliberately: a per-attempt value would look like an idempotency key while letting
|
|
305
|
+
every retry be charged again.
|
|
306
|
+
|
|
307
|
+
On top of that, EVM and Tron sources get an on-chain backstop — the deposit nonce is derived from
|
|
308
|
+
the same identifier, and the escrow reverts a second deposit reusing it. Solana's replay nonce only
|
|
309
|
+
guards a short window, so there the gateway's `request_id` de-duplication is the durable protection.
|
|
310
|
+
|
|
311
|
+
### The merchant-side defence, and why you want it
|
|
312
|
+
|
|
313
|
+
Nothing can detect an identifier reused across two genuine purchases — the identifier *is* the
|
|
314
|
+
identity, so the second purchase resolves onto the first payment and only one payment is made. As the
|
|
315
|
+
merchant, you can close that on your own side.
|
|
316
|
+
|
|
317
|
+
Both purchases resolve to the **same payment**, so `verify()` hands you the **same receipt** for
|
|
318
|
+
both — the same `paymentId` and the same destination transaction. Key fulfilment on the receipt
|
|
319
|
+
rather than on the incoming request, and the second purchase is recognised as already-fulfilled
|
|
320
|
+
instead of being shipped a second time against a single payment.
|
|
321
|
+
|
|
322
|
+
Treat this as your responsibility rather than the network's: `atum-escrow` guarantees you will not be
|
|
323
|
+
paid twice for one identifier, and receipt-keyed fulfilment is how you avoid *delivering* twice for
|
|
324
|
+
one.
|
|
325
|
+
|
|
326
|
+
### Reusing an identifier with different terms
|
|
327
|
+
|
|
328
|
+
The gateway rejects a purchase identifier that comes back with different economics — who pays, who
|
|
329
|
+
receives, in which assets, or for how much — instead of silently resolving it to the first payment.
|
|
330
|
+
Surface it as a `PaymentRejectedError` from your submitter and the payer sees the cause; the fix is
|
|
331
|
+
always a distinct identifier per purchase.
|
|
332
|
+
|
|
333
|
+
## Settlement outcomes
|
|
334
|
+
|
|
335
|
+
A payment that settles returns a receipt. A payment that does not settle within the gateway's
|
|
336
|
+
synchronous window is **not** a failure, and the two are distinguished because they call for
|
|
337
|
+
opposite responses:
|
|
338
|
+
|
|
339
|
+
| `verify()` outcome | Meaning | What the payer should do |
|
|
340
|
+
| --- | --- | --- |
|
|
341
|
+
| receipt | settled | nothing — the resource is served |
|
|
342
|
+
| `SettlementPendingError` | accepted, still settling | re-attempt the **same** purchase; it resolves to this payment and returns its result |
|
|
343
|
+
| `SettlementFailedError` | terminal failure | start a **new** purchase under a new identifier — this one can never settle |
|
|
344
|
+
| `PaymentRejectedError` | refused, nothing charged | fix the request and pay the same purchase again |
|
|
345
|
+
|
|
346
|
+
The pending and failed rows call for **opposite** actions, and the reason is worth stating: a
|
|
347
|
+
terminal failure keeps its identifier. The gateway does not release it, so re-attempting that
|
|
348
|
+
purchase under the same `intentId` resolves to the same dead payment for good — which is exactly what
|
|
349
|
+
you want for a pending payment and exactly what you must not do after a terminal one. Recovering from
|
|
350
|
+
a terminal failure therefore means minting a **new** `intentId`; it is a new purchase as far as the
|
|
351
|
+
network is concerned. Getting these two the wrong way round is how a payer either charges twice or
|
|
352
|
+
waits forever.
|
|
353
|
+
|
|
354
|
+
### What re-attempting a purchase means
|
|
355
|
+
|
|
356
|
+
Run the purchase again: request a fresh challenge, sign it again, and keep the **same `intentId`**.
|
|
357
|
+
|
|
358
|
+
That split — rebuild the authorization, reuse the identifier — is the whole contract, and each half
|
|
359
|
+
matters for a different reason. The `intentId` must not change because the payment's identity derives
|
|
360
|
+
from it; change it and the re-attempt is a second charge rather than a retry. Everything time-bound
|
|
361
|
+
must change because `quote_deadline` and `fulfillment_deadline` are **absolute timestamps**, fixed at
|
|
362
|
+
the moment the challenge was built.
|
|
363
|
+
|
|
364
|
+
So a credential you have already signed cannot simply be presented a second time. Once its quote
|
|
365
|
+
window has closed, `verify()` refuses it and says so — and on Solana it is worse than a policy
|
|
366
|
+
refusal, because the deposit authorization carries its own on-chain replay window that expires along
|
|
367
|
+
with the deadlines.
|
|
368
|
+
|
|
369
|
+
A payment-enabled `fetch` handles the rebuild-and-resubmit mechanics for you, since every attempt
|
|
370
|
+
fetches a new challenge — but its own retry loop is a fixed, short number of attempts with no delay
|
|
371
|
+
between them (3, by default; configurable via `maxPaymentRetries`, but that only changes how many
|
|
372
|
+
times it hammers the endpoint, not how long it waits). That is enough when settlement finishes almost
|
|
373
|
+
immediately, but not for a corridor that can genuinely take minutes: raising `maxPaymentRetries`
|
|
374
|
+
turns "retry a few times" into "retry rapidly, still for no longer," which is not the same thing as
|
|
375
|
+
waiting for settlement. For that, drive the retry yourself, with a real interval between attempts.
|
|
376
|
+
|
|
377
|
+
The example below is a *payer* driving a *merchant* over real HTTP — two separate processes, not two
|
|
378
|
+
functions called back to back — since that is the case `mppx.fetch` does not cover and this package
|
|
379
|
+
provides no shortcut for. `method.createCredential` (payer) and the merchant's own endpoint are on
|
|
380
|
+
opposite ends of the request; nothing here runs the merchant's `verify()` in the payer's process.
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
import { Transport } from "mppx/client";
|
|
384
|
+
import type { AtumEscrowChallenge } from "@atumlabs/mppx-atum-escrow/client";
|
|
385
|
+
|
|
386
|
+
// `method` is the client from "Quick start — payer" above. The merchant is what keeps `intentId`
|
|
387
|
+
// stable across attempts (it must derive the same value from the order on every request); the
|
|
388
|
+
// payer never supplies or sees it directly, only the challenge that already carries it.
|
|
389
|
+
const transport = Transport.http();
|
|
390
|
+
|
|
391
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
392
|
+
// 1. Ask for the resource. The merchant answers 402 with a fresh challenge.
|
|
393
|
+
const firstResponse = await fetch(url);
|
|
394
|
+
if (!(await transport.isPaymentRequired(firstResponse))) return firstResponse; // free, or already paid
|
|
395
|
+
|
|
396
|
+
// 2. Build and sign a payment for THIS challenge. The challenge arrives off the wire, so confirm
|
|
397
|
+
// it is this scheme's before signing against it — a bare cast would sign whatever the
|
|
398
|
+
// response contained. Each attempt re-signs — a full round trip for a Turnkey/KMS signer — so
|
|
399
|
+
// pick an interval below with that cost in mind, not one copied from a lightweight status poller.
|
|
400
|
+
const offered = await transport.getChallenge(firstResponse);
|
|
401
|
+
if (offered.method !== "atum-escrow" || offered.intent !== "charge") {
|
|
402
|
+
throw new Error(`unexpected challenge ${offered.method}.${offered.intent}`);
|
|
403
|
+
}
|
|
404
|
+
const challenge = offered as AtumEscrowChallenge;
|
|
405
|
+
const credential = await method.createCredential({ challenge });
|
|
406
|
+
|
|
407
|
+
// 3. Resubmit with the credential attached. `response.ok` (200) is the only success case — a
|
|
408
|
+
// non-402 failure (e.g. PaymentRejectedError, a 400) is neither settled nor a fresh challenge
|
|
409
|
+
// to retry, so check for it explicitly rather than falling into the pending/failed handling
|
|
410
|
+
// below, which assumes a 402.
|
|
411
|
+
const response = await fetch(url, transport.setCredential({}, credential, { challenge }));
|
|
412
|
+
if (response.ok) return response; // paid & settled
|
|
413
|
+
if (!(await transport.isPaymentRequired(response))) {
|
|
414
|
+
const problem = await response.json();
|
|
415
|
+
throw new Error(problem.detail ?? `request failed with ${response.status}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Pending and failed are BOTH a 402 — the wire-visible discriminator is the problem-details
|
|
419
|
+
// `type`, which the mppx framework's error classes fix per outcome (see isSettlementPending /
|
|
420
|
+
// isSettlementFailed below for the merchant-side equivalent check). `paymentId` is its own
|
|
421
|
+
// field too, not just interpolated into `detail`'s prose, so the payer can log/reconcile it
|
|
422
|
+
// without parsing a sentence.
|
|
423
|
+
const problem = await response.json();
|
|
424
|
+
if (problem.type !== "https://paymentauth.org/problems/payment-action-required") {
|
|
425
|
+
throw new Error(problem.detail); // terminal — a new purchase needs a new intentId
|
|
426
|
+
}
|
|
427
|
+
// `payment-action-required` covers two different causes: a payment genuinely still settling
|
|
428
|
+
// (paymentId present — it reached the gateway) and this attempt's authorization having gone
|
|
429
|
+
// stale before it could be submitted (paymentId absent — nothing reached the gateway yet). Log
|
|
430
|
+
// accordingly rather than always saying "still settling", which is only true of the first.
|
|
431
|
+
console.log(
|
|
432
|
+
`attempt ${attempt}: ${problem.paymentId ? `still settling (payment ${problem.paymentId})` : "authorization expired before submission, retrying with a fresh one"}`,
|
|
433
|
+
);
|
|
434
|
+
await sleep(interval);
|
|
435
|
+
}
|
|
436
|
+
throw new Error(`purchase never settled after ${maxAttempts} attempts`);
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Both settlement errors carry the gateway's `paymentId` for reconciliation — as its own field on
|
|
440
|
+
`toProblemDetails()`, not only interpolated into `detail`'s prose — and both extend the framework's
|
|
441
|
+
error types, so a merchant that does not distinguish them still gets the standard `402` +
|
|
442
|
+
problem-details response. A same-process merchant discriminates with `isSettlementPending` /
|
|
443
|
+
`isSettlementFailed` / `isPaymentRejected` on the thrown error object; a payer, receiving only the
|
|
444
|
+
wire response, discriminates on `type` as shown above.
|
|
445
|
+
|
|
446
|
+
Because the payer's retry is what collects a pending result, a `PaymentSubmitter` should submit and
|
|
447
|
+
return what the gateway said — **not** poll for completion. Polling holds the merchant's request
|
|
448
|
+
open for the whole settlement window and hides the pending state the retry depends on.
|
|
288
449
|
|
|
289
450
|
## Scope
|
|
290
451
|
|
|
291
452
|
This version supports the **`charge`** intent with **EVM, Tron, and Solana** source chains and a
|
|
292
|
-
merchant-configured destination chain
|
|
293
|
-
|
|
294
|
-
budgets). One source option is offered per challenge; a corridor may list several and offer a
|
|
295
|
-
different one per `402`.
|
|
453
|
+
merchant-configured destination chain. One source option is offered per challenge; a corridor may
|
|
454
|
+
list several and offer a different one per `402`.
|
|
296
455
|
|
|
297
456
|
> **Solana deadline constraint:** a Solana deposit authorization expires within the escrow's fixed
|
|
298
457
|
> on-chain replay window (~2 minutes), independent of the corridor budget. So a corridor that
|
|
@@ -307,25 +466,31 @@ each side needs):
|
|
|
307
466
|
|
|
308
467
|
- `@atumlabs/mppx-atum-escrow` — everything below
|
|
309
468
|
- `@atumlabs/mppx-atum-escrow/client` — `registerClient`, `ensureSourceApproval` + payer types
|
|
310
|
-
- `@atumlabs/mppx-atum-escrow/server` — `registerServer`, `
|
|
311
|
-
`validateCorridor`, `corridorFromDefaults
|
|
469
|
+
- `@atumlabs/mppx-atum-escrow/server` — `registerServer`, `buildChargeChallenge`,
|
|
470
|
+
`buildChargeRequest`, `validateCorridor`, `corridorFromDefaults`, the settlement errors +
|
|
471
|
+
merchant types
|
|
312
472
|
|
|
313
473
|
| Export | Description |
|
|
314
474
|
| --- | --- |
|
|
315
475
|
| `registerClient(config)` | Payer-side method. `config`: `{ signer, account, now?, solanaClockReader? }`. |
|
|
316
476
|
| `registerServer(config)` | Merchant-side method. `config`: `{ submitter, now? }`. |
|
|
317
|
-
| `
|
|
318
|
-
| `
|
|
477
|
+
| `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 }`. |
|
|
478
|
+
| `buildChargeRequest(corridor, select, fulfillmentAmount, options?)` | The payment terms alone, without the identifier. For supplying challenge metadata by hand. |
|
|
479
|
+
| `validateCorridor(corridor)` | Validates a corridor's shape and per-source addresses (run automatically by both builders). |
|
|
319
480
|
| `corridorFromDefaults(defaults, params)` | Builds a corridor by fetching escrow/role/proxy addresses from the gateway `/defaults`. |
|
|
320
481
|
| `ensureSourceApproval(params)` | Ensures the payer has approved Permit2 to move the source token (EVM/Tron; no-op on Solana). |
|
|
482
|
+
| `SettlementPendingError`, `SettlementFailedError`, `PaymentRejectedError` | The non-receipt outcomes of `verify()`. See [Settlement outcomes](#settlement-outcomes). |
|
|
483
|
+
| `isSettlementPending`, `isSettlementFailed`, `isPaymentRejected` | Guards for the above. |
|
|
321
484
|
| `atumEscrowChargeMethod` | The base `mppx` method (advanced/custom wiring). |
|
|
322
485
|
| `ChargeRequestSchema`, `CredentialPayloadSchema` | The `zod` wire schemas. |
|
|
323
486
|
| `METHOD_NAME` (`"atum-escrow"`), `INTENT` (`"charge"`) | The method/intent identifiers. |
|
|
487
|
+
| `INTENT_ID_META_KEY` | The challenge-metadata key carrying the per-purchase identifier. |
|
|
324
488
|
|
|
325
489
|
Key types: `AtumEscrowCorridor`, `AtumEscrowSource`, `SenderSigner`, `SenderSignerOptions`,
|
|
326
|
-
`PaymentSubmitter`, `
|
|
327
|
-
`
|
|
328
|
-
`
|
|
490
|
+
`PaymentSubmitter`, `PaymentSubmitResult`, `PaymentSettlementStatus`, `AtumEscrowClientConfig`,
|
|
491
|
+
`AtumEscrowServerConfig`, `ChainDefaultsSource`, `EnsureApprovalResult`, `AtumEscrowChallenge`,
|
|
492
|
+
`AtumEscrowCredential`, `AtumEscrowReceipt`, `ChargeChallenge`, `ChargeRequest`,
|
|
493
|
+
`SettlementErrorDetails`, `PaymentRequest`, `FulfillmentConfirmation`.
|
|
329
494
|
|
|
330
495
|
> **EVM implementation note:** on EVM the deposit authorization is a Permit2
|
|
331
496
|
> `PermitWitnessTransferFrom` signature; Tron uses the equivalent TIP-712 typed data, and Solana
|