Add Nostr ecash transaction history CLI

- CLI tool to export NIP-60 (Cashu Wallet) and NIP-61 (Nutzap) transaction history as CSV
- Fetches and decrypts kind:17375 wallet, kind:7375 token, kind:7376 history, kind:9321 nutzap, and kind:7377 redemption events
- Accepts NOSTR_SECRET_KEY env var and optional --mints flag
- Outputs CSV to stdout with columns: date,type,amount,unit,mint,token_id,memo
- Progress and errors go to stderr
This commit is contained in:
Jon Staab
2026-04-11 05:35:36 -07:00
parent 285a95dbbb
commit def3157f9b
9 changed files with 715 additions and 1 deletions
+86
View File
@@ -0,0 +1,86 @@
import { initNDK } from "./ndk.js";
import {
fetchWalletData,
fetchTokenEvents,
fetchHistoryEvents,
fetchNutzapEvents,
fetchNutzapRedemptions,
buildTransactionHistory,
} from "./events.js";
import { formatCSV } from "./csv.js";
function usage(): never {
console.error(`Usage: NOSTR_SECRET_KEY=<hex|nsec> bun run src/index.ts [--mints <url1,url2,...>]
Options:
--mints Comma-separated list of additional mint URLs to include
Environment:
NOSTR_SECRET_KEY Required. Nostr private key in hex or nsec format.`);
process.exit(1);
}
async function main() {
const secretKey = process.env.NOSTR_SECRET_KEY;
if (!secretKey) {
console.error("Error: NOSTR_SECRET_KEY environment variable is required");
usage();
}
// Parse --mints flag
const args = process.argv.slice(2);
const mintsIdx = args.indexOf("--mints");
const extraMints =
mintsIdx !== -1 && args[mintsIdx + 1]
? args[mintsIdx + 1].split(",").filter(Boolean)
: [];
// Initialize NDK
const { ndk, signer, user } = await initNDK(secretKey, extraMints);
// Fetch wallet data first (need mints for context)
const walletData = await fetchWalletData(ndk, user, signer);
const allMints = [...new Set([...walletData.mints, ...extraMints])];
if (allMints.length > 0) {
console.error(`Discovered mints: ${allMints.join(", ")}`);
}
// Fetch all events in parallel
const [tokens, history, nutzaps, redemptions] = await Promise.all([
fetchTokenEvents(ndk, user, signer),
fetchHistoryEvents(ndk, user, signer),
fetchNutzapEvents(ndk, user),
fetchNutzapRedemptions(ndk, user, signer),
]);
// Build unified transaction history
const records = buildTransactionHistory(
walletData,
tokens,
history,
nutzaps,
redemptions
);
console.error(`Total transactions: ${records.length}`);
// Output CSV to stdout
const csv = formatCSV(records);
process.stdout.write(csv + "\n");
// Clean shutdown
// NDK doesn't have a disconnect method that returns a promise reliably,
// so we just exit after output
process.exit(0);
}
// Handle SIGINT
process.on("SIGINT", () => {
console.error("\nInterrupted.");
process.exit(1);
});
main().catch((err) => {
console.error(`Error: ${err instanceof Error ? err.message : err}`);
process.exit(1);
});