@phala/pay 0.8.3 → 0.9.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 +20 -83
- package/dist/checkout.d.ts +4 -2
- package/dist/checkout.d.ts.map +1 -1
- package/dist/checkout.js +16 -1
- package/dist/checkout.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/shared/checkout-params.d.ts +7 -0
- package/dist/shared/checkout-params.d.ts.map +1 -0
- package/dist/shared/checkout-params.js +2 -0
- package/dist/shared/checkout-params.js.map +1 -0
- package/package.json +12 -30
- package/src/checkout.ts +19 -2
- package/src/index.ts +5 -2
- package/src/shared/checkout-params.ts +6 -0
- package/dist/react/Checkout.d.ts +0 -43
- package/dist/react/Checkout.d.ts.map +0 -1
- package/dist/react/Checkout.js +0 -195
- package/dist/react/Checkout.js.map +0 -1
- package/dist/react/DepositAddress.d.ts +0 -32
- package/dist/react/DepositAddress.d.ts.map +0 -1
- package/dist/react/DepositAddress.js +0 -131
- package/dist/react/DepositAddress.js.map +0 -1
- package/dist/react/Field.d.ts +0 -7
- package/dist/react/Field.d.ts.map +0 -1
- package/dist/react/Field.js +0 -21
- package/dist/react/Field.js.map +0 -1
- package/dist/react/Icons.d.ts +0 -17
- package/dist/react/Icons.d.ts.map +0 -1
- package/dist/react/Icons.js +0 -18
- package/dist/react/Icons.js.map +0 -1
- package/dist/react/QrCode.d.ts +0 -8
- package/dist/react/QrCode.d.ts.map +0 -1
- package/dist/react/QrCode.js +0 -20
- package/dist/react/QrCode.js.map +0 -1
- package/dist/react/appearance.d.ts +0 -5
- package/dist/react/appearance.d.ts.map +0 -1
- package/dist/react/appearance.js +0 -2
- package/dist/react/appearance.js.map +0 -1
- package/dist/react/icon-paths.d.ts +0 -13
- package/dist/react/icon-paths.d.ts.map +0 -1
- package/dist/react/icon-paths.js +0 -14
- package/dist/react/icon-paths.js.map +0 -1
- package/dist/react/index.d.ts +0 -6
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js +0 -6
- package/dist/react/index.js.map +0 -1
- package/dist/react/useCheckout.d.ts +0 -9
- package/dist/react/useCheckout.d.ts.map +0 -1
- package/dist/react/useCheckout.js +0 -32
- package/dist/react/useCheckout.js.map +0 -1
- package/dist/server/addresses.d.ts +0 -78
- package/dist/server/addresses.d.ts.map +0 -1
- package/dist/server/addresses.js +0 -118
- package/dist/server/addresses.js.map +0 -1
- package/dist/server/index.d.ts +0 -12
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js +0 -12
- package/dist/server/index.js.map +0 -1
- package/dist/server/sweeps.d.ts +0 -78
- package/dist/server/sweeps.d.ts.map +0 -1
- package/dist/server/sweeps.js +0 -117
- package/dist/server/sweeps.js.map +0 -1
- package/dist/server/webhook.d.ts +0 -61
- package/dist/server/webhook.d.ts.map +0 -1
- package/dist/server/webhook.js +0 -154
- package/dist/server/webhook.js.map +0 -1
- package/src/css.d.ts +0 -1
- package/src/react/Checkout.tsx +0 -475
- package/src/react/DepositAddress.tsx +0 -240
- package/src/react/Field.tsx +0 -46
- package/src/react/Icons.tsx +0 -43
- package/src/react/QrCode.tsx +0 -39
- package/src/react/appearance.ts +0 -4
- package/src/react/icon-paths.tsx +0 -13
- package/src/react/index.ts +0 -7
- package/src/react/useCheckout.ts +0 -58
- package/src/server/addresses.ts +0 -223
- package/src/server/index.ts +0 -38
- package/src/server/sweeps.ts +0 -197
- package/src/server/webhook.ts +0 -208
- package/src/styles.css +0 -80
package/src/server/index.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server-side helpers of `@phala/pay`. Nothing here needs or takes an API key: webhook
|
|
3
|
-
* verification uses your account's public webhook key, and the address and sweep helpers run
|
|
4
|
-
* offline. Call the API itself from your backend with a restricted key (the Python SDK, or any
|
|
5
|
-
* HTTP client with `Authorization: Bearer ppay_rk_…`); never ship a key to a browser. Recompute
|
|
6
|
-
* every address with `verifyQuoteAddress` or `verifyDepositAddress` from the pins you configure
|
|
7
|
-
* yourself before you show it.
|
|
8
|
-
*/
|
|
9
|
-
export {
|
|
10
|
-
WebhookSignatureError,
|
|
11
|
-
constructEvent,
|
|
12
|
-
type ConstructEventOptions,
|
|
13
|
-
type WebhookEvent,
|
|
14
|
-
} from "./webhook.js";
|
|
15
|
-
export {
|
|
16
|
-
AddressMismatchError,
|
|
17
|
-
depositAddress,
|
|
18
|
-
depositAddressSalt,
|
|
19
|
-
forwarderAddress,
|
|
20
|
-
quoteSalt,
|
|
21
|
-
quoteAddress,
|
|
22
|
-
verifyDepositAddress,
|
|
23
|
-
verifyQuoteAddress,
|
|
24
|
-
type AddressPins,
|
|
25
|
-
type Forwarder,
|
|
26
|
-
} from "./addresses.js";
|
|
27
|
-
export {
|
|
28
|
-
batchChecksum,
|
|
29
|
-
flushTransaction,
|
|
30
|
-
flushTransactions,
|
|
31
|
-
safeBatch,
|
|
32
|
-
type BatchFile,
|
|
33
|
-
type BatchFileMeta,
|
|
34
|
-
type BatchTransaction,
|
|
35
|
-
type Call,
|
|
36
|
-
type ForwarderObject,
|
|
37
|
-
type SafeBatchOptions,
|
|
38
|
-
} from "./sweeps.js";
|
package/src/server/sweeps.ts
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
encodeFunctionData,
|
|
3
|
-
getAddress,
|
|
4
|
-
isHex,
|
|
5
|
-
keccak256,
|
|
6
|
-
stringToBytes,
|
|
7
|
-
type Address,
|
|
8
|
-
type Hex,
|
|
9
|
-
} from "viem";
|
|
10
|
-
|
|
11
|
-
const FLUSH_ABI = [
|
|
12
|
-
{
|
|
13
|
-
type: "function",
|
|
14
|
-
name: "flush",
|
|
15
|
-
stateMutability: "nonpayable",
|
|
16
|
-
inputs: [
|
|
17
|
-
{ name: "treasury", type: "address" },
|
|
18
|
-
{ name: "salts", type: "bytes32[]" },
|
|
19
|
-
{ name: "token", type: "address" },
|
|
20
|
-
],
|
|
21
|
-
outputs: [],
|
|
22
|
-
},
|
|
23
|
-
] as const;
|
|
24
|
-
|
|
25
|
-
/** A contract call as a wallet or the Safe Transaction Builder takes it. */
|
|
26
|
-
export interface Call {
|
|
27
|
-
to: Address;
|
|
28
|
-
data: Hex;
|
|
29
|
-
/** Native value in wei, a decimal string: `"0"` for a flush. */
|
|
30
|
-
value: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* `factory.flush(treasury, salts, token)`, encoded offline from the exported forwarders
|
|
35
|
-
* (`GET /v1/forwarders`): every listed forwarder's whole balance of `token` moves to `treasury`,
|
|
36
|
-
* the only address it can pay. Anyone may send it; the sender pays the gas.
|
|
37
|
-
*/
|
|
38
|
-
export function flushTransaction(
|
|
39
|
-
factory: string,
|
|
40
|
-
treasury: string,
|
|
41
|
-
salts: readonly Hex[],
|
|
42
|
-
token: string,
|
|
43
|
-
): Call {
|
|
44
|
-
if (salts.length === 0) {
|
|
45
|
-
throw new TypeError("flush needs at least one salt");
|
|
46
|
-
}
|
|
47
|
-
if (!salts.every((salt) => isHex(salt) && salt.length === 66)) {
|
|
48
|
-
throw new TypeError("a salt is 32 bytes of hex");
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
to: getAddress(factory),
|
|
52
|
-
data: encodeFunctionData({
|
|
53
|
-
abi: FLUSH_ABI,
|
|
54
|
-
functionName: "flush",
|
|
55
|
-
args: [getAddress(treasury), salts, getAddress(token)],
|
|
56
|
-
}),
|
|
57
|
-
value: "0",
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** A forwarder as `GET /v1/forwarders` returns it. */
|
|
62
|
-
export interface ForwarderObject {
|
|
63
|
-
chain_id: number;
|
|
64
|
-
factory: string;
|
|
65
|
-
treasury: string;
|
|
66
|
-
salt: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Groups forwarders of one chain, such as `GET /v1/forwarders?sweepable=<token>` lists, into
|
|
71
|
-
* `flushTransaction` calls: one per factory and treasury, of at most `maxSalts` forwarders.
|
|
72
|
-
*/
|
|
73
|
-
export function flushTransactions(
|
|
74
|
-
forwarders: Iterable<ForwarderObject>,
|
|
75
|
-
token: string,
|
|
76
|
-
maxSalts = 200,
|
|
77
|
-
): Call[] {
|
|
78
|
-
const groups = new Map<string, { factory: string; treasury: string; salts: Hex[] }>();
|
|
79
|
-
const chains = new Set<number>();
|
|
80
|
-
for (const forwarder of forwarders) {
|
|
81
|
-
chains.add(forwarder.chain_id);
|
|
82
|
-
const factory = getAddress(forwarder.factory);
|
|
83
|
-
const treasury = getAddress(forwarder.treasury);
|
|
84
|
-
const key = `${factory}:${treasury}`;
|
|
85
|
-
const group = groups.get(key) ?? { factory, treasury, salts: [] };
|
|
86
|
-
if (!isHex(forwarder.salt)) {
|
|
87
|
-
throw new TypeError("a salt is 32 bytes of hex");
|
|
88
|
-
}
|
|
89
|
-
group.salts.push(forwarder.salt);
|
|
90
|
-
groups.set(key, group);
|
|
91
|
-
}
|
|
92
|
-
if (chains.size > 1) {
|
|
93
|
-
throw new TypeError("a batch of flush calls is for one chain");
|
|
94
|
-
}
|
|
95
|
-
const calls: Call[] = [];
|
|
96
|
-
for (const { factory, treasury, salts } of groups.values()) {
|
|
97
|
-
for (let start = 0; start < salts.length; start += maxSalts) {
|
|
98
|
-
calls.push(flushTransaction(factory, treasury, salts.slice(start, start + maxSalts), token));
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return calls;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* The Safe Transaction Builder's batch file, its `BatchFile` type
|
|
106
|
-
* (safe-global/safe-react-apps, `apps/tx-builder/src/typings/models.ts` at commit
|
|
107
|
-
* e8cccfb9a1042fa2954087988bae59c3b8c81780).
|
|
108
|
-
*/
|
|
109
|
-
export interface BatchFile {
|
|
110
|
-
version: string;
|
|
111
|
-
chainId: string;
|
|
112
|
-
createdAt: number;
|
|
113
|
-
meta: BatchFileMeta;
|
|
114
|
-
transactions: BatchTransaction[];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export interface BatchFileMeta {
|
|
118
|
-
txBuilderVersion?: string;
|
|
119
|
-
checksum?: string;
|
|
120
|
-
createdFromSafeAddress?: string;
|
|
121
|
-
createdFromOwnerAddress?: string;
|
|
122
|
-
name: string;
|
|
123
|
-
description?: string;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export interface BatchTransaction {
|
|
127
|
-
to: string;
|
|
128
|
-
value: string;
|
|
129
|
-
data?: string;
|
|
130
|
-
contractMethod?: { inputs: unknown[]; name: string; payable: boolean };
|
|
131
|
-
contractInputsValues?: { [key: string]: string };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export interface SafeBatchOptions {
|
|
135
|
-
name?: string;
|
|
136
|
-
description?: string;
|
|
137
|
-
/** Milliseconds; now by default. */
|
|
138
|
-
createdAt?: number;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* A Transaction Builder batch file of `calls` for the Safe `safe` on `chainId`, with the app's
|
|
143
|
-
* `meta.checksum`, so it imports without the "modified since it was generated" warning. A Safe
|
|
144
|
-
* owner imports it in Safe{Wallet} > Apps > Transaction Builder; the owners sign and execute it.
|
|
145
|
-
*/
|
|
146
|
-
export function safeBatch(
|
|
147
|
-
chainId: number,
|
|
148
|
-
safe: string,
|
|
149
|
-
calls: readonly Call[],
|
|
150
|
-
options: SafeBatchOptions = {},
|
|
151
|
-
): BatchFile {
|
|
152
|
-
if (calls.length === 0) {
|
|
153
|
-
throw new TypeError("a batch needs at least one call");
|
|
154
|
-
}
|
|
155
|
-
const batch: BatchFile = {
|
|
156
|
-
version: "1.0",
|
|
157
|
-
chainId: String(chainId),
|
|
158
|
-
createdAt: options.createdAt ?? Date.now(),
|
|
159
|
-
meta: {
|
|
160
|
-
name: options.name ?? "Phala Pay sweep",
|
|
161
|
-
description: options.description ?? "",
|
|
162
|
-
createdFromSafeAddress: getAddress(safe),
|
|
163
|
-
},
|
|
164
|
-
transactions: calls.map((call) => {
|
|
165
|
-
if (!/^\d+$/.test(call.value) || !isHex(call.data)) {
|
|
166
|
-
throw new TypeError("a call needs hex data and a decimal value");
|
|
167
|
-
}
|
|
168
|
-
return { to: getAddress(call.to), value: call.value, data: call.data };
|
|
169
|
-
}),
|
|
170
|
-
};
|
|
171
|
-
batch.meta.checksum = batchChecksum(batch);
|
|
172
|
-
return batch;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* The Transaction Builder's `meta.checksum` (`apps/tx-builder/src/lib/checksum.ts`): Keccak-256
|
|
177
|
-
* of its key-sorted serialization with `meta.name` set to `null`, over the file without its
|
|
178
|
-
* checksum, as the app's `validateChecksum` recomputes it.
|
|
179
|
-
*/
|
|
180
|
-
export function batchChecksum(batch: BatchFile): Hex {
|
|
181
|
-
const meta: Record<string, unknown> = { ...batch.meta, name: null };
|
|
182
|
-
delete meta["checksum"];
|
|
183
|
-
return keccak256(stringToBytes(serialize({ ...batch, meta })));
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** The app's `serializeJSONObject`. */
|
|
187
|
-
function serialize(value: unknown): string {
|
|
188
|
-
if (Array.isArray(value)) {
|
|
189
|
-
return `[${value.map(serialize).join(",")}]`;
|
|
190
|
-
}
|
|
191
|
-
if (typeof value === "object" && value !== null) {
|
|
192
|
-
const record = value as Record<string, unknown>;
|
|
193
|
-
const keys = Object.keys(record).sort();
|
|
194
|
-
return `{${JSON.stringify(keys)}${keys.map((key) => `${serialize(record[key])},`).join("")}}`;
|
|
195
|
-
}
|
|
196
|
-
return JSON.stringify(value === undefined ? null : value);
|
|
197
|
-
}
|
package/src/server/webhook.ts
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Standard Webhooks `v1a` verification of Phala Pay deliveries (design D11): an ed25519 signature
|
|
3
|
-
* over `{webhook-id}.{webhook-timestamp}.{body}` by your account's webhook key in the endpoint's
|
|
4
|
-
* mode, pinned from `GET /v1/attestation`. Uses WebCrypto, so it runs on Node 20+, Deno, Bun, and
|
|
5
|
-
* edge runtimes.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** A delivery that did not verify: answer `400` and do nothing. */
|
|
9
|
-
export class WebhookSignatureError extends Error {
|
|
10
|
-
override readonly name = "WebhookSignatureError";
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** A verified event. `data.object` is the object the event is about, such as a deposit. */
|
|
14
|
-
export interface WebhookEvent {
|
|
15
|
-
/** `evt_…`, stable across retries and resends: process each id once. */
|
|
16
|
-
id: string;
|
|
17
|
-
object: "event";
|
|
18
|
-
/** Your account, `acct_…`. */
|
|
19
|
-
account: string;
|
|
20
|
-
livemode: boolean;
|
|
21
|
-
/** Such as `deposit.credited`; claw back a `deposit.reversed` deposit's credit. */
|
|
22
|
-
type: string;
|
|
23
|
-
created: number;
|
|
24
|
-
/** Who caused it: an API key id (`key_…`), `admin`, or `system`. */
|
|
25
|
-
actor: string;
|
|
26
|
-
/**
|
|
27
|
-
* The API request that caused it, with the `Idempotency-Key` it sent; `null` when the
|
|
28
|
-
* service's own workers did (a payment credited, a quote expired).
|
|
29
|
-
*/
|
|
30
|
-
request: { id: string; idempotency_key: string | null } | null;
|
|
31
|
-
/**
|
|
32
|
-
* `object` is the object as it was when the event happened, never re-rendered; `*.updated`
|
|
33
|
-
* events add `previous_attributes`, the former values of what changed.
|
|
34
|
-
*/
|
|
35
|
-
data: { object: Record<string, unknown>; previous_attributes?: Record<string, unknown> };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface ConstructEventOptions {
|
|
39
|
-
/** Your account id, `acct_…`: an event of another account fails closed. */
|
|
40
|
-
expectedAccount: string;
|
|
41
|
-
/** The mode of the endpoint receiving it: an event of the other mode fails closed. */
|
|
42
|
-
expectedLivemode: boolean;
|
|
43
|
-
/** Finite, non-negative seconds a delivery's timestamp may differ from now; default 300. */
|
|
44
|
-
tolerance?: number;
|
|
45
|
-
/** Finite current time in Unix seconds; for tests. */
|
|
46
|
-
now?: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Verifies a delivery and returns its event, as Stripe's `constructEvent` does, failing closed
|
|
51
|
-
* unless a `v1a` signature verifies with one of `publicKeys` (ed25519 keys in Standard Webhooks'
|
|
52
|
-
* `whpk_` form, as `GET /v1/attestation` lists them; pass both while a rotation overlaps), the
|
|
53
|
-
* timestamp is within tolerance, the body's id is the `webhook-id`, and the event is
|
|
54
|
-
* `expectedAccount`'s in `expectedLivemode`.
|
|
55
|
-
*
|
|
56
|
-
* `payload` is the raw request body, before any JSON parsing.
|
|
57
|
-
*/
|
|
58
|
-
export async function constructEvent(
|
|
59
|
-
payload: string | Uint8Array,
|
|
60
|
-
headers: Headers | Record<string, string | string[] | undefined>,
|
|
61
|
-
publicKeys: string | readonly string[],
|
|
62
|
-
options: ConstructEventOptions,
|
|
63
|
-
): Promise<WebhookEvent> {
|
|
64
|
-
if (options.expectedAccount === "") {
|
|
65
|
-
throw new TypeError("expectedAccount is required");
|
|
66
|
-
}
|
|
67
|
-
const now = options.now ?? Math.floor(Date.now() / 1000);
|
|
68
|
-
const tolerance = options.tolerance ?? 300;
|
|
69
|
-
if (!Number.isFinite(now) || !Number.isFinite(tolerance) || tolerance < 0) {
|
|
70
|
-
throw new TypeError("webhook now must be finite and tolerance finite and non-negative");
|
|
71
|
-
}
|
|
72
|
-
const body = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
|
|
73
|
-
const id = header(headers, "webhook-id");
|
|
74
|
-
const timestamp = header(headers, "webhook-timestamp");
|
|
75
|
-
const signatures = header(headers, "webhook-signature");
|
|
76
|
-
if (id === undefined || timestamp === undefined || signatures === undefined) {
|
|
77
|
-
throw new WebhookSignatureError("webhook headers missing");
|
|
78
|
-
}
|
|
79
|
-
if (!/^\d+$/.test(timestamp) || !Number.isSafeInteger(Number(timestamp))) {
|
|
80
|
-
throw new WebhookSignatureError("webhook timestamp malformed");
|
|
81
|
-
}
|
|
82
|
-
if (Math.abs(now - Number(timestamp)) > tolerance) {
|
|
83
|
-
throw new WebhookSignatureError("webhook timestamp outside tolerance");
|
|
84
|
-
}
|
|
85
|
-
const signed = concat(new TextEncoder().encode(`${id}.${timestamp}.`), body);
|
|
86
|
-
const keys = await Promise.all(
|
|
87
|
-
(typeof publicKeys === "string" ? [publicKeys] : publicKeys).map(importKey),
|
|
88
|
-
);
|
|
89
|
-
if (keys.length === 0) {
|
|
90
|
-
throw new TypeError("no webhook public key pinned");
|
|
91
|
-
}
|
|
92
|
-
let verified = false;
|
|
93
|
-
for (const entry of signatures.split(" ")) {
|
|
94
|
-
const [version, encoded] = entry.split(",", 2);
|
|
95
|
-
const signature = version === "v1a" && encoded !== undefined ? base64(encoded) : undefined;
|
|
96
|
-
if (signature === undefined) {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
for (const key of keys) {
|
|
100
|
-
if (await crypto.subtle.verify("Ed25519", key, signature, signed)) {
|
|
101
|
-
verified = true;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (!verified) {
|
|
106
|
-
throw new WebhookSignatureError("no valid webhook signature");
|
|
107
|
-
}
|
|
108
|
-
let event: unknown;
|
|
109
|
-
try {
|
|
110
|
-
event = JSON.parse(new TextDecoder().decode(body));
|
|
111
|
-
} catch {
|
|
112
|
-
throw new TypeError("webhook body is not an event");
|
|
113
|
-
}
|
|
114
|
-
if (!isRecord(event)) {
|
|
115
|
-
throw new TypeError("webhook body is not an event");
|
|
116
|
-
}
|
|
117
|
-
const e = event;
|
|
118
|
-
const data = e["data"];
|
|
119
|
-
const object: unknown = isRecord(data) ? data["object"] : null;
|
|
120
|
-
const { id: eventId, type, account, livemode, created, actor, request } = e;
|
|
121
|
-
if (
|
|
122
|
-
e["object"] !== "event" ||
|
|
123
|
-
typeof eventId !== "string" ||
|
|
124
|
-
typeof type !== "string" ||
|
|
125
|
-
typeof account !== "string" ||
|
|
126
|
-
typeof livemode !== "boolean" ||
|
|
127
|
-
typeof created !== "number" ||
|
|
128
|
-
!Number.isSafeInteger(created) ||
|
|
129
|
-
!isRecord(object) ||
|
|
130
|
-
typeof actor !== "string" ||
|
|
131
|
-
!(request === null || isRequest(request))
|
|
132
|
-
) {
|
|
133
|
-
throw new TypeError("webhook body is not an event");
|
|
134
|
-
}
|
|
135
|
-
if (eventId !== id) {
|
|
136
|
-
throw new WebhookSignatureError("webhook id does not match the event");
|
|
137
|
-
}
|
|
138
|
-
if (account !== options.expectedAccount) {
|
|
139
|
-
throw new WebhookSignatureError("webhook event is for another account");
|
|
140
|
-
}
|
|
141
|
-
if (livemode !== options.expectedLivemode) {
|
|
142
|
-
throw new WebhookSignatureError("webhook event is for the other mode");
|
|
143
|
-
}
|
|
144
|
-
const previous = isRecord(data) ? data["previous_attributes"] : undefined;
|
|
145
|
-
return {
|
|
146
|
-
id: eventId,
|
|
147
|
-
object: "event",
|
|
148
|
-
account,
|
|
149
|
-
livemode,
|
|
150
|
-
type,
|
|
151
|
-
created,
|
|
152
|
-
actor,
|
|
153
|
-
request,
|
|
154
|
-
data: {
|
|
155
|
-
object,
|
|
156
|
-
...(isRecord(previous) ? { previous_attributes: previous } : {}),
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function header(
|
|
162
|
-
headers: Headers | Record<string, string | string[] | undefined>,
|
|
163
|
-
name: string,
|
|
164
|
-
): string | undefined {
|
|
165
|
-
if (headers instanceof Headers) {
|
|
166
|
-
return headers.get(name)?.trim() ?? undefined;
|
|
167
|
-
}
|
|
168
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
169
|
-
if (key.toLowerCase() === name) {
|
|
170
|
-
return (Array.isArray(value) ? value.join(" ") : value)?.trim();
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return undefined;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
177
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function isRequest(value: unknown): value is { id: string; idempotency_key: string | null } {
|
|
181
|
-
if (!isRecord(value)) {
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
const { id, idempotency_key: key } = value;
|
|
185
|
-
return typeof id === "string" && (key === null || typeof key === "string");
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function importKey(encoded: string): Promise<CryptoKey> {
|
|
189
|
-
const raw = encoded.startsWith("whpk_") ? base64(encoded.slice("whpk_".length)) : undefined;
|
|
190
|
-
if (raw?.length !== 32) {
|
|
191
|
-
throw new TypeError("a webhook public key is whpk_ and the base64 of 32 bytes");
|
|
192
|
-
}
|
|
193
|
-
return crypto.subtle.importKey("raw", raw, "Ed25519", false, ["verify"]);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function base64(value: string): Uint8Array<ArrayBuffer> | undefined {
|
|
197
|
-
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
|
|
198
|
-
return undefined;
|
|
199
|
-
}
|
|
200
|
-
return Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function concat(left: Uint8Array, right: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
204
|
-
const joined = new Uint8Array(left.length + right.length);
|
|
205
|
-
joined.set(left);
|
|
206
|
-
joined.set(right, left.length);
|
|
207
|
-
return joined;
|
|
208
|
-
}
|
package/src/styles.css
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
.pp-root {
|
|
2
|
-
--pp-color-primary: #0f62fe;
|
|
3
|
-
--pp-accessible-color-on-color-primary: #ffffff;
|
|
4
|
-
--pp-color-background: #ffffff;
|
|
5
|
-
--pp-color-text: #1a1a1a;
|
|
6
|
-
--pp-color-text-secondary: #5c5f66;
|
|
7
|
-
--pp-color-border: #d9dce1;
|
|
8
|
-
--pp-color-danger: #c62828;
|
|
9
|
-
--pp-color-success: #1b7f3b;
|
|
10
|
-
--pp-font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
11
|
-
--pp-border-radius: 8px;
|
|
12
|
-
box-sizing: border-box;
|
|
13
|
-
max-width: var(--pp-root-max-width, 440px);
|
|
14
|
-
padding: var(--pp-root-padding, 20px);
|
|
15
|
-
border: var(--pp-root-border, 1px solid var(--pp-color-border));
|
|
16
|
-
border-radius: var(--pp-border-radius);
|
|
17
|
-
background: var(--pp-root-background, var(--pp-color-background));
|
|
18
|
-
color: var(--pp-color-text);
|
|
19
|
-
font-family: var(--pp-font-family);
|
|
20
|
-
font-size: 14px;
|
|
21
|
-
line-height: 1.45;
|
|
22
|
-
}
|
|
23
|
-
.pp-root[data-theme="dark"] {
|
|
24
|
-
--pp-color-primary: #78a9ff;
|
|
25
|
-
--pp-color-background: #161616;
|
|
26
|
-
--pp-color-text: #f4f4f4;
|
|
27
|
-
--pp-color-text-secondary: #a8a8a8;
|
|
28
|
-
--pp-color-border: #393939;
|
|
29
|
-
--pp-color-danger: #ff8389;
|
|
30
|
-
--pp-color-success: #42be65;
|
|
31
|
-
}
|
|
32
|
-
.pp-root *, .pp-root *::before, .pp-root *::after { box-sizing: inherit; }
|
|
33
|
-
.pp-amount { margin: 0; overflow-wrap: anywhere; font-size: 22px; font-weight: 600; }
|
|
34
|
-
.pp-subtitle { margin: 2px 0 12px; color: var(--pp-color-text-secondary); }
|
|
35
|
-
.pp-badge { font-weight: 600; }
|
|
36
|
-
.pp-payments { list-style: none; margin: 12px 0 0; padding: 0; }
|
|
37
|
-
.pp-payments li { margin: 4px 0; }
|
|
38
|
-
.pp-status { display: flex; gap: 8px; align-items: baseline; justify-content: space-between;
|
|
39
|
-
margin-bottom: 10px; padding: 8px 10px; border-radius: var(--pp-border-radius);
|
|
40
|
-
border: 1px solid var(--pp-color-border); }
|
|
41
|
-
.pp-status[data-tone="success"] { border-color: var(--pp-color-success); color: var(--pp-color-success); }
|
|
42
|
-
.pp-status[data-tone="danger"] { border-color: var(--pp-color-danger); color: var(--pp-color-danger); }
|
|
43
|
-
.pp-countdown { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
44
|
-
.pp-notice { margin: 0 0 12px; color: var(--pp-color-text-secondary); font-size: 13px; }
|
|
45
|
-
.pp-notice strong { color: var(--pp-color-text); }
|
|
46
|
-
.pp-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--pp-color-border); margin-bottom: 12px; }
|
|
47
|
-
.pp-tab { appearance: none; border: 0; border-bottom: 2px solid transparent; background: none;
|
|
48
|
-
color: var(--pp-color-text-secondary); font: inherit; min-height: 44px; white-space: nowrap; padding: 8px 10px; cursor: pointer; }
|
|
49
|
-
.pp-tab[aria-selected="true"] { color: var(--pp-color-text); border-bottom-color: var(--pp-color-primary); font-weight: 600; }
|
|
50
|
-
.pp-root button:focus-visible, .pp-root a:focus-visible { outline: 2px solid var(--pp-color-primary); outline-offset: 2px; }
|
|
51
|
-
.pp-wallets { display: grid; gap: 8px; }
|
|
52
|
-
.pp-button { display: flex; gap: 10px; align-items: center; justify-content: center; width: 100%;
|
|
53
|
-
appearance: none; border: 1px solid var(--pp-color-primary); border-radius: var(--pp-border-radius);
|
|
54
|
-
background: var(--pp-color-primary); color: var(--pp-accessible-color-on-color-primary); font: inherit; font-weight: 600; min-height: 44px; padding: 10px 14px; cursor: pointer; }
|
|
55
|
-
.pp-button:disabled { opacity: 0.6; cursor: progress; }
|
|
56
|
-
.pp-button img { width: 20px; height: 20px; }
|
|
57
|
-
.pp-wallet-name { font-weight: 400; opacity: 0.85; }
|
|
58
|
-
.pp-message { margin: 8px 0 0; color: var(--pp-color-text-secondary); }
|
|
59
|
-
.pp-message:empty { display: none; }
|
|
60
|
-
.pp-message[data-tone="danger"] { color: var(--pp-color-danger); }
|
|
61
|
-
.pp-qr-panel { display: grid; justify-items: center; gap: 8px; text-align: center; }
|
|
62
|
-
.pp-qr { border-radius: 4px; }
|
|
63
|
-
.pp-fields { display: grid; gap: 8px; margin: 0; }
|
|
64
|
-
.pp-field dt { color: var(--pp-color-text-secondary); font-size: 12px; }
|
|
65
|
-
.pp-field dd { display: flex; gap: 8px; align-items: center; margin: 2px 0 0; }
|
|
66
|
-
.pp-value { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }
|
|
67
|
-
.pp-copy { flex: none; margin-inline-start: auto; appearance: none; border: 1px solid var(--pp-color-border); border-radius: 6px;
|
|
68
|
-
background: none; color: var(--pp-color-text); font: inherit; font-size: 12px; min-height: 44px; min-width: 44px; padding: 3px 8px; cursor: pointer; }
|
|
69
|
-
.pp-tx { margin: 0 0 12px; font-size: 13px; }
|
|
70
|
-
.pp-tx a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
|
71
|
-
.pp-tx .pp-value { overflow-wrap: anywhere; }
|
|
72
|
-
|
|
73
|
-
.pp-choices { display: flex; flex-wrap: wrap; gap: 8px; border: 0; padding: 0; margin: 0 0 12px; }
|
|
74
|
-
.pp-choices legend { padding: 0; margin-bottom: 6px; font-weight: 600; }
|
|
75
|
-
.pp-choices label { display: flex; align-items: center; gap: 4px; min-height: 44px; cursor: pointer; }
|
|
76
|
-
.pp-choices input { margin: 0; accent-color: var(--pp-color-primary); }
|
|
77
|
-
.pp-choices input:focus-visible { outline: 2px solid var(--pp-color-primary); outline-offset: 2px; }
|
|
78
|
-
|
|
79
|
-
.pp-icon-label { display: inline-flex; align-items: center; gap: 4px; vertical-align: middle; white-space: nowrap; }
|
|
80
|
-
.pp-icon { display: inline-block; flex: none; vertical-align: middle; }
|