Skip to content

Commit

Permalink
upgrade cashu-ts
Browse files Browse the repository at this point in the history
  • Loading branch information
pablof7z committed Sep 30, 2024
1 parent d2c323c commit 543067b
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 192 deletions.
2 changes: 1 addition & 1 deletion ndk-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
},
"homepage": "https://github.com/nostr-dev-kit/ndk",
"dependencies": {
"@cashu/cashu-ts": "1.0.0-rc.9",
"@cashu/cashu-ts": "1.1.0",
"@getalby/sdk": "^3.6.1",
"@nostr-dev-kit/ndk": "workspace:*",
"debug": "^4.3.4",
Expand Down
2 changes: 1 addition & 1 deletion ndk-wallet/src/cashu/deposit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class NDKCashuDeposit extends EventEmitter<{
}

async start() {
const quote = await this._wallet.mintQuote(this.amount);
const quote = await this._wallet.createMintQuote(this.amount);
d("created quote %s for %d %s", quote.quote, this.amount, this.mint);

this.quoteId = quote.quote;
Expand Down
3 changes: 3 additions & 0 deletions ndk-wallet/src/cashu/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const MARKERS = {
DESTROYED: "destroyed",
};

/**
* This class represents a balance change in the wallet, whether money being added or removed.
*/
export class NDKWalletChange extends NDKEvent {
static MARKERS = MARKERS;

Expand Down
219 changes: 141 additions & 78 deletions ndk-wallet/src/cashu/pay/ln.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,102 +4,165 @@ import type { NDKCashuPay } from "../pay";
import type { TokenSelection} from "../proofs";
import { rollOverProofs, chooseProofsForPr } from "../proofs";
import type { MintUrl } from "../mint/utils";
import { NDKCashuWallet } from "../wallet";

/**
*
* @param useMint Forces the payment to use a specific mint
* @returns
*/
export async function payLn(this: NDKCashuPay, useMint?: MintUrl): Promise<string | undefined> {
const mintBalances = this.wallet.mintBalances;
const selections: TokenSelection[] = [];
let amount = this.getAmount();
const amount = this.getAmount() / 1000; // convert msat to sat
const data = this.info as LnPaymentInfo;
if (!data.pr) throw new Error("missing pr");

amount /= 1000; // convert msat to sat

let paid = false;
let processingSelections = false;
const processSelections = async (resolve: (value: string) => void, reject: (err: string) => void) => {
processingSelections = true;
for (const selection of selections) {
const _wallet = new CashuWallet(new CashuMint(selection.mint));
this.debug(
"paying LN invoice for %d sats (%d in fees) with proofs %o, %s",
selection.quote!.amount,
selection.quote!.fee_reserve,
selection.usedProofs,
data.pr
);
try {
const result = await _wallet.payLnInvoice(
data.pr,
selection.usedProofs,
selection.quote
);
this.debug("payment result: %o", result);

if (result.isPaid && result.preimage) {
this.debug("payment successful");
rollOverProofs(selection, result.change, selection.mint, this.wallet);
paid = true;
resolve(result.preimage);
}
} catch (e) {
this.debug("failed to pay with mint %s", e.message);
if (e?.message.match(/already spent/i)) {
this.debug("proofs already spent, rolling over");
rollOverProofs(selection, [], selection.mint, this.wallet);
}
}
}

if (!paid) {
reject("failed to pay with any mint");
}

processingSelections = false;
};

return new Promise<string>((resolve, reject) => {
let foundMint = false;
const eligibleMints = getEligibleMints(mintBalances, amount, useMint, this.debug);

if (eligibleMints.length === 0) {
handleInsufficientBalance(amount, data.pr, this.wallet, this.debug, reject);
return;
}

for (const [mint, balance] of Object.entries(mintBalances)) {
if (useMint && mint !== useMint) continue;
attemptPaymentWithEligibleMints(eligibleMints, data.pr, amount, this.wallet, this.debug, resolve, reject);
});
}

function getEligibleMints(
mintBalances: Record<string, number>,
amount: number,
useMint: MintUrl | undefined,
debug: NDKCashuPay['debug']
): string[] {
return Object.entries(mintBalances)
.filter(([mint, balance]) => {
if (useMint && mint !== useMint) return false;
if (balance < amount) {
this.debug("mint %s has insufficient balance %d", mint, balance, amount);

if (useMint) {
reject(`insufficient balance in mint ${mint} (${balance} < ${amount})`);
return;
}

continue;
debug("mint %s has insufficient balance %d", mint, balance, amount);
return false;
}
return true;
})
.map(([mint]) => mint);
}

foundMint = true;
function handleInsufficientBalance(
amount: number,
pr: string,
wallet: NDKCashuPay['wallet'],
debug: NDKCashuPay['debug'],
reject: (reason: string) => void
): void {
wallet.emit("insufficient_balance", {amount, pr});
debug("no mint with sufficient balance found");
reject("no mint with sufficient balance found");
}

chooseProofsForPr(data.pr, mint, this.wallet).then(async (result) => {
async function attemptPaymentWithEligibleMints(
eligibleMints: string[],
pr: string,
amount: number,
wallet: NDKCashuPay['wallet'],
debug: NDKCashuPay['debug'],
resolve: (value: string) => void,
reject: (reason: string) => void
): Promise<void> {
const TIMEOUT = 10000; // 10 seconds timeout
const selections: TokenSelection[] = [];

const selectionPromises = eligibleMints.map(mint =>
chooseProofsForPr(pr, mint, wallet)
.then(result => {
if (result) {
this.debug("successfully chose proofs for mint %s", mint);
debug("Successfully chose proofs for mint %s", mint);
selections.push(result);
if (!processingSelections) {
this.debug("processing selections");
await processSelections(resolve, reject);
}
}
});
return result;
})
.catch(error => {
debug("Error choosing proofs for mint %s: %s", mint, error);
return null;
})
);

try {
await Promise.race([
Promise.all(selectionPromises),
new Promise((_, timeoutReject) => setTimeout(() => timeoutReject(new Error("Timeout")), TIMEOUT))
]);
} catch (error) {
debug("Timed out while choosing proofs: %s", error);
}

if (selections.length === 0) {
reject("Failed to choose proofs for any mint");
return;
}

// Sort selections by fee reserve (lower is better)
selections.sort((a, b) => (a.quote?.fee_reserve ?? 0) - (b.quote?.fee_reserve ?? 0));

for (const selection of selections) {
try {
const result = await executePayment(selection, pr, wallet, debug);
if (result) {
resolve(result);
return;
}
} catch (error) {
debug("Failed to execute payment for mint %s: %s", selection.mint, error);
}
}

this.debug({foundMint})
reject("Failed to pay with any mint");
}

/**
* Attempts to pay using a selected set of Cashu tokens.
*
* @param selection - The TokenSelection object containing the chosen proofs and quote for the payment.
* @param pr - The Lightning Network payment request (invoice) to pay.
* @param wallet - The NDKCashuPay wallet instance.
* @param debug - The debug function for logging.
* @returns A Promise that resolves to the payment preimage as a string if successful, or null if the payment fails.
*
* @throws Will throw an error if the payment fails due to network issues or other problems.
*
* This function performs the following steps:
* 1. Creates a new CashuWallet instance for the specific mint.
* 2. Attempts to pay the Lightning invoice using the selected proofs.
* 3. If successful, it rolls over any change proofs.
* 4. If the proofs are already spent, it rolls over the selection without change.
* 5. Logs the process and any errors for debugging purposes.
*/
async function executePayment(
selection: TokenSelection,
pr: string,
wallet: NDKCashuWallet,
debug: NDKCashuPay['debug']
): Promise<string | null> {
const _wallet = new CashuWallet(new CashuMint(selection.mint));
debug(
"Attempting LN payment for %d sats (%d in fees) with proofs %o, %s",
selection.quote!.amount,
selection.quote!.fee_reserve,
selection.usedProofs,
pr
);

if (!foundMint) {
this.wallet.emit("insufficient_balance", {amount, pr: data.pr});

this.debug("no mint with sufficient balance found");
reject("no mint with sufficient balance found");
try {
const result = await _wallet.payLnInvoice(pr, selection.usedProofs, selection.quote);
debug("Payment result: %o", result);

if (result.isPaid && result.preimage) {
debug("Payment successful");
rollOverProofs(selection, result.change, selection.mint, wallet);
return result.preimage;
}
});
} catch (e) {
debug("Failed to pay with mint %s", e.message);
if (e?.message.match(/already spent/i)) {
debug("Proofs already spent, rolling over");
rollOverProofs(selection, [], selection.mint, wallet);
}
throw e;
}

return null;
}
2 changes: 1 addition & 1 deletion ndk-wallet/src/cashu/pay/nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ async function payNutWithMintTransfer(
// get quotes from the mints the recipient has
const quotesPromises = mints.map(async (mint) => {
const wallet = new CashuWallet(new CashuMint(mint), { unit: pay.unit });
const quote = await wallet.mintQuote(amount);
const quote = await wallet.createMintQuote(amount);
return { quote, mint };
});

Expand Down
4 changes: 2 additions & 2 deletions ndk-wallet/src/cashu/proofs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function chooseProofsForPr(
wallet: NDKCashuWallet
): Promise<TokenSelection | undefined> {
const _wallet = new CashuWallet(new CashuMint(mint));
const quote = await _wallet.meltQuote(pr);
const quote = await _wallet.createMeltQuote(pr);
return chooseProofsForQuote(quote, wallet, mint);
}

Expand Down Expand Up @@ -135,7 +135,7 @@ export async function rollOverProofs(
d(
"adding to delete a token that was seen on relay %s %o",
token.relay?.url,
token.onRelays
token.onRelays.map((r) => r.url)
);
deleteEvent.tag(["e", token.id]);
if (token.relay) relaySet?.addRelay(token.relay);
Expand Down
1 change: 0 additions & 1 deletion ndk-wallet/src/cashu/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { decrypt } from "./decrypt";

export function proofsTotalBalance(proofs: Proof[]): number {
for (const proof of proofs) {
console.log("proof", proof.secret);
if (proof.amount < 0) {
throw new Error("proof amount is negative");
}
Expand Down
3 changes: 1 addition & 2 deletions ndk-wallet/src/cashu/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export class NDKCashuWallet extends EventEmitter<NDKWalletEvents> implements NDK
}

get name(): string | undefined {
return this.getPrivateTag("name");
return this.getPrivateTag("name") ?? this.event.tagValue("name");
}

get unit(): string {
Expand Down Expand Up @@ -386,7 +386,6 @@ export class NDKCashuWallet extends EventEmitter<NDKWalletEvents> implements NDK
}

public addToken(token: NDKCashuToken) {
console.trace("adding token", token.id, token.rawEvent());
if (!this.knownTokens.has(token.id)) {
this.knownTokens.add(token.id);
this.tokens.push(token);
Expand Down
12 changes: 0 additions & 12 deletions ndk-wallet/src/service/lifecycle/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,8 @@ class NDKWalletLifecycle extends EventEmitter<{
}

private eoseHandler() {
this.debug("Loaded wallets", {
defaultWallet: this.defaultWallet?.event?.rawEvent(),
wallets: Array.from(this.wallets.values()).map((w) => w.event?.rawEvent()),
});
this.eosed = true;

if (this.tokensSub) {
this.debug("WE ALREADY HAVE TOKENS SUB!!!");
return;
}

// if we don't have a default wallet, choose the first one if there is one
const firstWallet = Array.from(this.wallets.values())[0];
if (!this.defaultWallet && firstWallet) {
Expand All @@ -152,14 +143,11 @@ class NDKWalletLifecycle extends EventEmitter<{
for (const wallet of this.wallets.values()) {
if (!oldestWalletTimestamp || wallet.event.created_at! > oldestWalletTimestamp) {
oldestWalletTimestamp = wallet.event.created_at!;
this.debug("oldest wallet timestamp", oldestWalletTimestamp);
}

this.emit("wallet", wallet);
}

this.debug("oldest wallet timestamp", oldestWalletTimestamp, this.wallets.values());

this.tokensSub = this.ndk.subscribe(
[
{ kinds: [NDKKind.CashuToken], authors: [this.user!.pubkey] },
Expand Down
3 changes: 0 additions & 3 deletions ndk-wallet/src/service/lifecycle/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import type NDKWalletLifecycle from ".";
import type { NDKCashuWallet } from "../../cashu/wallet";

async function handleToken(this: NDKWalletLifecycle, event: NDKEvent, relay?: NDKRelay) {
this.debug("Received token event %s from %s", event.id, relay?.url);

if (this.knownTokens.has(event.id)) return;
this.knownTokens.add(event.id);

Expand All @@ -21,7 +19,6 @@ async function handleToken(this: NDKWalletLifecycle, event: NDKEvent, relay?: ND
this.debug("no wallet found for token %s", token.id);
this.orphanedTokens.set(token.id, token);
} else {
this.debug("adding token %s to wallet %s", token.id, wallet.name);
wallet.addToken(token);
}
}
Expand Down
Loading

0 comments on commit 543067b

Please sign in to comment.