@atumlabs/payment-gateway-client 2.1.0 → 3.0.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 +114 -0
- package/README.md +604 -40
- package/THIRD-PARTY-NOTICES.txt +7 -0
- package/dist/cli/approve-permit2.d.ts +1 -0
- package/dist/cli/approve-permit2.js +7100 -0
- package/dist/cli/payment-status.js +2311 -234
- package/dist/cli/quote-status.js +2271 -233
- package/dist/cli/request-quote.js +2299 -240
- package/dist/cli/send-payment.js +3131 -410
- package/dist/index.d.ts +1392 -194
- package/dist/index.js +3264 -414
- package/docs/idempotency.md +155 -19
- package/package.json +24 -9
package/docs/idempotency.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Idempotency
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Send the same request twice — a retry, a timeout, a network blip — and it should
|
|
4
|
+
count once, not twice, and give you the **same** answer both times. On a payment
|
|
5
|
+
that means you are charged once. The key to both properties is the `request_id`
|
|
6
|
+
you send.
|
|
7
|
+
|
|
8
|
+
One contract covers every write endpoint. Today that is `submitPayment` and
|
|
9
|
+
`createQuoteRequest`; they use the same key, reject the same misuse the same way,
|
|
10
|
+
and report a replay the same way, so what you learn on one applies to the other.
|
|
11
|
+
Where an endpoint genuinely differs it is called out inline.
|
|
6
12
|
|
|
7
13
|
## Using the SDK
|
|
8
14
|
|
|
@@ -12,21 +18,46 @@ With `@atumlabs/payment-gateway-client` this is automatic. Pass a stable
|
|
|
12
18
|
deterministically from it, so both guarantees below line up on their own. You
|
|
13
19
|
never manage nonces — `request_id` is the only value you control.
|
|
14
20
|
|
|
15
|
-
Reuse the same `request_id` across attempts of the same logical
|
|
16
|
-
fresh `request_id` only
|
|
21
|
+
Reuse the same `request_id` across attempts of the same logical request. Use a
|
|
22
|
+
fresh `request_id` only when you genuinely mean a new one.
|
|
23
|
+
|
|
24
|
+
Both builders require it: `preparePaymentRequest` and `prepareQuoteRequest` each
|
|
25
|
+
take `requestId` explicitly, and neither invents one. That is deliberate — an id
|
|
26
|
+
the library chose for you is an id you cannot reuse on a retry, so the retry would
|
|
27
|
+
create a second payment or open a second batch rather than recovering the first.
|
|
28
|
+
|
|
29
|
+
The CLIs are the exception, because a command invocation is a one-shot request:
|
|
30
|
+
`send-payment` and `request-quote` generate an id when `--request-id` is omitted
|
|
31
|
+
and print it back, so you can re-run with `--request-id <id>` to retry the same
|
|
32
|
+
one. A bare re-run, with no `--request-id`, is a **separate** payment or batch.
|
|
17
33
|
|
|
18
34
|
## The two guarantees
|
|
19
35
|
|
|
20
|
-
### 1. Gateway dedup —
|
|
36
|
+
### 1. Gateway dedup — every write endpoint, every caller
|
|
21
37
|
|
|
22
38
|
The gateway records your `request_id` (scoped to your source account and signing
|
|
23
|
-
identity). If the same `request_id` arrives again, the gateway returns
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
identity). If the same `request_id` arrives again, the gateway returns what it
|
|
40
|
+
created the first time and creates nothing new:
|
|
41
|
+
|
|
42
|
+
| Endpoint | A repeat returns | And does not |
|
|
43
|
+
| -- | -- | -- |
|
|
44
|
+
| `submitPayment` | the original payment — same `payment_id`, status and receipt | create a second payment or broadcast a second deposit |
|
|
45
|
+
| `createQuoteRequest` | the original batch — same `quote_request_id` and its quotes | open a second batch or re-broadcast to settlement agents |
|
|
46
|
+
|
|
47
|
+
This holds whether you use the SDK or call the API directly, as long as you send a
|
|
48
|
+
stable `request_id` and reuse it on retry.
|
|
49
|
+
|
|
50
|
+
"The same request" is the one condition: an id re-used for *different* economics
|
|
51
|
+
is rejected rather than replayed. See
|
|
52
|
+
[Reusing an id for different terms is rejected](#reusing-an-id-for-different-terms-is-rejected).
|
|
53
|
+
|
|
54
|
+
### 2. On-chain single-settlement — payments only, enforced by the blockchain
|
|
28
55
|
|
|
29
|
-
|
|
56
|
+
This second guarantee applies where funds move, which today means `submitPayment`.
|
|
57
|
+
A quote-request authorizes no spend — it carries no `sender_auth` and no deposit
|
|
58
|
+
nonce — so guarantee 1 is the whole mechanism there. Nothing is charged either
|
|
59
|
+
way; what a retry protects on that endpoint is your ability to find the batch
|
|
60
|
+
again.
|
|
30
61
|
|
|
31
62
|
Each deposit authorization carries a nonce, and the chain guarantees a given
|
|
32
63
|
nonce can settle at most once. This ties your retries together on-chain **only
|
|
@@ -34,12 +65,117 @@ if** each retry carries the **same** nonce. The SDK derives that nonce from your
|
|
|
34
65
|
`request_id`, so retries reuse it automatically. A direct caller sets the nonce
|
|
35
66
|
themselves in the signed permit.
|
|
36
67
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
68
|
+
A changing `request_id` defeats both guarantees, because each attempt is then
|
|
69
|
+
treated as a different request — keep it stable and reuse it on retry.
|
|
70
|
+
|
|
71
|
+
## Reusing an id for different terms is rejected
|
|
72
|
+
|
|
73
|
+
One `request_id` identifies one payment, or one quote-request batch, so it cannot
|
|
74
|
+
be pointed at different economics. Re-submit an id you have already used with a
|
|
75
|
+
changed amount, token, account or destination and the gateway answers **`409`**
|
|
76
|
+
with `IDEMPOTENCY_TERMS_MISMATCH` rather than acting on it, and rather than
|
|
77
|
+
quietly serving you the original.
|
|
78
|
+
|
|
79
|
+
What counts as "different economics" is who pays, who receives, in which assets,
|
|
80
|
+
and for how much — the terms that decide where the money goes. Change any of them
|
|
81
|
+
and you get the `409`; the error's `message` names the category that differed. A
|
|
82
|
+
genuinely different request needs a new `request_id`, which is what the error is
|
|
83
|
+
telling you.
|
|
84
|
+
|
|
85
|
+
On a quote-request the `409` is doing the same job one step earlier: without it
|
|
86
|
+
you would silently receive the batch you opened *earlier*, whose quotes price the
|
|
87
|
+
corridor you asked for then — and awarding one of those settles a payment on that
|
|
88
|
+
corridor, not the one you meant.
|
|
89
|
+
|
|
90
|
+
Anything outside that list is taken from what already exists, and the value you
|
|
91
|
+
sent is not applied. So a re-submission is not a way to amend something in flight;
|
|
92
|
+
it is a way to ask for the one you already made.
|
|
93
|
+
|
|
94
|
+
### Deadlines and the signature are exempt
|
|
95
|
+
|
|
96
|
+
Two things you may freely change on a re-submission, because neither identifies
|
|
97
|
+
the request:
|
|
98
|
+
|
|
99
|
+
- **Deadlines.** Re-send the original bytes unchanged and the deadlines will
|
|
100
|
+
usually have expired by then: `quote_deadline` is short — it bounds the auction
|
|
101
|
+
on a payment and the collection window on a quote-request — and either endpoint
|
|
102
|
+
can take longer than that to answer. An expired deadline on a re-submission is
|
|
103
|
+
**not** an error; the request resolves onto what already exists, so nothing is
|
|
104
|
+
scheduled against it.
|
|
105
|
+
- **The signature.** Re-signing is fine. So is re-sending the original signature.
|
|
106
|
+
|
|
107
|
+
Which means both retry styles work: keep the exact bytes from the first attempt,
|
|
108
|
+
or rebuild and re-sign. What must not change is the `request_id`.
|
|
109
|
+
|
|
110
|
+
One consequence is specific to quote-requests: **the collection window belongs to
|
|
111
|
+
the batch, not to your request.** Asking again with a later `quote_deadline` does
|
|
112
|
+
not extend a window already open. The `quote_deadline` you get back is the
|
|
113
|
+
batch's own, and `status` tells you whether collection is still running. This is
|
|
114
|
+
the one place where a re-submission returns something that may not match what you
|
|
115
|
+
just sent — by design, because the response has to describe the batch you
|
|
116
|
+
actually received.
|
|
117
|
+
|
|
118
|
+
## Telling a retry from a first attempt
|
|
119
|
+
|
|
120
|
+
`idempotent_replay` is `true` when you have been handed something that already
|
|
121
|
+
existed, and `false` when this request created it. Every write response carries
|
|
122
|
+
it — `PaymentResponse`, `QuoteRequestResponse` and `QuoteRequestAccepted`. Read it
|
|
123
|
+
if you need to distinguish "this went through just now" from "this was already
|
|
124
|
+
there" — for logging, for reconciliation, or to avoid double-counting a
|
|
125
|
+
conversion.
|
|
126
|
+
|
|
127
|
+
It is always present, so `false` means `false`. A response missing the field
|
|
128
|
+
altogether is from a gateway predating it.
|
|
129
|
+
|
|
130
|
+
## Reading the outcome (payments)
|
|
131
|
+
|
|
132
|
+
`status` is on every payment response, and the three endings mean different things for a
|
|
133
|
+
retry:
|
|
134
|
+
|
|
135
|
+
| `status` | What it means | What to do |
|
|
136
|
+
| ---------------------- | --------------------------------- | ------------------------------------------------------ |
|
|
137
|
+
| `completed` | Settled. | Nothing. `fulfillment_confirmation` carries the proof. |
|
|
138
|
+
| `pending` | Accepted, still settling. | Re-submit the **same** `request_id`, or read the status endpoint. Do not start a new payment. |
|
|
139
|
+
| `failed` / `cancelled` | Terminal — it can never complete. | A fresh attempt needs a **new** `request_id`. |
|
|
140
|
+
|
|
141
|
+
`pending` is not success. Cross-chain settlement can outrun the gateway's
|
|
142
|
+
synchronous window, so a payment that has been accepted may still have no result
|
|
143
|
+
when you get your response.
|
|
144
|
+
|
|
145
|
+
A terminal failure **keeps** its `request_id`. Re-submitting that id returns the
|
|
146
|
+
same failed payment for good, which is why recovering from one means minting a
|
|
147
|
+
new id — the opposite of what a pending payment needs. Getting these two the
|
|
148
|
+
wrong way round is how a caller either pays twice or waits for something that
|
|
149
|
+
will never happen.
|
|
150
|
+
|
|
151
|
+
### How to learn the outcome
|
|
152
|
+
|
|
153
|
+
Either works, and you can mix them:
|
|
154
|
+
|
|
155
|
+
- **Poll `GET /v1/payments/{payment_id}/status`** — cheapest once you hold a
|
|
156
|
+
`payment_id`. This is what the `payment-status` CLI does, and what
|
|
157
|
+
`send-payment --wait` does after submitting.
|
|
158
|
+
- **Re-submit the same request** — idempotent, and it returns the payment's
|
|
159
|
+
current state. Useful when you would rather not track `payment_id`, or when the
|
|
160
|
+
first attempt failed before you learned one.
|
|
161
|
+
|
|
162
|
+
If you are building a *merchant* that mediates someone else's HTTP request,
|
|
163
|
+
prefer having the payer re-attempt over polling inside that request: polling
|
|
164
|
+
holds the connection open for the whole settlement window. That constraint is
|
|
165
|
+
specific to sitting in the middle of someone else's request, and does not apply
|
|
166
|
+
to a backend calling this SDK directly.
|
|
167
|
+
|
|
168
|
+
## Recovering a lost quote-request response
|
|
169
|
+
|
|
170
|
+
The rules above are all you need for payments. On `createQuoteRequest` they add up
|
|
171
|
+
to one workflow worth spelling out, because it is the reason the endpoint behaves
|
|
172
|
+
this way.
|
|
173
|
+
|
|
174
|
+
If you lose the create response, re-send the request. You get your
|
|
175
|
+
`quote_request_id` back and can read the batch's quotes — rather than being told
|
|
176
|
+
your `quote_deadline` has expired while holding no id to look the batch up with.
|
|
177
|
+
The collection window is short, so by the time you retry it usually *has* expired;
|
|
178
|
+
that is exactly the case the deadline exemption exists to cover.
|
|
43
179
|
|
|
44
180
|
## Chain-specific behavior
|
|
45
181
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atumlabs/payment-gateway-client",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.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",
|
|
@@ -35,16 +35,18 @@
|
|
|
35
35
|
"send-payment": "./dist/cli/send-payment.js",
|
|
36
36
|
"request-quote": "./dist/cli/request-quote.js",
|
|
37
37
|
"quote-status": "./dist/cli/quote-status.js",
|
|
38
|
-
"payment-status": "./dist/cli/payment-status.js"
|
|
38
|
+
"payment-status": "./dist/cli/payment-status.js",
|
|
39
|
+
"approve-permit2": "./dist/cli/approve-permit2.js"
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
|
-
"generate": "openapi-ts --input ../schemas/apis/payment-gateway/v1/openapi.yaml --output
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"build": "pnpm
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
42
|
+
"generate": "openapi-ts --input ../schemas/apis/payment-gateway/v1/openapi.yaml --output \"${PGC_GEN_OUT:-./src/generated}\" --client axios --name PaymentGatewayClient --useOptions",
|
|
43
|
+
"lint": "eslint . --max-warnings 0",
|
|
44
|
+
"prebuild:deps": "node -e \"if(!require('fs').existsSync('../pnpm-workspace.yaml')){console.error('protocol workspace not found at ../pnpm-workspace.yaml. Is this checked out inside the protocol monorepo?');process.exit(1)}\"",
|
|
45
|
+
"build:deps": "pnpm --filter \"@atum-labs/payment-gateway-client^...\" run build",
|
|
46
|
+
"build": "tsup && chmod +x dist/cli/*.js && node scripts/stamp-entry-headers.mjs",
|
|
47
|
+
"typecheck:tests": "tsc -p tsconfig.test.json",
|
|
48
|
+
"test": "pnpm run typecheck:tests && jest",
|
|
49
|
+
"clean": "rm -rf dist && pnpm --filter \"@atum-labs/payment-gateway-client^...\" run clean",
|
|
48
50
|
"check:publishable": "node scripts/check-publishable.mjs",
|
|
49
51
|
"gen:notices": "node scripts/gen-third-party-notices.mjs",
|
|
50
52
|
"prepare": "pnpm run build",
|
|
@@ -61,9 +63,11 @@
|
|
|
61
63
|
"zod": "^3.25.0"
|
|
62
64
|
},
|
|
63
65
|
"devDependencies": {
|
|
66
|
+
"@atum-labs/chain-address": "workspace:*",
|
|
64
67
|
"@atum-labs/evm-escrow-encoding": "workspace:*",
|
|
65
68
|
"@atum-labs/km-core": "workspace:*",
|
|
66
69
|
"@atum-labs/payment-request-sender-auth": "workspace:*",
|
|
70
|
+
"@atum-labs/payment-request-token-approval": "workspace:*",
|
|
67
71
|
"@atum-labs/publish-guard": "workspace:*",
|
|
68
72
|
"@atum-labs/solana-escrow-encoding": "workspace:*",
|
|
69
73
|
"@atum-labs/turnkey-signer": "workspace:*",
|
|
@@ -71,7 +75,10 @@
|
|
|
71
75
|
"@hey-api/openapi-ts": "^0.45.1",
|
|
72
76
|
"@types/jest": "^29.5.5",
|
|
73
77
|
"@types/node": "^20.0.0",
|
|
78
|
+
"ajv": "^8.17.1",
|
|
79
|
+
"ajv-formats": "^3.0.1",
|
|
74
80
|
"jest": "^29.7.0",
|
|
81
|
+
"tronweb": "^6.0.0",
|
|
75
82
|
"ts-jest": "^29.1.1",
|
|
76
83
|
"tsup": "^8.5.1",
|
|
77
84
|
"typescript": "^5.0.0",
|
|
@@ -88,5 +95,13 @@
|
|
|
88
95
|
"license": "SEE LICENSE IN LICENSE",
|
|
89
96
|
"publishConfig": {
|
|
90
97
|
"access": "restricted"
|
|
98
|
+
},
|
|
99
|
+
"peerDependencies": {
|
|
100
|
+
"tronweb": "^6.0.0"
|
|
101
|
+
},
|
|
102
|
+
"peerDependenciesMeta": {
|
|
103
|
+
"tronweb": {
|
|
104
|
+
"optional": true
|
|
105
|
+
}
|
|
91
106
|
}
|
|
92
107
|
}
|