From 0061b9a2b0c6050765d92a107f59c354be061238 Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Tue, 14 Nov 2023 23:44:14 +0100 Subject: [PATCH] Support paying of hold lightning invoices --- package.json | 2 +- patches/@cashu+cashu-ts+0.8.2-rc.7.patch | 45 +- src/models/MintsStore.ts | 6 +- src/models/ProofsStore.ts | 37 +- src/screens/BackupScreen.tsx | 15 +- src/screens/LocalRecoveryScreen.tsx | 49 ++- .../PaymentRequestListItem.tsx | 10 +- src/screens/ReceiveScreen.tsx | 14 +- src/screens/SendScreen.tsx | 7 +- src/screens/TopupScreen.tsx | 7 +- src/screens/TranDetailScreen.tsx | 13 +- .../Transactions/TransactionListItem.tsx | 8 +- src/screens/TransferScreen.tsx | 44 +- src/screens/Wallet/CurrencySign.tsx | 41 ++ src/screens/WalletScreen.tsx | 24 +- src/services/cashu/cashuUtils.ts | 4 +- src/services/cashuMintClient.ts | 31 +- src/services/lnurlService.ts | 11 - src/services/nostrService.ts | 4 +- src/services/sqlite.ts | 2 +- src/services/walletService.ts | 399 +++++++++++++----- yarn.lock | 14 +- 22 files changed, 584 insertions(+), 203 deletions(-) create mode 100644 src/screens/Wallet/CurrencySign.tsx diff --git a/package.json b/package.json index 31c4a1e7..521bb91c 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "mobx": "^6.9.0", "mobx-react-lite": "^3.4.3", "mobx-state-tree": "^5.1.8", - "nostr-tools": "^1.14.0", + "nostr-tools": "^1.17.0", "numbro": "^2.3.6", "patch-package": "^7.0.0", "postinstall-postinstall": "^2.1.0", diff --git a/patches/@cashu+cashu-ts+0.8.2-rc.7.patch b/patches/@cashu+cashu-ts+0.8.2-rc.7.patch index a9014ef7..189e4213 100644 --- a/patches/@cashu+cashu-ts+0.8.2-rc.7.patch +++ b/patches/@cashu+cashu-ts+0.8.2-rc.7.patch @@ -281,7 +281,7 @@ index f6bc3b1..0bfe1dc 100644 method: 'POST', requestBody: restorePayload diff --git a/node_modules/@cashu/cashu-ts/src/CashuWallet.ts b/node_modules/@cashu/cashu-ts/src/CashuWallet.ts -index b7cf354..d3e9f0a 100644 +index b7cf354..d0a9af2 100644 --- a/node_modules/@cashu/cashu-ts/src/CashuWallet.ts +++ b/node_modules/@cashu/cashu-ts/src/CashuWallet.ts @@ -1,7 +1,7 @@ @@ -315,6 +315,37 @@ index b7cf354..d3e9f0a 100644 /** * Class that represents a Cashu wallet. +@@ -69,13 +69,16 @@ class CashuWallet { + * @param proofs (only the 'secret' field is required) + * @returns + */ +- async checkProofsSpent(proofs: Array): Promise> { ++ async checkProofsSpent(proofs: Array): Promise<{spent: Array, pending: Array}> { + const payload = { + //send only the secret + proofs: proofs.map((p) => ({ secret: p.secret })) + }; +- const { spendable } = await this.mint.check(payload); +- return proofs.filter((_, i) => !spendable[i]); ++ const { spendable, pending: isPending } = await this.mint.check(payload); ++ ++ const spent = proofs.filter((_, i) => !spendable[i]); ++ const pending = isPending && Array.isArray(isPending) ? proofs.filter((_, i) => isPending[i]) : []; ++ return {spent, pending} + } + /** + * Starts a minting process by requesting an invoice from the mint +@@ -512,8 +515,8 @@ class CashuWallet { + const secrets: Array = []; + const rs: Array = []; + for (let i = 0; i < amounts.length; i++) { +- let deterministicR = undefined; +- let secret = undefined; ++ let deterministicR: bigint | undefined = undefined; ++ let secret: Uint8Array | undefined = undefined; + if (this._seed && count != undefined) { + secret = deriveSecret(this._seed, keysetId??this.keysetId, count + i); + deterministicR = bytesToNumber(deriveBlindingFactor(this._seed, keysetId??this.keysetId, count + i)); diff --git a/node_modules/@cashu/cashu-ts/src/DHKE.ts b/node_modules/@cashu/cashu-ts/src/DHKE.ts index ca37559..02843b5 100644 --- a/node_modules/@cashu/cashu-ts/src/DHKE.ts @@ -352,6 +383,18 @@ index b1f2c00..138c251 100644 export { CashuMint, +diff --git a/node_modules/@cashu/cashu-ts/src/model/types/index.ts b/node_modules/@cashu/cashu-ts/src/model/types/index.ts +index 4855c33..4546acc 100644 +--- a/node_modules/@cashu/cashu-ts/src/model/types/index.ts ++++ b/node_modules/@cashu/cashu-ts/src/model/types/index.ts +@@ -215,6 +215,7 @@ export type CheckSpendableResponse = { + * Ordered list for checked proofs. True if the secret has not been redeemed at the mint before + */ + spendable: Array; ++ pending?: Array; + } & ApiError; + /** + * blinded message for sending to the mint diff --git a/node_modules/@cashu/cashu-ts/src/secrets.ts b/node_modules/@cashu/cashu-ts/src/secrets.ts index ce70d91..4ebe676 100644 --- a/node_modules/@cashu/cashu-ts/src/secrets.ts diff --git a/src/models/MintsStore.ts b/src/models/MintsStore.ts index 57bb9658..2c57ddd8 100644 --- a/src/models/MintsStore.ts +++ b/src/models/MintsStore.ts @@ -17,7 +17,7 @@ export type MintsByHostname = { export const MintsStoreModel = types .model('MintsStore', { mints: types.array(MintModel), - blockedMintUrls: types.array(types.string), + blockedMintUrls: types.array(types.string), }) .views(self => ({ findByUrl: (mintUrl: string | URL) => { @@ -66,6 +66,10 @@ export const MintsStoreModel = types } }, blockMint(mintToBeBlocked: Mint) { + if(self.blockedMintUrls.some(url => url === mintToBeBlocked.mintUrl)) { + return + } + self.blockedMintUrls.push(mintToBeBlocked.mintUrl) log.debug('[blockMint]', 'Mint blocked in MintsStore') }, diff --git a/src/models/ProofsStore.ts b/src/models/ProofsStore.ts index 1c24bce6..ceb7c0bf 100644 --- a/src/models/ProofsStore.ts +++ b/src/models/ProofsStore.ts @@ -17,6 +17,7 @@ export const ProofsStoreModel = types .model('Proofs', { proofs: types.array(ProofModel), pendingProofs: types.array(ProofModel), + pendingByMintSecrets: types.array(types.string), }) .actions(withSetPropAction) .views(self => ({ @@ -55,23 +56,34 @@ export const ProofsStoreModel = types return proofInstance }, - + alreadyExists(proof: Proof, isPending: boolean = false) { + const proofs = isPending ? self.pendingProofs : self.proofs + return proofs.some(p => p.secret === proof.secret) ? true : false + }, })) .actions(self => ({ - addProofs(newProofs: Proof[], isPending: boolean = false) { + addProofs(newProofs: Proof[], isPending: boolean = false): number { try { const proofs = isPending ? self.pendingProofs : self.proofs + let addedAmount: number = 0 + + for (const proof of newProofs) { + if(self.alreadyExists(proof)) { + log.warn('[addProofs]', `${isPending ? ' pending' : ''} proof already exists in the ProofsStore`, proof.secret) + continue + } - for (const proof of newProofs) { if (isStateTreeNode(proof)) { - proofs.push(proof) + proofs.push(proof) } else { const proofInstance = ProofModel.create(proof) proofs.push(proofInstance) } + + addedAmount += proof.amount } - log.debug('[addProofs]', `Added new ${newProofs.length}${isPending ? ' pending' : ''} proofs to ProofsStore`,) + log.debug('[addProofs]', `Added new ${newProofs.length}${isPending ? ' pending' : ''} proofs to the ProofsStore`,) const rootStore = getRootStore(self) const {userSettingsStore} = rootStore @@ -79,6 +91,8 @@ export const ProofsStoreModel = types if (userSettingsStore.isLocalBackupOn === true) { Database.addOrUpdateProofs(newProofs, isPending) // isSpent = false } + + return addedAmount } catch (e: any) { throw new AppError(Err.STORAGE_ERROR, e.message) } @@ -119,6 +133,19 @@ export const ProofsStoreModel = types throw new AppError(Err.STORAGE_ERROR, e.message.toString()) } }, + addToPendingByMint(proof: Proof): boolean { + if(self.pendingByMintSecrets.some(s => s === proof.secret)) { + return false + } + + self.pendingByMintSecrets.push(proof.secret) + log.trace('[addToPendingByMint]', 'Proof marked as pending by mint, secret', proof.secret) + return true + }, + removeFromPendingByMint(proof: Proof) { + self.pendingByMintSecrets.remove(proof.secret) + log.trace('[removeFromPendingByMint]', 'Proof removed from pending by mint, secret', proof.secret) + }, })) .views(self => ({ get proofsCount() { diff --git a/src/screens/BackupScreen.tsx b/src/screens/BackupScreen.tsx index 4ecf7f18..ef258915 100644 --- a/src/screens/BackupScreen.tsx +++ b/src/screens/BackupScreen.tsx @@ -140,7 +140,14 @@ export const BackupScreen: FC> = observer(fun style={$item} onPress={gotoLocalRecovery} /> - )} + )} + + } + /> + > = observer(fun /> } - /> - {isLoading && } + /> + {isLoading && } {} @@ -127,30 +132,46 @@ export const LocalRecoveryScreen: FC = const copyEncodedTokens = function (proofs: BackupProof[]) { try { + setIsLoading(true) const encodedTokens: string[] = [] // TODO group by mints - for (const proof of proofs) { - const mint = getMintForToken(proof, mintsStore.allMints) - const { tId, isPending, isSpent, updatedAt, ...cleanedProof } = proof + for (const mint of mintsStore.allMints) { + + let proofsByMint: CashuProof[] = [] - if(mint) { - const encoded = getEncodedToken({ + for (const proof of proofs) { + + const proofMint = CashuUtils.getMintFromProof(proof, mintsStore.allMints) + const { tId, isPending, isSpent, updatedAt, ...cleanedProof } = proof + + if (!proofMint) { continue } + + if(mint.mintUrl === proofMint.mintUrl) { + proofsByMint.push(cleanedProof) + } + } + + if (proofsByMint.length > 0) { + const tokenByMint = { token: [ { mint: mint.mintUrl, - proofs: [ - cleanedProof - ] + proofs: proofsByMint } ] - }) + } - encodedTokens.push(encoded) + log.trace(tokenByMint) + + const encodedByMint = getEncodedToken(tokenByMint) + encodedTokens.push(encodedByMint) } - } + } - Clipboard.setString(JSON.stringify(encodedTokens)) + Clipboard.setString(JSON.stringify(encodedTokens)) + setIsLoading(false) + } catch (e: any) { setInfo(`Could not copy: ${e.message}`) } @@ -234,7 +255,7 @@ export const LocalRecoveryScreen: FC = ContentComponent={ <> - + } ContentComponent={ diff --git a/src/screens/ReceiveScreen.tsx b/src/screens/ReceiveScreen.tsx index 413c497d..555363cd 100644 --- a/src/screens/ReceiveScreen.tsx +++ b/src/screens/ReceiveScreen.tsx @@ -32,6 +32,7 @@ import {MintListItem} from './Mints/MintListItem' import useIsInternetReachable from '../utils/useIsInternetReachable' import { resolveTxt } from 'dns' import { verticalScale } from '@gocodingnow/rn-size-matters' +import { CurrencyCode, CurrencySign } from './Wallet/CurrencySign' export const ReceiveScreen: FC> = observer( function ReceiveScreen({route, navigation}) { @@ -207,10 +208,8 @@ export const ReceiveScreen: FC> = observer( tx="receiveScreen.toReceive" style={{color: 'white'}} /> - > = observer( tx="receiveScreen.received" style={{color: 'white'}} /> - > = observer( style={{color: 'white'}} /> - > = observer( style={{color: 'white'}} /> - + } /> {isDataParsable && ( @@ -1038,10 +1040,18 @@ const TransferInfoBlock = function (props: { label="tranDetailScreen.lightningFee" value={`${transaction.fee}`} /> + {transaction.memo && ( + )} + {transaction.sentTo && ( + + )} > = observer( }) const amountInputRef = useRef(null) - const {proofsStore, mintsStore, paymentRequestsStore} = useStores() + const {proofsStore, mintsStore, paymentRequestsStore, transactionsStore} = useStores() const [encodedInvoice, setEncodedInvoice] = useState('') const [invoice, setInvoice] = useState() @@ -401,10 +402,17 @@ const transfer = async function () { const {status} = transaction as Transaction setTransactionStatus(status) - if (error) { + if(transaction && lnurlPayParams && lnurlPayParams.address) { + await transactionsStore.updateSentTo( // set ln address to send to to the tx, could be elsewhere // + transaction.id as number, + lnurlPayParams.address as string + ) + } + + if (error) { // This handles timed out pending payments setResultModalInfo({ status, - message: error.message, + message: message || error.message, }) } else { if(!isInvoiceDonation) { // Donation polling triggers own ResultModal on paid invoice @@ -469,10 +477,8 @@ const satsColor = colors.palette.primary200 style={{color: 'white'}} /> - @@ -639,6 +645,26 @@ const satsColor = colors.palette.primary200 )} + {resultModalInfo && + transactionStatus === TransactionStatus.PENDING && ( + <> + + +