@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.
package/src/wallet.ts CHANGED
@@ -6,10 +6,13 @@ import {
6
6
  erc20Abi,
7
7
  SwitchChainError,
8
8
  UserRejectedRequestError,
9
+ type Address,
9
10
  type Hash,
10
11
  type WalletClient,
11
12
  } from "viem";
13
+ import { readContract } from "viem/actions";
12
14
  import { knownChain, networkName } from "./chains.js";
15
+ import { formatTokenAmount, formatUnitsGrouped } from "./format.js";
13
16
  import { quoteTransfer } from "./payment.js";
14
17
  import type { ClientQuote } from "./quote.js";
15
18
 
@@ -83,7 +86,12 @@ function isProvider(value: unknown): value is EthereumProvider {
83
86
  return typeof (value as Partial<EthereumProvider> | null | undefined)?.request === "function";
84
87
  }
85
88
 
86
- export type WalletErrorCode = "rejected" | "no_account" | "wrong_chain" | "failed";
89
+ export type WalletErrorCode =
90
+ | "rejected"
91
+ | "no_account"
92
+ | "wrong_chain"
93
+ | "insufficient_balance"
94
+ | "failed";
87
95
 
88
96
  export class WalletError extends Error {
89
97
  override readonly name = "WalletError";
@@ -101,8 +109,10 @@ export class WalletError extends Error {
101
109
  * Pays a quote from a wallet: a viem `WalletClient` (for example wagmi's `useWalletClient()`), or an
102
110
  * EIP-1193 provider such as a discovered `Wallet`'s. Connects (unless the client already has an
103
111
  * 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.
112
+ * `payment_uri` states. Before sending, it reads the account's token balance and, when it is below
113
+ * the quote's amount, sends nothing and throws `WalletError` `insufficient_balance`: the transfer
114
+ * would revert and still cost gas. Resolves with the transaction hash once the wallet has broadcast
115
+ * it; the checkout's status follows the payment from there.
106
116
  */
107
117
  export async function payWithWallet(
108
118
  wallet: WalletClient | EthereumProvider,
@@ -116,6 +126,16 @@ export async function payWithWallet(
116
126
  throw new WalletError("no_account", "The wallet shared no account");
117
127
  }
118
128
  await ensureChain(client, transfer.chainId);
129
+ const address = typeof account === "string" ? account : account.address;
130
+ const balance = await tokenBalance(client, transfer.token, address);
131
+ if (balance !== undefined && balance < transfer.amount) {
132
+ const symbol = quote.asset.toUpperCase();
133
+ throw new WalletError(
134
+ "insufficient_balance",
135
+ `Your wallet holds ${formatUnitsGrouped(balance, quote.decimals)} ${symbol}, less than the ` +
136
+ `${formatTokenAmount(quote)} ${symbol} to pay. Nothing was sent.`,
137
+ );
138
+ }
119
139
  return await client.writeContract({
120
140
  account,
121
141
  chain: knownChain(transfer.chainId) ?? null,
@@ -135,6 +155,19 @@ export async function payWithWallet(
135
155
  }
136
156
  }
137
157
 
158
+ /**
159
+ * The account's balance of `token`, read through the wallet on its current chain; `undefined` when
160
+ * the wallet cannot read it. The check only spares the payer a reverting transfer, so a wallet that
161
+ * does not serve `eth_call` still pays.
162
+ */
163
+ async function tokenBalance(wallet: WalletClient, token: Address, owner: Address): Promise<bigint | undefined> {
164
+ try {
165
+ return await readContract(wallet, { address: token, abi: erc20Abi, functionName: "balanceOf", args: [owner] });
166
+ } catch {
167
+ return undefined;
168
+ }
169
+ }
170
+
138
171
  function isWalletClient(wallet: WalletClient | EthereumProvider): wallet is WalletClient {
139
172
  return "writeContract" in wallet;
140
173
  }