#!/usr/bin/env ts-node /* eslint-disable @typescript-eslint/no-empty-function */ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import axios, { AxiosResponse } from "axios"; import { NitroRpcClient } from "../src/rpc-client"; import { compactJson, getLocalRPCUrl, logOutChannelUpdates, } from "../src/utils"; const clientNames = ["alice", "irene", "bob", "ivan"] as const; const clientPortMap: Record = { alice: 4005, irene: 4006, bob: 4007, ivan: 4008, }; type ClientNames = (typeof clientNames)[number]; type Clients = Map; async function initializeClients(): Promise { let port = 4005; const clients: Clients = new Map(); for (const clientName of clientNames) { const client = await NitroRpcClient.CreateHttpNitroClient( getLocalRPCUrl(port) ); clients.set(clientName, client); port++; } return clients; } function closeClients(clients: Clients): Promise { return Promise.all( Array.from(clients.values()).map((client) => client.Close()) ); } function isValidClientName(name: string): name is ClientNames { return clientNames.includes(name as ClientNames); } yargs(hideBin(process.argv)) .scriptName("client-runner") .command( "print-channels", "Prints all channels", async () => {}, async () => { const clients = await initializeClients(); for (const client of clients.values()) { const ledgers = await client.GetAllLedgerChannels(); console.log( `Client ${await client.GetAddress()} found ${ledgers.length} ledgers` ); if (ledgers.length > 0) { console.log(`LEDGERS`); } for (const ledger of ledgers) { console.log(`${compactJson(ledger)}`); const paymentChans = await client.GetPaymentChannelsByLedger( ledger.ID ); if (paymentChans.length > 0) { console.log(`PAYMENT CHANNELS FUNDED BY ${ledger.ID}`); for (const paymentChan of paymentChans) { console.log(`${compactJson(paymentChan)}`); } } } } await closeClients(clients); process.exit(0); } ) .command( "create-channels", "Creates some virtual channels and makes some payments", (yargsBuilder) => { return yargsBuilder .option("createledgers", { describe: `Whether we attempt to create new ledger channels. Set to false if you already have some ledger channels created.`, type: "boolean", default: true, }) .option("numvirtual", { describe: "The number of virtual channels to create between Alice and Bob.", type: "number", default: 5, }) .option("ledgerdeposit", { describe: "The amount of wei for each participant to deposit into the ledger channel", type: "number", default: 1_000_000_000_000, }) .option("virtualdeposit", { describe: "The amount of wei for each participant to deposit into a virtual channel", type: "number", default: 1_000_000, }) .option("numclosevirtual", { describe: "The number of virtual channels to close and defund.", type: "number", default: 2, }) .option("numpayments", { describe: "The number of payments to make from Alice to Bob.Each payment is made on a random virtual channel", type: "number", default: 5, }) .option("printnotifications", { alias: "n", describe: "Whether channel notifications are printed to the console", type: "boolean", default: false, }) .option("waitduration", { alias: "w", describe: "The amount of time, in milliseconds, to wait for the RPC servers to be responsive", type: "number", default: 0, }); }, async (yargs) => { if (yargs.waitduration > 0) { await Promise.all( Object.values(clientPortMap).map((port) => waitForRPCServer(port, yargs.waitduration) ) ); } const clients = await initializeClients(); if (yargs.printnotifications) { clients.forEach(logOutChannelUpdates); } const aliceClient = clients.get("alice"); const ireneClient = clients.get("irene"); const bobClient = clients.get("bob"); if (!aliceClient || !ireneClient || !bobClient) { throw new Error("An client is undefined"); } console.log("Retrieving client addresses"); const ireneAddress = await ireneClient.GetAddress(); const bobAddress = await bobClient.GetAddress(); if (yargs.createledgers) { // Setup ledger channels console.log("Constructing ledger channels"); const aliceLedger = await aliceClient.CreateLedgerChannel( ireneAddress, yargs.ledgerdeposit ); const bobLedger = await ireneClient.CreateLedgerChannel( bobAddress, yargs.ledgerdeposit ); await Promise.all([ aliceClient.WaitForLedgerChannelStatus(aliceLedger.ChannelId, "Open"), bobClient.WaitForLedgerChannelStatus(bobLedger.ChannelId, "Open"), ]); console.log(`Ledger channel ${bobLedger.ChannelId} created`); console.log(`Ledger channel ${aliceLedger.ChannelId} created`); } // Setup virtual channels const virtualChannels: string[] = []; console.log(`Constructing ${yargs.numvirtual} virtual channels`); for (let i = 0; i < yargs.numvirtual; i++) { const res = await aliceClient.CreatePaymentChannel( bobAddress, [ireneAddress], yargs.virtualdeposit ); await aliceClient.WaitForPaymentChannelStatus(res.ChannelId, "Open"); console.log(`Virtual channel ${res.ChannelId} created`); virtualChannels.push(res.ChannelId); } // Make payments console.log(`Making ${yargs.numpayments} payments`); for (let i = 0; i < yargs.numpayments; i++) { const channelId = getRandomElement(virtualChannels); const res = await aliceClient.Pay(channelId, 1); console.log(`Paid ${res.Amount} on channel ${res.Channel}`); } await wait(1000); // Close virtual channels console.log(`Closing ${yargs.numclosevirtual} virtual channels`); let closeCount = 0; for (const channelId of virtualChannels) { if (closeCount >= yargs.numclosevirtual) { break; } await aliceClient.ClosePaymentChannel(channelId); await aliceClient.WaitForPaymentChannelStatus(channelId, "Complete"); console.log(`Virtual channel ${channelId} closed`); closeCount++; } await closeClients(clients); process.exit(0); } ) .command( "create-ledger", "Create a ledger channel", (yargsBuilder) => { return yargsBuilder .option("left", { describe: "Left rpc node", type: "string", default: "alice", }) .option("right", { describe: "Right rpc node", type: "string", default: "irene", }); }, async (yargs) => { const { left, right } = yargs; if (!isValidClientName(left) || !isValidClientName(right)) { throw new Error("Invalid client name"); } const clients = await initializeClients(); const leftClient = clients.get(left); const rightClient = clients.get(right); if (!leftClient || !rightClient) { throw new Error("A client is undefined"); } const rightAddress = await rightClient.GetAddress(); if (yargs.printnotifications) { [leftClient, rightClient].forEach(logOutChannelUpdates); } // Setup ledger channels console.log("Constructing ledger channels"); const ledger = await leftClient.CreateLedgerChannel( rightAddress, 1_000_000 ); await leftClient.WaitForLedgerChannelStatus(ledger.ChannelId, "Open"); console.log(`Ledger channel ${ledger.ChannelId} created`); await closeClients(clients); } ) .demandCommand(1, "You need at least one command before moving on") .strict() .parse(); function getRandomElement(col: T[]): T { return col[Math.floor(Math.random() * col.length)]; } async function wait(ms: number) { await new Promise((res) => setTimeout(res, ms)); } // Waits for the RPC server to be available by sending a simple get_address POST request until we get a response async function waitForRPCServer( port: number, waitDuration: number ): Promise { const startTime = Date.now(); while (Date.now() - startTime < waitDuration) { const isUp = await isServerUp(port); if (isUp) { console.log( `RPC server ${getLocalRPCUrl(port)} is responding! Waited ${ Date.now() - startTime } milliseconds for server to be responsive` ); return; } else { console.log(`RPC server ${getLocalRPCUrl(port)} not available, waiting!`); await wait(1000); } } throw new Error( `RPC server ${getLocalRPCUrl(port)} not reachable in ${waitDuration} ms` ); } // Checks if the server is up by sending a simple get_address POST request // This is specific to the HTTP/WS RPC transport async function isServerUp(port: number): Promise { let result: AxiosResponse; const url = new URL(`https://${getLocalRPCUrl(port)}/health`).toString(); try { result = await axios.get(url); } catch (e) { return false; } return result.status === 200; }