@atumlabs/payment-gateway-client 2.2.0 → 3.0.1
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 +108 -0
- package/LICENSE +202 -83
- package/NOTICE +2 -0
- package/README.md +547 -46
- package/THIRD-PARTY-NOTICES.txt +7 -1
- package/dist/cli/approve-permit2.d.ts +1 -0
- package/dist/cli/approve-permit2.js +7325 -0
- package/dist/cli/payment-status.js +2307 -261
- package/dist/cli/quote-status.js +2305 -259
- package/dist/cli/request-quote.js +2333 -266
- package/dist/cli/send-payment.js +3079 -360
- package/dist/index.d.ts +926 -1701
- package/dist/index.js +3470 -492
- package/dist/sender-auth-verify-C-Q-zvAO.d.ts +2013 -0
- package/dist/signing.d.ts +223 -0
- package/dist/signing.js +7219 -0
- package/docs/idempotency.md +97 -40
- package/docs/signing.md +218 -0
- package/package.json +48 -14
- package/signing.d.ts +2 -0
- package/signing.js +11 -0
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,25 +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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
31
52
|
[Reusing an id for different terms is rejected](#reusing-an-id-for-different-terms-is-rejected).
|
|
32
53
|
|
|
33
|
-
### 2. On-chain single-settlement — enforced by the blockchain
|
|
54
|
+
### 2. On-chain single-settlement — payments only, enforced by the blockchain
|
|
55
|
+
|
|
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.
|
|
34
61
|
|
|
35
62
|
Each deposit authorization carries a nonce, and the chain guarantees a given
|
|
36
63
|
nonce can settle at most once. This ties your retries together on-chain **only
|
|
@@ -39,60 +66,78 @@ if** each retry carries the **same** nonce. The SDK derives that nonce from your
|
|
|
39
66
|
themselves in the signed permit.
|
|
40
67
|
|
|
41
68
|
A changing `request_id` defeats both guarantees, because each attempt is then
|
|
42
|
-
treated as a different
|
|
69
|
+
treated as a different request — keep it stable and reuse it on retry.
|
|
43
70
|
|
|
44
71
|
## Reusing an id for different terms is rejected
|
|
45
72
|
|
|
46
|
-
One `request_id` identifies one payment,
|
|
47
|
-
economics. Re-submit an id you have already used with a
|
|
48
|
-
account or destination and the gateway answers **`409`**
|
|
49
|
-
`IDEMPOTENCY_TERMS_MISMATCH` rather than
|
|
50
|
-
you the original
|
|
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.
|
|
51
78
|
|
|
52
79
|
What counts as "different economics" is who pays, who receives, in which assets,
|
|
53
80
|
and for how much — the terms that decide where the money goes. Change any of them
|
|
54
81
|
and you get the `409`; the error's `message` names the category that differed. A
|
|
55
|
-
genuinely different
|
|
82
|
+
genuinely different request needs a new `request_id`, which is what the error is
|
|
56
83
|
telling you.
|
|
57
84
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
|
61
93
|
|
|
62
94
|
### Deadlines and the signature are exempt
|
|
63
95
|
|
|
64
96
|
Two things you may freely change on a re-submission, because neither identifies
|
|
65
|
-
the
|
|
97
|
+
the request:
|
|
66
98
|
|
|
67
99
|
- **Deadlines.** Re-send the original bytes unchanged and the deadlines will
|
|
68
|
-
usually have expired by then: `quote_deadline` bounds the auction
|
|
69
|
-
and
|
|
70
|
-
|
|
71
|
-
|
|
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.
|
|
72
105
|
- **The signature.** Re-signing is fine. So is re-sending the original signature.
|
|
73
106
|
|
|
74
107
|
Which means both retry styles work: keep the exact bytes from the first attempt,
|
|
75
|
-
or rebuild and re-sign
|
|
76
|
-
|
|
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.
|
|
77
117
|
|
|
78
118
|
## Telling a retry from a first attempt
|
|
79
119
|
|
|
80
|
-
`
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
84
125
|
conversion.
|
|
85
126
|
|
|
86
|
-
|
|
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)
|
|
87
131
|
|
|
88
|
-
`status` is on every response, and the three endings mean different things for a
|
|
132
|
+
`status` is on every payment response, and the three endings mean different things for a
|
|
89
133
|
retry:
|
|
90
134
|
|
|
91
135
|
| `status` | What it means | What to do |
|
|
92
136
|
| ---------------------- | --------------------------------- | ------------------------------------------------------ |
|
|
93
137
|
| `completed` | Settled. | Nothing. `fulfillment_confirmation` carries the proof. |
|
|
94
138
|
| `pending` | Accepted, still settling. | Re-submit the **same** `request_id`, or read the status endpoint. Do not start a new payment. |
|
|
95
|
-
| `
|
|
139
|
+
| `finalizing` | Paid on chain, collecting confirmations. Not final. | As `pending`. Do not treat it as delivered. |
|
|
140
|
+
| `failed` | Terminal — it can never complete. | A fresh attempt needs a **new** `request_id`. |
|
|
96
141
|
|
|
97
142
|
`pending` is not success. Cross-chain settlement can outrun the gateway's
|
|
98
143
|
synchronous window, so a payment that has been accepted may still have no result
|
|
@@ -121,6 +166,18 @@ holds the connection open for the whole settlement window. That constraint is
|
|
|
121
166
|
specific to sitting in the middle of someone else's request, and does not apply
|
|
122
167
|
to a backend calling this SDK directly.
|
|
123
168
|
|
|
169
|
+
## Recovering a lost quote-request response
|
|
170
|
+
|
|
171
|
+
The rules above are all you need for payments. On `createQuoteRequest` they add up
|
|
172
|
+
to one workflow worth spelling out, because it is the reason the endpoint behaves
|
|
173
|
+
this way.
|
|
174
|
+
|
|
175
|
+
If you lose the create response, re-send the request. You get your
|
|
176
|
+
`quote_request_id` back and can read the batch's quotes — rather than being told
|
|
177
|
+
your `quote_deadline` has expired while holding no id to look the batch up with.
|
|
178
|
+
The collection window is short, so by the time you retry it usually *has* expired;
|
|
179
|
+
that is exactly the case the deadline exemption exists to cover.
|
|
180
|
+
|
|
124
181
|
## Chain-specific behavior
|
|
125
182
|
|
|
126
183
|
The SDK gives you the same guarantee on every chain; the on-chain half works
|
package/docs/signing.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Signing a PaymentRequest
|
|
2
|
+
|
|
3
|
+
A PaymentRequest carries **three unrelated signatures**. They are produced by different calls,
|
|
4
|
+
cover different things, and none substitutes for another. Choosing the wrong one is the mistake
|
|
5
|
+
this page exists to prevent.
|
|
6
|
+
|
|
7
|
+
| What you are signing | Call | What it authorizes |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `sender_auth.signed_messages[]` | `signPaymentRequest` | the on-chain **transfer** |
|
|
10
|
+
| the document's `proof` | `signDeclaration` | that the **document** came from its signer |
|
|
11
|
+
| `presentation` | `buildEnvelope` | the **originator's identity** |
|
|
12
|
+
|
|
13
|
+
A request with a valid `sender_auth` and no identity envelope authorizes a transfer by an
|
|
14
|
+
unidentified originator. The reverse identifies an originator who has authorized nothing. Most
|
|
15
|
+
integrations need the first two; the third applies once the originator holds credentials about
|
|
16
|
+
itself.
|
|
17
|
+
|
|
18
|
+
> The two names are confusingly close, and that is not an accident of this client: the upstream
|
|
19
|
+
> library calls its document signer `signPaymentRequest` too. This package exports it as
|
|
20
|
+
> `signDeclaration` so the two cannot be confused for one another, matching `SignDeclaration`
|
|
21
|
+
> in the Go client.
|
|
22
|
+
|
|
23
|
+
## Keys never reach this package
|
|
24
|
+
|
|
25
|
+
Every signing surface takes a **signer**, not a private key:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
export interface DeclarationSigner {
|
|
29
|
+
getAddress(): Promise<string> | string;
|
|
30
|
+
signDigest(digest: Uint8Array): Promise<Uint8Array> | Uint8Array;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That is deliberate. A hex key forces raw key material into a JavaScript string, which cannot be
|
|
35
|
+
zeroized, is copied on assignment, and lands in heap snapshots and any log line that stringifies
|
|
36
|
+
the surrounding object. It also excludes every custodian: a holder whose key sits in a KMS, an
|
|
37
|
+
HSM, or a provider like Turnkey has no key to hand over.
|
|
38
|
+
|
|
39
|
+
For a local key, use ethers — a library built to hold keys:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { Wallet } from 'ethers';
|
|
43
|
+
import { signerFromEthersWallet } from '@atum-labs/payment-gateway-client/signing';
|
|
44
|
+
|
|
45
|
+
const signer = signerFromEthersWallet(new Wallet(process.env.PRIVATE_KEY!));
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For a custodian, implement the interface over its own digest-signing call — `buildEnvelope`
|
|
49
|
+
needs only `signDigest`. Two details the implementation must get right:
|
|
50
|
+
|
|
51
|
+
- **Sign the raw digest.** Not `signMessage` — that applies the EIP-191 prefix, and the
|
|
52
|
+
resulting signature will not verify as a Data Integrity proof.
|
|
53
|
+
- **Return `v ∈ {0, 1}`.** The `+27` alignment is applied once downstream. A signature whose `v`
|
|
54
|
+
is already 27 or 28 is **refused**, because applying it twice recovers a *different* address:
|
|
55
|
+
a well-formed signature that verifies as somebody else.
|
|
56
|
+
|
|
57
|
+
## Signing both surfaces
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { Wallet } from 'ethers';
|
|
61
|
+
import {
|
|
62
|
+
PaymentGatewayClient,
|
|
63
|
+
signPaymentRequest, // transfer authorization — lives at the package root
|
|
64
|
+
createSenderSigner,
|
|
65
|
+
} from '@atum-labs/payment-gateway-client';
|
|
66
|
+
import {
|
|
67
|
+
signDeclaration, // document proof — the signing surface is its own subpath
|
|
68
|
+
signerFromEthersWallet,
|
|
69
|
+
} from '@atum-labs/payment-gateway-client/signing';
|
|
70
|
+
|
|
71
|
+
const wallet = new Wallet(process.env.PRIVATE_KEY!);
|
|
72
|
+
|
|
73
|
+
// 1. Authorize the transfer. Fills every sender_auth.signed_messages[] entry, and attaches
|
|
74
|
+
// payload.delegate_signer on chains whose signatures are not recoverable (Solana).
|
|
75
|
+
const authorized = await signPaymentRequest(request, createSenderSigner(/* ... */));
|
|
76
|
+
|
|
77
|
+
// 2. Attest the document.
|
|
78
|
+
const signed = await signDeclaration(authorized, signerFromEthersWallet(wallet), { chainId: 1 });
|
|
79
|
+
|
|
80
|
+
// 3. Submit.
|
|
81
|
+
await new PaymentGatewayClient({ BASE: gateway }).payments.submitPayment({ requestBody: signed });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Check what you are signing
|
|
85
|
+
|
|
86
|
+
`signPaymentRequest` signs **every** entry in `sender_auth.signed_messages[]` as given. It does
|
|
87
|
+
not check that the terms inside those messages are the ones you agreed to, and neither does the
|
|
88
|
+
Go client — the same is true of building the request in the first place, because the permit's
|
|
89
|
+
spender, escrow and amounts come from the gateway's chain defaults.
|
|
90
|
+
|
|
91
|
+
That is a deliberate division of labour, not an oversight: the SDK holds no notion of what you
|
|
92
|
+
intended. But it means a gateway that returned hostile defaults would have your wallet sign a
|
|
93
|
+
permit naming an address you never chose, and nothing in the signing path would object.
|
|
94
|
+
|
|
95
|
+
If you are signing on behalf of someone else's funds, bind the signed permit to the terms **you**
|
|
96
|
+
agreed before the request leaves your process. `verifyPaymentRequest` recovers every
|
|
97
|
+
`signed_messages[]` entry and checks the terms it actually commits to:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { verifyPaymentRequest } from '@atum-labs/payment-gateway-client/signing';
|
|
101
|
+
|
|
102
|
+
// Sign locally, then bind the result to your own terms before submitting.
|
|
103
|
+
const { recoveredAccount } = verifyPaymentRequest(signed, {
|
|
104
|
+
sourceAsset: agreedTokenAddress,
|
|
105
|
+
network: 'eip155:1',
|
|
106
|
+
maxSourceAmount: agreedMaxAmount,
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`sourceAsset` is the **bare token address**, not a CAIP-19 identifier — it differs in shape from
|
|
111
|
+
the request's own `source.asset_identifier`, which is full CAIP-19. Passing the CAIP form here
|
|
112
|
+
fails to match and rejects a request that was in fact correct.
|
|
113
|
+
|
|
114
|
+
**Pass those three fields.** Called bare, `verifyPaymentRequest` takes the token, network and
|
|
115
|
+
amount *from the request itself*, so it proves only that the signatures are self-consistent with
|
|
116
|
+
the request's own stated fields — which a hostile gateway controls. It is the `sourceAsset` /
|
|
117
|
+
`network` / `maxSourceAmount` arguments that turn it into a check against your terms, and
|
|
118
|
+
omitting them is the difference between a real check and a reassuring one.
|
|
119
|
+
|
|
120
|
+
It is strict by design: every entry must be a bindable permit, and a malformed one throws rather
|
|
121
|
+
than being skipped. A caller with deliberately mixed messages — say an EVM setup approval
|
|
122
|
+
alongside the permit — must compose the per-entry adapter calls itself and own the skip policy.
|
|
123
|
+
|
|
124
|
+
In Go, `senderauth.GetChainAdapter(caip2).RecoverAndVerify(signed, source, expected)` returns the
|
|
125
|
+
same signed terms for the same comparison.
|
|
126
|
+
|
|
127
|
+
A custodial signer is the natural place to enforce this: it is the component that can refuse.
|
|
128
|
+
|
|
129
|
+
## Verifying
|
|
130
|
+
|
|
131
|
+
`verifyDeclaration` **throws** on an invalid proof rather than returning a flag — a boolean can
|
|
132
|
+
be ignored by accident, an exception cannot. It also refuses a document whose signature recovers
|
|
133
|
+
a signer other than the one the document claims, which is what makes a tampered document fail
|
|
134
|
+
rather than verify as its original signer.
|
|
135
|
+
|
|
136
|
+
It returns a **discriminated** signer:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const { signer } = await verifyDeclaration(signed);
|
|
140
|
+
|
|
141
|
+
if (signer.kind === 'recovered-secp256k1') {
|
|
142
|
+
// The address was recovered FROM the signature: self-evident proof of which key signed.
|
|
143
|
+
console.log(signer.address);
|
|
144
|
+
} else {
|
|
145
|
+
// Self-asserted: the ed25519 key came from the document itself. A successful verify proves
|
|
146
|
+
// the signature matches that key — NOT that the key belongs to anyone in particular. Bind it
|
|
147
|
+
// to a trusted source before relying on it.
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## What the proof does and does not cover
|
|
152
|
+
|
|
153
|
+
The proof is computed over the **URDNA2015-canonicalized** document, so it is stable across key
|
|
154
|
+
reordering and whitespace, and it excludes signing artifacts and the deprecated credential
|
|
155
|
+
carriers.
|
|
156
|
+
|
|
157
|
+
One consequence worth knowing: **canonicalization drops terms the declaration `@context` does not
|
|
158
|
+
define.** A field this client's types do not model travels inside the signed document without
|
|
159
|
+
being covered by the proof, and can be altered without invalidating it. Making a new field
|
|
160
|
+
tamper-evident means adding it to the declaration context, not just to the payload.
|
|
161
|
+
|
|
162
|
+
Any `@context` on the input is **replaced** before hashing, so a caller-supplied one cannot
|
|
163
|
+
influence what gets signed.
|
|
164
|
+
|
|
165
|
+
## Contexts are local, always
|
|
166
|
+
|
|
167
|
+
JSON-LD contexts are compiled into the bundle. **Nothing is fetched over the network** — there is
|
|
168
|
+
no HTTP client in the signing libraries, and the JSON-LD processor is always handed the local
|
|
169
|
+
loader in place of its built-in fetcher. A document whose `@context` names an unregistered URL is
|
|
170
|
+
refused rather than resolved, since an attacker-served context could otherwise change what each
|
|
171
|
+
term means, and therefore what the signature covers.
|
|
172
|
+
|
|
173
|
+
One precision, because this page is about what your signatures cover: the embedded copies can be
|
|
174
|
+
overridden by an environment variable intended for development against a modified snapshot. It
|
|
175
|
+
reads from local disk, never the network, and nothing in normal operation sets it — but if it is
|
|
176
|
+
set, the library now says so on first use rather than substituting the contexts silently.
|
|
177
|
+
|
|
178
|
+
This is structural: there is no HTTP client in the signing libraries, and the JSON-LD processor
|
|
179
|
+
is always handed the local loader in place of its built-in fetcher.
|
|
180
|
+
|
|
181
|
+
## The identity envelope
|
|
182
|
+
|
|
183
|
+
**Read this before the example: you need an issuer-signed credential about yourself, and
|
|
184
|
+
originators do not hold one yet.** The schema requires at least one credential and rejects an
|
|
185
|
+
empty bag, so until issuance exists this surface cannot be exercised end to end. It
|
|
186
|
+
is documented because the shape is stable and the signing half is done, not because it is
|
|
187
|
+
callable today.
|
|
188
|
+
|
|
189
|
+
`buildEnvelope` wraps the originator's issuer-signed credentials in a holder-signed
|
|
190
|
+
PaymentRequestEnvelope. It takes the same kind of signer as the other surfaces, so a custodial
|
|
191
|
+
key works here too.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { buildEnvelopeForAccount, computeRequestHash, signerFromEthersWallet } from '@atum-labs/payment-gateway-client/signing';
|
|
195
|
+
|
|
196
|
+
const envelope = await buildEnvelopeForAccount('eip155:1', wallet.address.toLowerCase(), {
|
|
197
|
+
holderVCs: [knownToAtumCredential],
|
|
198
|
+
signer: signerFromEthersWallet(wallet),
|
|
199
|
+
// The challenge is the request's content hash, which the gateway recomputes.
|
|
200
|
+
challenge: await computeRequestHash(request),
|
|
201
|
+
// The domain the gateway you are calling expects. It is compared exactly, so an envelope built
|
|
202
|
+
// for one environment is rejected by another, and it is NOT derived from the gateway URL —
|
|
203
|
+
// take it from that environment's configuration rather than assuming it.
|
|
204
|
+
domain: envelopeDomainForYourEnvironment,
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Three things the verifier enforces, so they are not optional:
|
|
209
|
+
|
|
210
|
+
- **At least one credential.** The schema requires it and an empty bag is rejected — see the
|
|
211
|
+
prerequisite above.
|
|
212
|
+
- **The challenge binds one request** and **the domain binds one environment**. Both are checked
|
|
213
|
+
against the values the gateway expects, so an envelope cannot be replayed across either.
|
|
214
|
+
- **The declaration type is bound into the signature**, not merely present in the document —
|
|
215
|
+
editing the `type` array does not make a PaymentRequestEnvelope pass where a FulfillmentQuoteEnvelope is expected.
|
|
216
|
+
|
|
217
|
+
Sign the inner document with the **same key**: the gateway asserts the document's signer is
|
|
218
|
+
the envelope's holder, and rejects the pair otherwise.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atumlabs/payment-gateway-client",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
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",
|
|
@@ -31,39 +31,59 @@
|
|
|
31
31
|
},
|
|
32
32
|
"main": "dist/index.js",
|
|
33
33
|
"types": "dist/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"default": "./dist/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./signing": {
|
|
40
|
+
"types": "./dist/signing.d.ts",
|
|
41
|
+
"default": "./dist/signing.js"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
34
45
|
"bin": {
|
|
35
46
|
"send-payment": "./dist/cli/send-payment.js",
|
|
36
47
|
"request-quote": "./dist/cli/request-quote.js",
|
|
37
48
|
"quote-status": "./dist/cli/quote-status.js",
|
|
38
|
-
"payment-status": "./dist/cli/payment-status.js"
|
|
49
|
+
"payment-status": "./dist/cli/payment-status.js",
|
|
50
|
+
"approve-permit2": "./dist/cli/approve-permit2.js"
|
|
39
51
|
},
|
|
40
52
|
"scripts": {
|
|
41
|
-
"generate": "openapi-ts --input ../schemas/apis/payment-gateway/v1/openapi.yaml --output
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"build": "pnpm
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
53
|
+
"generate": "openapi-ts --input ../schemas/apis/payment-gateway/v1/openapi.yaml --output \"${PGC_GEN_OUT:-./src/generated}\" --client axios --name PaymentGatewayClient --useOptions",
|
|
54
|
+
"lint": "eslint . --max-warnings 0",
|
|
55
|
+
"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)}\"",
|
|
56
|
+
"build:deps": "pnpm --filter \"@atum-labs/payment-gateway-client^...\" run build",
|
|
57
|
+
"build": "tsup && chmod +x dist/cli/*.js && node scripts/stamp-entry-headers.mjs",
|
|
58
|
+
"typecheck:tests": "tsc -p tsconfig.test.json",
|
|
59
|
+
"test": "pnpm run typecheck:tests && jest",
|
|
60
|
+
"clean": "rm -rf dist && pnpm --filter \"@atum-labs/payment-gateway-client^...\" run clean",
|
|
48
61
|
"check:publishable": "node scripts/check-publishable.mjs",
|
|
49
62
|
"gen:notices": "node scripts/gen-third-party-notices.mjs",
|
|
50
63
|
"prepare": "pnpm run build",
|
|
51
|
-
"prepublishOnly": "pnpm run build && pnpm run check:publishable"
|
|
64
|
+
"prepublishOnly": "pnpm run build && pnpm run check:publishable",
|
|
65
|
+
"prebuild": "node scripts/gen-pre-context.mjs",
|
|
66
|
+
"check:pre-context": "node scripts/gen-pre-context.mjs --check"
|
|
52
67
|
},
|
|
53
68
|
"dependencies": {
|
|
54
69
|
"@solana/web3.js": "^1.91.0",
|
|
55
|
-
"axios": "1.
|
|
70
|
+
"axios": "1.20.0",
|
|
56
71
|
"bs58": "^6.0.0",
|
|
57
72
|
"commander": "^12.0.0",
|
|
58
73
|
"dotenv": "^16.6.1",
|
|
59
74
|
"ethers": "^6.11.0",
|
|
75
|
+
"jsonld": "8.3.3",
|
|
60
76
|
"tweetnacl": "^1.0.3",
|
|
61
77
|
"zod": "^3.25.0"
|
|
62
78
|
},
|
|
63
79
|
"devDependencies": {
|
|
80
|
+
"@atum-labs/chain-address": "workspace:*",
|
|
64
81
|
"@atum-labs/evm-escrow-encoding": "workspace:*",
|
|
65
82
|
"@atum-labs/km-core": "workspace:*",
|
|
83
|
+
"@atum-labs/ld-signing": "workspace:*",
|
|
84
|
+
"@atum-labs/payment-request-ld-signing": "workspace:*",
|
|
66
85
|
"@atum-labs/payment-request-sender-auth": "workspace:*",
|
|
86
|
+
"@atum-labs/payment-request-token-approval": "workspace:*",
|
|
67
87
|
"@atum-labs/publish-guard": "workspace:*",
|
|
68
88
|
"@atum-labs/solana-escrow-encoding": "workspace:*",
|
|
69
89
|
"@atum-labs/turnkey-signer": "workspace:*",
|
|
@@ -71,7 +91,10 @@
|
|
|
71
91
|
"@hey-api/openapi-ts": "^0.45.1",
|
|
72
92
|
"@types/jest": "^29.5.5",
|
|
73
93
|
"@types/node": "^20.0.0",
|
|
94
|
+
"ajv": "^8.17.1",
|
|
95
|
+
"ajv-formats": "^3.0.1",
|
|
74
96
|
"jest": "^29.7.0",
|
|
97
|
+
"tronweb": "^6.0.0",
|
|
75
98
|
"ts-jest": "^29.1.1",
|
|
76
99
|
"tsup": "^8.5.1",
|
|
77
100
|
"typescript": "^5.0.0",
|
|
@@ -83,10 +106,21 @@
|
|
|
83
106
|
"CHANGELOG.md",
|
|
84
107
|
"docs/**",
|
|
85
108
|
"LICENSE",
|
|
86
|
-
"
|
|
109
|
+
"NOTICE",
|
|
110
|
+
"THIRD-PARTY-NOTICES.txt",
|
|
111
|
+
"signing.js",
|
|
112
|
+
"signing.d.ts"
|
|
87
113
|
],
|
|
88
|
-
"license": "
|
|
114
|
+
"license": "Apache-2.0",
|
|
89
115
|
"publishConfig": {
|
|
90
|
-
"access": "
|
|
116
|
+
"access": "public"
|
|
117
|
+
},
|
|
118
|
+
"peerDependencies": {
|
|
119
|
+
"tronweb": "^6.0.0"
|
|
120
|
+
},
|
|
121
|
+
"peerDependenciesMeta": {
|
|
122
|
+
"tronweb": {
|
|
123
|
+
"optional": true
|
|
124
|
+
}
|
|
91
125
|
}
|
|
92
126
|
}
|
package/signing.d.ts
ADDED
package/signing.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Subpath shim for the classic Node resolution algorithm.
|
|
2
|
+
//
|
|
3
|
+
// package.json's "exports" map already declares "./signing", but `moduleResolution: "node"` —
|
|
4
|
+
// still the default for plenty of consumers — does not read "exports" at all. It resolves
|
|
5
|
+
// `@atum-labs/payment-gateway-client/signing` by looking for this file on disk. Without it the
|
|
6
|
+
// subpath works under "node16"/"bundler" and fails everywhere else, which is a failure a
|
|
7
|
+
// consumer meets at install rather than one we meet in CI.
|
|
8
|
+
//
|
|
9
|
+
// The publish guard caught exactly that: it type-checks against the packed tarball on the default
|
|
10
|
+
// settings, and the import did not resolve until this existed.
|
|
11
|
+
module.exports = require('./dist/signing.js');
|