Files
smom-dbis-138/services/watchtower/index.js
2026-03-02 12:14:09 -08:00

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);
});