@pod-protocol/cli
Version:
Command-line interface for PoD Protocol (Prompt or Die) AI Agent Communication Protocol
270 lines (269 loc) • 11.2 kB
JavaScript
import inquirer from "inquirer";
import { table } from "table";
import { PublicKey } from "@solana/web3.js";
import { lamportsToSol, solToLamports } from "@pod-protocol/sdk";
import { createCommandHandler, handleDryRun, createSpinner, showSuccess, getTableConfig, formatValue, } from "../utils/shared.js";
import { validatePublicKey, validateSolAmount, validatePositiveInteger, ValidationError, } from "../utils/validation.js";
// Helper for interactive channel/amount prompts
async function promptChannelAndAmount({ interactive, channel, amount, lamports, all, withdraw = false, }) {
let channelId = channel;
let amt = 0;
let withdrawAll = all;
if (interactive) {
const questions = [
{
type: "input",
name: "channelId",
message: "Channel ID:",
validate: (input) => {
try {
new PublicKey(input);
return true;
}
catch {
return "Please enter a valid channel ID";
}
},
},
];
if (withdraw) {
questions.push({
type: "confirm",
name: "withdrawAll",
message: "Withdraw all available funds?",
default: false,
});
}
questions.push({
type: "list",
name: "unit",
message: "Amount unit:",
choices: [
{ name: "SOL", value: "sol" },
{ name: "Lamports", value: "lamports" },
],
when: (answers) => !withdraw || !answers.withdrawAll,
});
questions.push({
type: "number",
name: "amount",
message: "Amount:",
validate: (input) => (input > 0 ? true : "Amount must be greater than 0"),
when: (answers) => !withdraw || !answers.withdrawAll,
});
const answers = await inquirer.prompt(questions);
channelId = answers.channelId;
withdrawAll = withdraw ? answers.withdrawAll : false;
amt =
withdraw && answers.withdrawAll
? 0
: answers.unit === "sol"
? solToLamports(answers.amount)
: answers.amount;
}
else {
if (!channelId) {
throw new ValidationError("Channel ID is required");
}
if (withdraw && !withdrawAll) {
validateAmountOptions(amount, lamports, withdraw);
amt = getAmountFromOptions(amount, lamports);
}
else if (!withdraw) {
validateAmountOptions(amount, lamports, withdraw);
amt = getAmountFromOptions(amount, lamports);
}
}
return { channelId, amount: amt, withdrawAll };
}
function validateAmountOptions(amount, lamports, withdraw = false) {
if (amount && lamports) {
throw new ValidationError("Specify either --amount (SOL) or --lamports, not both");
}
if (!withdraw && !amount && !lamports) {
throw new ValidationError("Amount is required (use --amount for SOL or --lamports)");
}
if (withdraw && !amount && !lamports) {
throw new ValidationError("Amount is required (use --amount for SOL, --lamports, or --all)");
}
}
function getAmountFromOptions(amount, lamports) {
if (amount) {
const solAmount = validateSolAmount(amount, "SOL amount");
return solToLamports(solAmount);
}
else if (lamports) {
return validatePositiveInteger(lamports, "lamports amount");
}
return 0;
}
export class EscrowCommands {
register(program) {
const escrow = program
.command("escrow")
.description("Manage escrow accounts for channel fees");
// Deposit to escrow
escrow
.command("deposit")
.description("Deposit SOL to channel escrow for fees")
.option("-c, --channel <channelId>", "Channel ID")
.option("-a, --amount <sol>", "Amount in SOL to deposit")
.option("-l, --lamports <lamports>", "Amount in lamports to deposit")
.option("-i, --interactive", "Interactive deposit")
.action(createCommandHandler("deposit to escrow", async (client, wallet, globalOpts, options) => {
await this.handleDeposit(client, wallet, globalOpts, options);
}));
// Withdraw from escrow
escrow
.command("withdraw")
.description("Withdraw SOL from channel escrow")
.option("-c, --channel <channelId>", "Channel ID")
.option("-a, --amount <sol>", "Amount in SOL to withdraw")
.option("-l, --lamports <lamports>", "Amount in lamports to withdraw")
.option("--all", "Withdraw all available funds")
.option("-i, --interactive", "Interactive withdrawal")
.action(createCommandHandler("withdraw from escrow", async (client, wallet, globalOpts, options) => {
await this.handleWithdraw(client, wallet, globalOpts, options);
}));
// Show escrow balance
escrow
.command("balance")
.description("Show escrow balance for a channel")
.option("-c, --channel <channelId>", "Channel ID")
.option("-a, --agent [address]", "Agent address (defaults to current wallet)")
.action(createCommandHandler("fetch escrow balance", async (client, wallet, globalOpts, options) => {
await this.handleBalance(client, wallet, options);
}));
// List all escrow accounts
escrow
.command("list")
.description("List all escrow accounts for current wallet")
.option("-l, --limit <number>", "Maximum number of escrows to show", "10")
.action(createCommandHandler("list escrow accounts", async (client, wallet, globalOpts, options) => {
await this.handleList(client, wallet, options);
}));
}
async handleDeposit(client, wallet, globalOpts, options) {
const { channelId, amount } = await promptChannelAndAmount({
interactive: options.interactive,
channel: options.channel,
amount: options.amount,
lamports: options.lamports,
all: options.all,
});
const channelKey = validatePublicKey(channelId, "channel ID");
const spinner = createSpinner("Depositing to escrow...");
if (handleDryRun(globalOpts, spinner, "Escrow deposit", {
Channel: channelId,
Amount: `${lamportsToSol(amount)} SOL (${amount} lamports)`,
})) {
return;
}
const signature = await client.depositEscrow(wallet, {
channel: channelKey,
amount,
});
showSuccess(spinner, "Escrow deposit successful!", {
Transaction: signature,
Channel: channelId,
Deposited: `${lamportsToSol(amount)} SOL`,
});
}
async handleWithdraw(client, wallet, globalOpts, options) {
const { channelId, amount: promptAmount, withdrawAll, } = await promptChannelAndAmount({
interactive: options.interactive,
channel: options.channel,
amount: options.amount,
lamports: options.lamports,
all: options.all,
withdraw: true,
});
const channelKey = validatePublicKey(channelId, "channel ID");
const spinner = createSpinner("Withdrawing from escrow...");
// If withdrawing all, get current balance first
let amount = promptAmount;
if (withdrawAll) {
const escrowData = await client.getEscrow(channelKey, wallet.publicKey);
if (!escrowData) {
spinner.fail("No escrow account found for this channel");
return;
}
amount = escrowData.balance;
}
if (handleDryRun(globalOpts, spinner, "Escrow withdrawal", {
Channel: channelId,
Amount: withdrawAll
? "All funds"
: `${lamportsToSol(amount)} SOL (${amount} lamports)`,
})) {
return;
}
const signature = await client.withdrawEscrow(wallet, {
channel: channelKey,
amount,
});
showSuccess(spinner, "Escrow withdrawal successful!", {
Transaction: signature,
Channel: channelId,
Withdrawn: `${lamportsToSol(amount)} SOL`,
});
}
async handleBalance(client, wallet, options) {
if (!options.channel) {
throw new ValidationError("Channel ID is required");
}
const channelKey = validatePublicKey(options.channel, "channel ID");
const spinner = createSpinner("Fetching escrow balance...");
let agentAddress;
if (options.agent) {
agentAddress = validatePublicKey(options.agent, "agent address");
}
else {
agentAddress = wallet.publicKey;
}
const escrowData = await client.getEscrow(channelKey, agentAddress);
if (!escrowData) {
spinner.succeed("No escrow account found");
return;
}
spinner.succeed("Escrow balance retrieved");
const data = [
["Channel", formatValue(escrowData.channel.toBase58(), "address")],
["Depositor", formatValue(escrowData.depositor.toBase58(), "address")],
[
"Balance",
formatValue(`${lamportsToSol(escrowData.balance)} SOL`, "number"),
],
[
"Balance (Lamports)",
formatValue(escrowData.balance.toString(), "number"),
],
[
"Last Updated",
formatValue(new Date(escrowData.lastUpdated * 1000).toLocaleString(), "text"),
],
];
console.log("\n" + table(data, getTableConfig("Escrow Balance")));
}
async handleList(client, wallet, options) {
const limit = validatePositiveInteger(options.limit, "limit");
const spinner = createSpinner("Fetching escrow accounts...");
const escrows = await client.getEscrowsByDepositor(wallet.publicKey, limit);
if (escrows.length === 0) {
spinner.succeed("No escrow accounts found");
return;
}
spinner.succeed(`Found ${escrows.length} escrow accounts`);
const data = escrows.map((escrow) => [
formatValue(escrow.channel.toBase58().slice(0, 8) + "...", "address"),
formatValue(`${lamportsToSol(escrow.balance)} SOL`, "number"),
formatValue(escrow.balance.toString(), "number"),
formatValue(new Date(escrow.lastUpdated * 1000).toLocaleDateString(), "text"),
]);
console.log("\n" +
table([
["Channel", "Balance (SOL)", "Balance (Lamports)", "Last Updated"],
...data,
], getTableConfig("Escrow Accounts")));
}
}