@atumlabs/mppx-atum-escrow 0.3.0 → 0.4.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 CHANGED
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.4.0] - 2026-09-16
11
+
12
+ ### Changed
13
+ - Clarified that the approval helpers raise an allowance and never lower one: a wallet already holding more than `approvalAmount` is left as it is and reports `alreadySufficient`. The wording said `approvalAmount` caps what the escrow can ever move, which is only true when no larger allowance already exists. Reducing or revoking an allowance is a separate operation these do not perform.
14
+ - `ensureSourceApproval` refuses to send when the client that would sign is not the `owner` whose allowance it read. An approval only ever applies to the account that sends it, so a mismatched signer approved that account instead, returned a transaction hash that read as success, and left the owner exactly as unable to pay as before — a silent failure that surfaced only as a settlement revert. Read-only `needsSourceApproval` is unaffected: asking whether a payer is ready without holding their keys is a legitimate preflight, and a client that cannot report which account it signs as is not treated as a mismatch.
15
+ - Tron approvals wait for confirmation before returning, so the hash they report means mined rather than broadcast — a payer that deposited immediately afterwards would otherwise race their own approval. The EVM path already waited; what is new there is a bound on how long it will wait (`confirmation.timeoutMs`, one minute by default) instead of waiting forever, and an `onSubmitted` callback that hands you the hash the moment it is broadcast so a stuck approval can still be looked up.
16
+ - On EVM, `ensureSourceApproval` resets an allowance to zero before setting a new one, but only where the token demands it. Some tokens — mainnet USDT most notably — refuse to move an allowance straight from one non-zero value to another, so a payer holding any leftover allowance could not be approved at all and saw a bare on-chain revert. The check is a simulated call that costs nothing and sends nothing, and a wallet with no allowance skips it entirely, so an ordinary approval still takes exactly one transaction. Tron does not do this: an ordinary TRC-20 accepts the change in place, and guessing otherwise would burn an extra transaction at Tron's fee limit — if a Tron token ever does refuse, the error names the manual remedy.
17
+ - The approval helpers are now one implementation shared by this package, the x402 scheme and the payment-gateway client, so the three cannot drift apart on what counts as a sufficient allowance. The exported names and their behaviour are unchanged.
18
+ - `ChargeRequest` declares the Solana `issuedAt` on its `extra` itself, instead of inheriting it
19
+ from the shared x402 request type. That type dropped the field — an x402 resource server rebuilds
20
+ its payment requirements on the paid request and compares them against what the payer echoed, and
21
+ a clock reading cannot survive that comparison. MPP has no such rebuild, so a merchant-stamped
22
+ issue time stays valid here and `buildChargeRequest` still emits it exactly as before. Type-only:
23
+ the emitted JavaScript is byte-identical, so nothing changes at runtime and no code needs editing
24
+ on upgrade.
25
+ - A destination identifier whose CAIP namespaces are not lowercase, such as `SOLANA:` or `SPL:`, is now accepted on Solana. Previously that chain compared the whole identifier byte for byte and refused it. CAIP pins both namespaces lowercase, so this only forgives a spelling the schema already treats as equivalent.
26
+
27
+ ### Added
28
+ - **`isUnconfirmed`**, for telling a broadcast-but-unseen approval apart from one that failed. The wait for a confirmation is bounded, and reaching that bound is not a failure: the transaction may confirm a moment later. The two need opposite responses — a failure needs another approval, an unconfirmed one needs the transaction checked and nothing else — and a caller that treats them alike pays for an allowance they are already getting. Previously the distinction existed only as an undocumented property on the error.
29
+ - **`needsSourceApproval`**, the read-only counterpart to `ensureSourceApproval`. It takes the same arguments and answers the same question, but only reads the allowance: no transaction, no gas. Use it to tell a payer that a one-off approval is due before asking them to sign, instead of letting them find out when one is broadcast.
30
+ - **`ensureSourceApproval` now works on Tron.** It previously documented Tron support it did not have — the implementation reached the chain through ethers, which cannot talk to Tron, so a Tron source failed either for want of a `spender` or somewhere inside ethers. Pass a TronWeb instance as `tronWeb` in place of the ethers `signer`, and the Permit2 address for the network is resolved for you rather than demanded as `spender`. `tronweb` is not a dependency of this package; the caller constructs the instance, exactly as they already construct the ethers signer.
31
+ - **`approvalAmount`**, for granting a bounded approval instead of an unlimited one, capping what the escrow can ever move. On its own the amount is its own requirement, so an allowance still holding the full bound is left alone rather than re-approved — the right answer for a one-off approval. Across repeated payments, pair it with `requiredAllowance`: a bound measured against itself stops covering itself the moment the first charge is decremented from it, and every later call would send another approval. The trade-off to weigh is that Permit2 decrements the allowance on every payment, so a bounded approval is consumed and eventually has to be granted again. Asking to approve less than an explicit `requiredAllowance` is refused outright, since that approval could never satisfy the charge it was granted for.
32
+ - `AtumEscrowServer`, the type `registerServer` returns, for naming the method where it is built in one place and registered in another. Its `verify()` now resolves with an `AtumEscrowReceipt`, so `fulfillmentConfirmation` is available directly on the result: any cast you added to reach it can be removed. Casts already in place continue to compile, so this needs no change on upgrade.
33
+
34
+ ### Fixed
35
+ - **`ensureSourceApproval` no longer sends a fresh approval before every payment after the first.** Permit2 decrements the payer's allowance on each transfer it makes, so an allowance granted as unlimited stops being exactly `MaxUint256` the moment the first payment settles. Sufficiency was measured against that exact value, so from the second payment onward the check never short-circuited again and every charge was preceded by another approval transaction. The payments were correct; the payer was buying an allowance they already held. Sufficiency is now measured against half of `MaxUint256`, which separates an unlimited approval that has been partly spent from one a payer bounded deliberately — crossing that floor would take more token units than any supply contains. Nothing changes for a caller passing `requiredAllowance`: that comparison was already right, and only the unlimited case was wrong.
36
+ - A payer whose `signer` option carries a blank `pinnedAddress` falls back to the payer's own account. Blank covers an empty string, which is how an unset variable arrives, and a whitespace only value, which is how one read from a file or a padded shell variable arrives. It previously passed the value straight through, which the payment-gateway client now refuses instead of reading as "no pin", and the two blank forms took different paths.
37
+ - `verify()` compares the payer's destination asset identifier against the merchant's advertised one part by part, instead of lowercasing the whole string for every chain except Solana. A Tron `trc20:` reference is base58, where capitalisation is part of the value, so the old fold treated two distinct tokens as the same one on a field the deposit signature does not cover. The token reference and a Solana chain reference are now compared the way the neighbouring address comparison already documented, and only the two CAIP namespaces fold.
38
+
8
39
  ## [0.3.0] - 2026-07-30
9
40
 
10
41
  ### Added
package/README.md CHANGED
@@ -6,6 +6,7 @@
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).
9
+ Full documentation: [docs.atum.xyz](https://docs.atum.xyz).
9
10
 
10
11
  - The **payer** funds the payment in any supported source asset and chain (e.g. USDC on Base,
11
12
  USDT on Tron, or USDC on Solana).
@@ -81,9 +82,6 @@ The merchant examples below also use
81
82
  [`@atumlabs/payment-gateway-client`](https://www.npmjs.com/package/@atumlabs/payment-gateway-client)
82
83
  for the gateway connection and corridor defaults.
83
84
 
84
- > The snippets below are illustrative. For complete, runnable, end-to-end integrations
85
- > (payer + merchant), see the examples repository: **https://github.com/Atum-Labs/examples**.
86
-
87
85
  ## Key concepts
88
86
 
89
87
  | Term | What it means |
@@ -101,6 +99,18 @@ for the gateway connection and corridor defaults.
101
99
  All addresses and amounts are strings; all amounts are **atomic units** (never floats). Addresses
102
100
  are in each chain's native form — `0x…` hex for EVM, base58 for Tron and Solana.
103
101
 
102
+ ### Which chains and assets can a corridor use?
103
+
104
+ Atum supports many corridors. The identifier for every supported token is listed under
105
+ [supported assets](https://docs.atum.xyz/get-started/reference/supported-assets), and the chain ids
106
+ under [supported networks](https://docs.atum.xyz/get-started/reference/supported-networks). Assets
107
+ are named with [CAIP-19](https://chainagnostic.org/CAIPs/caip-19) identifiers, for example
108
+ `eip155:84532/erc20:0x036CbD53842c5426634e7929541eC2318f3dCF7e` for USDC on Base Sepolia.
109
+
110
+ Copy identifiers exactly: base58 values, such as Solana token mints and account addresses, are
111
+ case-sensitive. The escrow and role addresses for each chain are filled in by
112
+ `corridorFromDefaults`, so you never paste those by hand.
113
+
104
114
  ## Quick start — merchant (server)
105
115
 
106
116
  ```ts
@@ -231,11 +241,66 @@ await ensureSourceApproval({
231
241
  owner: wallet.address,
232
242
  signer: wallet,
233
243
  // Pass the source cap this charge needs (from the challenge). A leftover smaller allowance
234
- // then won't be mistaken for enough. Omit it to ensure an unlimited approval instead.
244
+ // then won't be mistaken for enough.
235
245
  requiredAllowance: BigInt(challenge.request.source.amount),
236
246
  });
237
247
  ```
238
248
 
249
+ Paying from Tron takes a TronWeb instance instead of an ethers signer; everything else is the
250
+ same, and the Permit2 address for the network is resolved for you:
251
+
252
+ ```ts
253
+ await ensureSourceApproval({ network: "tron:mainnet", token, owner, tronWeb });
254
+ ```
255
+
256
+ The approval waits to be mined before returning, bounded by `confirmation.timeoutMs` (one minute
257
+ by default); a timeout is reported as unconfirmed rather than failed, since the transaction may
258
+ still land. `onSubmitted` hands you the hash the moment it is broadcast.
259
+
260
+ `isUnconfirmed(error)` tells the two apart. It matters because the responses are opposite: a
261
+ failed approval needs another one, an unconfirmed approval needs a look at the transaction and
262
+ nothing else. Sending a second one on top of the first only pays for an allowance you are already
263
+ getting.
264
+
265
+ ```ts
266
+ try {
267
+ await ensureSourceApproval({ network, token, owner, signer });
268
+ } catch (error) {
269
+ if (isUnconfirmed(error)) {
270
+ // Broadcast, not yet seen to confirm. Check the hash from onSubmitted; do not re-send.
271
+ } else {
272
+ throw error;
273
+ }
274
+ }
275
+ ```
276
+
277
+ On EVM, tokens that refuse to overwrite a non-zero allowance — mainnet USDT most notably — are
278
+ reset to zero first, automatically and only where the token demands it; the result then carries a
279
+ `resetTxHash` too. Tron does not do this, because an ordinary TRC-20 accepts the change in place.
280
+
281
+ The approval sent is unlimited, so later charges on the same token need no further transaction.
282
+ To bound the approval that gets sent, pass `approvalAmount`. On its own the amount is treated as
283
+ its own requirement, so an allowance still holding the full bound is left alone rather than
284
+ re-approved — the right answer for a one-off approval. Across repeated payments, pair it with
285
+ `requiredAllowance`: Permit2 decrements the allowance on every payment, so a bound measured
286
+ against itself stops covering itself the moment the first charge lands, and every later call
287
+ would send another approval. Either way a bounded approval is consumed as it is spent and
288
+ eventually has to be granted again.
289
+
290
+ These helpers raise an allowance to what a payment needs; they never lower one. A wallet already
291
+ holding more than `approvalAmount` is left exactly as it is and reports `alreadySufficient`
292
+ without sending anything. Reducing or revoking an allowance is a separate operation, and not one
293
+ these perform.
294
+
295
+ `needsSourceApproval(params)` takes the same arguments and answers the same question without
296
+ sending anything, so it is safe to call before every charge:
297
+
298
+ ```ts
299
+ if (await needsSourceApproval({ network, token, owner, signer })) {
300
+ // tell the payer a one-off approval transaction is coming, before asking them to sign
301
+ }
302
+ ```
303
+
239
304
  ## What the merchant `verify()` guarantees
240
305
 
241
306
  `verify()` fails fast (no chain I/O) before submitting, and throws if any check fails. The Atum
@@ -274,6 +339,9 @@ On success you get an `AtumEscrowReceipt` — the MPP receipt plus the full sett
274
339
  }
275
340
  ```
276
341
 
342
+ `verify()` resolves with this type, so `fulfillmentConfirmation` is available straight off the
343
+ result. `AtumEscrowReceipt` is exported, so you can name it in your own function signatures.
344
+
277
345
  ## Retries and idempotency
278
346
 
279
347
  A retry must never become a second charge, and two different purchases must never collapse into
@@ -465,7 +533,7 @@ Import everything from the root, or from the role-specific entry points (which e
465
533
  each side needs):
466
534
 
467
535
  - `@atumlabs/mppx-atum-escrow` — everything below
468
- - `@atumlabs/mppx-atum-escrow/client` — `registerClient`, `ensureSourceApproval` + payer types
536
+ - `@atumlabs/mppx-atum-escrow/client` — `registerClient`, `ensureSourceApproval`, `needsSourceApproval` + payer types
469
537
  - `@atumlabs/mppx-atum-escrow/server` — `registerServer`, `buildChargeChallenge`,
470
538
  `buildChargeRequest`, `validateCorridor`, `corridorFromDefaults`, the settlement errors +
471
539
  merchant types
@@ -473,12 +541,13 @@ each side needs):
473
541
  | Export | Description |
474
542
  | --- | --- |
475
543
  | `registerClient(config)` | Payer-side method. `config`: `{ signer, account, now?, solanaClockReader? }`. |
476
- | `registerServer(config)` | Merchant-side method. `config`: `{ submitter, now? }`. |
544
+ | `registerServer(config)` | Merchant-side method. `config`: `{ submitter, now? }`. Returns an `AtumEscrowServer`, whose `verify()` resolves with an `AtumEscrowReceipt`. |
477
545
  | `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
546
  | `buildChargeRequest(corridor, select, fulfillmentAmount, options?)` | The payment terms alone, without the identifier. For supplying challenge metadata by hand. |
479
547
  | `validateCorridor(corridor)` | Validates a corridor's shape and per-source addresses (run automatically by both builders). |
480
548
  | `corridorFromDefaults(defaults, params)` | Builds a corridor by fetching escrow/role/proxy addresses from the gateway `/defaults`. |
481
- | `ensureSourceApproval(params)` | Ensures the payer has approved Permit2 to move the source token (EVM/Tron; no-op on Solana). |
549
+ | `ensureSourceApproval(params)` | Ensures the payer has approved Permit2 to move the source token. `signer` for EVM, `tronWeb` for Tron; no-op on Solana. |
550
+ | `needsSourceApproval(params)` | Whether `ensureSourceApproval` would send a transaction. Read-only, spends no gas. |
482
551
  | `SettlementPendingError`, `SettlementFailedError`, `PaymentRejectedError` | The non-receipt outcomes of `verify()`. See [Settlement outcomes](#settlement-outcomes). |
483
552
  | `isSettlementPending`, `isSettlementFailed`, `isPaymentRejected` | Guards for the above. |
484
553
  | `atumEscrowChargeMethod` | The base `mppx` method (advanced/custom wiring). |
@@ -488,10 +557,17 @@ each side needs):
488
557
 
489
558
  Key types: `AtumEscrowCorridor`, `AtumEscrowSource`, `SenderSigner`, `SenderSignerOptions`,
490
559
  `PaymentSubmitter`, `PaymentSubmitResult`, `PaymentSettlementStatus`, `AtumEscrowClientConfig`,
491
- `AtumEscrowServerConfig`, `ChainDefaultsSource`, `EnsureApprovalResult`, `AtumEscrowChallenge`,
492
- `AtumEscrowCredential`, `AtumEscrowReceipt`, `ChargeChallenge`, `ChargeRequest`,
560
+ `AtumEscrowServer`, `AtumEscrowServerConfig`, `ChainDefaultsSource`, `EnsureApprovalResult`,
561
+ `AtumEscrowChallenge`, `AtumEscrowCredential`, `AtumEscrowReceipt`, `ChargeChallenge`, `ChargeRequest`,
493
562
  `SettlementErrorDetails`, `PaymentRequest`, `FulfillmentConfirmation`.
494
563
 
495
564
  > **EVM implementation note:** on EVM the deposit authorization is a Permit2
496
565
  > `PermitWitnessTransferFrom` signature; Tron uses the equivalent TIP-712 typed data, and Solana
497
566
  > uses an ed25519-signed deposit. The public API is the same across all three.
567
+
568
+ ## Further reading
569
+
570
+ - [Accepting and making MPP payments with Atum](https://docs.atum.xyz/payment-protocols/mpp/overview)
571
+ - [Supported assets](https://docs.atum.xyz/get-started/reference/supported-assets) and [supported networks](https://docs.atum.xyz/get-started/reference/supported-networks)
572
+ - [MPP](https://mpp.dev)
573
+ - [Atum documentation](https://docs.atum.xyz)
@@ -2,6 +2,13 @@ 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
7
+ package includes the third-party software identified below; the licenses and
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.
11
+
5
12
  The published artifact of this package (the compiled code under dist/) statically
6
13
  bundles the third-party open-source components listed below; their source is
7
14
  redistributed within this package, so their license and copyright notices are