This commit is contained in:
Thomas E Lackey 2024-03-14 15:16:09 -05:00
parent 940a728724
commit 6d5be18915
24 changed files with 5900 additions and 0 deletions

2
dist/src/cli.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
#!/usr/bin/env ts-node
export {};

230
dist/src/cli.js vendored Normal file
View File

@ -0,0 +1,230 @@
#!/usr/bin/env ts-node
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-shadow */
import yargs from "yargs/yargs";
import { hideBin } from "yargs/helpers";
import { NitroRpcClient } from "./rpc-client";
import { compactJson, getLocalRPCUrl, logOutChannelUpdates } from "./utils";
yargs(hideBin(process.argv))
.scriptName("nitro-rpc-client")
.option({
p: { alias: "port", default: 4005, type: "number" },
n: {
alias: "printnotifications",
default: false,
type: "boolean",
description: "Whether channel notifications are printed to the console",
},
})
.command("version", "Get the version of the Nitro RPC server", async () => { }, async (yargs) => {
const rpcPort = yargs.p;
const rpcClient = await NitroRpcClient.CreateHttpNitroClient(getLocalRPCUrl(rpcPort));
const version = await rpcClient.GetVersion();
console.log(version);
await rpcClient.Close();
process.exit(0);
})
.command("address", "Get the address of the Nitro RPC server", async () => { }, async (yargs) => {
const rpcPort = yargs.p;
const rpcClient = await NitroRpcClient.CreateHttpNitroClient(getLocalRPCUrl(rpcPort));
const address = await rpcClient.GetAddress();
console.log(address);
await rpcClient.Close();
process.exit(0);
})
.command("get-all-ledger-channels", "Get all ledger channels", async () => { }, async (yargs) => {
const rpcPort = yargs.p;
const rpcClient = await NitroRpcClient.CreateHttpNitroClient(getLocalRPCUrl(rpcPort));
const ledgers = await rpcClient.GetAllLedgerChannels();
for (const ledger of ledgers) {
console.log(`${compactJson(ledger)}`);
}
await rpcClient.Close();
process.exit(0);
})
.command("get-payment-channels-by-ledger <ledgerId>", "Gets any payment channels funded by the given ledger", (yargsBuilder) => {
return yargsBuilder.positional("ledgerId", {
describe: "The id of the ledger channel to defund",
type: "string",
demandOption: true,
});
}, async (yargs) => {
const rpcPort = yargs.p;
const rpcClient = await NitroRpcClient.CreateHttpNitroClient(getLocalRPCUrl(rpcPort));
const paymentChans = await rpcClient.GetPaymentChannelsByLedger(yargs.ledgerId);
for (const p of paymentChans) {
console.log(`${compactJson(p)}`);
}
await rpcClient.Close();
process.exit(0);
})
.command("direct-fund <counterparty>", "Creates a directly funded ledger channel", (yargsBuilder) => {
return yargsBuilder
.positional("counterparty", {
describe: "The counterparty's address",
type: "string",
demandOption: true,
})
.option("amount", {
describe: "The amount to fund the channel with",
type: "number",
default: 1_000_000,
});
}, async (yargs) => {
const rpcPort = yargs.p;
const rpcClient = await NitroRpcClient.CreateHttpNitroClient(getLocalRPCUrl(rpcPort));
if (yargs.n)
logOutChannelUpdates(rpcClient);
const dfObjective = await rpcClient.CreateLedgerChannel(yargs.counterparty, yargs.amount);
const { Id, ChannelId } = dfObjective;
console.log(`Objective started ${Id}`);
await rpcClient.WaitForLedgerChannelStatus(ChannelId, "Open");
console.log(`Channel Open ${ChannelId}`);
await rpcClient.Close();
process.exit(0);
})
.command("direct-defund <channelId>", "Defunds a directly funded ledger channel", (yargsBuilder) => {
return yargsBuilder.positional("channelId", {
describe: "The id of the ledger channel to defund",
type: "string",
demandOption: true,
});
}, async (yargs) => {
const rpcPort = yargs.p;
const rpcClient = await NitroRpcClient.CreateHttpNitroClient(getLocalRPCUrl(rpcPort));
if (yargs.n)
logOutChannelUpdates(rpcClient);
const id = await rpcClient.CloseLedgerChannel(yargs.channelId);
console.log(`Objective started ${id}`);
await rpcClient.WaitForPaymentChannelStatus(yargs.channelId, "Complete");
console.log(`Channel Complete ${yargs.channelId}`);
await rpcClient.Close();
process.exit(0);
})
.command("virtual-fund <counterparty> [intermediaries...]", "Creates a virtually funded payment channel", (yargsBuilder) => {
return yargsBuilder
.positional("counterparty", {
describe: "The counterparty's address",
type: "string",
demandOption: true,
})
.array("intermediaries")
.option("amount", {
describe: "The amount to fund the channel with",
type: "number",
default: 1000,
});
}, async (yargs) => {
const rpcPort = yargs.p;
const rpcClient = await NitroRpcClient.CreateHttpNitroClient(getLocalRPCUrl(rpcPort));
if (yargs.n)
logOutChannelUpdates(rpcClient);
// Parse all intermediary args to strings
const intermediaries = yargs.intermediaries?.map((intermediary) => {
if (typeof intermediary === "string") {
return intermediary;
}
return intermediary.toString(16);
}) ?? [];
const vfObjective = await rpcClient.CreatePaymentChannel(yargs.counterparty, intermediaries, yargs.amount);
const { ChannelId, Id } = vfObjective;
console.log(`Objective started ${Id}`);
await rpcClient.WaitForPaymentChannelStatus(ChannelId, "Open");
console.log(`Channel Open ${ChannelId}`);
await rpcClient.Close();
process.exit(0);
})
.command("virtual-defund <channelId>", "Defunds a virtually funded payment channel", (yargsBuilder) => {
return yargsBuilder.positional("channelId", {
describe: "The id of the payment channel to defund",
type: "string",
demandOption: true,
});
}, async (yargs) => {
const rpcPort = yargs.p;
const rpcClient = await NitroRpcClient.CreateHttpNitroClient(getLocalRPCUrl(rpcPort));
if (yargs.n)
logOutChannelUpdates(rpcClient);
const id = await rpcClient.ClosePaymentChannel(yargs.channelId);
console.log(`Objective started ${id}`);
await rpcClient.WaitForPaymentChannelStatus(yargs.channelId, "Complete");
console.log(`Channel complete ${yargs.channelId}`);
await rpcClient.Close();
process.exit(0);
})
.command("get-ledger-channel <channelId>", "Gets information about a ledger channel", (yargsBuilder) => {
return yargsBuilder.positional("channelId", {
describe: "The channel ID of the ledger channel",
type: "string",
demandOption: true,
});
}, async (yargs) => {
const rpcPort = yargs.p;
const rpcClient = await NitroRpcClient.CreateHttpNitroClient(getLocalRPCUrl(rpcPort));
const ledgerInfo = await rpcClient.GetLedgerChannel(yargs.channelId);
console.log(ledgerInfo);
await rpcClient.Close();
process.exit(0);
})
.command("get-payment-channel <channelId>", "Gets information about a payment channel", (yargsBuilder) => {
return yargsBuilder.positional("channelId", {
describe: "The channel ID of the payment channel",
type: "string",
demandOption: true,
});
}, async (yargs) => {
const rpcPort = yargs.p;
const rpcClient = await NitroRpcClient.CreateHttpNitroClient(getLocalRPCUrl(rpcPort));
const paymentChannelInfo = await rpcClient.GetPaymentChannel(yargs.channelId);
console.log(paymentChannelInfo);
await rpcClient.Close();
process.exit(0);
})
.command("pay <channelId> <amount>", "Sends a payment on the given channel", (yargsBuilder) => {
return yargsBuilder
.positional("channelId", {
describe: "The channel ID of the payment channel",
type: "string",
demandOption: true,
})
.positional("amount", {
describe: "The amount to pay",
type: "number",
demandOption: true,
});
}, async (yargs) => {
const rpcPort = yargs.p;
const rpcClient = await NitroRpcClient.CreateHttpNitroClient(getLocalRPCUrl(rpcPort));
if (yargs.n)
logOutChannelUpdates(rpcClient);
const paymentChannelInfo = await rpcClient.Pay(yargs.channelId, yargs.amount);
console.log(paymentChannelInfo);
await rpcClient.Close();
process.exit(0);
})
.command("create-voucher <channelId> <amount>", "Create a payment on the given channel", (yargsBuilder) => {
return yargsBuilder
.positional("channelId", {
describe: "The channel ID of the payment channel",
type: "string",
demandOption: true,
})
.positional("amount", {
describe: "The amount to pay",
type: "number",
demandOption: true,
});
}, async (yargs) => {
const rpcPort = yargs.p;
const rpcClient = await NitroRpcClient.CreateHttpNitroClient(getLocalRPCUrl(rpcPort));
if (yargs.n)
logOutChannelUpdates(rpcClient);
const voucher = await rpcClient.CreateVoucher(yargs.channelId, yargs.amount);
console.log(voucher);
await rpcClient.Close();
process.exit(0);
})
.demandCommand(1, "You need at least one command before moving on")
.parserConfiguration({ "parse-numbers": false })
.strict()
.parse();

1
dist/src/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export { NitroRpcClient } from "./rpc-client";

1
dist/src/index.js vendored Normal file
View File

@ -0,0 +1 @@
export { NitroRpcClient } from "./rpc-client";

125
dist/src/interface.d.ts vendored Normal file
View File

@ -0,0 +1,125 @@
import { ChannelStatus, LedgerChannelInfo, ObjectiveResponse, PaymentChannelInfo, PaymentPayload, ReceiveVoucherResult, Voucher } from "./types";
interface ledgerChannelApi {
/**
* CreateLedgerChannel creates a directly funded ledger channel with the counterparty.
*
* @param counterParty - The counterparty to create the channel with
* @returns A promise that resolves to an objective response, containing the ID of the objective and the channel id.
*/
CreateLedgerChannel(counterParty: string, amount: number): Promise<ObjectiveResponse>;
/**
* CloseLedgerChannel defunds a directly funded ledger channel.
*
* @param channelId - The ID of the channel to defund
* @returns The ID of the objective that was created
*/
CloseLedgerChannel(channelId: string): Promise<string>;
/**
* GetLedgerChannel queries the RPC server for a payment channel.
*
* @param channelId - The ID of the channel to query for
* @returns A `LedgerChannelInfo` object containing the channel's information
*/
GetLedgerChannel(channelId: string): Promise<LedgerChannelInfo>;
/**
* GetAllLedgerChannels queries the RPC server for all ledger channels.
* @returns A `LedgerChannelInfo` object containing the channel's information for each ledger channel
*/
GetAllLedgerChannels(): Promise<LedgerChannelInfo[]>;
}
interface paymentChannelApi {
/**
* CreatePaymentChannel creates a virtually funded payment channel with the counterparty, using the given intermediaries.
*
* @param counterParty - The counterparty to create the channel with
* @param intermediaries - The intermerdiaries to use
* @returns A promise that resolves to an objective response, containing the ID of the objective and the channel id.
*/
CreatePaymentChannel(counterParty: string, intermediaries: string[], amount: number): Promise<ObjectiveResponse>;
/**
* ClosePaymentChannel defunds a virtually funded payment channel.
*
* @param channelId - The ID of the channel to defund
* @returns The ID of the objective that was created
*/
ClosePaymentChannel(channelId: string): Promise<string>;
/**
* GetPaymentChannel queries the RPC server for a payment channel.
*
* @param channelId - The ID of the channel to query for
* @returns A `PaymentChannelInfo` object containing the channel's information
*/
GetPaymentChannel(channelId: string): Promise<PaymentChannelInfo>;
/**
* GetPaymentChannelsByLedger queries the RPC server for any payment channels that are actively funded by the given ledger.
*
* @param ledgerId - The ID of the ledger to find payment channels for
* @returns A `PaymentChannelInfo` object containing the channel's information for each payment channel
*/
GetPaymentChannelsByLedger(ledgerId: string): Promise<PaymentChannelInfo[]>;
}
interface paymentApi {
/**
* Creates a payment voucher for the given channel and amount.
* The voucher does not get sent to the other party automatically.
* @param channelId The payment channel to use for the voucher
* @param amount The amount for the voucher
* @returns A signed voucher
*/
CreateVoucher(channelId: string, amount: number): Promise<Voucher>;
/**
* Adds a voucher to the go-nitro node that was received from the other party to the channel.
* @param voucher The voucher to add
* @returns The total amount of the channel and the delta of the voucher
*/
ReceiveVoucher(voucher: Voucher): Promise<ReceiveVoucherResult>;
/**
* Pay sends a payment on a virtual payment chanel.
*
* @param channelId - The ID of the payment channel to use
* @param amount - The amount to pay
*/
Pay(channelId: string, amount: number): Promise<PaymentPayload>;
}
interface syncAPI {
/**
* WaitForLedgerChannelStatus blocks until the ledger channel with the given ID to have the given status.
*
* @param objectiveId - The channel id to wait for
* @param status - The channel id to wait for (e.g. Ready or Closing)
*/
WaitForLedgerChannelStatus(objectiveId: string, status: ChannelStatus): Promise<void>;
/**
* WaitForPaymentChannelStatus blocks until the payment channel with the given ID to have the given status.
*
* @param objectiveId - The channel id to wait for
* @param status - The channel id to wait for (e.g. Ready or Closing)
*/
WaitForPaymentChannelStatus(objectiveId: string, status: ChannelStatus): Promise<void>;
/**
* PaymentChannelUpdated attaches a callback which is triggered when the channel with supplied ID is updated.
* Returns a cleanup function which can be used to remove the subscription.
*
* @param objectiveId - The id objective to wait for
*/
onPaymentChannelUpdated(channelId: string, callback: (info: PaymentChannelInfo) => void): () => void;
}
export interface RpcClientApi extends ledgerChannelApi, paymentChannelApi, paymentApi, syncAPI {
/**
* GetVersion queries the API server for it's version.
*
* @returns The version of the RPC server
*/
GetVersion(): Promise<string>;
/**
* GetAddress queries the RPC server for it's state channel address.
*
* @returns The address of the wallet connected to the RPC server
*/
GetAddress(): Promise<string>;
/**
* Close closes the RPC client and stops listening for notifications.
*/
Close(): Promise<void>;
}
export {};

1
dist/src/interface.js vendored Normal file
View File

@ -0,0 +1 @@
export {};

41
dist/src/rpc-client.d.ts vendored Normal file
View File

@ -0,0 +1,41 @@
import { LedgerChannelInfo, PaymentChannelInfo, PaymentPayload, ObjectiveResponse, Voucher, ReceiveVoucherResult, ChannelStatus } from "./types";
import { RpcClientApi } from "./interface";
export declare class NitroRpcClient implements RpcClientApi {
private transport;
private myAddress;
private authToken;
get Notifications(): import("eventemitter3").EventEmitter<"objective_completed" | "payment_channel_updated" | "ledger_channel_updated", {
payload: string;
} | {
payload: PaymentChannelInfo;
} | {
payload: LedgerChannelInfo;
}>;
CreateVoucher(channelId: string, amount: number): Promise<Voucher>;
ReceiveVoucher(voucher: Voucher): Promise<ReceiveVoucherResult>;
WaitForLedgerChannelStatus(channelId: string, status: ChannelStatus): Promise<void>;
WaitForPaymentChannelStatus(channelId: string, status: ChannelStatus): Promise<void>;
onPaymentChannelUpdated(channelId: string, callback: (info: PaymentChannelInfo) => void): () => void;
CreateLedgerChannel(counterParty: string, amount: number): Promise<ObjectiveResponse>;
CreatePaymentChannel(counterParty: string, intermediaries: string[], amount: number): Promise<ObjectiveResponse>;
Pay(channelId: string, amount: number): Promise<PaymentPayload>;
CloseLedgerChannel(channelId: string): Promise<string>;
ClosePaymentChannel(channelId: string): Promise<string>;
GetVersion(): Promise<string>;
GetAddress(): Promise<string>;
GetLedgerChannel(channelId: string): Promise<LedgerChannelInfo>;
GetAllLedgerChannels(): Promise<LedgerChannelInfo[]>;
GetPaymentChannel(channelId: string): Promise<PaymentChannelInfo>;
GetPaymentChannelsByLedger(ledgerId: string): Promise<PaymentChannelInfo[]>;
private getAuthToken;
private sendRequest;
Close(): Promise<void>;
private constructor();
/**
* Creates an RPC client that uses HTTP/WS as the transport.
*
* @param url - The URL of the HTTP/WS server
* @returns A NitroRpcClient that uses WS as the transport
*/
static CreateHttpNitroClient(url: string): Promise<NitroRpcClient>;
}

160
dist/src/rpc-client.js vendored Normal file
View File

@ -0,0 +1,160 @@
import { createOutcome, generateRequest } from "./utils";
import { HttpTransport } from "./transport/http";
import { getAndValidateResult } from "./serde";
export class NitroRpcClient {
transport;
// We fetch the address from the RPC server on first use
myAddress;
authToken;
get Notifications() {
return this.transport.Notifications;
}
async CreateVoucher(channelId, amount) {
const payload = {
Amount: amount,
Channel: channelId,
};
const request = generateRequest("create_voucher", payload, this.authToken || "");
const res = await this.transport.sendRequest(request);
return getAndValidateResult(res, "create_voucher");
}
async ReceiveVoucher(voucher) {
const request = generateRequest("receive_voucher", voucher, this.authToken || "");
const res = await this.transport.sendRequest(request);
return getAndValidateResult(res, "receive_voucher");
}
async WaitForLedgerChannelStatus(channelId, status) {
const promise = new Promise((resolve) => {
this.transport.Notifications.on("ledger_channel_updated", (payload) => {
if (payload.ID === channelId) {
this.GetLedgerChannel(channelId).then((l) => {
if (l.Status == status)
resolve();
});
}
});
});
const ledger = await this.GetLedgerChannel(channelId);
if (ledger.Status == status)
return;
return promise;
}
async WaitForPaymentChannelStatus(channelId, status) {
const promise = new Promise((resolve) => {
this.transport.Notifications.on("payment_channel_updated", (payload) => {
if (payload.ID === channelId) {
this.GetPaymentChannel(channelId).then((l) => {
if (l.Status == status)
resolve();
});
}
});
});
const channel = await this.GetPaymentChannel(channelId);
if (channel.Status == status)
return;
return promise;
}
onPaymentChannelUpdated(channelId, callback) {
const wrapperFn = (info) => {
if (info.ID.toLowerCase() == channelId.toLowerCase()) {
callback(info);
}
};
this.transport.Notifications.on("payment_channel_updated", wrapperFn);
return () => {
this.transport.Notifications.off("payment_channel_updated", wrapperFn);
};
}
async CreateLedgerChannel(counterParty, amount) {
const asset = `0x${"00".repeat(20)}`;
const payload = {
CounterParty: counterParty,
ChallengeDuration: 0,
Outcome: createOutcome(asset, await this.GetAddress(), counterParty, amount),
AppDefinition: asset,
AppData: "0x00",
Nonce: Date.now(),
};
return this.sendRequest("create_ledger_channel", payload);
}
async CreatePaymentChannel(counterParty, intermediaries, amount) {
const asset = `0x${"00".repeat(20)}`;
const payload = {
CounterParty: counterParty,
Intermediaries: intermediaries,
ChallengeDuration: 0,
Outcome: createOutcome(asset, await this.GetAddress(), counterParty, amount),
AppDefinition: asset,
Nonce: Date.now(),
};
return this.sendRequest("create_payment_channel", payload);
}
async Pay(channelId, amount) {
const payload = {
Amount: amount,
Channel: channelId,
};
const request = generateRequest("pay", payload, this.authToken || "");
const res = await this.transport.sendRequest(request);
return getAndValidateResult(res, "pay");
}
async CloseLedgerChannel(channelId) {
const payload = { ChannelId: channelId };
return this.sendRequest("close_ledger_channel", payload);
}
async ClosePaymentChannel(channelId) {
const payload = { ChannelId: channelId };
return this.sendRequest("close_payment_channel", payload);
}
async GetVersion() {
return this.sendRequest("version", {});
}
async GetAddress() {
if (this.myAddress) {
return this.myAddress;
}
this.myAddress = await this.sendRequest("get_address", {});
return this.myAddress;
}
async GetLedgerChannel(channelId) {
return this.sendRequest("get_ledger_channel", { Id: channelId });
}
async GetAllLedgerChannels() {
return this.sendRequest("get_all_ledger_channels", {});
}
async GetPaymentChannel(channelId) {
return this.sendRequest("get_payment_channel", { Id: channelId });
}
async GetPaymentChannelsByLedger(ledgerId) {
return this.sendRequest("get_payment_channels_by_ledger", {
LedgerId: ledgerId,
});
}
async getAuthToken() {
return this.sendRequest("get_auth_token", {});
}
async sendRequest(method, payload) {
const request = generateRequest(method, payload, this.authToken || "");
const res = await this.transport.sendRequest(request);
return getAndValidateResult(res, method);
}
async Close() {
return this.transport.Close();
}
constructor(transport) {
this.transport = transport;
}
/**
* Creates an RPC client that uses HTTP/WS as the transport.
*
* @param url - The URL of the HTTP/WS server
* @returns A NitroRpcClient that uses WS as the transport
*/
static async CreateHttpNitroClient(url) {
const transport = await HttpTransport.createTransport(url);
const rpcClient = new NitroRpcClient(transport);
rpcClient.authToken = await rpcClient.getAuthToken();
return rpcClient;
}
}

9
dist/src/serde.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import { RPCNotification, RPCRequestAndResponses, RequestMethod } from "./types";
/**
* Validates that the response is a valid JSON RPC response with a valid result
* @param response - JSON RPC response
* @param method - JSON RPC method
* @returns The validated result of the JSON RPC response
*/
export declare function getAndValidateResult<T extends RequestMethod>(response: unknown, method: T): RPCRequestAndResponses[T][1]["result"];
export declare function getAndValidateNotification<T extends RPCNotification["method"]>(data: unknown, method: T): RPCNotification["params"]["payload"];

204
dist/src/serde.js vendored Normal file
View File

@ -0,0 +1,204 @@
import Ajv from "ajv/dist/jtd";
const ajv = new Ajv();
const jsonRpcSchema = {
properties: {
jsonrpc: { type: "string" },
id: { type: "uint32" },
},
optionalProperties: {
result: {
nullable: true,
},
error: {
properties: {
code: { type: "int32" },
message: { type: "string" },
},
additionalProperties: true,
nullable: true,
},
},
};
const objectiveSchema = {
properties: {
Id: { type: "string" },
ChannelId: { type: "string" },
},
};
const stringSchema = { type: "string" };
const ledgerChannelSchema = {
properties: {
ID: { type: "string" },
Status: { type: "string" },
Balance: {
properties: {
AssetAddress: { type: "string" },
Them: { type: "string" },
Me: { type: "string" },
MyBalance: { type: "string" },
TheirBalance: { type: "string" },
},
},
},
};
const paymentChannelSchema = {
properties: {
ID: { type: "string" },
Status: { type: "string" },
Balance: {
properties: {
AssetAddress: { type: "string" },
Payee: { type: "string" },
Payer: { type: "string" },
PaidSoFar: { type: "string" },
RemainingFunds: { type: "string" },
},
},
},
};
const ledgerChannelsSchema = {
elements: {
...ledgerChannelSchema,
},
};
const paymentChannelsSchema = {
elements: {
...paymentChannelSchema,
},
};
const paymentSchema = {
properties: {
Amount: { type: "uint32" },
Channel: { type: "string" },
},
};
const voucherSchema = {
properties: {
ChannelId: { type: "string" },
Amount: { type: "uint32" },
Signature: {
type: "string",
},
},
};
const receiveVoucherSchema = {
properties: {
Total: { type: "string" },
Delta: { type: "string" },
},
};
/**
* Validates that the response is a valid JSON RPC response with a valid result
* @param response - JSON RPC response
* @param method - JSON RPC method
* @returns The validated result of the JSON RPC response
*/
export function getAndValidateResult(response, method) {
const { result, error } = getJsonRpcResult(response);
if (error) {
throw new Error("jsonrpc response: " + error.message);
}
switch (method) {
case "create_ledger_channel":
case "create_payment_channel":
return validateAndConvertResult(objectiveSchema, result, (result) => result);
case "get_auth_token":
case "close_ledger_channel":
case "version":
case "get_address":
case "close_payment_channel":
return validateAndConvertResult(stringSchema, result, (result) => result);
case "get_ledger_channel":
return validateAndConvertResult(ledgerChannelSchema, result, convertToInternalLedgerChannelType);
case "get_all_ledger_channels":
return validateAndConvertResult(ledgerChannelsSchema, result, convertToInternalLedgerChannelsType);
case "get_payment_channel":
return validateAndConvertResult(paymentChannelSchema, result, convertToInternalPaymentChannelType);
case "get_payment_channels_by_ledger":
return validateAndConvertResult(paymentChannelsSchema, result, convertToInternalPaymentChannelsType);
case "pay":
return validateAndConvertResult(paymentSchema, result, (result) => result);
case "receive_voucher":
return validateAndConvertResult(receiveVoucherSchema, result, (result) => ({
Total: BigInt(result.Total),
Delta: BigInt(result.Delta),
}));
case "create_voucher":
return validateAndConvertResult(voucherSchema, result, (result) => {
return {
...result,
};
});
default:
throw new Error(`Unknown method: ${method}`);
}
}
export function getAndValidateNotification(data, method) {
switch (method) {
case "payment_channel_updated":
return convertToInternalPaymentChannelType(data);
case "ledger_channel_updated":
return convertToInternalPaymentChannelType(data);
case "objective_completed":
return data;
default:
throw new Error(`Unknown method: ${method}`);
}
}
/**
* Validates that the response is a valid JSON RPC response and pulls out the result
* @param response - JSON RPC response
* @returns The result of the response
*/
function getJsonRpcResult(response) {
const validate = ajv.compile(jsonRpcSchema);
if (validate(response)) {
return response;
}
throw new Error(`Invalid json rpc response: ${JSON.stringify(validate.errors)}. The response is ${JSON.stringify(response)}`);
}
/**
* validateAndConvertResult validates that the result object conforms to the schema and converts it to the internal type
*
* @param schema - JSON Type Definition
* @param result - Object to validate
* @param converstionFn - Function to convert the valiated object to internal type
* @returns A validated object of internal type
*/
function validateAndConvertResult(schema, result, converstionFn) {
const validate = ajv.compile(schema);
if (validate(result)) {
return converstionFn(result);
}
throw new Error(`Error parsing json rpc result: ${JSON.stringify(validate.errors)}. The result is ${JSON.stringify(result)}`);
}
function convertToInternalLedgerChannelType(result) {
// todo: validate channel status
return {
...result,
Status: result.Status,
Balance: {
...result.Balance,
TheirBalance: BigInt(result.Balance.TheirBalance),
MyBalance: BigInt(result.Balance.MyBalance),
},
};
}
function convertToInternalLedgerChannelsType(result) {
return result.map((lc) => convertToInternalLedgerChannelType(lc));
}
function convertToInternalPaymentChannelType(result) {
// todo: validate channel status
return {
...result,
Status: result.Status,
Balance: {
...result.Balance,
PaidSoFar: BigInt(result.Balance.PaidSoFar ?? 0),
RemainingFunds: BigInt(result.Balance.RemainingFunds ?? 0),
},
};
}
function convertToInternalPaymentChannelsType(result) {
return result.map((pc) => convertToInternalPaymentChannelType(pc));
}

1
dist/src/serde.test.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export {};

79
dist/src/serde.test.js vendored Normal file
View File

@ -0,0 +1,79 @@
import { getAndValidateResult } from "./serde";
describe("get_address", () => {
it("success: validate response string", () => {
const getAddressResponse = {
jsonrpc: "2.0",
id: 168513765,
result: "0x111A00868581f73AB42FEEF67D235Ca09ca1E8db",
};
const validatedResponse = getAndValidateResult(getAddressResponse, "get_address");
expect(validatedResponse).toEqual(getAddressResponse.result);
});
});
describe("get_ledger_channel", () => {
it("success: validate response object", () => {
const getLedgerChannelResponse = {
jsonrpc: "2.0",
id: 168513765,
result: {
ID: "0x586d127530f69177d790bb940eae132922e7648c29264648af5375de2c19e270",
Status: "Open",
Balance: {
AssetAddress: "0x0000000000000000000000000000000000000000",
Them: "0x111a00868581f73ab42feef67d235ca09ca1e8db",
Me: "0xaaa6628ec44a8a742987ef3a114ddfe2d4f7adce",
TheirBalance: "0xf368a",
MyBalance: "0xf3686",
},
},
};
const validatedGetLedgerChannelResponse = {
ID: "0x586d127530f69177d790bb940eae132922e7648c29264648af5375de2c19e270",
Status: "Open",
Balance: {
AssetAddress: "0x0000000000000000000000000000000000000000",
Them: "0x111a00868581f73ab42feef67d235ca09ca1e8db",
Me: "0xaaa6628ec44a8a742987ef3a114ddfe2d4f7adce",
TheirBalance: 997002n,
MyBalance: 996998n,
},
};
const validatedResponse = getAndValidateResult(getLedgerChannelResponse, "get_ledger_channel");
expect(validatedResponse).toEqual(validatedGetLedgerChannelResponse);
});
});
describe("create_ledger_channel", () => {
it("error", () => {
const failedCreateLedgerResponse = {
jsonrpc: "2.0",
id: 168513765,
error: {
code: -32603,
message: "Internal Server Error",
},
};
try {
getAndValidateResult(failedCreateLedgerResponse, "create_ledger_channel");
}
catch (err) {
if (err instanceof Error) {
expect(err.message).toEqual("jsonrpc response: Internal Server Error");
}
else {
expect(false);
}
}
});
it("success: validate response object", () => {
const successCreateLedgerResponse = {
jsonrpc: "2.0",
id: 995772692,
result: {
Id: "123",
ChannelId: "456",
},
};
const validatedResponse = getAndValidateResult(successCreateLedgerResponse, "create_ledger_channel");
expect(validatedResponse).toEqual(successCreateLedgerResponse.result);
});
});

15
dist/src/transport/http.d.ts vendored Normal file
View File

@ -0,0 +1,15 @@
/// <reference types="node" />
import https from "https";
import { EventEmitter } from "eventemitter3";
import { NotificationMethod, NotificationParams, RequestMethod, RPCRequestAndResponses } from "../types";
import { Transport } from ".";
export declare class HttpTransport {
Notifications: EventEmitter<NotificationMethod, NotificationParams>;
static createTransport(server: string): Promise<Transport>;
sendRequest<K extends RequestMethod>(req: RPCRequestAndResponses[K][0]): Promise<unknown>;
Close(): Promise<void>;
private ws;
private server;
private constructor();
}
export declare function unsecureHttpsAgent(): https.Agent;

49
dist/src/transport/http.js vendored Normal file
View File

@ -0,0 +1,49 @@
import https from "https";
import axios from "axios";
import { w3cwebsocket } from "websocket";
import { EventEmitter } from "eventemitter3";
import { getAndValidateNotification } from "../serde";
export class HttpTransport {
Notifications;
static async createTransport(server) {
// eslint-disable-next-line new-cap
const ws = new w3cwebsocket(`wss://${server}/subscribe`);
// throw any websocket errors so we don't fail silently
ws.onerror = (e) => {
console.error("Error with websocket connection to server: " + e);
throw e;
};
// Wait for onopen to fire so we know the connection is ready
await new Promise((resolve) => (ws.onopen = () => resolve()));
const transport = new HttpTransport(ws, server);
return transport;
}
async sendRequest(req) {
const url = new URL(`https://${this.server}`).toString();
const result = await axios.post(url.toString(), JSON.stringify(req));
return result.data;
}
async Close() {
this.ws.close(1000);
}
ws;
server;
constructor(ws, server) {
this.ws = ws;
this.server = server;
this.Notifications = new EventEmitter();
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data.toString());
const validatedResult = getAndValidateNotification(data.params.payload, data.method);
this.Notifications.emit(data.method, validatedResult);
};
}
}
// For testing with self-signed certs, ignore certificate errors. DO NOT use in production.
export function unsecureHttpsAgent() {
// For testing with self-signed certs, ignore certificate errors. DO NOT use in production.
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
return httpsAgent;
}

21
dist/src/transport/index.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
import { EventEmitter } from "eventemitter3";
import { NotificationMethod, NotificationParams, RequestMethod, RPCNotification, RPCRequestAndResponses } from "../types";
export { HttpTransport } from "./http";
export { NatsTransport } from "./nats";
/**
* NotificationHandler is a function that takes a notification and does something with it.
*/
export type NotificationHandler<T extends RPCNotification> = (notif: T) => void;
/**
* Transport is an interface for some kind of RPC transport.
*/
export type Transport = {
Notifications: EventEmitter<NotificationMethod, NotificationParams>;
/**
* Send the JSON-RPC request and returns the response.
*
* @param req - The request to send
*/
sendRequest<K extends RequestMethod>(req: RPCRequestAndResponses[K][0]): Promise<unknown>;
Close(): Promise<void>;
};

2
dist/src/transport/index.js vendored Normal file
View File

@ -0,0 +1,2 @@
export { HttpTransport } from "./http";
export { NatsTransport } from "./nats";

14
dist/src/transport/nats.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
import { EventEmitter } from "eventemitter3";
import { NotificationMethod, NotificationParams, RequestMethod, RPCRequestAndResponses } from "../types";
import { Transport } from ".";
export declare class NatsTransport {
private natsConn;
private natsSub;
private notifications;
static createTransport(server: string): Promise<Transport>;
private constructor();
get Notifications(): EventEmitter<NotificationMethod, NotificationParams>;
private listenForMessages;
sendRequest<K extends RequestMethod>(req: RPCRequestAndResponses[K][0]): Promise<RPCRequestAndResponses[K][1]>;
Close(): Promise<void>;
}

53
dist/src/transport/nats.js vendored Normal file
View File

@ -0,0 +1,53 @@
import { connect, JSONCodec } from "nats";
import { EventEmitter } from "eventemitter3";
const NITRO_REQUEST_TOPIC = "nitro-request";
const NITRO_NOTIFICATION_TOPIC = "nitro-notify";
export class NatsTransport {
natsConn;
natsSub;
notifications = new EventEmitter();
static async createTransport(server) {
const natConn = await connect({ servers: server });
const natsSub = natConn.subscribe(NITRO_NOTIFICATION_TOPIC);
const transport = new NatsTransport(natConn, natsSub);
// Start listening for messages without blocking
transport.listenForMessages(transport.natsSub);
return transport;
}
constructor(natsConn, natsSub) {
this.natsConn = natsConn;
this.natsSub = natsSub;
}
get Notifications() {
return this.notifications;
}
async listenForMessages(sub) {
for await (const msg of sub) {
msg.data;
const notif = JSONCodec().decode(msg.data);
switch (notif.method) {
case "objective_completed":
this.notifications.emit(notif.method, notif);
break;
case "ledger_channel_updated":
this.notifications.emit(notif.method, notif);
break;
case "payment_channel_updated":
this.notifications.emit(notif.method, notif);
break;
}
}
}
async sendRequest(req) {
const natsRes = await this.natsConn?.request(NITRO_REQUEST_TOPIC, JSONCodec().encode(req));
if (!natsRes) {
throw new Error("No response");
}
const decoded = JSONCodec().decode(natsRes?.data);
return decoded;
}
async Close() {
this.natsSub.unsubscribe();
await this.natsConn.close();
}
}

198
dist/src/types.d.ts vendored Normal file
View File

@ -0,0 +1,198 @@
/**
* JSON RPC Types
*/
export type JsonRpcRequest<MethodName extends RequestMethod, RequestPayload> = {
id: number;
jsonrpc: "2.0";
method: MethodName;
params: {
authtoken: string;
payload: RequestPayload;
};
};
export type JsonRpcResponse<ResultType> = {
id: number;
jsonrpc: "2.0";
result: ResultType;
};
export type JsonRpcNotification<NotificationName, NotificationPayload> = {
jsonrpc: "2.0";
method: NotificationName;
params: {
payload: NotificationPayload;
};
};
export type JsonRpcError<Code, Message, Data = undefined> = {
id: number;
jsonrpc: "2.0";
error: Data extends undefined ? {
code: Code;
message: Message;
} : {
code: Code;
message: Message;
data: Data;
};
};
/**
* Objective payloads and responses
*/
export type DirectFundPayload = {
CounterParty: string;
ChallengeDuration: number;
Outcome: Outcome;
Nonce: number;
AppDefinition: string;
AppData: string;
};
export type VirtualFundPayload = {
Intermediaries: string[];
CounterParty: string;
ChallengeDuration: number;
Outcome: Outcome;
Nonce: number;
AppDefinition: string;
};
export type PaymentPayload = {
Amount: number;
Channel: string;
};
export type Voucher = {
ChannelId: string;
Amount: number;
Signature: string;
};
type GetChannelRequest = {
Id: string;
};
type GetByLedgerRequest = {
LedgerId: string;
};
export type DefundObjectiveRequest = {
ChannelId: string;
};
export type ObjectiveResponse = {
Id: string;
ChannelId: string;
};
export type ReceiveVoucherResult = {
Total: bigint;
Delta: bigint;
};
/**
* RPC Requests
*/
export type GetAuthTokenRequest = JsonRpcRequest<"get_auth_token", Record<string, never>>;
export type GetAddressRequest = JsonRpcRequest<"get_address", Record<string, never>>;
export type DirectFundRequest = JsonRpcRequest<"create_ledger_channel", DirectFundPayload>;
export type PaymentRequest = JsonRpcRequest<"pay", PaymentPayload>;
export type VirtualFundRequest = JsonRpcRequest<"create_payment_channel", VirtualFundPayload>;
export type GetLedgerChannelRequest = JsonRpcRequest<"get_ledger_channel", GetChannelRequest>;
export type GetAllLedgerChannelsRequest = JsonRpcRequest<"get_all_ledger_channels", Record<string, never>>;
export type GetPaymentChannelRequest = JsonRpcRequest<"get_payment_channel", GetChannelRequest>;
export type GetPaymentChannelsByLedgerRequest = JsonRpcRequest<"get_payment_channels_by_ledger", GetByLedgerRequest>;
export type VersionRequest = JsonRpcRequest<"version", Record<string, never>>;
export type DirectDefundRequest = JsonRpcRequest<"close_ledger_channel", DefundObjectiveRequest>;
export type VirtualDefundRequest = JsonRpcRequest<"close_payment_channel", DefundObjectiveRequest>;
export type CreateVoucherRequest = JsonRpcRequest<"create_voucher", PaymentPayload>;
export type ReceiveVoucherRequest = JsonRpcRequest<"receive_voucher", Voucher>;
/**
* RPC Responses
*/
export type GetAuthTokenResponse = JsonRpcResponse<string>;
export type GetPaymentChannelResponse = JsonRpcResponse<PaymentChannelInfo>;
export type PaymentResponse = JsonRpcResponse<PaymentPayload>;
export type GetLedgerChannelResponse = JsonRpcResponse<LedgerChannelInfo>;
export type VirtualFundResponse = JsonRpcResponse<ObjectiveResponse>;
export type VersionResponse = JsonRpcResponse<string>;
export type GetAddressResponse = JsonRpcResponse<string>;
export type DirectFundResponse = JsonRpcResponse<ObjectiveResponse>;
export type DirectDefundResponse = JsonRpcResponse<string>;
export type VirtualDefundResponse = JsonRpcResponse<string>;
export type GetAllLedgerChannelsResponse = JsonRpcResponse<LedgerChannelInfo[]>;
export type GetPaymentChannelsByLedgerResponse = JsonRpcResponse<PaymentChannelInfo[]>;
export type CreateVoucherResponse = JsonRpcResponse<Voucher>;
export type ReceiveVoucherResponse = JsonRpcResponse<ReceiveVoucherResult>;
/**
* RPC Request/Response map
* This is a map of all the RPC methods to their request and response types
*/
export type RPCRequestAndResponses = {
get_auth_token: [GetAuthTokenRequest, GetAuthTokenResponse];
create_ledger_channel: [DirectFundRequest, DirectFundResponse];
close_ledger_channel: [DirectDefundRequest, DirectDefundResponse];
version: [VersionRequest, VersionResponse];
create_payment_channel: [VirtualFundRequest, VirtualFundResponse];
get_address: [GetAddressRequest, GetAddressResponse];
get_ledger_channel: [GetLedgerChannelRequest, GetLedgerChannelResponse];
get_payment_channel: [GetPaymentChannelRequest, GetPaymentChannelResponse];
pay: [PaymentRequest, PaymentResponse];
close_payment_channel: [VirtualDefundRequest, VirtualDefundResponse];
get_all_ledger_channels: [
GetAllLedgerChannelsRequest,
GetAllLedgerChannelsResponse
];
get_payment_channels_by_ledger: [
GetPaymentChannelsByLedgerRequest,
GetPaymentChannelsByLedgerResponse
];
create_voucher: [CreateVoucherRequest, CreateVoucherResponse];
receive_voucher: [ReceiveVoucherRequest, ReceiveVoucherResponse];
};
export type RequestMethod = keyof RPCRequestAndResponses;
export type RPCRequest = RPCRequestAndResponses[keyof RPCRequestAndResponses][0];
export type RPCResponse = RPCRequestAndResponses[keyof RPCRequestAndResponses][1];
/**
* RPC Notifications
*/
export type RPCNotification = ObjectiveCompleteNotification | PaymentChannelUpdatedNotification | LedgerChannelUpdatedNotification;
export type NotificationMethod = RPCNotification["method"];
export type NotificationParams = RPCNotification["params"];
export type PaymentChannelUpdatedNotification = JsonRpcNotification<"payment_channel_updated", PaymentChannelInfo>;
export type LedgerChannelUpdatedNotification = JsonRpcNotification<"ledger_channel_updated", LedgerChannelInfo>;
export type ObjectiveCompleteNotification = JsonRpcNotification<"objective_completed", string>;
/**
* Outcome related types
*/
export type LedgerChannelInfo = {
ID: string;
Status: ChannelStatus;
Balance: LedgerChannelBalance;
};
export type LedgerChannelBalance = {
AssetAddress: string;
Them: string;
Me: string;
TheirBalance: bigint;
MyBalance: bigint;
};
export type PaymentChannelBalance = {
AssetAddress: string;
Payee: string;
Payer: string;
PaidSoFar: bigint;
RemainingFunds: bigint;
};
export type PaymentChannelInfo = {
ID: string;
Status: ChannelStatus;
Balance: PaymentChannelBalance;
};
export type Outcome = SingleAssetOutcome[];
export type SingleAssetOutcome = {
Asset: string;
AssetMetadata: AssetMetadata;
Allocations: Allocation[];
};
export type Allocation = {
Destination: string;
Amount: number;
AllocationType: number;
Metadata: null;
};
export type AssetMetadata = {
AssetType: number;
Metadata: null;
};
export type ChannelStatus = "Proposed" | "Open" | "Closing" | "Complete";
export {};

1
dist/src/types.js vendored Normal file
View File

@ -0,0 +1 @@
export {};

34
dist/src/utils.d.ts vendored Normal file
View File

@ -0,0 +1,34 @@
import { NitroRpcClient } from "./rpc-client";
import { Outcome, RequestMethod, RPCRequestAndResponses } from "./types";
export declare const RPC_PATH = "api/v1";
/**
* createOutcome creates a basic outcome for a channel
*
* @param asset - The asset to fund the channel with
* @param alpha - The address of the first participant
* @param beta - The address of the second participant
* @param amount - The amount to allocate to each participant
* @returns An outcome for a directly funded channel with 100 wei allocated to each participant
*/
export declare function createOutcome(asset: string, alpha: string, beta: string, amount: number): Outcome;
/**
* Left pads a 20 byte address hex string with zeros until it is a 32 byte hex string
* e.g.,
* 0x9546E319878D2ca7a21b481F873681DF344E0Df8 becomes
* 0x0000000000000000000000009546E319878D2ca7a21b481F873681DF344E0Df8
*
* @param address - 20 byte hex string
* @returns 32 byte padded hex string
*/
export declare function convertAddressToBytes32(address: string): string;
/**
* generateRequest is a helper function that generates a request object for the given method and payloads
*
* @param method - The RPC method to generate a request for
* @param payload - The payloads to include in the request
* @returns A request object of the correct type
*/
export declare function generateRequest<K extends RequestMethod, T extends RPCRequestAndResponses[K][0]>(method: K, payload: T["params"]["payload"], authToken: string): T;
export declare function getLocalRPCUrl(port: number): string;
export declare function logOutChannelUpdates(rpcClient: NitroRpcClient): Promise<void>;
export declare function compactJson(obj: unknown): string;

82
dist/src/utils.js vendored Normal file
View File

@ -0,0 +1,82 @@
export const RPC_PATH = "api/v1";
/**
* createOutcome creates a basic outcome for a channel
*
* @param asset - The asset to fund the channel with
* @param alpha - The address of the first participant
* @param beta - The address of the second participant
* @param amount - The amount to allocate to each participant
* @returns An outcome for a directly funded channel with 100 wei allocated to each participant
*/
export function createOutcome(asset, alpha, beta, amount) {
return [
{
Asset: asset,
AssetMetadata: {
AssetType: 0,
Metadata: null,
},
Allocations: [
{
Destination: convertAddressToBytes32(alpha),
Amount: amount,
AllocationType: 0,
Metadata: null,
},
{
Destination: convertAddressToBytes32(beta),
Amount: amount,
AllocationType: 0,
Metadata: null,
},
],
},
];
}
/**
* Left pads a 20 byte address hex string with zeros until it is a 32 byte hex string
* e.g.,
* 0x9546E319878D2ca7a21b481F873681DF344E0Df8 becomes
* 0x0000000000000000000000009546E319878D2ca7a21b481F873681DF344E0Df8
*
* @param address - 20 byte hex string
* @returns 32 byte padded hex string
*/
export function convertAddressToBytes32(address) {
const digits = address.startsWith("0x") ? address.substring(2) : address;
return `0x${digits.padStart(24, "0")}`;
}
/**
* generateRequest is a helper function that generates a request object for the given method and payloads
*
* @param method - The RPC method to generate a request for
* @param payload - The payloads to include in the request
* @returns A request object of the correct type
*/
export function generateRequest(method, payload, authToken) {
return {
jsonrpc: "2.0",
method,
params: { authtoken: authToken, payload: payload },
// Our schema defines id as a uint32. We mod the current time to ensure that we don't overflow
id: Date.now() % 1_000_000_000,
}; // TODO: We shouldn't have to cast here
}
export function getLocalRPCUrl(port) {
return `127.0.0.1:${port}/${RPC_PATH}`;
}
export async function logOutChannelUpdates(rpcClient) {
const shortAddress = (await rpcClient.GetAddress()).slice(0, 8);
rpcClient.Notifications.on("ledger_channel_updated", (info) => {
console.log(`${shortAddress}: Ledger channel update\n${prettyJson(info)}`);
});
rpcClient.Notifications.on("payment_channel_updated", (info) => {
console.log(`${shortAddress}: Payment channel update\n${prettyJson(info)}`);
});
}
function prettyJson(obj) {
return JSON.stringify(obj, null, 2);
}
export function compactJson(obj) {
return JSON.stringify(obj, null, 0);
}

1
dist/tsconfig.tsbuildinfo vendored Normal file

File diff suppressed because one or more lines are too long

4576
yarn.lock Normal file

File diff suppressed because it is too large Load Diff