Skip to content

Commit

Permalink
Merge pull request #137 from golemfactory/feature/JST-664/standalone-…
Browse files Browse the repository at this point in the history
…payment-module

JST-664: standalone payment module
  • Loading branch information
SewerynKras authored Jan 26, 2024
2 parents f594c7e + 10e2d96 commit 034b991
Show file tree
Hide file tree
Showing 9 changed files with 601 additions and 92 deletions.
313 changes: 225 additions & 88 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@
"node": ">=18.0.0"
},
"dependencies": {
"@golem-sdk/golem-js": "^1.0.1",
"@golem-sdk/golem-js": "^2.0.0-beta.16",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"chalk": "^4.1.2",
"commander": "^11.1.0",
"console-table-printer": "^2.12.0",
"decimal.js-light": "^2.5.1",
"enquirer": "^2.4.1",
"lodash": "^4.17.21",
"luxon": "^3.4.4",
Expand Down
3 changes: 2 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { version } from "./lib/version";
import { manifestCommand } from "./manifest/manifest.command";
import { newCommand } from "./new/new.command";
import { runOnGolemCommand } from "./run-on-golem/run-on-golem.command";
import { invoiceCommand } from "./payment/invoice.command";

const program = new Command("golem-sdk");
program.version(version);
Expand All @@ -12,6 +13,6 @@ program.version(version);
// chalk.level = 0;
// });

program.addCommand(manifestCommand).addCommand(newCommand).addCommand(runOnGolemCommand);
program.addCommand(manifestCommand).addCommand(newCommand).addCommand(runOnGolemCommand).addCommand(invoiceCommand);

program.parse();
69 changes: 69 additions & 0 deletions src/payment/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Command, InvalidArgumentError, Option } from "commander";
import { InvoiceSearchOptions } from "./invoice.options";
import { InvoiceProcessor } from "@golem-sdk/golem-js";

function parseIntOrThrow(value: string) {
const parsedValue = parseInt(value, 10);
if (isNaN(parsedValue) || parsedValue < 0) {
throw new InvalidArgumentError("Not a valid positive integer.");
}
return parsedValue;
}
function parseDateOrThrow(value: string) {
const parsedValue = new Date(value);
if (isNaN(parsedValue.getTime())) {
throw new InvalidArgumentError("Not a valid date.");
}
return parsedValue;
}

export function createInvoiceCommand(name: string): Command {
return new Command(name)
.addOption(new Option("-k, --yagna-appkey <key>", "Yagna app key").env("YAGNA_APPKEY").makeOptionMandatory())
.addOption(
new Option("--after <after>", "Search for invoices after this date")
.default(new Date(0))
.argParser(parseDateOrThrow),
)
.addOption(
new Option("--limit <limit>", "Limit the number of invoices returned by the search")
.default(50)
.argParser(parseIntOrThrow),
)
.addOption(new Option("--provider [provider...]", "Search by provider ID"))
.option(
"--columns [columns...]",
"Columns to display. Valid options are: id, status, amount, timestamp, platform, payer, issuer, providerId",
["id", "status", "amount", "timestamp", "providerId", "platform"],
)
.option("--wallet [wallet...]", "Search by wallet address")
.option("--min-amount <minAmount>", "Search by minimum invoice amount")
.option("--max-amount <maxAmount>", "Search by maximum invoice amount")
.option(
"--status [status...]",
"Search by invoice status. For example to search for invoices you received but did not accept yet, use `--status RECEIVED`. Valid options are: ISSUED, RECEIVED, ACCEPTED, REJECTED, FAILED, SETTLED, CANCELLED.",
["RECEIVED", "ACCEPTED", "SETTLED"],
)
.option("--payment-platform [paymentPlatform...]", "Search by payment platform")
.option(
"-i --invoice [invoice...]",
"Instead of searching, fetch specific invoices by ID. If this option is used, all other search options are ignored.",
)
.option("-f, --format <format>", "Output format: table, json, csv.", "table");
}

export async function fetchInvoices(options: InvoiceSearchOptions, processor: InvoiceProcessor) {
if (options.invoice && options.invoice.length > 0) {
return Promise.all(options.invoice.map(async (invoiceId) => processor.fetchSingleInvoice(invoiceId)));
}
return processor.collectInvoices({
limit: options.limit,
after: options.after,
statuses: options.status,
providerIds: options.provider,
providerWallets: options.wallet,
minAmount: options.minAmount,
maxAmount: options.maxAmount,
paymentPlatforms: options.paymentPlatform,
});
}
158 changes: 158 additions & 0 deletions src/payment/invoice-accept.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { prompt } from "enquirer";
import Decimal from "decimal.js-light";
import { Table } from "console-table-printer";
import { Invoice } from "ya-ts-client/dist/ya-payment";
import chalk from "chalk";
import { InvoiceAcceptOptions } from "./invoice.options";
import { InvoiceAcceptResult, InvoiceProcessor } from "@golem-sdk/golem-js";
import { fetchInvoices } from "./common";
import _ from "lodash";

async function askForConfirmation(invoices: Invoice[], columns: InvoiceAcceptOptions["columns"]) {
const invoicesToPay = [];

let i = 0;
for (const invoice of invoices) {
const allColumns = {
id: invoice.invoiceId,
accepted: invoice.status !== "RECEIVED" ? "accepted" : "not accepted",
status: invoice.status,
amount: invoice.amount,
timestamp: invoice.timestamp,
platform: invoice.paymentPlatform,
payer: invoice.payerAddr,
issuer: invoice.payeeAddr,
providerId: invoice.issuerId,
};
const selectedColumns = _.pick(allColumns, columns);

console.log(
`${++i}/${invoices.length}:\n` +
Object.entries(selectedColumns)
.map(([key, value]) => ` - ${chalk.bold(key)}:\t${value}`)
.join("\n"),
);
const { decision } = (await prompt({
type: "confirm",
name: "decision",
message: "Add this invoice to the list of invoices to accept?",
})) as { decision: boolean };
if (decision) {
invoicesToPay.push(invoice);
}
}
return invoicesToPay;
}

export async function acceptAction(options: InvoiceAcceptOptions) {
const paymentProcessor = await InvoiceProcessor.create({
apiKey: options.yagnaAppkey,
});
let invoices: Invoice[];
try {
invoices = await fetchInvoices(options, paymentProcessor);
} catch (e) {
console.error(e);
console.log(chalk.red("Failed to fetch invoices, check your parameters and try again."));
process.exitCode = 1;
return;
}

if (invoices.length === 0) {
if (!options.silent) {
console.log(chalk.blue.bold("No unaccepted invoices found"));
}
return;
}
const invoicesToPay = [];

if (options.yes) {
invoicesToPay.push(...invoices);
} else {
console.log(chalk.blue.bold(`Found ${invoices.length} unaccepted invoices:`));
try {
invoicesToPay.push(...(await askForConfirmation(invoices, options.columns)));

if (invoicesToPay.length === 0) {
console.log(chalk.blue.bold("No invoices selected"));
return;
}

const invoicesOnTestnet = invoicesToPay.filter((invoice) =>
invoice.paymentPlatform.toLowerCase().endsWith("-tglm"),
);
const invoicesOnMainnet = invoicesToPay.filter((invoice) =>
invoice.paymentPlatform.toLowerCase().endsWith("-glm"),
);
if (invoicesOnTestnet.length > 0) {
const totalTestGLM = invoicesOnTestnet.reduce((acc, invoice) => acc.add(invoice.amount), new Decimal(0));
console.log(
chalk.blue.bold(
`Selected ${invoicesOnTestnet.length} invoices on Testnet for a total of ${totalTestGLM} tGLM`,
),
);
}
if (invoicesOnMainnet.length > 0) {
const totalRealGLM = invoicesOnMainnet.reduce((acc, invoice) => acc.add(invoice.amount), new Decimal(0));
console.log(
chalk.blue.bold(
`Selected ${invoicesOnMainnet.length} invoices on Mainnet for a total of ${totalRealGLM} GLM`,
),
);
}
const { decision } = (await prompt({
type: "confirm",
name: "decision",
message:
invoicesToPay.length === 1
? "Do you want to accept this invoice?"
: `Do you want to accept these ${invoicesToPay.length} invoices?`,
})) as { decision: boolean };
if (!decision) {
return;
}
} catch {
process.exitCode = 1;
console.log(chalk.red("Script cancelled"));
return;
}
}

const paymentResults = await paymentProcessor.acceptManyInvoices({
invoices: invoicesToPay,
dryRun: options.dryRun,
});

if (options.silent) {
return;
}

const getRow = (result: InvoiceAcceptResult) => {
return {
invoiceId: result.invoiceId,
status: (result.success ? "success" : "failed") + (result.dryRun ? " (dry run)" : ""),
amount: result.amount,
platform: result.allocation.paymentPlatform,
};
};

if (options.format === "table") {
const summaryTable = new Table();
paymentResults.forEach((result) =>
summaryTable.addRow(getRow(result), {
color: result.dryRun ? "yellow" : result.success ? "green" : "red",
}),
);
summaryTable.printTable();
}

if (options.format === "json") {
console.log(JSON.stringify(paymentResults.map(getRow)));
}

if (options.format === "csv") {
const rows = paymentResults.map(getRow);
console.log(Object.keys(rows[0]).join(","));
console.log(rows.map((row) => Object.values(row).join(",")).join("\n"));
}
}
69 changes: 69 additions & 0 deletions src/payment/invoice-search.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { InvoiceProcessor } from "@golem-sdk/golem-js";
import { Invoice } from "ya-ts-client/dist/ya-payment";
import { Table } from "console-table-printer";
import { InvoiceSearchOptions } from "./invoice.options";
import _ from "lodash";
import chalk from "chalk";
import { fetchInvoices } from "./common";

function printRows(invoices: Invoice[], options: InvoiceSearchOptions) {
const getRow = (invoice: Invoice) => {
const allColumns = {
id: invoice.invoiceId,
status: invoice.status,
amount: invoice.amount,
timestamp: invoice.timestamp,
platform: invoice.paymentPlatform,
payer: invoice.payerAddr,
issuer: invoice.payeeAddr,
providerId: invoice.issuerId,
};
return _.pick(allColumns, options.columns);
};

if (options.format === "table") {
if (invoices.length === 0) {
console.log(chalk.red("No invoices found"));
return;
}
const table = new Table();
invoices.forEach((invoice) => {
const isHighlighted = invoice.status === "RECEIVED" || invoice.status === "ISSUED";
const row = getRow(invoice);
table.addRow(row, {
color: isHighlighted ? "red" : "green",
});
});

table.printTable();
return;
}
if (options.format === "json") {
console.log(JSON.stringify(invoices.map(getRow)));
return;
}
if (options.format === "csv") {
console.log(options.columns);
console.log(
invoices
.map(getRow)
.map((row) => Object.values(row).join(","))
.join("\n"),
);
return;
}
}

export async function searchAction(options: InvoiceSearchOptions) {
const paymentProcessor = await InvoiceProcessor.create({
apiKey: options.yagnaAppkey,
});
let invoices: Invoice[];
try {
invoices = await fetchInvoices(options, paymentProcessor);
} catch {
console.log(chalk.red("Failed to fetch invoices, check your parameters and try again"));
return;
}
printRows(invoices, options);
}
52 changes: 52 additions & 0 deletions src/payment/invoice.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Command, Option } from "commander";
import { InvoiceAcceptOptions, InvoiceSearchOptions } from "./invoice.options";
import { createInvoiceCommand } from "./common";

export const invoiceCommand = new Command("invoice").summary("Search and accept invoices.").addHelpText(
"after",
`
Examples:
Search for the first 10 invoices after 2023-01-01:
$ golem-sdk invoice search -k yagna-appkey --after 2023-01-01 --limit 10
Search for invoices issued by provider 0x1234 with status RECEIVED and print them in JSON format:
$ golem-sdk invoice search -k yagna-appkey --provider 0x1234 --status RECEIVED --format json
Search for invoices above 0.5 GLM on payment platform erc20-polygon-glm:
$ golem-sdk invoice search -k yagna-appkey --min-amount 0.5 --payment-platform erc20-polygon-glm
Search for invoices by their ID and only list their id, timestamp and payment platform:
$ golem-sdk invoice search -k yagna-appkey --invoice 0x1234 0x5678 --columns id timestamp platform
Accept all invoices from provider 0x1234 (interactive):
$ golem-sdk invoice accept -k yagna-appkey --provider 0x1234
Accept all invoices from provider 0x1234 (auto-accept):
$ golem-sdk invoice accept -k yagna-appkey --provider 0x1234 --yes
Accept all invoices from provider 0x1234 (dry run):
$ golem-sdk invoice accept -k yagna-appkey --provider 0x1234 --dry-run
`,
);

const searchCommand = createInvoiceCommand("search")
.summary("Search for invoices.")
.allowUnknownOption(false)
.action(async (options: InvoiceSearchOptions) => {
const action = await import("./invoice-search.action.js");
await action.default.searchAction(options);
});

const payCommand = createInvoiceCommand("accept")
.summary("Accept invoices. This command is interactive by default and takes the same options as search.")
.addOption(new Option("-y --yes", "Skip confirmation").default(false))
.addOption(new Option("--dry-run", "Dry run").default(false))
.addOption(new Option("-s, --silent", "Don't print anything to stdout").default(false))
.allowUnknownOption(false)
.action(async (options: InvoiceAcceptOptions) => {
const action = await import("./invoice-accept.action.js");
await action.default.acceptAction(options);
});

invoiceCommand.addCommand(searchCommand).addCommand(payCommand);
Loading

0 comments on commit 034b991

Please sign in to comment.