124 lines
4.0 KiB
JavaScript
124 lines
4.0 KiB
JavaScript
/**
|
|
* Channel Watchtower: subscribes to ChallengeSubmitted and calls challengeClose
|
|
* with a newer signed state before the dispute deadline when we have one.
|
|
*
|
|
* Config: RPC_URL, PAYMENT_CHANNEL_MANAGER_ADDRESS, PRIVATE_KEY, CHANNELS_FILE (optional)
|
|
* Channels file: JSON array of { channelId, nonce, balanceA, balanceB, vA, rA, sA, vB, rB, sB }
|
|
*/
|
|
|
|
import 'dotenv/config';
|
|
import { ethers } from 'ethers';
|
|
import { readFileSync, watch } from 'fs';
|
|
import { fileURLToPath } from 'url';
|
|
import { dirname, join } from 'path';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
const PAYMENT_CHANNEL_ABI = [
|
|
'event ChallengeSubmitted(uint256 indexed channelId, uint256 nonce, uint256 balanceA, uint256 balanceB, uint256 newDeadline)',
|
|
'function challengeClose(uint256 channelId, uint256 nonce, uint256 balanceA, uint256 balanceB, uint8 vA, bytes32 rA, bytes32 sA, uint8 vB, bytes32 rB, bytes32 sB) external'
|
|
];
|
|
|
|
function loadChannels(filePath) {
|
|
try {
|
|
const raw = readFileSync(filePath, 'utf8');
|
|
const arr = JSON.parse(raw);
|
|
const map = new Map();
|
|
for (const c of arr) {
|
|
const id = String(c.channelId);
|
|
map.set(id, {
|
|
channelId: BigInt(c.channelId),
|
|
nonce: BigInt(c.nonce),
|
|
balanceA: BigInt(c.balanceA),
|
|
balanceB: BigInt(c.balanceB),
|
|
vA: Number(c.vA),
|
|
rA: c.rA,
|
|
sA: c.sA,
|
|
vB: Number(c.vB),
|
|
rB: c.rB,
|
|
sB: c.sB
|
|
});
|
|
}
|
|
return map;
|
|
} catch (e) {
|
|
return new Map();
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const rpcUrl = process.env.RPC_URL;
|
|
const contractAddress = process.env.PAYMENT_CHANNEL_MANAGER_ADDRESS;
|
|
const privateKey = process.env.PRIVATE_KEY;
|
|
const channelsFile = process.env.CHANNELS_FILE || join(__dirname, 'channels.json');
|
|
|
|
if (!rpcUrl || !contractAddress || !privateKey) {
|
|
console.error('Set RPC_URL, PAYMENT_CHANNEL_MANAGER_ADDRESS, and PRIVATE_KEY');
|
|
process.exit(1);
|
|
}
|
|
|
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
const wallet = new ethers.Wallet(privateKey, provider);
|
|
const contract = new ethers.Contract(contractAddress, PAYMENT_CHANNEL_ABI, wallet);
|
|
|
|
let channels = loadChannels(channelsFile);
|
|
console.log('Watching', channels.size, 'channel(s). Contract:', contractAddress);
|
|
|
|
try {
|
|
watch(channelsFile, (event, filename) => {
|
|
channels = loadChannels(channelsFile);
|
|
console.log('Reloaded channels file, now', channels.size, 'channel(s)');
|
|
});
|
|
} catch (_) {
|
|
// watch may fail on some systems
|
|
}
|
|
|
|
const BUFFER_SECONDS = 60n; // submit challenge this many seconds before deadline
|
|
|
|
contract.on(
|
|
'ChallengeSubmitted',
|
|
async (channelId, nonce, balanceA, balanceB, newDeadline) => {
|
|
const idStr = channelId.toString();
|
|
const state = channels.get(idStr);
|
|
if (!state) return;
|
|
if (state.nonce <= nonce) return;
|
|
|
|
const deadline = BigInt(newDeadline.toString());
|
|
const now = BigInt(Math.floor(Date.now() / 1000));
|
|
const waitSec = deadline - now - BUFFER_SECONDS;
|
|
if (waitSec <= 0n) {
|
|
console.log('[Watchtower] Channel', idStr, 'deadline too soon, submitting challenge now');
|
|
} else {
|
|
console.log('[Watchtower] Channel', idStr, 'dispute: their nonce', nonce.toString(), 'we have', state.nonce.toString(), '- will challenge in', waitSec.toString(), 's');
|
|
await new Promise((r) => setTimeout(r, Number(waitSec) * 1000));
|
|
}
|
|
|
|
try {
|
|
const tx = await contract.challengeClose(
|
|
state.channelId,
|
|
state.nonce,
|
|
state.balanceA,
|
|
state.balanceB,
|
|
state.vA,
|
|
state.rA,
|
|
state.sA,
|
|
state.vB,
|
|
state.rB,
|
|
state.sB
|
|
);
|
|
console.log('[Watchtower] Challenge tx:', tx.hash);
|
|
await tx.wait();
|
|
console.log('[Watchtower] Channel', idStr, 'challenge confirmed');
|
|
} catch (err) {
|
|
console.error('[Watchtower] challengeClose failed:', err.message);
|
|
}
|
|
}
|
|
);
|
|
|
|
console.log('Subscribed to ChallengeSubmitted. Waiting for events...');
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|