@bitpal/checkout 0.9.0 → 0.11.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/README.md +38 -3
- package/dist/index.cjs +49 -72
- package/dist/index.d.cts +25 -15
- package/dist/index.d.ts +25 -15
- package/dist/index.js +48 -71
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,6 +180,7 @@ app.post(
|
|
|
180
180
|
| `checkout.session.expired` | Session expired unpaid |
|
|
181
181
|
| `checkout.session.unresolved` | Underpaid / overpaid / wrong token — needs review |
|
|
182
182
|
| `checkout.refund.recorded` | An on-chain auto-refund (over / under / late payment) was recorded |
|
|
183
|
+
| `x402.payment.resolved` | An x402 payment that `/settle` left unresolved was later settled on-chain (see below) |
|
|
183
184
|
|
|
184
185
|
### ⚠️ Replay defense + at-least-once delivery
|
|
185
186
|
|
|
@@ -227,6 +228,12 @@ handler with `X-PAYMENT-RESPONSE` set. The middleware also does what the API can
|
|
|
227
228
|
timeout — answers `503` and asks for a retry with the *same* header. Handing back a `402` there
|
|
228
229
|
would make the caller re-sign and pay twice.
|
|
229
230
|
|
|
231
|
+
When `/settle` answers `503`, nobody knows yet whether the money moved — so BitPal watches the chain
|
|
232
|
+
and tells you how it ended with a **`x402.payment.resolved`** webhook (`status`: `confirmed` |
|
|
233
|
+
`reverted` | `expired`, plus `payment_id` and `tx_hash`). It fires only on that path: the normal case
|
|
234
|
+
already returned the result to you, and you should not have to handle the same outcome twice. Verify
|
|
235
|
+
it exactly like a checkout webhook.
|
|
236
|
+
|
|
230
237
|
Not on Express? `X402Gate` is the framework-neutral core — give it the header and the resource URL,
|
|
231
238
|
it tells you whether to bill or serve. On a `settled` result, call `commit()` once you have delivered
|
|
232
239
|
the response, or `release()` if you could not.
|
|
@@ -242,9 +249,13 @@ the response, or `release()` if you could not.
|
|
|
242
249
|
|
|
243
250
|
### Payer side
|
|
244
251
|
|
|
245
|
-
The
|
|
246
|
-
`
|
|
247
|
-
|
|
252
|
+
The payer signs **one** standard EIP-3009 authorization, so the `X-PAYMENT` payload is plain x402:
|
|
253
|
+
`{ signature, authorization }`. The fee split happens on-chain — `payTo` in the `402` is not the
|
|
254
|
+
merchant's wallet but a CREATE2 address that commits to the whole split, so nothing extra has to be
|
|
255
|
+
signed for it.
|
|
256
|
+
|
|
257
|
+
`createX402Payment` reads the `402` and builds the header. You supply the signer; the SDK has no
|
|
258
|
+
crypto dependency.
|
|
248
259
|
|
|
249
260
|
```ts
|
|
250
261
|
import { createX402Payment } from '@bitpal/checkout';
|
|
@@ -270,6 +281,10 @@ if (res.status === 402) {
|
|
|
270
281
|
Amounts are human-readable (`'1.00'`), the same model as `line_items[].amount` — the server converts.
|
|
271
282
|
Pass `amountAtomic` instead if you already have raw units.
|
|
272
283
|
|
|
284
|
+
The `402` also carries a `feeBreakdown` (net / fee / bps / recipient). It is informational — a payer
|
|
285
|
+
can ignore it entirely and still pay correctly, because the split is already committed by the `payTo`
|
|
286
|
+
address. It is there so you can see where the money goes without reading the chain.
|
|
287
|
+
|
|
273
288
|
EOA signers only — smart-contract wallets cannot sign EIP-3009 authorizations. Amounts are exact:
|
|
274
289
|
over- and underpayment are both rejected, and this lane has no refund path. Authorizations are valid
|
|
275
290
|
for 90–3600 seconds (the server's floor is 60s; the extra 30s absorbs clock skew and round trips).
|
|
@@ -301,6 +316,26 @@ store wiring session creation + webhook verification + idempotency.
|
|
|
301
316
|
pnpm build # tsup → dist/ (ESM + .d.ts)
|
|
302
317
|
```
|
|
303
318
|
|
|
319
|
+
## Migrating to 0.11.0 (breaking — x402 payer only)
|
|
320
|
+
|
|
321
|
+
The `X-PAYMENT` payload changed from BitPal's two-authorization shape to the **x402 standard**:
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
0.10.0 payload: { merchant: {...}, fee: {...} } two signatures
|
|
325
|
+
0.11.0 payload: { signature, authorization } one signature — plain x402
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
- If you call `createX402Payment`, nothing changes in your code. It signs once now instead of twice,
|
|
329
|
+
and the wallet prompts once.
|
|
330
|
+
- If you built the payload by hand, rebuild it in the standard shape.
|
|
331
|
+
- `splitSignature` is gone. `normalizeSignature` replaces it — same input handling (65-byte or
|
|
332
|
+
EIP-2098 compact, `v` of 0/1/27/28) but it returns a 65-byte packed hex string, which is what the
|
|
333
|
+
payload carries.
|
|
334
|
+
- The `402` is unchanged apart from `payTo`, which now points at the settlement address rather than
|
|
335
|
+
the merchant wallet. Both `X402Gate` and `x402()` handle this for you.
|
|
336
|
+
|
|
337
|
+
Old headers are rejected — a payer on 0.10.0 gets a fresh `402` and re-signs, so no payment is lost.
|
|
338
|
+
|
|
304
339
|
## Migrating from 0.3.x → 0.5.0 (breaking)
|
|
305
340
|
|
|
306
341
|
- **Webhook verification now requires the timestamp** and enforces replay defense. Pass the
|
package/dist/index.cjs
CHANGED
|
@@ -48,8 +48,8 @@ __export(index_exports, {
|
|
|
48
48
|
encodePaymentHeader: () => encodePaymentHeader,
|
|
49
49
|
fromAtomic: () => fromAtomic,
|
|
50
50
|
fromAtomicUSDC: () => fromAtomicUSDC,
|
|
51
|
+
normalizeSignature: () => normalizeSignature,
|
|
51
52
|
parseWebhookEvent: () => parseWebhookEvent,
|
|
52
|
-
splitSignature: () => splitSignature,
|
|
53
53
|
toAtomic: () => toAtomic,
|
|
54
54
|
toAtomicUSDC: () => toAtomicUSDC,
|
|
55
55
|
verifyWebhookSignature: () => verifyWebhookSignature,
|
|
@@ -351,7 +351,12 @@ var CHECKOUT_WEBHOOK_EVENTS = [
|
|
|
351
351
|
"checkout.session.settled",
|
|
352
352
|
"checkout.session.expired",
|
|
353
353
|
"checkout.session.unresolved",
|
|
354
|
-
"checkout.refund.recorded"
|
|
354
|
+
"checkout.refund.recorded",
|
|
355
|
+
// x402 결제의 **사후** 확정. 정상 경로(/settle 이 그 자리에서 결과를 반환)는 발행되지 않는다 —
|
|
356
|
+
// 이미 아는 결과를 중복으로 받지 않게 하기 위함이다. /settle 이 503 을 반환해 결과를 모른 채
|
|
357
|
+
// 남았을 때, reconciler 가 체인을 보고 종결시키면 이 이벤트가 온다.
|
|
358
|
+
// payload: { payment_id, status: 'confirmed'|'reverted'|'expired', tx_hash, resource, amount, … }
|
|
359
|
+
"x402.payment.resolved"
|
|
355
360
|
];
|
|
356
361
|
var WEBHOOK_HEADERS = {
|
|
357
362
|
signature: "X-Webhook-Signature-256",
|
|
@@ -539,11 +544,11 @@ var X402Gate = class {
|
|
|
539
544
|
} catch {
|
|
540
545
|
return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
|
|
541
546
|
}
|
|
542
|
-
const
|
|
543
|
-
if (!
|
|
547
|
+
const auth = payload?.payload?.authorization;
|
|
548
|
+
if (!auth?.from || !auth?.nonce) {
|
|
544
549
|
return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
|
|
545
550
|
}
|
|
546
|
-
const key = `${payload.network}:${
|
|
551
|
+
const key = `${payload.network}:${auth.from.toLowerCase()}:${auth.nonce.toLowerCase()}`;
|
|
547
552
|
const claim = await this.store.claim(key, this.maxTimeoutSeconds);
|
|
548
553
|
if (claim === "used") {
|
|
549
554
|
return {
|
|
@@ -581,7 +586,12 @@ var X402Gate = class {
|
|
|
581
586
|
},
|
|
582
587
|
paymentRequirements: {
|
|
583
588
|
asset: accept.asset,
|
|
589
|
+
// payer 가 서명한 목적지 = 분배 조건을 커밋한 정산 주소.
|
|
584
590
|
payTo: accept.payTo,
|
|
591
|
+
// 머천트 정산 지갑. facilitator 가 이 값으로 정산 주소를 재계산해 payTo 와 대조한다 —
|
|
592
|
+
// CREATE2 는 단방향이라 payTo 에서 되돌릴 수 없으므로 따로 보내야 한다.
|
|
593
|
+
// 우리 설정값을 그대로 쓴다(402 응답에서 읽지 않는다 — 그러면 재계산의 의미가 없다).
|
|
594
|
+
merchant: this.opts.payTo,
|
|
585
595
|
maxAmountRequired: accept.maxAmountRequired,
|
|
586
596
|
resource: accept.resource
|
|
587
597
|
}
|
|
@@ -633,7 +643,7 @@ var X402Gate = class {
|
|
|
633
643
|
txHash: settled.tx_hash,
|
|
634
644
|
feeTxHash: settled.fee_tx_hash ?? null,
|
|
635
645
|
network: settled.network,
|
|
636
|
-
payer:
|
|
646
|
+
payer: auth.from,
|
|
637
647
|
amount: accept.maxAmountRequired,
|
|
638
648
|
idempotent: settled.idempotent ?? false
|
|
639
649
|
};
|
|
@@ -645,7 +655,7 @@ var X402Gate = class {
|
|
|
645
655
|
success: true,
|
|
646
656
|
transaction: settled.tx_hash,
|
|
647
657
|
network: settled.network,
|
|
648
|
-
payer:
|
|
658
|
+
payer: auth.from,
|
|
649
659
|
paymentId: settled.payment_id
|
|
650
660
|
}),
|
|
651
661
|
// 소진 확정은 **응답을 실제로 내준 뒤**다. 핸들러가 throw 하거나 프로세스가 죽으면 결제만 하고
|
|
@@ -766,18 +776,22 @@ function assertPaymentPayload(value) {
|
|
|
766
776
|
throw new Error("[BitPal] x402: invalid network.");
|
|
767
777
|
}
|
|
768
778
|
const inner = asObject(root.payload, "payload");
|
|
769
|
-
|
|
770
|
-
|
|
779
|
+
if (typeof inner.signature !== "string" || !PACKED_SIG_RE.test(inner.signature)) {
|
|
780
|
+
throw new Error("[BitPal] x402: invalid payload.signature (expected 65-byte packed hex).");
|
|
781
|
+
}
|
|
771
782
|
return {
|
|
772
783
|
x402Version: 1,
|
|
773
784
|
scheme: "exact",
|
|
774
785
|
network: root.network,
|
|
775
|
-
payload:
|
|
786
|
+
payload: {
|
|
787
|
+
signature: inner.signature,
|
|
788
|
+
authorization: assertAuthorization(inner.authorization, "authorization")
|
|
789
|
+
}
|
|
776
790
|
};
|
|
777
791
|
}
|
|
792
|
+
var PACKED_SIG_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
778
793
|
function assertAuthorization(value, label) {
|
|
779
|
-
const a = asObject(value,
|
|
780
|
-
const sig = asObject(a.signature, `${label} signature`);
|
|
794
|
+
const a = asObject(value, label);
|
|
781
795
|
const check = (ok, field) => {
|
|
782
796
|
if (!ok) throw new Error(`[BitPal] x402: invalid ${label}.${field}.`);
|
|
783
797
|
};
|
|
@@ -787,17 +801,13 @@ function assertAuthorization(value, label) {
|
|
|
787
801
|
check(typeof a.validAfter === "string" && DECIMAL_RE.test(a.validAfter), "validAfter");
|
|
788
802
|
check(typeof a.validBefore === "string" && DECIMAL_RE.test(a.validBefore), "validBefore");
|
|
789
803
|
check(typeof a.nonce === "string" && HEX32_RE.test(a.nonce), "nonce");
|
|
790
|
-
check(typeof sig.v === "number" && Number.isInteger(sig.v), "signature.v");
|
|
791
|
-
check(typeof sig.r === "string" && HEX32_RE.test(sig.r), "signature.r");
|
|
792
|
-
check(typeof sig.s === "string" && HEX32_RE.test(sig.s), "signature.s");
|
|
793
804
|
return {
|
|
794
805
|
from: a.from,
|
|
795
806
|
to: a.to,
|
|
796
807
|
value: a.value,
|
|
797
808
|
validAfter: a.validAfter,
|
|
798
809
|
validBefore: a.validBefore,
|
|
799
|
-
nonce: a.nonce
|
|
800
|
-
signature: { v: sig.v, r: sig.r, s: sig.s }
|
|
810
|
+
nonce: a.nonce
|
|
801
811
|
};
|
|
802
812
|
}
|
|
803
813
|
function asObject(value, label) {
|
|
@@ -871,49 +881,33 @@ async function createX402Payment(options) {
|
|
|
871
881
|
}
|
|
872
882
|
const validAfter = "0";
|
|
873
883
|
const validBefore = String(now + validFor);
|
|
874
|
-
const
|
|
875
|
-
const
|
|
876
|
-
const feeAmount = fee ? BigInt(fee.feeAmount) : 0n;
|
|
877
|
-
const merchantValue = fee ? fee.netAmount : accept.maxAmountRequired;
|
|
878
|
-
const merchant = await signAuthorization({
|
|
879
|
-
domain,
|
|
880
|
-
signTypedData: options.signTypedData,
|
|
884
|
+
const nonce = (options.randomNonce ?? randomNonce)();
|
|
885
|
+
const authorization = {
|
|
881
886
|
from: options.from,
|
|
882
887
|
to: accept.payTo,
|
|
883
|
-
value:
|
|
888
|
+
value: accept.maxAmountRequired,
|
|
884
889
|
validAfter,
|
|
885
890
|
validBefore,
|
|
886
|
-
nonce
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
if (!fee?.feeRecipient) {
|
|
891
|
-
throw new Error("[BitPal] x402: feeAmount > 0 but feeRecipient is missing in the 402 response.");
|
|
892
|
-
}
|
|
893
|
-
feeLeg = await signAuthorization({
|
|
891
|
+
nonce
|
|
892
|
+
};
|
|
893
|
+
const signature = normalizeSignature(
|
|
894
|
+
await options.signTypedData({
|
|
894
895
|
domain,
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
validBefore,
|
|
901
|
-
nonce: nextNonce()
|
|
902
|
-
});
|
|
903
|
-
if (feeLeg.nonce.toLowerCase() === merchant.nonce.toLowerCase()) {
|
|
904
|
-
throw new Error("[BitPal] x402: the two authorizations must use different nonces.");
|
|
905
|
-
}
|
|
906
|
-
}
|
|
896
|
+
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
897
|
+
primaryType: "TransferWithAuthorization",
|
|
898
|
+
message: authorization
|
|
899
|
+
})
|
|
900
|
+
);
|
|
907
901
|
const payload = {
|
|
908
902
|
x402Version: 1,
|
|
909
903
|
scheme: "exact",
|
|
910
904
|
network: accept.network,
|
|
911
|
-
payload:
|
|
905
|
+
payload: { signature, authorization }
|
|
912
906
|
};
|
|
913
907
|
return {
|
|
914
908
|
header: encodePaymentHeader(payload),
|
|
915
909
|
payload,
|
|
916
|
-
totalAmount:
|
|
910
|
+
totalAmount: accept.maxAmountRequired
|
|
917
911
|
};
|
|
918
912
|
}
|
|
919
913
|
function resolveAccept(input) {
|
|
@@ -924,41 +918,24 @@ function resolveAccept(input) {
|
|
|
924
918
|
}
|
|
925
919
|
return input;
|
|
926
920
|
}
|
|
927
|
-
|
|
928
|
-
const message = {
|
|
929
|
-
from: args.from,
|
|
930
|
-
to: args.to,
|
|
931
|
-
value: args.value,
|
|
932
|
-
validAfter: args.validAfter,
|
|
933
|
-
validBefore: args.validBefore,
|
|
934
|
-
nonce: args.nonce
|
|
935
|
-
};
|
|
936
|
-
const signature = await args.signTypedData({
|
|
937
|
-
domain: args.domain,
|
|
938
|
-
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
939
|
-
primaryType: "TransferWithAuthorization",
|
|
940
|
-
message
|
|
941
|
-
});
|
|
942
|
-
return { ...message, signature: splitSignature(signature) };
|
|
943
|
-
}
|
|
944
|
-
function splitSignature(signature) {
|
|
921
|
+
function normalizeSignature(signature) {
|
|
945
922
|
const hex = signature.startsWith("0x") ? signature.slice(2) : signature;
|
|
946
923
|
if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length !== 130 && hex.length !== 128) {
|
|
947
924
|
throw new Error(
|
|
948
925
|
`[BitPal] x402: expected a 65-byte or 64-byte (EIP-2098) hex signature, got ${signature.length} chars.`
|
|
949
926
|
);
|
|
950
927
|
}
|
|
951
|
-
const r =
|
|
928
|
+
const r = hex.slice(0, 64);
|
|
952
929
|
if (hex.length === 128) {
|
|
953
930
|
const yParityAndS = BigInt(`0x${hex.slice(64, 128)}`);
|
|
954
931
|
const yParity = Number(yParityAndS >> 255n & 1n);
|
|
955
|
-
const s2 = yParityAndS & (1n << 255n) - 1n;
|
|
956
|
-
return {
|
|
932
|
+
const s2 = (yParityAndS & (1n << 255n) - 1n).toString(16).padStart(64, "0");
|
|
933
|
+
return `0x${r}${s2}${(27 + yParity).toString(16).padStart(2, "0")}`;
|
|
957
934
|
}
|
|
958
|
-
const s =
|
|
935
|
+
const s = hex.slice(64, 128);
|
|
959
936
|
const raw = parseInt(hex.slice(128, 130), 16);
|
|
960
|
-
if (raw === 27 || raw === 28) return {
|
|
961
|
-
if (raw === 0 || raw === 1) return {
|
|
937
|
+
if (raw === 27 || raw === 28) return `0x${r}${s}${raw.toString(16)}`;
|
|
938
|
+
if (raw === 0 || raw === 1) return `0x${r}${s}${(raw + 27).toString(16)}`;
|
|
962
939
|
throw new Error(`[BitPal] x402: unexpected signature v=${raw} (expected 0, 1, 27 or 28).`);
|
|
963
940
|
}
|
|
964
941
|
function randomNonce() {
|
|
@@ -1000,8 +977,8 @@ var BitPal = class {
|
|
|
1000
977
|
encodePaymentHeader,
|
|
1001
978
|
fromAtomic,
|
|
1002
979
|
fromAtomicUSDC,
|
|
980
|
+
normalizeSignature,
|
|
1003
981
|
parseWebhookEvent,
|
|
1004
|
-
splitSignature,
|
|
1005
982
|
toAtomic,
|
|
1006
983
|
toAtomicUSDC,
|
|
1007
984
|
verifyWebhookSignature,
|
package/dist/index.d.cts
CHANGED
|
@@ -584,7 +584,7 @@ declare class BitPalError extends Error {
|
|
|
584
584
|
/**
|
|
585
585
|
* BitPal 이 발송하는 webhook 이벤트 타입 전체 목록.
|
|
586
586
|
*/
|
|
587
|
-
declare const CHECKOUT_WEBHOOK_EVENTS: readonly ["checkout.session.created", "checkout.session.deposit_detected", "checkout.session.paid", "checkout.session.settled", "checkout.session.expired", "checkout.session.unresolved", "checkout.refund.recorded"];
|
|
587
|
+
declare const CHECKOUT_WEBHOOK_EVENTS: readonly ["checkout.session.created", "checkout.session.deposit_detected", "checkout.session.paid", "checkout.session.settled", "checkout.session.expired", "checkout.session.unresolved", "checkout.refund.recorded", "x402.payment.resolved"];
|
|
588
588
|
type CheckoutWebhookEvent = (typeof CHECKOUT_WEBHOOK_EVENTS)[number];
|
|
589
589
|
/**
|
|
590
590
|
* Canonical envelope (HTTP body).
|
|
@@ -750,6 +750,7 @@ interface X402Requirements {
|
|
|
750
750
|
x402Version: 1;
|
|
751
751
|
accepts: X402Accept[];
|
|
752
752
|
}
|
|
753
|
+
/** EIP-3009 `TransferWithAuthorization` 의 서명 대상 필드. x402 표준 `payload.authorization`. */
|
|
753
754
|
interface Eip3009Authorization {
|
|
754
755
|
from: string;
|
|
755
756
|
to: string;
|
|
@@ -757,21 +758,23 @@ interface Eip3009Authorization {
|
|
|
757
758
|
validAfter: string;
|
|
758
759
|
validBefore: string;
|
|
759
760
|
nonce: string;
|
|
760
|
-
signature: {
|
|
761
|
-
v: number;
|
|
762
|
-
r: string;
|
|
763
|
-
s: string;
|
|
764
|
-
};
|
|
765
761
|
}
|
|
762
|
+
/**
|
|
763
|
+
* x402 표준 payload 그대로다 — 단일 authorization + 65바이트 packed 서명.
|
|
764
|
+
*
|
|
765
|
+
* 0.10.0 까지는 `{ merchant, fee }` 2건이었다. 포워더 없이 수수료를 나누려면 payer 가 두 번
|
|
766
|
+
* 서명해야 했고, 그 탓에 순정 x402 클라이언트가 BitPal 머천트에게 결제할 수 없었다.
|
|
767
|
+
* 이제 분배는 `payTo`(포워더) 주소가 커밋하므로 서명이 1건이면 된다.
|
|
768
|
+
*/
|
|
766
769
|
interface X402PaymentPayload {
|
|
767
770
|
x402Version: 1;
|
|
768
771
|
scheme: 'exact';
|
|
769
772
|
/** x402 wire 이름 — 'base' / 'base-sepolia'. */
|
|
770
773
|
network: string;
|
|
771
774
|
payload: {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
+
/** 65바이트 packed hex(`0x` + r + s + v). */
|
|
776
|
+
signature: string;
|
|
777
|
+
authorization: Eip3009Authorization;
|
|
775
778
|
};
|
|
776
779
|
}
|
|
777
780
|
/** `ok` = 이번 호출이 최초 점유 / `inflight` = 다른 요청이 처리 중 / `used` = 이미 리소스를 내줌. */
|
|
@@ -1080,11 +1083,18 @@ declare function createX402Payment(options: CreateX402PaymentOptions): Promise<X
|
|
|
1080
1083
|
* 65바이트(r‖s‖v)와 64바이트 EIP-2098 compact(r‖yParityAndS) 둘 다 받는다 — viem 의
|
|
1081
1084
|
* `signTypedData` 는 65바이트를 주지만 일부 지갑·라이브러리는 compact 를 돌려준다.
|
|
1082
1085
|
*/
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1086
|
+
/**
|
|
1087
|
+
* 지갑이 돌려준 서명을 **65바이트 packed hex** 로 수렴시킨다. x402 표준이 그 형태다.
|
|
1088
|
+
*
|
|
1089
|
+
* 두 가지를 정규화한다:
|
|
1090
|
+
* - EIP-2098 compact(64바이트) — 일부 지갑이 이걸 낸다. s 의 최상위 비트가 yParity 라 떼어내고
|
|
1091
|
+
* v 를 복원해 65바이트로 편다.
|
|
1092
|
+
* - v 가 0/1 인 경우 — 온체인 ecrecover 규약인 27/28 로 맞춘다.
|
|
1093
|
+
*
|
|
1094
|
+
* 서버가 130 hex 만 받으므로 여기서 안 맞추면 그쪽에서 거부된다. 그 외 값은 recover 가 어긋나
|
|
1095
|
+
* 어차피 실패하므로 여기서 끊는다.
|
|
1096
|
+
*/
|
|
1097
|
+
declare function normalizeSignature(signature: string): string;
|
|
1088
1098
|
|
|
1089
1099
|
/**
|
|
1090
1100
|
* @bitpal/checkout — BitPal Checkout SDK
|
|
@@ -1114,4 +1124,4 @@ declare class BitPal {
|
|
|
1114
1124
|
constructor(config: BitPalConfig | string);
|
|
1115
1125
|
}
|
|
1116
1126
|
|
|
1117
|
-
export { type ApiResponse, BitPal, BitPalCheckoutClient, type BitPalConfig, BitPalError, CHECKOUT_WEBHOOK_EVENTS, type CheckoutLineItemInput, type CheckoutPaymentLink, type CheckoutPrice, type CheckoutProduct, type CheckoutSession, type CheckoutWebhookEvent, type CreatePaymentLinkParams, type CreatePriceParams, type CreateProductParams, type CreateSessionParams, type CreateX402PaymentOptions, type DepositAddress, type Eip3009Authorization, type Eip712Domain, type FeePreview, type IdempotencyOptions, type IssueDepositAddressParams, type LineItem, type ListPaymentLinksParams, type ListPricesParams, type ListProductsParams, type ListResponse, PaymentLinksResource, type PaymentOption, type PriceLineItem, PricesResource, ProductsResource, type PublicSession, type SessionStatus, TRANSFER_WITH_AUTHORIZATION_TYPES, type UpdatePaymentLinkParams, type UpdatePriceParams, type UpdateProductParams, WEBHOOK_HEADERS, type WebhookPayload, type X402Accept, type X402ClaimState, type X402FeeBreakdown, X402Gate, type X402GateOptions, type X402GateResult, type X402MiddlewareOptions, type X402PaymentPayload, type X402PaymentResult, type X402ReplayStore, type X402Request, type X402Requirements, type X402Response, type X402SettledPayment, type X402Signer, type X402TypedDataRequest, X402_PAYMENT_HEADER, X402_PAYMENT_RESPONSE_HEADER, createMemoryReplayStore, createX402Payment, decodePaymentHeader, encodePaymentHeader, fromAtomic, fromAtomicUSDC,
|
|
1127
|
+
export { type ApiResponse, BitPal, BitPalCheckoutClient, type BitPalConfig, BitPalError, CHECKOUT_WEBHOOK_EVENTS, type CheckoutLineItemInput, type CheckoutPaymentLink, type CheckoutPrice, type CheckoutProduct, type CheckoutSession, type CheckoutWebhookEvent, type CreatePaymentLinkParams, type CreatePriceParams, type CreateProductParams, type CreateSessionParams, type CreateX402PaymentOptions, type DepositAddress, type Eip3009Authorization, type Eip712Domain, type FeePreview, type IdempotencyOptions, type IssueDepositAddressParams, type LineItem, type ListPaymentLinksParams, type ListPricesParams, type ListProductsParams, type ListResponse, PaymentLinksResource, type PaymentOption, type PriceLineItem, PricesResource, ProductsResource, type PublicSession, type SessionStatus, TRANSFER_WITH_AUTHORIZATION_TYPES, type UpdatePaymentLinkParams, type UpdatePriceParams, type UpdateProductParams, WEBHOOK_HEADERS, type WebhookPayload, type X402Accept, type X402ClaimState, type X402FeeBreakdown, X402Gate, type X402GateOptions, type X402GateResult, type X402MiddlewareOptions, type X402PaymentPayload, type X402PaymentResult, type X402ReplayStore, type X402Request, type X402Requirements, type X402Response, type X402SettledPayment, type X402Signer, type X402TypedDataRequest, X402_PAYMENT_HEADER, X402_PAYMENT_RESPONSE_HEADER, createMemoryReplayStore, createX402Payment, decodePaymentHeader, encodePaymentHeader, fromAtomic, fromAtomicUSDC, normalizeSignature, parseWebhookEvent, toAtomic, toAtomicUSDC, verifyWebhookSignature, x402 };
|
package/dist/index.d.ts
CHANGED
|
@@ -584,7 +584,7 @@ declare class BitPalError extends Error {
|
|
|
584
584
|
/**
|
|
585
585
|
* BitPal 이 발송하는 webhook 이벤트 타입 전체 목록.
|
|
586
586
|
*/
|
|
587
|
-
declare const CHECKOUT_WEBHOOK_EVENTS: readonly ["checkout.session.created", "checkout.session.deposit_detected", "checkout.session.paid", "checkout.session.settled", "checkout.session.expired", "checkout.session.unresolved", "checkout.refund.recorded"];
|
|
587
|
+
declare const CHECKOUT_WEBHOOK_EVENTS: readonly ["checkout.session.created", "checkout.session.deposit_detected", "checkout.session.paid", "checkout.session.settled", "checkout.session.expired", "checkout.session.unresolved", "checkout.refund.recorded", "x402.payment.resolved"];
|
|
588
588
|
type CheckoutWebhookEvent = (typeof CHECKOUT_WEBHOOK_EVENTS)[number];
|
|
589
589
|
/**
|
|
590
590
|
* Canonical envelope (HTTP body).
|
|
@@ -750,6 +750,7 @@ interface X402Requirements {
|
|
|
750
750
|
x402Version: 1;
|
|
751
751
|
accepts: X402Accept[];
|
|
752
752
|
}
|
|
753
|
+
/** EIP-3009 `TransferWithAuthorization` 의 서명 대상 필드. x402 표준 `payload.authorization`. */
|
|
753
754
|
interface Eip3009Authorization {
|
|
754
755
|
from: string;
|
|
755
756
|
to: string;
|
|
@@ -757,21 +758,23 @@ interface Eip3009Authorization {
|
|
|
757
758
|
validAfter: string;
|
|
758
759
|
validBefore: string;
|
|
759
760
|
nonce: string;
|
|
760
|
-
signature: {
|
|
761
|
-
v: number;
|
|
762
|
-
r: string;
|
|
763
|
-
s: string;
|
|
764
|
-
};
|
|
765
761
|
}
|
|
762
|
+
/**
|
|
763
|
+
* x402 표준 payload 그대로다 — 단일 authorization + 65바이트 packed 서명.
|
|
764
|
+
*
|
|
765
|
+
* 0.10.0 까지는 `{ merchant, fee }` 2건이었다. 포워더 없이 수수료를 나누려면 payer 가 두 번
|
|
766
|
+
* 서명해야 했고, 그 탓에 순정 x402 클라이언트가 BitPal 머천트에게 결제할 수 없었다.
|
|
767
|
+
* 이제 분배는 `payTo`(포워더) 주소가 커밋하므로 서명이 1건이면 된다.
|
|
768
|
+
*/
|
|
766
769
|
interface X402PaymentPayload {
|
|
767
770
|
x402Version: 1;
|
|
768
771
|
scheme: 'exact';
|
|
769
772
|
/** x402 wire 이름 — 'base' / 'base-sepolia'. */
|
|
770
773
|
network: string;
|
|
771
774
|
payload: {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
+
/** 65바이트 packed hex(`0x` + r + s + v). */
|
|
776
|
+
signature: string;
|
|
777
|
+
authorization: Eip3009Authorization;
|
|
775
778
|
};
|
|
776
779
|
}
|
|
777
780
|
/** `ok` = 이번 호출이 최초 점유 / `inflight` = 다른 요청이 처리 중 / `used` = 이미 리소스를 내줌. */
|
|
@@ -1080,11 +1083,18 @@ declare function createX402Payment(options: CreateX402PaymentOptions): Promise<X
|
|
|
1080
1083
|
* 65바이트(r‖s‖v)와 64바이트 EIP-2098 compact(r‖yParityAndS) 둘 다 받는다 — viem 의
|
|
1081
1084
|
* `signTypedData` 는 65바이트를 주지만 일부 지갑·라이브러리는 compact 를 돌려준다.
|
|
1082
1085
|
*/
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1086
|
+
/**
|
|
1087
|
+
* 지갑이 돌려준 서명을 **65바이트 packed hex** 로 수렴시킨다. x402 표준이 그 형태다.
|
|
1088
|
+
*
|
|
1089
|
+
* 두 가지를 정규화한다:
|
|
1090
|
+
* - EIP-2098 compact(64바이트) — 일부 지갑이 이걸 낸다. s 의 최상위 비트가 yParity 라 떼어내고
|
|
1091
|
+
* v 를 복원해 65바이트로 편다.
|
|
1092
|
+
* - v 가 0/1 인 경우 — 온체인 ecrecover 규약인 27/28 로 맞춘다.
|
|
1093
|
+
*
|
|
1094
|
+
* 서버가 130 hex 만 받으므로 여기서 안 맞추면 그쪽에서 거부된다. 그 외 값은 recover 가 어긋나
|
|
1095
|
+
* 어차피 실패하므로 여기서 끊는다.
|
|
1096
|
+
*/
|
|
1097
|
+
declare function normalizeSignature(signature: string): string;
|
|
1088
1098
|
|
|
1089
1099
|
/**
|
|
1090
1100
|
* @bitpal/checkout — BitPal Checkout SDK
|
|
@@ -1114,4 +1124,4 @@ declare class BitPal {
|
|
|
1114
1124
|
constructor(config: BitPalConfig | string);
|
|
1115
1125
|
}
|
|
1116
1126
|
|
|
1117
|
-
export { type ApiResponse, BitPal, BitPalCheckoutClient, type BitPalConfig, BitPalError, CHECKOUT_WEBHOOK_EVENTS, type CheckoutLineItemInput, type CheckoutPaymentLink, type CheckoutPrice, type CheckoutProduct, type CheckoutSession, type CheckoutWebhookEvent, type CreatePaymentLinkParams, type CreatePriceParams, type CreateProductParams, type CreateSessionParams, type CreateX402PaymentOptions, type DepositAddress, type Eip3009Authorization, type Eip712Domain, type FeePreview, type IdempotencyOptions, type IssueDepositAddressParams, type LineItem, type ListPaymentLinksParams, type ListPricesParams, type ListProductsParams, type ListResponse, PaymentLinksResource, type PaymentOption, type PriceLineItem, PricesResource, ProductsResource, type PublicSession, type SessionStatus, TRANSFER_WITH_AUTHORIZATION_TYPES, type UpdatePaymentLinkParams, type UpdatePriceParams, type UpdateProductParams, WEBHOOK_HEADERS, type WebhookPayload, type X402Accept, type X402ClaimState, type X402FeeBreakdown, X402Gate, type X402GateOptions, type X402GateResult, type X402MiddlewareOptions, type X402PaymentPayload, type X402PaymentResult, type X402ReplayStore, type X402Request, type X402Requirements, type X402Response, type X402SettledPayment, type X402Signer, type X402TypedDataRequest, X402_PAYMENT_HEADER, X402_PAYMENT_RESPONSE_HEADER, createMemoryReplayStore, createX402Payment, decodePaymentHeader, encodePaymentHeader, fromAtomic, fromAtomicUSDC,
|
|
1127
|
+
export { type ApiResponse, BitPal, BitPalCheckoutClient, type BitPalConfig, BitPalError, CHECKOUT_WEBHOOK_EVENTS, type CheckoutLineItemInput, type CheckoutPaymentLink, type CheckoutPrice, type CheckoutProduct, type CheckoutSession, type CheckoutWebhookEvent, type CreatePaymentLinkParams, type CreatePriceParams, type CreateProductParams, type CreateSessionParams, type CreateX402PaymentOptions, type DepositAddress, type Eip3009Authorization, type Eip712Domain, type FeePreview, type IdempotencyOptions, type IssueDepositAddressParams, type LineItem, type ListPaymentLinksParams, type ListPricesParams, type ListProductsParams, type ListResponse, PaymentLinksResource, type PaymentOption, type PriceLineItem, PricesResource, ProductsResource, type PublicSession, type SessionStatus, TRANSFER_WITH_AUTHORIZATION_TYPES, type UpdatePaymentLinkParams, type UpdatePriceParams, type UpdateProductParams, WEBHOOK_HEADERS, type WebhookPayload, type X402Accept, type X402ClaimState, type X402FeeBreakdown, X402Gate, type X402GateOptions, type X402GateResult, type X402MiddlewareOptions, type X402PaymentPayload, type X402PaymentResult, type X402ReplayStore, type X402Request, type X402Requirements, type X402Response, type X402SettledPayment, type X402Signer, type X402TypedDataRequest, X402_PAYMENT_HEADER, X402_PAYMENT_RESPONSE_HEADER, createMemoryReplayStore, createX402Payment, decodePaymentHeader, encodePaymentHeader, fromAtomic, fromAtomicUSDC, normalizeSignature, parseWebhookEvent, toAtomic, toAtomicUSDC, verifyWebhookSignature, x402 };
|
package/dist/index.js
CHANGED
|
@@ -292,7 +292,12 @@ var CHECKOUT_WEBHOOK_EVENTS = [
|
|
|
292
292
|
"checkout.session.settled",
|
|
293
293
|
"checkout.session.expired",
|
|
294
294
|
"checkout.session.unresolved",
|
|
295
|
-
"checkout.refund.recorded"
|
|
295
|
+
"checkout.refund.recorded",
|
|
296
|
+
// x402 결제의 **사후** 확정. 정상 경로(/settle 이 그 자리에서 결과를 반환)는 발행되지 않는다 —
|
|
297
|
+
// 이미 아는 결과를 중복으로 받지 않게 하기 위함이다. /settle 이 503 을 반환해 결과를 모른 채
|
|
298
|
+
// 남았을 때, reconciler 가 체인을 보고 종결시키면 이 이벤트가 온다.
|
|
299
|
+
// payload: { payment_id, status: 'confirmed'|'reverted'|'expired', tx_hash, resource, amount, … }
|
|
300
|
+
"x402.payment.resolved"
|
|
296
301
|
];
|
|
297
302
|
var WEBHOOK_HEADERS = {
|
|
298
303
|
signature: "X-Webhook-Signature-256",
|
|
@@ -480,11 +485,11 @@ var X402Gate = class {
|
|
|
480
485
|
} catch {
|
|
481
486
|
return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
|
|
482
487
|
}
|
|
483
|
-
const
|
|
484
|
-
if (!
|
|
488
|
+
const auth = payload?.payload?.authorization;
|
|
489
|
+
if (!auth?.from || !auth?.nonce) {
|
|
485
490
|
return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
|
|
486
491
|
}
|
|
487
|
-
const key = `${payload.network}:${
|
|
492
|
+
const key = `${payload.network}:${auth.from.toLowerCase()}:${auth.nonce.toLowerCase()}`;
|
|
488
493
|
const claim = await this.store.claim(key, this.maxTimeoutSeconds);
|
|
489
494
|
if (claim === "used") {
|
|
490
495
|
return {
|
|
@@ -522,7 +527,12 @@ var X402Gate = class {
|
|
|
522
527
|
},
|
|
523
528
|
paymentRequirements: {
|
|
524
529
|
asset: accept.asset,
|
|
530
|
+
// payer 가 서명한 목적지 = 분배 조건을 커밋한 정산 주소.
|
|
525
531
|
payTo: accept.payTo,
|
|
532
|
+
// 머천트 정산 지갑. facilitator 가 이 값으로 정산 주소를 재계산해 payTo 와 대조한다 —
|
|
533
|
+
// CREATE2 는 단방향이라 payTo 에서 되돌릴 수 없으므로 따로 보내야 한다.
|
|
534
|
+
// 우리 설정값을 그대로 쓴다(402 응답에서 읽지 않는다 — 그러면 재계산의 의미가 없다).
|
|
535
|
+
merchant: this.opts.payTo,
|
|
526
536
|
maxAmountRequired: accept.maxAmountRequired,
|
|
527
537
|
resource: accept.resource
|
|
528
538
|
}
|
|
@@ -574,7 +584,7 @@ var X402Gate = class {
|
|
|
574
584
|
txHash: settled.tx_hash,
|
|
575
585
|
feeTxHash: settled.fee_tx_hash ?? null,
|
|
576
586
|
network: settled.network,
|
|
577
|
-
payer:
|
|
587
|
+
payer: auth.from,
|
|
578
588
|
amount: accept.maxAmountRequired,
|
|
579
589
|
idempotent: settled.idempotent ?? false
|
|
580
590
|
};
|
|
@@ -586,7 +596,7 @@ var X402Gate = class {
|
|
|
586
596
|
success: true,
|
|
587
597
|
transaction: settled.tx_hash,
|
|
588
598
|
network: settled.network,
|
|
589
|
-
payer:
|
|
599
|
+
payer: auth.from,
|
|
590
600
|
paymentId: settled.payment_id
|
|
591
601
|
}),
|
|
592
602
|
// 소진 확정은 **응답을 실제로 내준 뒤**다. 핸들러가 throw 하거나 프로세스가 죽으면 결제만 하고
|
|
@@ -707,18 +717,22 @@ function assertPaymentPayload(value) {
|
|
|
707
717
|
throw new Error("[BitPal] x402: invalid network.");
|
|
708
718
|
}
|
|
709
719
|
const inner = asObject(root.payload, "payload");
|
|
710
|
-
|
|
711
|
-
|
|
720
|
+
if (typeof inner.signature !== "string" || !PACKED_SIG_RE.test(inner.signature)) {
|
|
721
|
+
throw new Error("[BitPal] x402: invalid payload.signature (expected 65-byte packed hex).");
|
|
722
|
+
}
|
|
712
723
|
return {
|
|
713
724
|
x402Version: 1,
|
|
714
725
|
scheme: "exact",
|
|
715
726
|
network: root.network,
|
|
716
|
-
payload:
|
|
727
|
+
payload: {
|
|
728
|
+
signature: inner.signature,
|
|
729
|
+
authorization: assertAuthorization(inner.authorization, "authorization")
|
|
730
|
+
}
|
|
717
731
|
};
|
|
718
732
|
}
|
|
733
|
+
var PACKED_SIG_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
719
734
|
function assertAuthorization(value, label) {
|
|
720
|
-
const a = asObject(value,
|
|
721
|
-
const sig = asObject(a.signature, `${label} signature`);
|
|
735
|
+
const a = asObject(value, label);
|
|
722
736
|
const check = (ok, field) => {
|
|
723
737
|
if (!ok) throw new Error(`[BitPal] x402: invalid ${label}.${field}.`);
|
|
724
738
|
};
|
|
@@ -728,17 +742,13 @@ function assertAuthorization(value, label) {
|
|
|
728
742
|
check(typeof a.validAfter === "string" && DECIMAL_RE.test(a.validAfter), "validAfter");
|
|
729
743
|
check(typeof a.validBefore === "string" && DECIMAL_RE.test(a.validBefore), "validBefore");
|
|
730
744
|
check(typeof a.nonce === "string" && HEX32_RE.test(a.nonce), "nonce");
|
|
731
|
-
check(typeof sig.v === "number" && Number.isInteger(sig.v), "signature.v");
|
|
732
|
-
check(typeof sig.r === "string" && HEX32_RE.test(sig.r), "signature.r");
|
|
733
|
-
check(typeof sig.s === "string" && HEX32_RE.test(sig.s), "signature.s");
|
|
734
745
|
return {
|
|
735
746
|
from: a.from,
|
|
736
747
|
to: a.to,
|
|
737
748
|
value: a.value,
|
|
738
749
|
validAfter: a.validAfter,
|
|
739
750
|
validBefore: a.validBefore,
|
|
740
|
-
nonce: a.nonce
|
|
741
|
-
signature: { v: sig.v, r: sig.r, s: sig.s }
|
|
751
|
+
nonce: a.nonce
|
|
742
752
|
};
|
|
743
753
|
}
|
|
744
754
|
function asObject(value, label) {
|
|
@@ -812,49 +822,33 @@ async function createX402Payment(options) {
|
|
|
812
822
|
}
|
|
813
823
|
const validAfter = "0";
|
|
814
824
|
const validBefore = String(now + validFor);
|
|
815
|
-
const
|
|
816
|
-
const
|
|
817
|
-
const feeAmount = fee ? BigInt(fee.feeAmount) : 0n;
|
|
818
|
-
const merchantValue = fee ? fee.netAmount : accept.maxAmountRequired;
|
|
819
|
-
const merchant = await signAuthorization({
|
|
820
|
-
domain,
|
|
821
|
-
signTypedData: options.signTypedData,
|
|
825
|
+
const nonce = (options.randomNonce ?? randomNonce)();
|
|
826
|
+
const authorization = {
|
|
822
827
|
from: options.from,
|
|
823
828
|
to: accept.payTo,
|
|
824
|
-
value:
|
|
829
|
+
value: accept.maxAmountRequired,
|
|
825
830
|
validAfter,
|
|
826
831
|
validBefore,
|
|
827
|
-
nonce
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if (!fee?.feeRecipient) {
|
|
832
|
-
throw new Error("[BitPal] x402: feeAmount > 0 but feeRecipient is missing in the 402 response.");
|
|
833
|
-
}
|
|
834
|
-
feeLeg = await signAuthorization({
|
|
832
|
+
nonce
|
|
833
|
+
};
|
|
834
|
+
const signature = normalizeSignature(
|
|
835
|
+
await options.signTypedData({
|
|
835
836
|
domain,
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
validBefore,
|
|
842
|
-
nonce: nextNonce()
|
|
843
|
-
});
|
|
844
|
-
if (feeLeg.nonce.toLowerCase() === merchant.nonce.toLowerCase()) {
|
|
845
|
-
throw new Error("[BitPal] x402: the two authorizations must use different nonces.");
|
|
846
|
-
}
|
|
847
|
-
}
|
|
837
|
+
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
838
|
+
primaryType: "TransferWithAuthorization",
|
|
839
|
+
message: authorization
|
|
840
|
+
})
|
|
841
|
+
);
|
|
848
842
|
const payload = {
|
|
849
843
|
x402Version: 1,
|
|
850
844
|
scheme: "exact",
|
|
851
845
|
network: accept.network,
|
|
852
|
-
payload:
|
|
846
|
+
payload: { signature, authorization }
|
|
853
847
|
};
|
|
854
848
|
return {
|
|
855
849
|
header: encodePaymentHeader(payload),
|
|
856
850
|
payload,
|
|
857
|
-
totalAmount:
|
|
851
|
+
totalAmount: accept.maxAmountRequired
|
|
858
852
|
};
|
|
859
853
|
}
|
|
860
854
|
function resolveAccept(input) {
|
|
@@ -865,41 +859,24 @@ function resolveAccept(input) {
|
|
|
865
859
|
}
|
|
866
860
|
return input;
|
|
867
861
|
}
|
|
868
|
-
|
|
869
|
-
const message = {
|
|
870
|
-
from: args.from,
|
|
871
|
-
to: args.to,
|
|
872
|
-
value: args.value,
|
|
873
|
-
validAfter: args.validAfter,
|
|
874
|
-
validBefore: args.validBefore,
|
|
875
|
-
nonce: args.nonce
|
|
876
|
-
};
|
|
877
|
-
const signature = await args.signTypedData({
|
|
878
|
-
domain: args.domain,
|
|
879
|
-
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
880
|
-
primaryType: "TransferWithAuthorization",
|
|
881
|
-
message
|
|
882
|
-
});
|
|
883
|
-
return { ...message, signature: splitSignature(signature) };
|
|
884
|
-
}
|
|
885
|
-
function splitSignature(signature) {
|
|
862
|
+
function normalizeSignature(signature) {
|
|
886
863
|
const hex = signature.startsWith("0x") ? signature.slice(2) : signature;
|
|
887
864
|
if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length !== 130 && hex.length !== 128) {
|
|
888
865
|
throw new Error(
|
|
889
866
|
`[BitPal] x402: expected a 65-byte or 64-byte (EIP-2098) hex signature, got ${signature.length} chars.`
|
|
890
867
|
);
|
|
891
868
|
}
|
|
892
|
-
const r =
|
|
869
|
+
const r = hex.slice(0, 64);
|
|
893
870
|
if (hex.length === 128) {
|
|
894
871
|
const yParityAndS = BigInt(`0x${hex.slice(64, 128)}`);
|
|
895
872
|
const yParity = Number(yParityAndS >> 255n & 1n);
|
|
896
|
-
const s2 = yParityAndS & (1n << 255n) - 1n;
|
|
897
|
-
return {
|
|
873
|
+
const s2 = (yParityAndS & (1n << 255n) - 1n).toString(16).padStart(64, "0");
|
|
874
|
+
return `0x${r}${s2}${(27 + yParity).toString(16).padStart(2, "0")}`;
|
|
898
875
|
}
|
|
899
|
-
const s =
|
|
876
|
+
const s = hex.slice(64, 128);
|
|
900
877
|
const raw = parseInt(hex.slice(128, 130), 16);
|
|
901
|
-
if (raw === 27 || raw === 28) return {
|
|
902
|
-
if (raw === 0 || raw === 1) return {
|
|
878
|
+
if (raw === 27 || raw === 28) return `0x${r}${s}${raw.toString(16)}`;
|
|
879
|
+
if (raw === 0 || raw === 1) return `0x${r}${s}${(raw + 27).toString(16)}`;
|
|
903
880
|
throw new Error(`[BitPal] x402: unexpected signature v=${raw} (expected 0, 1, 27 or 28).`);
|
|
904
881
|
}
|
|
905
882
|
function randomNonce() {
|
|
@@ -940,8 +917,8 @@ export {
|
|
|
940
917
|
encodePaymentHeader,
|
|
941
918
|
fromAtomic,
|
|
942
919
|
fromAtomicUSDC,
|
|
920
|
+
normalizeSignature,
|
|
943
921
|
parseWebhookEvent,
|
|
944
|
-
splitSignature,
|
|
945
922
|
toAtomic,
|
|
946
923
|
toAtomicUSDC,
|
|
947
924
|
verifyWebhookSignature,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitpal/checkout",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "BitPal Checkout SDK — non-custodial crypto checkout: create sessions, issue per-order deposit addresses, poll status, verify signed webhooks, and charge per API call with x402.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|