// End-to-end test for the test-mpp service on the relay. // // Fill in the CONFIG block, then run from the isolated install dir // (same workaround as tempo-transfer.ts — repo node_modules has a // @noble/hashes conflict that breaks viem here): // // mkdir -p /tmp/tempo-transfer && cd /tmp/tempo-transfer \ // && npm init -y && npm install viem@2.47.6 mppx tsx // /tmp/tempo-transfer/node_modules/.bin/tsx scripts/test-mpp-call.ts // // What it does: // 1. Reads on-chain USDC balance for the bot wallet on Tempo. // 2. POSTs to /api/services/test-mpp/use through the mppx client, // which auto-handles the 402 → sign USDC transfer → retry flow. // 3. Prints status, body, Payment-Receipt header, and the new balance. // // Expected first-run output (with $0 account balance, $1 wallet): // paidVia=mpp · balance drops by $0.01 · Payment-Receipt header present. // ─── CONFIG ────────────────────────────────────────────────────────────────── const RELAY = 'https://api.bloby.bot'; const BOT = { // Bot's relay token — the value from `bloby init` / config.json `relay.token`. // Used as `X-Bloby-Token` header so identity survives the mppx 402 retry. relayToken: '', // Bot's USDC wallet on Tempo. Must match the `walletAddress` registered // with the relay (so account-balance lookups go through the right user). privateKey: '0x0a5194196773b67790f0da564337929b842c1c17f9c445b6c449205dbd426a49', wallet: '0xc65d8a0dB80f637337B87e42b8BEb5D86D46f942', }; // ───────────────────────────────────────────────────────────────────────────── import { Mppx, tempo } from 'mppx/client'; import { privateKeyToAccount } from 'viem/accounts'; import { createPublicClient, http, formatUnits } from 'viem'; import { tempo as tempoChain } from 'viem/chains'; const USDC = '0x20c000000000000000000000b9537d11c60e8b50' as const; const RPC = 'https://rpc.tempo.xyz'; const erc20Abi = [{ name: 'balanceOf', type: 'function', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }], outputs: [{ name: '', type: 'uint256' }], }] as const; function normalizePk(pk: string): `0x${string}` { const t = pk.trim(); return (t.startsWith('0x') ? t : `0x${t}`) as `0x${string}`; } async function main() { if (BOT.relayToken.startsWith('<')) { console.error('Fill in BOT.relayToken before running.'); process.exit(1); } const account = privateKeyToAccount(normalizePk(BOT.privateKey)); if (account.address.toLowerCase() !== BOT.wallet.toLowerCase()) { console.error(`privateKey derives ${account.address}, but BOT.wallet is ${BOT.wallet}`); process.exit(1); } const publicClient = createPublicClient({ chain: tempoChain, transport: http(RPC) }); const before = await publicClient.readContract({ address: USDC, abi: erc20Abi, functionName: 'balanceOf', args: [account.address], }); console.log(`Wallet: ${account.address}`); console.log(`Balance before: ${formatUnits(before, 6)} USDC`); // mppx client: do NOT polyfill global fetch — only the request through // mppx.fetch(...) gets the auto-pay-on-402 behavior. const mppx = Mppx.create({ polyfill: false, methods: [tempo({ account })], }); const url = `${RELAY}/api/services/test-mpp/use`; console.log(`POST ${url}`); const res = await mppx.fetch(url, { method: 'POST', headers: { 'X-Bloby-Token': BOT.relayToken, 'Content-Type': 'application/json', }, body: JSON.stringify({}), }); const body = await res.text(); const receipt = res.headers.get('Payment-Receipt'); console.log(`Status: ${res.status}`); console.log(`Body: ${body}`); console.log(`Payment-Receipt: ${receipt ?? '(none — paid via balance or free)'}`); const after = await publicClient.readContract({ address: USDC, abi: erc20Abi, functionName: 'balanceOf', args: [account.address], }); console.log(`Balance after: ${formatUnits(after, 6)} USDC`); const delta = before - after; console.log(`Delta: -${formatUnits(delta, 6)} USDC`); } main().catch((err) => { console.error(err); process.exit(1); });