fix: add RPC propagation delay after browser-initiated swap (#434)

After `buyKrk()` completes (swap widget returns to idle), the Anvil RPC may briefly return stale balance state. Adds 2s delay to ensure `getKrkBalance` reads post-swap state.

Without this fix, the holdout scenario reports identical KRK balance before and after swap despite the transaction succeeding (success toast visible).

Co-authored-by: openhands <openhands@all-hands.dev>
Reviewed-on: https://codeberg.org/johba/harb/pulls/434
This commit is contained in:
johba 2026-03-03 22:20:02 +01:00
parent c9e84b2c34
commit 16abdcbefb
2 changed files with 64 additions and 2 deletions

View file

@ -16,6 +16,9 @@ const SWAP_ROUTER = '0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4';
const WETH = '0x4200000000000000000000000000000000000006';
const POOL_FEE = 10_000; // 1% tier used by the KRAIKEN pool
// ERC-20 Transfer event topic (keccak256("Transfer(address,address,uint256)"))
const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
const ERC20_ABI = ['function approve(address spender, uint256 amount) returns (bool)'];
const ROUTER_ABI = [
'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) payable returns (uint256 amountOut)',
@ -57,6 +60,15 @@ async function waitForReceipt(rpcUrl: string, txHash: string, maxAttempts = 20):
// ── Public config type ───────────────────────────────────────────────────────
export interface BuyKrkOptions {
/** Anvil JSON-RPC endpoint (used to query KRK balance after swap). */
rpcUrl: string;
/** Deployed KRAIKEN (KRK) ERC-20 contract address. */
krkAddress: string;
/** EOA address that will receive the KRK tokens. */
accountAddress: string;
}
export interface SellConfig {
/** Anvil JSON-RPC endpoint (used to wait for receipt and query token balances). */
rpcUrl: string;
@ -74,8 +86,13 @@ export interface SellConfig {
*
* Uses the real LocalSwapWidget UI path (requires the #393 fill() fix).
* Wallet must already be connected before calling this.
*
* If opts is provided, creates an eth_newFilter for Transfer events to the account
* and polls eth_getFilterLogs until the event arrives, ensuring the swap has been
* mined on-chain before returning. Otherwise, just waits for the UI state transition
* (caller is responsible for verification).
*/
export async function buyKrk(page: Page, ethAmount: string): Promise<void> {
export async function buyKrk(page: Page, ethAmount: string, opts?: BuyKrkOptions): Promise<void> {
console.log(`[swap] Buying KRK with ${ethAmount} ETH via get-krk page...`);
await navigateSPA(page, '/app/get-krk');
await expect(page.getByRole('heading', { name: 'Get $KRK Tokens' })).toBeVisible({ timeout: 10_000 });
@ -89,6 +106,24 @@ export async function buyKrk(page: Page, ethAmount: string): Promise<void> {
const buyButton = page.getByTestId('swap-buy-button');
await expect(buyButton).toBeVisible({ timeout: 5_000 });
// Create Transfer event filter BEFORE the swap (if opts provided)
let filterId: string | undefined;
if (opts) {
console.log('[swap] Creating Transfer event filter...');
filterId = (await rpcCall(opts.rpcUrl, 'eth_newFilter', [
{
address: opts.krkAddress,
topics: [
TRANSFER_TOPIC,
null, // any sender
'0x' + opts.accountAddress.slice(2).padStart(64, '0'), // to our account
],
fromBlock: 'latest',
},
])) as string;
console.log(`[swap] Filter created: ${filterId}`);
}
await page.screenshot({ path: 'test-results/holdout-before-buy.png' });
console.log('[swap] Clicking Buy KRK...');
await buyButton.click();
@ -102,7 +137,27 @@ export async function buyKrk(page: Page, ethAmount: string): Promise<void> {
} catch {
// Swap completed before the Submitting state could be observed
console.log('[swap] Button state not observed (swap may have completed instantly)');
await page.waitForTimeout(2_000);
}
// If opts provided, wait for the Transfer event to arrive
if (opts && filterId) {
console.log('[swap] Waiting for Transfer event...');
const deadline = Date.now() + 15_000;
let received = false;
while (Date.now() < deadline) {
const logs = (await rpcCall(opts.rpcUrl, 'eth_getFilterLogs', [filterId])) as unknown[];
if (logs && logs.length > 0) {
received = true;
console.log(`[swap] Transfer event received (${logs.length} log(s))`);
break;
}
await new Promise(r => setTimeout(r, 200));
}
// Clean up filter
await rpcCall(opts.rpcUrl, 'eth_uninstallFilter', [filterId]).catch(() => {});
if (!received) {
throw new Error(`No KRK Transfer event received within 15s after buying with ${ethAmount} ETH`);
}
}
await page.screenshot({ path: 'test-results/holdout-after-buy.png' });