chain.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. /**
  2. * Chain helpers for mavi — balance, send, optional test fund.
  3. * RPC comes from the network switcher (local private or public anvil).
  4. */
  5. import {
  6. createPublicClient,
  7. createWalletClient,
  8. formatEther,
  9. http,
  10. parseEther,
  11. type Hex,
  12. } from "viem";
  13. import { privateKeyToAccount } from "viem/accounts";
  14. import { getActiveNetwork, type MaviNetwork } from "./network";
  15. /** Anvil account #0 — local dev only, never production */
  16. const ANVIL_0_KEY =
  17. "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex;
  18. function chainFromNetwork(net: MaviNetwork) {
  19. return {
  20. id: net.chainId,
  21. name: net.label,
  22. nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
  23. rpcUrls: {
  24. default: { http: [net.rpcUrl] },
  25. },
  26. } as const;
  27. }
  28. function publicClient(net = getActiveNetwork()) {
  29. return createPublicClient({
  30. chain: chainFromNetwork(net),
  31. transport: http(net.rpcUrl),
  32. });
  33. }
  34. export type ChainBalance = {
  35. ok: boolean;
  36. eth: string;
  37. wei: string;
  38. chainId?: number;
  39. networkId?: string;
  40. networkLabel?: string;
  41. error?: string;
  42. };
  43. export async function fetchEthBalance(
  44. address: `0x${string}`,
  45. ): Promise<ChainBalance> {
  46. const net = getActiveNetwork();
  47. try {
  48. const client = publicClient(net);
  49. const [wei, chainId] = await Promise.all([
  50. client.getBalance({ address }),
  51. client.getChainId(),
  52. ]);
  53. return {
  54. ok: true,
  55. eth: formatEther(wei),
  56. wei: wei.toString(),
  57. chainId,
  58. networkId: net.id,
  59. networkLabel: net.shortLabel,
  60. };
  61. } catch (e) {
  62. return {
  63. ok: false,
  64. eth: "0",
  65. wei: "0",
  66. networkId: net.id,
  67. networkLabel: net.shortLabel,
  68. error:
  69. e instanceof Error
  70. ? e.message
  71. : `Chain unreachable (${net.shortLabel}). Start: bun run chains:up`,
  72. };
  73. }
  74. }
  75. export async function sendEth(opts: {
  76. privateKey: Hex;
  77. to: `0x${string}`;
  78. amountEth: string;
  79. }): Promise<{ hash: `0x${string}`; networkId: string }> {
  80. const net = getActiveNetwork();
  81. const chain = chainFromNetwork(net);
  82. const account = privateKeyToAccount(opts.privateKey);
  83. const client = createWalletClient({
  84. account,
  85. chain,
  86. transport: http(net.rpcUrl),
  87. });
  88. const publicC = publicClient(net);
  89. const hash = await client.sendTransaction({
  90. account,
  91. to: opts.to,
  92. value: parseEther(opts.amountEth),
  93. chain,
  94. });
  95. await publicC.waitForTransactionReceipt({ hash, timeout: 20_000 });
  96. return { hash, networkId: net.id };
  97. }
  98. /**
  99. * Dev-only: send test ETH from anvil account #0 to a mavi address.
  100. */
  101. export async function fundFromAnvil(
  102. to: `0x${string}`,
  103. amountEth = "1",
  104. ): Promise<{ hash: `0x${string}`; networkId: string }> {
  105. const net = getActiveNetwork();
  106. if (!net.canFund) {
  107. throw new Error("Test fund is only available on local anvil networks.");
  108. }
  109. return sendEth({
  110. privateKey: ANVIL_0_KEY,
  111. to,
  112. amountEth,
  113. });
  114. }
  115. export function isValidEthAddress(value: string): value is `0x${string}` {
  116. return /^0x[a-fA-F0-9]{40}$/.test(value.trim());
  117. }