@phala/pay 0.1.2 → 0.3.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 +106 -1
  2. package/README.md +146 -44
  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 +5 -3
  26. package/dist/quote.js.map +1 -1
  27. package/dist/react/Checkout.d.ts +11 -1
  28. package/dist/react/Checkout.d.ts.map +1 -1
  29. package/dist/react/Checkout.js +28 -37
  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 +3 -0
  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 +61 -0
  64. package/dist/server/webhook.d.ts.map +1 -0
  65. package/dist/server/webhook.js +150 -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 +12 -6
  78. package/src/react/Checkout.tsx +72 -70
  79. package/src/react/DepositAddress.tsx +213 -0
  80. package/src/react/Field.tsx +46 -0
  81. package/src/react/appearance.ts +3 -0
  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 +204 -0
  88. package/src/wallet.ts +74 -68
package/src/wallet.ts CHANGED
@@ -1,7 +1,11 @@
1
+ import { createStore } from "mipd";
1
2
  import {
3
+ BaseError,
2
4
  createWalletClient,
3
5
  custom,
4
6
  erc20Abi,
7
+ SwitchChainError,
8
+ UserRejectedRequestError,
5
9
  type Hash,
6
10
  type WalletClient,
7
11
  } from "viem";
@@ -31,57 +35,52 @@ export interface Wallet {
31
35
  /** The legacy `window.ethereum` provider, offered when no wallet announces itself (EIP-6963). */
32
36
  export const INJECTED_WALLET_UUID = "injected";
33
37
 
34
- interface AnnounceProviderEvent extends Event {
35
- detail?: { info?: Partial<WalletInfo>; provider?: EthereumProvider };
36
- }
37
-
38
38
  /**
39
- * Discovers browser wallets with EIP-6963, falling back to `window.ethereum`, and calls `onChange`
40
- * with the current list whenever a wallet announces itself. Returns the function that stops
41
- * listening.
39
+ * Discovers browser wallets with EIP-6963 (through mipd's store), falling back to
40
+ * `window.ethereum`, and calls `onChange` with the current list whenever a wallet announces
41
+ * itself. Returns the function that stops listening.
42
42
  */
43
43
  export function watchWallets(onChange: (wallets: Wallet[]) => void): () => void {
44
44
  if (typeof window === "undefined") {
45
45
  onChange([]);
46
46
  return () => undefined;
47
47
  }
48
- const announced = new Map<string, Wallet>();
49
- const emit = () => {
50
- const wallets = [...announced.values()];
51
- const injected = (window as { ethereum?: EthereumProvider }).ethereum;
52
- if (wallets.length === 0 && injected !== undefined) {
53
- wallets.push({
54
- info: { uuid: INJECTED_WALLET_UUID, name: "Browser wallet", icon: "", rdns: "" },
55
- provider: injected,
56
- });
57
- }
58
- onChange(wallets);
48
+ const store = createStore();
49
+ const unsubscribe = store.subscribe((details) => onChange(walletsFrom(details)), {
50
+ emitImmediately: true,
51
+ });
52
+ return () => {
53
+ unsubscribe();
54
+ store.destroy();
59
55
  };
60
- const onAnnounce = (event: Event) => {
61
- const detail = (event as AnnounceProviderEvent).detail;
62
- const info = detail?.info;
63
- if (
64
- detail?.provider === undefined ||
65
- typeof info?.uuid !== "string" ||
66
- typeof info.name !== "string"
67
- ) {
68
- return;
56
+ }
57
+
58
+ type Announcement = { info?: Partial<WalletInfo>; provider?: unknown };
59
+
60
+ /** Announcements are untrusted page events: keep well-formed ones, and only data-URI icons. */
61
+ function walletsFrom(announcements: readonly Announcement[]): Wallet[] {
62
+ const wallets: Wallet[] = [];
63
+ for (const { info, provider } of announcements) {
64
+ if (!isProvider(provider) || typeof info?.uuid !== "string" || typeof info.name !== "string") {
65
+ continue;
69
66
  }
70
- announced.set(info.uuid, {
71
- info: {
72
- uuid: info.uuid,
73
- name: info.name,
74
- icon: typeof info.icon === "string" && info.icon.startsWith("data:image/") ? info.icon : "",
75
- rdns: typeof info.rdns === "string" ? info.rdns : "",
76
- },
77
- provider: detail.provider,
67
+ const icon = typeof info.icon === "string" && info.icon.startsWith("data:image/") ? info.icon : "";
68
+ const rdns = typeof info.rdns === "string" ? info.rdns : "";
69
+ wallets.push({ info: { uuid: info.uuid, name: info.name, icon, rdns }, provider });
70
+ }
71
+ // Wallets that predate EIP-6963, such as some in-app mobile browsers, only set `window.ethereum`.
72
+ const injected = (window as { ethereum?: EthereumProvider }).ethereum;
73
+ if (wallets.length === 0 && injected !== undefined) {
74
+ wallets.push({
75
+ info: { uuid: INJECTED_WALLET_UUID, name: "Browser wallet", icon: "", rdns: "" },
76
+ provider: injected,
78
77
  });
79
- emit();
80
- };
81
- window.addEventListener("eip6963:announceProvider", onAnnounce);
82
- window.dispatchEvent(new Event("eip6963:requestProvider"));
83
- emit();
84
- return () => window.removeEventListener("eip6963:announceProvider", onAnnounce);
78
+ }
79
+ return wallets;
80
+ }
81
+
82
+ function isProvider(value: unknown): value is EthereumProvider {
83
+ return typeof (value as Partial<EthereumProvider> | null | undefined)?.request === "function";
85
84
  }
86
85
 
87
86
  export type WalletErrorCode = "rejected" | "no_account" | "wrong_chain" | "failed";
@@ -99,20 +98,25 @@ export class WalletError extends Error {
99
98
  }
100
99
 
101
100
  /**
102
- * Pays a quote from a browser wallet: connects, switches to (or adds) the quote's chain, and sends
103
- * the ERC-20 `transfer` that the quote's `payment_uri` states. Resolves with the transaction hash
104
- * once the wallet has broadcast it; the checkout's status follows the payment from there.
101
+ * Pays a quote from a wallet: a viem `WalletClient` (for example wagmi's `useWalletClient()`), or an
102
+ * EIP-1193 provider such as a discovered `Wallet`'s. Connects (unless the client already has an
103
+ * account), switches to (or adds) the quote's chain, and sends the ERC-20 `transfer` that the quote's
104
+ * `payment_uri` states. Resolves with the transaction hash once the wallet has broadcast it; the
105
+ * checkout's status follows the payment from there.
105
106
  */
106
- export async function payWithWallet(provider: EthereumProvider, quote: ClientQuote): Promise<Hash> {
107
+ export async function payWithWallet(
108
+ wallet: WalletClient | EthereumProvider,
109
+ quote: ClientQuote,
110
+ ): Promise<Hash> {
107
111
  const transfer = quoteTransfer(quote);
108
- const wallet = createWalletClient({ transport: custom(provider) });
112
+ const client = isWalletClient(wallet) ? wallet : createWalletClient({ transport: custom(wallet) });
109
113
  try {
110
- const [account] = await wallet.requestAddresses();
114
+ const account = client.account ?? (await client.requestAddresses())[0];
111
115
  if (account === undefined) {
112
116
  throw new WalletError("no_account", "The wallet shared no account");
113
117
  }
114
- await ensureChain(wallet, transfer.chainId);
115
- return await wallet.writeContract({
118
+ await ensureChain(client, transfer.chainId);
119
+ return await client.writeContract({
116
120
  account,
117
121
  chain: knownChain(transfer.chainId) ?? null,
118
122
  address: transfer.token,
@@ -124,13 +128,17 @@ export async function payWithWallet(provider: EthereumProvider, quote: ClientQuo
124
128
  if (error instanceof WalletError) {
125
129
  throw error;
126
130
  }
127
- if (hasCode(error, 4001)) {
131
+ if (isRejection(error)) {
128
132
  throw new WalletError("rejected", "The request was rejected in the wallet", { cause: error });
129
133
  }
130
134
  throw new WalletError("failed", "The wallet could not send the payment", { cause: error });
131
135
  }
132
136
  }
133
137
 
138
+ function isWalletClient(wallet: WalletClient | EthereumProvider): wallet is WalletClient {
139
+ return "writeContract" in wallet;
140
+ }
141
+
134
142
  async function ensureChain(wallet: WalletClient, chainId: number): Promise<void> {
135
143
  if ((await wallet.getChainId()) === chainId) {
136
144
  return;
@@ -138,10 +146,9 @@ async function ensureChain(wallet: WalletClient, chainId: number): Promise<void>
138
146
  try {
139
147
  await wallet.switchChain({ id: chainId });
140
148
  } catch (error) {
141
- // 4902: the wallet does not know the chain yet.
142
149
  const chain = knownChain(chainId);
143
- if (!hasCode(error, 4902) || chain === undefined) {
144
- throw hasCode(error, 4001)
150
+ if (!isUnknownChain(error) || chain === undefined) {
151
+ throw isRejection(error)
145
152
  ? error
146
153
  : new WalletError("wrong_chain", `Switch your wallet to ${networkName(chainId)}`, {
147
154
  cause: error,
@@ -157,19 +164,18 @@ async function ensureChain(wallet: WalletClient, chainId: number): Promise<void>
157
164
  }
158
165
  }
159
166
 
160
- /** Whether an error or any of its causes carries an EIP-1193 error code; some mobile wallets nest
161
- * the original error under `data.originalError`. */
162
- function hasCode(error: unknown, code: number): boolean {
163
- for (let current = error; typeof current === "object" && current !== null; ) {
164
- const value = current as { code?: unknown; cause?: unknown; data?: { originalError?: unknown } };
165
- if (value.code === code) {
166
- return true;
167
- }
168
- const nested = value.data?.originalError;
169
- if (typeof nested === "object" && nested !== null && hasCode(nested, code)) {
170
- return true;
171
- }
172
- current = value.cause;
173
- }
174
- return false;
167
+ function isRejection(error: unknown): boolean {
168
+ return error instanceof BaseError && error.walk((e) => e instanceof UserRejectedRequestError) !== null;
169
+ }
170
+
171
+ /** Whether the wallet does not know the chain (4902). MetaMask Mobile nests that code in an internal
172
+ * error's `data.originalError`, which wagmi's injected connector also unwraps. */
173
+ function isUnknownChain(error: unknown): boolean {
174
+ return (
175
+ error instanceof BaseError &&
176
+ error.walk((e) => {
177
+ const nested = (e as { data?: { originalError?: { code?: unknown } } } | null)?.data;
178
+ return e instanceof SwitchChainError || nested?.originalError?.code === SwitchChainError.code;
179
+ }) !== null
180
+ );
175
181
  }