UNPKG

run402-mcp

Version:

MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.

149 lines • 8.22 kB
import { LocalError } from "../errors.js"; import { PATH_USD, TEMPO_FEE_HEADROOM_USD_MICROS } from "./tempo.js"; export const SEND_NETWORKS = ["base", "base-sepolia", "tempo", "tempo-moderato"]; const TOKENS = { base: { token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", asset: "USDC" }, "base-sepolia": { token: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", asset: "USDC" }, tempo: { token: PATH_USD, asset: "pathUSD" }, "tempo-moderato": { token: PATH_USD, asset: "pathUSD" }, }; const ERC20_ABI = [ { type: "function", name: "transfer", stateMutability: "nonpayable", inputs: [{ name: "to", type: "address" }, { name: "amount", type: "uint256" }], outputs: [{ type: "bool" }] }, { type: "function", name: "balanceOf", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ type: "uint256" }] }, ]; const SMART_WALLET_ABI = [ { type: "function", name: "isOwnerAddress", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ type: "bool" }] }, { type: "function", name: "execute", stateMutability: "payable", inputs: [{ name: "target", type: "address" }, { name: "value", type: "uint256" }, { name: "data", type: "bytes" }], outputs: [] }, ]; const CONTEXT = "sending from a wallet"; function badInput(message, code, details) { return new LocalError(message, CONTEXT, { code, details, next_actions: [{ type: "edit_request", why: message }] }); } /** "10.5" → 10_500_000n; refuses anything that is not a positive amount with at most 6 decimals. */ export function parseStablecoinAmount(amount) { const text = typeof amount === "string" ? amount.trim() : ""; const match = /^(\d+)(?:\.(\d{1,6}))?$/.exec(text); const micros = match ? BigInt(match[1]) * 1000000n + BigInt((match[2] ?? "").padEnd(6, "0") || "0") : 0n; if (!match || micros <= 0n) { throw badInput("The amount must be a positive number with at most 6 decimal places, e.g. 10 or 0.5.", "BAD_AMOUNT", { amount: text.slice(0, 32) }); } return micros; } async function chainFor(network) { const chains = await import("viem/chains"); switch (network) { case "base": return chains.base; case "base-sepolia": return chains.baseSepolia; // Tempo pays fees in a TIP-20 stablecoin: name pathUSD so the fee comes from the same balance. case "tempo": return chains.tempo.extend({ feeToken: PATH_USD }); case "tempo-moderato": return chains.tempoModerato.extend({ feeToken: PATH_USD }); } } function insufficient(asset, address, needed, held, unit) { const shortfall = needed - held; return new LocalError(`${address} holds too little ${asset} for this send: ${shortfall} ${unit === "wei" ? "wei" : "USD micros"} short.`, CONTEXT, { code: "SEND_INSUFFICIENT_BALANCE", details: unit === "wei" ? { asset, address, needed_wei: needed.toString(), held_wei: held.toString(), shortfall_wei: shortfall.toString() } : { asset, address, needed_usd_micros: Number(needed), held_usd_micros: Number(held), shortfall_usd_micros: Number(shortfall) }, next_actions: [{ type: "fund_wallet", why: `Send ${asset} to ${address}, then retry.` }], }); } /** The CLI command for these options, as the sandbox refusal names it. */ export function sendCommand(opts) { const word = (v, placeholder) => typeof v === "string" && /^[A-Za-z0-9.x_-]+$/.test(v.trim()) ? v.trim() : placeholder; return [ "run402 wallets send", word(opts.amount, "<amount>"), "--to", word(opts.to, "<address>"), ...(opts.from ? ["--from", word(opts.from, "<smart-account>")] : []), ...(opts.network && opts.network !== "base" ? ["--network", word(opts.network, "<network>")] : []), ].join(" "); } export async function sendFromWallet(account, opts, deps = {}) { const viem = await import("viem"); const network = opts.network ?? "base"; if (!SEND_NETWORKS.includes(network)) { throw badInput(`--network must be one of ${SEND_NETWORKS.join(", ")}.`, "BAD_NETWORK", { network: String(network).slice(0, 32) }); } const amount = parseStablecoinAmount(opts.amount); if (typeof opts.to !== "string" || !viem.isAddress(opts.to, { strict: false })) { throw badInput("--to must be a 0x address.", "BAD_ADDRESS", { field: "to" }); } const to = viem.getAddress(opts.to); const isTempo = network === "tempo" || network === "tempo-moderato"; let from = null; if (opts.from !== undefined) { if (isTempo) throw badInput("--from (a smart account) is supported on Base only.", "SEND_FROM_UNSUPPORTED", { network }); if (!viem.isAddress(opts.from, { strict: false })) throw badInput("--from must be a 0x address.", "BAD_ADDRESS", { field: "from" }); from = viem.getAddress(opts.from); } const chain = await chainFor(network); const transport = deps.transport ?? viem.http(); const publicClient = viem.createPublicClient({ chain, transport }); const { token, asset } = TOKENS[network]; const sender = account.address; const payer = from ?? sender; if (from) { const owner = await publicClient.readContract({ address: from, abi: SMART_WALLET_ABI, functionName: "isOwnerAddress", args: [sender] }); if (!owner) { throw new LocalError(`${sender} is not an owner of smart account ${from}.`, CONTEXT, { code: "SMART_ACCOUNT_NOT_OWNER", details: { smart_account: from, wallet: sender, network }, next_actions: [{ type: "edit_request", why: "Select a wallet that owns this smart account with --wallet, or drop --from to send from the wallet itself." }], }); } } const balance = await publicClient.readContract({ address: token, abi: ERC20_ABI, functionName: "balanceOf", args: [payer] }); // On Tempo the fee comes out of the same pathUSD balance. const needed = isTempo ? amount + TEMPO_FEE_HEADROOM_USD_MICROS : amount; if (balance < needed) throw insufficient(asset, payer, needed, balance, "usd_micros"); const transfer = viem.encodeFunctionData({ abi: ERC20_ABI, functionName: "transfer", args: [to, amount] }); const call = from ? { address: from, abi: SMART_WALLET_ABI, functionName: "execute", args: [token, 0n, transfer] } : { address: token, abi: ERC20_ABI, functionName: "transfer", args: [to, amount] }; let gas = null; try { gas = await publicClient.estimateContractGas({ ...call, account }); } catch { gas = null; } if (!isTempo && gas !== null) { const { maxFeePerGas } = await publicClient.estimateFeesPerGas(); const fee = gas * (maxFeePerGas ?? 0n); const eth = await publicClient.getBalance({ address: sender }); if (eth < fee) throw insufficient("ETH", sender, fee, eth, "wei"); } if (!opts.confirm) { return { preview: true, network, asset, amount_usd_micros: Number(amount), from: payer, to, sender, balance_usd_micros: Number(balance), estimated_gas: gas === null ? null : Number(gas), }; } const walletClient = viem.createWalletClient({ account, chain, transport }); const hash = await walletClient.writeContract({ ...call, account, chain }); const explorer = chain.blockExplorers?.default.url ?? ""; const explorerUrl = explorer ? `${explorer.replace(/\/$/, "")}/tx/${hash}` : hash; const receipt = await publicClient.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") { throw new LocalError(`The send transaction ${hash} reverted.`, CONTEXT, { code: "SEND_REVERTED", details: { tx_hash: hash, network, explorer_url: explorerUrl }, next_actions: [{ type: "edit_request", why: "Inspect the transaction on the explorer before retrying." }], }); } return { tx_hash: hash, from: payer, to, amount_usd_micros: Number(amount), network, asset, explorer_url: explorerUrl }; } //# sourceMappingURL=wallets-send.js.map