@bitpal/checkout 0.10.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 +31 -3
- package/dist/index.cjs +43 -71
- package/dist/index.d.cts +24 -14
- package/dist/index.d.ts +24 -14
- package/dist/index.js +42 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -249,9 +249,13 @@ the response, or `release()` if you could not.
|
|
|
249
249
|
|
|
250
250
|
### Payer side
|
|
251
251
|
|
|
252
|
-
The
|
|
253
|
-
`
|
|
254
|
-
|
|
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.
|
|
255
259
|
|
|
256
260
|
```ts
|
|
257
261
|
import { createX402Payment } from '@bitpal/checkout';
|
|
@@ -277,6 +281,10 @@ if (res.status === 402) {
|
|
|
277
281
|
Amounts are human-readable (`'1.00'`), the same model as `line_items[].amount` — the server converts.
|
|
278
282
|
Pass `amountAtomic` instead if you already have raw units.
|
|
279
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
|
+
|
|
280
288
|
EOA signers only — smart-contract wallets cannot sign EIP-3009 authorizations. Amounts are exact:
|
|
281
289
|
over- and underpayment are both rejected, and this lane has no refund path. Authorizations are valid
|
|
282
290
|
for 90–3600 seconds (the server's floor is 60s; the extra 30s absorbs clock skew and round trips).
|
|
@@ -308,6 +316,26 @@ store wiring session creation + webhook verification + idempotency.
|
|
|
308
316
|
pnpm build # tsup → dist/ (ESM + .d.ts)
|
|
309
317
|
```
|
|
310
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
|
+
|
|
311
339
|
## Migrating from 0.3.x → 0.5.0 (breaking)
|
|
312
340
|
|
|
313
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,
|
|
@@ -544,11 +544,11 @@ var X402Gate = class {
|
|
|
544
544
|
} catch {
|
|
545
545
|
return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
|
|
546
546
|
}
|
|
547
|
-
const
|
|
548
|
-
if (!
|
|
547
|
+
const auth = payload?.payload?.authorization;
|
|
548
|
+
if (!auth?.from || !auth?.nonce) {
|
|
549
549
|
return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
|
|
550
550
|
}
|
|
551
|
-
const key = `${payload.network}:${
|
|
551
|
+
const key = `${payload.network}:${auth.from.toLowerCase()}:${auth.nonce.toLowerCase()}`;
|
|
552
552
|
const claim = await this.store.claim(key, this.maxTimeoutSeconds);
|
|
553
553
|
if (claim === "used") {
|
|
554
554
|
return {
|
|
@@ -586,7 +586,12 @@ var X402Gate = class {
|
|
|
586
586
|
},
|
|
587
587
|
paymentRequirements: {
|
|
588
588
|
asset: accept.asset,
|
|
589
|
+
// payer 가 서명한 목적지 = 분배 조건을 커밋한 정산 주소.
|
|
589
590
|
payTo: accept.payTo,
|
|
591
|
+
// 머천트 정산 지갑. facilitator 가 이 값으로 정산 주소를 재계산해 payTo 와 대조한다 —
|
|
592
|
+
// CREATE2 는 단방향이라 payTo 에서 되돌릴 수 없으므로 따로 보내야 한다.
|
|
593
|
+
// 우리 설정값을 그대로 쓴다(402 응답에서 읽지 않는다 — 그러면 재계산의 의미가 없다).
|
|
594
|
+
merchant: this.opts.payTo,
|
|
590
595
|
maxAmountRequired: accept.maxAmountRequired,
|
|
591
596
|
resource: accept.resource
|
|
592
597
|
}
|
|
@@ -638,7 +643,7 @@ var X402Gate = class {
|
|
|
638
643
|
txHash: settled.tx_hash,
|
|
639
644
|
feeTxHash: settled.fee_tx_hash ?? null,
|
|
640
645
|
network: settled.network,
|
|
641
|
-
payer:
|
|
646
|
+
payer: auth.from,
|
|
642
647
|
amount: accept.maxAmountRequired,
|
|
643
648
|
idempotent: settled.idempotent ?? false
|
|
644
649
|
};
|
|
@@ -650,7 +655,7 @@ var X402Gate = class {
|
|
|
650
655
|
success: true,
|
|
651
656
|
transaction: settled.tx_hash,
|
|
652
657
|
network: settled.network,
|
|
653
|
-
payer:
|
|
658
|
+
payer: auth.from,
|
|
654
659
|
paymentId: settled.payment_id
|
|
655
660
|
}),
|
|
656
661
|
// 소진 확정은 **응답을 실제로 내준 뒤**다. 핸들러가 throw 하거나 프로세스가 죽으면 결제만 하고
|
|
@@ -771,18 +776,22 @@ function assertPaymentPayload(value) {
|
|
|
771
776
|
throw new Error("[BitPal] x402: invalid network.");
|
|
772
777
|
}
|
|
773
778
|
const inner = asObject(root.payload, "payload");
|
|
774
|
-
|
|
775
|
-
|
|
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
|
+
}
|
|
776
782
|
return {
|
|
777
783
|
x402Version: 1,
|
|
778
784
|
scheme: "exact",
|
|
779
785
|
network: root.network,
|
|
780
|
-
payload:
|
|
786
|
+
payload: {
|
|
787
|
+
signature: inner.signature,
|
|
788
|
+
authorization: assertAuthorization(inner.authorization, "authorization")
|
|
789
|
+
}
|
|
781
790
|
};
|
|
782
791
|
}
|
|
792
|
+
var PACKED_SIG_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
783
793
|
function assertAuthorization(value, label) {
|
|
784
|
-
const a = asObject(value,
|
|
785
|
-
const sig = asObject(a.signature, `${label} signature`);
|
|
794
|
+
const a = asObject(value, label);
|
|
786
795
|
const check = (ok, field) => {
|
|
787
796
|
if (!ok) throw new Error(`[BitPal] x402: invalid ${label}.${field}.`);
|
|
788
797
|
};
|
|
@@ -792,17 +801,13 @@ function assertAuthorization(value, label) {
|
|
|
792
801
|
check(typeof a.validAfter === "string" && DECIMAL_RE.test(a.validAfter), "validAfter");
|
|
793
802
|
check(typeof a.validBefore === "string" && DECIMAL_RE.test(a.validBefore), "validBefore");
|
|
794
803
|
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
804
|
return {
|
|
799
805
|
from: a.from,
|
|
800
806
|
to: a.to,
|
|
801
807
|
value: a.value,
|
|
802
808
|
validAfter: a.validAfter,
|
|
803
809
|
validBefore: a.validBefore,
|
|
804
|
-
nonce: a.nonce
|
|
805
|
-
signature: { v: sig.v, r: sig.r, s: sig.s }
|
|
810
|
+
nonce: a.nonce
|
|
806
811
|
};
|
|
807
812
|
}
|
|
808
813
|
function asObject(value, label) {
|
|
@@ -876,49 +881,33 @@ async function createX402Payment(options) {
|
|
|
876
881
|
}
|
|
877
882
|
const validAfter = "0";
|
|
878
883
|
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,
|
|
884
|
+
const nonce = (options.randomNonce ?? randomNonce)();
|
|
885
|
+
const authorization = {
|
|
886
886
|
from: options.from,
|
|
887
887
|
to: accept.payTo,
|
|
888
|
-
value:
|
|
888
|
+
value: accept.maxAmountRequired,
|
|
889
889
|
validAfter,
|
|
890
890
|
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({
|
|
891
|
+
nonce
|
|
892
|
+
};
|
|
893
|
+
const signature = normalizeSignature(
|
|
894
|
+
await options.signTypedData({
|
|
899
895
|
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
|
-
}
|
|
896
|
+
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
897
|
+
primaryType: "TransferWithAuthorization",
|
|
898
|
+
message: authorization
|
|
899
|
+
})
|
|
900
|
+
);
|
|
912
901
|
const payload = {
|
|
913
902
|
x402Version: 1,
|
|
914
903
|
scheme: "exact",
|
|
915
904
|
network: accept.network,
|
|
916
|
-
payload:
|
|
905
|
+
payload: { signature, authorization }
|
|
917
906
|
};
|
|
918
907
|
return {
|
|
919
908
|
header: encodePaymentHeader(payload),
|
|
920
909
|
payload,
|
|
921
|
-
totalAmount:
|
|
910
|
+
totalAmount: accept.maxAmountRequired
|
|
922
911
|
};
|
|
923
912
|
}
|
|
924
913
|
function resolveAccept(input) {
|
|
@@ -929,41 +918,24 @@ function resolveAccept(input) {
|
|
|
929
918
|
}
|
|
930
919
|
return input;
|
|
931
920
|
}
|
|
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) {
|
|
921
|
+
function normalizeSignature(signature) {
|
|
950
922
|
const hex = signature.startsWith("0x") ? signature.slice(2) : signature;
|
|
951
923
|
if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length !== 130 && hex.length !== 128) {
|
|
952
924
|
throw new Error(
|
|
953
925
|
`[BitPal] x402: expected a 65-byte or 64-byte (EIP-2098) hex signature, got ${signature.length} chars.`
|
|
954
926
|
);
|
|
955
927
|
}
|
|
956
|
-
const r =
|
|
928
|
+
const r = hex.slice(0, 64);
|
|
957
929
|
if (hex.length === 128) {
|
|
958
930
|
const yParityAndS = BigInt(`0x${hex.slice(64, 128)}`);
|
|
959
931
|
const yParity = Number(yParityAndS >> 255n & 1n);
|
|
960
|
-
const s2 = yParityAndS & (1n << 255n) - 1n;
|
|
961
|
-
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")}`;
|
|
962
934
|
}
|
|
963
|
-
const s =
|
|
935
|
+
const s = hex.slice(64, 128);
|
|
964
936
|
const raw = parseInt(hex.slice(128, 130), 16);
|
|
965
|
-
if (raw === 27 || raw === 28) return {
|
|
966
|
-
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)}`;
|
|
967
939
|
throw new Error(`[BitPal] x402: unexpected signature v=${raw} (expected 0, 1, 27 or 28).`);
|
|
968
940
|
}
|
|
969
941
|
function randomNonce() {
|
|
@@ -1005,8 +977,8 @@ var BitPal = class {
|
|
|
1005
977
|
encodePaymentHeader,
|
|
1006
978
|
fromAtomic,
|
|
1007
979
|
fromAtomicUSDC,
|
|
980
|
+
normalizeSignature,
|
|
1008
981
|
parseWebhookEvent,
|
|
1009
|
-
splitSignature,
|
|
1010
982
|
toAtomic,
|
|
1011
983
|
toAtomicUSDC,
|
|
1012
984
|
verifyWebhookSignature,
|
package/dist/index.d.cts
CHANGED
|
@@ -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
|
@@ -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
|
@@ -485,11 +485,11 @@ var X402Gate = class {
|
|
|
485
485
|
} catch {
|
|
486
486
|
return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
|
|
487
487
|
}
|
|
488
|
-
const
|
|
489
|
-
if (!
|
|
488
|
+
const auth = payload?.payload?.authorization;
|
|
489
|
+
if (!auth?.from || !auth?.nonce) {
|
|
490
490
|
return this.paymentRequired(resource, "MALFORMED_PAYMENT_HEADER");
|
|
491
491
|
}
|
|
492
|
-
const key = `${payload.network}:${
|
|
492
|
+
const key = `${payload.network}:${auth.from.toLowerCase()}:${auth.nonce.toLowerCase()}`;
|
|
493
493
|
const claim = await this.store.claim(key, this.maxTimeoutSeconds);
|
|
494
494
|
if (claim === "used") {
|
|
495
495
|
return {
|
|
@@ -527,7 +527,12 @@ var X402Gate = class {
|
|
|
527
527
|
},
|
|
528
528
|
paymentRequirements: {
|
|
529
529
|
asset: accept.asset,
|
|
530
|
+
// payer 가 서명한 목적지 = 분배 조건을 커밋한 정산 주소.
|
|
530
531
|
payTo: accept.payTo,
|
|
532
|
+
// 머천트 정산 지갑. facilitator 가 이 값으로 정산 주소를 재계산해 payTo 와 대조한다 —
|
|
533
|
+
// CREATE2 는 단방향이라 payTo 에서 되돌릴 수 없으므로 따로 보내야 한다.
|
|
534
|
+
// 우리 설정값을 그대로 쓴다(402 응답에서 읽지 않는다 — 그러면 재계산의 의미가 없다).
|
|
535
|
+
merchant: this.opts.payTo,
|
|
531
536
|
maxAmountRequired: accept.maxAmountRequired,
|
|
532
537
|
resource: accept.resource
|
|
533
538
|
}
|
|
@@ -579,7 +584,7 @@ var X402Gate = class {
|
|
|
579
584
|
txHash: settled.tx_hash,
|
|
580
585
|
feeTxHash: settled.fee_tx_hash ?? null,
|
|
581
586
|
network: settled.network,
|
|
582
|
-
payer:
|
|
587
|
+
payer: auth.from,
|
|
583
588
|
amount: accept.maxAmountRequired,
|
|
584
589
|
idempotent: settled.idempotent ?? false
|
|
585
590
|
};
|
|
@@ -591,7 +596,7 @@ var X402Gate = class {
|
|
|
591
596
|
success: true,
|
|
592
597
|
transaction: settled.tx_hash,
|
|
593
598
|
network: settled.network,
|
|
594
|
-
payer:
|
|
599
|
+
payer: auth.from,
|
|
595
600
|
paymentId: settled.payment_id
|
|
596
601
|
}),
|
|
597
602
|
// 소진 확정은 **응답을 실제로 내준 뒤**다. 핸들러가 throw 하거나 프로세스가 죽으면 결제만 하고
|
|
@@ -712,18 +717,22 @@ function assertPaymentPayload(value) {
|
|
|
712
717
|
throw new Error("[BitPal] x402: invalid network.");
|
|
713
718
|
}
|
|
714
719
|
const inner = asObject(root.payload, "payload");
|
|
715
|
-
|
|
716
|
-
|
|
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
|
+
}
|
|
717
723
|
return {
|
|
718
724
|
x402Version: 1,
|
|
719
725
|
scheme: "exact",
|
|
720
726
|
network: root.network,
|
|
721
|
-
payload:
|
|
727
|
+
payload: {
|
|
728
|
+
signature: inner.signature,
|
|
729
|
+
authorization: assertAuthorization(inner.authorization, "authorization")
|
|
730
|
+
}
|
|
722
731
|
};
|
|
723
732
|
}
|
|
733
|
+
var PACKED_SIG_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
724
734
|
function assertAuthorization(value, label) {
|
|
725
|
-
const a = asObject(value,
|
|
726
|
-
const sig = asObject(a.signature, `${label} signature`);
|
|
735
|
+
const a = asObject(value, label);
|
|
727
736
|
const check = (ok, field) => {
|
|
728
737
|
if (!ok) throw new Error(`[BitPal] x402: invalid ${label}.${field}.`);
|
|
729
738
|
};
|
|
@@ -733,17 +742,13 @@ function assertAuthorization(value, label) {
|
|
|
733
742
|
check(typeof a.validAfter === "string" && DECIMAL_RE.test(a.validAfter), "validAfter");
|
|
734
743
|
check(typeof a.validBefore === "string" && DECIMAL_RE.test(a.validBefore), "validBefore");
|
|
735
744
|
check(typeof a.nonce === "string" && HEX32_RE.test(a.nonce), "nonce");
|
|
736
|
-
check(typeof sig.v === "number" && Number.isInteger(sig.v), "signature.v");
|
|
737
|
-
check(typeof sig.r === "string" && HEX32_RE.test(sig.r), "signature.r");
|
|
738
|
-
check(typeof sig.s === "string" && HEX32_RE.test(sig.s), "signature.s");
|
|
739
745
|
return {
|
|
740
746
|
from: a.from,
|
|
741
747
|
to: a.to,
|
|
742
748
|
value: a.value,
|
|
743
749
|
validAfter: a.validAfter,
|
|
744
750
|
validBefore: a.validBefore,
|
|
745
|
-
nonce: a.nonce
|
|
746
|
-
signature: { v: sig.v, r: sig.r, s: sig.s }
|
|
751
|
+
nonce: a.nonce
|
|
747
752
|
};
|
|
748
753
|
}
|
|
749
754
|
function asObject(value, label) {
|
|
@@ -817,49 +822,33 @@ async function createX402Payment(options) {
|
|
|
817
822
|
}
|
|
818
823
|
const validAfter = "0";
|
|
819
824
|
const validBefore = String(now + validFor);
|
|
820
|
-
const
|
|
821
|
-
const
|
|
822
|
-
const feeAmount = fee ? BigInt(fee.feeAmount) : 0n;
|
|
823
|
-
const merchantValue = fee ? fee.netAmount : accept.maxAmountRequired;
|
|
824
|
-
const merchant = await signAuthorization({
|
|
825
|
-
domain,
|
|
826
|
-
signTypedData: options.signTypedData,
|
|
825
|
+
const nonce = (options.randomNonce ?? randomNonce)();
|
|
826
|
+
const authorization = {
|
|
827
827
|
from: options.from,
|
|
828
828
|
to: accept.payTo,
|
|
829
|
-
value:
|
|
829
|
+
value: accept.maxAmountRequired,
|
|
830
830
|
validAfter,
|
|
831
831
|
validBefore,
|
|
832
|
-
nonce
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if (!fee?.feeRecipient) {
|
|
837
|
-
throw new Error("[BitPal] x402: feeAmount > 0 but feeRecipient is missing in the 402 response.");
|
|
838
|
-
}
|
|
839
|
-
feeLeg = await signAuthorization({
|
|
832
|
+
nonce
|
|
833
|
+
};
|
|
834
|
+
const signature = normalizeSignature(
|
|
835
|
+
await options.signTypedData({
|
|
840
836
|
domain,
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
validBefore,
|
|
847
|
-
nonce: nextNonce()
|
|
848
|
-
});
|
|
849
|
-
if (feeLeg.nonce.toLowerCase() === merchant.nonce.toLowerCase()) {
|
|
850
|
-
throw new Error("[BitPal] x402: the two authorizations must use different nonces.");
|
|
851
|
-
}
|
|
852
|
-
}
|
|
837
|
+
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
838
|
+
primaryType: "TransferWithAuthorization",
|
|
839
|
+
message: authorization
|
|
840
|
+
})
|
|
841
|
+
);
|
|
853
842
|
const payload = {
|
|
854
843
|
x402Version: 1,
|
|
855
844
|
scheme: "exact",
|
|
856
845
|
network: accept.network,
|
|
857
|
-
payload:
|
|
846
|
+
payload: { signature, authorization }
|
|
858
847
|
};
|
|
859
848
|
return {
|
|
860
849
|
header: encodePaymentHeader(payload),
|
|
861
850
|
payload,
|
|
862
|
-
totalAmount:
|
|
851
|
+
totalAmount: accept.maxAmountRequired
|
|
863
852
|
};
|
|
864
853
|
}
|
|
865
854
|
function resolveAccept(input) {
|
|
@@ -870,41 +859,24 @@ function resolveAccept(input) {
|
|
|
870
859
|
}
|
|
871
860
|
return input;
|
|
872
861
|
}
|
|
873
|
-
|
|
874
|
-
const message = {
|
|
875
|
-
from: args.from,
|
|
876
|
-
to: args.to,
|
|
877
|
-
value: args.value,
|
|
878
|
-
validAfter: args.validAfter,
|
|
879
|
-
validBefore: args.validBefore,
|
|
880
|
-
nonce: args.nonce
|
|
881
|
-
};
|
|
882
|
-
const signature = await args.signTypedData({
|
|
883
|
-
domain: args.domain,
|
|
884
|
-
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
885
|
-
primaryType: "TransferWithAuthorization",
|
|
886
|
-
message
|
|
887
|
-
});
|
|
888
|
-
return { ...message, signature: splitSignature(signature) };
|
|
889
|
-
}
|
|
890
|
-
function splitSignature(signature) {
|
|
862
|
+
function normalizeSignature(signature) {
|
|
891
863
|
const hex = signature.startsWith("0x") ? signature.slice(2) : signature;
|
|
892
864
|
if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length !== 130 && hex.length !== 128) {
|
|
893
865
|
throw new Error(
|
|
894
866
|
`[BitPal] x402: expected a 65-byte or 64-byte (EIP-2098) hex signature, got ${signature.length} chars.`
|
|
895
867
|
);
|
|
896
868
|
}
|
|
897
|
-
const r =
|
|
869
|
+
const r = hex.slice(0, 64);
|
|
898
870
|
if (hex.length === 128) {
|
|
899
871
|
const yParityAndS = BigInt(`0x${hex.slice(64, 128)}`);
|
|
900
872
|
const yParity = Number(yParityAndS >> 255n & 1n);
|
|
901
|
-
const s2 = yParityAndS & (1n << 255n) - 1n;
|
|
902
|
-
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")}`;
|
|
903
875
|
}
|
|
904
|
-
const s =
|
|
876
|
+
const s = hex.slice(64, 128);
|
|
905
877
|
const raw = parseInt(hex.slice(128, 130), 16);
|
|
906
|
-
if (raw === 27 || raw === 28) return {
|
|
907
|
-
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)}`;
|
|
908
880
|
throw new Error(`[BitPal] x402: unexpected signature v=${raw} (expected 0, 1, 27 or 28).`);
|
|
909
881
|
}
|
|
910
882
|
function randomNonce() {
|
|
@@ -945,8 +917,8 @@ export {
|
|
|
945
917
|
encodePaymentHeader,
|
|
946
918
|
fromAtomic,
|
|
947
919
|
fromAtomicUSDC,
|
|
920
|
+
normalizeSignature,
|
|
948
921
|
parseWebhookEvent,
|
|
949
|
-
splitSignature,
|
|
950
922
|
toAtomic,
|
|
951
923
|
toAtomicUSDC,
|
|
952
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": {
|