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:
commit
efecc5c348
4 changed files with 195 additions and 21 deletions
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue