@atumlabs/mppx-atum-escrow 0.1.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 +328 -0
- package/dist/chunk-V2SJ747H.js +131 -0
- package/dist/chunk-WWSNMK7N.js +50018 -0
- package/dist/chunk-XDYCJ36Z.js +1410 -0
- package/dist/client.d.ts +126 -0
- package/dist/client.js +21 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +31 -0
- package/dist/internal-CjcEyEsm.d.ts +842 -0
- package/dist/server.d.ts +230 -0
- package/dist/server.js +25 -0
- package/package.json +86 -0
package/README.md
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# @atumlabs/mppx-atum-escrow
|
|
2
|
+
|
|
3
|
+
An [`mppx`](https://www.npmjs.com/package/mppx) payment method that lets a merchant accept
|
|
4
|
+
**cross-chain stablecoin payments** through Atum, over the Machine Payments Protocol (MPP).
|
|
5
|
+
|
|
6
|
+
- The **payer** funds the payment in any supported source asset and chain (e.g. USDC on Base,
|
|
7
|
+
USDT on Tron, or USDC on Solana).
|
|
8
|
+
- The **merchant** receives an exact, pinned amount of the asset it chose, on the chain it chose
|
|
9
|
+
(e.g. USDC on Arbitrum).
|
|
10
|
+
- Atum's escrow, auction, and settlement network bridges the two. Neither side has to hold or
|
|
11
|
+
move the other's asset.
|
|
12
|
+
|
|
13
|
+
If you have used MPP with a card or same-chain method before, this is the same integration shape
|
|
14
|
+
— you register a method and MPP does the 402 dance for you — except the money can cross chains.
|
|
15
|
+
|
|
16
|
+
## What is MPP, in one paragraph
|
|
17
|
+
|
|
18
|
+
MPP is an "HTTP 402" protocol: a client requests a resource, the server answers `402 Payment
|
|
19
|
+
Required` with a machine-readable description of how to pay, the client pays and retries, and the
|
|
20
|
+
server returns `200` plus a receipt. Unlike some 402 protocols, **MPP has no central "facilitator"
|
|
21
|
+
service** — the logic that validates and settles a payment runs **inside the merchant's own
|
|
22
|
+
process**, as a *payment method* plugged into the `mppx` SDK. This package **is** that method for
|
|
23
|
+
Atum. Its one `verify()` hook both checks the payment and settles it through the Atum Payment
|
|
24
|
+
Gateway.
|
|
25
|
+
|
|
26
|
+
## How a payment flows
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
Payer (client) Merchant (server) Atum Payment Gateway
|
|
30
|
+
| | |
|
|
31
|
+
| ─ GET /paid-resource ────> | |
|
|
32
|
+
| <─ 402 + atum-escrow ───── | (what to pay, where it lands) |
|
|
33
|
+
| challenge | |
|
|
34
|
+
| (registerClient builds & | |
|
|
35
|
+
| signs the deposit auth) | |
|
|
36
|
+
| ─ retry + credential ────> | |
|
|
37
|
+
| | (registerServer.verify: |
|
|
38
|
+
| | checks signature & terms) |
|
|
39
|
+
| | ─ submit payment request ────> |
|
|
40
|
+
| | <─ FulfillmentConfirmation ─── |
|
|
41
|
+
| <─ 200 + resource + ────── | |
|
|
42
|
+
| receipt | |
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
1. The merchant answers a request with a `402` **challenge** describing what it receives
|
|
46
|
+
(destination asset, chain, exact amount) and the source option a payer may fund from. You build
|
|
47
|
+
this challenge from a **corridor** with `buildChargeRequest`.
|
|
48
|
+
2. The payer's `registerClient` reads the challenge, builds an Atum payment request, signs the
|
|
49
|
+
source-chain deposit authorization for that chain, and returns it as an MPP **credential**.
|
|
50
|
+
3. The merchant's `registerServer` **verifies** the credential (recovers the signature, matches
|
|
51
|
+
every term against the challenge) and **submits** it to the Atum Payment Gateway, blocking until
|
|
52
|
+
settlement completes. On success it returns a **receipt** carrying the settlement confirmation.
|
|
53
|
+
|
|
54
|
+
The source-chain authorization is built and signed through the Atum
|
|
55
|
+
[`@atumlabs/payment-gateway-client`](https://www.npmjs.com/package/@atumlabs/payment-gateway-client),
|
|
56
|
+
so **one code path supports EVM, Tron, and Solana sources** — you configure a source
|
|
57
|
+
and the method picks the right signing scheme for its chain.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
npm install @atumlabs/mppx-atum-escrow
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This package is an `mppx` plugin, so **`mppx` and `zod` are peer dependencies** — it registers into
|
|
66
|
+
*your* `mppx` instance and must share the same `mppx` and `zod` as the rest of your integration.
|
|
67
|
+
Install them if your project doesn't already depend on them:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npm install mppx zod
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The merchant examples below also use
|
|
74
|
+
[`@atumlabs/payment-gateway-client`](https://www.npmjs.com/package/@atumlabs/payment-gateway-client)
|
|
75
|
+
for the gateway connection and corridor defaults.
|
|
76
|
+
|
|
77
|
+
> The snippets below are illustrative. For complete, runnable, end-to-end integrations
|
|
78
|
+
> (payer + merchant), see the examples repository: **https://github.com/Atum-Labs/examples**.
|
|
79
|
+
|
|
80
|
+
## Key concepts
|
|
81
|
+
|
|
82
|
+
| Term | What it means |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| **Corridor** | The merchant's payment configuration: what it receives (destination), and which source chains/tokens it accepts. You define it once. |
|
|
85
|
+
| **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)`. |
|
|
86
|
+
| **`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. |
|
|
87
|
+
| **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. |
|
|
88
|
+
| **Escrow** | The source-chain contract the payer's funds lock into until the merchant is paid. |
|
|
89
|
+
| **Deadlines** | Two budgets in seconds: `quoteDeadlineSeconds` (how long the auction runs) and `fulfillmentDeadlineSeconds` (how long settlement may take). Required order: `now < quote < fulfillment`. |
|
|
90
|
+
| **`PaymentSubmitter`** | A small adapter *you* provide that hands a signed payment request to the Atum Payment Gateway and returns the result. |
|
|
91
|
+
| **Receipt** | What `verify()` returns on success: the MPP receipt plus the full settlement confirmation. |
|
|
92
|
+
|
|
93
|
+
All addresses and amounts are strings; all amounts are **atomic units** (never floats). Addresses
|
|
94
|
+
are in each chain's native form — `0x…` hex for EVM, base58 for Tron and Solana.
|
|
95
|
+
|
|
96
|
+
## Quick start — merchant (server)
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { Mppx } from "mppx/server";
|
|
100
|
+
import { PaymentGatewayClient } from "@atumlabs/payment-gateway-client";
|
|
101
|
+
import {
|
|
102
|
+
registerServer,
|
|
103
|
+
buildChargeRequest,
|
|
104
|
+
corridorFromDefaults,
|
|
105
|
+
type AtumEscrowCorridor,
|
|
106
|
+
type PaymentSubmitter,
|
|
107
|
+
} from "@atumlabs/mppx-atum-escrow/server";
|
|
108
|
+
|
|
109
|
+
const gateway = new PaymentGatewayClient({ BASE: "https://gateway.example.com" });
|
|
110
|
+
|
|
111
|
+
// 1. Describe your corridor: you receive 10 USDC on Arbitrum, and accept USDC on Base and
|
|
112
|
+
// USDT on Tron as sources. `corridorFromDefaults` fills the escrow/role/proxy addresses
|
|
113
|
+
// from the gateway, so you only specify what you receive, the sources, and your budgets.
|
|
114
|
+
const corridor: AtumEscrowCorridor = await corridorFromDefaults(gateway, {
|
|
115
|
+
destination: {
|
|
116
|
+
network: "eip155:42161", // Arbitrum One (CAIP-2)
|
|
117
|
+
asset: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum
|
|
118
|
+
account: "0xYourMerchantReceiveAddress",
|
|
119
|
+
},
|
|
120
|
+
sources: [
|
|
121
|
+
// One entry per source chain; list several tokens in `assets` to accept more than one
|
|
122
|
+
// on that chain (they share the same escrow/role addresses — no need to duplicate).
|
|
123
|
+
{
|
|
124
|
+
network: "eip155:8453", // Base
|
|
125
|
+
assets: [
|
|
126
|
+
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC
|
|
127
|
+
"0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2", // USDT
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{ network: "tron:mainnet", assets: ["TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"] }, // USDT on Tron
|
|
131
|
+
],
|
|
132
|
+
markupBps: 300, // allow up to +3% on the source side to cover the spread
|
|
133
|
+
quoteDeadlineSeconds: 20,
|
|
134
|
+
fulfillmentDeadlineSeconds: 300,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// You can also build a corridor by hand if you already hold the addresses — see AtumEscrowCorridor.
|
|
138
|
+
|
|
139
|
+
// 2. Provide the gateway adapter. The gateway response maps 1:1 onto what the method needs.
|
|
140
|
+
const submitter: PaymentSubmitter = {
|
|
141
|
+
async submit(request) {
|
|
142
|
+
const res = await gateway.payments.submitPayment({ requestBody: request });
|
|
143
|
+
return {
|
|
144
|
+
payment_id: res.payment_id,
|
|
145
|
+
fulfillment_confirmation: res.fulfillment_confirmation,
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// 3. Register the method and create your mppx server. The corridor is NOT part of the server
|
|
151
|
+
// config — verification trusts the signed challenge, so one registration serves every corridor.
|
|
152
|
+
const method = registerServer({ submitter });
|
|
153
|
+
const mppx = Mppx.create({
|
|
154
|
+
realm: "api.example.com",
|
|
155
|
+
secretKey: process.env.MPP_SECRET_KEY, // recommended: binds each challenge to its contents
|
|
156
|
+
methods: [method],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// 4. Guard a route. buildChargeRequest turns your corridor + a chosen source + the price into
|
|
160
|
+
// the challenge. Set the price per charge, so one corridor serves any amount.
|
|
161
|
+
app.get("/paid-resource", async (req) => {
|
|
162
|
+
const request = buildChargeRequest(
|
|
163
|
+
corridor,
|
|
164
|
+
{ network: "eip155:8453", asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" }, // offer Base USDC
|
|
165
|
+
"10000000", // receive exactly 10 USDC
|
|
166
|
+
);
|
|
167
|
+
const result = await mppx.compose(["atum-escrow/charge", request])(req);
|
|
168
|
+
if (result.status === 402) return result.challenge; // not paid yet — ask for payment
|
|
169
|
+
return result.withReceipt(new Response("here is your resource")); // paid & settled
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Quick start — payer (client)
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { Mppx } from "mppx/client";
|
|
177
|
+
import { registerClient } from "@atumlabs/mppx-atum-escrow/client";
|
|
178
|
+
|
|
179
|
+
// The signer is chain-agnostic: pass private-key options (bound to the challenge's source chain
|
|
180
|
+
// automatically) or a ready-made SenderSigner. `account` is your source-chain address.
|
|
181
|
+
const method = registerClient({
|
|
182
|
+
signer: { privateKey: process.env.PAYER_PRIVATE_KEY! },
|
|
183
|
+
account: process.env.PAYER_ADDRESS!,
|
|
184
|
+
});
|
|
185
|
+
const mppx = Mppx.create({ methods: [method] });
|
|
186
|
+
|
|
187
|
+
// mppx.fetch handles the 402 automatically: it reads the atum-escrow challenge, builds and
|
|
188
|
+
// signs the payment, retries with the credential attached, and returns the final 200 response.
|
|
189
|
+
const res = await mppx.fetch("https://api.example.com/paid-resource");
|
|
190
|
+
const resource = await res.text();
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Approving the source token
|
|
194
|
+
|
|
195
|
+
On EVM and Tron, the payer must approve the token-transfer contract (Permit2) to move the source
|
|
196
|
+
token before paying, or the escrow deposit reverts at settlement. `ensureSourceApproval` reads the
|
|
197
|
+
current allowance and sends an approval only if it falls short (it is a no-op on Solana, which
|
|
198
|
+
authorizes the transfer in the signed deposit itself):
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { Wallet, JsonRpcProvider } from "ethers";
|
|
202
|
+
import { ensureSourceApproval } from "@atumlabs/mppx-atum-escrow/client";
|
|
203
|
+
|
|
204
|
+
const wallet = new Wallet(process.env.PAYER_PRIVATE_KEY!, new JsonRpcProvider(RPC_URL));
|
|
205
|
+
await ensureSourceApproval({
|
|
206
|
+
network: "eip155:8453",
|
|
207
|
+
token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
|
|
208
|
+
owner: wallet.address,
|
|
209
|
+
signer: wallet,
|
|
210
|
+
// Pass the source cap this charge needs (from the challenge). A leftover smaller allowance
|
|
211
|
+
// then won't be mistaken for enough. Omit it to ensure an unlimited approval instead.
|
|
212
|
+
requiredAllowance: BigInt(challenge.request.source.amount),
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## What the merchant `verify()` guarantees
|
|
217
|
+
|
|
218
|
+
`verify()` fails fast (no chain I/O) before submitting, and throws if any check fails. The Atum
|
|
219
|
+
Payment Gateway independently re-validates everything on submit; these checks just reject
|
|
220
|
+
obviously-bad input early:
|
|
221
|
+
|
|
222
|
+
- the source-chain deposit signature **recovers to the payer's own `source.account`** (an EVM/Tron
|
|
223
|
+
secp256k1 recovery, or a Solana ed25519 verification against the signer's public key);
|
|
224
|
+
- the signed deposit's **token, escrow, and witness roles** match the challenge's source option;
|
|
225
|
+
- `max_source_amount` **does not exceed the source cap**, and the deposit authorizes exactly that;
|
|
226
|
+
- the **destination account, asset, and `fulfillment_amount`** are exactly what the merchant
|
|
227
|
+
advertised (the deposit signature does not bind the receive side, so the method does);
|
|
228
|
+
- the **settlement-routing fields** — `escrow_contract_address`, `quote_selector`,
|
|
229
|
+
`fulfillment_proxy`, and `fulfillment_verifier` — match the challenge. These travel in
|
|
230
|
+
plaintext and aren't covered by the deposit signature, so the method checks them explicitly
|
|
231
|
+
to stop a payer from redirecting settlement;
|
|
232
|
+
- the deadlines satisfy `now < quote_deadline < fulfillment_deadline`, and do not exceed the
|
|
233
|
+
challenge's advertised budgets; and (for EVM/Tron) the deposit authorization **does not expire
|
|
234
|
+
before `fulfillment_deadline`**, so a slow-but-valid settlement never lands on an expired signature.
|
|
235
|
+
|
|
236
|
+
On success you get an `AtumEscrowReceipt` — the MPP receipt plus the full settlement confirmation:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
{
|
|
240
|
+
method: "atum-escrow",
|
|
241
|
+
status: "success",
|
|
242
|
+
timestamp: string, // settlement time (ISO 8601)
|
|
243
|
+
reference: string, // destination-chain transaction hash
|
|
244
|
+
fulfillmentConfirmation: { // full settlement details
|
|
245
|
+
payment_id: string,
|
|
246
|
+
request_id: string,
|
|
247
|
+
destination_chain_id: string, // CAIP-2, e.g. "eip155:42161"
|
|
248
|
+
destination_tx_hash: string,
|
|
249
|
+
// …
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Retries and idempotency
|
|
255
|
+
|
|
256
|
+
Two layers protect a retry, and they protect **different** things:
|
|
257
|
+
|
|
258
|
+
1. **Build once, resubmit identical bytes — returns the original result.** `mppx.fetch` builds one
|
|
259
|
+
credential per payment and reuses it across its automatic retries, so the gateway sees identical
|
|
260
|
+
content, deduplicates, and returns the *original* result (including the receipt). This is the
|
|
261
|
+
only layer that gives you a graceful retry. Driving the flow manually, do the same — never
|
|
262
|
+
rebuild for a retry.
|
|
263
|
+
2. **Deterministic nonce — charge-safe, but not retry-graceful.** For EVM and Tron sources the
|
|
264
|
+
deposit nonce and `request_id` are derived from the challenge id, so even a rebuild for the
|
|
265
|
+
*same* challenge reuses the same nonce; the escrow consumes a nonce on the first deposit and
|
|
266
|
+
reverts any second, so **at most one payment ever settles on-chain**. This is only
|
|
267
|
+
*charge-safety*: a rebuild's deadlines are wall-clock, so its content differs and the gateway
|
|
268
|
+
does not recognize it as the original — use layer 1 to get the original receipt back. Solana
|
|
269
|
+
sources use a replay-window nonce and rely on the gateway's content-keyed dedup (the request id,
|
|
270
|
+
and thus the payment id, are still derived deterministically from the challenge id).
|
|
271
|
+
|
|
272
|
+
Layer 2 holds only if the challenge id is **stable across retries of one payment intent and unique
|
|
273
|
+
across distinct intents**, and the merchant controls that. With `Mppx.create({ secretKey })` the
|
|
274
|
+
challenge id is an HMAC over the challenge contents — including `opaque` and `expires` — so:
|
|
275
|
+
|
|
276
|
+
- set **`opaque` to a per-intent identifier** (e.g. your order or invoice id), reused if that same
|
|
277
|
+
intent is retried; and
|
|
278
|
+
- keep **`expires` stable or absent** per intent — a floating `now() + TTL` changes the id on every
|
|
279
|
+
`402` and defeats the anchor.
|
|
280
|
+
|
|
281
|
+
Anchoring on a per-intent `opaque` (rather than the payment *terms*) is deliberate: two legitimate
|
|
282
|
+
identical purchases must get **different** nonces, which distinct order ids provide. Without a
|
|
283
|
+
`secretKey`, or with a per-request-random challenge id, only layer 1 protects you — so build once.
|
|
284
|
+
|
|
285
|
+
## Scope
|
|
286
|
+
|
|
287
|
+
This version supports the **`charge`** intent with **EVM, Tron, and Solana** source chains and a
|
|
288
|
+
merchant-configured destination chain, with **synchronous** settlement (the request is held open
|
|
289
|
+
until settlement completes, so your client and server timeouts must exceed the corridor's deadline
|
|
290
|
+
budgets). One source option is offered per challenge; a corridor may list several and offer a
|
|
291
|
+
different one per `402`.
|
|
292
|
+
|
|
293
|
+
> **Solana deadline constraint:** a Solana deposit authorization expires within the escrow's fixed
|
|
294
|
+
> on-chain replay window (~2 minutes), independent of the corridor budget. So a corridor that
|
|
295
|
+
> includes a Solana source must keep `fulfillmentDeadlineSeconds` within that window —
|
|
296
|
+
> `validateCorridor` (run by `buildChargeRequest`) enforces this and rejects a longer budget. If you
|
|
297
|
+
> need a longer budget for EVM/Tron sources, put the Solana source in its own corridor.
|
|
298
|
+
|
|
299
|
+
## API
|
|
300
|
+
|
|
301
|
+
Import everything from the root, or from the role-specific entry points (which expose only what
|
|
302
|
+
each side needs):
|
|
303
|
+
|
|
304
|
+
- `@atumlabs/mppx-atum-escrow` — everything below
|
|
305
|
+
- `@atumlabs/mppx-atum-escrow/client` — `registerClient`, `ensureSourceApproval` + payer types
|
|
306
|
+
- `@atumlabs/mppx-atum-escrow/server` — `registerServer`, `buildChargeRequest`,
|
|
307
|
+
`validateCorridor`, `corridorFromDefaults` + merchant types
|
|
308
|
+
|
|
309
|
+
| Export | Description |
|
|
310
|
+
| --- | --- |
|
|
311
|
+
| `registerClient(config)` | Payer-side method. `config`: `{ signer, account, now?, solanaClockReader? }`. |
|
|
312
|
+
| `registerServer(config)` | Merchant-side method. `config`: `{ submitter, now? }`. |
|
|
313
|
+
| `buildChargeRequest(corridor, select, fulfillmentAmount)` | Builds the `charge` challenge request for one source option, at a per-charge price. Throws if `select` matches no configured source. |
|
|
314
|
+
| `validateCorridor(corridor)` | Validates a corridor's shape and per-source addresses (run automatically by `buildChargeRequest`). |
|
|
315
|
+
| `corridorFromDefaults(defaults, params)` | Builds a corridor by fetching escrow/role/proxy addresses from the gateway `/defaults`. |
|
|
316
|
+
| `ensureSourceApproval(params)` | Ensures the payer has approved Permit2 to move the source token (EVM/Tron; no-op on Solana). |
|
|
317
|
+
| `atumEscrowChargeMethod` | The base `mppx` method (advanced/custom wiring). |
|
|
318
|
+
| `ChargeRequestSchema`, `CredentialPayloadSchema` | The `zod` wire schemas. |
|
|
319
|
+
| `METHOD_NAME` (`"atum-escrow"`), `INTENT` (`"charge"`) | The method/intent identifiers. |
|
|
320
|
+
|
|
321
|
+
Key types: `AtumEscrowCorridor`, `AtumEscrowSource`, `SenderSigner`, `SenderSignerOptions`,
|
|
322
|
+
`PaymentSubmitter`, `AtumEscrowClientConfig`, `AtumEscrowServerConfig`, `ChainDefaultsSource`,
|
|
323
|
+
`EnsureApprovalResult`, `AtumEscrowChallenge`, `AtumEscrowCredential`, `AtumEscrowReceipt`,
|
|
324
|
+
`ChargeRequest`, `PaymentRequest`, `FulfillmentConfirmation`.
|
|
325
|
+
|
|
326
|
+
> **EVM implementation note:** on EVM the deposit authorization is a Permit2
|
|
327
|
+
> `PermitWitnessTransferFrom` signature; Tron uses the equivalent TIP-712 typed data, and Solana
|
|
328
|
+
> uses an ed25519-signed deposit. The public API is the same across all three.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createRequire as __atumCreateRequire } from 'module'; const require = __atumCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
__toESM,
|
|
4
|
+
assetIdentifier,
|
|
5
|
+
atumEscrowChargeMethod,
|
|
6
|
+
chainDefaultsFromExtra,
|
|
7
|
+
namespaceOf,
|
|
8
|
+
require_dist,
|
|
9
|
+
require_dist2
|
|
10
|
+
} from "./chunk-WWSNMK7N.js";
|
|
11
|
+
|
|
12
|
+
// src/client.ts
|
|
13
|
+
var import_evm_escrow_encoding = __toESM(require_dist(), 1);
|
|
14
|
+
var import_payment_gateway_client = __toESM(require_dist2(), 1);
|
|
15
|
+
import { keccak256, toUtf8Bytes, Contract, MaxUint256 } from "ethers";
|
|
16
|
+
import { Credential, Method } from "mppx";
|
|
17
|
+
function registerClient(config) {
|
|
18
|
+
const now = config.now ?? (() => Date.now());
|
|
19
|
+
const gateway = new import_payment_gateway_client.PaymentGatewayClient();
|
|
20
|
+
return Method.toClient(atumEscrowChargeMethod, {
|
|
21
|
+
async createCredential({ challenge }) {
|
|
22
|
+
const { source, extra } = challenge.request;
|
|
23
|
+
const namespace = namespaceOf(source.network);
|
|
24
|
+
const account = config.account;
|
|
25
|
+
const nowMs = now();
|
|
26
|
+
if (!(extra.quoteDeadlineSeconds > 0 && extra.quoteDeadlineSeconds < extra.fulfillmentDeadlineSeconds)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`atum-escrow: deadline ordering violated (now < quote_deadline < fulfillment_deadline); quoteDeadlineSeconds=${extra.quoteDeadlineSeconds}, fulfillmentDeadlineSeconds=${extra.fulfillmentDeadlineSeconds}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const anchorAccount = namespace === "eip155" ? account.toLowerCase() : account;
|
|
32
|
+
const idempotencyAnchor = keccak256(
|
|
33
|
+
toUtf8Bytes(`atum-escrow:${challenge.id}:${anchorAccount}`)
|
|
34
|
+
);
|
|
35
|
+
const requestId = `req_mpp_${idempotencyAnchor.slice(2, 26)}`;
|
|
36
|
+
const sourceDefaults = chainDefaultsFromExtra(extra, source.network);
|
|
37
|
+
const sourceAssetId = assetIdentifier(source.network, source.asset);
|
|
38
|
+
const isSolana = namespace === "solana";
|
|
39
|
+
const paymentRequestGw = await gateway.preparePaymentRequest({
|
|
40
|
+
depositor: account,
|
|
41
|
+
fulfillmentAmount: extra.fulfillmentAmount,
|
|
42
|
+
sourceAsset: sourceAssetId,
|
|
43
|
+
destinationAccount: extra.destination.account,
|
|
44
|
+
destinationAsset: assetIdentifier(extra.destination.network, extra.destination.asset),
|
|
45
|
+
// Sign the full advertised cap. The payer MAY sign a lower value, but that only
|
|
46
|
+
// lowers its own escrow lock and risks the auction not clearing; the full cap is
|
|
47
|
+
// the safe default and never affects the receive side.
|
|
48
|
+
maxSourceAmount: source.amount,
|
|
49
|
+
requestId,
|
|
50
|
+
quoteDeadlineSeconds: extra.quoteDeadlineSeconds,
|
|
51
|
+
fulfillmentDeadlineSeconds: extra.fulfillmentDeadlineSeconds,
|
|
52
|
+
now: () => nowMs,
|
|
53
|
+
resolvedDefaults: {
|
|
54
|
+
source: sourceDefaults,
|
|
55
|
+
destination: { fulfillmentProxy: extra.fulfillmentProxy }
|
|
56
|
+
},
|
|
57
|
+
// EVM/Tron: thread the deterministic nonce, and keep the deposit deadline at least
|
|
58
|
+
// as late as the fulfillment deadline. The adapter floors the relative deadline, so
|
|
59
|
+
// +1s guards against truncating below fulfillment_deadline.
|
|
60
|
+
...isSolana ? {
|
|
61
|
+
// Solana stamps issued_at from the (injectable) clock and derives its own
|
|
62
|
+
// replay-window deadline; the offline build reads the client clock.
|
|
63
|
+
solanaRpcUrl: "atum-escrow:offline",
|
|
64
|
+
solanaClockReader: config.solanaClockReader ?? (async () => BigInt(Math.floor(nowMs / 1e3)))
|
|
65
|
+
} : {
|
|
66
|
+
nonce: BigInt(idempotencyAnchor),
|
|
67
|
+
// Keep the deposit deadline at least as late as fulfillment. The adapter
|
|
68
|
+
// floors its relative deadline, so round the budget up and add a second to
|
|
69
|
+
// guard against truncating below fulfillment_deadline (and to keep an
|
|
70
|
+
// integer, since the adapter derives a bigint from it).
|
|
71
|
+
deadlineSeconds: Math.ceil(extra.fulfillmentDeadlineSeconds) + 1
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const signer = buildSenderSigner(source.network, config.signer, account);
|
|
75
|
+
await (0, import_payment_gateway_client.signPaymentRequest)(paymentRequestGw, signer);
|
|
76
|
+
const paymentRequest = paymentRequestGw;
|
|
77
|
+
return Credential.serialize({
|
|
78
|
+
challenge,
|
|
79
|
+
payload: { paymentRequest },
|
|
80
|
+
source: `did:pkh:${source.network}:${anchorAccount}`
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function isSenderSigner(signer) {
|
|
86
|
+
return typeof signer.sign === "function";
|
|
87
|
+
}
|
|
88
|
+
function buildSenderSigner(network, signer, account) {
|
|
89
|
+
if (isSenderSigner(signer)) {
|
|
90
|
+
return signer;
|
|
91
|
+
}
|
|
92
|
+
if (signer.provider === "turnkey") {
|
|
93
|
+
throw new Error(
|
|
94
|
+
"atum-escrow: construct a Turnkey SenderSigner with the payment-gateway client and pass it as `signer`"
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return (0, import_payment_gateway_client.createSenderSigner)(network, {
|
|
98
|
+
provider: "raw",
|
|
99
|
+
privateKey: signer.privateKey,
|
|
100
|
+
pinnedAddress: signer.pinnedAddress ?? account
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
var ERC20_ALLOWANCE_ABI = [
|
|
104
|
+
"function allowance(address owner, address spender) view returns (uint256)",
|
|
105
|
+
"function approve(address spender, uint256 amount) returns (bool)"
|
|
106
|
+
];
|
|
107
|
+
async function ensureSourceApproval(params) {
|
|
108
|
+
if (namespaceOf(params.network) === "solana") {
|
|
109
|
+
return { alreadySufficient: true };
|
|
110
|
+
}
|
|
111
|
+
const spender = params.spender ?? (namespaceOf(params.network) === "eip155" ? import_evm_escrow_encoding.PERMIT2_CONTRACT_ADDRESS : void 0);
|
|
112
|
+
if (!spender) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`atum-escrow: a spender (Permit2 address) is required to approve on ${params.network}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const erc20 = new Contract(params.token, ERC20_ALLOWANCE_ABI, params.signer);
|
|
118
|
+
const current = await erc20.allowance(params.owner, spender);
|
|
119
|
+
const sufficient = params.requiredAllowance !== void 0 ? current >= params.requiredAllowance : current >= MaxUint256;
|
|
120
|
+
if (sufficient) {
|
|
121
|
+
return { alreadySufficient: true };
|
|
122
|
+
}
|
|
123
|
+
const tx = await erc20.approve(spender, MaxUint256);
|
|
124
|
+
await tx.wait?.();
|
|
125
|
+
return { alreadySufficient: false, txHash: tx.hash };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export {
|
|
129
|
+
registerClient,
|
|
130
|
+
ensureSourceApproval
|
|
131
|
+
};
|