@phala/pay 0.1.1 → 0.2.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +100 -1
  2. package/README.md +133 -28
  3. package/dist/checkout.d.ts +37 -7
  4. package/dist/checkout.d.ts.map +1 -1
  5. package/dist/checkout.js +55 -11
  6. package/dist/checkout.js.map +1 -1
  7. package/dist/client.d.ts +8 -7
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +14 -8
  10. package/dist/client.js.map +1 -1
  11. package/dist/deposit-address.d.ts +47 -0
  12. package/dist/deposit-address.d.ts.map +1 -0
  13. package/dist/deposit-address.js +88 -0
  14. package/dist/deposit-address.js.map +1 -0
  15. package/dist/index.d.ts +3 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/payment.d.ts +43 -1
  20. package/dist/payment.d.ts.map +1 -1
  21. package/dist/payment.js +32 -10
  22. package/dist/payment.js.map +1 -1
  23. package/dist/quote.d.ts +7 -3
  24. package/dist/quote.d.ts.map +1 -1
  25. package/dist/quote.js +3 -1
  26. package/dist/quote.js.map +1 -1
  27. package/dist/react/Checkout.d.ts +15 -1
  28. package/dist/react/Checkout.d.ts.map +1 -1
  29. package/dist/react/Checkout.js +36 -39
  30. package/dist/react/Checkout.js.map +1 -1
  31. package/dist/react/DepositAddress.d.ts +31 -0
  32. package/dist/react/DepositAddress.d.ts.map +1 -0
  33. package/dist/react/DepositAddress.js +108 -0
  34. package/dist/react/DepositAddress.js.map +1 -0
  35. package/dist/react/Field.d.ts +7 -0
  36. package/dist/react/Field.d.ts.map +1 -0
  37. package/dist/react/Field.js +21 -0
  38. package/dist/react/Field.js.map +1 -0
  39. package/dist/react/appearance.d.ts +1 -1
  40. package/dist/react/appearance.d.ts.map +1 -1
  41. package/dist/react/appearance.js +4 -1
  42. package/dist/react/appearance.js.map +1 -1
  43. package/dist/react/index.d.ts +1 -0
  44. package/dist/react/index.d.ts.map +1 -1
  45. package/dist/react/index.js +1 -0
  46. package/dist/react/index.js.map +1 -1
  47. package/dist/react/useCheckout.d.ts +3 -1
  48. package/dist/react/useCheckout.d.ts.map +1 -1
  49. package/dist/react/useCheckout.js +4 -3
  50. package/dist/react/useCheckout.js.map +1 -1
  51. package/dist/server/addresses.d.ts +78 -0
  52. package/dist/server/addresses.d.ts.map +1 -0
  53. package/dist/server/addresses.js +118 -0
  54. package/dist/server/addresses.js.map +1 -0
  55. package/dist/server/index.d.ts +12 -0
  56. package/dist/server/index.d.ts.map +1 -0
  57. package/dist/server/index.js +12 -0
  58. package/dist/server/index.js.map +1 -0
  59. package/dist/server/sweeps.d.ts +78 -0
  60. package/dist/server/sweeps.d.ts.map +1 -0
  61. package/dist/server/sweeps.js +117 -0
  62. package/dist/server/sweeps.js.map +1 -0
  63. package/dist/server/webhook.d.ts +60 -0
  64. package/dist/server/webhook.d.ts.map +1 -0
  65. package/dist/server/webhook.js +148 -0
  66. package/dist/server/webhook.js.map +1 -0
  67. package/dist/wallet.d.ts +10 -8
  68. package/dist/wallet.d.ts.map +1 -1
  69. package/dist/wallet.js +61 -63
  70. package/dist/wallet.js.map +1 -1
  71. package/package.json +6 -1
  72. package/src/checkout.ts +89 -15
  73. package/src/client.ts +14 -8
  74. package/src/deposit-address.ts +138 -0
  75. package/src/index.ts +19 -1
  76. package/src/payment.ts +72 -10
  77. package/src/quote.ts +10 -4
  78. package/src/react/Checkout.tsx +84 -72
  79. package/src/react/DepositAddress.tsx +213 -0
  80. package/src/react/Field.tsx +46 -0
  81. package/src/react/appearance.ts +4 -1
  82. package/src/react/index.ts +1 -0
  83. package/src/react/useCheckout.ts +6 -2
  84. package/src/server/addresses.ts +223 -0
  85. package/src/server/index.ts +38 -0
  86. package/src/server/sweeps.ts +197 -0
  87. package/src/server/webhook.ts +202 -0
  88. package/src/wallet.ts +74 -68
@@ -0,0 +1,148 @@
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
+ /** A delivery that did not verify: answer `400` and do nothing. */
8
+ export class WebhookSignatureError extends Error {
9
+ name = "WebhookSignatureError";
10
+ }
11
+ /**
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`.
16
+ *
17
+ * `payload` is the raw request body, before any JSON parsing.
18
+ */
19
+ export async function constructEvent(payload, headers, publicKeys, options) {
20
+ if (options.expectedAccount === "") {
21
+ throw new TypeError("expectedAccount is required");
22
+ }
23
+ const body = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
24
+ const id = header(headers, "webhook-id");
25
+ const timestamp = header(headers, "webhook-timestamp");
26
+ const signatures = header(headers, "webhook-signature");
27
+ if (id === undefined || timestamp === undefined || signatures === undefined) {
28
+ throw new WebhookSignatureError("webhook headers missing");
29
+ }
30
+ if (!/^\d+$/.test(timestamp)) {
31
+ throw new WebhookSignatureError("webhook timestamp malformed");
32
+ }
33
+ const now = options.now ?? Math.floor(Date.now() / 1000);
34
+ if (Math.abs(now - Number(timestamp)) > (options.tolerance ?? 300)) {
35
+ throw new WebhookSignatureError("webhook timestamp outside tolerance");
36
+ }
37
+ const signed = concat(new TextEncoder().encode(`${id}.${timestamp}.`), body);
38
+ const keys = await Promise.all((typeof publicKeys === "string" ? [publicKeys] : publicKeys).map(importKey));
39
+ if (keys.length === 0) {
40
+ throw new TypeError("no webhook public key pinned");
41
+ }
42
+ let verified = false;
43
+ for (const entry of signatures.split(" ")) {
44
+ const [version, encoded] = entry.split(",", 2);
45
+ const signature = version === "v1a" && encoded !== undefined ? base64(encoded) : undefined;
46
+ if (signature === undefined) {
47
+ continue;
48
+ }
49
+ for (const key of keys) {
50
+ if (await crypto.subtle.verify("Ed25519", key, signature, signed)) {
51
+ verified = true;
52
+ }
53
+ }
54
+ }
55
+ if (!verified) {
56
+ throw new WebhookSignatureError("no valid webhook signature");
57
+ }
58
+ let event;
59
+ try {
60
+ event = JSON.parse(new TextDecoder().decode(body));
61
+ }
62
+ catch {
63
+ throw new TypeError("webhook body is not an event");
64
+ }
65
+ if (typeof event !== "object" || event === null) {
66
+ throw new TypeError("webhook body is not an event");
67
+ }
68
+ const e = event;
69
+ const data = e["data"];
70
+ const object = typeof data === "object" && data !== null ? data["object"] : null;
71
+ const { id: eventId, type, account, livemode, created, actor } = e;
72
+ if (e["object"] !== "event" ||
73
+ typeof eventId !== "string" ||
74
+ typeof type !== "string" ||
75
+ typeof account !== "string" ||
76
+ typeof livemode !== "boolean" ||
77
+ typeof created !== "number" ||
78
+ !Number.isSafeInteger(created) ||
79
+ typeof object !== "object" ||
80
+ object === null ||
81
+ !(actor === undefined || typeof actor === "string")) {
82
+ throw new TypeError("webhook body is not an event");
83
+ }
84
+ if (eventId !== id) {
85
+ throw new WebhookSignatureError("webhook id does not match the event");
86
+ }
87
+ if (account !== options.expectedAccount) {
88
+ throw new WebhookSignatureError("webhook event is for another account");
89
+ }
90
+ if (livemode !== options.expectedLivemode) {
91
+ throw new WebhookSignatureError("webhook event is for the other mode");
92
+ }
93
+ const previous = data["previous_attributes"];
94
+ return {
95
+ id: eventId,
96
+ object: "event",
97
+ account,
98
+ livemode,
99
+ type,
100
+ created,
101
+ ...(actor === undefined ? {} : { actor }),
102
+ data: {
103
+ object: object,
104
+ ...(typeof previous === "object" && previous !== null
105
+ ? { previous_attributes: previous }
106
+ : {}),
107
+ },
108
+ };
109
+ }
110
+ function header(headers, name) {
111
+ if (headers instanceof Headers) {
112
+ return headers.get(name)?.trim() ?? undefined;
113
+ }
114
+ for (const [key, value] of Object.entries(headers)) {
115
+ if (key.toLowerCase() === name) {
116
+ return (Array.isArray(value) ? value.join(" ") : value)?.trim();
117
+ }
118
+ }
119
+ return undefined;
120
+ }
121
+ 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);
124
+ if (raw?.length !== 32) {
125
+ throw new TypeError("a webhook public key is 32 bytes of hex or base64");
126
+ }
127
+ return crypto.subtle.importKey("raw", raw, "Ed25519", false, ["verify"]);
128
+ }
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
+ function base64(value) {
137
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
138
+ return undefined;
139
+ }
140
+ return Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
141
+ }
142
+ function concat(left, right) {
143
+ const joined = new Uint8Array(left.length + right.length);
144
+ joined.set(left);
145
+ joined.set(right, left.length);
146
+ return joined;
147
+ }
148
+ //# sourceMappingURL=webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/server/webhook.ts"],"names":[],"mappings":"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"}
package/dist/wallet.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Hash } from "viem";
1
+ import { type Hash, type WalletClient } from "viem";
2
2
  import type { ClientQuote } from "./quote.js";
3
3
  /** EIP-6963 provider info. */
4
4
  export interface WalletInfo {
@@ -22,9 +22,9 @@ export interface Wallet {
22
22
  /** The legacy `window.ethereum` provider, offered when no wallet announces itself (EIP-6963). */
23
23
  export declare const INJECTED_WALLET_UUID = "injected";
24
24
  /**
25
- * Discovers browser wallets with EIP-6963, falling back to `window.ethereum`, and calls `onChange`
26
- * with the current list whenever a wallet announces itself. Returns the function that stops
27
- * listening.
25
+ * Discovers browser wallets with EIP-6963 (through mipd's store), falling back to
26
+ * `window.ethereum`, and calls `onChange` with the current list whenever a wallet announces
27
+ * itself. Returns the function that stops listening.
28
28
  */
29
29
  export declare function watchWallets(onChange: (wallets: Wallet[]) => void): () => void;
30
30
  export type WalletErrorCode = "rejected" | "no_account" | "wrong_chain" | "failed";
@@ -34,9 +34,11 @@ export declare class WalletError extends Error {
34
34
  constructor(code: WalletErrorCode, message: string, options?: ErrorOptions);
35
35
  }
36
36
  /**
37
- * Pays a quote from a browser wallet: connects, switches to (or adds) the quote's chain, and sends
38
- * the ERC-20 `transfer` that the quote's `payment_uri` states. Resolves with the transaction hash
39
- * once the wallet has broadcast it; the checkout's status follows the payment from there.
37
+ * Pays a quote from a wallet: a viem `WalletClient` (for example wagmi's `useWalletClient()`), or an
38
+ * EIP-1193 provider such as a discovered `Wallet`'s. Connects (unless the client already has an
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
42
  */
41
- export declare function payWithWallet(provider: EthereumProvider, quote: ClientQuote): Promise<Hash>;
43
+ export declare function payWithWallet(wallet: WalletClient | EthereumProvider, quote: ClientQuote): Promise<Hash>;
42
44
  //# sourceMappingURL=wallet.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wallet.d.ts","sourceRoot":"","sources":["../src/wallet.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,IAAI,EAEV,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;AAM/C;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CA0C9E;AAED,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;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,gBAAgB,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BjG"}
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"}
package/dist/wallet.js CHANGED
@@ -1,53 +1,51 @@
1
- import { createWalletClient, custom, erc20Abi, } from "viem";
1
+ import { createStore } from "mipd";
2
+ import { BaseError, createWalletClient, custom, erc20Abi, SwitchChainError, UserRejectedRequestError, } from "viem";
2
3
  import { knownChain, networkName } from "./chains.js";
3
4
  import { quoteTransfer } from "./payment.js";
4
5
  /** The legacy `window.ethereum` provider, offered when no wallet announces itself (EIP-6963). */
5
6
  export const INJECTED_WALLET_UUID = "injected";
6
7
  /**
7
- * Discovers browser wallets with EIP-6963, falling back to `window.ethereum`, and calls `onChange`
8
- * with the current list whenever a wallet announces itself. Returns the function that stops
9
- * listening.
8
+ * Discovers browser wallets with EIP-6963 (through mipd's store), falling back to
9
+ * `window.ethereum`, and calls `onChange` with the current list whenever a wallet announces
10
+ * itself. Returns the function that stops listening.
10
11
  */
11
12
  export function watchWallets(onChange) {
12
13
  if (typeof window === "undefined") {
13
14
  onChange([]);
14
15
  return () => undefined;
15
16
  }
16
- const announced = new Map();
17
- const emit = () => {
18
- const wallets = [...announced.values()];
19
- const injected = window.ethereum;
20
- if (wallets.length === 0 && injected !== undefined) {
21
- wallets.push({
22
- info: { uuid: INJECTED_WALLET_UUID, name: "Browser wallet", icon: "", rdns: "" },
23
- provider: injected,
24
- });
25
- }
26
- onChange(wallets);
17
+ const store = createStore();
18
+ const unsubscribe = store.subscribe((details) => onChange(walletsFrom(details)), {
19
+ emitImmediately: true,
20
+ });
21
+ return () => {
22
+ unsubscribe();
23
+ store.destroy();
27
24
  };
28
- const onAnnounce = (event) => {
29
- const detail = event.detail;
30
- const info = detail?.info;
31
- if (detail?.provider === undefined ||
32
- typeof info?.uuid !== "string" ||
33
- typeof info.name !== "string") {
34
- return;
25
+ }
26
+ /** Announcements are untrusted page events: keep well-formed ones, and only data-URI icons. */
27
+ function walletsFrom(announcements) {
28
+ const wallets = [];
29
+ for (const { info, provider } of announcements) {
30
+ if (!isProvider(provider) || typeof info?.uuid !== "string" || typeof info.name !== "string") {
31
+ continue;
35
32
  }
36
- announced.set(info.uuid, {
37
- info: {
38
- uuid: info.uuid,
39
- name: info.name,
40
- icon: typeof info.icon === "string" && info.icon.startsWith("data:image/") ? info.icon : "",
41
- rdns: typeof info.rdns === "string" ? info.rdns : "",
42
- },
43
- provider: detail.provider,
33
+ const icon = typeof info.icon === "string" && info.icon.startsWith("data:image/") ? info.icon : "";
34
+ const rdns = typeof info.rdns === "string" ? info.rdns : "";
35
+ wallets.push({ info: { uuid: info.uuid, name: info.name, icon, rdns }, provider });
36
+ }
37
+ // Wallets that predate EIP-6963, such as some in-app mobile browsers, only set `window.ethereum`.
38
+ const injected = window.ethereum;
39
+ if (wallets.length === 0 && injected !== undefined) {
40
+ wallets.push({
41
+ info: { uuid: INJECTED_WALLET_UUID, name: "Browser wallet", icon: "", rdns: "" },
42
+ provider: injected,
44
43
  });
45
- emit();
46
- };
47
- window.addEventListener("eip6963:announceProvider", onAnnounce);
48
- window.dispatchEvent(new Event("eip6963:requestProvider"));
49
- emit();
50
- return () => window.removeEventListener("eip6963:announceProvider", onAnnounce);
44
+ }
45
+ return wallets;
46
+ }
47
+ function isProvider(value) {
48
+ return typeof value?.request === "function";
51
49
  }
52
50
  export class WalletError extends Error {
53
51
  code;
@@ -58,20 +56,22 @@ export class WalletError extends Error {
58
56
  }
59
57
  }
60
58
  /**
61
- * Pays a quote from a browser wallet: connects, switches to (or adds) the quote's chain, and sends
62
- * the ERC-20 `transfer` that the quote's `payment_uri` states. Resolves with the transaction hash
63
- * once the wallet has broadcast it; the checkout's status follows the payment from there.
59
+ * Pays a quote from a wallet: a viem `WalletClient` (for example wagmi's `useWalletClient()`), or an
60
+ * EIP-1193 provider such as a discovered `Wallet`'s. Connects (unless the client already has an
61
+ * 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
64
  */
65
- export async function payWithWallet(provider, quote) {
65
+ export async function payWithWallet(wallet, quote) {
66
66
  const transfer = quoteTransfer(quote);
67
- const wallet = createWalletClient({ transport: custom(provider) });
67
+ const client = isWalletClient(wallet) ? wallet : createWalletClient({ transport: custom(wallet) });
68
68
  try {
69
- const [account] = await wallet.requestAddresses();
69
+ const account = client.account ?? (await client.requestAddresses())[0];
70
70
  if (account === undefined) {
71
71
  throw new WalletError("no_account", "The wallet shared no account");
72
72
  }
73
- await ensureChain(wallet, transfer.chainId);
74
- return await wallet.writeContract({
73
+ await ensureChain(client, transfer.chainId);
74
+ return await client.writeContract({
75
75
  account,
76
76
  chain: knownChain(transfer.chainId) ?? null,
77
77
  address: transfer.token,
@@ -84,12 +84,15 @@ export async function payWithWallet(provider, quote) {
84
84
  if (error instanceof WalletError) {
85
85
  throw error;
86
86
  }
87
- if (hasCode(error, 4001)) {
87
+ if (isRejection(error)) {
88
88
  throw new WalletError("rejected", "The request was rejected in the wallet", { cause: error });
89
89
  }
90
90
  throw new WalletError("failed", "The wallet could not send the payment", { cause: error });
91
91
  }
92
92
  }
93
+ function isWalletClient(wallet) {
94
+ return "writeContract" in wallet;
95
+ }
93
96
  async function ensureChain(wallet, chainId) {
94
97
  if ((await wallet.getChainId()) === chainId) {
95
98
  return;
@@ -98,10 +101,9 @@ async function ensureChain(wallet, chainId) {
98
101
  await wallet.switchChain({ id: chainId });
99
102
  }
100
103
  catch (error) {
101
- // 4902: the wallet does not know the chain yet.
102
104
  const chain = knownChain(chainId);
103
- if (!hasCode(error, 4902) || chain === undefined) {
104
- throw hasCode(error, 4001)
105
+ if (!isUnknownChain(error) || chain === undefined) {
106
+ throw isRejection(error)
105
107
  ? error
106
108
  : new WalletError("wrong_chain", `Switch your wallet to ${networkName(chainId)}`, {
107
109
  cause: error,
@@ -116,20 +118,16 @@ async function ensureChain(wallet, chainId) {
116
118
  throw new WalletError("wrong_chain", `Switch your wallet to ${networkName(chainId)}`);
117
119
  }
118
120
  }
119
- /** Whether an error or any of its causes carries an EIP-1193 error code; some mobile wallets nest
120
- * the original error under `data.originalError`. */
121
- function hasCode(error, code) {
122
- for (let current = error; typeof current === "object" && current !== null;) {
123
- const value = current;
124
- if (value.code === code) {
125
- return true;
126
- }
127
- const nested = value.data?.originalError;
128
- if (typeof nested === "object" && nested !== null && hasCode(nested, code)) {
129
- return true;
130
- }
131
- current = value.cause;
132
- }
133
- return false;
121
+ function isRejection(error) {
122
+ return error instanceof BaseError && error.walk((e) => e instanceof UserRejectedRequestError) !== null;
123
+ }
124
+ /** Whether the wallet does not know the chain (4902). MetaMask Mobile nests that code in an internal
125
+ * error's `data.originalError`, which wagmi's injected connector also unwraps. */
126
+ function isUnknownChain(error) {
127
+ return (error instanceof BaseError &&
128
+ error.walk((e) => {
129
+ const nested = e?.data;
130
+ return e instanceof SwitchChainError || nested?.originalError?.code === SwitchChainError.code;
131
+ }) !== null);
134
132
  }
135
133
  //# sourceMappingURL=wallet.js.map
@@ -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":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,MAAM,EACN,QAAQ,EACR,gBAAgB,EAChB,wBAAwB,GAGzB,MAAM,MAAM,CAAC;AACd,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAsB7C,iGAAiG;AACjG,MAAM,CAAC,MAAM,oBAAoB,GAAG,UAAU,CAAC;AAE/C;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,QAAqC;IAChE,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACb,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC;IACzB,CAAC;IACD,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;IAC5B,MAAM,WAAW,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,EAAE;QAC/E,eAAe,EAAE,IAAI;KACtB,CAAC,CAAC;IACH,OAAO,GAAG,EAAE;QACV,WAAW,EAAE,CAAC;QACd,KAAK,CAAC,OAAO,EAAE,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC;AAID,+FAA+F;AAC/F,SAAS,WAAW,CAAC,aAAsC;IACzD,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,aAAa,EAAE,CAAC;QAC/C,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,OAAO,IAAI,EAAE,IAAI,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7F,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACnG,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IACrF,CAAC;IACD,kGAAkG;IAClG,MAAM,QAAQ,GAAI,MAA0C,CAAC,QAAQ,CAAC;IACtE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QACnD,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;YAChF,QAAQ,EAAE,QAAQ;SACnB,CAAC,CAAC;IACL,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,UAAU,CAAC,KAAc;IAChC,OAAO,OAAQ,KAAsD,EAAE,OAAO,KAAK,UAAU,CAAC;AAChG,CAAC;AAID,MAAM,OAAO,WAAY,SAAQ,KAAK;IAIzB;IAHO,IAAI,GAAG,aAAa,CAAC;IAEvC,YACW,IAAqB,EAC9B,OAAe,EACf,OAAsB;QAEtB,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAJf,SAAI,GAAJ,IAAI,CAAiB;IAKhC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAuC,EACvC,KAAkB;IAElB,MAAM,QAAQ,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IACtC,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACnG,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACvE,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,IAAI,WAAW,CAAC,YAAY,EAAE,8BAA8B,CAAC,CAAC;QACtE,CAAC;QACD,MAAM,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC5C,OAAO,MAAM,MAAM,CAAC,aAAa,CAAC;YAChC,OAAO;YACP,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI;YAC3C,OAAO,EAAE,QAAQ,CAAC,KAAK;YACvB,GAAG,EAAE,QAAQ;YACb,YAAY,EAAE,UAAU;YACxB,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,MAAM,CAAC;SACrC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;YACjC,MAAM,KAAK,CAAC;QACd,CAAC;QACD,IAAI,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,WAAW,CAAC,UAAU,EAAE,wCAAwC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAChG,CAAC;QACD,MAAM,IAAI,WAAW,CAAC,QAAQ,EAAE,uCAAuC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7F,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,MAAuC;IAC7D,OAAO,eAAe,IAAI,MAAM,CAAC;AACnC,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,MAAoB,EAAE,OAAe;IAC9D,IAAI,CAAC,MAAM,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,OAAO,EAAE,CAAC;QAC5C,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAC5C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;QAClC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAClD,MAAM,WAAW,CAAC,KAAK,CAAC;gBACtB,CAAC,CAAC,KAAK;gBACP,CAAC,CAAC,IAAI,WAAW,CAAC,aAAa,EAAE,yBAAyB,WAAW,CAAC,OAAO,CAAC,EAAE,EAAE;oBAC9E,KAAK,EAAE,KAAK;iBACb,CAAC,CAAC;QACT,CAAC;QACD,MAAM,MAAM,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACjC,IAAI,CAAC,MAAM,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,OAAO,EAAE,CAAC;YAC5C,MAAM,MAAM,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;IACD,IAAI,CAAC,MAAM,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,OAAO,EAAE,CAAC;QAC5C,MAAM,IAAI,WAAW,CAAC,aAAa,EAAE,yBAAyB,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACxF,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,OAAO,KAAK,YAAY,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,YAAY,wBAAwB,CAAC,KAAK,IAAI,CAAC;AACzG,CAAC;AAED;kFACkF;AAClF,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,CACL,KAAK,YAAY,SAAS;QAC1B,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;YACf,MAAM,MAAM,GAAI,CAA8D,EAAE,IAAI,CAAC;YACrF,OAAO,CAAC,YAAY,gBAAgB,IAAI,MAAM,EAAE,aAAa,EAAE,IAAI,KAAK,gBAAgB,CAAC,IAAI,CAAC;QAChG,CAAC,CAAC,KAAK,IAAI,CACZ,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phala/pay",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Phala Pay checkout: a framework-agnostic client and React bindings",
5
5
  "keywords": [
6
6
  "crypto",
@@ -32,6 +32,10 @@
32
32
  "./react": {
33
33
  "types": "./dist/react/index.d.ts",
34
34
  "default": "./dist/react/index.js"
35
+ },
36
+ "./server": {
37
+ "types": "./dist/server/index.d.ts",
38
+ "default": "./dist/server/index.js"
35
39
  }
36
40
  },
37
41
  "files": [
@@ -57,6 +61,7 @@
57
61
  "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build"
58
62
  },
59
63
  "dependencies": {
64
+ "mipd": "^0.0.7",
60
65
  "uqr": "^0.1.3"
61
66
  },
62
67
  "peerDependencies": {
package/src/checkout.ts CHANGED
@@ -1,13 +1,18 @@
1
+ import { getAddress, isAddress, isAddressEqual } from "viem";
1
2
  import { parseClientQuote, quoteIdFromClientSecret, type ClientQuote } from "./quote.js";
2
3
 
3
4
  /**
4
5
  * What the payer sees:
5
6
  * - `loading` until the quote is first read;
6
7
  * - `waiting` for a payment, until `expires_at`;
7
- * - `seen` once a transfer is in a block, `confirming` once it is final and being valued;
8
- * - `credited`, or `rejected` for a final payment that will not be credited;
8
+ * - `seen` once a transfer is in a block, `confirming` once it reaches the route's confirmation
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;
12
+ * - `reversed` when a credited payment's transaction left the chain before finality;
9
13
  * - `expired` or `canceled` without a payment;
10
- * - `error` when the client secret is not valid.
14
+ * - `error` when the client secret is not valid, or the quote's address is not the one your
15
+ * backend expects (`address_mismatch`): nothing is shown to pay.
11
16
  */
12
17
  export type CheckoutStatus =
13
18
  | "loading"
@@ -16,27 +21,72 @@ export type CheckoutStatus =
16
21
  | "confirming"
17
22
  | "credited"
18
23
  | "rejected"
24
+ | "reversed"
19
25
  | "expired"
20
26
  | "canceled"
21
27
  | "error";
22
28
 
23
29
  export type CheckoutErrorCode =
24
30
  | "invalid_client_secret"
31
+ | "address_mismatch"
25
32
  | "rate_limited"
26
33
  | "network_error"
27
34
  | "invalid_response"
28
35
  | "api_error";
29
36
 
37
+ export interface CheckoutErrorOptions extends ErrorOptions {
38
+ /** The failed response's `Request-Id`, `req_…`, to quote to support. */
39
+ requestId?: string;
40
+ /** For `rate_limited`: the seconds the service asked to wait (`Retry-After`). */
41
+ retryAfter?: number;
42
+ }
43
+
30
44
  export class CheckoutError extends Error {
31
45
  override readonly name = "CheckoutError";
46
+ /** The failed response's `Request-Id`, when the service answered. */
47
+ readonly requestId: string | undefined;
48
+ /** For `rate_limited`: the seconds to wait before the next read. */
49
+ readonly retryAfter: number | undefined;
32
50
 
33
51
  constructor(
34
52
  readonly code: CheckoutErrorCode,
35
53
  message: string,
36
- options?: ErrorOptions,
54
+ options?: CheckoutErrorOptions,
37
55
  ) {
38
56
  super(message, options);
57
+ this.requestId = options?.requestId;
58
+ this.retryAfter = options?.retryAfter;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * The error of a failed read of a public view, with the response's `Request-Id` and, on `429`,
64
+ * its `Retry-After`.
65
+ */
66
+ export function responseError(response: Response, notFound: string): CheckoutError {
67
+ const header = response.headers.get("request-id");
68
+ const requestId = header === null ? {} : { requestId: header };
69
+ if (response.status === 404) {
70
+ return new CheckoutError("invalid_client_secret", notFound, requestId);
71
+ }
72
+ if (response.status === 429) {
73
+ const seconds = Number(response.headers.get("retry-after"));
74
+ return new CheckoutError("rate_limited", "too many status requests", {
75
+ ...requestId,
76
+ ...(Number.isFinite(seconds) && seconds > 0 ? { retryAfter: seconds } : {}),
77
+ });
39
78
  }
79
+ return new CheckoutError(
80
+ "api_error",
81
+ `the payment service answered ${response.status}`,
82
+ requestId,
83
+ );
84
+ }
85
+
86
+ /** The delay before the next read after `failures` failed ones, at least what `error` asks. */
87
+ export function pollDelay(interval: number, failures: number, error: CheckoutError | null): number {
88
+ const backoff = failures === 0 ? interval : Math.min(interval * 2 ** failures, MAX_BACKOFF);
89
+ return Math.max(backoff, (error?.retryAfter ?? 0) * 1000);
40
90
  }
41
91
 
42
92
  export interface CheckoutState {
@@ -49,6 +99,12 @@ export interface CheckoutState {
49
99
  export interface CheckoutOptions {
50
100
  /** The quote's `client_secret`, from your backend's `POST /v1/quotes`. */
51
101
  clientSecret: string;
102
+ /**
103
+ * The quote's address as your backend's SDK recomputed it from the pinned forwarder. The
104
+ * checkout fails closed, showing nothing to pay, when the quote read from the service names
105
+ * another address.
106
+ */
107
+ expectedAddress: string;
52
108
  /** The service origin, for example `https://topup.example.com`. */
53
109
  apiBase: string;
54
110
  /** Milliseconds between status reads; default 3000. */
@@ -73,37 +129,50 @@ const MAX_BACKOFF = 30_000;
73
129
 
74
130
  export interface RetrieveQuoteOptions {
75
131
  clientSecret: string;
132
+ /** The address your backend recomputed; a quote naming another one is refused. */
133
+ expectedAddress: string;
76
134
  apiBase: string;
77
135
  fetch?: typeof globalThis.fetch;
78
136
  }
79
137
 
80
138
  /**
81
139
  * Reads a quote's public view once, as Stripe.js's `retrievePaymentIntent(clientSecret)` does.
82
- * Rejects with a `CheckoutError`: `invalid_client_secret` for an unknown quote or secret.
140
+ * Rejects with a `CheckoutError`: `invalid_client_secret` for an unknown quote or secret,
141
+ * `address_mismatch` for a quote whose address is not `expectedAddress`.
83
142
  */
84
143
  export async function retrieveQuote(options: RetrieveQuoteOptions): Promise<ClientQuote> {
85
144
  const quoteId = quoteIdFromClientSecret(options.clientSecret);
145
+ const expected = expectedAddress(options.expectedAddress);
86
146
  const base = options.apiBase.replace(/\/+$/, "");
87
147
  const url = `${base}/v1/quotes/${quoteId}?client_secret=${encodeURIComponent(options.clientSecret)}`;
88
148
  const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
89
149
  // A simple GET with no custom headers, so the browser sends no CORS preflight.
90
150
  const response = await fetchImpl(url, { cache: "no-store", credentials: "omit" });
91
- if (response.status === 404) {
92
- throw new CheckoutError("invalid_client_secret", "the quote or its client secret is unknown");
93
- }
94
- if (response.status === 429) {
95
- throw new CheckoutError("rate_limited", "too many status requests");
96
- }
97
151
  if (!response.ok) {
98
- throw new CheckoutError("api_error", `the payment service answered ${response.status}`);
152
+ throw responseError(response, "the quote or its client secret is unknown");
99
153
  }
154
+ let quote: ClientQuote;
100
155
  try {
101
- return parseClientQuote(await response.json());
156
+ quote = parseClientQuote(await response.json());
102
157
  } catch (cause) {
103
158
  throw new CheckoutError("invalid_response", "unexpected response from the payment service", {
104
159
  cause,
105
160
  });
106
161
  }
162
+ if (!isAddressEqual(getAddress(quote.address), expected)) {
163
+ throw new CheckoutError(
164
+ "address_mismatch",
165
+ "the quote's address is not the one the merchant expects",
166
+ );
167
+ }
168
+ return quote;
169
+ }
170
+
171
+ function expectedAddress(value: string): `0x${string}` {
172
+ if (!isAddress(value, { strict: false })) {
173
+ throw new TypeError("expectedAddress is not an address");
174
+ }
175
+ return getAddress(value);
107
176
  }
108
177
 
109
178
  /** The payer-facing status of a quote at `nowSeconds`. */
@@ -128,6 +197,7 @@ export function checkoutStatus(quote: ClientQuote, nowSeconds: number): Checkout
128
197
  */
129
198
  export function createCheckout(options: CheckoutOptions): CheckoutSession {
130
199
  quoteIdFromClientSecret(options.clientSecret);
200
+ expectedAddress(options.expectedAddress);
131
201
  const now = options.now ?? Date.now;
132
202
  const interval = options.pollInterval ?? DEFAULT_POLL_INTERVAL;
133
203
  const listeners = new Set<(state: CheckoutState) => void>();
@@ -159,6 +229,7 @@ export function createCheckout(options: CheckoutOptions): CheckoutSession {
159
229
  status === "error" ||
160
230
  status === "credited" ||
161
231
  status === "rejected" ||
232
+ status === "reversed" ||
162
233
  status === "canceled" ||
163
234
  (status === "expired" && quote?.status === "expired")
164
235
  );
@@ -176,7 +247,10 @@ export function createCheckout(options: CheckoutOptions): CheckoutSession {
176
247
  : new CheckoutError("network_error", "could not reach the payment service", { cause });
177
248
  failures += 1;
178
249
  const quote = state.quote;
179
- if (error.code === "invalid_client_secret") {
250
+ if (error.code === "address_mismatch") {
251
+ // Fail closed: forget the quote, so nothing about its address is shown.
252
+ setState({ status: "error", quote: null, error });
253
+ } else if (error.code === "invalid_client_secret") {
180
254
  setState({ status: "error", quote, error });
181
255
  } else {
182
256
  setState({
@@ -199,7 +273,7 @@ export function createCheckout(options: CheckoutOptions): CheckoutSession {
199
273
  if (finished()) {
200
274
  return;
201
275
  }
202
- const delay = failures === 0 ? interval : Math.min(interval * 2 ** failures, MAX_BACKOFF);
276
+ const delay = pollDelay(interval, failures, state.error);
203
277
  timer = setTimeout(() => {
204
278
  void refresh().then(schedule);
205
279
  }, delay);