diff --git a/.changeset/fix-mainnet-default.md b/.changeset/fix-mainnet-default.md new file mode 100644 index 00000000..64246b36 --- /dev/null +++ b/.changeset/fix-mainnet-default.md @@ -0,0 +1,5 @@ +--- +'mppx': patch +--- + +Fixed CLI defaulting to testnet when `--rpc-url` is omitted. The CLI now defaults to Tempo mainnet. Also added `resolveRpcUrl` helper so `MPPX_RPC_URL` and `RPC_URL` env vars are respected consistently across all commands. diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 48b58f63..8f336688 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -32,6 +32,7 @@ import { printResponseHeaders, prompt, resolveChain, + resolveRpcUrl, } from './utils.js' const packageJson = createRequire(import.meta.url)('../../package.json') as { @@ -516,8 +517,9 @@ const account = Cli.create('account', { ? link(`${explorerUrl}/address/${acct.address}`, acct.address) : acct.address console.log(pc.dim(`Address ${addrDisplay}`)) - resolveChain(c.options) - .then((chain) => createClient({ chain, transport: http(c.options.rpcUrl) })) + const rpcUrl = resolveRpcUrl(c.options.rpcUrl) + resolveChain({ rpcUrl }) + .then((chain) => createClient({ chain, transport: http(rpcUrl) })) .then((client) => import('viem/tempo').then(({ Actions }) => Actions.faucet.fund(client, { account: acct }).catch(() => {}), @@ -629,8 +631,9 @@ const account = Cli.create('account', { return c.error({ code: 'ACCOUNT_NOT_FOUND', message: 'No account found.', exitCode: 69 }) } const acct = privateKeyToAccount(key as `0x${string}`) - const chain = await resolveChain(c.options) - const client = createClient({ chain, transport: http(c.options.rpcUrl) }) + const rpcUrl = resolveRpcUrl(c.options.rpcUrl) + const chain = await resolveChain({ rpcUrl }) + const client = createClient({ chain, transport: http(rpcUrl) }) console.log(`Funding "${accountName}" on ${chainName(chain)}`) try { const { Actions } = await import('viem/tempo') @@ -711,8 +714,8 @@ const account = Cli.create('account', { }) } const address = tempoEntry.wallet_address as Address - const rpcUrl = c.options.rpcUrl ?? (process.env.MPPX_RPC_URL || undefined) - const chain = rpcUrl ? await resolveChain({ rpcUrl }) : tempoMainnet + const rpcUrl = resolveRpcUrl(c.options.rpcUrl) + const chain = await resolveChain({ rpcUrl }) const explorerUrl = chain.blockExplorers?.default?.url const addrDisplay = explorerUrl ? link(`${explorerUrl}/address/${address}`, address) @@ -744,8 +747,8 @@ const account = Cli.create('account', { return c.error({ code: 'ACCOUNT_NOT_FOUND', message: 'No account found.', exitCode: 69 }) } const acct = privateKeyToAccount(key as `0x${string}`) - const rpcUrl = c.options.rpcUrl ?? (process.env.MPPX_RPC_URL || undefined) - const chain = rpcUrl ? await resolveChain({ rpcUrl }) : tempoMainnet + const rpcUrl = resolveRpcUrl(c.options.rpcUrl) + const chain = await resolveChain({ rpcUrl }) const explorerUrl = chain.blockExplorers?.default?.url const addrDisplay = explorerUrl ? link(`${explorerUrl}/address/${acct.address}`, acct.address) diff --git a/src/cli/plugins/tempo.ts b/src/cli/plugins/tempo.ts index 25fcb40b..4ed822c9 100644 --- a/src/cli/plugins/tempo.ts +++ b/src/cli/plugins/tempo.ts @@ -24,6 +24,7 @@ import { link, pc, resolveChain, + resolveRpcUrl, } from '../utils.js' import { createPlugin, type Plugin } from './plugin.js' @@ -67,7 +68,7 @@ export function tempo() { useTempoCliSign = true const tempoEntry = resolveTempoAccount(accountName) if (tempoEntry) { - const rpcUrl = options.rpcUrl ?? process.env.RPC_URL + const rpcUrl = resolveRpcUrl(options.rpcUrl) client = createClient({ chain: await resolveChain({ rpcUrl }), transport: http(rpcUrl), @@ -107,7 +108,7 @@ export function tempo() { } else account = privateKeyToAccount(privateKey as `0x${string}`) if (!useTempoCliSign && account) { - const rpcUrl = options.rpcUrl ?? process.env.RPC_URL + const rpcUrl = resolveRpcUrl(options.rpcUrl) client = createClient({ chain: await resolveChain({ rpcUrl }), transport: http(rpcUrl), diff --git a/src/cli/utils.test.ts b/src/cli/utils.test.ts new file mode 100644 index 00000000..cf4320fb --- /dev/null +++ b/src/cli/utils.test.ts @@ -0,0 +1,64 @@ +import { tempo as tempoMainnet, tempoModerato } from 'viem/chains' +import { afterEach, describe, expect, test } from 'vp/test' + +import { resolveChain, resolveRpcUrl } from './utils.js' + +describe('resolveRpcUrl', () => { + afterEach(() => { + delete process.env.MPPX_RPC_URL + delete process.env.RPC_URL + }) + + test('returns explicit value when provided', () => { + process.env.MPPX_RPC_URL = 'https://env.example.com' + expect(resolveRpcUrl('https://explicit.example.com')).toBe('https://explicit.example.com') + }) + + test('falls back to MPPX_RPC_URL env var', () => { + process.env.MPPX_RPC_URL = 'https://mppx.example.com' + process.env.RPC_URL = 'https://rpc.example.com' + expect(resolveRpcUrl()).toBe('https://mppx.example.com') + }) + + test('falls back to RPC_URL env var when MPPX_RPC_URL is not set', () => { + process.env.RPC_URL = 'https://rpc.example.com' + expect(resolveRpcUrl()).toBe('https://rpc.example.com') + }) + + test('returns undefined when nothing is set', () => { + expect(resolveRpcUrl()).toBeUndefined() + }) + + test('trims whitespace from env vars', () => { + process.env.MPPX_RPC_URL = ' https://mppx.example.com ' + expect(resolveRpcUrl()).toBe('https://mppx.example.com') + }) + + test('skips empty MPPX_RPC_URL and falls back to RPC_URL', () => { + process.env.MPPX_RPC_URL = ' ' + process.env.RPC_URL = 'https://rpc.example.com' + expect(resolveRpcUrl()).toBe('https://rpc.example.com') + }) +}) + +describe('resolveChain', () => { + afterEach(() => { + delete process.env.MPPX_RPC_URL + delete process.env.RPC_URL + }) + + test('defaults to tempo mainnet when no rpcUrl is provided', async () => { + const chain = await resolveChain() + expect(chain.id).toBe(tempoMainnet.id) + }) + + test('defaults to tempo mainnet when rpcUrl is undefined', async () => { + const chain = await resolveChain({ rpcUrl: undefined }) + expect(chain.id).toBe(tempoMainnet.id) + }) + + test('does not default to testnet', async () => { + const chain = await resolveChain() + expect(chain.id).not.toBe(tempoModerato.id) + }) +}) diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 7fc68589..28fd5ad4 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -221,17 +221,23 @@ export function fmtBalance( return `${dec ? `${formatted}.${dec}` : formatted} ${sym}` } +/** Resolve RPC URL from explicit option, then MPPX_RPC_URL, then RPC_URL env vars. */ +export function resolveRpcUrl(explicit?: string | undefined): string | undefined { + return explicit ?? (process.env.MPPX_RPC_URL?.trim() || process.env.RPC_URL?.trim() || undefined) +} + export async function resolveChain(opts: { rpcUrl?: string | undefined } = {}): Promise { - if (!opts.rpcUrl) return tempoModerato + const rpcUrl = resolveRpcUrl(opts.rpcUrl) + if (!rpcUrl) return tempoMainnet const { getChainId } = await import('viem/actions') - const chainId = await getChainId(createClient({ transport: http(opts.rpcUrl) })) + const chainId = await getChainId(createClient({ transport: http(rpcUrl) })) const allExports = Object.values(await import('viem/chains')) as unknown[] const candidates = allExports.filter( (c): c is Chain => typeof c === 'object' && c !== null && 'id' in c && (c as Chain).id === chainId, ) const found = candidates.find((c) => 'serializers' in c && c.serializers) ?? candidates[0] - if (!found) throw new Error(`Unknown chain ID ${chainId} from RPC ${opts.rpcUrl}`) + if (!found) throw new Error(`Unknown chain ID ${chainId} from RPC ${rpcUrl}`) return found } @@ -306,7 +312,7 @@ export async function fetchBalanceLines( const mainnetClient = createClient({ chain: tempoMainnet, - transport: http(process.env.MPPX_RPC_URL || undefined), + transport: http(resolveRpcUrl()), }) const mainnetExplorerUrl = tempoMainnet.blockExplorers?.default?.url const mainnetResults = await Promise.all(