@bitpal/checkout 0.10.0 → 0.12.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 +52 -0
- package/LICENSE +21 -21
- package/README.md +54 -14
- package/dist/index.cjs +105 -80
- package/dist/index.d.cts +100 -38
- package/dist/index.d.ts +100 -38
- package/dist/index.js +103 -79
- package/package.json +11 -11
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@bitpal/checkout`. Versions follow [semver](https://semver.org/) (pre-1.0: a minor bump may contain breaking changes, called out below).
|
|
4
|
+
|
|
5
|
+
Source of truth: the BitPal monorepo (`packages/checkout-sdk`). The public repo `superlabs-x/bitpal-checkout-sdk` mirrors it.
|
|
6
|
+
|
|
7
|
+
## 0.12.0 — 2026-11-06
|
|
8
|
+
|
|
9
|
+
Merges the x402 line (0.8.0–0.11.0, previously released from the standalone repo) with the checkout API alignment fixes made in the monorepo.
|
|
10
|
+
|
|
11
|
+
### Breaking (types)
|
|
12
|
+
- `CreatePriceParams.currency` removed. The API always rejected it — a Price's currency is derived from `token_symbol` (`'USDC' | 'USDT' | 'JPYC'`, default `'USDC'`). Code that passed `currency` got a `400`; pass `token_symbol` instead.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `PublicSession.pay_amount` — the exact amount the buyer must send, in the pay token's atomic units (`token_decimals`).
|
|
16
|
+
- `BitPalError` now carries structured fields parsed from the API error body: `code`, `apiMessage`, `retryable`, `traceId`. `error.message` is unchanged from 0.11 (it is still the body's `error` value), so existing `catch` code keeps working. Also exported: `parseApiErrorBody`, `BitPalErrorDetails`.
|
|
17
|
+
- `CreateSessionParams.expires_in_minutes` (1–60, default 15) — the session payment window.
|
|
18
|
+
- Non-JSON error responses (e.g. an HTML `502` from a gateway) now throw `BitPalError` with `HTTP <status>` instead of a `SyntaxError`.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Session expiry was silently ignored.** `expires_in_seconds` was never an API field, so every session used the 15-minute default. The SDK now sends `expires_in_minutes`; `expires_in_seconds` is deprecated and converted (rounded up to whole minutes, 60–3600 s). Out-of-range values throw before the request.
|
|
22
|
+
- `payer_wallet` and `pay_asset` on `createSession` are deprecated and no longer sent — the API never accepted them (it dropped them silently).
|
|
23
|
+
- A contract test checks every SDK request (method, path, query and body keys) against the API's generated OpenAPI spec, so SDK ↔ API drift like the two above fails CI.
|
|
24
|
+
|
|
25
|
+
### Fixed (docs / type comments)
|
|
26
|
+
- `amount_total` is **always canonical 6-decimal**, regardless of the pay token. Only `amount_received`, `pay_amount` and the issued `expectedAmount` use `token_decimals`. (0.11 said `amount_total` used token decimals — wrong for 18-decimal tokens.)
|
|
27
|
+
- Line item / Price `amount` is quoted in the line item's `currency` (`USDC`/`USDT` = USD, `JPYC` = JPY, no FX), not always USD.
|
|
28
|
+
- Browser use: only `getPublicSession` may be called from a merchant's browser (CORS). Call `issueDepositAddress` / `getStatus` through your backend.
|
|
29
|
+
- README: `previewFee` example now passes a human amount (`'29.00'`) — 0.11's README passed an atomic value, which the API reads as a human amount (1,000,000× too large).
|
|
30
|
+
- README: full payments after the deadline (up to 7 days) are accepted as late payments (`late: true` on `checkout.session.paid`), not refunded.
|
|
31
|
+
|
|
32
|
+
## 0.11.0 — 2026-09-17
|
|
33
|
+
- **Breaking (x402 payer):** standard x402 payload — one EIP-3009 authorization and one signature (`{ signature, authorization }`). Fee split is committed by the `payTo` forwarder address. Old two-signature headers get a fresh `402`.
|
|
34
|
+
- `paymentRequirements` carries the merchant wallet.
|
|
35
|
+
|
|
36
|
+
## 0.10.0 — 2026-09-17
|
|
37
|
+
- Webhook event `x402.payment.resolved` (an x402 payment left unresolved by `/settle` was later settled on-chain).
|
|
38
|
+
|
|
39
|
+
## 0.9.0 — 2026-09-17
|
|
40
|
+
- **Breaking:** `x402({ amount })` and `previewFee(amount)` take a human amount (`'1.00'`); use `amountAtomic` / `{ amountAtomic }` for raw values.
|
|
41
|
+
|
|
42
|
+
## 0.8.0 — 2026-09-16
|
|
43
|
+
- `x402()` per-call payment middleware, `createX402Payment()` payer helper, `X402ReplayStore` (memory default; pass a shared store when running several instances).
|
|
44
|
+
|
|
45
|
+
## 0.7.0 — 2026-07-30
|
|
46
|
+
- `allowed_assets: { chain, token }[]` as the standard way to set payment options.
|
|
47
|
+
|
|
48
|
+
## 0.6.0 — 2026-07-30
|
|
49
|
+
- Human `amount` input for line items and Prices; `paymentLinks` resource.
|
|
50
|
+
|
|
51
|
+
## 0.5.0 — 2026-07-23
|
|
52
|
+
- Initial public release: sessions, per-order deposit addresses, status polling, timestamped webhook signatures.
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 BitPal
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BitPal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -13,23 +13,28 @@ pnpm add @bitpal/checkout
|
|
|
13
13
|
|
|
14
14
|
## Amounts — just pass a human number
|
|
15
15
|
|
|
16
|
-
Use **`amount`** in line items — a plain
|
|
16
|
+
Use **`amount`** in line items — a plain decimal string like `"20"`, `"0.5"`, `"29.00"`. No decimals to compute.
|
|
17
17
|
|
|
18
18
|
```ts
|
|
19
19
|
line_items: [{ name: 'Pro', amount: '29', currency: 'USDC' }]
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
**The amount is
|
|
22
|
+
**The amount is quoted in the line item's `currency` — it does NOT depend on the chain or token decimals.** `currency: 'USDC'` / `'USDT'` means a USD price; `currency: 'JPYC'` means a JPY price (`amount: '1500'` → 1,500 JPYC — there is no FX conversion). Whether the buyer pays USDC (6dp) on Base or USDT (18dp) on BNB, you pass the same `amount`; BitPal rescales to the buyer's chosen token at deposit time. (Prices via `prices.create` take the same `amount`; their currency comes from `token_symbol`.)
|
|
23
23
|
|
|
24
24
|
<details><summary>Advanced: raw atomic amounts</summary>
|
|
25
25
|
|
|
26
|
-
If you already hold **
|
|
26
|
+
If you already hold **canonical 6-decimal integer strings** (`"1"` = `"1000000"`), pass `amount_atomic` instead (mutually exclusive with `amount`):
|
|
27
27
|
|
|
28
28
|
```
|
|
29
|
-
|
|
29
|
+
1.00 → "1000000" 29.00 → "29000000" 0.50 → "500000"
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Reading amounts back:
|
|
33
|
+
|
|
34
|
+
- `amount_total` is **always canonical 6dp**, whatever token the buyer pays with — `fromAtomic(amount_total, 6)`.
|
|
35
|
+
- `amount_received`, `pay_amount` (the exact amount the buyer must send) and the issued `expectedAmount` are in the **pay token's units** — use `token_decimals` (6 for USDC/USDT, 18 for BNB USDT / JPYC). Never assume 6dp for these.
|
|
36
|
+
|
|
37
|
+
Helpers: `toAtomic(v, decimals)` / `fromAtomic(v, decimals)`; `toAtomicUSDC`/`fromAtomicUSDC` are 6dp shortcuts.
|
|
33
38
|
</details>
|
|
34
39
|
|
|
35
40
|
## Environments
|
|
@@ -73,7 +78,7 @@ const { data: session } = await bitpal.checkout.createSession({
|
|
|
73
78
|
{ chain: 'tron', token: 'USDT' },
|
|
74
79
|
{ chain: 'solana', token: 'USDC' },
|
|
75
80
|
],
|
|
76
|
-
|
|
81
|
+
expires_in_minutes: 30,
|
|
77
82
|
success_url: 'https://yourstore.com/thanks',
|
|
78
83
|
});
|
|
79
84
|
|
|
@@ -96,6 +101,10 @@ The buyer then sends the exact amount from **any wallet or exchange** — no wal
|
|
|
96
101
|
signature; they pay their own network gas. A watcher detects the deposit and a keeper sweeps it to
|
|
97
102
|
you.
|
|
98
103
|
|
|
104
|
+
> **Call this from your backend.** Browsers on your own domain can only call `getPublicSession`
|
|
105
|
+
> directly (CORS). Proxy `issueDepositAddress` / `getStatus` through your server — and never ship a
|
|
106
|
+
> secret key to the browser.
|
|
107
|
+
|
|
99
108
|
```ts
|
|
100
109
|
const { data } = await bitpal.checkout.issueDepositAddress(session.id, {
|
|
101
110
|
session_token: session.session_token!, // from createSession
|
|
@@ -106,11 +115,13 @@ const { data } = await bitpal.checkout.issueDepositAddress(session.id, {
|
|
|
106
115
|
});
|
|
107
116
|
|
|
108
117
|
console.log(data.depositAddress); // buyer sends the exact amount here
|
|
109
|
-
console.log(data.expectedAmount); // atomic
|
|
118
|
+
console.log(data.expectedAmount); // atomic, in the pay token's decimals — display this exact amount
|
|
110
119
|
```
|
|
111
120
|
|
|
112
|
-
> `refundAddress` is committed into the deposit address and **cannot change** after issuance
|
|
113
|
-
>
|
|
121
|
+
> `refundAddress` is committed into the deposit address and **cannot change** after issuance.
|
|
122
|
+
> Overpaid excess, wrong tokens, and underpayments left at expiry are refunded there on-chain. A full
|
|
123
|
+
> payment that arrives after the deadline (up to 7 days) is accepted as a **late payment** — the
|
|
124
|
+
> `checkout.session.paid` webhook carries `late: true` so you can decide how to fulfil it.
|
|
114
125
|
|
|
115
126
|
### Poll status (buyer UX)
|
|
116
127
|
|
|
@@ -124,8 +135,9 @@ Polling is for buyer-facing UX. For **server-side** confirmation, trust the webh
|
|
|
124
135
|
### Preview the fee
|
|
125
136
|
|
|
126
137
|
```ts
|
|
127
|
-
const { data: fee } = await bitpal.checkout.previewFee(
|
|
128
|
-
|
|
138
|
+
const { data: fee } = await bitpal.checkout.previewFee('29.00'); // human amount
|
|
139
|
+
// or: await bitpal.checkout.previewFee({ amountAtomic: '29000000' }); // raw 6dp
|
|
140
|
+
console.log(fromAtomicUSDC(fee.fee_amount)); // response is atomic — e.g. "0.43"
|
|
129
141
|
```
|
|
130
142
|
|
|
131
143
|
## Webhooks
|
|
@@ -249,9 +261,13 @@ the response, or `release()` if you could not.
|
|
|
249
261
|
|
|
250
262
|
### Payer side
|
|
251
263
|
|
|
252
|
-
The
|
|
253
|
-
`
|
|
254
|
-
|
|
264
|
+
The payer signs **one** standard EIP-3009 authorization, so the `X-PAYMENT` payload is plain x402:
|
|
265
|
+
`{ signature, authorization }`. The fee split happens on-chain — `payTo` in the `402` is not the
|
|
266
|
+
merchant's wallet but a CREATE2 address that commits to the whole split, so nothing extra has to be
|
|
267
|
+
signed for it.
|
|
268
|
+
|
|
269
|
+
`createX402Payment` reads the `402` and builds the header. You supply the signer; the SDK has no
|
|
270
|
+
crypto dependency.
|
|
255
271
|
|
|
256
272
|
```ts
|
|
257
273
|
import { createX402Payment } from '@bitpal/checkout';
|
|
@@ -277,6 +293,10 @@ if (res.status === 402) {
|
|
|
277
293
|
Amounts are human-readable (`'1.00'`), the same model as `line_items[].amount` — the server converts.
|
|
278
294
|
Pass `amountAtomic` instead if you already have raw units.
|
|
279
295
|
|
|
296
|
+
The `402` also carries a `feeBreakdown` (net / fee / bps / recipient). It is informational — a payer
|
|
297
|
+
can ignore it entirely and still pay correctly, because the split is already committed by the `payTo`
|
|
298
|
+
address. It is there so you can see where the money goes without reading the chain.
|
|
299
|
+
|
|
280
300
|
EOA signers only — smart-contract wallets cannot sign EIP-3009 authorizations. Amounts are exact:
|
|
281
301
|
over- and underpayment are both rejected, and this lane has no refund path. Authorizations are valid
|
|
282
302
|
for 90–3600 seconds (the server's floor is 60s; the extra 30s absorbs clock skew and round trips).
|
|
@@ -308,6 +328,26 @@ store wiring session creation + webhook verification + idempotency.
|
|
|
308
328
|
pnpm build # tsup → dist/ (ESM + .d.ts)
|
|
309
329
|
```
|
|
310
330
|
|
|
331
|
+
## Migrating to 0.11.0 (breaking — x402 payer only)
|
|
332
|
+
|
|
333
|
+
The `X-PAYMENT` payload changed from BitPal's two-authorization shape to the **x402 standard**:
|
|
334
|
+
|
|
335
|
+
```
|
|
336
|
+
0.10.0 payload: { merchant: {...}, fee: {...} } two signatures
|
|
337
|
+
0.11.0 payload: { signature, authorization } one signature — plain x402
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
- If you call `createX402Payment`, nothing changes in your code. It signs once now instead of twice,
|
|
341
|
+
and the wallet prompts once.
|
|
342
|
+
- If you built the payload by hand, rebuild it in the standard shape.
|
|
343
|
+
- `splitSignature` is gone. `normalizeSignature` replaces it — same input handling (65-byte or
|
|
344
|
+
EIP-2098 compact, `v` of 0/1/27/28) but it returns a 65-byte packed hex string, which is what the
|
|
345
|
+
payload carries.
|
|
346
|
+
- The `402` is unchanged apart from `payTo`, which now points at the settlement address rather than
|
|
347
|
+
the merchant wallet. Both `X402Gate` and `x402()` handle this for you.
|
|
348
|
+
|
|
349
|
+
Old headers are rejected — a payer on 0.10.0 gets a fresh `402` and re-signs, so no payment is lost.
|
|
350
|
+
|
|
311
351
|
## Migrating from 0.3.x → 0.5.0 (breaking)
|
|
312
352
|
|
|
313
353
|
- **Webhook verification now requires the timestamp** and enforces replay defense. Pass the
|
package/dist/index.cjs
CHANGED
|
@@ -48,8 +48,9 @@ __export(index_exports, {
|
|
|
48
48
|
encodePaymentHeader: () => encodePaymentHeader,
|
|
49
49
|
fromAtomic: () => fromAtomic,
|
|
50
50
|
fromAtomicUSDC: () => fromAtomicUSDC,
|
|
51
|
+
normalizeSignature: () => normalizeSignature,
|
|
52
|
+
parseApiErrorBody: () => parseApiErrorBody,
|
|
51
53
|
parseWebhookEvent: () => parseWebhookEvent,
|
|
52
|
-
splitSignature: () => splitSignature,
|
|
53
54
|
toAtomic: () => toAtomic,
|
|
54
55
|
toAtomicUSDC: () => toAtomicUSDC,
|
|
55
56
|
verifyWebhookSignature: () => verifyWebhookSignature,
|
|
@@ -138,12 +139,25 @@ var PaymentLinksResource = class {
|
|
|
138
139
|
};
|
|
139
140
|
|
|
140
141
|
// src/client.ts
|
|
142
|
+
var EXPIRY_MIN_MINUTES = 1;
|
|
143
|
+
var EXPIRY_MAX_MINUTES = 60;
|
|
144
|
+
function resolveExpiryMinutes(minutes, seconds) {
|
|
145
|
+
if (minutes !== void 0 && seconds !== void 0) {
|
|
146
|
+
throw new Error("[BitPal] pass only one of `expires_in_minutes` or `expires_in_seconds`.");
|
|
147
|
+
}
|
|
148
|
+
const resolved = seconds !== void 0 ? Math.ceil(seconds / 60) : minutes;
|
|
149
|
+
if (resolved === void 0) return void 0;
|
|
150
|
+
if (!Number.isInteger(resolved) || resolved < EXPIRY_MIN_MINUTES || resolved > EXPIRY_MAX_MINUTES) {
|
|
151
|
+
throw new Error(`[BitPal] session expiry must be ${EXPIRY_MIN_MINUTES}\u2013${EXPIRY_MAX_MINUTES} minutes (expires_in_minutes) \u2014 got ${seconds !== void 0 ? seconds + "s" : minutes}.`);
|
|
152
|
+
}
|
|
153
|
+
return resolved;
|
|
154
|
+
}
|
|
141
155
|
function validateLineItem(li) {
|
|
142
156
|
if ("price_id" in li) return li;
|
|
143
157
|
const hasHuman = li.amount != null;
|
|
144
158
|
const hasAtomic = li.amount_atomic != null;
|
|
145
159
|
if (hasHuman === hasAtomic) {
|
|
146
|
-
throw new Error('[BitPal] line item requires exactly one of `amount` (
|
|
160
|
+
throw new Error('[BitPal] line item requires exactly one of `amount` (canonical decimal number, e.g. "29") or `amount_atomic` (raw canonical 6dp integer, e.g. "29000000").');
|
|
147
161
|
}
|
|
148
162
|
return li;
|
|
149
163
|
}
|
|
@@ -186,10 +200,18 @@ var BitPalCheckoutClient = class {
|
|
|
186
200
|
body: body ? JSON.stringify(body) : void 0,
|
|
187
201
|
signal: controller.signal
|
|
188
202
|
});
|
|
189
|
-
const
|
|
203
|
+
const text = await res.text();
|
|
204
|
+
let json = null;
|
|
205
|
+
try {
|
|
206
|
+
json = text ? JSON.parse(text) : null;
|
|
207
|
+
} catch {
|
|
208
|
+
if (!res.ok) throw new BitPalError(`HTTP ${res.status}`, res.status);
|
|
209
|
+
throw new BitPalError(`Invalid JSON response (HTTP ${res.status})`, res.status);
|
|
210
|
+
}
|
|
190
211
|
if (!res.ok) {
|
|
191
|
-
const
|
|
192
|
-
|
|
212
|
+
const err = json && typeof json === "object" ? json.error : void 0;
|
|
213
|
+
const msg = typeof err === "string" && err.length > 0 ? err : `HTTP ${res.status}`;
|
|
214
|
+
throw new BitPalError(msg, res.status, parseApiErrorBody(json));
|
|
193
215
|
}
|
|
194
216
|
return json;
|
|
195
217
|
} finally {
|
|
@@ -200,14 +222,16 @@ var BitPalCheckoutClient = class {
|
|
|
200
222
|
/**
|
|
201
223
|
* 체크아웃 세션 생성.
|
|
202
224
|
*
|
|
203
|
-
* `line_items[].amount` 는 사람 표기
|
|
204
|
-
*
|
|
225
|
+
* `line_items[].amount` 는 사람 표기 canonical 6dp 문자열("29", "0.5") — decimal 계산 불필요.
|
|
226
|
+
* `currency`('USDC'/'USDT'/'JPYC')가 실제 결제 통화를 결정하고, amount 는 항상 그 통화 기준 값이다
|
|
227
|
+
* (USD 로 고정된 게 아님 — currency='JPYC' + amount='1500' → 1500엔/1500 JPYC, 환율 변환 없음).
|
|
228
|
+
* 이미 raw canonical 6dp 정수라면 `amount_atomic`("29000000") 사용(고급). 둘은 상호 배타.
|
|
205
229
|
*
|
|
206
230
|
* @example
|
|
207
231
|
* import { BitPal } from '@bitpal/checkout';
|
|
208
232
|
* const bp = new BitPal('bp_test_...');
|
|
209
233
|
* const { data } = await bp.checkout.createSession({
|
|
210
|
-
* // amount: 사람 표기
|
|
234
|
+
* // amount: 사람 표기 canonical 6dp("29", "0.5"). 체인/토큰 decimal 은 발급 시 서버가 재스케일.
|
|
211
235
|
* line_items: [{ name: 'Pro', amount: '29', currency: 'USDC' }],
|
|
212
236
|
* });
|
|
213
237
|
* console.log(data.url); // → buyer를 redirect할 결제 페이지
|
|
@@ -224,8 +248,11 @@ var BitPalCheckoutClient = class {
|
|
|
224
248
|
if (params.pay_chain && params.allowed_pay_chains && params.allowed_pay_chains.length > 0) {
|
|
225
249
|
throw new Error("[BitPal] pay_chain and allowed_pay_chains are mutually exclusive \u2014 use only one.");
|
|
226
250
|
}
|
|
251
|
+
const { expires_in_seconds, payer_wallet: _payerWallet, pay_asset: _payAsset, ...rest } = params;
|
|
252
|
+
const expiresInMinutes = resolveExpiryMinutes(params.expires_in_minutes, expires_in_seconds);
|
|
227
253
|
const body = {
|
|
228
|
-
...
|
|
254
|
+
...rest,
|
|
255
|
+
...expiresInMinutes !== void 0 ? { expires_in_minutes: expiresInMinutes } : {},
|
|
229
256
|
line_items: params.line_items.map(validateLineItem),
|
|
230
257
|
...params.allowed_assets ? { allowed_assets: params.allowed_assets.map((a) => ({ caip2: a.chain, token: a.token })) } : {}
|
|
231
258
|
};
|
|
@@ -336,12 +363,37 @@ var BitPalCheckoutClient = class {
|
|
|
336
363
|
};
|
|
337
364
|
var BitPalError = class extends Error {
|
|
338
365
|
statusCode;
|
|
339
|
-
|
|
366
|
+
code;
|
|
367
|
+
apiMessage;
|
|
368
|
+
retryable;
|
|
369
|
+
traceId;
|
|
370
|
+
constructor(message, statusCode, details = {}) {
|
|
340
371
|
super(message);
|
|
341
372
|
this.name = "BitPalError";
|
|
342
373
|
this.statusCode = statusCode;
|
|
374
|
+
this.code = details.code;
|
|
375
|
+
this.apiMessage = details.apiMessage;
|
|
376
|
+
this.retryable = details.retryable;
|
|
377
|
+
this.traceId = details.traceId;
|
|
343
378
|
}
|
|
344
379
|
};
|
|
380
|
+
var ERROR_CODE_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
381
|
+
function parseApiErrorBody(body) {
|
|
382
|
+
if (!body || typeof body !== "object") return {};
|
|
383
|
+
const b = body;
|
|
384
|
+
const str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
385
|
+
const error = str(b.error);
|
|
386
|
+
const explicitCode = str(b.code);
|
|
387
|
+
const message = str(b.message);
|
|
388
|
+
const code = explicitCode ?? (error && ERROR_CODE_RE.test(error) ? error : void 0);
|
|
389
|
+
const apiMessage = message ?? (explicitCode && error && error !== explicitCode ? error : void 0);
|
|
390
|
+
return {
|
|
391
|
+
code,
|
|
392
|
+
apiMessage,
|
|
393
|
+
retryable: typeof b.retryable === "boolean" ? b.retryable : void 0,
|
|
394
|
+
traceId: str(b.trace_id)
|
|
395
|
+
};
|
|
396
|
+
}
|
|
345
397
|
|
|
346
398
|
// src/webhook.ts
|
|
347
399
|
var CHECKOUT_WEBHOOK_EVENTS = [
|
|
@@ -544,11 +596,11 @@ var X402Gate = class {
|
|
|
544
596
|
} catch {
|
|
545
597
|
return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
|
|
546
598
|
}
|
|
547
|
-
const
|
|
548
|
-
if (!
|
|
599
|
+
const auth = payload?.payload?.authorization;
|
|
600
|
+
if (!auth?.from || !auth?.nonce) {
|
|
549
601
|
return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
|
|
550
602
|
}
|
|
551
|
-
const key = `${payload.network}:${
|
|
603
|
+
const key = `${payload.network}:${auth.from.toLowerCase()}:${auth.nonce.toLowerCase()}`;
|
|
552
604
|
const claim = await this.store.claim(key, this.maxTimeoutSeconds);
|
|
553
605
|
if (claim === "used") {
|
|
554
606
|
return {
|
|
@@ -586,7 +638,12 @@ var X402Gate = class {
|
|
|
586
638
|
},
|
|
587
639
|
paymentRequirements: {
|
|
588
640
|
asset: accept.asset,
|
|
641
|
+
// payer 가 서명한 목적지 = 분배 조건을 커밋한 정산 주소.
|
|
589
642
|
payTo: accept.payTo,
|
|
643
|
+
// 머천트 정산 지갑. facilitator 가 이 값으로 정산 주소를 재계산해 payTo 와 대조한다 —
|
|
644
|
+
// CREATE2 는 단방향이라 payTo 에서 되돌릴 수 없으므로 따로 보내야 한다.
|
|
645
|
+
// 우리 설정값을 그대로 쓴다(402 응답에서 읽지 않는다 — 그러면 재계산의 의미가 없다).
|
|
646
|
+
merchant: this.opts.payTo,
|
|
590
647
|
maxAmountRequired: accept.maxAmountRequired,
|
|
591
648
|
resource: accept.resource
|
|
592
649
|
}
|
|
@@ -638,7 +695,7 @@ var X402Gate = class {
|
|
|
638
695
|
txHash: settled.tx_hash,
|
|
639
696
|
feeTxHash: settled.fee_tx_hash ?? null,
|
|
640
697
|
network: settled.network,
|
|
641
|
-
payer:
|
|
698
|
+
payer: auth.from,
|
|
642
699
|
amount: accept.maxAmountRequired,
|
|
643
700
|
idempotent: settled.idempotent ?? false
|
|
644
701
|
};
|
|
@@ -650,7 +707,7 @@ var X402Gate = class {
|
|
|
650
707
|
success: true,
|
|
651
708
|
transaction: settled.tx_hash,
|
|
652
709
|
network: settled.network,
|
|
653
|
-
payer:
|
|
710
|
+
payer: auth.from,
|
|
654
711
|
paymentId: settled.payment_id
|
|
655
712
|
}),
|
|
656
713
|
// 소진 확정은 **응답을 실제로 내준 뒤**다. 핸들러가 throw 하거나 프로세스가 죽으면 결제만 하고
|
|
@@ -771,18 +828,22 @@ function assertPaymentPayload(value) {
|
|
|
771
828
|
throw new Error("[BitPal] x402: invalid network.");
|
|
772
829
|
}
|
|
773
830
|
const inner = asObject(root.payload, "payload");
|
|
774
|
-
|
|
775
|
-
|
|
831
|
+
if (typeof inner.signature !== "string" || !PACKED_SIG_RE.test(inner.signature)) {
|
|
832
|
+
throw new Error("[BitPal] x402: invalid payload.signature (expected 65-byte packed hex).");
|
|
833
|
+
}
|
|
776
834
|
return {
|
|
777
835
|
x402Version: 1,
|
|
778
836
|
scheme: "exact",
|
|
779
837
|
network: root.network,
|
|
780
|
-
payload:
|
|
838
|
+
payload: {
|
|
839
|
+
signature: inner.signature,
|
|
840
|
+
authorization: assertAuthorization(inner.authorization, "authorization")
|
|
841
|
+
}
|
|
781
842
|
};
|
|
782
843
|
}
|
|
844
|
+
var PACKED_SIG_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
783
845
|
function assertAuthorization(value, label) {
|
|
784
|
-
const a = asObject(value,
|
|
785
|
-
const sig = asObject(a.signature, `${label} signature`);
|
|
846
|
+
const a = asObject(value, label);
|
|
786
847
|
const check = (ok, field) => {
|
|
787
848
|
if (!ok) throw new Error(`[BitPal] x402: invalid ${label}.${field}.`);
|
|
788
849
|
};
|
|
@@ -792,17 +853,13 @@ function assertAuthorization(value, label) {
|
|
|
792
853
|
check(typeof a.validAfter === "string" && DECIMAL_RE.test(a.validAfter), "validAfter");
|
|
793
854
|
check(typeof a.validBefore === "string" && DECIMAL_RE.test(a.validBefore), "validBefore");
|
|
794
855
|
check(typeof a.nonce === "string" && HEX32_RE.test(a.nonce), "nonce");
|
|
795
|
-
check(typeof sig.v === "number" && Number.isInteger(sig.v), "signature.v");
|
|
796
|
-
check(typeof sig.r === "string" && HEX32_RE.test(sig.r), "signature.r");
|
|
797
|
-
check(typeof sig.s === "string" && HEX32_RE.test(sig.s), "signature.s");
|
|
798
856
|
return {
|
|
799
857
|
from: a.from,
|
|
800
858
|
to: a.to,
|
|
801
859
|
value: a.value,
|
|
802
860
|
validAfter: a.validAfter,
|
|
803
861
|
validBefore: a.validBefore,
|
|
804
|
-
nonce: a.nonce
|
|
805
|
-
signature: { v: sig.v, r: sig.r, s: sig.s }
|
|
862
|
+
nonce: a.nonce
|
|
806
863
|
};
|
|
807
864
|
}
|
|
808
865
|
function asObject(value, label) {
|
|
@@ -876,49 +933,33 @@ async function createX402Payment(options) {
|
|
|
876
933
|
}
|
|
877
934
|
const validAfter = "0";
|
|
878
935
|
const validBefore = String(now + validFor);
|
|
879
|
-
const
|
|
880
|
-
const
|
|
881
|
-
const feeAmount = fee ? BigInt(fee.feeAmount) : 0n;
|
|
882
|
-
const merchantValue = fee ? fee.netAmount : accept.maxAmountRequired;
|
|
883
|
-
const merchant = await signAuthorization({
|
|
884
|
-
domain,
|
|
885
|
-
signTypedData: options.signTypedData,
|
|
936
|
+
const nonce = (options.randomNonce ?? randomNonce)();
|
|
937
|
+
const authorization = {
|
|
886
938
|
from: options.from,
|
|
887
939
|
to: accept.payTo,
|
|
888
|
-
value:
|
|
940
|
+
value: accept.maxAmountRequired,
|
|
889
941
|
validAfter,
|
|
890
942
|
validBefore,
|
|
891
|
-
nonce
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
if (!fee?.feeRecipient) {
|
|
896
|
-
throw new Error("[BitPal] x402: feeAmount > 0 but feeRecipient is missing in the 402 response.");
|
|
897
|
-
}
|
|
898
|
-
feeLeg = await signAuthorization({
|
|
943
|
+
nonce
|
|
944
|
+
};
|
|
945
|
+
const signature = normalizeSignature(
|
|
946
|
+
await options.signTypedData({
|
|
899
947
|
domain,
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
validBefore,
|
|
906
|
-
nonce: nextNonce()
|
|
907
|
-
});
|
|
908
|
-
if (feeLeg.nonce.toLowerCase() === merchant.nonce.toLowerCase()) {
|
|
909
|
-
throw new Error("[BitPal] x402: the two authorizations must use different nonces.");
|
|
910
|
-
}
|
|
911
|
-
}
|
|
948
|
+
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
949
|
+
primaryType: "TransferWithAuthorization",
|
|
950
|
+
message: authorization
|
|
951
|
+
})
|
|
952
|
+
);
|
|
912
953
|
const payload = {
|
|
913
954
|
x402Version: 1,
|
|
914
955
|
scheme: "exact",
|
|
915
956
|
network: accept.network,
|
|
916
|
-
payload:
|
|
957
|
+
payload: { signature, authorization }
|
|
917
958
|
};
|
|
918
959
|
return {
|
|
919
960
|
header: encodePaymentHeader(payload),
|
|
920
961
|
payload,
|
|
921
|
-
totalAmount:
|
|
962
|
+
totalAmount: accept.maxAmountRequired
|
|
922
963
|
};
|
|
923
964
|
}
|
|
924
965
|
function resolveAccept(input) {
|
|
@@ -929,41 +970,24 @@ function resolveAccept(input) {
|
|
|
929
970
|
}
|
|
930
971
|
return input;
|
|
931
972
|
}
|
|
932
|
-
|
|
933
|
-
const message = {
|
|
934
|
-
from: args.from,
|
|
935
|
-
to: args.to,
|
|
936
|
-
value: args.value,
|
|
937
|
-
validAfter: args.validAfter,
|
|
938
|
-
validBefore: args.validBefore,
|
|
939
|
-
nonce: args.nonce
|
|
940
|
-
};
|
|
941
|
-
const signature = await args.signTypedData({
|
|
942
|
-
domain: args.domain,
|
|
943
|
-
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
944
|
-
primaryType: "TransferWithAuthorization",
|
|
945
|
-
message
|
|
946
|
-
});
|
|
947
|
-
return { ...message, signature: splitSignature(signature) };
|
|
948
|
-
}
|
|
949
|
-
function splitSignature(signature) {
|
|
973
|
+
function normalizeSignature(signature) {
|
|
950
974
|
const hex = signature.startsWith("0x") ? signature.slice(2) : signature;
|
|
951
975
|
if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length !== 130 && hex.length !== 128) {
|
|
952
976
|
throw new Error(
|
|
953
977
|
`[BitPal] x402: expected a 65-byte or 64-byte (EIP-2098) hex signature, got ${signature.length} chars.`
|
|
954
978
|
);
|
|
955
979
|
}
|
|
956
|
-
const r =
|
|
980
|
+
const r = hex.slice(0, 64);
|
|
957
981
|
if (hex.length === 128) {
|
|
958
982
|
const yParityAndS = BigInt(`0x${hex.slice(64, 128)}`);
|
|
959
983
|
const yParity = Number(yParityAndS >> 255n & 1n);
|
|
960
|
-
const s2 = yParityAndS & (1n << 255n) - 1n;
|
|
961
|
-
return {
|
|
984
|
+
const s2 = (yParityAndS & (1n << 255n) - 1n).toString(16).padStart(64, "0");
|
|
985
|
+
return `0x${r}${s2}${(27 + yParity).toString(16).padStart(2, "0")}`;
|
|
962
986
|
}
|
|
963
|
-
const s =
|
|
987
|
+
const s = hex.slice(64, 128);
|
|
964
988
|
const raw = parseInt(hex.slice(128, 130), 16);
|
|
965
|
-
if (raw === 27 || raw === 28) return {
|
|
966
|
-
if (raw === 0 || raw === 1) return {
|
|
989
|
+
if (raw === 27 || raw === 28) return `0x${r}${s}${raw.toString(16)}`;
|
|
990
|
+
if (raw === 0 || raw === 1) return `0x${r}${s}${(raw + 27).toString(16)}`;
|
|
967
991
|
throw new Error(`[BitPal] x402: unexpected signature v=${raw} (expected 0, 1, 27 or 28).`);
|
|
968
992
|
}
|
|
969
993
|
function randomNonce() {
|
|
@@ -1005,8 +1029,9 @@ var BitPal = class {
|
|
|
1005
1029
|
encodePaymentHeader,
|
|
1006
1030
|
fromAtomic,
|
|
1007
1031
|
fromAtomicUSDC,
|
|
1032
|
+
normalizeSignature,
|
|
1033
|
+
parseApiErrorBody,
|
|
1008
1034
|
parseWebhookEvent,
|
|
1009
|
-
splitSignature,
|
|
1010
1035
|
toAtomic,
|
|
1011
1036
|
toAtomicUSDC,
|
|
1012
1037
|
verifyWebhookSignature,
|