@bitpal/checkout 0.3.0 → 0.6.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/LICENSE +21 -0
- package/README.md +222 -392
- package/dist/index.cjs +431 -0
- package/dist/index.d.cts +708 -0
- package/dist/index.d.ts +430 -433
- package/dist/index.js +187 -216
- package/package.json +33 -17
- package/MIGRATION_v0.2.md +0 -135
package/dist/index.js
CHANGED
|
@@ -1,15 +1,114 @@
|
|
|
1
|
+
// src/catalog.ts
|
|
2
|
+
function toQuery(params) {
|
|
3
|
+
if (!params) return "";
|
|
4
|
+
const entries = Object.entries(params).filter(([, v]) => v !== void 0);
|
|
5
|
+
if (entries.length === 0) return "";
|
|
6
|
+
const qs = new URLSearchParams(entries.map(([k, v]) => [k, String(v)]));
|
|
7
|
+
return `?${qs.toString()}`;
|
|
8
|
+
}
|
|
9
|
+
var ProductsResource = class {
|
|
10
|
+
constructor(request) {
|
|
11
|
+
this.request = request;
|
|
12
|
+
}
|
|
13
|
+
request;
|
|
14
|
+
/** POST /v1/checkout/products */
|
|
15
|
+
create(params) {
|
|
16
|
+
return this.request("POST", "/v1/checkout/products", params);
|
|
17
|
+
}
|
|
18
|
+
/** GET /v1/checkout/products */
|
|
19
|
+
list(params) {
|
|
20
|
+
return this.request("GET", `/v1/checkout/products${toQuery(params)}`);
|
|
21
|
+
}
|
|
22
|
+
/** GET /v1/checkout/products/:id */
|
|
23
|
+
retrieve(productId) {
|
|
24
|
+
return this.request("GET", `/v1/checkout/products/${encodeURIComponent(productId)}`);
|
|
25
|
+
}
|
|
26
|
+
/** PATCH /v1/checkout/products/:id */
|
|
27
|
+
update(productId, params) {
|
|
28
|
+
return this.request("PATCH", `/v1/checkout/products/${encodeURIComponent(productId)}`, params);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var PricesResource = class {
|
|
32
|
+
constructor(request) {
|
|
33
|
+
this.request = request;
|
|
34
|
+
}
|
|
35
|
+
request;
|
|
36
|
+
/** POST /v1/checkout/products/:id/prices */
|
|
37
|
+
create(productId, params) {
|
|
38
|
+
return this.request("POST", `/v1/checkout/products/${encodeURIComponent(productId)}/prices`, params);
|
|
39
|
+
}
|
|
40
|
+
/** GET /v1/checkout/products/:id/prices */
|
|
41
|
+
list(productId, params) {
|
|
42
|
+
return this.request("GET", `/v1/checkout/products/${encodeURIComponent(productId)}/prices${toQuery(params)}`);
|
|
43
|
+
}
|
|
44
|
+
/** GET /v1/checkout/prices/:id */
|
|
45
|
+
retrieve(priceId) {
|
|
46
|
+
return this.request("GET", `/v1/checkout/prices/${encodeURIComponent(priceId)}`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* PATCH /v1/checkout/prices/:id — nickname(표시 메타)만.
|
|
50
|
+
* 금액류 필드(amount_atomic 등)는 immutable — 서버가 PRICE_IMMUTABLE_FIELD로 거부한다.
|
|
51
|
+
* 가격 변경은 `prices.create()`로 새 price를 만들 것. "그만 팔기"는 해당 Price 를 쓰는
|
|
52
|
+
* Payment Link 를 inactive 로 한다(Price 자체에는 active/inactive 개념이 없다).
|
|
53
|
+
*/
|
|
54
|
+
update(priceId, params) {
|
|
55
|
+
return this.request("PATCH", `/v1/checkout/prices/${encodeURIComponent(priceId)}`, params);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var PaymentLinksResource = class {
|
|
59
|
+
constructor(request) {
|
|
60
|
+
this.request = request;
|
|
61
|
+
}
|
|
62
|
+
request;
|
|
63
|
+
/** POST /v1/checkout/payment-links */
|
|
64
|
+
create(params) {
|
|
65
|
+
return this.request("POST", "/v1/checkout/payment-links", params);
|
|
66
|
+
}
|
|
67
|
+
/** GET /v1/checkout/payment-links */
|
|
68
|
+
list(params) {
|
|
69
|
+
return this.request("GET", `/v1/checkout/payment-links${toQuery(params)}`);
|
|
70
|
+
}
|
|
71
|
+
/** GET /v1/checkout/payment-links/:id */
|
|
72
|
+
retrieve(linkId) {
|
|
73
|
+
return this.request("GET", `/v1/checkout/payment-links/${encodeURIComponent(linkId)}`);
|
|
74
|
+
}
|
|
75
|
+
/** PATCH /v1/checkout/payment-links/:id */
|
|
76
|
+
update(linkId, params) {
|
|
77
|
+
return this.request("PATCH", `/v1/checkout/payment-links/${encodeURIComponent(linkId)}`, params);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
1
81
|
// src/client.ts
|
|
82
|
+
function validateLineItem(li) {
|
|
83
|
+
if ("price_id" in li) return li;
|
|
84
|
+
const hasHuman = li.amount != null;
|
|
85
|
+
const hasAtomic = li.amount_atomic != null;
|
|
86
|
+
if (hasHuman === hasAtomic) {
|
|
87
|
+
throw new Error('[BitPal] line item requires exactly one of `amount` (USD number, e.g. "29") or `amount_atomic` (raw \u03BCUSD integer, e.g. "29000000").');
|
|
88
|
+
}
|
|
89
|
+
return li;
|
|
90
|
+
}
|
|
2
91
|
var DEFAULT_BASE_URL = "https://api.bitpal.io";
|
|
3
92
|
var DEFAULT_TIMEOUT = 3e4;
|
|
4
93
|
var BitPalCheckoutClient = class {
|
|
94
|
+
/** 서버 메서드용 secret 키. 공개(buyer-facing) 메서드는 없이도 동작. */
|
|
5
95
|
apiKey;
|
|
6
96
|
baseUrl;
|
|
7
97
|
timeout;
|
|
98
|
+
/** PROD-LINK-3a-SDK: Product 카탈로그 관리 (server-side 전용) */
|
|
99
|
+
products;
|
|
100
|
+
/** PROD-LINK-3a-SDK: immutable Price 관리 — 가격 변경은 비활성화 + 새 price 생성 */
|
|
101
|
+
prices;
|
|
102
|
+
/** PROD-LINK-4-SDK: Payment Link 관리 — 대시보드 없이 코드로 결제 링크 생성 (server-side 전용) */
|
|
103
|
+
paymentLinks;
|
|
8
104
|
constructor(config) {
|
|
9
|
-
if (!config.apiKey) throw new Error("apiKey is required");
|
|
10
105
|
this.apiKey = config.apiKey;
|
|
11
106
|
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
12
107
|
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
108
|
+
const requester = (method, path, body) => this.request(method, path, body);
|
|
109
|
+
this.products = new ProductsResource(requester);
|
|
110
|
+
this.prices = new PricesResource(requester);
|
|
111
|
+
this.paymentLinks = new PaymentLinksResource(requester);
|
|
13
112
|
}
|
|
14
113
|
/* ─── 내부 HTTP ─── */
|
|
15
114
|
async request(method, path, body, extraHeaders) {
|
|
@@ -21,7 +120,8 @@ var BitPalCheckoutClient = class {
|
|
|
21
120
|
method,
|
|
22
121
|
headers: {
|
|
23
122
|
"Content-Type": "application/json",
|
|
24
|
-
|
|
123
|
+
// 공개 메서드(session_token 기반)는 apiKey 없이 호출 가능 — 있을 때만 Authorization 첨부.
|
|
124
|
+
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
|
|
25
125
|
...extraHeaders ?? {}
|
|
26
126
|
},
|
|
27
127
|
body: body ? JSON.stringify(body) : void 0,
|
|
@@ -41,22 +141,35 @@ var BitPalCheckoutClient = class {
|
|
|
41
141
|
/**
|
|
42
142
|
* 체크아웃 세션 생성.
|
|
43
143
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
144
|
+
* `line_items[].amount` 는 사람 표기 USD 문자열("29", "0.5") — decimal 계산 불필요.
|
|
145
|
+
* 이미 raw μUSD 정수라면 `amount_atomic`("29000000") 사용(고급). 둘은 상호 배타.
|
|
46
146
|
*
|
|
47
147
|
* @example
|
|
48
|
-
* import { BitPal
|
|
148
|
+
* import { BitPal } from '@bitpal/checkout';
|
|
49
149
|
* const bp = new BitPal('bp_test_...');
|
|
50
150
|
* const { data } = await bp.checkout.createSession({
|
|
51
|
-
*
|
|
151
|
+
* // amount: 사람 표기 USD("29", "0.5"). 금액은 USD 가격이라 체인/토큰 decimal 무관.
|
|
152
|
+
* line_items: [{ name: 'Pro', amount: '29', currency: 'USDC' }],
|
|
52
153
|
* });
|
|
53
154
|
* console.log(data.url); // → buyer를 redirect할 결제 페이지
|
|
155
|
+
*
|
|
156
|
+
* // 멀티체인 허용 — 구매자가 Base/Optimism 중 선택(하나뿐이면 자동 선택):
|
|
157
|
+
* // 어떤 체인·토큰이든 amount 그대로 — 서버가 발급 시 토큰 decimals 로 재스케일.
|
|
158
|
+
* await bp.checkout.createSession({
|
|
159
|
+
* line_items: [{ name: 'Pro', amount: '29', currency: 'USDC' }],
|
|
160
|
+
* allowed_pay_chains: ['eip155:84532', 'eip155:11155420'],
|
|
161
|
+
* });
|
|
162
|
+
* // 단일 체인 고정은 pay_chain 사용(둘은 상호 배타).
|
|
54
163
|
*/
|
|
55
164
|
async createSession(params) {
|
|
165
|
+
if (params.pay_chain && params.allowed_pay_chains && params.allowed_pay_chains.length > 0) {
|
|
166
|
+
throw new Error("[BitPal] pay_chain and allowed_pay_chains are mutually exclusive \u2014 use only one.");
|
|
167
|
+
}
|
|
168
|
+
const body = { ...params, line_items: params.line_items.map(validateLineItem) };
|
|
56
169
|
const res = await this.request(
|
|
57
170
|
"POST",
|
|
58
171
|
"/v1/checkout/sessions",
|
|
59
|
-
|
|
172
|
+
body
|
|
60
173
|
);
|
|
61
174
|
if (res.data) {
|
|
62
175
|
if (res.data.checkout_url) {
|
|
@@ -86,148 +199,56 @@ var BitPalCheckoutClient = class {
|
|
|
86
199
|
`/v1/checkout/sessions${query ? `?${query}` : ""}`
|
|
87
200
|
);
|
|
88
201
|
}
|
|
89
|
-
/* ───
|
|
90
|
-
/**
|
|
91
|
-
* Auto refund — escrow 보관 중일 때 컨트랙트가 buyer에게 직접 transfer.
|
|
92
|
-
*
|
|
93
|
-
* `params.amount` 미지정 시 backend가 잔액 전액(`type: 'full'`). 명시 시 `type: 'partial'`.
|
|
94
|
-
* **Idempotency-Key 권장** — 같은 키로 재시도 시 backend가 같은 결과 반환.
|
|
95
|
-
*
|
|
96
|
-
* 머천트가 `frozen` 상태면 환불은 허용되지만, `suspended` 상태에서는 차단된다.
|
|
97
|
-
*
|
|
98
|
-
* @example
|
|
99
|
-
* ```ts
|
|
100
|
-
* import { randomUUID } from 'node:crypto';
|
|
101
|
-
* const { data } = await bitpal.checkout.refundSession(
|
|
102
|
-
* sessionId,
|
|
103
|
-
* { amount: toAtomicUSDC('5.00'), reason: 'customer_requested' },
|
|
104
|
-
* { idempotencyKey: randomUUID() },
|
|
105
|
-
* );
|
|
106
|
-
* console.log(data.txHash); // on-chain tx hash (또는 simulation 환경에서 sim_xxx)
|
|
107
|
-
* ```
|
|
108
|
-
*/
|
|
109
|
-
async refundSession(sessionId, params = {}, opts) {
|
|
110
|
-
return this.request(
|
|
111
|
-
"POST",
|
|
112
|
-
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/refund`,
|
|
113
|
-
params,
|
|
114
|
-
opts?.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : void 0
|
|
115
|
-
);
|
|
116
|
-
}
|
|
202
|
+
/* ─── 안2 비수탁 결제 (deposit-address) ─── */
|
|
117
203
|
/**
|
|
118
|
-
*
|
|
119
|
-
* 컨트랙트 호출 없음 (DB-only).
|
|
204
|
+
* 공개 세션 조회 (`GET /v1/checkout/sessions/:id/public`) — 인증 불요.
|
|
120
205
|
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* @example
|
|
124
|
-
* ```ts
|
|
125
|
-
* await bitpal.checkout.recordRefund(
|
|
126
|
-
* sessionId,
|
|
127
|
-
* {
|
|
128
|
-
* refund_amount: toAtomicUSDC('5.00'),
|
|
129
|
-
* tx_hash: '0xabc...',
|
|
130
|
-
* reason: 'manual_offchain_refund',
|
|
131
|
-
* },
|
|
132
|
-
* { idempotencyKey: randomUUID() },
|
|
133
|
-
* );
|
|
134
|
-
* ```
|
|
206
|
+
* buyer-facing 결제 페이지가 표시/자산선택에 쓰는 서버 권위 뷰. `payment_options` 로
|
|
207
|
+
* 구매자 선택 가능한 chain×token 을 얻고, 발급 후에는 `deposit_address` 가 채워진다.
|
|
135
208
|
*/
|
|
136
|
-
async
|
|
137
|
-
return this.request(
|
|
138
|
-
"POST",
|
|
139
|
-
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/record-refund`,
|
|
140
|
-
params,
|
|
141
|
-
opts?.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : void 0
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
/** 세션별 환불 내역 (auto + record 통합) */
|
|
145
|
-
async listRefunds(sessionId) {
|
|
209
|
+
async getPublicSession(sessionId) {
|
|
146
210
|
return this.request(
|
|
147
211
|
"GET",
|
|
148
|
-
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/
|
|
212
|
+
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/public`
|
|
149
213
|
);
|
|
150
214
|
}
|
|
151
|
-
/* ─── External On-chain Payments ─── */
|
|
152
215
|
/**
|
|
153
|
-
*
|
|
216
|
+
* 주문별 비수탁 입금주소 발급 (`POST /v1/checkout/sessions/:id/deposit-address`).
|
|
154
217
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
218
|
+
* 안2 결제 진입점. 구매자가 자산(멀티옵션이면 chain/token)과 환불 주소를 확정하면 서버가
|
|
219
|
+
* 그 주문 전용 CREATE2 forwarder 주소를 돌려준다. 구매자는 아무 지갑/거래소에서 **정확한 금액**을
|
|
220
|
+
* 그 주소로 일반 송금하면 된다(서명·연결 불필요). watcher 가 감지 → keeper 가 정산.
|
|
158
221
|
*
|
|
159
|
-
*
|
|
160
|
-
* - Merchant server (API key): `Authorization: Bearer bp_test_*` 헤더 자동 첨부
|
|
161
|
-
* - Buyer 측 (session_token): `params.session_token` 전달
|
|
222
|
+
* refundAddress 는 입금주소에 커밋되어 발급 후 변경 불가. session_token 은 createSession 응답값.
|
|
162
223
|
*
|
|
163
224
|
* @example
|
|
164
225
|
* ```ts
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* chain: '
|
|
226
|
+
* const { data } = await bitpal.checkout.issueDepositAddress(sessionId, {
|
|
227
|
+
* session_token: session.session_token!,
|
|
228
|
+
* refundAddress: '0xBuyerWallet...',
|
|
229
|
+
* chain: 'eip155:8453', // 멀티옵션 세션만 필요
|
|
230
|
+
* token: 'USDC',
|
|
169
231
|
* });
|
|
170
|
-
* console.log(data.
|
|
171
|
-
* console.log(data.session_status); // 'paid' | 'unresolved_underpaid' | ...
|
|
232
|
+
* console.log(data.depositAddress, data.expectedAmount); // 이 주소로 정확 금액 송금
|
|
172
233
|
* ```
|
|
173
234
|
*/
|
|
174
|
-
async
|
|
235
|
+
async issueDepositAddress(sessionId, params) {
|
|
175
236
|
return this.request(
|
|
176
237
|
"POST",
|
|
177
|
-
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/
|
|
178
|
-
params
|
|
179
|
-
opts?.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : void 0
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Gasless checkout payment through the BitPal API relayer.
|
|
184
|
-
*
|
|
185
|
-
* The buyer signs **two** EIP-712 typed-data structures and the API calls
|
|
186
|
-
* `CheckoutEscrow.depositWithAuthorization()` with the platform relayer:
|
|
187
|
-
*
|
|
188
|
-
* 1. **EIP-3009 USDC TransferWithAuthorization** (`params.authorization`) —
|
|
189
|
-
* authorizes USDC transfer from buyer to escrow.
|
|
190
|
-
* 2. **PaymentAuthorization** (`params.payment_auth`, since SDK v0.2.0) —
|
|
191
|
-
* binds the entire payment intent (paymentId, merchant, amount, fee,
|
|
192
|
-
* reserve, duration) to the buyer's signature. Without this, a compromised
|
|
193
|
-
* gateway key could redirect funds to an attacker-controlled merchant.
|
|
194
|
-
*
|
|
195
|
-
* Build the PaymentAuthorization typed-data with `buildPaymentAuthTypedData()`.
|
|
196
|
-
* The PaymentAuthorization nonce comes from `CheckoutEscrow.paymentAuthNonce(payer)`.
|
|
197
|
-
*
|
|
198
|
-
* Use the returned session/order state only as immediate UX feedback; the
|
|
199
|
-
* merchant backend should finalize fulfillment from signed webhooks.
|
|
200
|
-
*
|
|
201
|
-
* @breaking-change v0.2.0 — `payment_auth` field is now required. v0.1.x clients
|
|
202
|
-
* must upgrade their signing flow. See MIGRATION_v0.2.md.
|
|
203
|
-
*/
|
|
204
|
-
async payWithAuthorization(sessionId, params, opts) {
|
|
205
|
-
return this.request(
|
|
206
|
-
"POST",
|
|
207
|
-
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/submit-payment`,
|
|
208
|
-
params,
|
|
209
|
-
opts?.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : void 0
|
|
238
|
+
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/deposit-address`,
|
|
239
|
+
params
|
|
210
240
|
);
|
|
211
241
|
}
|
|
212
242
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* `getSession()`의 `payment_observations` 필드와 동일한 데이터를 반환하지만
|
|
216
|
-
* 검증 기록만 명시적으로 조회할 때 사용한다.
|
|
243
|
+
* 경량 상태 폴링 (`GET /v1/checkout/sessions/:id/status`) — 인증 불요.
|
|
217
244
|
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* const { data } = await bitpal.checkout.getExternalPaymentStatus(sessionId);
|
|
221
|
-
* const latest = data[0];
|
|
222
|
-
* if (latest?.finality_status === 'final_confirmed') {
|
|
223
|
-
* // 12 confirmations 완료 — 결제 확정
|
|
224
|
-
* }
|
|
225
|
-
* ```
|
|
245
|
+
* status: awaiting_deposit → deposit_detected → paid_confirmed/swept. 확정/정산은 webhook 으로도
|
|
246
|
+
* 통지되므로, 서버 사이드 확정은 webhook 을 신뢰(폴링은 buyer UX 용).
|
|
226
247
|
*/
|
|
227
|
-
async
|
|
248
|
+
async getStatus(sessionId) {
|
|
228
249
|
return this.request(
|
|
229
250
|
"GET",
|
|
230
|
-
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/
|
|
251
|
+
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/status`
|
|
231
252
|
);
|
|
232
253
|
}
|
|
233
254
|
/* ─── Fees ─── */
|
|
@@ -251,17 +272,12 @@ var BitPalError = class extends Error {
|
|
|
251
272
|
// src/webhook.ts
|
|
252
273
|
var CHECKOUT_WEBHOOK_EVENTS = [
|
|
253
274
|
"checkout.session.created",
|
|
275
|
+
"checkout.session.deposit_detected",
|
|
254
276
|
"checkout.session.paid",
|
|
255
277
|
"checkout.session.settled",
|
|
256
278
|
"checkout.session.expired",
|
|
257
279
|
"checkout.session.unresolved",
|
|
258
|
-
"checkout.
|
|
259
|
-
"checkout.payment.failed",
|
|
260
|
-
"checkout.payment.finalized",
|
|
261
|
-
"checkout.payment.revoked",
|
|
262
|
-
"checkout.refund.recorded",
|
|
263
|
-
"checkout.refund.completed",
|
|
264
|
-
"checkout.refund.partial"
|
|
280
|
+
"checkout.refund.recorded"
|
|
265
281
|
];
|
|
266
282
|
var WEBHOOK_HEADERS = {
|
|
267
283
|
signature: "X-Webhook-Signature-256",
|
|
@@ -271,14 +287,21 @@ var WEBHOOK_HEADERS = {
|
|
|
271
287
|
deliveryId: "X-Webhook-Delivery-Id"
|
|
272
288
|
};
|
|
273
289
|
var SIGNATURE_PREFIX = "sha256=";
|
|
274
|
-
async function verifyWebhookSignature(payload, signature, secret) {
|
|
290
|
+
async function verifyWebhookSignature(payload, signature, secret, opts) {
|
|
275
291
|
if (!payload || !signature || !secret) return false;
|
|
276
292
|
if (!signature.startsWith(SIGNATURE_PREFIX)) return false;
|
|
293
|
+
const timestamp = opts?.timestamp;
|
|
294
|
+
if (!timestamp) return false;
|
|
295
|
+
const parsedMs = Date.parse(timestamp);
|
|
296
|
+
if (!Number.isFinite(parsedMs)) return false;
|
|
297
|
+
if (new Date(parsedMs).toISOString() !== timestamp) return false;
|
|
298
|
+
const toleranceMs = (opts?.toleranceSec ?? 300) * 1e3;
|
|
299
|
+
if (Math.abs(Date.now() - parsedMs) > toleranceMs) return false;
|
|
277
300
|
const sigHex = signature.slice(SIGNATURE_PREFIX.length);
|
|
278
|
-
if (!/^[0-9a-f]
|
|
301
|
+
if (!/^[0-9a-f]{64}$/.test(sigHex)) return false;
|
|
279
302
|
try {
|
|
280
303
|
const crypto = await import("crypto");
|
|
281
|
-
const expectedHex = crypto.createHmac("sha256", secret).update(payload
|
|
304
|
+
const expectedHex = crypto.createHmac("sha256", secret).update(`${timestamp}.${payload}`, "utf8").digest("hex");
|
|
282
305
|
if (sigHex.length !== expectedHex.length) return false;
|
|
283
306
|
return crypto.timingSafeEqual(
|
|
284
307
|
Buffer.from(sigHex, "hex"),
|
|
@@ -297,93 +320,39 @@ function parseWebhookEvent(body) {
|
|
|
297
320
|
}
|
|
298
321
|
|
|
299
322
|
// src/amount.ts
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (typeof decimal !== "string" || !/^\d+(\.\d{1,6})?$/.test(decimal)) {
|
|
304
|
-
throw new Error(`toAtomicUSDC: invalid decimal '${decimal}' (max 6 fractional digits, no negatives)`);
|
|
323
|
+
function toAtomic(decimal, decimals) {
|
|
324
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 36) {
|
|
325
|
+
throw new Error(`toAtomic: invalid decimals '${decimals}'`);
|
|
305
326
|
}
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
327
|
+
const fracPattern = decimals > 0 ? `(\\.\\d{1,${decimals}})?` : "";
|
|
328
|
+
if (typeof decimal !== "string" || !new RegExp(`^\\d+${fracPattern}$`).test(decimal)) {
|
|
329
|
+
throw new Error(`toAtomic: invalid decimal '${decimal}' (max ${decimals} fractional digits, no negatives)`);
|
|
330
|
+
}
|
|
331
|
+
const scale = 10n ** BigInt(decimals);
|
|
332
|
+
const [whole = "0", frac = ""] = decimal.split(".");
|
|
333
|
+
const fracPadded = frac.padEnd(decimals, "0");
|
|
334
|
+
return (BigInt(whole) * scale + BigInt(fracPadded || "0")).toString();
|
|
309
335
|
}
|
|
310
|
-
function
|
|
336
|
+
function fromAtomic(atomic, decimals) {
|
|
337
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 36) {
|
|
338
|
+
throw new Error(`fromAtomic: invalid decimals '${decimals}'`);
|
|
339
|
+
}
|
|
311
340
|
if (typeof atomic !== "string" || !/^\d+$/.test(atomic)) {
|
|
312
|
-
throw new Error(`
|
|
341
|
+
throw new Error(`fromAtomic: invalid atomic '${atomic}' (must be non-negative integer string)`);
|
|
313
342
|
}
|
|
343
|
+
const scale = 10n ** BigInt(decimals);
|
|
314
344
|
const big = BigInt(atomic);
|
|
315
|
-
const whole = big /
|
|
316
|
-
const frac = big %
|
|
317
|
-
|
|
345
|
+
const whole = big / scale;
|
|
346
|
+
const frac = big % scale;
|
|
347
|
+
if (decimals === 0) return `${whole}`;
|
|
348
|
+
const fracStr = frac.toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
318
349
|
return fracStr.length > 0 ? `${whole}.${fracStr.padEnd(2, "0")}` : `${whole}.00`;
|
|
319
350
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
function buildPaymentAuthTypedData(args) {
|
|
323
|
-
return {
|
|
324
|
-
domain: {
|
|
325
|
-
name: "CheckoutEscrow",
|
|
326
|
-
version: "1",
|
|
327
|
-
chainId: args.chainId,
|
|
328
|
-
verifyingContract: args.escrowAddress
|
|
329
|
-
},
|
|
330
|
-
types: {
|
|
331
|
-
PaymentAuthorization: [
|
|
332
|
-
{ name: "paymentId", type: "bytes32" },
|
|
333
|
-
{ name: "payer", type: "address" },
|
|
334
|
-
{ name: "merchant", type: "address" },
|
|
335
|
-
{ name: "token", type: "address" },
|
|
336
|
-
{ name: "amount", type: "uint256" },
|
|
337
|
-
{ name: "feeAmount", type: "uint256" },
|
|
338
|
-
{ name: "reserveAmount", type: "uint256" },
|
|
339
|
-
{ name: "duration", type: "uint256" },
|
|
340
|
-
{ name: "deadline", type: "uint256" },
|
|
341
|
-
{ name: "nonce", type: "uint256" }
|
|
342
|
-
]
|
|
343
|
-
},
|
|
344
|
-
primaryType: "PaymentAuthorization",
|
|
345
|
-
message: {
|
|
346
|
-
paymentId: args.paymentId,
|
|
347
|
-
payer: args.payer,
|
|
348
|
-
merchant: args.merchant,
|
|
349
|
-
token: args.token,
|
|
350
|
-
amount: BigInt(args.amount),
|
|
351
|
-
feeAmount: BigInt(args.feeAmount),
|
|
352
|
-
reserveAmount: BigInt(args.reserveAmount),
|
|
353
|
-
duration: BigInt(args.duration),
|
|
354
|
-
deadline: BigInt(args.deadline),
|
|
355
|
-
nonce: BigInt(args.nonce)
|
|
356
|
-
}
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
function buildConfirmReceiptTypedData(args) {
|
|
360
|
-
return {
|
|
361
|
-
domain: {
|
|
362
|
-
name: "CheckoutEscrow",
|
|
363
|
-
version: "1",
|
|
364
|
-
chainId: args.chainId,
|
|
365
|
-
verifyingContract: args.escrowAddress
|
|
366
|
-
},
|
|
367
|
-
types: {
|
|
368
|
-
ConfirmReceipt: [
|
|
369
|
-
{ name: "paymentId", type: "bytes32" },
|
|
370
|
-
{ name: "payer", type: "address" },
|
|
371
|
-
{ name: "deadline", type: "uint256" },
|
|
372
|
-
{ name: "nonce", type: "uint256" }
|
|
373
|
-
]
|
|
374
|
-
},
|
|
375
|
-
primaryType: "ConfirmReceipt",
|
|
376
|
-
message: {
|
|
377
|
-
paymentId: args.paymentId,
|
|
378
|
-
payer: args.payer,
|
|
379
|
-
deadline: BigInt(args.deadline),
|
|
380
|
-
nonce: BigInt(args.nonce)
|
|
381
|
-
}
|
|
382
|
-
};
|
|
351
|
+
function toAtomicUSDC(decimal) {
|
|
352
|
+
return toAtomic(decimal, 6);
|
|
383
353
|
}
|
|
384
|
-
function
|
|
385
|
-
|
|
386
|
-
return `0x${hex}`;
|
|
354
|
+
function fromAtomicUSDC(atomic) {
|
|
355
|
+
return fromAtomic(atomic, 6);
|
|
387
356
|
}
|
|
388
357
|
|
|
389
358
|
// src/index.ts
|
|
@@ -399,12 +368,14 @@ export {
|
|
|
399
368
|
BitPalCheckoutClient,
|
|
400
369
|
BitPalError,
|
|
401
370
|
CHECKOUT_WEBHOOK_EVENTS,
|
|
371
|
+
PaymentLinksResource,
|
|
372
|
+
PricesResource,
|
|
373
|
+
ProductsResource,
|
|
402
374
|
WEBHOOK_HEADERS,
|
|
403
|
-
|
|
404
|
-
buildPaymentAuthTypedData,
|
|
375
|
+
fromAtomic,
|
|
405
376
|
fromAtomicUSDC,
|
|
406
377
|
parseWebhookEvent,
|
|
407
|
-
|
|
378
|
+
toAtomic,
|
|
408
379
|
toAtomicUSDC,
|
|
409
380
|
verifyWebhookSignature
|
|
410
381
|
};
|
package/package.json
CHANGED
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitpal/checkout",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "BitPal Checkout SDK — crypto
|
|
5
|
-
"license": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "BitPal Checkout SDK — non-custodial crypto checkout: create sessions, issue per-order deposit addresses, poll status, verify signed webhooks.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/superlabs-x/bitpal-checkout-sdk.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/superlabs-x/bitpal-checkout-sdk#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/superlabs-x/bitpal-checkout-sdk/issues"
|
|
13
|
+
},
|
|
6
14
|
"type": "module",
|
|
7
|
-
"main": "dist/index.
|
|
8
|
-
"
|
|
15
|
+
"main": "./dist/index.cjs",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
9
18
|
"exports": {
|
|
10
19
|
".": {
|
|
11
|
-
"import":
|
|
12
|
-
|
|
20
|
+
"import": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"require": {
|
|
25
|
+
"types": "./dist/index.d.cts",
|
|
26
|
+
"default": "./dist/index.cjs"
|
|
27
|
+
}
|
|
13
28
|
}
|
|
14
29
|
},
|
|
15
30
|
"files": [
|
|
16
31
|
"dist",
|
|
17
|
-
"README.md"
|
|
18
|
-
"MIGRATION_v0.2.md"
|
|
32
|
+
"README.md"
|
|
19
33
|
],
|
|
20
34
|
"keywords": [
|
|
21
35
|
"bitpal",
|
|
@@ -27,7 +41,7 @@
|
|
|
27
41
|
"evm",
|
|
28
42
|
"base",
|
|
29
43
|
"stablecoin",
|
|
30
|
-
"
|
|
44
|
+
"non-custodial"
|
|
31
45
|
],
|
|
32
46
|
"publishConfig": {
|
|
33
47
|
"access": "public"
|
|
@@ -35,15 +49,17 @@
|
|
|
35
49
|
"engines": {
|
|
36
50
|
"node": ">=18"
|
|
37
51
|
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
54
|
+
"lint": "tsc --noEmit",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"prepublishOnly": "npm test && npm run build",
|
|
57
|
+
"release": "npm publish --access public"
|
|
58
|
+
},
|
|
38
59
|
"devDependencies": {
|
|
60
|
+
"@types/node": "^20.11.0",
|
|
39
61
|
"tsup": "^8.5.1",
|
|
40
62
|
"typescript": "^5.5.0",
|
|
41
63
|
"vitest": "^2.1.9"
|
|
42
|
-
},
|
|
43
|
-
"scripts": {
|
|
44
|
-
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
45
|
-
"lint": "tsc --noEmit",
|
|
46
|
-
"test": "vitest run",
|
|
47
|
-
"release": "pnpm publish --access public --no-git-checks"
|
|
48
64
|
}
|
|
49
|
-
}
|
|
65
|
+
}
|