Skip to content

Commit e17645e

Browse files
MajorTalclaude
andcommitted
fix(cli): init mpp polls faucet balance and updates funded summary
Before this change, `run402 init mpp --json` reported `allowance.funded: false` and `balance.usd_micros: 0` even after a successful Tempo faucet top-up. Two problems: 1. `summary.allowance` was snapshotted BEFORE the faucet branch ran (captured from the pre-faucet `allowance.funded`), so the JSON emit never reflected the post-faucet state even though `saveAllowance(...)` persisted `funded: true`. 2. The MPP path re-read the balance only once after the faucet call, while the Tempo RPC read is racy relative to faucet settlement (the human-readable line was "faucet sent — checking balance..." and then the JSON reported usd_micros: 0). Fix: after a successful faucet on the MPP rail, poll `readContract` up to 30 times with 1s intervals — mirroring the x402 path that has used this pattern since initial MPP support. On both rails, write the up-to-date `funded` state onto `summary.allowance` so the final JSON matches the allowance storage and the human-readable output. Regression test (`init mpp --json reports funded=true after faucet settles (GH-81)`) asserts `summary.allowance.funded === true` and `summary.balance.usd_micros > 0` after the faucet branch runs. The test fails on pre-fix `init.mjs` (reproduces the issue exactly). Refs #81 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 13138a8 commit e17645e

2 files changed

Lines changed: 36 additions & 6 deletions

File tree

cli-e2e.test.mjs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,6 +1575,26 @@ describe("CLI e2e happy path", () => {
15751575
assert.equal(allowance.rail, "mpp", "rail should be mpp");
15761576
});
15771577

1578+
// GH-81: after MPP faucet succeeds, `--json` summary must reflect funded=true
1579+
// and the polled balance. Previously `summary.allowance` was captured before
1580+
// the faucet branch ran, so the JSON reported `funded: false` and
1581+
// `usd_micros: 0` even when the human-readable lines said "funded".
1582+
it("init mpp --json reports funded=true after faucet settles (GH-81)", async () => {
1583+
tempoRpcCallCount = 0; // first eth_call returns 0 → triggers faucet path
1584+
const { run } = await import("./cli/lib/init.mjs");
1585+
captureStart();
1586+
await run(["mpp", "--json"]);
1587+
captureStop();
1588+
const stdout = capturedStdout();
1589+
const parsed = JSON.parse(stdout);
1590+
assert.equal(parsed.rail, "mpp", `rail must be mpp; got: ${parsed.rail}`);
1591+
assert.equal(parsed.allowance.funded, true,
1592+
`summary.allowance.funded must be true after successful faucet; got: ${JSON.stringify(parsed.allowance)}`);
1593+
assert.equal(parsed.balance.symbol, "pathUSD");
1594+
assert.ok(parsed.balance.usd_micros > 0,
1595+
`summary.balance.usd_micros must reflect polled balance (>0); got: ${parsed.balance.usd_micros}`);
1596+
});
1597+
15781598
it("allowance status (MPP rail)", async () => {
15791599
const { run } = await import("./cli/lib/allowance.mjs");
15801600
captureStart();

cli/lib/init.mjs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,23 @@ export async function run(args = []) {
112112
});
113113
const data = await res.json();
114114
if (data.result) {
115-
// Tempo faucet is instant — re-read balance once
116-
try {
117-
const raw = await client.readContract({ address: PATH_USD, abi: USDC_ABI, functionName: "balanceOf", args: [allowance.address] });
118-
balance = Number(raw);
119-
} catch {}
115+
// Tempo faucet is "instant" on-chain, but the client RPC read can be
116+
// racy relative to faucet settlement — poll up to 30s (GH-81), mirroring
117+
// the x402 path below.
118+
for (let i = 0; i < 30; i++) {
119+
await new Promise(r => setTimeout(r, 1000));
120+
try {
121+
const raw = await client.readContract({ address: PATH_USD, abi: USDC_ABI, functionName: "balanceOf", args: [allowance.address] });
122+
balance = Number(raw);
123+
if (balance > 0) break;
124+
} catch {}
125+
}
120126
saveAllowance({ ...allowance, funded: true, lastFaucet: new Date().toISOString() });
127+
summary.allowance.funded = true;
121128
if (balance > 0) {
122129
line("Balance", `${(balance / 1e6).toFixed(2)} pathUSD (funded)`);
123130
} else {
124-
line("Balance", "faucet sent — checking balance...");
131+
line("Balance", "faucet sent — not yet confirmed on-chain");
125132
}
126133
} else {
127134
line("Balance", `faucet failed: ${data.error?.message || "unknown error"}`);
@@ -131,6 +138,7 @@ export async function run(args = []) {
131138
}
132139
} else {
133140
line("Balance", `${(balance / 1e6).toFixed(2)} pathUSD`);
141+
summary.allowance.funded = balance > 0;
134142
}
135143
summary.balance = { symbol: "pathUSD", usd_micros: balance };
136144
} else {
@@ -162,6 +170,7 @@ export async function run(args = []) {
162170
} catch {}
163171
}
164172
saveAllowance({ ...allowance, funded: true, lastFaucet: new Date().toISOString() });
173+
summary.allowance.funded = true;
165174
if (balance > 0) {
166175
line("Balance", `${(balance / 1e6).toFixed(2)} USDC (funded)`);
167176
} else {
@@ -174,6 +183,7 @@ export async function run(args = []) {
174183
}
175184
} else {
176185
line("Balance", `${(balance / 1e6).toFixed(2)} USDC`);
186+
summary.allowance.funded = balance > 0;
177187
}
178188
summary.balance = { symbol: "USDC", usd_micros: balance };
179189
}

0 commit comments

Comments
 (0)