Initial Commit
This commit is contained in:
commit
2d858727b0
10 changed files with 1128 additions and 0 deletions
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
15
README.md
Normal file
15
README.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# portalbtc-lib
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.2.17. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
76
bun.lock
Normal file
76
bun.lock
Normal file
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "portalbtc-lib",
|
||||
"dependencies": {
|
||||
"@breeztech/breez-sdk-liquid": "^0.9.2-rc2",
|
||||
"@cashu/cashu-ts": "^2.5.2",
|
||||
"nostr-tools": "^2.15.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"typescript": "^5.8.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@breeztech/breez-sdk-liquid": ["@breeztech/breez-sdk-liquid@0.9.2-rc2", "", {}, "sha512-6t7b6RphlO/vH3VZn2MXJOotT8bvc4WlDqXBb2wWitpubOfIvVvCIoo+gSezlc0U87ovLF0NOEzYTQkKBtlU0g=="],
|
||||
|
||||
"@cashu/cashu-ts": ["@cashu/cashu-ts@2.5.2", "", { "dependencies": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "buffer": "^6.0.3" } }, "sha512-AjfDOZKb3RWWhmpHABC4KJxwJs3wp6eOFg6U3S6d3QOqtSoNkceMTn6lLN4/bYQarLR19rysbrIJ8MHsSwNxeQ=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="],
|
||||
|
||||
"@noble/curves": ["@noble/curves@1.9.2", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g=="],
|
||||
|
||||
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||
|
||||
"@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
|
||||
|
||||
"@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="],
|
||||
|
||||
"@scure/bip39": ["@scure/bip39@1.2.1", "", { "dependencies": { "@noble/hashes": "~1.3.0", "@scure/base": "~1.1.0" } }, "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.12", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||
|
||||
"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.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"nostr-tools": ["nostr-tools@2.15.0", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-Jj/+UFbu3JbTAWP4ipPFNuyD4W5eVRBNAP+kmnoRCYp3bLmTrlQ0Qhs5O1xSQJTFpjdZqoS0zZOUKdxUdjc+pw=="],
|
||||
|
||||
"nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
|
||||
"@scure/bip32/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
|
||||
|
||||
"@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
|
||||
|
||||
"nostr-tools/@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="],
|
||||
|
||||
"nostr-tools/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
|
||||
|
||||
"nostr-tools/@scure/bip32": ["@scure/bip32@1.3.1", "", { "dependencies": { "@noble/curves": "~1.1.0", "@noble/hashes": "~1.3.1", "@scure/base": "~1.1.0" } }, "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A=="],
|
||||
|
||||
"nostr-tools/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
|
||||
|
||||
"nostr-tools/@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="],
|
||||
|
||||
"nostr-tools/@scure/bip32/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
|
||||
|
||||
"nostr-tools/@scure/bip32/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
|
||||
}
|
||||
}
|
107
cashu.ts
Normal file
107
cashu.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import { CashuMint, type Proof } from "@cashu/cashu-ts";
|
||||
import { CashuWallet } from "@cashu/cashu-ts";
|
||||
import NpubCash from "./npubCash";
|
||||
import type { CashuStore, CashuTxn } from "./paymentStatus";
|
||||
import PortalBtcWallet from ".";
|
||||
|
||||
export default class PortalBtcWalletCashu {
|
||||
private npubCash: NpubCash;
|
||||
private cashuTxns: CashuTxn[] = [];
|
||||
private proofs: Proof[] = [];
|
||||
private onBalanceUpdated: (() => void) | null = null;
|
||||
|
||||
get txns() {
|
||||
return this.cashuTxns;
|
||||
}
|
||||
|
||||
get cashuWallet() {
|
||||
const mint = new CashuMint(PortalBtcWallet.MINT_URL);
|
||||
return new CashuWallet(mint);
|
||||
}
|
||||
|
||||
get balance() {
|
||||
return this.proofs.reduce((sum, p) => sum + p.amount, 0);
|
||||
}
|
||||
|
||||
constructor(
|
||||
mnemonic: string,
|
||||
private cashuStore: CashuStore,
|
||||
) {
|
||||
this.npubCash = new NpubCash(mnemonic);
|
||||
}
|
||||
|
||||
async init(onBalanceUpdated: () => void) {
|
||||
this.cashuTxns = await this.cashuStore.getTxns();
|
||||
this.proofs = await this.cashuStore.getProofs();
|
||||
this.onBalanceUpdated = onBalanceUpdated;
|
||||
}
|
||||
|
||||
async redeemCashuQuotes() {
|
||||
const attempt = this.npubCash.tryRedeemUnredeemedCashuQuotes(
|
||||
await this.cashuStore.getLastRedeemedCashuQuoteTimestamp(),
|
||||
);
|
||||
for await (const quote of attempt) {
|
||||
this.cashuTxns.push({
|
||||
txId: `cashu-quote-${quote.quoteId}`,
|
||||
paymentType: "receive",
|
||||
amountSat: quote.amountSat,
|
||||
timestamp: quote.timestamp,
|
||||
status: "complete",
|
||||
});
|
||||
}
|
||||
const proofs = await attempt.next();
|
||||
if (!proofs.done || typeof proofs.value === "undefined") return [];
|
||||
this.proofs.push(...proofs.value);
|
||||
await this.persistState();
|
||||
this.onBalanceUpdated?.();
|
||||
return proofs.value;
|
||||
}
|
||||
|
||||
async persistState() {
|
||||
await this.cashuStore.persistProofs(this.proofs);
|
||||
await this.cashuStore.persistTxns(this.cashuTxns);
|
||||
await this.cashuStore.persistLastRedeemedCashuQuoteTimestamp(
|
||||
this.cashuTxns[this.cashuTxns.length - 1]?.timestamp ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
async meltProofsToPayInvoice(invoice: string) {
|
||||
const meltQuote = await this.cashuWallet.createMeltQuote(invoice);
|
||||
const amountToMelt = meltQuote.amount + meltQuote.fee_reserve;
|
||||
const { keep, send } = await this.cashuWallet.send(
|
||||
amountToMelt,
|
||||
this.proofs,
|
||||
{
|
||||
includeFees: true,
|
||||
},
|
||||
);
|
||||
const { change } = await this.cashuWallet.meltProofs(meltQuote, send);
|
||||
this.proofs = [...keep, ...change];
|
||||
this.cashuTxns.push({
|
||||
txId: `cashu-send-${Date.now()}`,
|
||||
paymentType: "send",
|
||||
amountSat: amountToMelt,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
status: "complete",
|
||||
});
|
||||
await this.persistState();
|
||||
this.onBalanceUpdated?.();
|
||||
}
|
||||
|
||||
async redeemToken(token: string): Promise<void> {
|
||||
if (!token.trim()) throw new Error("Token is empty");
|
||||
|
||||
const received = await this.cashuWallet.receive(token.trim());
|
||||
this.proofs.push(...received);
|
||||
const amountReceived = received.reduce((sum, p) => sum + p.amount, 0);
|
||||
this.cashuTxns.push({
|
||||
txId: `cashu-receive-${Date.now()}`,
|
||||
paymentType: "receive",
|
||||
amountSat: amountReceived,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
status: "complete",
|
||||
});
|
||||
await this.persistState();
|
||||
this.onBalanceUpdated?.();
|
||||
}
|
||||
}
|
172
examples/portalbtc-cli.ts
Normal file
172
examples/portalbtc-cli.ts
Normal file
|
@ -0,0 +1,172 @@
|
|||
#!/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);
|
||||
});
|
531
index.ts
Normal file
531
index.ts
Normal file
|
@ -0,0 +1,531 @@
|
|||
import initBreez, {
|
||||
type BindingLiquidSdk,
|
||||
connect as breezConnect,
|
||||
defaultConfig as defaultBreezConfig,
|
||||
type InputType,
|
||||
type LogEntry,
|
||||
type Payment,
|
||||
type PaymentMethod,
|
||||
type PreparePayOnchainRequest,
|
||||
type PrepareReceiveRequest,
|
||||
type PrepareSendRequest,
|
||||
setLogger,
|
||||
} from "@breeztech/breez-sdk-liquid/web";
|
||||
import { privateKeyFromSeedWords as nostrPrivateKeyFromSeedWords } from "nostr-tools/nip06";
|
||||
import { getPublicKey, nip19 } from "nostr-tools";
|
||||
import { type CashuStore, type CashuTxn, PaymentStatus } from "./paymentStatus";
|
||||
export { type CashuStore, PaymentStatus } from "./paymentStatus";
|
||||
import PortalBtcWalletCashu from "./cashu";
|
||||
|
||||
type CombinedPayment = Payment | CashuTxn;
|
||||
|
||||
export type { CombinedPayment as Payment };
|
||||
|
||||
export default class PortalBtcWallet {
|
||||
static MINT_URL = "https://mint.minibits.cash/Bitcoin";
|
||||
|
||||
private breezSDK: BindingLiquidSdk | null = null;
|
||||
private cashuSDK: PortalBtcWalletCashu;
|
||||
|
||||
private balances = {
|
||||
balance: 0,
|
||||
pendingReceive: 0,
|
||||
pendingSend: 0,
|
||||
};
|
||||
|
||||
get cashuBalance() {
|
||||
return this.cashuSDK.balance;
|
||||
}
|
||||
|
||||
get balance() {
|
||||
return this.balances.balance + this.cashuBalance;
|
||||
}
|
||||
|
||||
get pendingBalance() {
|
||||
return this.balances.pendingReceive - this.balances.pendingSend;
|
||||
}
|
||||
|
||||
get npub() {
|
||||
const nostrPrivateKey = nostrPrivateKeyFromSeedWords(this.mnemonic);
|
||||
const nostrPubkey = getPublicKey(nostrPrivateKey);
|
||||
return nip19.npubEncode(nostrPubkey);
|
||||
}
|
||||
|
||||
get lightningAddress() {
|
||||
return `${this.npub}@${this.baseDomain}`;
|
||||
}
|
||||
|
||||
static async create(
|
||||
mnemonic: string,
|
||||
cashuStore: CashuStore,
|
||||
loggingEnabled = false,
|
||||
network: "mainnet" | "testnet" = "testnet",
|
||||
breezApiKey?: string,
|
||||
cashuMeltThreshold?: number,
|
||||
baseDomain?: string,
|
||||
) {
|
||||
const wallet = new PortalBtcWallet(
|
||||
mnemonic,
|
||||
cashuStore,
|
||||
loggingEnabled,
|
||||
network,
|
||||
breezApiKey,
|
||||
cashuMeltThreshold,
|
||||
baseDomain,
|
||||
);
|
||||
await wallet.init();
|
||||
return wallet;
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private mnemonic: string,
|
||||
cashuStore: CashuStore,
|
||||
private loggingEnabled = false,
|
||||
private network: "mainnet" | "testnet" = "testnet",
|
||||
private breezApiKey =
|
||||
"MIIBajCCARygAwIBAgIHPgwAQY4DlTAFBgMrZXAwEDEOMAwGA1UEAxMFQnJlZXowHhcNMjUwNTA1MTY1OTM5WhcNMzUwNTAzMTY1OTM5WjAnMQwwCgYDVQQKEwNBcngxFzAVBgNVBAMTDkRhbm55IE1vcmFiaXRvMCowBQYDK2VwAyEA0IP1y98gPByiIMoph1P0G6cctLb864rNXw1LRLOpXXejfjB8MA4GA1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTaOaPuXmtLDTJVv++VYBiQr9gHCTAfBgNVHSMEGDAWgBTeqtaSVvON53SSFvxMtiCyayiYazAcBgNVHREEFTATgRFkYW5ueUBhcngtY2NuLmNvbTAFBgMrZXADQQAwJoh9BG8rEH1sOl+BpS12oNSwzgQga8ZcIAZ8Bjmd6QT4GSST0nLj06fs49pCkiULOl9ZoRIeIMc3M1XqV5UA",
|
||||
private cashuMeltThreshold = 2000,
|
||||
private baseDomain = "portalbtc.live",
|
||||
) {
|
||||
this.cashuSDK = new PortalBtcWalletCashu(
|
||||
mnemonic,
|
||||
cashuStore,
|
||||
);
|
||||
}
|
||||
|
||||
private async init() {
|
||||
await initBreez();
|
||||
this.breezSDK = await breezConnect({
|
||||
mnemonic: this.mnemonic,
|
||||
config: defaultBreezConfig(this.network, this.breezApiKey),
|
||||
});
|
||||
this.breezSDK.addEventListener({
|
||||
onEvent: (e) => {
|
||||
if (this.loggingEnabled) {
|
||||
console.log(e);
|
||||
}
|
||||
this.refreshBreezBalances();
|
||||
this.emitEvent("balanceUpdated");
|
||||
},
|
||||
});
|
||||
setLogger({
|
||||
log: (e: LogEntry) => {
|
||||
if (this.loggingEnabled) {
|
||||
console.log(`[Breez] [${e.level}] ${e.line}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
await this.cashuSDK.init(() => this.emitEvent("balanceUpdated"));
|
||||
await this.redeemCashuQuotes();
|
||||
await this.maybeMelt();
|
||||
}
|
||||
|
||||
private async refreshBreezBalances() {
|
||||
if (!this.breezSDK) throw new Error("Breez SDK not initialized");
|
||||
const info = await this.breezSDK.getInfo();
|
||||
this.balances.balance = info?.walletInfo.balanceSat ?? 0;
|
||||
this.balances.pendingReceive = info?.walletInfo.pendingReceiveSat ?? 0;
|
||||
this.balances.pendingSend = info?.walletInfo.pendingSendSat ?? 0;
|
||||
}
|
||||
|
||||
private async maybeMelt() {
|
||||
if (!this.breezSDK) throw new Error("Breez SDK not initialized");
|
||||
if (this.cashuBalance < this.cashuMeltThreshold) return;
|
||||
const cashuAmountToSwapToLiquid = this.cashuBalance -
|
||||
(this.cashuBalance % this.cashuMeltThreshold);
|
||||
const invoice = await this.generateBolt11Invoice(cashuAmountToSwapToLiquid);
|
||||
await this.cashuSDK.meltProofsToPayInvoice(invoice);
|
||||
this.emitEvent("balanceUpdated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Redeems unredeemed Cashu quotes
|
||||
*
|
||||
* @returns Promise<Proof[]>
|
||||
*/
|
||||
async redeemCashuQuotes() {
|
||||
return this.cashuSDK.redeemCashuQuotes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Lightning invoice
|
||||
*
|
||||
* @param amountSat - The amount to generate the invoice for
|
||||
* @returns Promise<string> - The Lightning invoice
|
||||
*/
|
||||
async generateBolt11Invoice(amountSat: number): Promise<string> {
|
||||
if (!this.breezSDK) throw new Error("Breez SDK not initialized");
|
||||
const prepare = await this.breezSDK?.prepareReceivePayment({
|
||||
paymentMethod: "bolt11Invoice",
|
||||
amount: { type: "bitcoin", payerAmountSat: amountSat },
|
||||
amountSat: amountSat,
|
||||
} as PrepareReceiveRequest);
|
||||
const { destination } = await this.breezSDK.receivePayment({
|
||||
prepareResponse: prepare,
|
||||
});
|
||||
return destination as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redeems a Cashu token
|
||||
*
|
||||
* @param token - The Cashu token to redeem
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async redeemToken(token: string): Promise<void> {
|
||||
await this.cashuSDK.redeemToken(token);
|
||||
await this.maybeMelt();
|
||||
this.emitEvent("balanceUpdated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pays a Lightning invoice using Cashu
|
||||
*
|
||||
* @param invoice - The Lightning invoice to pay
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async payBolt11InvoiceUsingCashu(invoice: string): Promise<void> {
|
||||
if (!invoice.trim()) throw new Error("Invoice is empty");
|
||||
await this.cashuSDK.meltProofsToPayInvoice(invoice);
|
||||
}
|
||||
|
||||
private async *payBolt11Invoice(
|
||||
parsedPayment: InputType,
|
||||
destination: string,
|
||||
amount: number,
|
||||
) {
|
||||
if (!this.breezSDK) throw new Error("Breez SDK not initialized");
|
||||
if (parsedPayment.type !== "bolt11") {
|
||||
throw new Error("Invalid payment type");
|
||||
}
|
||||
|
||||
try {
|
||||
yield {
|
||||
status: PaymentStatus.AttemptingCashuPayment,
|
||||
error: null,
|
||||
};
|
||||
await this.payBolt11InvoiceUsingCashu(destination);
|
||||
yield {
|
||||
status: PaymentStatus.PaymentSent,
|
||||
error: null,
|
||||
paymentHash: parsedPayment.invoice.paymentHash,
|
||||
};
|
||||
return;
|
||||
} catch (cashuErr) {
|
||||
yield {
|
||||
status: PaymentStatus.CashuPaymentFailed,
|
||||
error: cashuErr instanceof Error ? cashuErr.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
yield {
|
||||
status: PaymentStatus.AttemptingLightningPayment,
|
||||
error: null,
|
||||
};
|
||||
const prep = await this.breezSDK.prepareSendPayment({
|
||||
destination,
|
||||
amount: amount > 0
|
||||
? { type: "bitcoin", receiverAmountSat: amount }
|
||||
: undefined,
|
||||
});
|
||||
const res = await this.breezSDK.sendPayment({ prepareResponse: prep });
|
||||
const paymentDetails = res.payment.details;
|
||||
if (paymentDetails.type === "lightning") {
|
||||
yield {
|
||||
status: PaymentStatus.PaymentSent,
|
||||
error: null,
|
||||
preimage: paymentDetails.preimage,
|
||||
paymentHash: paymentDetails.paymentHash,
|
||||
};
|
||||
} else {
|
||||
yield {
|
||||
status: PaymentStatus.PaymentSent,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async *payChainAddress(
|
||||
parsedPayment: InputType,
|
||||
amount: number,
|
||||
) {
|
||||
if (!this.breezSDK) throw new Error("Breez SDK not initialized");
|
||||
if (
|
||||
parsedPayment.type !== "bitcoinAddress" &&
|
||||
parsedPayment.type !== "liquidAddress"
|
||||
) {
|
||||
throw new Error("Invalid payment type");
|
||||
}
|
||||
if (amount <= 0) {
|
||||
yield {
|
||||
status: PaymentStatus.AmountRequired,
|
||||
error: "Amount required for bitcoin address",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const onChainRequest: PreparePayOnchainRequest = {
|
||||
amount: { type: "bitcoin", receiverAmountSat: amount },
|
||||
};
|
||||
|
||||
yield {
|
||||
status: PaymentStatus.PreparingOnchainPayment,
|
||||
error: null,
|
||||
};
|
||||
const onChainPreparedResponse = await this.breezSDK.preparePayOnchain(
|
||||
onChainRequest,
|
||||
);
|
||||
|
||||
yield {
|
||||
status: PaymentStatus.BroadcastingOnchainPayment,
|
||||
error: null,
|
||||
};
|
||||
await this.breezSDK.payOnchain({
|
||||
address: parsedPayment.address.address,
|
||||
prepareResponse: onChainPreparedResponse,
|
||||
});
|
||||
yield {
|
||||
status: PaymentStatus.PaymentSent,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a payment destination
|
||||
*
|
||||
* @param destination - The payment destination
|
||||
* @returns Promise<InputType> - The parsed payment
|
||||
*/
|
||||
async parsePayment(destination: string): Promise<InputType> {
|
||||
if (!this.breezSDK) throw new Error("Breez SDK not initialized");
|
||||
const parsedPayment = await this.breezSDK.parse(destination);
|
||||
if (!parsedPayment) throw new Error("Failed to parse payment");
|
||||
return parsedPayment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pays a payment destination
|
||||
*
|
||||
* @param destination - The payment destination
|
||||
* @param amount - The amount to pay
|
||||
* @returns Promise<void> - The payment status
|
||||
*/
|
||||
async *pay(
|
||||
destination: string,
|
||||
amount = 0,
|
||||
) {
|
||||
if (!this.breezSDK) throw new Error("Breez SDK not initialized");
|
||||
yield {
|
||||
status: PaymentStatus.ParsingDestination,
|
||||
error: null,
|
||||
};
|
||||
const parsedPayment = await this.parsePayment(destination);
|
||||
try {
|
||||
if (parsedPayment.type === "bolt11") {
|
||||
yield* this.payBolt11Invoice(parsedPayment, destination, amount);
|
||||
this.emitEvent("balanceUpdated");
|
||||
return;
|
||||
}
|
||||
|
||||
const paymentTypesThatRequireAmount = [
|
||||
"bitcoinAddress",
|
||||
"liquidAddress",
|
||||
"bolt12Offer",
|
||||
"nodeId",
|
||||
];
|
||||
|
||||
if (paymentTypesThatRequireAmount.includes(parsedPayment.type)) {
|
||||
if (amount <= 0) {
|
||||
yield {
|
||||
status: PaymentStatus.AmountRequired,
|
||||
error: "Amount required for payment type",
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
parsedPayment.type === "bitcoinAddress" ||
|
||||
parsedPayment.type === "liquidAddress"
|
||||
) {
|
||||
yield* this.payChainAddress(parsedPayment, amount);
|
||||
this.emitEvent("balanceUpdated");
|
||||
return;
|
||||
}
|
||||
|
||||
const paymentRequest: PrepareSendRequest = { destination };
|
||||
if (amount > 0) {
|
||||
paymentRequest.amount = {
|
||||
type: "bitcoin",
|
||||
receiverAmountSat: amount,
|
||||
};
|
||||
}
|
||||
|
||||
yield {
|
||||
status: PaymentStatus.PreparingOnchainPayment,
|
||||
error: null,
|
||||
};
|
||||
const prep = await this.breezSDK.prepareSendPayment(
|
||||
paymentRequest,
|
||||
);
|
||||
yield {
|
||||
status: PaymentStatus.BroadcastingOnchainPayment,
|
||||
error: null,
|
||||
};
|
||||
await this.breezSDK.sendPayment({ prepareResponse: prep });
|
||||
yield {
|
||||
status: PaymentStatus.PaymentSent,
|
||||
error: null,
|
||||
};
|
||||
this.emitEvent("balanceUpdated");
|
||||
return;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
const raw = e.message;
|
||||
const protoMatch = raw.match(/Generic error:.*?\(\"(.+?)\"\)/);
|
||||
yield {
|
||||
status: PaymentStatus.PaymentFailed,
|
||||
error: protoMatch ? protoMatch[1] : raw,
|
||||
};
|
||||
} else {
|
||||
yield {
|
||||
status: PaymentStatus.PaymentFailed,
|
||||
error: "Send failed",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists payments
|
||||
*
|
||||
* @param limit - The number of payments to list
|
||||
* @param offset - The offset to start listing from
|
||||
* @returns Promise<Payment[]> - The payments
|
||||
*/
|
||||
async listPayments(limit: number, offset: number) {
|
||||
const allPayments: CombinedPayment[] = [];
|
||||
const breezPayments = await this.breezSDK?.listPayments({});
|
||||
if (breezPayments) {
|
||||
allPayments.push(...breezPayments);
|
||||
}
|
||||
allPayments.push(...this.cashuSDK.txns);
|
||||
const sortedPayments = allPayments.sort((a, b) =>
|
||||
b.timestamp - a.timestamp
|
||||
);
|
||||
return sortedPayments.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
private eventListeners: Record<string, (() => void)[]> = {};
|
||||
|
||||
/**
|
||||
* Adds an event listener
|
||||
*
|
||||
* @param event - The event to listen for
|
||||
* @param listener - The listener function
|
||||
*/
|
||||
addEventListener(event: "balanceUpdated", listener: () => void) {
|
||||
if (!this.eventListeners[event]) {
|
||||
this.eventListeners[event] = [];
|
||||
}
|
||||
this.eventListeners[event].push(listener);
|
||||
}
|
||||
|
||||
private emitEvent(event: "balanceUpdated") {
|
||||
if (this.loggingEnabled) {
|
||||
console.log(`[${event}]`);
|
||||
}
|
||||
for (const listener of this.eventListeners[event] ?? []) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refunds a payment
|
||||
*
|
||||
* @param payment - The payment to refund
|
||||
*/
|
||||
async refundPayment(payment: Payment) {
|
||||
if (!this.breezSDK) throw new Error("Breez SDK not initialized");
|
||||
const prepareAddr = await this.breezSDK.prepareReceivePayment({
|
||||
paymentMethod: "bitcoinAddress",
|
||||
});
|
||||
const receiveRes = await this.breezSDK.receivePayment({
|
||||
prepareResponse: prepareAddr,
|
||||
});
|
||||
const refundAddress = receiveRes.destination as string;
|
||||
|
||||
try {
|
||||
const refundables = await this.breezSDK.listRefundables();
|
||||
let swapAddress: string | undefined;
|
||||
if (refundables.length === 1) {
|
||||
swapAddress = refundables[0]?.swapAddress;
|
||||
} else {
|
||||
swapAddress = refundables.find(
|
||||
(r) =>
|
||||
r.amountSat === payment.amountSat &&
|
||||
Math.abs(r.timestamp - payment.timestamp) < 300,
|
||||
)?.swapAddress;
|
||||
}
|
||||
|
||||
if (!swapAddress) {
|
||||
throw new Error("Could not identify refundable swap for this payment.");
|
||||
}
|
||||
|
||||
const fees = await this.breezSDK.recommendedFees();
|
||||
const feeRateSatPerVbyte = fees.economyFee;
|
||||
|
||||
const refundRequest = {
|
||||
swapAddress,
|
||||
refundAddress,
|
||||
feeRateSatPerVbyte,
|
||||
} as const;
|
||||
|
||||
try {
|
||||
await this.breezSDK.prepareRefund(refundRequest);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"prepareRefund failed (may be expected for some swaps)",
|
||||
err,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.breezSDK.refund(refundRequest);
|
||||
} catch (err) {
|
||||
console.error("Refund failed", err);
|
||||
throw new Error(
|
||||
`Refund failed: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new address for a payment method
|
||||
*
|
||||
* @param paymentMethod - The payment method to generate an address for
|
||||
* @returns Promise<string> - The new address
|
||||
*/
|
||||
async generateAddress(paymentMethod: PaymentMethod) {
|
||||
if (!this.breezSDK) throw new Error("Breez SDK not initialized");
|
||||
const prep = await this.breezSDK.prepareReceivePayment({ paymentMethod });
|
||||
const res = await this.breezSDK.receivePayment({ prepareResponse: prep });
|
||||
const rawDestination = res.destination;
|
||||
|
||||
if (rawDestination === undefined) {
|
||||
throw new Error("Failed to generate address: destination is undefined.");
|
||||
}
|
||||
|
||||
let destination = rawDestination;
|
||||
|
||||
const colonIndex = destination.indexOf(":");
|
||||
if (colonIndex !== -1) {
|
||||
destination = destination.substring(colonIndex + 1);
|
||||
}
|
||||
|
||||
const questionMarkIndex = destination.indexOf("?");
|
||||
if (questionMarkIndex !== -1) {
|
||||
destination = destination.substring(0, questionMarkIndex);
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
}
|
124
npubCash.ts
Normal file
124
npubCash.ts
Normal file
|
@ -0,0 +1,124 @@
|
|||
import {
|
||||
CashuMint,
|
||||
CashuWallet,
|
||||
MintQuoteState,
|
||||
type Proof,
|
||||
} from "@cashu/cashu-ts";
|
||||
|
||||
import { privateKeyFromSeedWords as nostrPrivateKeyFromSeedWords } from "nostr-tools/nip06";
|
||||
import { finalizeEvent, nip98 } from "nostr-tools";
|
||||
import type { BodyInit } from "bun";
|
||||
|
||||
interface NpubCashQuote {
|
||||
createdAt: number;
|
||||
paidAt?: number;
|
||||
expiresAt: number;
|
||||
mintUrl: string;
|
||||
quoteId: string;
|
||||
request: string;
|
||||
amount: number;
|
||||
state: "PAID" | "ISSUED" | "INFLIGHT";
|
||||
locked: boolean;
|
||||
}
|
||||
|
||||
interface NpubCashPaginationMetadata {
|
||||
total: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export default class NpubCash {
|
||||
constructor(private mnemonic: string) {}
|
||||
|
||||
private async fetchWithNip98<T>(
|
||||
{ url, method, body }: {
|
||||
url: string;
|
||||
method: string;
|
||||
body?: BodyInit | null;
|
||||
},
|
||||
): Promise<T> {
|
||||
const nostrPrivateKey = nostrPrivateKeyFromSeedWords(this.mnemonic);
|
||||
const urlWithoutQueryParams = url.split("?")[0] ?? "";
|
||||
const npubCashNip98 = await nip98.getToken(
|
||||
urlWithoutQueryParams,
|
||||
method,
|
||||
(event) => finalizeEvent(event, nostrPrivateKey),
|
||||
);
|
||||
return fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
"Authorization": `Nostr ${npubCashNip98}`,
|
||||
},
|
||||
body,
|
||||
}).then((data) => data.json() as Promise<T>);
|
||||
}
|
||||
|
||||
private async getQuotes(since: number, limit: number, offset: number) {
|
||||
return this.fetchWithNip98<
|
||||
{
|
||||
error: false;
|
||||
data: { quotes: NpubCashQuote[] };
|
||||
metadata: NpubCashPaginationMetadata;
|
||||
} | {
|
||||
error: true;
|
||||
message: string;
|
||||
}
|
||||
>({
|
||||
url:
|
||||
`https://npubx.cash/api/v2/wallet/quotes?since=${since}&limit=${limit}&offset=${offset}`,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
private async getAllPaidQuotes(since: number) {
|
||||
const quotes: NpubCashQuote[] = [];
|
||||
let offset = 0;
|
||||
while (true) {
|
||||
const currentQuotes = await this.getQuotes(since, 50, offset);
|
||||
if (currentQuotes.error === false) {
|
||||
quotes.push(...currentQuotes.data.quotes);
|
||||
if (quotes.length >= currentQuotes.metadata.total) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
throw new Error(currentQuotes.message ?? "Unknown error");
|
||||
}
|
||||
offset += 50;
|
||||
}
|
||||
return quotes.sort((a, b) => a.createdAt - b.createdAt).filter(
|
||||
(quote) => quote.state === "PAID",
|
||||
);
|
||||
}
|
||||
|
||||
async *tryRedeemUnredeemedCashuQuotes(
|
||||
latestRedeemedCashuQuoteTimestamp: number,
|
||||
) {
|
||||
const proofs: Proof[] = [];
|
||||
const quotes: NpubCashQuote[] = await this.getAllPaidQuotes(
|
||||
latestRedeemedCashuQuoteTimestamp,
|
||||
);
|
||||
for (const quote of quotes) {
|
||||
const mint = new CashuMint(quote.mintUrl);
|
||||
const wallet = new CashuWallet(mint);
|
||||
const req = await mint.checkMintQuote(quote.quoteId);
|
||||
if (
|
||||
req.state === MintQuoteState.PAID && typeof quote.paidAt === "number"
|
||||
) {
|
||||
const newProofs = await wallet.mintProofs(
|
||||
quote.amount,
|
||||
quote.quoteId,
|
||||
);
|
||||
proofs.push(...newProofs);
|
||||
const amountReceived = newProofs.reduce(
|
||||
(sum, p) => sum + p.amount,
|
||||
0,
|
||||
);
|
||||
yield {
|
||||
quoteId: quote.quoteId,
|
||||
amountSat: amountReceived,
|
||||
timestamp: quote.paidAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
return proofs;
|
||||
}
|
||||
}
|
16
package.json
Normal file
16
package.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "portalbtc-lib",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"description": "Portal BTC Wallet",
|
||||
"dependencies": {
|
||||
"@breeztech/breez-sdk-liquid": "^0.9.2-rc2",
|
||||
"@cashu/cashu-ts": "^2.5.2",
|
||||
"nostr-tools": "^2.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
30
paymentStatus.ts
Normal file
30
paymentStatus.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import type { Proof } from "@cashu/cashu-ts";
|
||||
|
||||
export const PaymentStatus = {
|
||||
ParsingDestination: 0,
|
||||
AttemptingCashuPayment: 1,
|
||||
AttemptingLightningPayment: 2,
|
||||
CashuPaymentFailed: 3,
|
||||
AmountRequired: 4,
|
||||
PreparingOnchainPayment: 5,
|
||||
BroadcastingOnchainPayment: 6,
|
||||
PaymentFailed: 0xfe,
|
||||
PaymentSent: 0xff,
|
||||
} as const;
|
||||
|
||||
export interface CashuTxn {
|
||||
txId: string;
|
||||
paymentType: "receive" | "send";
|
||||
amountSat: number;
|
||||
timestamp: number;
|
||||
status: "complete";
|
||||
}
|
||||
|
||||
export interface CashuStore {
|
||||
getProofs(): Promise<Proof[]>;
|
||||
getTxns(): Promise<CashuTxn[]>;
|
||||
getLastRedeemedCashuQuoteTimestamp(): Promise<number>;
|
||||
persistProofs(proofs: Proof[]): Promise<void>;
|
||||
persistTxns(txns: CashuTxn[]): Promise<void>;
|
||||
persistLastRedeemedCashuQuoteTimestamp(timestamp: number): Promise<void>;
|
||||
}
|
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"ESNext"
|
||||
],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"allowJs": false,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue