172 lines
4.8 KiB
TypeScript
172 lines
4.8 KiB
TypeScript
#!/usr/bin/env bun
|
||
|
||
// Example CLI for interacting with PortalBtcWallet
|
||
// Usage examples:
|
||
// bun run examples/portalbtc-cli.ts balance
|
||
// bun run examples/portalbtc-cli.ts invoice 1000
|
||
// bun run examples/portalbtc-cli.ts pay <destination> [amountSat]
|
||
// bun run examples/portalbtc-cli.ts redeem-token <cashu-token>
|
||
// bun run examples/portalbtc-cli.ts list [limit] [offset]
|
||
//
|
||
// Environment variables required:
|
||
// MNEMONIC – BIP39 seed words for the wallet (string, required)
|
||
// BREEZ_API_KEY – Breez SDK API key (string, required)
|
||
// NETWORK – "mainnet" | "testnet" (default: "testnet")
|
||
|
||
import PortalBtcWallet from "../index";
|
||
import { PaymentStatus } from "../paymentStatus";
|
||
import type { CashuStore, CashuTxn } from "../paymentStatus";
|
||
import type { Proof } from "@cashu/cashu-ts";
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
|
||
interface CashuPersistenceFile {
|
||
proofs: Proof[];
|
||
txns: CashuTxn[];
|
||
lastRedeemedCashuQuoteTimestamp: number;
|
||
}
|
||
|
||
class FileCashuStore implements CashuStore {
|
||
private data: CashuPersistenceFile;
|
||
|
||
constructor(private filePath: string) {
|
||
if (fs.existsSync(filePath)) {
|
||
this.data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||
} else {
|
||
this.data = {
|
||
proofs: [],
|
||
txns: [],
|
||
lastRedeemedCashuQuoteTimestamp: 0,
|
||
};
|
||
this.flush();
|
||
}
|
||
}
|
||
|
||
private flush() {
|
||
fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
||
}
|
||
|
||
async getProofs(): Promise<Proof[]> {
|
||
return this.data.proofs;
|
||
}
|
||
|
||
async getTxns(): Promise<CashuTxn[]> {
|
||
return this.data.txns;
|
||
}
|
||
|
||
async getLastRedeemedCashuQuoteTimestamp(): Promise<number> {
|
||
return this.data.lastRedeemedCashuQuoteTimestamp;
|
||
}
|
||
|
||
async persistProofs(proofs: Proof[]): Promise<void> {
|
||
this.data.proofs = proofs;
|
||
this.flush();
|
||
}
|
||
|
||
async persistTxns(txns: CashuTxn[]): Promise<void> {
|
||
this.data.txns = txns;
|
||
this.flush();
|
||
}
|
||
|
||
async persistLastRedeemedCashuQuoteTimestamp(
|
||
timestamp: number,
|
||
): Promise<void> {
|
||
this.data.lastRedeemedCashuQuoteTimestamp = timestamp;
|
||
this.flush();
|
||
}
|
||
}
|
||
|
||
function usage() {
|
||
console.log(`Usage:
|
||
balance – Show wallet balances (on-chain + Cashu)
|
||
invoice <amountSat> – Generate a Lightning invoice
|
||
pay <destination> [amountSat] – Pay a destination (invoice / LNURL / address)
|
||
redeem-token <cashu-token> – Redeem a Cashu token
|
||
list [limit] [offset] – List recent payments (default limit 20)
|
||
`);
|
||
process.exit(1);
|
||
}
|
||
|
||
async function main() {
|
||
const [cmd, ...rest] = process.argv.slice(2);
|
||
if (!cmd) usage();
|
||
|
||
const mnemonic = process.env.MNEMONIC;
|
||
const network = (process.env.NETWORK ?? "testnet") as "mainnet" | "testnet";
|
||
|
||
if (!mnemonic) {
|
||
console.error(
|
||
"Error: MNEMONIC environment variable is required.",
|
||
);
|
||
process.exit(1);
|
||
}
|
||
|
||
const statePath = path.resolve(
|
||
path.dirname(new URL(import.meta.url).pathname),
|
||
".portalbtc_state.json",
|
||
);
|
||
const cashuStore = new FileCashuStore(statePath);
|
||
|
||
const wallet = await PortalBtcWallet.create(
|
||
mnemonic,
|
||
cashuStore,
|
||
false,
|
||
network,
|
||
);
|
||
|
||
switch (cmd) {
|
||
case "balance": {
|
||
console.log("Balance (sats):", wallet.balance);
|
||
console.log(" – Cashu balance:", wallet.cashuBalance);
|
||
console.log(" – Pending balance:", wallet.pendingBalance);
|
||
console.log(" – Lightning address:", wallet.lightningAddress);
|
||
break;
|
||
}
|
||
case "invoice": {
|
||
const amountSat = Number.parseInt(rest[0] ?? "0", 10);
|
||
if (!amountSat) {
|
||
console.error("Amount (sat) required for invoice command");
|
||
process.exit(1);
|
||
}
|
||
const invoice = await wallet.generateBolt11Invoice(amountSat);
|
||
console.log("Generated invoice:", invoice);
|
||
break;
|
||
}
|
||
case "pay": {
|
||
const destination = rest[0];
|
||
const amountArg = rest[1] ? Number.parseInt(rest[1], 10) : 0;
|
||
if (!destination) {
|
||
console.error("Destination required for pay command");
|
||
process.exit(1);
|
||
}
|
||
for await (const status of wallet.pay(destination, amountArg)) {
|
||
console.log("→", PaymentStatus[status.status], status);
|
||
}
|
||
break;
|
||
}
|
||
case "redeem-token": {
|
||
const token = rest[0];
|
||
if (!token) {
|
||
console.error("Cashu token required for redeem-token command");
|
||
process.exit(1);
|
||
}
|
||
await wallet.redeemToken(token);
|
||
console.log("Token redeemed successfully");
|
||
break;
|
||
}
|
||
case "list": {
|
||
const limit = rest[0] ? Number.parseInt(rest[0], 10) : 20;
|
||
const offset = rest[1] ? Number.parseInt(rest[1], 10) : 0;
|
||
const payments = await wallet.listPayments(limit, offset);
|
||
console.log(payments);
|
||
break;
|
||
}
|
||
default:
|
||
usage();
|
||
}
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error(err);
|
||
process.exit(1);
|
||
});
|