@phala/pay 0.2.0 → 0.4.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.
@@ -1 +1 @@
1
- {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/server/webhook.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,mEAAmE;AACnE,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,SAAkB,IAAI,2BAA2B;CAClD;AAED,2FAA2F;AAC3F,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAChE;;;OAGG;IACH,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;CAC1F;AAED,MAAM,WAAW,qBAAqB;IACpC,2EAA2E;IAC3E,eAAe,EAAE,MAAM,CAAC;IACxB,sFAAsF;IACtF,gBAAgB,EAAE,OAAO,CAAC;IAC1B,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,GAAG,UAAU,EAC5B,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EAChE,UAAU,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACtC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,YAAY,CAAC,CA8FvB"}
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/server/webhook.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,mEAAmE;AACnE,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,SAAkB,IAAI,2BAA2B;CAClD;AAED,2FAA2F;AAC3F,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAC/D;;;OAGG;IACH,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;CAC1F;AAED,MAAM,WAAW,qBAAqB;IACpC,2EAA2E;IAC3E,eAAe,EAAE,MAAM,CAAC;IACxB,sFAAsF;IACtF,gBAAgB,EAAE,OAAO,CAAC;IAC1B,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,GAAG,UAAU,EAC5B,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EAChE,UAAU,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACtC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,YAAY,CAAC,CAgGvB"}
@@ -10,9 +10,10 @@ export class WebhookSignatureError extends Error {
10
10
  }
11
11
  /**
12
12
  * Verifies a delivery and returns its event, as Stripe's `constructEvent` does, failing closed
13
- * unless a `v1a` signature verifies with one of `publicKeys` (hex or base64 raw ed25519 keys; pass
14
- * both while a rotation overlaps), the timestamp is within tolerance, the body's id is the
15
- * `webhook-id`, and the event is `expectedAccount`'s in `expectedLivemode`.
13
+ * unless a `v1a` signature verifies with one of `publicKeys` (ed25519 keys in Standard Webhooks'
14
+ * `whpk_` form, as `GET /v1/attestation` lists them; pass both while a rotation overlaps), the
15
+ * timestamp is within tolerance, the body's id is the `webhook-id`, and the event is
16
+ * `expectedAccount`'s in `expectedLivemode`.
16
17
  *
17
18
  * `payload` is the raw request body, before any JSON parsing.
18
19
  */
@@ -68,7 +69,7 @@ export async function constructEvent(payload, headers, publicKeys, options) {
68
69
  const e = event;
69
70
  const data = e["data"];
70
71
  const object = typeof data === "object" && data !== null ? data["object"] : null;
71
- const { id: eventId, type, account, livemode, created, actor } = e;
72
+ const { id: eventId, type, account, livemode, created, actor, request } = e;
72
73
  if (e["object"] !== "event" ||
73
74
  typeof eventId !== "string" ||
74
75
  typeof type !== "string" ||
@@ -78,7 +79,8 @@ export async function constructEvent(payload, headers, publicKeys, options) {
78
79
  !Number.isSafeInteger(created) ||
79
80
  typeof object !== "object" ||
80
81
  object === null ||
81
- !(actor === undefined || typeof actor === "string")) {
82
+ typeof actor !== "string" ||
83
+ !(request === null || isRequest(request))) {
82
84
  throw new TypeError("webhook body is not an event");
83
85
  }
84
86
  if (eventId !== id) {
@@ -98,7 +100,8 @@ export async function constructEvent(payload, headers, publicKeys, options) {
98
100
  livemode,
99
101
  type,
100
102
  created,
101
- ...(actor === undefined ? {} : { actor }),
103
+ actor,
104
+ request,
102
105
  data: {
103
106
  object: object,
104
107
  ...(typeof previous === "object" && previous !== null
@@ -118,21 +121,20 @@ function header(headers, name) {
118
121
  }
119
122
  return undefined;
120
123
  }
124
+ function isRequest(value) {
125
+ if (typeof value !== "object" || value === null) {
126
+ return false;
127
+ }
128
+ const { id, idempotency_key: key } = value;
129
+ return typeof id === "string" && (key === null || typeof key === "string");
130
+ }
121
131
  async function importKey(encoded) {
122
- const value = encoded.trim().replace(/^0x/, "");
123
- const raw = /^[0-9a-fA-F]{64}$/.test(value) ? hex(value) : base64(value);
132
+ const raw = encoded.startsWith("whpk_") ? base64(encoded.slice("whpk_".length)) : undefined;
124
133
  if (raw?.length !== 32) {
125
- throw new TypeError("a webhook public key is 32 bytes of hex or base64");
134
+ throw new TypeError("a webhook public key is whpk_ and the base64 of 32 bytes");
126
135
  }
127
136
  return crypto.subtle.importKey("raw", raw, "Ed25519", false, ["verify"]);
128
137
  }
129
- function hex(value) {
130
- const bytes = new Uint8Array(value.length / 2);
131
- for (let index = 0; index < bytes.length; index += 1) {
132
- bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
133
- }
134
- return bytes;
135
- }
136
138
  function base64(value) {
137
139
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
138
140
  return undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/server/webhook.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/server/webhook.ts"],"names":[],"mappings":"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"}
package/dist/wallet.d.ts CHANGED
@@ -27,7 +27,7 @@ export declare const INJECTED_WALLET_UUID = "injected";
27
27
  * itself. Returns the function that stops listening.
28
28
  */
29
29
  export declare function watchWallets(onChange: (wallets: Wallet[]) => void): () => void;
30
- export type WalletErrorCode = "rejected" | "no_account" | "wrong_chain" | "failed";
30
+ export type WalletErrorCode = "rejected" | "no_account" | "wrong_chain" | "insufficient_balance" | "failed";
31
31
  export declare class WalletError extends Error {
32
32
  readonly code: WalletErrorCode;
33
33
  readonly name = "WalletError";
@@ -37,8 +37,10 @@ export declare class WalletError extends Error {
37
37
  * Pays a quote from a wallet: a viem `WalletClient` (for example wagmi's `useWalletClient()`), or an
38
38
  * EIP-1193 provider such as a discovered `Wallet`'s. Connects (unless the client already has an
39
39
  * account), switches to (or adds) the quote's chain, and sends the ERC-20 `transfer` that the quote's
40
- * `payment_uri` states. Resolves with the transaction hash once the wallet has broadcast it; the
41
- * checkout's status follows the payment from there.
40
+ * `payment_uri` states. Before sending, it reads the account's token balance and, when it is below
41
+ * the quote's amount, sends nothing and throws `WalletError` `insufficient_balance`: the transfer
42
+ * would revert and still cost gas. Resolves with the transaction hash once the wallet has broadcast
43
+ * it; the checkout's status follows the payment from there.
42
44
  */
43
45
  export declare function payWithWallet(wallet: WalletClient | EthereumProvider, quote: ClientQuote): Promise<Hash>;
44
46
  //# sourceMappingURL=wallet.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wallet.d.ts","sourceRoot":"","sources":["../src/wallet.ts"],"names":[],"mappings":"AACA,OAAO,EAOL,KAAK,IAAI,EACT,KAAK,YAAY,EAClB,MAAM,MAAM,CAAC;AAGd,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,8BAA8B;AAC9B,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,2FAA2F;AAC3F,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAED,iGAAiG;AACjG,eAAO,MAAM,oBAAoB,aAAa,CAAC;AAE/C;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAa9E;AA8BD,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,YAAY,GAAG,aAAa,GAAG,QAAQ,CAAC;AAEnF,qBAAa,WAAY,SAAQ,KAAK;IAIlC,QAAQ,CAAC,IAAI,EAAE,eAAe;IAHhC,SAAkB,IAAI,iBAAiB;gBAG5B,IAAI,EAAE,eAAe,EAC9B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY;CAIzB;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,YAAY,GAAG,gBAAgB,EACvC,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,IAAI,CAAC,CA0Bf"}
1
+ {"version":3,"file":"wallet.d.ts","sourceRoot":"","sources":["../src/wallet.ts"],"names":[],"mappings":"AACA,OAAO,EAQL,KAAK,IAAI,EACT,KAAK,YAAY,EAClB,MAAM,MAAM,CAAC;AAKd,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,8BAA8B;AAC9B,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,2FAA2F;AAC3F,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAED,iGAAiG;AACjG,eAAO,MAAM,oBAAoB,aAAa,CAAC;AAE/C;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAa9E;AA8BD,MAAM,MAAM,eAAe,GACvB,UAAU,GACV,YAAY,GACZ,aAAa,GACb,sBAAsB,GACtB,QAAQ,CAAC;AAEb,qBAAa,WAAY,SAAQ,KAAK;IAIlC,QAAQ,CAAC,IAAI,EAAE,eAAe;IAHhC,SAAkB,IAAI,iBAAiB;gBAG5B,IAAI,EAAE,eAAe,EAC9B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY;CAIzB;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,YAAY,GAAG,gBAAgB,EACvC,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,IAAI,CAAC,CAoCf"}
package/dist/wallet.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { createStore } from "mipd";
2
2
  import { BaseError, createWalletClient, custom, erc20Abi, SwitchChainError, UserRejectedRequestError, } from "viem";
3
+ import { readContract } from "viem/actions";
3
4
  import { knownChain, networkName } from "./chains.js";
5
+ import { formatTokenAmount, formatUnitsGrouped } from "./format.js";
4
6
  import { quoteTransfer } from "./payment.js";
5
7
  /** The legacy `window.ethereum` provider, offered when no wallet announces itself (EIP-6963). */
6
8
  export const INJECTED_WALLET_UUID = "injected";
@@ -59,8 +61,10 @@ export class WalletError extends Error {
59
61
  * Pays a quote from a wallet: a viem `WalletClient` (for example wagmi's `useWalletClient()`), or an
60
62
  * EIP-1193 provider such as a discovered `Wallet`'s. Connects (unless the client already has an
61
63
  * account), switches to (or adds) the quote's chain, and sends the ERC-20 `transfer` that the quote's
62
- * `payment_uri` states. Resolves with the transaction hash once the wallet has broadcast it; the
63
- * checkout's status follows the payment from there.
64
+ * `payment_uri` states. Before sending, it reads the account's token balance and, when it is below
65
+ * the quote's amount, sends nothing and throws `WalletError` `insufficient_balance`: the transfer
66
+ * would revert and still cost gas. Resolves with the transaction hash once the wallet has broadcast
67
+ * it; the checkout's status follows the payment from there.
64
68
  */
65
69
  export async function payWithWallet(wallet, quote) {
66
70
  const transfer = quoteTransfer(quote);
@@ -71,6 +75,13 @@ export async function payWithWallet(wallet, quote) {
71
75
  throw new WalletError("no_account", "The wallet shared no account");
72
76
  }
73
77
  await ensureChain(client, transfer.chainId);
78
+ const address = typeof account === "string" ? account : account.address;
79
+ const balance = await tokenBalance(client, transfer.token, address);
80
+ if (balance !== undefined && balance < transfer.amount) {
81
+ const symbol = quote.asset.toUpperCase();
82
+ throw new WalletError("insufficient_balance", `Your wallet holds ${formatUnitsGrouped(balance, quote.decimals)} ${symbol}, less than the ` +
83
+ `${formatTokenAmount(quote)} ${symbol} to pay. Nothing was sent.`);
84
+ }
74
85
  return await client.writeContract({
75
86
  account,
76
87
  chain: knownChain(transfer.chainId) ?? null,
@@ -90,6 +101,19 @@ export async function payWithWallet(wallet, quote) {
90
101
  throw new WalletError("failed", "The wallet could not send the payment", { cause: error });
91
102
  }
92
103
  }
104
+ /**
105
+ * The account's balance of `token`, read through the wallet on its current chain; `undefined` when
106
+ * the wallet cannot read it. The check only spares the payer a reverting transfer, so a wallet that
107
+ * does not serve `eth_call` still pays.
108
+ */
109
+ async function tokenBalance(wallet, token, owner) {
110
+ try {
111
+ return await readContract(wallet, { address: token, abi: erc20Abi, functionName: "balanceOf", args: [owner] });
112
+ }
113
+ catch {
114
+ return undefined;
115
+ }
116
+ }
93
117
  function isWalletClient(wallet) {
94
118
  return "writeContract" in wallet;
95
119
  }
@@ -1 +1 @@
1
- {"version":3,"file":"wallet.js","sourceRoot":"","sources":["../src/wallet.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wallet.js","sourceRoot":"","sources":["../src/wallet.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phala/pay",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Phala Pay checkout: a framework-agnostic client and React bindings",
5
5
  "keywords": [
6
6
  "crypto",
package/src/checkout.ts CHANGED
@@ -7,8 +7,8 @@ import { parseClientQuote, quoteIdFromClientSecret, type ClientQuote } from "./q
7
7
  * - `waiting` for a payment, until `expires_at`;
8
8
  * - `seen` once a transfer is in a block, `confirming` once it reaches the route's confirmation
9
9
  * (two blocks on Ethereum) and is being valued;
10
- * - `credited` (about 30 seconds after paying), or `rejected` for a payment that will not be
11
- * credited;
10
+ * - `credited` (typically the quote's `typical_credit_seconds` after paying), or `rejected` for a
11
+ * payment that will not be credited;
12
12
  * - `reversed` when a credited payment's transaction left the chain before finality;
13
13
  * - `expired` or `canceled` without a payment;
14
14
  * - `error` when the client secret is not valid, or the quote's address is not the one your
package/src/format.ts CHANGED
@@ -6,7 +6,12 @@ import type { ClientQuote } from "./quote.js";
6
6
  * `1,273.9185`. Every digit is kept: the service rounds a quote to a few decimals.
7
7
  */
8
8
  export function formatTokenAmount(quote: ClientQuote, locale?: string): string {
9
- const [whole = "0", fraction] = tokenAmount(quote).split(".");
9
+ return formatUnitsGrouped(BigInt(quote.amount_atomic), quote.decimals, locale);
10
+ }
11
+
12
+ /** An atomic token amount, grouped for `locale` and without trailing zeros, as `formatTokenAmount`. */
13
+ export function formatUnitsGrouped(atomic: bigint, decimals: number, locale?: string): string {
14
+ const [whole = "0", fraction] = formatUnits(atomic, decimals).split(".");
10
15
  const format = new Intl.NumberFormat(locale);
11
16
  const grouped = format.format(BigInt(whole));
12
17
  if (fraction === undefined) {
@@ -23,10 +28,23 @@ export function tokenAmount(quote: ClientQuote): string {
23
28
 
24
29
  /** The credit in the quote's currency, for example `$25.00`. */
25
30
  export function formatAmount(quote: ClientQuote, locale?: string): string {
31
+ return formatMinorAmount(quote, quote.amount, locale);
32
+ }
33
+
34
+ /** `minor` units of the quote's currency, for example `$10.00` for 1000. */
35
+ export function formatMinorAmount(quote: ClientQuote, minor: number, locale?: string): string {
26
36
  return new Intl.NumberFormat(locale, {
27
37
  style: "currency",
28
38
  currency: quote.currency.toUpperCase(),
29
- }).format(quote.amount / 100);
39
+ }).format(minor / 100);
40
+ }
41
+
42
+ /** A typical wait, for example `about 30 seconds` or `about 15 minutes`. */
43
+ export function formatWait(seconds: number): string {
44
+ if (seconds < 90) {
45
+ return `about ${seconds} second${seconds === 1 ? "" : "s"}`;
46
+ }
47
+ return `about ${Math.round(seconds / 60)} minutes`;
30
48
  }
31
49
 
32
50
  /** `mm:ss` (or `h:mm:ss`) until `expiresAt` Unix seconds, never negative. */
package/src/quote.ts CHANGED
@@ -8,7 +8,7 @@ export interface ClientQuote {
8
8
  livemode: boolean;
9
9
  /** `open` until a matching payment completes it, it expires by chain time, or it is canceled. */
10
10
  status: "open" | "complete" | "expired" | "canceled";
11
- /** Credit in the currency's minor unit (US cents). */
11
+ /** The quoted credit in the currency's minor unit (US cents). */
12
12
  amount: number;
13
13
  currency: string;
14
14
  /** The token's code, for example `pha`. */
@@ -25,13 +25,24 @@ export interface ClientQuote {
25
25
  expires_at: number;
26
26
  /**
27
27
  * `seen` once in a block (a reorg can remove it), `confirming` while a payment at the route's
28
- * confirmation is valued and screened, `credited` once it is credited (about 30 seconds after
29
- * paying), `rejected` when it will not be, `reversed` when a credited payment's transaction
30
- * left the chain before finality (the payment did not happen).
28
+ * confirmation is valued and screened, `credited` once it is credited (typically
29
+ * `typical_credit_seconds` after paying), `rejected` when it will not be, `reversed` when a
30
+ * credited payment's transaction left the chain before finality (the payment did not happen).
31
31
  */
32
32
  payment_status: "none" | "seen" | "confirming" | "credited" | "rejected" | "reversed";
33
33
  /** Block confirmations while `payment_status` is `seen`, otherwise `null`. */
34
34
  confirmations: number | null;
35
+ /**
36
+ * While `payment_status` is `credited`: what was credited, in the currency's minor unit. It
37
+ * differs from `amount` for a payment valued at the market price (another amount, or paid late);
38
+ * otherwise `null`.
39
+ */
40
+ amount_credited: number | null;
41
+ /**
42
+ * Typical seconds from paying to the credit at the confirmation this quote's payments need:
43
+ * 30 on Ethereum (two blocks), about 300 at an OP-stack chain's `safe` block, 900 at finality.
44
+ */
45
+ typical_credit_seconds: number;
35
46
  }
36
47
 
37
48
  const QUOTE_STATUSES = ["open", "complete", "expired", "canceled"] as const;
@@ -53,6 +64,7 @@ export function parseClientQuote(value: unknown): ClientQuote {
53
64
  }
54
65
  const v = value as Record<string, unknown>;
55
66
  const confirmations = v["confirmations"];
67
+ const amountCredited = v["amount_credited"];
56
68
  if (
57
69
  typeof v["id"] !== "string" ||
58
70
  v["object"] !== "quote" ||
@@ -70,7 +82,9 @@ export function parseClientQuote(value: unknown): ClientQuote {
70
82
  typeof v["payment_uri"] !== "string" ||
71
83
  !isSafeInteger(v["expires_at"]) ||
72
84
  !oneOf(v["payment_status"], PAYMENT_STATUSES) ||
73
- !(confirmations === null || confirmations === undefined || isSafeInteger(confirmations))
85
+ !(confirmations === null || isSafeInteger(confirmations)) ||
86
+ !(amountCredited === null || isSafeInteger(amountCredited)) ||
87
+ !isSafeInteger(v["typical_credit_seconds"])
74
88
  ) {
75
89
  throw new TypeError("quote response does not match the public quote view");
76
90
  }
@@ -89,7 +103,9 @@ export function parseClientQuote(value: unknown): ClientQuote {
89
103
  payment_uri: v["payment_uri"],
90
104
  expires_at: v["expires_at"],
91
105
  payment_status: v["payment_status"],
92
- confirmations: confirmations ?? null,
106
+ confirmations,
107
+ amount_credited: amountCredited,
108
+ typical_credit_seconds: v["typical_credit_seconds"],
93
109
  };
94
110
  }
95
111
 
@@ -4,7 +4,14 @@ import { useEffect, useId, useRef, useState, type KeyboardEvent } from "react";
4
4
  import type { Hash, WalletClient } from "viem";
5
5
  import type { CheckoutErrorCode, CheckoutState, CheckoutStatus } from "../checkout.js";
6
6
  import { networkName, transactionUrl } from "../chains.js";
7
- import { formatAmount, formatCountdown, formatTokenAmount, tokenAmount } from "../format.js";
7
+ import {
8
+ formatAmount,
9
+ formatCountdown,
10
+ formatMinorAmount,
11
+ formatTokenAmount,
12
+ formatWait,
13
+ tokenAmount,
14
+ } from "../format.js";
8
15
  import { quoteTransfer } from "../payment.js";
9
16
  import type { ClientQuote } from "../quote.js";
10
17
  import {
@@ -30,13 +37,20 @@ export interface CheckoutProps {
30
37
  expectedAddress: string;
31
38
  /** The service origin, for example `https://topup.example.com`. */
32
39
  apiBase: string;
33
- /** Called once when the payment is credited. Fulfil from the `deposit.credited` webhook, not here. */
40
+ /** Called once when the payment is credited, with the quote: its `amount_credited` is what was
41
+ * credited, which differs from `amount` for a payment of another amount or a late one. Fulfil
42
+ * from the `deposit.credited` webhook, not here. */
34
43
  onSuccess?: (quote: ClientQuote) => void;
35
44
  /** Called once when the quote expires or is canceled without a payment. */
36
45
  onExpire?: (quote: ClientQuote) => void;
37
46
  /** Called whenever the checkout's status changes, like Stripe Elements' `onChange`: for example,
38
47
  * to hide your own "new payment" control while a payment is `seen` or `confirming`. */
39
48
  onChange?: (state: CheckoutState) => void;
49
+ /** Called when the wallet tab could not send the payment, with the `WalletError` and the wallet
50
+ * it tried (the chosen browser wallet's provider, or `walletClient`): for example on
51
+ * `insufficient_balance` (nothing was sent), to offer your own way to fund that wallet. The
52
+ * checkout shows the error's message itself. */
53
+ onWalletError?: (error: WalletError, wallet: WalletClient | EthereumProvider) => void;
40
54
  appearance?: Appearance;
41
55
  /** Milliseconds between status reads; default 3000. */
42
56
  pollInterval?: number;
@@ -64,6 +78,7 @@ export function Checkout({
64
78
  onSuccess,
65
79
  onExpire,
66
80
  onChange,
81
+ onWalletError,
67
82
  appearance,
68
83
  pollInterval,
69
84
  className,
@@ -133,6 +148,7 @@ export function Checkout({
133
148
  setTxHash(hash);
134
149
  refresh();
135
150
  }}
151
+ onWalletError={onWalletError}
136
152
  />
137
153
  )}
138
154
  </div>
@@ -182,13 +198,11 @@ function statusMessage(
182
198
  case "waiting":
183
199
  return "Waiting for your payment";
184
200
  case "seen":
185
- return quote?.confirmations == null
186
- ? "Payment received, crediting in about 30 seconds"
187
- : `Payment received, ${quote.confirmations} confirmation${quote.confirmations === 1 ? "" : "s"}. Crediting in about 30 seconds`;
201
+ return seenMessage(quote);
188
202
  case "confirming":
189
203
  return "Payment confirmed on chain, crediting…";
190
204
  case "credited":
191
- return quote === null ? "Payment credited" : `Payment credited: ${formatAmount(quote)}`;
205
+ return creditedMessage(quote);
192
206
  case "rejected":
193
207
  return "This payment cannot be credited. Contact support with your transaction.";
194
208
  case "reversed":
@@ -204,6 +218,31 @@ function statusMessage(
204
218
  }
205
219
  }
206
220
 
221
+ function seenMessage(quote: ClientQuote | null): string {
222
+ if (quote === null) {
223
+ return "Payment received, crediting once it is confirmed";
224
+ }
225
+ const wait = formatWait(quote.typical_credit_seconds);
226
+ return quote.confirmations === null
227
+ ? `Payment received, crediting in ${wait}`
228
+ : `Payment received, ${quote.confirmations} confirmation${quote.confirmations === 1 ? "" : "s"}. Crediting in ${wait}`;
229
+ }
230
+
231
+ /** What was credited and, when the payment was valued at the market price, the quote it missed. */
232
+ function creditedMessage(quote: ClientQuote | null): string {
233
+ if (quote?.amount_credited == null) {
234
+ return "Payment credited";
235
+ }
236
+ const credited = formatMinorAmount(quote, quote.amount_credited);
237
+ if (quote.amount_credited < quote.amount) {
238
+ return `Payment credited: ${credited} of ${formatAmount(quote)}`;
239
+ }
240
+ if (quote.amount_credited > quote.amount) {
241
+ return `Payment credited: ${credited}, more than the ${formatAmount(quote)} quoted`;
242
+ }
243
+ return `Payment credited: ${credited}`;
244
+ }
245
+
207
246
  function Transaction({ hash, chainId }: { hash: Hash; chainId: number }) {
208
247
  const url = transactionUrl(chainId, hash);
209
248
  return (
@@ -226,12 +265,14 @@ function PaymentOptions({
226
265
  buttonText,
227
266
  walletClient,
228
267
  onSent,
268
+ onWalletError,
229
269
  }: {
230
270
  quote: ClientQuote;
231
271
  now: number;
232
272
  buttonText: string;
233
273
  walletClient: WalletClient | undefined;
234
274
  onSent: (hash: Hash) => void;
275
+ onWalletError: CheckoutProps["onWalletError"];
235
276
  }) {
236
277
  const [method, setMethod] = useState<Method>("wallet");
237
278
  const id = useId();
@@ -298,6 +339,7 @@ function PaymentOptions({
298
339
  buttonText={buttonText}
299
340
  walletClient={walletClient}
300
341
  onSent={onSent}
342
+ onWalletError={onWalletError}
301
343
  />
302
344
  )}
303
345
  {method === "qr" && (
@@ -325,11 +367,13 @@ function WalletPanel({
325
367
  buttonText,
326
368
  walletClient,
327
369
  onSent,
370
+ onWalletError,
328
371
  }: {
329
372
  quote: ClientQuote;
330
373
  buttonText: string;
331
374
  walletClient: WalletClient | undefined;
332
375
  onSent: (hash: Hash) => void;
376
+ onWalletError: CheckoutProps["onWalletError"];
333
377
  }) {
334
378
  const [discovered, setDiscovered] = useState<Wallet[]>([]);
335
379
  const [step, setStep] = useState<WalletStep>({ kind: "idle" });
@@ -352,6 +396,9 @@ function WalletPanel({
352
396
  kind: "failed",
353
397
  message: error instanceof WalletError ? error.message : "The payment could not be sent",
354
398
  });
399
+ if (error instanceof WalletError) {
400
+ onWalletError?.(error, wallet);
401
+ }
355
402
  }
356
403
  };
357
404
 
@@ -22,12 +22,12 @@ export interface WebhookEvent {
22
22
  type: string;
23
23
  created: number;
24
24
  /** Who caused it: an API key id (`key_…`), `admin`, or `system`. */
25
- actor?: string;
25
+ actor: string;
26
26
  /**
27
27
  * The API request that caused it, with the `Idempotency-Key` it sent; `null` when the
28
28
  * service's own workers did (a payment credited, a quote expired).
29
29
  */
30
- request?: { id: string; idempotency_key: string | null } | null;
30
+ request: { id: string; idempotency_key: string | null } | null;
31
31
  /**
32
32
  * `object` is the object as it was when the event happened, never re-rendered; `*.updated`
33
33
  * events add `previous_attributes`, the former values of what changed.
@@ -48,9 +48,10 @@ export interface ConstructEventOptions {
48
48
 
49
49
  /**
50
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` (hex or base64 raw ed25519 keys; pass
52
- * both while a rotation overlaps), the timestamp is within tolerance, the body's id is the
53
- * `webhook-id`, and the event is `expectedAccount`'s in `expectedLivemode`.
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`.
54
55
  *
55
56
  * `payload` is the raw request body, before any JSON parsing.
56
57
  */
@@ -113,7 +114,7 @@ export async function constructEvent(
113
114
  const data = e["data"];
114
115
  const object: unknown =
115
116
  typeof data === "object" && data !== null ? (data as Record<string, unknown>)["object"] : null;
116
- const { id: eventId, type, account, livemode, created, actor } = e;
117
+ const { id: eventId, type, account, livemode, created, actor, request } = e;
117
118
  if (
118
119
  e["object"] !== "event" ||
119
120
  typeof eventId !== "string" ||
@@ -124,7 +125,8 @@ export async function constructEvent(
124
125
  !Number.isSafeInteger(created) ||
125
126
  typeof object !== "object" ||
126
127
  object === null ||
127
- !(actor === undefined || typeof actor === "string")
128
+ typeof actor !== "string" ||
129
+ !(request === null || isRequest(request))
128
130
  ) {
129
131
  throw new TypeError("webhook body is not an event");
130
132
  }
@@ -145,7 +147,8 @@ export async function constructEvent(
145
147
  livemode,
146
148
  type,
147
149
  created,
148
- ...(actor === undefined ? {} : { actor }),
150
+ actor,
151
+ request,
149
152
  data: {
150
153
  object: object as Record<string, unknown>,
151
154
  ...(typeof previous === "object" && previous !== null
@@ -170,21 +173,20 @@ function header(
170
173
  return undefined;
171
174
  }
172
175
 
173
- async function importKey(encoded: string): Promise<CryptoKey> {
174
- const value = encoded.trim().replace(/^0x/, "");
175
- const raw = /^[0-9a-fA-F]{64}$/.test(value) ? hex(value) : base64(value);
176
- if (raw?.length !== 32) {
177
- throw new TypeError("a webhook public key is 32 bytes of hex or base64");
176
+ function isRequest(value: unknown): value is { id: string; idempotency_key: string | null } {
177
+ if (typeof value !== "object" || value === null) {
178
+ return false;
178
179
  }
179
- return crypto.subtle.importKey("raw", raw, "Ed25519", false, ["verify"]);
180
+ const { id, idempotency_key: key } = value as Record<string, unknown>;
181
+ return typeof id === "string" && (key === null || typeof key === "string");
180
182
  }
181
183
 
182
- function hex(value: string): Uint8Array<ArrayBuffer> {
183
- const bytes = new Uint8Array(value.length / 2);
184
- for (let index = 0; index < bytes.length; index += 1) {
185
- bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
184
+ async function importKey(encoded: string): Promise<CryptoKey> {
185
+ const raw = encoded.startsWith("whpk_") ? base64(encoded.slice("whpk_".length)) : undefined;
186
+ if (raw?.length !== 32) {
187
+ throw new TypeError("a webhook public key is whpk_ and the base64 of 32 bytes");
186
188
  }
187
- return bytes;
189
+ return crypto.subtle.importKey("raw", raw, "Ed25519", false, ["verify"]);
188
190
  }
189
191
 
190
192
  function base64(value: string): Uint8Array<ArrayBuffer> | undefined {