Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .changelog/add-machine-usd-funding.md

This file was deleted.

2 changes: 0 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@ cli.command("fund", {
action: fundAction({
credits: options.credits,
crypto: options.crypto,
machineUsd: options["machine-usd"],
referralCode: options["referral-code"] ?? options.claim,
}),
address: options.address,
Expand Down Expand Up @@ -655,7 +654,6 @@ function describeCli() {
"Open the direct crypto funding flow (bridge on mainnet, faucet on testnet)",
),
flag("credits", "--credits", "Open the credits purchase flow"),
flag("machine_usd", "--machine-usd", "Open the machineUSD purchase flow"),
option("referral_code", "--referral-code", "Referral code to claim while funding", {
valueName: "CODE",
}),
Expand Down
74 changes: 8 additions & 66 deletions src/commands/fund.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Provider as CoreProvider } from "accounts";
import { isAddress } from "viem";
import { Actions } from "viem/tempo";

import { usageError } from "../shared/errors.js";
Expand All @@ -10,14 +9,7 @@ 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" | "machine-usd" | "claim";

export type MachineUsdConfig = {
chainId: number;
tokenAddress: `0x${string}`;
};
export type FundAction = "fund" | "crypto" | "credits" | "claim";

export async function runFundingFlow(options: {
action: FundAction;
Expand All @@ -34,29 +26,18 @@ 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 === "machine-usd" ? 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: selectedChainId,
token: machineUsdConfig?.tokenAddress,
chainId: chainId(options.network),
walletAddress,
});
const url = fundUrl(options.action, { address: walletAddress ?? undefined, code: options.code });
const url = fundUrl(options.action, { code: options.code });

console.error(`Fund URL: ${url}`);
console.error(`Open this link on your device: ${url}`);
if (!options.noBrowser) openExternal(url);

if (options.action === "machine-usd") {
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 if (options.action === "credits") {
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...");
Expand All @@ -67,9 +48,8 @@ export async function runFundingFlow(options: {

const completed = await waitForFunding({
action: options.action,
chainId: selectedChainId,
chainId: chainId(options.network),
initialRawBalance: initial.rawBalance,
token: machineUsdConfig?.tokenAddress,
walletAddress,
});
console.error("Funding received!");
Expand All @@ -86,10 +66,8 @@ export async function runFundingFlow(options: {
export function fundAction(options: {
credits?: boolean | undefined;
crypto?: boolean | undefined;
machineUsd?: boolean | undefined;
referralCode?: string | undefined;
}): FundAction {
if (options.machineUsd) return "machine-usd";
if (options.credits) return "credits";
if (options.crypto) return "crypto";
if (options.referralCode) return "claim";
Expand All @@ -99,7 +77,6 @@ export function fundAction(options: {
async function fundingBalance(options: {
action: FundAction;
chainId: number;
token?: `0x${string}` | undefined;
walletAddress: string | null;
}) {
if (options.action === "credits") {
Expand Down Expand Up @@ -127,7 +104,7 @@ async function fundingBalance(options: {
const rawBalance = (
await Actions.token.getBalance(provider.getClient() as never, {
account: options.walletAddress as `0x${string}`,
token: options.token ?? tokenAddress(options.chainId),
token: tokenAddress(options.chainId),
})
).amount;

Expand All @@ -141,7 +118,6 @@ 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);
Expand All @@ -159,49 +135,15 @@ async function waitForFunding(options: {
}
}

export function fundUrl(
action: FundAction,
options: { address?: string | undefined; code?: string | undefined } = {},
) {
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.
const url = new URL("https://wallet.tempo.xyz/agent");
if (action === "claim" && options.code) {
url.searchParams.set("claim", options.code);
return url.toString();
}
url.searchParams.set(
"action",
action === "credits" || action === "machine-usd" ? "fund" : action,
);
url.searchParams.set("action", action === "credits" ? "fund" : action);
if (action === "credits") url.searchParams.set("intent", "credits");
if (action === "machine-usd") {
url.searchParams.set("intent", "machine-usd");
if (options.address) url.searchParams.set("address", options.address);
}
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;
}
2 changes: 0 additions & 2 deletions src/compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,10 @@ async function runFundCompat(args: readonly string[]) {
action: fundAction({
credits: args.includes("--credits"),
crypto: args.includes("--crypto"),
machineUsd: args.includes("--machine-usd"),
referralCode: stringArg(args, "--referral-code") ?? stringArg(args, "--claim"),
Comment on lines 58 to 59

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject the withdrawn machineUSD flag

When a caller uses the previously supported tempo wallet fund --machine-usd, the compatibility handler still intercepts the invocation before the schema parser but now ignores this flag, causing fundAction to select the ordinary fund flow. Instead of reporting that machineUSD funding is unavailable, the CLI opens a different purchase flow and waits for the regular token balance, which can mislead callers into funding the wrong asset. Explicitly reject this withdrawn flag or allow the normal option parser to reject it.

Useful? React with 👍 / 👎.

}),
address: stringArg(args, "--address"),
code: stringArg(args, "--referral-code") ?? stringArg(args, "--claim"),
network: stringArg(args, "--network") ?? stringArg(args, "-n"),
noBrowser: args.includes("--no-browser"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the fund network argument

When tempo wallet fund --network testnet (or -n testnet) is used, main() routes the command through handleCompatCommand before cli.serve, and this call now omits the parsed network. runFundingFlow consequently defaults to mainnet and polls the mainnet token balance, so testnet faucet funding is never detected and the command can wait indefinitely. Keep forwarding the network while reverting only the machineUSD-specific behavior.

Useful? React with 👍 / 👎.

});
printCompatOutput(result, args);
Expand Down
1 change: 0 additions & 1 deletion src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,6 @@ export const fundOptions = z.object({
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"),
"machine-usd": z.boolean().optional().describe("Open the machineUSD purchase flow"),
"referral-code": z.string().optional().describe("Open referral-code redeem flow"),
claim: z.string().optional().describe("Alias for --referral-code"),
});
Expand Down
30 changes: 0 additions & 30 deletions test/fund-compat.test.ts

This file was deleted.

53 changes: 1 addition & 52 deletions test/fund-services.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import {
fundAction,
fundUrl,
queryMachineUsdConfig,
runFundingFlow,
} from "../src/commands/fund.js";
import { fundAction, fundUrl, runFundingFlow } from "../src/commands/fund.js";
import { fetchServices, fetchServiceList } from "../src/commands/services.js";
import { expectUsageError, useTempHome } from "./helpers.js";

Expand All @@ -24,10 +19,6 @@ describe("fundAction", () => {
expect(fundAction({ crypto: true })).toBe("crypto");
});

it('returns "machine-usd" when machineUsd is set', () => {
expect(fundAction({ machineUsd: true })).toBe("machine-usd");
});

it('returns "claim" when a referral code is provided', () => {
expect(fundAction({ referralCode: "ABC" })).toBe("claim");
});
Expand All @@ -36,12 +27,6 @@ describe("fundAction", () => {
expect(fundAction({ credits: true, crypto: true, referralCode: "ABC" })).toBe("credits");
});

it("prioritizes machineUSD over the other funding intents", () => {
expect(fundAction({ credits: true, crypto: true, machineUsd: true, referralCode: "ABC" })).toBe(
"machine-usd",
);
});

it("prioritizes crypto over referral code", () => {
expect(fundAction({ crypto: true, referralCode: "ABC" })).toBe("crypto");
});
Expand All @@ -52,48 +37,12 @@ describe("fundUrl", () => {
expect(fundUrl("fund")).toBe("https://wallet.tempo.xyz/agent?action=fund");
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("machine-usd", { address: "0x1111111111111111111111111111111111111111" })).toBe(
"https://wallet.tempo.xyz/agent?action=fund&intent=machine-usd&address=0x1111111111111111111111111111111111111111",
);
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<typeof vi.spyOn>;

Expand Down