Merge pull request 'fix web-app' (#19) from fix-web-app2 into master

Reviewed-on: https://codeberg.org/johba/harb/pulls/19
This commit is contained in:
johba 2025-09-24 14:03:51 +02:00
commit efecc5c348
4 changed files with 195 additions and 21 deletions

View file

@ -90,7 +90,7 @@ log() {
wait_for_rpc() {
local url="$1"
for _ in {1..60}; do
if curl -s -o /dev/null -X POST "$url" -H "Content-Type: application/json" \
if curl --max-time 1 --connect-timeout 1 -s -o /dev/null -X POST "$url" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}'; then
return 0
fi
@ -103,7 +103,7 @@ wait_for_rpc() {
wait_for_http() {
local url="$1"
for _ in {1..60}; do
if curl -sSf "$url" >/dev/null 2>&1; then
if curl --max-time 1 --connect-timeout 1 -sSf "$url" >/dev/null 2>&1; then
return 0
fi
sleep 1
@ -186,7 +186,8 @@ start_anvil() {
log "Starting Anvil (forking $FORK_URL)"
local anvil_args=("--fork-url" "$FORK_URL" "--chain-id" 31337 "--block-time" 1 \
"--host" 127.0.0.1 "--port" 8545)
"--host" 127.0.0.1 "--port" 8545 "--threads" 4 "--timeout" 2000 "--retries" 2 \
"--fork-retry-backoff" 100)
if [[ -f "$MNEMONIC_FILE" ]]; then
local mnemonic
@ -305,7 +306,7 @@ prepare_application_state() {
prime_chain_for_indexing() {
log "Pre-mining blocks for indexer stability"
for _ in {1..1200}; do
for _ in {1..2000}; do
"$CAST" rpc --rpc-url "$ANVIL_RPC" evm_mine > /dev/null 2>&1 || true
done
}
@ -393,9 +394,12 @@ start_frontend() {
"VITE_LOCAL_RPC_PROXY_TARGET=$ANVIL_RPC"
"VITE_KRAIKEN_ADDRESS=$KRAIKEN"
"VITE_STAKE_ADDRESS=$STAKE"
"VITE_LIQUIDITY_MANAGER=$LIQUIDITY_MANAGER"
"VITE_SWAP_ROUTER=$SWAP_ROUTER"
"VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK=$GRAPHQL_ENDPOINT"
"VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK=$LOCAL_TXNBOT_URL"
"VITE_ENABLE_EVENT_STREAM=false"
"VITE_POSITIONS_POLL_MS=0"
)
log "Starting frontend (Vite dev server)"

View file

@ -8,6 +8,7 @@ const networks = {
BASE_SEPOLIA_LOCAL_FORK: {
chainId: 31337,
rpc: process.env.PONDER_RPC_URL_BASE_SEPOLIA_LOCAL_FORK || "http://127.0.0.1:8545",
disableCache: true,
contracts: {
kraiken: process.env.KRAIKEN_ADDRESS || "0x56186c1E64cA8043dEF78d06AfF222212eA5df71",
stake: process.env.STAKE_ADDRESS || "0x056E4a859558A3975761ABd7385506BC4D8A8E60",
@ -56,6 +57,7 @@ export default createConfig({
[NETWORK]: {
id: selectedNetwork.chainId,
rpc: selectedNetwork.rpc,
disableCache: selectedNetwork.disableCache,
},
},
contracts: {

View file

@ -57,11 +57,25 @@ const provider = new ethers.JsonRpcProvider(PROVIDER_URL);
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
const liquidityManager = new ethers.Contract(LM_CONTRACT_ADDRESS, LM_ABI, wallet);
const stakeContract = new ethers.Contract(STAKE_CONTRACT_ADDRESS, STAKE_ABI, wallet);
const walletAddress = ethers.getAddress(wallet.address);
const ZERO_ADDRESS = ethers.ZeroAddress;
const DEFAULT_RECENTER_ACCESS_SLOT = 6n;
const STORAGE_SLOT_SEARCH_LIMIT = 64n;
let recenterAccessSlot = null;
let slotDetectionAttempted = false;
let startTime = new Date();
let lastRecenterTime = null;
let lastLiquidationTime = null;
let lastRecenterTx = null;
let lastRecenterAccessStatus = null;
let lastRecenterEligibility = {
checkedAtMs: null,
canRecenter: null,
reason: null,
error: null,
};
async function fetchActivePositions() {
const response = await fetch(GRAPHQL_ENDPOINT, {
@ -88,21 +102,156 @@ async function checkFunds() {
return ethers.formatEther(balance);
}
async function canCallFunction() {
function extractRevertReason(error) {
const candidates = [
error?.shortMessage,
error?.reason,
error?.info?.error?.message,
error?.error?.message,
error?.message,
];
for (const candidate of candidates) {
if (typeof candidate !== 'string' || candidate.length === 0) {
continue;
}
const match = candidate.match(/execution reverted(?: due to)?(?::|: )?(.*)/i);
if (match && match[1]) {
return match[1].trim();
}
if (!candidate.toLowerCase().startsWith('execution reverted')) {
return candidate.trim();
}
}
return null;
}
function parseStorageAddress(value) {
if (typeof value !== 'string' || value.length <= 2) {
return ZERO_ADDRESS;
}
try {
// this will throw if the function is not callable
await liquidityManager.recenter.estimateGas();
return true;
return ethers.getAddress(ethers.dataSlice(value, 12, 32));
} catch (error) {
return false;
return ZERO_ADDRESS;
}
}
async function detectRecenterAccessSlot() {
if (recenterAccessSlot !== null) {
return recenterAccessSlot;
}
if (slotDetectionAttempted) {
return DEFAULT_RECENTER_ACCESS_SLOT;
}
slotDetectionAttempted = true;
try {
const feeDestinationAddress = await liquidityManager.feeDestination();
if (feeDestinationAddress !== ZERO_ADDRESS) {
for (let slot = 0n; slot < STORAGE_SLOT_SEARCH_LIMIT; slot++) {
const raw = await provider.getStorage(LM_CONTRACT_ADDRESS, slot);
if (parseStorageAddress(raw) === feeDestinationAddress) {
recenterAccessSlot = slot > 0n ? slot - 1n : DEFAULT_RECENTER_ACCESS_SLOT;
break;
}
}
}
} catch (error) {
console.error('Failed to detect recenter access slot:', error);
}
if (recenterAccessSlot === null) {
recenterAccessSlot = DEFAULT_RECENTER_ACCESS_SLOT;
}
return recenterAccessSlot;
}
async function getRecenterAccessStatus(forceRefresh = false) {
const now = Date.now();
if (!forceRefresh && lastRecenterAccessStatus && lastRecenterAccessStatus.checkedAtMs && (now - lastRecenterAccessStatus.checkedAtMs) < 30000) {
return lastRecenterAccessStatus;
}
let recenterAddress = null;
let hasAccess = null;
let slotHex = null;
let errorMessage = null;
try {
const slot = await detectRecenterAccessSlot();
slotHex = ethers.toBeHex(slot);
const raw = await provider.getStorage(LM_CONTRACT_ADDRESS, slot);
recenterAddress = parseStorageAddress(raw);
hasAccess = recenterAddress === ZERO_ADDRESS || recenterAddress === walletAddress;
} catch (error) {
errorMessage = error?.shortMessage || error?.message || 'unknown error';
recenterAddress = null;
}
lastRecenterAccessStatus = {
hasAccess,
recenterAccessAddress: recenterAddress,
slot: slotHex,
checkedAtMs: now,
error: errorMessage,
};
return lastRecenterAccessStatus;
}
async function evaluateRecenterOpportunity() {
const now = Date.now();
const accessStatus = await getRecenterAccessStatus(true);
if (accessStatus.error && accessStatus.hasAccess === null) {
lastRecenterEligibility = {
checkedAtMs: now,
canRecenter: false,
reason: 'Failed to determine recenter access.',
error: accessStatus.error,
};
return lastRecenterEligibility;
}
if (accessStatus.hasAccess === false) {
lastRecenterEligibility = {
checkedAtMs: now,
canRecenter: false,
reason: 'txnBot is not the authorized recenter caller.',
error: null,
};
return lastRecenterEligibility;
}
try {
await liquidityManager.recenter.estimateGas();
lastRecenterEligibility = {
checkedAtMs: now,
canRecenter: true,
reason: null,
error: null,
};
} catch (error) {
lastRecenterEligibility = {
checkedAtMs: now,
canRecenter: false,
reason: extractRevertReason(error) || 'recenter not currently executable.',
error: error?.shortMessage || error?.message || null,
};
}
return lastRecenterEligibility;
}
async function attemptRecenter() {
if (!(await canCallFunction())) {
const eligibility = await evaluateRecenterOpportunity();
if (!eligibility.canRecenter) {
return {
executed: false,
message: 'Liquidity manager denied recenter (likely already centered).'
message: eligibility.reason || 'Liquidity manager denied recenter.',
eligibility,
};
}
@ -113,7 +262,8 @@ async function attemptRecenter() {
return {
executed: true,
txHash: tx.hash,
message: 'recenter transaction submitted'
message: 'recenter transaction submitted',
eligibility: lastRecenterEligibility,
};
}
@ -151,7 +301,8 @@ async function liquidityLoop() {
if (result.executed) {
console.log(`recenter called successfully. tx=${result.txHash}`);
} else {
console.log(`No liquidity can be moved at the moment. - ${(new Date()).toISOString()}`);
const reason = result.message ? `Reason: ${result.message}` : 'Reason: unavailable';
console.log(`Recenter skipped. ${reason} - ${(new Date()).toISOString()}`);
}
} catch (error) {
console.error('Error in liquidity loop:', error);
@ -205,12 +356,27 @@ app.get('/status', async (req, res) => {
try {
const balance = await checkFunds();
const uptime = formatDuration(new Date() - startTime);
const recenterAccessStatus = await getRecenterAccessStatus();
const recenterEligibility = lastRecenterEligibility;
const status = {
balance: `${balance} ETH`,
uptime: uptime,
lastRecenterTime: lastRecenterTime ? lastRecenterTime.toISOString() : 'Never',
lastLiquidationTime: lastLiquidationTime ? lastLiquidationTime.toISOString() : 'Never',
lastRecenterTx,
recenterAccess: {
hasAccess: recenterAccessStatus?.hasAccess ?? null,
grantedTo: recenterAccessStatus?.recenterAccessAddress ?? null,
slot: recenterAccessStatus?.slot ?? null,
checkedAt: recenterAccessStatus?.checkedAtMs ? new Date(recenterAccessStatus.checkedAtMs).toISOString() : null,
error: recenterAccessStatus?.error ?? null,
},
recenterEligibility: {
canRecenter: recenterEligibility?.canRecenter ?? null,
reason: recenterEligibility?.reason ?? null,
checkedAt: recenterEligibility?.checkedAtMs ? new Date(recenterEligibility.checkedAtMs).toISOString() : null,
error: recenterEligibility?.error ?? null,
},
};
if (parseFloat(balance) < 0.1) {

View file

@ -1,16 +1,18 @@
import deploymentsLocal from "../../onchain/deployments-local.json";
const LOCAL_PONDER_URL = import.meta.env.VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK ?? "http://127.0.0.1:42069/graphql";
const LOCAL_TXNBOT_URL = import.meta.env.VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK ?? "http://127.0.0.1:43069";
const LOCAL_RPC_URL = import.meta.env.VITE_LOCAL_RPC_URL ?? "/rpc/anvil";
const env = import.meta.env;
const LOCAL_PONDER_URL = env.VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK ?? "http://127.0.0.1:42069/graphql";
const LOCAL_TXNBOT_URL = env.VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK ?? "http://127.0.0.1:43069";
const LOCAL_RPC_URL = env.VITE_LOCAL_RPC_URL ?? "/rpc/anvil";
const localContracts = (deploymentsLocal as any)?.contracts ?? {};
const localInfra = (deploymentsLocal as any)?.infrastructure ?? {};
const LOCAL_KRAIKEN = (localContracts.Kraiken ?? "").trim();
const LOCAL_STAKE = (localContracts.Stake ?? "").trim();
const LOCAL_LM = (localContracts.LiquidityManager ?? "").trim();
const LOCAL_WETH = (localInfra.weth ?? "0x4200000000000000000000000000000000000006").trim();
const LOCAL_ROUTER = "0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4";
const LOCAL_KRAIKEN = (env.VITE_KRAIKEN_ADDRESS ?? localContracts.Kraiken ?? "").trim();
const LOCAL_STAKE = (env.VITE_STAKE_ADDRESS ?? localContracts.Stake ?? "").trim();
const LOCAL_LM = (env.VITE_LIQUIDITY_MANAGER ?? localContracts.LiquidityManager ?? "").trim();
const LOCAL_WETH = (env.VITE_LOCAL_WETH ?? localInfra.weth ?? "0x4200000000000000000000000000000000000006").trim();
const LOCAL_ROUTER = (env.VITE_SWAP_ROUTER ?? "0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4").trim();
function detectDefaultChainId(): number {
const envValue = import.meta.env.VITE_DEFAULT_CHAIN_ID;