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:
+3
-1
@@ -1 +1,3 @@
|
||||
.claude
|
||||
node_modules/
|
||||
dist/
|
||||
bun.lockb
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "nostr-nut-history",
|
||||
"dependencies": {
|
||||
"@nostr-dev-kit/ndk": "latest",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"typescript": "latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@codesandbox/nodebox": ["@codesandbox/nodebox@0.1.8", "", { "dependencies": { "outvariant": "^1.4.0", "strict-event-emitter": "^0.4.3" } }, "sha512-2VRS6JDSk+M+pg56GA6CryyUSGPjBEe8Pnae0QL3jJF1mJZJVMDKr93gJRtBbLkfZN6LD/DwMtf+2L0bpWrjqg=="],
|
||||
|
||||
"@codesandbox/sandpack-client": ["@codesandbox/sandpack-client@2.19.8", "", { "dependencies": { "@codesandbox/nodebox": "0.1.8", "buffer": "^6.0.3", "dequal": "^2.0.2", "mime-db": "^1.52.0", "outvariant": "1.4.0", "static-browser-server": "1.0.3" } }, "sha512-CMV4nr1zgKzVpx4I3FYvGRM5YT0VaQhALMW9vy4wZRhEyWAtJITQIqZzrTGWqB1JvV7V72dVEUCUPLfYz5hgJQ=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
|
||||
|
||||
"@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="],
|
||||
|
||||
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||
|
||||
"@noble/secp256k1": ["@noble/secp256k1@2.3.0", "", {}, "sha512-0TQed2gcBbIrh7Ccyw+y/uZQvbJwm7Ao4scBUxqpBCcsOlZG0O4KGfjtNAy/li4W8n1xt3dxrwJ0beZ2h2G6Kw=="],
|
||||
|
||||
"@nostr-dev-kit/ndk": ["@nostr-dev-kit/ndk@3.0.3", "", { "dependencies": { "@codesandbox/sandpack-client": "^2.19.8", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@noble/secp256k1": "^2.1.0", "@scure/base": "^1.1.9", "debug": "^4.3.7", "light-bolt11-decoder": "^3.2.0", "shiki": "^3.13.0", "tseep": "^1.3.1", "typescript-lru-cache": "^2.0.0" }, "peerDependencies": { "nostr-tools": "^2.17.2" } }, "sha512-wGQOnvpKjhkq/j0pnRGTiHyrujww7N/iJQKOBrjsIxqTvcy6iyqh/3TijTKlaapdvexx2E2xCn9X+q0dxxISug=="],
|
||||
|
||||
"@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="],
|
||||
|
||||
"@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
|
||||
|
||||
"@scure/bip32": ["@scure/bip32@2.0.1", "", { "dependencies": { "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA=="],
|
||||
|
||||
"@scure/bip39": ["@scure/bip39@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
|
||||
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
|
||||
|
||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="],
|
||||
|
||||
"@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="],
|
||||
|
||||
"@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
|
||||
|
||||
"@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
|
||||
|
||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
|
||||
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||
|
||||
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
||||
|
||||
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||
|
||||
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"light-bolt11-decoder": ["light-bolt11-decoder@3.2.0", "", { "dependencies": { "@scure/base": "1.1.1" } }, "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ=="],
|
||||
|
||||
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||
|
||||
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||
|
||||
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||
|
||||
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||
|
||||
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nostr-tools": ["nostr-tools@2.23.3", "", { "dependencies": { "@noble/ciphers": "2.1.1", "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", "@scure/base": "2.0.0", "@scure/bip32": "2.0.1", "@scure/bip39": "2.0.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-AALyt9k8xPdF4UV2mlLJ2mgCn4kpTB0DZ8t2r6wjdUh6anfx2cTVBsHUlo9U0EY/cKC5wcNyiMAmRJV5OVEalA=="],
|
||||
|
||||
"nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="],
|
||||
|
||||
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
|
||||
|
||||
"oniguruma-to-es": ["oniguruma-to-es@4.3.5", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ=="],
|
||||
|
||||
"outvariant": ["outvariant@1.4.0", "", {}, "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw=="],
|
||||
|
||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
|
||||
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
||||
|
||||
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
||||
|
||||
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
||||
|
||||
"shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"static-browser-server": ["static-browser-server@1.0.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.1.0", "dotenv": "^16.0.3", "mime-db": "^1.52.0", "outvariant": "^1.3.0" } }, "sha512-ZUyfgGDdFRbZGGJQ1YhiM930Yczz5VlbJObrQLlk24+qNHVQx4OlLcYswEUo3bIyNAbQUIUR9Yr5/Hqjzqb4zA=="],
|
||||
|
||||
"strict-event-emitter": ["strict-event-emitter@0.4.6", "", {}, "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
"tseep": ["tseep@1.3.1", "", {}, "sha512-ZPtfk1tQnZVyr7BPtbJ93qaAh2lZuIOpTMjhrYa4XctT8xe7t4SAW9LIxrySDuYMsfNNayE51E/WNGrNVgVicQ=="],
|
||||
|
||||
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
||||
|
||||
"typescript-lru-cache": ["typescript-lru-cache@2.0.0", "", {}, "sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA=="],
|
||||
|
||||
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
||||
|
||||
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||
|
||||
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
|
||||
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@scure/bip32/@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="],
|
||||
|
||||
"@scure/bip32/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||
|
||||
"@scure/bip32/@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="],
|
||||
|
||||
"@scure/bip39/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||
|
||||
"@scure/bip39/@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="],
|
||||
|
||||
"light-bolt11-decoder/@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
|
||||
|
||||
"nostr-tools/@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="],
|
||||
|
||||
"nostr-tools/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||
|
||||
"nostr-tools/@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "nostr-nut-history",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "bun run src/index.ts",
|
||||
"type-check": "bunx tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nostr-dev-kit/ndk": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"typescript": "latest"
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import type { TransactionRecord } from "./types.js";
|
||||
|
||||
function escapeField(value: string): string {
|
||||
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function formatCSV(records: TransactionRecord[]): string {
|
||||
const header = "date,type,amount,unit,mint,token_id,memo";
|
||||
const sorted = [...records].sort(
|
||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||
);
|
||||
|
||||
const rows = sorted.map((r) =>
|
||||
[
|
||||
escapeField(r.date),
|
||||
escapeField(r.type),
|
||||
String(r.amount),
|
||||
escapeField(r.unit),
|
||||
escapeField(r.mint),
|
||||
escapeField(r.token_id),
|
||||
escapeField(r.memo),
|
||||
].join(",")
|
||||
);
|
||||
|
||||
return [header, ...rows].join("\n");
|
||||
}
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
import type NDK from "@nostr-dev-kit/ndk";
|
||||
import {
|
||||
NDKKind,
|
||||
type NDKUser,
|
||||
type NDKSigner,
|
||||
type NDKEvent,
|
||||
} from "@nostr-dev-kit/ndk";
|
||||
import type {
|
||||
WalletData,
|
||||
TokenData,
|
||||
HistoryEntry,
|
||||
TransactionRecord,
|
||||
} from "./types.js";
|
||||
|
||||
async function decryptEvent(
|
||||
event: NDKEvent,
|
||||
user: NDKUser,
|
||||
signer: NDKSigner
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
await event.decrypt(user, signer, "nip44");
|
||||
return event.content;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Warning: failed to decrypt event ${event.id}: ${err instanceof Error ? err.message : err}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchWalletData(
|
||||
ndk: NDK,
|
||||
user: NDKUser,
|
||||
signer: NDKSigner
|
||||
): Promise<WalletData> {
|
||||
console.error("Fetching wallet events (kind:17375)...");
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [NDKKind.CashuWallet as number],
|
||||
authors: [user.pubkey],
|
||||
});
|
||||
|
||||
const mints: string[] = [];
|
||||
let privkey = "";
|
||||
|
||||
for (const event of events) {
|
||||
const content = await decryptEvent(event, user, signer);
|
||||
if (!content) continue;
|
||||
|
||||
try {
|
||||
const tuples: string[][] = JSON.parse(content);
|
||||
for (const tuple of tuples) {
|
||||
if (tuple[0] === "mint" && tuple[1]) {
|
||||
mints.push(tuple[1]);
|
||||
}
|
||||
if (tuple[0] === "privkey" && tuple[1]) {
|
||||
privkey = tuple[1];
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Warning: failed to parse wallet event ${event.id}: ${err instanceof Error ? err.message : err}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`Found ${events.size} wallet event(s), ${mints.length} mint(s)`);
|
||||
return { mints: [...new Set(mints)], privkey };
|
||||
}
|
||||
|
||||
export async function fetchTokenEvents(
|
||||
ndk: NDK,
|
||||
user: NDKUser,
|
||||
signer: NDKSigner
|
||||
): Promise<TokenData[]> {
|
||||
console.error("Fetching token events (kind:7375)...");
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [NDKKind.CashuToken as number],
|
||||
authors: [user.pubkey],
|
||||
});
|
||||
|
||||
const tokens: TokenData[] = [];
|
||||
|
||||
for (const event of events) {
|
||||
const content = await decryptEvent(event, user, signer);
|
||||
if (!content) continue;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(content);
|
||||
tokens.push({
|
||||
mint: data.mint ?? "",
|
||||
unit: data.unit ?? "sat",
|
||||
proofs: data.proofs ?? [],
|
||||
del: data.del,
|
||||
eventId: event.id,
|
||||
createdAt: event.created_at ?? 0,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Warning: failed to parse token event ${event.id}: ${err instanceof Error ? err.message : err}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`Found ${tokens.length} token event(s)`);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export async function fetchHistoryEvents(
|
||||
ndk: NDK,
|
||||
user: NDKUser,
|
||||
signer: NDKSigner
|
||||
): Promise<HistoryEntry[]> {
|
||||
console.error("Fetching history events (kind:7376)...");
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [NDKKind.CashuWalletTx as number],
|
||||
authors: [user.pubkey],
|
||||
});
|
||||
|
||||
const entries: HistoryEntry[] = [];
|
||||
|
||||
for (const event of events) {
|
||||
const content = await decryptEvent(event, user, signer);
|
||||
if (!content) continue;
|
||||
|
||||
try {
|
||||
const tuples: string[][] = JSON.parse(content);
|
||||
const entry: HistoryEntry = {
|
||||
direction: "in",
|
||||
amount: "0",
|
||||
unit: "sat",
|
||||
eventId: event.id,
|
||||
createdAt: event.created_at ?? 0,
|
||||
referencedEvents: [],
|
||||
};
|
||||
|
||||
for (const tuple of tuples) {
|
||||
if (tuple[0] === "direction") entry.direction = tuple[1] as "in" | "out";
|
||||
if (tuple[0] === "amount") entry.amount = tuple[1];
|
||||
if (tuple[0] === "unit") entry.unit = tuple[1];
|
||||
}
|
||||
|
||||
// Extract e-tag references
|
||||
const eTags = event.getMatchingTags("e");
|
||||
entry.referencedEvents = eTags.map((t) => t[1]);
|
||||
|
||||
entries.push(entry);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Warning: failed to parse history event ${event.id}: ${err instanceof Error ? err.message : err}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`Found ${entries.length} history event(s)`);
|
||||
return entries;
|
||||
}
|
||||
|
||||
export async function fetchNutzapEvents(
|
||||
ndk: NDK,
|
||||
user: NDKUser
|
||||
): Promise<TransactionRecord[]> {
|
||||
console.error("Fetching nutzap events (kind:9321)...");
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [NDKKind.Nutzap as number],
|
||||
"#p": [user.pubkey],
|
||||
});
|
||||
|
||||
const records: TransactionRecord[] = [];
|
||||
|
||||
for (const event of events) {
|
||||
try {
|
||||
const proofTags = event.getMatchingTags("proof");
|
||||
let totalAmount = 0;
|
||||
for (const tag of proofTags) {
|
||||
try {
|
||||
const proof = JSON.parse(tag[1]);
|
||||
totalAmount += proof.amount ?? 0;
|
||||
} catch {
|
||||
// skip malformed proof tag
|
||||
}
|
||||
}
|
||||
|
||||
const uTag = event.getMatchingTags("u");
|
||||
const mint = uTag.length > 0 ? uTag[0][1] : "";
|
||||
|
||||
records.push({
|
||||
date: new Date((event.created_at ?? 0) * 1000).toISOString(),
|
||||
type: "zap",
|
||||
amount: totalAmount,
|
||||
unit: "sat",
|
||||
mint,
|
||||
token_id: event.id,
|
||||
memo: event.content ?? "",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Warning: failed to parse nutzap event ${event.id}: ${err instanceof Error ? err.message : err}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`Found ${records.length} nutzap event(s)`);
|
||||
return records;
|
||||
}
|
||||
|
||||
export async function fetchNutzapRedemptions(
|
||||
ndk: NDK,
|
||||
user: NDKUser,
|
||||
signer: NDKSigner
|
||||
): Promise<TransactionRecord[]> {
|
||||
console.error("Fetching nutzap redemptions (kind:7377)...");
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [7377 as number],
|
||||
authors: [user.pubkey],
|
||||
});
|
||||
|
||||
const records: TransactionRecord[] = [];
|
||||
|
||||
for (const event of events) {
|
||||
const content = await decryptEvent(event, user, signer);
|
||||
if (!content) continue;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(content);
|
||||
// Redemption events track the redeemed nutzap
|
||||
const eTags = event.getMatchingTags("e");
|
||||
const redeemId = eTags.length > 0 ? eTags[0][1] : event.id;
|
||||
|
||||
if (data.proofs && Array.isArray(data.proofs)) {
|
||||
const amount = data.proofs.reduce(
|
||||
(sum: number, p: { amount?: number }) => sum + (p.amount ?? 0),
|
||||
0
|
||||
);
|
||||
records.push({
|
||||
date: new Date((event.created_at ?? 0) * 1000).toISOString(),
|
||||
type: "receive",
|
||||
amount,
|
||||
unit: data.unit ?? "sat",
|
||||
mint: data.mint ?? "",
|
||||
token_id: redeemId,
|
||||
memo: "nutzap redemption",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Warning: failed to parse redemption event ${event.id}: ${err instanceof Error ? err.message : err}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`Found ${records.length} nutzap redemption(s)`);
|
||||
return records;
|
||||
}
|
||||
|
||||
export function buildTransactionHistory(
|
||||
_walletData: WalletData,
|
||||
tokenEvents: TokenData[],
|
||||
historyEvents: HistoryEntry[],
|
||||
nutzaps: TransactionRecord[],
|
||||
redemptions: TransactionRecord[]
|
||||
): TransactionRecord[] {
|
||||
const records: TransactionRecord[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
// Primary: history events (kind:7376)
|
||||
for (const entry of historyEvents) {
|
||||
if (seen.has(entry.eventId)) continue;
|
||||
seen.add(entry.eventId);
|
||||
|
||||
records.push({
|
||||
date: new Date(entry.createdAt * 1000).toISOString(),
|
||||
type: entry.direction === "in" ? "receive" : "send",
|
||||
amount: parseInt(entry.amount, 10) || 0,
|
||||
unit: entry.unit,
|
||||
mint: "",
|
||||
token_id: entry.eventId,
|
||||
memo: "",
|
||||
});
|
||||
}
|
||||
|
||||
// If no history events, fall back to token events
|
||||
if (historyEvents.length === 0) {
|
||||
console.error(
|
||||
"No history events found, deriving from token events..."
|
||||
);
|
||||
for (const token of tokenEvents) {
|
||||
if (seen.has(token.eventId)) continue;
|
||||
seen.add(token.eventId);
|
||||
|
||||
const amount = token.proofs.reduce((sum, p) => sum + p.amount, 0);
|
||||
records.push({
|
||||
date: new Date(token.createdAt * 1000).toISOString(),
|
||||
type: "receive",
|
||||
amount,
|
||||
unit: token.unit,
|
||||
mint: token.mint,
|
||||
token_id: token.eventId,
|
||||
memo: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Nutzap events (kind:9321)
|
||||
for (const zap of nutzaps) {
|
||||
if (seen.has(zap.token_id)) continue;
|
||||
seen.add(zap.token_id);
|
||||
records.push(zap);
|
||||
}
|
||||
|
||||
// Nutzap redemptions (kind:7377)
|
||||
for (const redemption of redemptions) {
|
||||
if (seen.has(redemption.token_id)) continue;
|
||||
seen.add(redemption.token_id);
|
||||
records.push(redemption);
|
||||
}
|
||||
|
||||
// Sort by date ascending
|
||||
records.sort(
|
||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||
);
|
||||
|
||||
return records;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import NDK, { NDKPrivateKeySigner, type NDKUser } from "@nostr-dev-kit/ndk";
|
||||
|
||||
const DEFAULT_RELAYS = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://relay.primal.net",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.nostr.band",
|
||||
];
|
||||
|
||||
export async function initNDK(secretKey: string, extraRelays: string[] = []) {
|
||||
const signer = new NDKPrivateKeySigner(secretKey);
|
||||
const relays = [...new Set([...DEFAULT_RELAYS, ...extraRelays])];
|
||||
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: relays,
|
||||
signer,
|
||||
});
|
||||
|
||||
console.error(`Connecting to ${relays.length} relays...`);
|
||||
await ndk.connect();
|
||||
console.error("Connected.");
|
||||
|
||||
const user: NDKUser = await signer.user();
|
||||
|
||||
return { ndk, signer, user };
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export interface TransactionRecord {
|
||||
date: string;
|
||||
type: "send" | "receive" | "zap";
|
||||
amount: number;
|
||||
unit: string;
|
||||
mint: string;
|
||||
token_id: string;
|
||||
memo: string;
|
||||
}
|
||||
|
||||
export interface WalletData {
|
||||
mints: string[];
|
||||
privkey: string;
|
||||
}
|
||||
|
||||
export interface TokenData {
|
||||
mint: string;
|
||||
unit: string;
|
||||
proofs: Array<{ id: string; amount: number; secret: string; C: string }>;
|
||||
del?: string[];
|
||||
eventId: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface HistoryEntry {
|
||||
direction: "in" | "out";
|
||||
amount: string;
|
||||
unit: string;
|
||||
eventId: string;
|
||||
createdAt: number;
|
||||
referencedEvents: string[];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "dist",
|
||||
"noEmit": true,
|
||||
"types": ["bun-types"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user