nitro-rpc-client/scripts/client-runner.ts
2024-03-14 14:46:32 -05:00

328 lines
9.7 KiB
TypeScript
Executable File

#!/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<ClientNames, number> = {
alice: 4005,
irene: 4006,
bob: 4007,
ivan: 4008,
};
type ClientNames = (typeof clientNames)[number];
type Clients = Map<ClientNames, NitroRpcClient>;
async function initializeClients(): Promise<Clients> {
let port = 4005;
const clients: Clients = new Map<ClientNames, NitroRpcClient>();
for (const clientName of clientNames) {
const client = await NitroRpcClient.CreateHttpNitroClient(
getLocalRPCUrl(port)
);
clients.set(clientName, client);
port++;
}
return clients;
}
function closeClients(clients: Clients): Promise<void[]> {
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<T>(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<void> {
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<boolean> {
let result: AxiosResponse<unknown, unknown>;
const url = new URL(`https://${getLocalRPCUrl(port)}/health`).toString();
try {
result = await axios.get(url);
} catch (e) {
return false;
}
return result.status === 200;
}