@bitpal/checkout 0.3.0 → 0.5.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 +221 -392
- package/dist/index.cjs +374 -0
- package/dist/index.d.cts +566 -0
- package/dist/index.d.ts +266 -411
- package/dist/index.js +118 -201
- package/package.json +33 -17
- package/MIGRATION_v0.2.md +0 -135
package/dist/index.js
CHANGED
|
@@ -1,15 +1,80 @@
|
|
|
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
|
+
|
|
1
59
|
// src/client.ts
|
|
2
60
|
var DEFAULT_BASE_URL = "https://api.bitpal.io";
|
|
3
61
|
var DEFAULT_TIMEOUT = 3e4;
|
|
4
62
|
var BitPalCheckoutClient = class {
|
|
63
|
+
/** 서버 메서드용 secret 키. 공개(buyer-facing) 메서드는 없이도 동작. */
|
|
5
64
|
apiKey;
|
|
6
65
|
baseUrl;
|
|
7
66
|
timeout;
|
|
67
|
+
/** PROD-LINK-3a-SDK: Product 카탈로그 관리 (server-side 전용) */
|
|
68
|
+
products;
|
|
69
|
+
/** PROD-LINK-3a-SDK: immutable Price 관리 — 가격 변경은 비활성화 + 새 price 생성 */
|
|
70
|
+
prices;
|
|
8
71
|
constructor(config) {
|
|
9
|
-
if (!config.apiKey) throw new Error("apiKey is required");
|
|
10
72
|
this.apiKey = config.apiKey;
|
|
11
73
|
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
12
74
|
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
75
|
+
const requester = (method, path, body) => this.request(method, path, body);
|
|
76
|
+
this.products = new ProductsResource(requester);
|
|
77
|
+
this.prices = new PricesResource(requester);
|
|
13
78
|
}
|
|
14
79
|
/* ─── 내부 HTTP ─── */
|
|
15
80
|
async request(method, path, body, extraHeaders) {
|
|
@@ -21,7 +86,8 @@ var BitPalCheckoutClient = class {
|
|
|
21
86
|
method,
|
|
22
87
|
headers: {
|
|
23
88
|
"Content-Type": "application/json",
|
|
24
|
-
|
|
89
|
+
// 공개 메서드(session_token 기반)는 apiKey 없이 호출 가능 — 있을 때만 Authorization 첨부.
|
|
90
|
+
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
|
|
25
91
|
...extraHeaders ?? {}
|
|
26
92
|
},
|
|
27
93
|
body: body ? JSON.stringify(body) : void 0,
|
|
@@ -51,8 +117,18 @@ var BitPalCheckoutClient = class {
|
|
|
51
117
|
* line_items: [{ name: 'Pro', amount: toAtomicUSDC('29.00'), currency: 'USDC' }],
|
|
52
118
|
* });
|
|
53
119
|
* console.log(data.url); // → buyer를 redirect할 결제 페이지
|
|
120
|
+
*
|
|
121
|
+
* // 멀티체인 허용 — 구매자가 Base/Optimism 중 선택(하나뿐이면 자동 선택):
|
|
122
|
+
* await bp.checkout.createSession({
|
|
123
|
+
* line_items: [{ name: 'Pro', amount: toAtomicUSDC('29.00'), currency: 'USDC' }],
|
|
124
|
+
* allowed_pay_chains: ['eip155:84532', 'eip155:11155420'],
|
|
125
|
+
* });
|
|
126
|
+
* // 단일 체인 고정은 pay_chain 사용(둘은 상호 배타).
|
|
54
127
|
*/
|
|
55
128
|
async createSession(params) {
|
|
129
|
+
if (params.pay_chain && params.allowed_pay_chains && params.allowed_pay_chains.length > 0) {
|
|
130
|
+
throw new Error("[BitPal] pay_chain and allowed_pay_chains are mutually exclusive \u2014 use only one.");
|
|
131
|
+
}
|
|
56
132
|
const res = await this.request(
|
|
57
133
|
"POST",
|
|
58
134
|
"/v1/checkout/sessions",
|
|
@@ -86,148 +162,56 @@ var BitPalCheckoutClient = class {
|
|
|
86
162
|
`/v1/checkout/sessions${query ? `?${query}` : ""}`
|
|
87
163
|
);
|
|
88
164
|
}
|
|
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
|
-
}
|
|
165
|
+
/* ─── 비수탁 결제 (deposit-address) ─── */
|
|
117
166
|
/**
|
|
118
|
-
*
|
|
119
|
-
* 컨트랙트 호출 없음 (DB-only).
|
|
120
|
-
*
|
|
121
|
-
* **Idempotency-Key 권장**.
|
|
167
|
+
* 공개 세션 조회 (`GET /v1/checkout/sessions/:id/public`) — 인증 불요.
|
|
122
168
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
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
|
-
* ```
|
|
169
|
+
* buyer-facing 결제 페이지가 표시/자산선택에 쓰는 서버 권위 뷰. `payment_options` 로
|
|
170
|
+
* 구매자 선택 가능한 chain×token 을 얻고, 발급 후에는 `deposit_address` 가 채워진다.
|
|
135
171
|
*/
|
|
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) {
|
|
172
|
+
async getPublicSession(sessionId) {
|
|
146
173
|
return this.request(
|
|
147
174
|
"GET",
|
|
148
|
-
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/
|
|
175
|
+
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/public`
|
|
149
176
|
);
|
|
150
177
|
}
|
|
151
|
-
/* ─── External On-chain Payments ─── */
|
|
152
178
|
/**
|
|
153
|
-
*
|
|
179
|
+
* 주문별 비수탁 입금주소 발급 (`POST /v1/checkout/sessions/:id/deposit-address`).
|
|
154
180
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
181
|
+
* 비수탁 결제 진입점. 구매자가 자산(멀티옵션이면 chain/token)과 환불 주소를 확정하면 서버가
|
|
182
|
+
* 그 주문 전용 CREATE2 forwarder 주소를 돌려준다. 구매자는 아무 지갑/거래소에서 **정확한 금액**을
|
|
183
|
+
* 그 주소로 일반 송금하면 된다(서명·연결 불필요). watcher 가 감지 → keeper 가 정산.
|
|
158
184
|
*
|
|
159
|
-
*
|
|
160
|
-
* - Merchant server (API key): `Authorization: Bearer bp_test_*` 헤더 자동 첨부
|
|
161
|
-
* - Buyer 측 (session_token): `params.session_token` 전달
|
|
185
|
+
* refundAddress 는 입금주소에 커밋되어 발급 후 변경 불가. session_token 은 createSession 응답값.
|
|
162
186
|
*
|
|
163
187
|
* @example
|
|
164
188
|
* ```ts
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* chain: '
|
|
189
|
+
* const { data } = await bitpal.checkout.issueDepositAddress(sessionId, {
|
|
190
|
+
* session_token: session.session_token!,
|
|
191
|
+
* refundAddress: '0xBuyerWallet...',
|
|
192
|
+
* chain: 'eip155:8453', // 멀티옵션 세션만 필요
|
|
193
|
+
* token: 'USDC',
|
|
169
194
|
* });
|
|
170
|
-
* console.log(data.
|
|
171
|
-
* console.log(data.session_status); // 'paid' | 'unresolved_underpaid' | ...
|
|
195
|
+
* console.log(data.depositAddress, data.expectedAmount); // 이 주소로 정확 금액 송금
|
|
172
196
|
* ```
|
|
173
197
|
*/
|
|
174
|
-
async
|
|
198
|
+
async issueDepositAddress(sessionId, params) {
|
|
175
199
|
return this.request(
|
|
176
200
|
"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
|
|
201
|
+
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/deposit-address`,
|
|
202
|
+
params
|
|
210
203
|
);
|
|
211
204
|
}
|
|
212
205
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* `getSession()`의 `payment_observations` 필드와 동일한 데이터를 반환하지만
|
|
216
|
-
* 검증 기록만 명시적으로 조회할 때 사용한다.
|
|
206
|
+
* 경량 상태 폴링 (`GET /v1/checkout/sessions/:id/status`) — 인증 불요.
|
|
217
207
|
*
|
|
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
|
-
* ```
|
|
208
|
+
* status: awaiting_deposit → deposit_detected → paid_confirmed/swept. 확정/정산은 webhook 으로도
|
|
209
|
+
* 통지되므로, 서버 사이드 확정은 webhook 을 신뢰(폴링은 buyer UX 용).
|
|
226
210
|
*/
|
|
227
|
-
async
|
|
211
|
+
async getStatus(sessionId) {
|
|
228
212
|
return this.request(
|
|
229
213
|
"GET",
|
|
230
|
-
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/
|
|
214
|
+
`/v1/checkout/sessions/${encodeURIComponent(sessionId)}/status`
|
|
231
215
|
);
|
|
232
216
|
}
|
|
233
217
|
/* ─── Fees ─── */
|
|
@@ -251,17 +235,12 @@ var BitPalError = class extends Error {
|
|
|
251
235
|
// src/webhook.ts
|
|
252
236
|
var CHECKOUT_WEBHOOK_EVENTS = [
|
|
253
237
|
"checkout.session.created",
|
|
238
|
+
"checkout.session.deposit_detected",
|
|
254
239
|
"checkout.session.paid",
|
|
255
240
|
"checkout.session.settled",
|
|
256
241
|
"checkout.session.expired",
|
|
257
242
|
"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"
|
|
243
|
+
"checkout.refund.recorded"
|
|
265
244
|
];
|
|
266
245
|
var WEBHOOK_HEADERS = {
|
|
267
246
|
signature: "X-Webhook-Signature-256",
|
|
@@ -271,14 +250,21 @@ var WEBHOOK_HEADERS = {
|
|
|
271
250
|
deliveryId: "X-Webhook-Delivery-Id"
|
|
272
251
|
};
|
|
273
252
|
var SIGNATURE_PREFIX = "sha256=";
|
|
274
|
-
async function verifyWebhookSignature(payload, signature, secret) {
|
|
253
|
+
async function verifyWebhookSignature(payload, signature, secret, opts) {
|
|
275
254
|
if (!payload || !signature || !secret) return false;
|
|
276
255
|
if (!signature.startsWith(SIGNATURE_PREFIX)) return false;
|
|
256
|
+
const timestamp = opts?.timestamp;
|
|
257
|
+
if (!timestamp) return false;
|
|
258
|
+
const parsedMs = Date.parse(timestamp);
|
|
259
|
+
if (!Number.isFinite(parsedMs)) return false;
|
|
260
|
+
if (new Date(parsedMs).toISOString() !== timestamp) return false;
|
|
261
|
+
const toleranceMs = (opts?.toleranceSec ?? 300) * 1e3;
|
|
262
|
+
if (Math.abs(Date.now() - parsedMs) > toleranceMs) return false;
|
|
277
263
|
const sigHex = signature.slice(SIGNATURE_PREFIX.length);
|
|
278
|
-
if (!/^[0-9a-f]
|
|
264
|
+
if (!/^[0-9a-f]{64}$/.test(sigHex)) return false;
|
|
279
265
|
try {
|
|
280
266
|
const crypto = await import("crypto");
|
|
281
|
-
const expectedHex = crypto.createHmac("sha256", secret).update(payload
|
|
267
|
+
const expectedHex = crypto.createHmac("sha256", secret).update(`${timestamp}.${payload}`, "utf8").digest("hex");
|
|
282
268
|
if (sigHex.length !== expectedHex.length) return false;
|
|
283
269
|
return crypto.timingSafeEqual(
|
|
284
270
|
Buffer.from(sigHex, "hex"),
|
|
@@ -303,7 +289,7 @@ function toAtomicUSDC(decimal) {
|
|
|
303
289
|
if (typeof decimal !== "string" || !/^\d+(\.\d{1,6})?$/.test(decimal)) {
|
|
304
290
|
throw new Error(`toAtomicUSDC: invalid decimal '${decimal}' (max 6 fractional digits, no negatives)`);
|
|
305
291
|
}
|
|
306
|
-
const [whole, frac = ""] = decimal.split(".");
|
|
292
|
+
const [whole = "0", frac = ""] = decimal.split(".");
|
|
307
293
|
const fracPadded = frac.padEnd(Number(USDC_DECIMALS), "0");
|
|
308
294
|
return (BigInt(whole) * USDC_SCALE + BigInt(fracPadded || "0")).toString();
|
|
309
295
|
}
|
|
@@ -318,74 +304,6 @@ function fromAtomicUSDC(atomic) {
|
|
|
318
304
|
return fracStr.length > 0 ? `${whole}.${fracStr.padEnd(2, "0")}` : `${whole}.00`;
|
|
319
305
|
}
|
|
320
306
|
|
|
321
|
-
// src/typed-data.ts
|
|
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
|
-
};
|
|
383
|
-
}
|
|
384
|
-
function sessionIdToPaymentId(sessionId) {
|
|
385
|
-
const hex = Array.from(new TextEncoder().encode(sessionId)).map((b) => b.toString(16).padStart(2, "0")).join("").padEnd(64, "0");
|
|
386
|
-
return `0x${hex}`;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
307
|
// src/index.ts
|
|
390
308
|
var BitPal = class {
|
|
391
309
|
checkout;
|
|
@@ -399,12 +317,11 @@ export {
|
|
|
399
317
|
BitPalCheckoutClient,
|
|
400
318
|
BitPalError,
|
|
401
319
|
CHECKOUT_WEBHOOK_EVENTS,
|
|
320
|
+
PricesResource,
|
|
321
|
+
ProductsResource,
|
|
402
322
|
WEBHOOK_HEADERS,
|
|
403
|
-
buildConfirmReceiptTypedData,
|
|
404
|
-
buildPaymentAuthTypedData,
|
|
405
323
|
fromAtomicUSDC,
|
|
406
324
|
parseWebhookEvent,
|
|
407
|
-
sessionIdToPaymentId,
|
|
408
325
|
toAtomicUSDC,
|
|
409
326
|
verifyWebhookSignature
|
|
410
327
|
};
|
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.5.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
|
+
}
|
package/MIGRATION_v0.2.md
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
# @bitpal/checkout — Migration Guide v0.1.x → v0.2.0
|
|
2
|
-
|
|
3
|
-
> Audit doc 11 §3 C-01 option 2 (PaymentAuthorization 이중방어) 적용으로 인한 SDK 메이저 bump.
|
|
4
|
-
|
|
5
|
-
## Why this change
|
|
6
|
-
|
|
7
|
-
v0.1.x 까지의 `payWithAuthorization` 흐름은 buyer 의 EIP-3009 USDC sig 만 받았다. 이 sig 는
|
|
8
|
-
`(from, to, value, validAfter, validBefore, nonce)` 만 바인딩하므로, **paymentId / merchant /
|
|
9
|
-
amount / fee / reserve / duration** 은 호출자(BitPal gateway)가 자유롭게 지정 가능했다.
|
|
10
|
-
|
|
11
|
-
→ gateway 키 탈취 시 공격자가 (a) buyer 의 EIP-3009 sig 입수 후 (b) 자기 주소를 merchant 로 설정,
|
|
12
|
-
(c) `depositWithAuthorization` 호출 → buyer 자금이 attacker merchant 로 흘러간다.
|
|
13
|
-
|
|
14
|
-
v0.2.0 는 buyer 가 **결제 의도 전체** 에 별도 EIP-712 서명 (`PaymentAuthorization`) 을 한다. gateway
|
|
15
|
-
키 탈취 시에도 buyer 의도 밖 결제 생성 불가 (이중방어).
|
|
16
|
-
|
|
17
|
-
## Breaking changes
|
|
18
|
-
|
|
19
|
-
### `BitPal.checkout.payWithAuthorization(sessionId, params)`
|
|
20
|
-
|
|
21
|
-
`params.payment_auth` 필드가 **필수** 가 됐다.
|
|
22
|
-
|
|
23
|
-
**Before (v0.1.x):**
|
|
24
|
-
```ts
|
|
25
|
-
await bp.checkout.payWithAuthorization(sessionId, {
|
|
26
|
-
session_token: 'xyz...',
|
|
27
|
-
chain: 'base',
|
|
28
|
-
authorization: {
|
|
29
|
-
from: buyer.address,
|
|
30
|
-
validAfter: '0',
|
|
31
|
-
validBefore: String(Math.floor(Date.now()/1000) + 3600),
|
|
32
|
-
authNonce: '0x...',
|
|
33
|
-
v: 27, r: '0x...', s: '0x...',
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
**After (v0.2.0):**
|
|
39
|
-
```ts
|
|
40
|
-
import {
|
|
41
|
-
buildPaymentAuthTypedData,
|
|
42
|
-
sessionIdToPaymentId,
|
|
43
|
-
} from '@bitpal/checkout';
|
|
44
|
-
|
|
45
|
-
// 1. PaymentAuthorization nonce 조회 (CheckoutEscrow.paymentAuthNonce)
|
|
46
|
-
const nonce = await escrow.read.paymentAuthNonce([buyer.address]);
|
|
47
|
-
|
|
48
|
-
// 2. PaymentAuthorization typed-data sig
|
|
49
|
-
const paymentAuthDeadline = Math.floor(Date.now()/1000) + 3600;
|
|
50
|
-
const paymentId = sessionIdToPaymentId(sessionId);
|
|
51
|
-
const paymentAuthTd = buildPaymentAuthTypedData({
|
|
52
|
-
escrowAddress: escrow.address,
|
|
53
|
-
chainId: 8453,
|
|
54
|
-
paymentId,
|
|
55
|
-
payer: buyer.address,
|
|
56
|
-
merchant: session.seller_wallet,
|
|
57
|
-
token: session.token_address,
|
|
58
|
-
amount: session.amount_total,
|
|
59
|
-
feeAmount: '0',
|
|
60
|
-
reserveAmount: '0',
|
|
61
|
-
duration: '0',
|
|
62
|
-
deadline: String(paymentAuthDeadline),
|
|
63
|
-
nonce: String(nonce),
|
|
64
|
-
});
|
|
65
|
-
const paymentAuthSig = await wallet.signTypedData(paymentAuthTd);
|
|
66
|
-
const paymentAuthParsed = parseSignature(paymentAuthSig); // viem helper
|
|
67
|
-
|
|
68
|
-
// 3. USDC EIP-3009 sig (변경 없음 — 기존 흐름 그대로)
|
|
69
|
-
const usdcSig = await wallet.signTypedData({ /* TransferWithAuthorization ... */ });
|
|
70
|
-
const usdcParsed = parseSignature(usdcSig);
|
|
71
|
-
|
|
72
|
-
// 4. payWithAuthorization 호출 — payment_auth 필드 추가
|
|
73
|
-
await bp.checkout.payWithAuthorization(sessionId, {
|
|
74
|
-
session_token: 'xyz...',
|
|
75
|
-
chain: 'base',
|
|
76
|
-
authorization: {
|
|
77
|
-
from: buyer.address,
|
|
78
|
-
validAfter: '0',
|
|
79
|
-
validBefore: String(Math.floor(Date.now()/1000) + 3600),
|
|
80
|
-
authNonce: '0x...',
|
|
81
|
-
v: Number(usdcParsed.v), r: usdcParsed.r, s: usdcParsed.s,
|
|
82
|
-
},
|
|
83
|
-
// ← 신규 필수
|
|
84
|
-
payment_auth: {
|
|
85
|
-
deadline: String(paymentAuthDeadline),
|
|
86
|
-
duration: '0',
|
|
87
|
-
v: Number(paymentAuthParsed.v),
|
|
88
|
-
r: paymentAuthParsed.r,
|
|
89
|
-
s: paymentAuthParsed.s,
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### 백엔드 응답에서 `payment_auth_nonce` 노출
|
|
95
|
-
|
|
96
|
-
서버는 `GET /v1/checkout/sessions/:id/public` 응답에 `payment_auth_nonce` 필드를 포함한다
|
|
97
|
-
(Q5 적용 후). buyer UI 가 컨트랙트를 직접 호출하지 않고도 nonce 를 얻을 수 있다. 단,
|
|
98
|
-
`merchant_wallet` (seller_wallet) 도 sig 에 들어가므로 public response 에서 확인할 수 있어야 한다.
|
|
99
|
-
|
|
100
|
-
## Non-breaking changes
|
|
101
|
-
|
|
102
|
-
다음 SDK 흐름은 변경 없음:
|
|
103
|
-
|
|
104
|
-
- `createSession()`, `getSession()`, `listSessions()`, `previewFee()`
|
|
105
|
-
- `refundSession()`, `recordRefund()`, `listRefunds()`
|
|
106
|
-
- `submitExternalPayment()` (Mode A — buyer 가 본인 지갑으로 직접 deposit)
|
|
107
|
-
- Webhook helpers (`verifyWebhookSignature`, `parseWebhookEvent`)
|
|
108
|
-
- Amount helpers (`toAtomicUSDC`, `fromAtomicUSDC`)
|
|
109
|
-
|
|
110
|
-
## Why `payment_auth` is required (no opt-out)
|
|
111
|
-
|
|
112
|
-
Q4 PR 적용 후 `CheckoutEscrow.depositWithAuthorization` 가 PaymentAuthorization sig 를 강제한다
|
|
113
|
-
(컨트랙트 require). SDK 가 sig 없이 호출 시 백엔드가 `PAYMENT_AUTH_REQUIRED` (400) 로 거부.
|
|
114
|
-
|
|
115
|
-
## Compatibility matrix
|
|
116
|
-
|
|
117
|
-
| SDK version | Backend (CheckoutEscrow contract) | Result |
|
|
118
|
-
|---|---|---|
|
|
119
|
-
| v0.1.x | post-Q5 (PaymentAuthorization 필수) | ❌ `PAYMENT_AUTH_REQUIRED` 400 |
|
|
120
|
-
| v0.2.0 | post-Q5 | ✅ OK |
|
|
121
|
-
| v0.2.0 | pre-Q5 (legacy 미배포 contract) | ❌ contract revert (signature 추가 인자 없음) |
|
|
122
|
-
|
|
123
|
-
→ SDK v0.2.0 와 컨트랙트 Q4+Q5 적용은 **동시 배포** 가 원칙. testnet 부터 함께 갈아끼움.
|
|
124
|
-
|
|
125
|
-
## New helpers
|
|
126
|
-
|
|
127
|
-
- `buildPaymentAuthTypedData(args)` — `signTypedData` 호환 객체 생성
|
|
128
|
-
- `buildConfirmReceiptTypedData(args)` — Q4 H-01 `confirmReceiptWithSignature` 용 (off-chain receipt confirmation 직접 구현 시 사용. 기본 흐름에서는 BitPal hosted page 가 처리)
|
|
129
|
-
- `sessionIdToPaymentId(sessionId)` — 32-byte hex 변환 (= 백엔드 `toBytes32()`)
|
|
130
|
-
|
|
131
|
-
## See also
|
|
132
|
-
|
|
133
|
-
- `docs/_dan/b2b-spec/2026-05-14-11-smart-contract-security-audit.md` §3 C-01
|
|
134
|
-
- `docs/_dan/b2b-spec/2026-05-18-14-post-audit-remaining-hardening-plan.md` §4 (PR-C 실행 가이드)
|
|
135
|
-
- README.md §"Gasless lane (Hosted / Gasless)"
|