fix: address review findings in stake-rpc.ts (#518)
- Remove dead krkAddress field from UnstakeRpcConfig (bug) - Drop swap.js import to avoid transitive Playwright dependency; fix header comment to accurately describe the module boundary (warning) - Inline pollReceipt() returning TxReceipt so snatch receipt is reused for log parsing without a second round-trip (warning) - Use ZeroAddress from ethers instead of manual constant (info) - Add comment on fromBlock '0x0' genesis-scan caveat (info) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1af022624f
commit
722ecaaa0e
1 changed files with 40 additions and 16 deletions
|
|
@ -1,19 +1,23 @@
|
|||
/**
|
||||
* RPC-only staking helpers for the red-team agent.
|
||||
*
|
||||
* All functions use ethers directly — no Playwright/browser dependency.
|
||||
* Uses the same ethers + rpcCall pattern as market.ts and recenter.ts.
|
||||
* No browser UI interaction required. Uses ethers + rpcCall directly
|
||||
* (same pattern as market.ts and recenter.ts).
|
||||
*
|
||||
* stakeViaRpc — approve KRK to Stake, call snatch() with empty positionsToSnatch
|
||||
* unstakeViaRpc — call exitPosition()
|
||||
* Note: importing from swap.js would drag in Playwright via its top-level
|
||||
* `import { expect } from '@playwright/test'`. This file avoids that import
|
||||
* by inlining a receipt poller that returns the receipt object.
|
||||
*
|
||||
* stakeViaRpc — approve KRK to Stake, call snatch() with empty positionsToSnatch
|
||||
* unstakeViaRpc — call exitPosition()
|
||||
* getStakingPositions — scan PositionCreated events and filter active positions
|
||||
* getStakingState — read averageTaxRate and percentageStaked from the contract
|
||||
* getStakingState — read averageTaxRate and percentageStaked from the contract
|
||||
*/
|
||||
import { Interface, JsonRpcProvider, Wallet } from 'ethers';
|
||||
import { Interface, JsonRpcProvider, Wallet, ZeroAddress } from 'ethers';
|
||||
import { rpcCall } from './rpc.js';
|
||||
import { waitForReceipt } from './swap.js';
|
||||
|
||||
const STAKE_ABI = [
|
||||
// taxRate param is the index into the TAX_RATES array (0-4), not a raw rate value
|
||||
'function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] positionsToSnatch) returns (uint256 positionId)',
|
||||
'function exitPosition(uint256 positionId)',
|
||||
'function positions(uint256 positionId) view returns (uint256 share, address owner, uint32 creationTime, uint32 lastTaxTime, uint32 taxRate)',
|
||||
|
|
@ -37,7 +41,6 @@ export interface UnstakeRpcConfig {
|
|||
rpcUrl: string;
|
||||
privateKey: string;
|
||||
stakeAddress: string;
|
||||
krkAddress: string;
|
||||
positionId: bigint;
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +64,27 @@ export interface StakingState {
|
|||
|
||||
const stakeIface = new Interface(STAKE_ABI);
|
||||
const erc20Iface = new Interface(ERC20_ABI);
|
||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||
|
||||
type ReceiptLog = { address: string; topics: string[]; data: string };
|
||||
type TxReceipt = { status: string; logs: ReceiptLog[] };
|
||||
|
||||
/**
|
||||
* Poll eth_getTransactionReceipt until the transaction is mined.
|
||||
* Returns the receipt so callers can parse logs without a second round-trip.
|
||||
* Throws if the transaction reverts (status 0x0) or times out.
|
||||
*/
|
||||
async function pollReceipt(rpcUrl: string, txHash: string, maxAttempts = 20): Promise<TxReceipt> {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const receipt = (await rpcCall(rpcUrl, 'eth_getTransactionReceipt', [txHash])) as TxReceipt | null;
|
||||
if (receipt !== null) {
|
||||
if (receipt.status === '0x0') throw new Error(`Transaction ${txHash} reverted (status 0x0)`);
|
||||
return receipt;
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-syntax -- Polling with timeout: no push source for tx receipt over HTTP RPC. See AGENTS.md #Engineering Principles.
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
throw new Error(`Transaction ${txHash} not mined after ${maxAttempts * 500}ms`);
|
||||
}
|
||||
|
||||
// ── Exported helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -80,7 +103,7 @@ export async function stakeViaRpc(config: StakeRpcConfig): Promise<bigint> {
|
|||
console.log(`[stake-rpc] Approving ${config.amount} KRK to Stake contract...`);
|
||||
const approveData = erc20Iface.encodeFunctionData('approve', [config.stakeAddress, config.amount]);
|
||||
const approveTx = await wallet.sendTransaction({ to: config.krkAddress, data: approveData });
|
||||
await waitForReceipt(config.rpcUrl, approveTx.hash);
|
||||
await pollReceipt(config.rpcUrl, approveTx.hash);
|
||||
console.log('[stake-rpc] Approve mined');
|
||||
|
||||
// Step 2: call snatch() — empty positionsToSnatch = simple stake with no snatching
|
||||
|
|
@ -94,15 +117,13 @@ export async function stakeViaRpc(config: StakeRpcConfig): Promise<bigint> {
|
|||
[],
|
||||
]);
|
||||
const snatchTx = await wallet.sendTransaction({ to: config.stakeAddress, data: snatchData });
|
||||
await waitForReceipt(config.rpcUrl, snatchTx.hash);
|
||||
// pollReceipt returns the receipt directly — no second round-trip needed for log parsing
|
||||
const receipt = await pollReceipt(config.rpcUrl, snatchTx.hash);
|
||||
console.log(`[stake-rpc] Stake mined: ${snatchTx.hash}`);
|
||||
|
||||
provider.destroy();
|
||||
|
||||
// Parse positionId from the PositionCreated event in the receipt
|
||||
const receipt = (await rpcCall(config.rpcUrl, 'eth_getTransactionReceipt', [snatchTx.hash])) as {
|
||||
logs: Array<{ address: string; topics: string[]; data: string }>;
|
||||
};
|
||||
const positionCreatedTopic = stakeIface.getEvent('PositionCreated')!.topicHash;
|
||||
const log = receipt.logs.find(
|
||||
l =>
|
||||
|
|
@ -129,7 +150,7 @@ export async function unstakeViaRpc(config: UnstakeRpcConfig): Promise<void> {
|
|||
console.log(`[stake-rpc] Calling exitPosition(${config.positionId})...`);
|
||||
const data = stakeIface.encodeFunctionData('exitPosition', [config.positionId]);
|
||||
const tx = await wallet.sendTransaction({ to: config.stakeAddress, data });
|
||||
await waitForReceipt(config.rpcUrl, tx.hash);
|
||||
await pollReceipt(config.rpcUrl, tx.hash);
|
||||
console.log(`[stake-rpc] ✅ Unstake mined: ${tx.hash}`);
|
||||
|
||||
provider.destroy();
|
||||
|
|
@ -141,6 +162,9 @@ export async function unstakeViaRpc(config: UnstakeRpcConfig): Promise<void> {
|
|||
* Discovers positions by scanning PositionCreated events filtered by owner,
|
||||
* then confirms each one is still active (non-zero share / non-zero owner)
|
||||
* by reading the positions() mapping directly.
|
||||
*
|
||||
* Note: fromBlock '0x0' scans from genesis — acceptable for local Anvil;
|
||||
* would need a deploy-block offset for use against a live node.
|
||||
*/
|
||||
export async function getStakingPositions(config: {
|
||||
rpcUrl: string;
|
||||
|
|
@ -178,7 +202,7 @@ export async function getStakingPositions(config: {
|
|||
const owner = decoded[1] as string;
|
||||
|
||||
// Exited positions have owner reset to zero address
|
||||
if (owner.toLowerCase() !== ZERO_ADDRESS && share > 0n) {
|
||||
if (owner.toLowerCase() !== ZeroAddress && share > 0n) {
|
||||
active.push({
|
||||
positionId,
|
||||
share,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue