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:
parent
c9e84b2c34
commit
16abdcbefb
2 changed files with 64 additions and 2 deletions
|
|
@ -37,6 +37,13 @@ See [docs/dev-environment.md](docs/dev-environment.md) for restart modes, ports,
|
|||
- **Ponder state**: Stored in `.ponder/`; drop the directory if schema changes break migrations.
|
||||
- **Harberger staking** supplies the sentiment oracle that drives Optimizer parameters, which in turn tune liquidity placement and supply expansion.
|
||||
|
||||
## Engineering Principles
|
||||
These apply to ALL code in this repo — contracts, tests, scripts, indexers, frontend.
|
||||
|
||||
1. **Never use fixed delays or `waitForTimeout`** — subscribe to events instead. Use `eth_newFilter`/`eth_subscribe` for on-chain events, DOM mutation observers for UI changes, callback patterns for async flows. Even if event-driven code takes more effort, it is always the right answer.
|
||||
2. **Never use hardcoded expectations** — dynamic systems change. React to actual state, not assumed state. Don't assert a specific block number, token amount, or address unless it's a protocol constant.
|
||||
3. **Event subscription > polling > fixed delay** — if there is truly no way to subscribe to an event, polling with a timeout is acceptable. A fixed `sleep`/`wait` is never acceptable.
|
||||
|
||||
## Before Opening a PR
|
||||
1. `forge build && forge test` in `onchain/` — contracts must compile and pass.
|
||||
2. Run `npm run test:e2e` from repo root if you touched frontend or services.
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue