@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 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
+ };