Initial commit with 🏗️ create-eth @ 2.0.4
This commit is contained in:
688
packages/hardhat/scripts/runStakingOracleBots.ts
Normal file
688
packages/hardhat/scripts/runStakingOracleBots.ts
Normal file
@@ -0,0 +1,688 @@
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import hre from "hardhat";
|
||||
import { sleep, getConfig } from "./utils";
|
||||
import { fetchPriceFromUniswap } from "./fetchPriceFromUniswap";
|
||||
import { parseEther } from "viem";
|
||||
|
||||
const oraTokenAbi = [
|
||||
{
|
||||
type: "function",
|
||||
name: "approve",
|
||||
stateMutability: "nonpayable",
|
||||
inputs: [
|
||||
{ name: "spender", type: "address" },
|
||||
{ name: "amount", type: "uint256" },
|
||||
],
|
||||
outputs: [{ name: "", type: "bool" }],
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "balanceOf",
|
||||
stateMutability: "view",
|
||||
inputs: [{ name: "owner", type: "address" }],
|
||||
outputs: [{ name: "", type: "uint256" }],
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "allowance",
|
||||
stateMutability: "view",
|
||||
inputs: [
|
||||
{ name: "owner", type: "address" },
|
||||
{ name: "spender", type: "address" },
|
||||
],
|
||||
outputs: [{ name: "", type: "uint256" }],
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "transfer",
|
||||
stateMutability: "nonpayable",
|
||||
inputs: [
|
||||
{ name: "to", type: "address" },
|
||||
{ name: "amount", type: "uint256" },
|
||||
],
|
||||
outputs: [{ name: "", type: "bool" }],
|
||||
},
|
||||
] as const;
|
||||
|
||||
type WalletClient = Awaited<ReturnType<typeof hre.viem.getWalletClients>>[number];
|
||||
|
||||
const normalizeNodeInfo = (raw: any) => {
|
||||
const zero = 0n;
|
||||
if (!raw)
|
||||
return {
|
||||
stakedAmount: zero,
|
||||
lastReportedBucket: zero,
|
||||
reportCount: zero,
|
||||
claimedReportCount: zero,
|
||||
firstBucket: zero,
|
||||
active: false,
|
||||
};
|
||||
const get = (idx: number, name: string) => {
|
||||
const byName = raw[name];
|
||||
const byIndex = Array.isArray(raw) ? raw[idx] : undefined;
|
||||
if (typeof byName === "bigint") return byName as bigint;
|
||||
if (typeof byIndex === "bigint") return byIndex as bigint;
|
||||
const val = byName ?? byIndex ?? 0;
|
||||
try {
|
||||
return BigInt(String(val));
|
||||
} catch {
|
||||
return zero;
|
||||
}
|
||||
};
|
||||
return {
|
||||
stakedAmount: get(0, "stakedAmount"),
|
||||
lastReportedBucket: get(1, "lastReportedBucket"),
|
||||
reportCount: get(2, "reportCount"),
|
||||
claimedReportCount: get(3, "claimedReportCount"),
|
||||
firstBucket: get(4, "firstBucket"),
|
||||
active:
|
||||
typeof raw?.active === "boolean"
|
||||
? (raw.active as boolean)
|
||||
: Array.isArray(raw) && typeof raw[5] === "boolean"
|
||||
? (raw[5] as boolean)
|
||||
: false,
|
||||
};
|
||||
};
|
||||
|
||||
// Current base price used by the bot. Initialized once at start from Uniswap
|
||||
// and updated from on-chain contract prices thereafter.
|
||||
let currentPrice: bigint | null = null;
|
||||
|
||||
const stringToBool = (value: string | undefined | null): boolean => {
|
||||
if (!value) return false;
|
||||
const normalized = value.toLowerCase();
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
||||
};
|
||||
|
||||
// Feature flag: enable automatic slashing when the AUTO_SLASH environment variable is truthy
|
||||
const AUTO_SLASH: boolean = stringToBool(process.env.AUTO_SLASH);
|
||||
|
||||
const getStakingOracleDeployment = async (runtime: HardhatRuntimeEnvironment) => {
|
||||
const deployment = await runtime.deployments.get("StakingOracle");
|
||||
return {
|
||||
address: deployment.address as `0x${string}`,
|
||||
abi: deployment.abi,
|
||||
deployedBlock: deployment.receipt?.blockNumber ? BigInt(deployment.receipt.blockNumber) : 0n,
|
||||
} as const;
|
||||
};
|
||||
|
||||
const getActiveNodeWalletClients = async (
|
||||
runtime: HardhatRuntimeEnvironment,
|
||||
stakingAddress: `0x${string}`,
|
||||
stakingAbi: any,
|
||||
): Promise<WalletClient[]> => {
|
||||
const accounts = await runtime.viem.getWalletClients();
|
||||
// Filter to only those that are registered (firstBucket != 0)
|
||||
const publicClient = await runtime.viem.getPublicClient();
|
||||
const nodeClients: WalletClient[] = [];
|
||||
for (const client of accounts) {
|
||||
try {
|
||||
const rawNodeInfo = await publicClient.readContract({
|
||||
address: stakingAddress,
|
||||
abi: stakingAbi,
|
||||
functionName: "nodes",
|
||||
args: [client.account.address],
|
||||
});
|
||||
const node = normalizeNodeInfo(rawNodeInfo);
|
||||
if (node.firstBucket !== 0n && node.active) {
|
||||
nodeClients.push(client);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return nodeClients;
|
||||
};
|
||||
|
||||
const findNodeIndex = async (
|
||||
runtime: HardhatRuntimeEnvironment,
|
||||
stakingAddress: `0x${string}`,
|
||||
stakingAbi: any,
|
||||
nodeAddress: `0x${string}`,
|
||||
): Promise<number | null> => {
|
||||
const publicClient = await runtime.viem.getPublicClient();
|
||||
// Iterate indices until out-of-bounds revert
|
||||
try {
|
||||
const addresses = (await publicClient.readContract({
|
||||
address: stakingAddress,
|
||||
abi: stakingAbi,
|
||||
functionName: "getNodeAddresses",
|
||||
args: [],
|
||||
})) as `0x${string}`[];
|
||||
return addresses.findIndex(addr => addr.toLowerCase() === nodeAddress.toLowerCase());
|
||||
} catch {}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getReportIndexForNode = async (
|
||||
publicClient: Awaited<ReturnType<typeof hre.viem.getPublicClient>>,
|
||||
stakingAddress: `0x${string}`,
|
||||
stakingAbi: any,
|
||||
bucketNumber: bigint,
|
||||
nodeAddress: `0x${string}`,
|
||||
fromBlock: bigint,
|
||||
): Promise<number | null> => {
|
||||
try {
|
||||
const events = (await publicClient.getContractEvents({
|
||||
address: stakingAddress,
|
||||
abi: stakingAbi,
|
||||
eventName: "PriceReported",
|
||||
fromBlock,
|
||||
toBlock: "latest",
|
||||
})) as any[];
|
||||
const bucketEvents = events.filter((ev: any) => {
|
||||
const bucket = ev.args?.bucketNumber as bigint | undefined;
|
||||
return bucket !== undefined && bucket === bucketNumber;
|
||||
});
|
||||
const idx = bucketEvents.findIndex((ev: any) => {
|
||||
const reporter = (ev.args?.node as string | undefined) ?? "";
|
||||
return reporter.toLowerCase() === nodeAddress.toLowerCase();
|
||||
});
|
||||
return idx === -1 ? null : idx;
|
||||
} catch (error) {
|
||||
console.warn("Failed to compute report index:", (error as Error).message);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const runCycle = async (runtime: HardhatRuntimeEnvironment) => {
|
||||
try {
|
||||
const { address, abi, deployedBlock } = await getStakingOracleDeployment(runtime);
|
||||
const publicClient = await runtime.viem.getPublicClient();
|
||||
const allWalletClients = await runtime.viem.getWalletClients();
|
||||
const blockNumber = await publicClient.getBlockNumber();
|
||||
console.log(`\n[Block ${blockNumber}] Starting new oracle cycle...`);
|
||||
|
||||
// Read current bucket window and bucket number
|
||||
const [bucketWindow, currentBucket] = await Promise.all([
|
||||
publicClient
|
||||
.readContract({ address, abi, functionName: "BUCKET_WINDOW", args: [] })
|
||||
.then(value => BigInt(String(value))),
|
||||
publicClient
|
||||
.readContract({ address, abi, functionName: "getCurrentBucketNumber", args: [] })
|
||||
.then(value => BigInt(String(value))),
|
||||
]);
|
||||
const previousBucket = currentBucket > 0n ? currentBucket - 1n : 0n;
|
||||
console.log(`BUCKET_WINDOW=${bucketWindow} | currentBucket=${currentBucket}`);
|
||||
|
||||
// Update base price from previous bucket using the RECORDED MEDIAN (not an average of reports).
|
||||
// Fallback to contract's latest price, then to previous cached value.
|
||||
try {
|
||||
const previous = previousBucket;
|
||||
if (previous > 0n) {
|
||||
try {
|
||||
// `getPastPrice(bucket)` returns the recorded median for that bucket (0 if not recorded yet).
|
||||
const pastMedian = await publicClient.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getPastPrice",
|
||||
args: [previous],
|
||||
});
|
||||
const median = BigInt(String(pastMedian));
|
||||
if (median > 0n) {
|
||||
currentPrice = median;
|
||||
}
|
||||
} catch {
|
||||
// ignore and fall back
|
||||
}
|
||||
|
||||
if (currentPrice === null) {
|
||||
// Fallback to on-chain latest average (previous bucket average)
|
||||
try {
|
||||
const onchain = await publicClient.readContract({ address, abi, functionName: "getLatestPrice", args: [] });
|
||||
currentPrice = BigInt(String(onchain));
|
||||
} catch {
|
||||
// keep prior currentPrice
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// keep prior currentPrice
|
||||
}
|
||||
|
||||
// Load config once per cycle so runtime edits to the config file are picked up
|
||||
const cfg = getConfig();
|
||||
|
||||
// 1) Reporting: each node only once per bucket
|
||||
const nodeWalletClients = await getActiveNodeWalletClients(runtime, address, abi);
|
||||
// Ensure we have an initial price (set once at startup in run())
|
||||
if (currentPrice === null) {
|
||||
currentPrice = await fetchPriceFromUniswap();
|
||||
}
|
||||
const reportTxHashes: `0x${string}`[] = [];
|
||||
for (const client of nodeWalletClients) {
|
||||
try {
|
||||
const rawNodeInfo = await publicClient.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "nodes",
|
||||
args: [client.account.address],
|
||||
});
|
||||
const node = normalizeNodeInfo(rawNodeInfo);
|
||||
if (node.lastReportedBucket !== currentBucket) {
|
||||
// Determine node config (probability to skip and variance)
|
||||
const nodeCfg = cfg.NODE_CONFIGS[client.account.address.toLowerCase()] || cfg.NODE_CONFIGS.default;
|
||||
const skipProb = Number(nodeCfg.PROBABILITY_OF_SKIPPING_REPORT ?? 0);
|
||||
if (Math.random() < skipProb) {
|
||||
console.log(`Skipping report (by probability) for ${client.account.address}`);
|
||||
continue;
|
||||
}
|
||||
// Compute deviated price as integer math using parts-per-million (ppm)
|
||||
const variancePpm = Math.floor((Number(nodeCfg.PRICE_VARIANCE) || 0) * 1_000_000);
|
||||
const randomPpm = variancePpm > 0 ? Math.floor(Math.random() * (variancePpm * 2 + 1)) - variancePpm : 0;
|
||||
const basePrice = currentPrice!; // derived from previous bucket excluding outliers
|
||||
const delta = (basePrice * BigInt(randomPpm)) / 1_000_000n;
|
||||
const priceToReport = basePrice + delta;
|
||||
|
||||
console.log(
|
||||
`Reporting price for node ${client.account.address} in bucket ${currentBucket} (price=${priceToReport})...`,
|
||||
);
|
||||
const txHash = await client.writeContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "reportPrice",
|
||||
args: [priceToReport],
|
||||
});
|
||||
reportTxHashes.push(txHash as `0x${string}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Skipping report for ${client.account.address}:`, (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for report transactions to be mined so subsequent reads (claiming) see the updated state.
|
||||
if (reportTxHashes.length > 0) {
|
||||
try {
|
||||
await Promise.all(reportTxHashes.map(hash => publicClient.waitForTransactionReceipt({ hash } as any)));
|
||||
} catch (err) {
|
||||
// If waiting fails, continue — claims will be attempted anyway but may not see the latest reports.
|
||||
console.warn("Error while waiting for report tx receipts:", (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Finalize median automatically when quorum is reached
|
||||
// You can only finalize buckets strictly in the past, so we finalize the *previous* bucket (current - 1).
|
||||
if (previousBucket > 0n) {
|
||||
let medianAlreadyRecorded = false;
|
||||
try {
|
||||
const median = await publicClient.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getPastPrice",
|
||||
args: [previousBucket],
|
||||
});
|
||||
medianAlreadyRecorded = BigInt(String(median)) > 0n;
|
||||
} catch {
|
||||
medianAlreadyRecorded = false;
|
||||
}
|
||||
|
||||
if (!medianAlreadyRecorded) {
|
||||
try {
|
||||
const activeNodeAddresses = (await publicClient.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getNodeAddresses",
|
||||
args: [],
|
||||
})) as `0x${string}`[];
|
||||
|
||||
const reportStatuses = await Promise.all(
|
||||
activeNodeAddresses.map(async nodeAddr => {
|
||||
try {
|
||||
const [price] = (await publicClient.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getSlashedStatus",
|
||||
args: [nodeAddr, previousBucket],
|
||||
})) as [bigint, boolean];
|
||||
return price;
|
||||
} catch {
|
||||
return 0n;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const reportedCount = reportStatuses.reduce((acc, price) => acc + (price > 0n ? 1n : 0n), 0n);
|
||||
const requiredReports =
|
||||
activeNodeAddresses.length === 0 ? 0n : (2n * BigInt(activeNodeAddresses.length) + 2n) / 3n;
|
||||
|
||||
if (activeNodeAddresses.length === 0) {
|
||||
console.log("No active nodes; skipping recordBucketMedian evaluation.");
|
||||
} else if (reportedCount >= requiredReports) {
|
||||
const finalizer = allWalletClients[0];
|
||||
try {
|
||||
await finalizer.writeContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "recordBucketMedian",
|
||||
args: [previousBucket],
|
||||
});
|
||||
console.log(
|
||||
`Recorded median for bucket ${previousBucket} (reports ${reportedCount}/${requiredReports}).`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(`Failed to record median for bucket ${previousBucket}:`, (err as Error).message);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`Skipping median recording for bucket ${previousBucket}; only ${reportedCount}/${requiredReports} reports.`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Unable to evaluate automatic recordBucketMedian:", (err as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Slashing: if previous bucket had outliers
|
||||
if (AUTO_SLASH) {
|
||||
try {
|
||||
const outliers = (await publicClient.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getOutlierNodes",
|
||||
args: [previousBucket],
|
||||
})) as `0x${string}`[];
|
||||
|
||||
if (outliers.length > 0) {
|
||||
console.log(`Found ${outliers.length} outliers in bucket ${previousBucket}, attempting to slash...`);
|
||||
// Use the first wallet (deployer) to slash
|
||||
const slasher = allWalletClients[0];
|
||||
for (const nodeAddr of outliers) {
|
||||
const index = await findNodeIndex(runtime, address, abi, nodeAddr);
|
||||
if (index === null) {
|
||||
console.warn(`Index not found for node ${nodeAddr}, skipping slashing.`);
|
||||
continue;
|
||||
}
|
||||
const reportIndex = await getReportIndexForNode(
|
||||
publicClient,
|
||||
address,
|
||||
abi,
|
||||
previousBucket,
|
||||
nodeAddr,
|
||||
deployedBlock,
|
||||
);
|
||||
if (reportIndex === null) {
|
||||
console.warn(`Report index not found for node ${nodeAddr}, skipping slashing.`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await slasher.writeContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "slashNode",
|
||||
args: [nodeAddr, previousBucket, BigInt(reportIndex), BigInt(index)],
|
||||
});
|
||||
console.log(
|
||||
`Slashed node ${nodeAddr} for bucket ${previousBucket} at indices report=${reportIndex}, node=${index}`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(`Failed to slash ${nodeAddr}:`, (err as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// getOutlierNodes may revert for small sample sizes (e.g., 0 or 1 report)
|
||||
console.log(`Skipping slashing check for bucket ${previousBucket}:`, (err as Error).message);
|
||||
}
|
||||
} else {
|
||||
// Auto-slash disabled by flag
|
||||
console.log(`Auto-slash disabled; skipping slashing for bucket ${previousBucket}`);
|
||||
}
|
||||
|
||||
// 4) Rewards: claim when there are unclaimed reports
|
||||
// Wait a couple seconds after reports have been mined before claiming
|
||||
console.log("Waiting 2s before claiming rewards...");
|
||||
await sleep(2000);
|
||||
for (const client of nodeWalletClients) {
|
||||
try {
|
||||
const rawNodeInfo = await publicClient.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "nodes",
|
||||
args: [client.account.address],
|
||||
});
|
||||
const node = normalizeNodeInfo(rawNodeInfo);
|
||||
if (node.reportCount > node.claimedReportCount) {
|
||||
await client.writeContract({ address, abi, functionName: "claimReward", args: [] });
|
||||
console.log(`Claimed rewards for ${client.account.address}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to claim rewards for ${client.account.address}:`, (err as Error).message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in oracle cycle:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
console.log("Starting oracle bot system...");
|
||||
// Fetch Uniswap price once at startup; subsequent cycles will base price on on-chain reports
|
||||
currentPrice = await fetchPriceFromUniswap();
|
||||
console.log(`Initial base price from Uniswap: ${currentPrice}`);
|
||||
|
||||
// Spin up nodes (fund + approve + register) for local testing if they aren't registered yet.
|
||||
try {
|
||||
const { address, abi } = await getStakingOracleDeployment(hre);
|
||||
const publicClient = await hre.viem.getPublicClient();
|
||||
const accounts = await hre.viem.getWalletClients();
|
||||
// Mirror deploy script: use accounts[1..10] as oracle nodes
|
||||
const nodeAccounts = accounts.slice(1, 11);
|
||||
const deployerClient = accounts[0];
|
||||
|
||||
const [minimumStake, oraTokenAddress] = await Promise.all([
|
||||
publicClient.readContract({ address, abi, functionName: "MINIMUM_STAKE", args: [] }).then(v => BigInt(String(v))),
|
||||
publicClient
|
||||
.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "oracleToken",
|
||||
args: [],
|
||||
})
|
||||
.then(v => v as unknown as `0x${string}`),
|
||||
]);
|
||||
|
||||
// Default bot stake for local simulations (keep it small so it matches the new UX expectations)
|
||||
const defaultStake = parseEther("500");
|
||||
const stakeAmount = minimumStake > defaultStake ? minimumStake : defaultStake;
|
||||
|
||||
// Build an idempotent setup plan based on current on-chain state (so restarts resume cleanly).
|
||||
const snapshots = await Promise.all(
|
||||
nodeAccounts.map(async nodeClient => {
|
||||
const nodeAddress = nodeClient.account.address;
|
||||
const [rawNodeInfo, balance, allowance] = await Promise.all([
|
||||
publicClient
|
||||
.readContract({ address, abi, functionName: "nodes", args: [nodeAddress] })
|
||||
.catch(() => null as any),
|
||||
publicClient.readContract({
|
||||
address: oraTokenAddress,
|
||||
abi: oraTokenAbi,
|
||||
functionName: "balanceOf",
|
||||
args: [nodeAddress],
|
||||
}) as Promise<bigint>,
|
||||
publicClient.readContract({
|
||||
address: oraTokenAddress,
|
||||
abi: oraTokenAbi,
|
||||
functionName: "allowance",
|
||||
args: [nodeAddress, address],
|
||||
}) as Promise<bigint>,
|
||||
]);
|
||||
|
||||
const node = normalizeNodeInfo(rawNodeInfo);
|
||||
const effectiveStake = node.active
|
||||
? await publicClient
|
||||
.readContract({ address, abi, functionName: "getEffectiveStake", args: [nodeAddress] })
|
||||
.then(v => BigInt(String(v)))
|
||||
.catch(() => 0n)
|
||||
: 0n;
|
||||
|
||||
return { nodeClient, nodeAddress, node, effectiveStake, balance, allowance };
|
||||
}),
|
||||
);
|
||||
|
||||
const transfers: { to: `0x${string}`; amount: bigint }[] = [];
|
||||
const perNodeActions: {
|
||||
nodeClient: WalletClient;
|
||||
nodeAddress: `0x${string}`;
|
||||
approveAmount: bigint;
|
||||
kind: "register" | "addStake" | "none";
|
||||
amount: bigint;
|
||||
note: string;
|
||||
}[] = [];
|
||||
|
||||
for (const snap of snapshots) {
|
||||
const { nodeClient, nodeAddress, node, effectiveStake, balance, allowance } = snap;
|
||||
|
||||
if (node.active) {
|
||||
if (effectiveStake < minimumStake) {
|
||||
const needed = minimumStake - effectiveStake;
|
||||
const transferAmount = balance < needed ? needed - balance : 0n;
|
||||
if (transferAmount > 0n) transfers.push({ to: nodeAddress, amount: transferAmount });
|
||||
|
||||
const approveAmount = allowance < needed ? needed : 0n;
|
||||
perNodeActions.push({
|
||||
nodeClient,
|
||||
nodeAddress,
|
||||
approveAmount,
|
||||
kind: "addStake",
|
||||
amount: needed,
|
||||
note: `top up effectiveStake=${effectiveStake} by ${needed}`,
|
||||
});
|
||||
} else {
|
||||
perNodeActions.push({
|
||||
nodeClient,
|
||||
nodeAddress,
|
||||
approveAmount: 0n,
|
||||
kind: "none",
|
||||
amount: 0n,
|
||||
note: "already active (no action)",
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Inactive -> fund/approve/register. On restart, we only do the missing pieces.
|
||||
const transferAmount = balance < stakeAmount ? stakeAmount - balance : 0n;
|
||||
if (transferAmount > 0n) transfers.push({ to: nodeAddress, amount: transferAmount });
|
||||
|
||||
const approveAmount = allowance < stakeAmount ? stakeAmount : 0n;
|
||||
perNodeActions.push({
|
||||
nodeClient,
|
||||
nodeAddress,
|
||||
approveAmount,
|
||||
kind: "register",
|
||||
amount: stakeAmount,
|
||||
note: `register with stake=${stakeAmount}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 1) Fund nodes in one burst from deployer using nonce chaining.
|
||||
if (transfers.length > 0) {
|
||||
const deployerNonce = await publicClient.getTransactionCount({ address: deployerClient.account.address });
|
||||
const transferTxs: `0x${string}`[] = [];
|
||||
console.log(`Funding ${transfers.length} node(s) from deployer (burst)...`);
|
||||
for (const [i, t] of transfers.entries()) {
|
||||
const tx = await deployerClient.writeContract({
|
||||
address: oraTokenAddress,
|
||||
abi: oraTokenAbi,
|
||||
functionName: "transfer",
|
||||
nonce: deployerNonce + i,
|
||||
args: [t.to, t.amount],
|
||||
});
|
||||
transferTxs.push(tx as `0x${string}`);
|
||||
}
|
||||
await Promise.all(transferTxs.map(hash => publicClient.waitForTransactionReceipt({ hash })));
|
||||
console.log("Funding burst mined.");
|
||||
}
|
||||
|
||||
// 2) For each node, chain approve -> (register|addStake) with explicit nonces, then wait for all receipts once.
|
||||
const nodeNonces = await Promise.all(
|
||||
perNodeActions.map(a => publicClient.getTransactionCount({ address: a.nodeAddress })),
|
||||
);
|
||||
const nodeTxs: `0x${string}`[] = [];
|
||||
|
||||
for (const [idx, action] of perNodeActions.entries()) {
|
||||
const { nodeClient, nodeAddress, approveAmount, kind, amount, note } = action;
|
||||
let nonce = nodeNonces[idx];
|
||||
|
||||
if (kind === "none") {
|
||||
console.log(`Node ${nodeAddress}: ${note}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Node ${nodeAddress}: ${note}`);
|
||||
|
||||
if (approveAmount > 0n) {
|
||||
const tx = await nodeClient.writeContract({
|
||||
address: oraTokenAddress,
|
||||
abi: oraTokenAbi,
|
||||
functionName: "approve",
|
||||
nonce,
|
||||
args: [address, approveAmount],
|
||||
});
|
||||
nodeTxs.push(tx as `0x${string}`);
|
||||
nonce += 1;
|
||||
}
|
||||
|
||||
if (kind === "register") {
|
||||
const tx = await nodeClient.writeContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "registerNode",
|
||||
nonce,
|
||||
args: [amount],
|
||||
});
|
||||
nodeTxs.push(tx as `0x${string}`);
|
||||
} else if (kind === "addStake") {
|
||||
const tx = await nodeClient.writeContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "addStake",
|
||||
nonce,
|
||||
args: [amount],
|
||||
});
|
||||
nodeTxs.push(tx as `0x${string}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeTxs.length > 0) {
|
||||
console.log(`Waiting for ${nodeTxs.length} node tx(s) to be mined...`);
|
||||
await Promise.all(nodeTxs.map(hash => publicClient.waitForTransactionReceipt({ hash })));
|
||||
console.log("Node setup txs mined.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Node registration step failed:", (err as Error).message);
|
||||
}
|
||||
while (true) {
|
||||
await runCycle(hre);
|
||||
await sleep(12000);
|
||||
}
|
||||
};
|
||||
|
||||
run().catch(error => {
|
||||
console.error("Fatal error in oracle bot system:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Handle process termination signals
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("\nReceived SIGINT (Ctrl+C). Cleaning up...");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", async () => {
|
||||
console.log("\nReceived SIGTERM. Cleaning up...");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Handle uncaught exceptions
|
||||
process.on("uncaughtException", async error => {
|
||||
console.error("Uncaught Exception:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on("unhandledRejection", async (reason, promise) => {
|
||||
console.error("Unhandled Rejection at:", promise, "reason:", reason);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user