From 8309fa4d9a9306eabf45b41984ffb4ed660b8a24 Mon Sep 17 00:00:00 2001 From: Parv Ahuja <17094219+parvahuja@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:33:28 -0400 Subject: [PATCH 1/2] feat: route funding through stablecoin onramp --- .changelog/route-funding-to-onramp.md | 5 +++++ src/commands/fund.ts | 18 ++++++++++++++---- test/fund-services.test.ts | 10 ++++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 .changelog/route-funding-to-onramp.md diff --git a/.changelog/route-funding-to-onramp.md b/.changelog/route-funding-to-onramp.md new file mode 100644 index 0000000..1c9113f --- /dev/null +++ b/.changelog/route-funding-to-onramp.md @@ -0,0 +1,5 @@ +--- +wallet-cli: patch +--- + +Open default wallet funding on the stablecoin onramp with the destination address prefilled. diff --git a/src/commands/fund.ts b/src/commands/fund.ts index b429efe..c8900e9 100644 --- a/src/commands/fund.ts +++ b/src/commands/fund.ts @@ -31,7 +31,10 @@ export async function runFundingFlow(options: { chainId: chainId(options.network), walletAddress, }); - const url = fundUrl(options.action, { code: options.code }); + const url = fundUrl(options.action, { + address: walletAddress ?? undefined, + code: options.code, + }); console.error(`Fund URL: ${url}`); console.error(`Open this link on your device: ${url}`); @@ -135,9 +138,16 @@ async function waitForFunding(options: { } } -export function fundUrl(action: FundAction, options: { code?: string | undefined } = {}) { - // The CLI is an agent/MPP surface, so all funding handoffs land on the dedicated - // /agent page rather than the consumer wallet home. +export function fundUrl( + action: FundAction, + options: { address?: string | undefined; code?: string | undefined } = {}, +) { + if (action === "fund") { + const url = new URL("https://wallet.tempo.xyz/onramp"); + if (options.address) url.searchParams.set("recipient", options.address); + return url.toString(); + } + const url = new URL("https://wallet.tempo.xyz/agent"); if (action === "claim" && options.code) { url.searchParams.set("claim", options.code); diff --git a/test/fund-services.test.ts b/test/fund-services.test.ts index 018fdfe..1b77ca9 100644 --- a/test/fund-services.test.ts +++ b/test/fund-services.test.ts @@ -33,8 +33,14 @@ describe("fundAction", () => { }); describe("fundUrl", () => { - it("routes every funding handoff to the /agent page", () => { - expect(fundUrl("fund")).toBe("https://wallet.tempo.xyz/agent?action=fund"); + it("routes default funding to the recipient-prefilled onramp", () => { + expect(fundUrl("fund", { address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" })).toBe( + "https://wallet.tempo.xyz/onramp?recipient=0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + ); + expect(fundUrl("fund")).toBe("https://wallet.tempo.xyz/onramp"); + }); + + it("keeps specialized funding handoffs on the agent page", () => { expect(fundUrl("crypto")).toBe("https://wallet.tempo.xyz/agent?action=crypto"); expect(fundUrl("credits")).toBe("https://wallet.tempo.xyz/agent?action=fund&intent=credits"); expect(fundUrl("claim", { code: "ABC123" })).toBe( From bb65379cca7b79f62527d1c3cc1e07c8ad6432b5 Mon Sep 17 00:00:00 2001 From: Parv Ahuja <17094219+parvahuja@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:16:05 -0400 Subject: [PATCH 2/2] fix: route credits funding to machineUSD --- .changelog/route-funding-to-onramp.md | 3 +- README.md | 3 +- src/cli.ts | 2 +- src/commands/fund.ts | 76 ++++++++++++++++++--------- src/schemas.ts | 2 +- test/fund-services.test.ts | 49 +++++++++++++++-- 6 files changed, 102 insertions(+), 33 deletions(-) diff --git a/.changelog/route-funding-to-onramp.md b/.changelog/route-funding-to-onramp.md index 1c9113f..a0b8c37 100644 --- a/.changelog/route-funding-to-onramp.md +++ b/.changelog/route-funding-to-onramp.md @@ -2,4 +2,5 @@ wallet-cli: patch --- -Open default wallet funding on the stablecoin onramp with the destination address prefilled. +Open default wallet funding on the stablecoin onramp with the destination address prefilled, and +route the legacy Credits purchase action to machineUSD. diff --git a/README.md b/README.md index fed0666..64b6dc8 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,8 @@ tempo wallet sessions close https://service.mpp.tempo.xyz - `debug` - `completions` -Credit-related flows use `whoami --credits`, `fund --credits`, and `transfer --credits`. +Coinflow balance and payment flows use `whoami --credits` and `transfer --credits`. The legacy +`fund --credits` flag now opens the machineUSD purchase flow. `tempo request` supports common curl-style flags for methods, headers, bodies, output files, redirects, retries, proxies, and streaming responses. diff --git a/src/cli.ts b/src/cli.ts index 4cffbc2..9915155 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -653,7 +653,7 @@ function describeCli() { "--crypto", "Open the direct crypto funding flow (bridge on mainnet, faucet on testnet)", ), - flag("credits", "--credits", "Open the credits purchase flow"), + flag("credits", "--credits", "Open the machineUSD purchase flow (legacy alias)"), option("referral_code", "--referral-code", "Referral code to claim while funding", { valueName: "CODE", }), diff --git a/src/commands/fund.ts b/src/commands/fund.ts index c8900e9..ac7c43e 100644 --- a/src/commands/fund.ts +++ b/src/commands/fund.ts @@ -1,4 +1,5 @@ import type { Provider as CoreProvider } from "accounts"; +import { isAddress } from "viem"; import { Actions } from "viem/tempo"; import { usageError } from "../shared/errors.js"; @@ -7,10 +8,16 @@ import { openExternal } from "../shared/process.js"; import { formatMicroUnits, sleep } from "../shared/utils.js"; import { createProvider } from "../provider.js"; import { loadWalletState } from "../wallet/store.js"; -import { queryCreditBalance } from "./credits.js"; + +const defaultMachineUsdOrigin = "https://machine-usd.porto.workers.dev"; export type FundAction = "fund" | "crypto" | "credits" | "claim"; +export type MachineUsdConfig = { + chainId: number; + tokenAddress: `0x${string}`; +}; + export async function runFundingFlow(options: { action: FundAction; address?: string | undefined; @@ -26,9 +33,14 @@ export async function runFundingFlow(options: { if (!walletAddress && options.action !== "claim") throw usageError("Configuration missing: No wallet configured. Run 'tempo wallet login'."); + const selectedChainId = chainId(options.network); + const machineUsdConfig = options.action === "credits" ? await queryMachineUsdConfig() : undefined; + if (machineUsdConfig && selectedChainId !== machineUsdConfig.chainId) + throw usageError("machineUSD funding is only available on Tempo mainnet."); + const initial = await fundingBalance({ - action: options.action, - chainId: chainId(options.network), + chainId: selectedChainId, + token: machineUsdConfig?.tokenAddress, walletAddress, }); const url = fundUrl(options.action, { @@ -41,18 +53,18 @@ export async function runFundingFlow(options: { if (!options.noBrowser) openExternal(url); if (options.action === "credits") { - console.error("Complete the credits purchase in the wallet app."); - console.error("After purchasing credits, return here to continue."); - console.error("Waiting for credits..."); + console.error("Complete the machineUSD purchase in the wallet app."); + console.error("After purchasing machineUSD, return here to continue."); + console.error("Waiting for machineUSD..."); } else { console.error("After funding is complete, return here to continue."); console.error("Waiting for funding..."); } const completed = await waitForFunding({ - action: options.action, - chainId: chainId(options.network), + chainId: selectedChainId, initialRawBalance: initial.rawBalance, + token: machineUsdConfig?.tokenAddress, walletAddress, }); console.error("Funding received!"); @@ -78,22 +90,10 @@ export function fundAction(options: { } async function fundingBalance(options: { - action: FundAction; chainId: number; + token?: `0x${string}` | undefined; walletAddress: string | null; }) { - if (options.action === "credits") { - if (!options.walletAddress) throw new Error("No wallet is logged in"); - const credits = await queryCreditBalance({ - chainId: options.chainId, - walletAddress: options.walletAddress, - }); - return { - balance: credits.balance, - rawBalance: BigInt(credits.rawBalance), - }; - } - if (!options.walletAddress) { return { balance: "0.000000", @@ -107,7 +107,7 @@ async function fundingBalance(options: { const rawBalance = ( await Actions.token.getBalance(provider.getClient() as never, { account: options.walletAddress as `0x${string}`, - token: tokenAddress(options.chainId), + token: options.token ?? tokenAddress(options.chainId), }) ).amount; @@ -118,9 +118,9 @@ async function fundingBalance(options: { } async function waitForFunding(options: { - action: FundAction; chainId: number; initialRawBalance: bigint; + token?: `0x${string}` | undefined; walletAddress: string | null; }) { const pollMs = Number(process.env.TEMPO_WALLET_FUND_POLL_MS ?? 2_000); @@ -142,9 +142,10 @@ export function fundUrl( action: FundAction, options: { address?: string | undefined; code?: string | undefined } = {}, ) { - if (action === "fund") { + if (action === "fund" || action === "credits") { const url = new URL("https://wallet.tempo.xyz/onramp"); if (options.address) url.searchParams.set("recipient", options.address); + if (action === "credits") url.searchParams.set("token", "MACHUSD"); return url.toString(); } @@ -153,7 +154,30 @@ export function fundUrl( url.searchParams.set("claim", options.code); return url.toString(); } - url.searchParams.set("action", action === "credits" ? "fund" : action); - if (action === "credits") url.searchParams.set("intent", "credits"); + url.searchParams.set("action", action); return url.toString(); } + +/** Reads the deployed token address and chain used to detect a completed machineUSD purchase. */ +export async function queryMachineUsdConfig(options: { origin?: string | undefined } = {}) { + const origin = options.origin || process.env.TEMPO_MACHINE_USD_ORIGIN || defaultMachineUsdOrigin; + const response = await fetch(new URL("/v1/config", `${origin.replace(/\/$/, "")}/`).toString()); + const body = (await response.json().catch(() => null)) as { + chain_id?: unknown; + token_address?: unknown; + } | null; + if (!response.ok) throw new Error("Unable to load machineUSD configuration"); + if ( + !body || + typeof body.chain_id !== "number" || + !Number.isSafeInteger(body.chain_id) || + typeof body.token_address !== "string" || + !isAddress(body.token_address, { strict: false }) + ) + throw new Error("machineUSD configuration is invalid"); + + return { + chainId: body.chain_id, + tokenAddress: body.token_address as `0x${string}`, + } satisfies MachineUsdConfig; +} diff --git a/src/schemas.ts b/src/schemas.ts index bde2edc..cb3f50b 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -323,7 +323,7 @@ export const fundOptions = z.object({ address: z.string().optional().describe("Wallet address to fund (defaults to current wallet)"), browser: z.boolean().default(true).describe("Open a browser; use --no-browser to disable"), crypto: z.boolean().optional().describe("Open the direct crypto funding flow"), - credits: z.boolean().optional().describe("Open the credits purchase flow"), + credits: z.boolean().optional().describe("Open the machineUSD purchase flow (legacy alias)"), "referral-code": z.string().optional().describe("Open referral-code redeem flow"), claim: z.string().optional().describe("Alias for --referral-code"), }); diff --git a/test/fund-services.test.ts b/test/fund-services.test.ts index 1b77ca9..e355837 100644 --- a/test/fund-services.test.ts +++ b/test/fund-services.test.ts @@ -1,6 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { fundAction, fundUrl, runFundingFlow } from "../src/commands/fund.js"; +import { + fundAction, + fundUrl, + queryMachineUsdConfig, + runFundingFlow, +} from "../src/commands/fund.js"; import { fetchServices, fetchServiceList } from "../src/commands/services.js"; import { expectUsageError, useTempHome } from "./helpers.js"; @@ -40,15 +45,53 @@ describe("fundUrl", () => { expect(fundUrl("fund")).toBe("https://wallet.tempo.xyz/onramp"); }); - it("keeps specialized funding handoffs on the agent page", () => { + it("routes the legacy credits action to machineUSD", () => { + expect(fundUrl("credits", { address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" })).toBe( + "https://wallet.tempo.xyz/onramp?recipient=0x70997970C51812dc3A010C7d01b50e0d17dc79C8&token=MACHUSD", + ); + }); + + it("keeps the other specialized funding handoffs on the agent page", () => { expect(fundUrl("crypto")).toBe("https://wallet.tempo.xyz/agent?action=crypto"); - expect(fundUrl("credits")).toBe("https://wallet.tempo.xyz/agent?action=fund&intent=credits"); expect(fundUrl("claim", { code: "ABC123" })).toBe( "https://wallet.tempo.xyz/agent?claim=ABC123", ); }); }); +describe("queryMachineUsdConfig", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("returns the public chain and token configuration", async () => { + const fetch = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response( + JSON.stringify({ + chain_id: 4217, + token_address: "0x20C0000000000000000000000000000000000001", + }), + ), + ); + + await expect(queryMachineUsdConfig({ origin: "https://machine.example" })).resolves.toEqual({ + chainId: 4217, + tokenAddress: "0x20C0000000000000000000000000000000000001", + }); + expect(fetch).toHaveBeenCalledWith("https://machine.example/v1/config"); + }); + + it("rejects malformed public configuration", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response(JSON.stringify({ chain_id: 4217, token_address: "not-an-address" })), + ); + + await expect(queryMachineUsdConfig({ origin: "https://machine.example" })).rejects.toThrow( + "machineUSD configuration is invalid", + ); + }); +}); + describe("runFundingFlow", () => { let consoleError: ReturnType;