@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.
@@ -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.0.0",
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
- "CHANGELOG.md"
82
+ "README.md",
83
+ "CHANGELOG.md",
84
+ "docs/**",
85
+ "LICENSE",
86
+ "THIRD-PARTY-NOTICES.txt"
80
87
  ],
81
- "license": "UNLICENSED"
88
+ "license": "SEE LICENSE IN LICENSE",
89
+ "publishConfig": {
90
+ "access": "restricted"
91
+ }
82
92
  }
File without changes