@atumlabs/payment-gateway-client 2.0.0 → 2.2.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 +49 -0
- package/README.md +214 -58
- package/THIRD-PARTY-NOTICES.txt +45 -0
- package/dist/cli/payment-status.js +157 -46
- package/dist/cli/quote-status.js +117 -45
- package/dist/cli/request-quote.js +118 -46
- package/dist/cli/send-payment.js +332 -94
- package/dist/index.d.ts +180 -29
- package/dist/index.js +268 -71
- package/docs/idempotency.md +142 -0
- package/package.json +14 -4
- /package/{LICENSE.md → LICENSE} +0 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Payment idempotency
|
|
2
|
+
|
|
3
|
+
Submit the same payment twice — a retry, a timeout, a network blip — and you
|
|
4
|
+
should end up with **one** payment, not two, and receive the **same** answer both
|
|
5
|
+
times. The key to both properties is the `request_id` you send with each payment.
|
|
6
|
+
|
|
7
|
+
## Using the SDK
|
|
8
|
+
|
|
9
|
+
With `@atumlabs/payment-gateway-client` this is automatic. Pass a stable
|
|
10
|
+
`request_id` and reuse it on retry. `request_id` is required on the payment path
|
|
11
|
+
(`preparePaymentRequest`); the SDK derives the on-chain deposit nonce
|
|
12
|
+
deterministically from it, so both guarantees below line up on their own. You
|
|
13
|
+
never manage nonces — `request_id` is the only value you control.
|
|
14
|
+
|
|
15
|
+
Reuse the same `request_id` across attempts of the same logical payment. Use a
|
|
16
|
+
fresh `request_id` only for a genuinely new payment.
|
|
17
|
+
|
|
18
|
+
## The two guarantees
|
|
19
|
+
|
|
20
|
+
### 1. Gateway dedup — enforced for every caller
|
|
21
|
+
|
|
22
|
+
The gateway records your `request_id` (scoped to your source account and signing
|
|
23
|
+
identity). If the same `request_id` arrives again for the same payment, the
|
|
24
|
+
gateway returns the original payment — same `payment_id`, same status and receipt
|
|
25
|
+
— and never creates a second payment or broadcasts a second deposit. This holds
|
|
26
|
+
whether you use the SDK or call the API directly, as long as you send a stable
|
|
27
|
+
`request_id` and reuse it on retry.
|
|
28
|
+
|
|
29
|
+
"For the same payment" is the one condition: an id re-used for *different*
|
|
30
|
+
economics is rejected rather than replayed. See
|
|
31
|
+
[Reusing an id for different terms is rejected](#reusing-an-id-for-different-terms-is-rejected).
|
|
32
|
+
|
|
33
|
+
### 2. On-chain single-settlement — enforced by the blockchain
|
|
34
|
+
|
|
35
|
+
Each deposit authorization carries a nonce, and the chain guarantees a given
|
|
36
|
+
nonce can settle at most once. This ties your retries together on-chain **only
|
|
37
|
+
if** each retry carries the **same** nonce. The SDK derives that nonce from your
|
|
38
|
+
`request_id`, so retries reuse it automatically. A direct caller sets the nonce
|
|
39
|
+
themselves in the signed permit.
|
|
40
|
+
|
|
41
|
+
A changing `request_id` defeats both guarantees, because each attempt is then
|
|
42
|
+
treated as a different payment — keep it stable and reuse it on retry.
|
|
43
|
+
|
|
44
|
+
## Reusing an id for different terms is rejected
|
|
45
|
+
|
|
46
|
+
One `request_id` identifies one payment, so it cannot be pointed at different
|
|
47
|
+
economics. Re-submit an id you have already used with a changed amount, token,
|
|
48
|
+
account or destination and the gateway answers **`409`** with
|
|
49
|
+
`IDEMPOTENCY_TERMS_MISMATCH` rather than paying, and rather than quietly serving
|
|
50
|
+
you the original payment.
|
|
51
|
+
|
|
52
|
+
What counts as "different economics" is who pays, who receives, in which assets,
|
|
53
|
+
and for how much — the terms that decide where the money goes. Change any of them
|
|
54
|
+
and you get the `409`; the error's `message` names the category that differed. A
|
|
55
|
+
genuinely different payment needs a new `request_id`, which is what the error is
|
|
56
|
+
telling you.
|
|
57
|
+
|
|
58
|
+
Anything outside that list is taken from the payment that already exists, and the
|
|
59
|
+
value you sent is not applied. So a re-submission is not a way to amend a payment
|
|
60
|
+
in flight; it is a way to ask for the one you already made.
|
|
61
|
+
|
|
62
|
+
### Deadlines and the signature are exempt
|
|
63
|
+
|
|
64
|
+
Two things you may freely change on a re-submission, because neither identifies
|
|
65
|
+
the payment:
|
|
66
|
+
|
|
67
|
+
- **Deadlines.** Re-send the original bytes unchanged and the deadlines will
|
|
68
|
+
usually have expired by then: `quote_deadline` bounds the auction and is short,
|
|
69
|
+
and `POST /v1/payments` can hold a submission for longer than that. An expired
|
|
70
|
+
deadline on a re-submission is **not** an error — the request resolves onto the
|
|
71
|
+
payment that already exists, so nothing is scheduled against it.
|
|
72
|
+
- **The signature.** Re-signing is fine. So is re-sending the original signature.
|
|
73
|
+
|
|
74
|
+
Which means both retry styles work: keep the exact bytes from the first attempt,
|
|
75
|
+
or rebuild and re-sign the same payment. What must not change is the
|
|
76
|
+
`request_id`.
|
|
77
|
+
|
|
78
|
+
## Telling a retry from a first attempt
|
|
79
|
+
|
|
80
|
+
`PaymentResponse.idempotent_replay` is `true` when you have been handed a payment
|
|
81
|
+
that already existed, and `false` when this request created one. Read it if you
|
|
82
|
+
need to distinguish "the payment went through just now" from "the payment was
|
|
83
|
+
already there" — for logging, for reconciliation, or to avoid double-counting a
|
|
84
|
+
conversion.
|
|
85
|
+
|
|
86
|
+
## Reading the outcome
|
|
87
|
+
|
|
88
|
+
`status` is on every response, and the three endings mean different things for a
|
|
89
|
+
retry:
|
|
90
|
+
|
|
91
|
+
| `status` | What it means | What to do |
|
|
92
|
+
| ---------------------- | --------------------------------- | ------------------------------------------------------ |
|
|
93
|
+
| `completed` | Settled. | Nothing. `fulfillment_confirmation` carries the proof. |
|
|
94
|
+
| `pending` | Accepted, still settling. | Re-submit the **same** `request_id`, or read the status endpoint. Do not start a new payment. |
|
|
95
|
+
| `failed` / `cancelled` | Terminal — it can never complete. | A fresh attempt needs a **new** `request_id`. |
|
|
96
|
+
|
|
97
|
+
`pending` is not success. Cross-chain settlement can outrun the gateway's
|
|
98
|
+
synchronous window, so a payment that has been accepted may still have no result
|
|
99
|
+
when you get your response.
|
|
100
|
+
|
|
101
|
+
A terminal failure **keeps** its `request_id`. Re-submitting that id returns the
|
|
102
|
+
same failed payment for good, which is why recovering from one means minting a
|
|
103
|
+
new id — the opposite of what a pending payment needs. Getting these two the
|
|
104
|
+
wrong way round is how a caller either pays twice or waits for something that
|
|
105
|
+
will never happen.
|
|
106
|
+
|
|
107
|
+
### How to learn the outcome
|
|
108
|
+
|
|
109
|
+
Either works, and you can mix them:
|
|
110
|
+
|
|
111
|
+
- **Poll `GET /v1/payments/{payment_id}/status`** — cheapest once you hold a
|
|
112
|
+
`payment_id`. This is what the `payment-status` CLI does, and what
|
|
113
|
+
`send-payment --wait` does after submitting.
|
|
114
|
+
- **Re-submit the same request** — idempotent, and it returns the payment's
|
|
115
|
+
current state. Useful when you would rather not track `payment_id`, or when the
|
|
116
|
+
first attempt failed before you learned one.
|
|
117
|
+
|
|
118
|
+
If you are building a *merchant* that mediates someone else's HTTP request,
|
|
119
|
+
prefer having the payer re-attempt over polling inside that request: polling
|
|
120
|
+
holds the connection open for the whole settlement window. That constraint is
|
|
121
|
+
specific to sitting in the middle of someone else's request, and does not apply
|
|
122
|
+
to a backend calling this SDK directly.
|
|
123
|
+
|
|
124
|
+
## Chain-specific behavior
|
|
125
|
+
|
|
126
|
+
The SDK gives you the same guarantee on every chain; the on-chain half works
|
|
127
|
+
slightly differently underneath:
|
|
128
|
+
|
|
129
|
+
- **EVM / Tron:** the on-chain nonce is spent permanently, so guarantee 2 lasts
|
|
130
|
+
indefinitely.
|
|
131
|
+
- **Solana:** the on-chain guard covers only a short, bounded window (roughly two
|
|
132
|
+
minutes). Beyond that window the durable protection is guarantee 1 (the
|
|
133
|
+
gateway's `request_id` record). The SDK keeps a rebuild inside the window by
|
|
134
|
+
pinning `issued_at` through its Solana clock reader, so you do not have to.
|
|
135
|
+
|
|
136
|
+
Either way, a stable `request_id` keeps you safe on every chain.
|
|
137
|
+
|
|
138
|
+
## A note on addresses
|
|
139
|
+
|
|
140
|
+
EVM `0x` addresses are case-insensitive, so casing never affects your payment.
|
|
141
|
+
Solana and Tron base58 addresses **are** case-sensitive — pass them exactly as
|
|
142
|
+
issued.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atumlabs/payment-gateway-client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "TypeScript client for Atum Payment Gateway API",
|
|
5
5
|
"author": "Atum Labs, Inc.",
|
|
6
6
|
"homepage": "https://github.com/Atum-Labs/protocol/tree/main/payment-gateway-client#readme",
|
|
@@ -41,11 +41,12 @@
|
|
|
41
41
|
"generate": "openapi-ts --input ../schemas/apis/payment-gateway/v1/openapi.yaml --output ./src/generated --client axios --name PaymentGatewayClient --useOptions",
|
|
42
42
|
"prebuild:deps": "node -e \"if(!require('fs').existsSync('../key-management/common/typescript/tsconfig.json')){console.error('protocol key-management packages not found at ../key-management. Is this checked out inside the protocol monorepo?');process.exit(1)}\"",
|
|
43
43
|
"build:deps": "tsc -p ../key-management/common/typescript/tsconfig.json && tsc -p ../key-management/providers/turnkey/typescript/tsconfig.json && tsc -p ../contracts/svm/escrow-encoding/typescript/tsconfig.json && tsc -p ../contracts/evm/escrow-encoding/typescript/tsconfig.json && tsc -p ../contracts/tvm/escrow-encoding/typescript/tsconfig.json && tsc -p ../schemas/declarations/PaymentRequest/v1/bindings/typescript/sender-auth/tsconfig.json",
|
|
44
|
-
"build": "pnpm run build:deps && tsup && chmod +x dist/cli/*.js",
|
|
44
|
+
"build": "pnpm run build:deps && tsup && chmod +x dist/cli/*.js && node scripts/stamp-entry-headers.mjs",
|
|
45
45
|
"pretest": "pnpm run build:deps",
|
|
46
46
|
"test": "jest",
|
|
47
47
|
"clean": "rm -rf dist ../key-management/common/typescript/dist ../key-management/providers/turnkey/typescript/dist ../contracts/svm/escrow-encoding/typescript/dist ../contracts/evm/escrow-encoding/typescript/dist ../contracts/tvm/escrow-encoding/typescript/dist ../schemas/declarations/PaymentRequest/v1/bindings/typescript/sender-auth/dist",
|
|
48
48
|
"check:publishable": "node scripts/check-publishable.mjs",
|
|
49
|
+
"gen:notices": "node scripts/gen-third-party-notices.mjs",
|
|
49
50
|
"prepare": "pnpm run build",
|
|
50
51
|
"prepublishOnly": "pnpm run build && pnpm run check:publishable"
|
|
51
52
|
},
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
"axios": "1.13.2",
|
|
55
56
|
"bs58": "^6.0.0",
|
|
56
57
|
"commander": "^12.0.0",
|
|
58
|
+
"dotenv": "^16.6.1",
|
|
57
59
|
"ethers": "^6.11.0",
|
|
58
60
|
"tweetnacl": "^1.0.3",
|
|
59
61
|
"zod": "^3.25.0"
|
|
@@ -62,6 +64,7 @@
|
|
|
62
64
|
"@atum-labs/evm-escrow-encoding": "workspace:*",
|
|
63
65
|
"@atum-labs/km-core": "workspace:*",
|
|
64
66
|
"@atum-labs/payment-request-sender-auth": "workspace:*",
|
|
67
|
+
"@atum-labs/publish-guard": "workspace:*",
|
|
65
68
|
"@atum-labs/solana-escrow-encoding": "workspace:*",
|
|
66
69
|
"@atum-labs/turnkey-signer": "workspace:*",
|
|
67
70
|
"@atum-labs/tvm-escrow-encoding": "workspace:*",
|
|
@@ -76,7 +79,14 @@
|
|
|
76
79
|
},
|
|
77
80
|
"files": [
|
|
78
81
|
"dist/**/*",
|
|
79
|
-
"
|
|
82
|
+
"README.md",
|
|
83
|
+
"CHANGELOG.md",
|
|
84
|
+
"docs/**",
|
|
85
|
+
"LICENSE",
|
|
86
|
+
"THIRD-PARTY-NOTICES.txt"
|
|
80
87
|
],
|
|
81
|
-
"license": "
|
|
88
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
89
|
+
"publishConfig": {
|
|
90
|
+
"access": "restricted"
|
|
91
|
+
}
|
|
82
92
|
}
|
/package/{LICENSE.md → LICENSE}
RENAMED
|
File without changes
|