@phala/pay 0.1.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 (69) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +202 -0
  3. package/README.md +142 -0
  4. package/dist/chains.d.ts +7 -0
  5. package/dist/chains.d.ts.map +1 -0
  6. package/dist/chains.js +16 -0
  7. package/dist/chains.js.map +1 -0
  8. package/dist/checkout.d.ts +63 -0
  9. package/dist/checkout.d.ts.map +1 -0
  10. package/dist/checkout.js +146 -0
  11. package/dist/checkout.js.map +1 -0
  12. package/dist/client.d.ts +26 -0
  13. package/dist/client.d.ts.map +1 -0
  14. package/dist/client.js +35 -0
  15. package/dist/client.js.map +1 -0
  16. package/dist/format.d.ts +13 -0
  17. package/dist/format.d.ts.map +1 -0
  18. package/dist/format.js +37 -0
  19. package/dist/format.js.map +1 -0
  20. package/dist/index.d.ts +8 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +8 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/payment.d.ts +19 -0
  25. package/dist/payment.d.ts.map +1 -0
  26. package/dist/payment.js +34 -0
  27. package/dist/payment.js.map +1 -0
  28. package/dist/quote.d.ts +36 -0
  29. package/dist/quote.d.ts.map +1 -0
  30. package/dist/quote.js +59 -0
  31. package/dist/quote.js.map +1 -0
  32. package/dist/react/Checkout.d.ts +21 -0
  33. package/dist/react/Checkout.d.ts.map +1 -0
  34. package/dist/react/Checkout.js +168 -0
  35. package/dist/react/Checkout.js.map +1 -0
  36. package/dist/react/QrCode.d.ts +8 -0
  37. package/dist/react/QrCode.d.ts.map +1 -0
  38. package/dist/react/QrCode.js +20 -0
  39. package/dist/react/QrCode.js.map +1 -0
  40. package/dist/react/appearance.d.ts +24 -0
  41. package/dist/react/appearance.d.ts.map +1 -0
  42. package/dist/react/appearance.js +95 -0
  43. package/dist/react/appearance.js.map +1 -0
  44. package/dist/react/index.d.ts +4 -0
  45. package/dist/react/index.d.ts.map +1 -0
  46. package/dist/react/index.js +4 -0
  47. package/dist/react/index.js.map +1 -0
  48. package/dist/react/useCheckout.d.ts +13 -0
  49. package/dist/react/useCheckout.d.ts.map +1 -0
  50. package/dist/react/useCheckout.js +29 -0
  51. package/dist/react/useCheckout.js.map +1 -0
  52. package/dist/wallet.d.ts +42 -0
  53. package/dist/wallet.d.ts.map +1 -0
  54. package/dist/wallet.js +135 -0
  55. package/dist/wallet.js.map +1 -0
  56. package/package.json +93 -0
  57. package/src/chains.ts +20 -0
  58. package/src/checkout.ts +225 -0
  59. package/src/client.ts +54 -0
  60. package/src/format.ts +41 -0
  61. package/src/index.ts +27 -0
  62. package/src/payment.ts +52 -0
  63. package/src/quote.ts +96 -0
  64. package/src/react/Checkout.tsx +409 -0
  65. package/src/react/QrCode.tsx +39 -0
  66. package/src/react/appearance.ts +114 -0
  67. package/src/react/index.ts +5 -0
  68. package/src/react/useCheckout.ts +48 -0
  69. package/src/wallet.ts +175 -0
package/src/wallet.ts ADDED
@@ -0,0 +1,175 @@
1
+ import {
2
+ createWalletClient,
3
+ custom,
4
+ erc20Abi,
5
+ type Hash,
6
+ type WalletClient,
7
+ } from "viem";
8
+ import { knownChain, networkName } from "./chains.js";
9
+ import { quoteTransfer } from "./payment.js";
10
+ import type { ClientQuote } from "./quote.js";
11
+
12
+ /** EIP-6963 provider info. */
13
+ export interface WalletInfo {
14
+ uuid: string;
15
+ name: string;
16
+ /** A data URI. */
17
+ icon: string;
18
+ rdns: string;
19
+ }
20
+
21
+ /** An EIP-1193 provider; viem's `EIP1193Provider` and every injected wallet satisfy it. */
22
+ export interface EthereumProvider {
23
+ request(args: { method: string; params?: unknown }): Promise<unknown>;
24
+ }
25
+
26
+ export interface Wallet {
27
+ info: WalletInfo;
28
+ provider: EthereumProvider;
29
+ }
30
+
31
+ /** The legacy `window.ethereum` provider, offered when no wallet announces itself (EIP-6963). */
32
+ export const INJECTED_WALLET_UUID = "injected";
33
+
34
+ interface AnnounceProviderEvent extends Event {
35
+ detail?: { info?: Partial<WalletInfo>; provider?: EthereumProvider };
36
+ }
37
+
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.
42
+ */
43
+ export function watchWallets(onChange: (wallets: Wallet[]) => void): () => void {
44
+ if (typeof window === "undefined") {
45
+ onChange([]);
46
+ return () => undefined;
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);
59
+ };
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;
69
+ }
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,
78
+ });
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);
85
+ }
86
+
87
+ export type WalletErrorCode = "rejected" | "no_account" | "wrong_chain" | "failed";
88
+
89
+ export class WalletError extends Error {
90
+ override readonly name = "WalletError";
91
+
92
+ constructor(
93
+ readonly code: WalletErrorCode,
94
+ message: string,
95
+ options?: ErrorOptions,
96
+ ) {
97
+ super(message, options);
98
+ }
99
+ }
100
+
101
+ /**
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.
105
+ */
106
+ export async function payWithWallet(provider: EthereumProvider, quote: ClientQuote): Promise<Hash> {
107
+ const transfer = quoteTransfer(quote);
108
+ const wallet = createWalletClient({ transport: custom(provider) });
109
+ try {
110
+ const [account] = await wallet.requestAddresses();
111
+ if (account === undefined) {
112
+ throw new WalletError("no_account", "The wallet shared no account");
113
+ }
114
+ await ensureChain(wallet, transfer.chainId);
115
+ return await wallet.writeContract({
116
+ account,
117
+ chain: knownChain(transfer.chainId) ?? null,
118
+ address: transfer.token,
119
+ abi: erc20Abi,
120
+ functionName: "transfer",
121
+ args: [transfer.to, transfer.amount],
122
+ });
123
+ } catch (error) {
124
+ if (error instanceof WalletError) {
125
+ throw error;
126
+ }
127
+ if (hasCode(error, 4001)) {
128
+ throw new WalletError("rejected", "The request was rejected in the wallet", { cause: error });
129
+ }
130
+ throw new WalletError("failed", "The wallet could not send the payment", { cause: error });
131
+ }
132
+ }
133
+
134
+ async function ensureChain(wallet: WalletClient, chainId: number): Promise<void> {
135
+ if ((await wallet.getChainId()) === chainId) {
136
+ return;
137
+ }
138
+ try {
139
+ await wallet.switchChain({ id: chainId });
140
+ } catch (error) {
141
+ // 4902: the wallet does not know the chain yet.
142
+ const chain = knownChain(chainId);
143
+ if (!hasCode(error, 4902) || chain === undefined) {
144
+ throw hasCode(error, 4001)
145
+ ? error
146
+ : new WalletError("wrong_chain", `Switch your wallet to ${networkName(chainId)}`, {
147
+ cause: error,
148
+ });
149
+ }
150
+ await wallet.addChain({ chain });
151
+ if ((await wallet.getChainId()) !== chainId) {
152
+ await wallet.switchChain({ id: chainId });
153
+ }
154
+ }
155
+ if ((await wallet.getChainId()) !== chainId) {
156
+ throw new WalletError("wrong_chain", `Switch your wallet to ${networkName(chainId)}`);
157
+ }
158
+ }
159
+
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;
175
+ }