mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
Important speedup and promise of improved reliability for zaps sent over NWC and paid from Minibits mint
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "minibits_wallet",
|
||||
"version": "0.1.9-beta.34",
|
||||
"version": "0.1.9-beta.35",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android:clean": "cd android && ./gradlew clean",
|
||||
@@ -19,7 +19,7 @@
|
||||
"postinstall": "patch-package --exclude 'nothing'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cashu/cashu-ts": "minibits-cash/cashu-ts#commit=1a60b7859627829dc29293d78a945095c335de63",
|
||||
"@cashu/cashu-ts": "minibits-cash/cashu-ts#commit=8dcac7ce6d192c419aee5547f0db03c23b593a57",
|
||||
"@exodus/borc": "^2.1.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||
|
||||
+1
-1
@@ -379,7 +379,7 @@ export const MintModel = types
|
||||
// Calculate the fees
|
||||
const feeReserve = Math.max(Math.floor((totalInputFees + 999) / 1000), 0)
|
||||
|
||||
log.debug('[getMintFeeReserve]', {feeReserve})
|
||||
log.trace('[getMintFeeReserve]', {feeReserve})
|
||||
return feeReserve
|
||||
}
|
||||
}))
|
||||
|
||||
+64
-40
@@ -34,6 +34,8 @@ import { roundUp } from '../utils/number'
|
||||
import { PaymentRequest } from './PaymentRequest'
|
||||
import { PaymentRequests } from './PaymentRequestsStore'
|
||||
import { transaction } from 'mobx'
|
||||
import { MINIBITS_MINT_URL } from '@env'
|
||||
import { MintBalance } from './Mint'
|
||||
|
||||
type NwcError = {
|
||||
result_type: string,
|
||||
@@ -75,6 +77,9 @@ const getConnectionRelays = function () {
|
||||
return minibitsRelays
|
||||
}
|
||||
|
||||
const MIN_LIGHTNING_FEE = 2 // sats
|
||||
const LIGHTNING_FEE_PERCENT = 1
|
||||
|
||||
const getSupportedMethods = function () {
|
||||
return [
|
||||
'pay_invoice',
|
||||
@@ -209,7 +214,7 @@ export const NwcConnectionModel = types.model('NwcConnection', {
|
||||
log.debug('[handleTransferTaskResult] Got transfer task result', {
|
||||
connection: self.name,
|
||||
meltQuote: result.meltQuote?.quote,
|
||||
txId: result.transaction?.id
|
||||
txId: result.transaction?.id,
|
||||
})
|
||||
|
||||
if(result.meltQuote?.quote === self.lastMeltQuoteId) {
|
||||
@@ -453,12 +458,18 @@ export const NwcConnectionModel = types.model('NwcConnection', {
|
||||
} as NwcResponse
|
||||
},
|
||||
handlePayInvoice: flow(function* handlePayInvoice(nwcRequest: NwcRequest, requestEvent: NostrEvent) {
|
||||
log.trace('[Nwc.handlePayInvoice] start')
|
||||
log.debug('[Nwc.handlePayInvoice] start')
|
||||
|
||||
try {
|
||||
const encoded = nwcRequest.params.invoice
|
||||
const walletStore = self.getWalletStore()
|
||||
const proofsStore = self.getProofsStore()
|
||||
const proofsStore = self.getProofsStore()
|
||||
|
||||
// reset daily limit if day changed while keeping live connection
|
||||
if(!isSameDay(self.currentDay, new Date())) {
|
||||
self.setRemainingDailyLimit(self.dailyLimit)
|
||||
self.setCurrentDay()
|
||||
}
|
||||
|
||||
const invoice = LightningUtils.decodeInvoice(encoded)
|
||||
|
||||
@@ -470,34 +481,25 @@ export const NwcConnectionModel = types.model('NwcConnection', {
|
||||
} = LightningUtils.getInvoiceData(invoice)
|
||||
|
||||
const invoiceExpiry = addSeconds(new Date(timestamp as number * 1000), expiry as number)
|
||||
|
||||
// Calculated on device to avoid mintQuote call for minibits mint
|
||||
const feeReserve = Math.max(MIN_LIGHTNING_FEE, amountToPay * LIGHTNING_FEE_PERCENT / 100)
|
||||
const totalAmountToPay = amountToPay + feeReserve
|
||||
|
||||
const mintBalance = proofsStore.getMintBalanceWithMaxBalance('sat')
|
||||
let mintBalance: MintBalance | undefined = undefined
|
||||
let isMinibitsMintSelected: boolean = false
|
||||
const minibitsBalance = proofsStore.getMintBalance(MINIBITS_MINT_URL)
|
||||
|
||||
if(minibitsBalance && minibitsBalance.balances.sat! >= totalAmountToPay) {
|
||||
mintBalance = minibitsBalance
|
||||
isMinibitsMintSelected = true
|
||||
} else {
|
||||
mintBalance = proofsStore.getMintBalanceWithMaxBalance('sat')
|
||||
}
|
||||
|
||||
const availableBalanceSat = mintBalance?.balances.sat || 0
|
||||
|
||||
if(!mintBalance || availableBalanceSat < amountToPay) { // decoded amount is in sat
|
||||
const message = `Insufficient balance to pay this invoice`
|
||||
return {
|
||||
result_type: nwcRequest.method,
|
||||
error: { code: 'INSUFFICIENT_BALANCE', message}
|
||||
} as NwcError
|
||||
}
|
||||
|
||||
// melt quote
|
||||
const meltQuote: MeltQuoteResponse = yield walletStore.createLightningMeltQuote(
|
||||
mintBalance.mintUrl,
|
||||
'sat',
|
||||
encoded,
|
||||
)
|
||||
|
||||
const totalAmountToPay = meltQuote.amount + meltQuote.fee_reserve
|
||||
|
||||
// reset daily limit if day changed while keeping live connection
|
||||
if(!isSameDay(self.currentDay, new Date())) {
|
||||
self.setRemainingDailyLimit(self.dailyLimit)
|
||||
self.setCurrentDay()
|
||||
}
|
||||
|
||||
if(availableBalanceSat < totalAmountToPay) {
|
||||
if(!mintBalance || availableBalanceSat < totalAmountToPay) {
|
||||
const message = `Insufficient balance to pay this invoice.`
|
||||
return {
|
||||
result_type: nwcRequest.method,
|
||||
@@ -512,18 +514,38 @@ export const NwcConnectionModel = types.model('NwcConnection', {
|
||||
error: { code: 'QUOTA_EXCEEDED', message}
|
||||
} as NwcError
|
||||
}
|
||||
|
||||
// Jachyme, hod ho do stroje!
|
||||
WalletTask.transfer(
|
||||
mintBalance,
|
||||
amountToPay,
|
||||
'sat',
|
||||
meltQuote,
|
||||
description || '',
|
||||
invoiceExpiry as Date,
|
||||
encoded,
|
||||
requestEvent
|
||||
)
|
||||
|
||||
if(isMinibitsMintSelected) {
|
||||
// Process the payment partly on the server side to avoid android background processing failures
|
||||
WalletTask.nwcTransfer(
|
||||
mintBalance,
|
||||
amountToPay,
|
||||
feeReserve,
|
||||
'sat',
|
||||
description || '',
|
||||
invoiceExpiry as Date,
|
||||
encoded,
|
||||
requestEvent
|
||||
)
|
||||
} else {
|
||||
// Full process on device for other mints so that minibits server does not touch foreign ecash
|
||||
const meltQuote: MeltQuoteResponse = yield walletStore.createLightningMeltQuote(
|
||||
mintBalance.mintUrl,
|
||||
'sat',
|
||||
encoded,
|
||||
)
|
||||
|
||||
WalletTask.transfer(
|
||||
mintBalance,
|
||||
amountToPay,
|
||||
'sat',
|
||||
meltQuote,
|
||||
description || '',
|
||||
invoiceExpiry as Date,
|
||||
encoded,
|
||||
requestEvent
|
||||
)
|
||||
}
|
||||
|
||||
} catch (e: any) {
|
||||
log.error(`[NwcConnection.handlePayInvoice] ${e.message}`)
|
||||
@@ -773,6 +795,7 @@ export const NwcStoreModel = types
|
||||
})
|
||||
|
||||
EventEmitter.on('ev_transferTask_result', self.handleTransferResult)
|
||||
EventEmitter.on('ev_nwcTransferTask_result', self.handleTransferResult)
|
||||
EventEmitter.on('ev_topupTask_result', self.handleTopupResult)
|
||||
|
||||
} catch (e: any) {
|
||||
@@ -794,6 +817,7 @@ export const NwcStoreModel = types
|
||||
yield targetConnection.handleRequest(event)
|
||||
|
||||
EventEmitter.on('ev_transferTask_result', self.handleTransferResult)
|
||||
EventEmitter.on('ev_nwcTransferTask_result', self.handleTransferResult)
|
||||
EventEmitter.on('ev_topupTask_result', self.handleTopupResult)
|
||||
|
||||
})
|
||||
|
||||
@@ -174,7 +174,7 @@ export const ProofsStoreModel = types
|
||||
proofsCounter.increaseProofsCounter(keysetProofs.length)
|
||||
}
|
||||
|
||||
log.info('[addProofs]', `Added new ${addedProofs.length}${isPending ? ' pending' : ''} proofs to the ProofsStore`)
|
||||
log.trace('[addProofs]', `Added new ${addedProofs.length}${isPending ? ' pending' : ''} proofs to the ProofsStore`)
|
||||
|
||||
if (addedProofs.length > 0) {
|
||||
Database.addOrUpdateProofs(addedProofs, isPending) // isSpent = false
|
||||
@@ -210,7 +210,7 @@ export const ProofsStoreModel = types
|
||||
|
||||
proofs.replace(proofs.filter(proof => !proofsToRemove.some(removed => removed.secret === proof.secret)))
|
||||
|
||||
log.debug('[removeProofs]', `${count} ${(isPending) ? 'pending' : ''} proofs removed from ProofsStore`)
|
||||
log.trace('[removeProofs]', `${count} ${(isPending) ? 'pending' : ''} proofs removed from ProofsStore`)
|
||||
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.STORAGE_ERROR, e.message.toString())
|
||||
@@ -411,7 +411,7 @@ export const ProofsStoreModel = types
|
||||
},
|
||||
getMintBalanceWithMaxBalance: (unit: MintUnit) => {
|
||||
const balances = self.getBalances().mintBalances
|
||||
let maxBalance = null;
|
||||
let maxBalance = undefined;
|
||||
let maxAmount = -Infinity;
|
||||
|
||||
for (const balance of balances) {
|
||||
|
||||
@@ -39,6 +39,7 @@ export enum TransactionType {
|
||||
RECEIVE_NOSTR = 'RECEIVE_NOSTR', // not used
|
||||
TOPUP = 'TOPUP',
|
||||
TRANSFER = 'TRANSFER',
|
||||
NWC_TRANSFER = 'NWC_TRANSFER',
|
||||
}
|
||||
|
||||
export enum TransactionStatus {
|
||||
@@ -140,12 +141,12 @@ export const TransactionModel = types
|
||||
setInputToken(inputToken: string) {
|
||||
Database.updateInputToken(self.id!, inputToken)
|
||||
self.inputToken = inputToken
|
||||
log.debug('[setInputToken]', 'Transaction inputToken updated', {id: self.id, inputToken})
|
||||
log.trace('[setInputToken]', 'Transaction inputToken updated', {id: self.id, inputToken})
|
||||
},
|
||||
setOutputToken(outputToken: string) {
|
||||
Database.updateOutputToken(self.id!, outputToken)
|
||||
self.outputToken = outputToken
|
||||
log.trace('[setOutputToken]', 'Transaction outputToken updated', {id: self.id, outputToken})
|
||||
log.trace('[setOutputToken]', 'Transaction outputToken updated', {id: self.id})
|
||||
},
|
||||
setProof(proof: string) {
|
||||
Database.updateProof(self.id!, proof)
|
||||
|
||||
@@ -189,7 +189,7 @@ export const TransactionsStoreModel = types
|
||||
self.history.unshift(reference!)
|
||||
self.recentByUnit.unshift(reference!)
|
||||
|
||||
log.debug('[addTransaction]', 'New transaction added to the TransactionsStore', {id})
|
||||
// log.trace('[addTransaction]', 'New transaction added to the TransactionsStore', {id})
|
||||
|
||||
// Purge the oldest references from cache, but keep some for each mint
|
||||
self.pruneRecentByUnit(newTransaction.unit)
|
||||
|
||||
+10
-12
@@ -214,8 +214,7 @@ export const WalletStoreModel = types
|
||||
keysetId?: string
|
||||
withSeed: boolean
|
||||
}
|
||||
) {
|
||||
log.trace('[WalletStore.getWallet] start', {mintUrl})
|
||||
) {
|
||||
// syncs mint model in wallet state and returns cashu-ts mint class instance
|
||||
const cashuMint = yield self.getMint(mintUrl)
|
||||
|
||||
@@ -315,8 +314,7 @@ export const WalletStoreModel = types
|
||||
log.trace('[WalletStore.getWallet]', 'Returning NEW cashuWallet instance', {mintUrl})
|
||||
return newWallet
|
||||
}),
|
||||
getMintKeysets: flow(function* getMintKeysets(mintUrl: string) {
|
||||
log.trace('[getMintKeysets] start')
|
||||
getMintKeysets: flow(function* getMintKeysets(mintUrl: string) {
|
||||
const cashuMint: CashuMint = yield self.getMint(mintUrl)
|
||||
|
||||
try {
|
||||
@@ -350,7 +348,7 @@ export const WalletStoreModel = types
|
||||
mintUrl: string,
|
||||
unit: MintUnit,
|
||||
decodedToken: TokenV3,
|
||||
mintFeeReserve: number,
|
||||
swapFeeReserve: number,
|
||||
options: {
|
||||
preference: AmountPreference[],
|
||||
counter: number
|
||||
@@ -368,7 +366,7 @@ export const WalletStoreModel = types
|
||||
|
||||
const proofs = yield cashuWallet.receive(
|
||||
decodedToken,
|
||||
mintFeeReserve,
|
||||
swapFeeReserve,
|
||||
{
|
||||
keysetId: cashuWallet.keys.id,
|
||||
preference: options.preference,
|
||||
@@ -378,9 +376,9 @@ export const WalletStoreModel = types
|
||||
})
|
||||
|
||||
const receivedAmount: number = CashuUtils.getProofsAmount(proofs as Proof[])
|
||||
const mintFeePaid = amountToReceive - receivedAmount
|
||||
const swapFeePaid = amountToReceive - receivedAmount
|
||||
|
||||
return {proofs, mintFeePaid}
|
||||
return {proofs, swapFeePaid}
|
||||
} catch (e: any) {
|
||||
throw new AppError(
|
||||
Err.MINT_ERROR,
|
||||
@@ -391,7 +389,7 @@ export const WalletStoreModel = types
|
||||
}),
|
||||
send: flow(function* send(mintUrl: string,
|
||||
amountToSend: number,
|
||||
mintFeeReserve: number,
|
||||
swapFeeReserve: number,
|
||||
unit: MintUnit,
|
||||
proofsToSendFrom: Proof[],
|
||||
options: {
|
||||
@@ -410,7 +408,7 @@ export const WalletStoreModel = types
|
||||
|
||||
const {returnChange, send} = yield cashuWallet.send(
|
||||
amountToSend,
|
||||
mintFeeReserve,
|
||||
swapFeeReserve,
|
||||
proofsToSendFrom,
|
||||
{
|
||||
keysetId: cashuWallet.keys.id,
|
||||
@@ -436,13 +434,13 @@ export const WalletStoreModel = types
|
||||
)
|
||||
}
|
||||
|
||||
const mintFeePaid = totalAmountToSendFrom - amountToSend - returnedAmount
|
||||
const swapFeePaid = totalAmountToSendFrom - amountToSend - returnedAmount
|
||||
|
||||
// we normalize naming of returned parameters
|
||||
return {
|
||||
returnedProofs: returnChange as Proof[],
|
||||
proofsToSend: send as Proof[],
|
||||
mintFeePaid
|
||||
swapFeePaid
|
||||
}
|
||||
} catch (e: any) {
|
||||
let message = 'The mint could not return signatures necessary for this transaction'
|
||||
|
||||
@@ -185,7 +185,7 @@ export const TranDetailScreen: FC<TransactionsStackScreenProps<'TranDetail'>> =
|
||||
return `-${formatCurrency(transaction.amount, getCurrency(transaction.unit).code)}`
|
||||
case TransactionType.TOPUP:
|
||||
return `+${formatCurrency(transaction.amount, getCurrency(transaction.unit).code)}`
|
||||
case TransactionType.TRANSFER:
|
||||
case TransactionType.TRANSFER || TransactionType.NWC_TRANSFER:
|
||||
return `-${formatCurrency(transaction.amount, getCurrency(transaction.unit).code)}`
|
||||
default:
|
||||
return `${formatCurrency(transaction.amount, getCurrency(transaction.unit).code)}`
|
||||
@@ -305,6 +305,14 @@ export const TranDetailScreen: FC<TransactionsStackScreenProps<'TranDetail'>> =
|
||||
colorScheme={colorScheme}
|
||||
/>
|
||||
)}
|
||||
{transaction.type === TransactionType.NWC_TRANSFER && (
|
||||
<TransferInfoBlock
|
||||
transaction={transaction}
|
||||
isDataParsable={isDataParsable}
|
||||
mint={mint}
|
||||
colorScheme={colorScheme}
|
||||
/>
|
||||
)}
|
||||
{isDataParsable && (
|
||||
<Card
|
||||
labelTx='tranDetailScreen.auditTrail'
|
||||
@@ -652,6 +660,7 @@ const ReceiveInfoBlock = function (props: {
|
||||
<>
|
||||
<Card
|
||||
style={$dataCard}
|
||||
label='Transaction data'
|
||||
ContentComponent={
|
||||
<>
|
||||
<TranItem
|
||||
@@ -891,6 +900,7 @@ const ReceiveOfflineInfoBlock = function (props: {
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
label='Transaction data'
|
||||
style={$dataCard}
|
||||
ContentComponent={
|
||||
<>
|
||||
@@ -1124,6 +1134,7 @@ const SendInfoBlock = function (props: {
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
label='Transaction data'
|
||||
style={$dataCard}
|
||||
ContentComponent={
|
||||
<>
|
||||
@@ -1338,6 +1349,7 @@ const TopupInfoBlock = function (props: {
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
label='Transaction data'
|
||||
style={$dataCard}
|
||||
ContentComponent={
|
||||
<>
|
||||
@@ -1610,12 +1622,14 @@ const TransferInfoBlock = function (props: {
|
||||
label="tranDetailScreen.type"
|
||||
value={transaction.type as string}
|
||||
/>
|
||||
<TranItem
|
||||
label="tranDetailScreen.fee"
|
||||
value={transaction.fee}
|
||||
unit={transaction.unit}
|
||||
isCurrency={true}
|
||||
/>
|
||||
{transaction.status === TransactionStatus.COMPLETED && (
|
||||
<TranItem
|
||||
label="tranDetailScreen.fee"
|
||||
value={transaction.fee}
|
||||
unit={transaction.unit}
|
||||
isCurrency={true}
|
||||
/>
|
||||
)}
|
||||
{transaction.status === TransactionStatus.PREPARED ? (
|
||||
<View
|
||||
style={{flexDirection: 'row', justifyContent: 'space-between'}}
|
||||
|
||||
@@ -67,7 +67,7 @@ export const TransactionListItem = observer(function (
|
||||
: tx.sentFrom
|
||||
? translate('transactionCommon.receivedFrom', {sender: getProfileName(tx.sentFrom)}).slice(0, 30)
|
||||
: translate('transactionCommon.youReceived')).slice(0, 30)
|
||||
case TransactionType.TRANSFER:
|
||||
case TransactionType.TRANSFER || TransactionType.NWC_TRANSFER:
|
||||
return (tx.memo && tx.memo !== 'LNbits'
|
||||
? tx.memo.slice(0, 30)
|
||||
: tx.sentTo
|
||||
@@ -234,7 +234,7 @@ export const TransactionListItem = observer(function (
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{([TransactionType.SEND, TransactionType.TRANSFER].includes(tx.type)) && (
|
||||
{([TransactionType.SEND, TransactionType.TRANSFER, TransactionType.NWC_TRANSFER].includes(tx.type)) && (
|
||||
<CurrencyAmount
|
||||
amount={-1 * tx.amount}
|
||||
mintUnit={tx.unit}
|
||||
|
||||
@@ -366,8 +366,13 @@ const findExactMatch = function (requestedAmount: number, proofs: Proof[]): Proo
|
||||
return null;
|
||||
}
|
||||
|
||||
const findMinExcess = function (requestedAmount: number, proofs: Proof[]): Proof[] {
|
||||
proofs.sort((a, b) => b.amount - a.amount);
|
||||
const findMinExcess = function (requestedAmount: number, proofs: Proof[], preference: 'SMALL' | 'BIG' = 'SMALL'): Proof[] {
|
||||
if(preference === 'SMALL') {
|
||||
proofs.sort((a, b) => a.amount - b.amount);
|
||||
} else {
|
||||
proofs.sort((a, b) => b.amount - a.amount);
|
||||
}
|
||||
|
||||
const selectedProofs: Proof[] = [];
|
||||
let currentAmount = 0;
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ import {
|
||||
} from '@env'
|
||||
import { WalletProfile, WalletProfileRecord } from "../models/WalletProfileStore"
|
||||
import { CurrencyCode } from "./wallet/currency"
|
||||
import { ProofV3, TokenV3 } from "./cashu/cashuUtils"
|
||||
import { NostrEvent } from "./nostrService"
|
||||
import { MeltQuoteResponse, MeltQuoteState } from "@cashu/cashu-ts"
|
||||
|
||||
|
||||
type MinibitsRequestArgs = {
|
||||
@@ -255,6 +258,29 @@ const checkDonationPaid = async function (paymentHash: string, pubkey: string) {
|
||||
}
|
||||
|
||||
|
||||
const payNwcTransfer = async function (encodedInvoice: string, tokenToPayFrom: TokenV3) {
|
||||
const url = MINIBITS_SERVER_API_HOST + '/payment/payInvoice'
|
||||
const method = 'POST'
|
||||
|
||||
const body = {
|
||||
encodedInvoice,
|
||||
tokenToPayFrom,
|
||||
}
|
||||
|
||||
const response: {
|
||||
meltQuote: MeltQuoteResponse,
|
||||
tokenToReturn?: TokenV3
|
||||
} = await fetchApi(url, {
|
||||
method,
|
||||
body
|
||||
})
|
||||
|
||||
log.debug(`[minibitsClient.payNwcTransfer] Got response`, {response})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
const createClaim = async function (walletId: string, seedHash: string, pubkey: string, batchFrom?: number) {
|
||||
const url = MINIBITS_SERVER_API_HOST + '/claim'
|
||||
const method = 'POST'
|
||||
@@ -362,6 +388,7 @@ export const MinibitsClient = {
|
||||
createDonation,
|
||||
checkDonationPaid,
|
||||
createClaim,
|
||||
payNwcTransfer,
|
||||
getExchangeRate,
|
||||
getPublicHeaders,
|
||||
fetchApi,
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
import {CashuUtils, ProofV3, TokenEntryV3} from '../cashu/cashuUtils'
|
||||
import AppError, {Err} from '../../utils/AppError'
|
||||
import {MeltQuoteResponse, MeltQuoteState} from '@cashu/cashu-ts'
|
||||
import {rootStoreInstance} from '../../models'
|
||||
import { TransactionTaskResult, WalletTask } from '../walletService'
|
||||
import { MintBalance, MintProofsCounter } from '../../models/Mint'
|
||||
import { Proof } from '../../models/Proof'
|
||||
import { Transaction, TransactionData, TransactionStatus, TransactionType } from '../../models/Transaction'
|
||||
import { log } from '../logService'
|
||||
import { WalletUtils } from './utils'
|
||||
import {isBefore} from 'date-fns'
|
||||
import { MintUnit, formatCurrency, getCurrency } from './currency'
|
||||
import { NostrEvent } from '../nostrService'
|
||||
import { MinibitsClient } from '../minibitsService'
|
||||
import { getSnapshot } from 'mobx-state-tree'
|
||||
import { MINIBITS_MINT_URL } from '@env'
|
||||
import { receiveSync } from './receiveTask'
|
||||
|
||||
const {
|
||||
transactionsStore,
|
||||
mintsStore,
|
||||
proofsStore,
|
||||
} = rootStoreInstance
|
||||
|
||||
const NWC_TRANSFER = 'nwcTransferTask'
|
||||
|
||||
export const nwcTransferTask = async function (
|
||||
mintBalanceToTransferFrom: MintBalance,
|
||||
amountToTransfer: number,
|
||||
feeReserve: number,
|
||||
unit: MintUnit,
|
||||
memo: string,
|
||||
invoiceExpiry: Date,
|
||||
encodedInvoice: string,
|
||||
nwcEvent: NostrEvent
|
||||
) : Promise<TransactionTaskResult> {
|
||||
|
||||
const mintUrl = mintBalanceToTransferFrom.mintUrl
|
||||
const mintInstance = mintsStore.findByUrl(mintUrl)
|
||||
|
||||
if(mintUrl !== MINIBITS_MINT_URL) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
'Payment of NWC invoice on the Minibits server is supported only for ecash issued by Minibits mint.',
|
||||
{mintBalanceToTransferFrom}
|
||||
)
|
||||
}
|
||||
|
||||
// TODO refresh - balance might be outdated if it waits in queue before other txs
|
||||
log.debug('[nwcTransfer]', 'mintBalanceToTransferFrom', {mintBalanceToTransferFrom})
|
||||
log.debug('[nwcTransfer]', 'amountToTransfer', {amountToTransfer, feeReserve})
|
||||
|
||||
// create draft transaction
|
||||
const transactionData: TransactionData[] = [
|
||||
{
|
||||
status: TransactionStatus.DRAFT,
|
||||
mintBalanceToTransferFrom,
|
||||
amountToTransfer,
|
||||
feeReserve,
|
||||
unit,
|
||||
encodedInvoice,
|
||||
isNwc: nwcEvent ? true : false,
|
||||
createdAt: new Date(),
|
||||
}
|
||||
]
|
||||
|
||||
let transaction: Transaction | undefined = undefined
|
||||
let meltQuote: MeltQuoteResponse | undefined = undefined
|
||||
let proofsToMeltFrom: Proof[] = []
|
||||
let proofsToMeltFromAmount: number = 0
|
||||
|
||||
try {
|
||||
const newTransaction = {
|
||||
type: TransactionType.NWC_TRANSFER,
|
||||
amount: amountToTransfer,
|
||||
fee: feeReserve,
|
||||
unit,
|
||||
data: JSON.stringify(transactionData),
|
||||
memo,
|
||||
mint: mintBalanceToTransferFrom.mintUrl,
|
||||
status: TransactionStatus.DRAFT,
|
||||
}
|
||||
|
||||
// store tx in db and in the model
|
||||
transaction = await transactionsStore.addTransaction(newTransaction)
|
||||
const transactionId = transaction.id
|
||||
|
||||
if (amountToTransfer + feeReserve > mintBalanceToTransferFrom.balances[unit]!) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
'Mint balance is insufficient to cover the amount to transfer with the expected Lightning fees.',
|
||||
{transactionId}
|
||||
)
|
||||
}
|
||||
|
||||
if(isBefore(invoiceExpiry, new Date())) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
'This invoice has already expired and can not be paid.',
|
||||
{invoiceExpiry, transactionId}
|
||||
)
|
||||
}
|
||||
|
||||
if (!mintInstance) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
'Could not find mint', {mintUrl, transactionId}
|
||||
)
|
||||
}
|
||||
|
||||
// calculate fees charged by mint for melt transaction to prepare enough proofs
|
||||
const proofsFromMint = proofsStore.getByMint(mintUrl, {isPending: false, unit})
|
||||
const totalAmountFromMint = CashuUtils.getProofsAmount(proofsFromMint)
|
||||
|
||||
let proofsToMelt = CashuUtils.getProofsToSend(
|
||||
amountToTransfer + feeReserve,
|
||||
proofsFromMint
|
||||
)
|
||||
|
||||
let meltFeeReserve = mintInstance.getMintFeeReserve(proofsToMelt)
|
||||
const amountWithFees = amountToTransfer + feeReserve + meltFeeReserve
|
||||
|
||||
if (totalAmountFromMint < amountWithFees) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
'There is not enough funds to send this amount.',
|
||||
{totalAmountFromMint, amountWithFees, transactionId, caller: 'nwcTransferTask'},
|
||||
)
|
||||
}
|
||||
|
||||
// exact match or min number of proofs that matches the amount
|
||||
proofsToMeltFrom = CashuUtils.getProofsToSend(
|
||||
amountWithFees,
|
||||
proofsFromMint
|
||||
)
|
||||
|
||||
proofsToMeltFromAmount = CashuUtils.getProofsAmount(proofsToMeltFrom)
|
||||
|
||||
proofsStore.removeProofs(proofsToMeltFrom)
|
||||
WalletUtils.addCashuProofs(
|
||||
mintUrl,
|
||||
proofsToMeltFrom,
|
||||
{
|
||||
unit,
|
||||
transactionId: transaction.id,
|
||||
isPending: true
|
||||
}
|
||||
)
|
||||
|
||||
log.trace('[nwcTransfer]', 'Prepared proofsToMeltFrom proofs', {
|
||||
proofsToMeltFromAmount,
|
||||
transactionId,
|
||||
unit,
|
||||
})
|
||||
|
||||
// Update transaction status
|
||||
transactionData.push({
|
||||
status: TransactionStatus.PREPARED,
|
||||
proofsToMeltFromAmount,
|
||||
lightningFeeReserve: feeReserve,
|
||||
meltFeeReserve,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.PREPARED,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
|
||||
const cleanedproofsToMeltFrom = proofsToMeltFrom.map(proof => {
|
||||
const {mintUrl, unit, tId, ...rest} = getSnapshot(proof)
|
||||
return rest
|
||||
})
|
||||
|
||||
const inputTokenEntry: TokenEntryV3 = {
|
||||
mint: mintUrl,
|
||||
proofs: cleanedproofsToMeltFrom,
|
||||
}
|
||||
|
||||
const tokenToPayFrom = {
|
||||
token: [inputTokenEntry],
|
||||
unit
|
||||
}
|
||||
|
||||
const inputToken = CashuUtils.encodeToken(tokenToPayFrom)
|
||||
|
||||
transaction.setInputToken(inputToken)
|
||||
|
||||
const result = await MinibitsClient.payNwcTransfer(
|
||||
encodedInvoice,
|
||||
{
|
||||
token: [inputTokenEntry],
|
||||
unit
|
||||
}
|
||||
)
|
||||
|
||||
meltQuote = result.meltQuote
|
||||
const tokenToReturn = result.tokenToReturn // this is server wallet ecash, needs swap
|
||||
|
||||
if (meltQuote.state === MeltQuoteState.PAID) {
|
||||
|
||||
log.debug('[nwcTransfer] Invoice PAID', {
|
||||
transactionId
|
||||
})
|
||||
|
||||
// Save preimage asap
|
||||
if(meltQuote.payment_preimage) {
|
||||
transaction.setProof(meltQuote.payment_preimage)
|
||||
}
|
||||
|
||||
// If noting was returned, all reserves were spent on fees
|
||||
let totalFeePaid = proofsToMeltFromAmount - amountToTransfer
|
||||
let lightningFeePaid = totalFeePaid - meltFeeReserve
|
||||
|
||||
let returnedAmount = 0
|
||||
|
||||
if (tokenToReturn) {
|
||||
// Save in case receive fails
|
||||
const outputToken = CashuUtils.encodeToken(tokenToReturn)
|
||||
transaction.setOutputToken(outputToken)
|
||||
|
||||
// Swap received feeSaved proofs as they were issued to the server wallet thus are not linked to wallet seed
|
||||
const {
|
||||
receivedProofs: returnedProofs
|
||||
} = await receiveSync(
|
||||
mintUrl,
|
||||
tokenToReturn,
|
||||
memo,
|
||||
transactionId
|
||||
)
|
||||
|
||||
// Spend pending proofs that were used to settle the lightning invoice
|
||||
proofsStore.removeProofs(proofsToMeltFrom as Proof[], true, false)
|
||||
returnedAmount = CashuUtils.getProofsAmount(returnedProofs)
|
||||
|
||||
const returnedTokenEntry: TokenEntryV3 = {
|
||||
mint: mintUrl,
|
||||
proofs: returnedProofs,
|
||||
}
|
||||
|
||||
const swappedOutputToken = CashuUtils.encodeToken({
|
||||
token: [returnedTokenEntry],
|
||||
unit,
|
||||
})
|
||||
|
||||
transaction.setOutputToken(swappedOutputToken)
|
||||
|
||||
totalFeePaid = totalFeePaid - returnedAmount
|
||||
lightningFeePaid = totalFeePaid - meltFeeReserve
|
||||
} else {
|
||||
// Spend pending proofs that were used to settle the lightning invoice
|
||||
proofsStore.removeProofs(proofsToMeltFrom as Proof[], true, false)
|
||||
}
|
||||
|
||||
// Save final fee in db
|
||||
if(totalFeePaid !== transaction.fee) {
|
||||
transaction.setFee(totalFeePaid)
|
||||
}
|
||||
|
||||
// Update transaction status
|
||||
transactionData.push({
|
||||
status: TransactionStatus.COMPLETED,
|
||||
lightningFeeReserve: meltQuote.fee_reserve,
|
||||
lightningFeePaid,
|
||||
mintFeesPaid: totalFeePaid - lightningFeePaid,
|
||||
preimage: meltQuote.payment_preimage,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.COMPLETED,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
|
||||
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance!
|
||||
transaction.setBalanceAfter(balanceAfter)
|
||||
|
||||
return {
|
||||
taskFunction: NWC_TRANSFER,
|
||||
mintUrl,
|
||||
transaction,
|
||||
message: `Lightning invoice has been successfully paid and settled with your Minibits ecash. Fee has been ${formatCurrency(transaction.fee, getCurrency(unit).code)} ${getCurrency(unit).code}.`,
|
||||
lightningFeePaid,
|
||||
mintFees: totalFeePaid - lightningFeePaid,
|
||||
totalFeePaid,
|
||||
meltQuote,
|
||||
preimage: meltQuote.payment_preimage,
|
||||
nwcEvent
|
||||
} as TransactionTaskResult
|
||||
|
||||
} else if(meltQuote.state === MeltQuoteState.PENDING) {
|
||||
|
||||
log.debug('[transfer] Invoice PENDING', {
|
||||
meltQuote,
|
||||
transactionId
|
||||
})
|
||||
|
||||
return {
|
||||
taskFunction: NWC_TRANSFER,
|
||||
mintUrl,
|
||||
transaction,
|
||||
message: `Lightning payment did not complete in time. Your ecash will remain pending until the payment completes or fails.`,
|
||||
meltQuote,
|
||||
nwcEvent
|
||||
} as TransactionTaskResult
|
||||
|
||||
} else {
|
||||
// throw so that proper state of proofs is synced inside the catch block
|
||||
throw new AppError(Err.MINT_ERROR, 'Lightning payment has not been paid.', {
|
||||
state: meltQuote.state,
|
||||
transactionId
|
||||
})
|
||||
}
|
||||
|
||||
} catch (e: any) {
|
||||
if (transaction) {
|
||||
|
||||
let message = e.message
|
||||
|
||||
if (proofsToMeltFrom.length > 0) {
|
||||
// check with the mint the real status of the proofs involved in transaction
|
||||
await WalletTask.syncStateWithMintSync(
|
||||
{
|
||||
proofsToSync: proofsToMeltFrom,
|
||||
mintUrl,
|
||||
isPending: true
|
||||
}
|
||||
)
|
||||
|
||||
// force refresh just in case above method did not update the model?
|
||||
const refreshed = transactionsStore.findById(transaction.id)
|
||||
|
||||
if(refreshed?.status === TransactionStatus.PENDING) {
|
||||
log.warn('[transfer]', 'proofsToPay from transfer with error are pending by mint', {
|
||||
proofsToMeltFromAmount,
|
||||
unit,
|
||||
transactionId: transaction.id
|
||||
})
|
||||
|
||||
message = 'Lightning payment did not complete in time. Your ecash will remain pending until the payment completes or fails.'
|
||||
|
||||
return {
|
||||
taskFunction: NWC_TRANSFER,
|
||||
mintUrl,
|
||||
transaction,
|
||||
message,
|
||||
meltQuote,
|
||||
nwcEvent
|
||||
} as TransactionTaskResult
|
||||
|
||||
} else if(refreshed?.status === TransactionStatus.COMPLETED) {
|
||||
// Likely receiving of change failed due to wallet error. We keep completed as status.
|
||||
log.error('[nwcTransfer]', 'NWC Transfer throwed error but the payment suceeded', {
|
||||
error: e.message,
|
||||
proofsToMeltFromAmount,
|
||||
unit,
|
||||
transactionId: transaction.id
|
||||
})
|
||||
|
||||
if(meltQuote?.payment_preimage) {
|
||||
refreshed.setProof(meltQuote?.payment_preimage)
|
||||
}
|
||||
|
||||
const message = `Lightning invoice has been successfully paid, however some error occured: ${e.message}`
|
||||
|
||||
return {
|
||||
taskFunction: NWC_TRANSFER,
|
||||
mintUrl,
|
||||
transaction,
|
||||
message,
|
||||
meltQuote,
|
||||
preimage: meltQuote?.payment_preimage,
|
||||
nwcEvent
|
||||
} as TransactionTaskResult
|
||||
|
||||
} else {
|
||||
// syncStateWithMintSync returns to spendable only proofs that were pending by mint before
|
||||
// so we need to take care of our transfer here.
|
||||
|
||||
log.warn('[transfer]', 'proofsToPay from transfer with error to be returned to spendable wallet', {
|
||||
proofsToMeltFromAmount,
|
||||
unit,
|
||||
transactionId: transaction.id
|
||||
})
|
||||
|
||||
// remove it from pending proofs in the wallet
|
||||
proofsStore.removeProofs(proofsToMeltFrom as Proof[], true, true)
|
||||
// add proofs back to the spendable wallet with internal references
|
||||
WalletUtils.addCashuProofs(
|
||||
mintUrl,
|
||||
proofsToMeltFrom,
|
||||
{
|
||||
unit,
|
||||
transactionId: transaction.id,
|
||||
isPending: false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
transactionData.push({
|
||||
status: TransactionStatus.ERROR,
|
||||
error: WalletUtils.formatError(e),
|
||||
createdAt: new Date()
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.ERROR,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
}
|
||||
|
||||
log.error('[transfer]', e.name, e.message, e.params)
|
||||
|
||||
return {
|
||||
taskFunction: NWC_TRANSFER,
|
||||
mintUrl,
|
||||
transaction,
|
||||
message: e.message,
|
||||
error: WalletUtils.formatError(e),
|
||||
meltQuote,
|
||||
nwcEvent
|
||||
} as TransactionTaskResult
|
||||
}
|
||||
}
|
||||
+151
-195
@@ -37,8 +37,7 @@ export const receiveTask = async function (
|
||||
const transactionData: TransactionData[] = []
|
||||
let transaction: Transaction | undefined = undefined
|
||||
let mintToReceive: string | undefined = undefined
|
||||
const unit = token.unit as MintUnit || 'sat'
|
||||
let lockedProofsCounter: MintProofsCounter | undefined = undefined
|
||||
const unit = token.unit as MintUnit || 'sat'
|
||||
|
||||
try {
|
||||
const tokenMints: string[] = CashuUtils.getMintsFromToken(token)
|
||||
@@ -103,94 +102,19 @@ export const receiveTask = async function (
|
||||
} as TransactionTaskResult
|
||||
}
|
||||
|
||||
// Handle missing mint, we add it automatically
|
||||
const alreadyExists = mintsStore.alreadyExists(mintToReceive)
|
||||
|
||||
if (!alreadyExists) {
|
||||
await mintsStore.addMint(mintToReceive)
|
||||
}
|
||||
|
||||
const mintInstance = mintsStore.findByUrl(mintToReceive)
|
||||
|
||||
if(!mintInstance) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Missing mint', {mintToReceive})
|
||||
}
|
||||
|
||||
// Increase the proofs counter before the mint call so that in case the response
|
||||
// is not received our recovery index counts for sigs the mint has already issued
|
||||
const amountPreferences = getDefaultAmountPreference(amountToReceive)
|
||||
const countOfInFlightProofs = CashuUtils.getAmountPreferencesCount(amountPreferences)
|
||||
const proofsToReceive = token.token.flatMap(entry => entry.proofs)
|
||||
const mintFeeReserve = mintInstance.getMintFeeReserve(proofsToReceive)
|
||||
|
||||
log.trace('[receiveTask]', 'amountPreferences', {amountPreferences, transactionId: transaction.id})
|
||||
log.trace('[receiveTask]', 'countOfInFlightProofs', {countOfInFlightProofs, transactionId: transaction.id})
|
||||
// log.trace('[receiveTask]', {tokenEntries})
|
||||
|
||||
// temp increase the counter + acquire lock and set inFlight values
|
||||
lockedProofsCounter = await WalletUtils.lockAndSetInFlight(
|
||||
mintInstance,
|
||||
unit,
|
||||
countOfInFlightProofs,
|
||||
transaction.id!
|
||||
)
|
||||
|
||||
const receivedResult = await walletStore.receive(
|
||||
const {
|
||||
receivedAmount,
|
||||
outputToken,
|
||||
swapFeeReserve,
|
||||
swapFeePaid,
|
||||
counter
|
||||
} = await receiveSync(
|
||||
mintToReceive,
|
||||
unit as MintUnit,
|
||||
token,
|
||||
mintFeeReserve,
|
||||
{
|
||||
preference: amountPreferences,
|
||||
counter: lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase
|
||||
}
|
||||
token.memo || '',
|
||||
transaction.id
|
||||
)
|
||||
|
||||
const receivedProofs = receivedResult.proofs
|
||||
const mintFeePaid = receivedResult.mintFeePaid
|
||||
|
||||
// log.trace('[receiveTask]', {receivedProofs})
|
||||
|
||||
// If we've got valid response, decrease proofsCounter and let it be increased back in next step when adding proofs
|
||||
lockedProofsCounter.decreaseProofsCounter(countOfInFlightProofs)
|
||||
|
||||
// store swapped proofs as encoded token in tx data
|
||||
const receivedTokenEntry = {
|
||||
mint: mintToReceive,
|
||||
proofs: receivedProofs,
|
||||
}
|
||||
|
||||
const outputToken = CashuUtils.encodeToken({
|
||||
token: [receivedTokenEntry],
|
||||
unit,
|
||||
memo,
|
||||
})
|
||||
|
||||
// Update transaction status
|
||||
transactionData.push({
|
||||
status: TransactionStatus.PREPARED,
|
||||
mintFeeReserve,
|
||||
mintFeePaid,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.PREPARED,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
|
||||
const { addedAmount: receivedAmount } = WalletUtils.addCashuProofs(
|
||||
mintToReceive,
|
||||
receivedProofs,
|
||||
{
|
||||
unit,
|
||||
transactionId: transaction.id!,
|
||||
isPending: false
|
||||
}
|
||||
)
|
||||
|
||||
// release lock
|
||||
lockedProofsCounter.resetInFlight(transaction.id!)
|
||||
|
||||
// Update tx amount if full amount was not received
|
||||
if (receivedAmount !== amountToReceive) {
|
||||
@@ -199,10 +123,12 @@ export const receiveTask = async function (
|
||||
|
||||
// Finally, update completed transaction
|
||||
transactionData.push({
|
||||
status: TransactionStatus.COMPLETED,
|
||||
status: TransactionStatus.COMPLETED,
|
||||
swapFeeReserve,
|
||||
swapFeePaid,
|
||||
receivedAmount,
|
||||
unit,
|
||||
counter: lockedProofsCounter.counter,
|
||||
counter,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
@@ -216,13 +142,13 @@ export const receiveTask = async function (
|
||||
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance!
|
||||
transaction.setBalanceAfter(balanceAfter)
|
||||
|
||||
if(mintFeePaid > 0) {
|
||||
transaction.setFee(mintFeePaid)
|
||||
if(swapFeePaid > 0) {
|
||||
transaction.setFee(swapFeePaid)
|
||||
}
|
||||
|
||||
return {
|
||||
taskFunction: RECEIVE,
|
||||
mintUrl: mintInstance.mintUrl,
|
||||
mintUrl: mintToReceive,
|
||||
transaction,
|
||||
message: `You've received ${formatCurrency(receivedAmount, getCurrency(unit).code)} ${getCurrency(unit).code} to your Minibits wallet.`,
|
||||
receivedAmount,
|
||||
@@ -230,9 +156,6 @@ export const receiveTask = async function (
|
||||
|
||||
} catch (e: any) {
|
||||
if (transaction) {
|
||||
if(lockedProofsCounter) {
|
||||
lockedProofsCounter.resetInFlight(transaction.id!)
|
||||
}
|
||||
|
||||
transactionData.push({
|
||||
status: TransactionStatus.ERROR,
|
||||
@@ -250,7 +173,7 @@ export const receiveTask = async function (
|
||||
|
||||
return {
|
||||
taskFunction: RECEIVE,
|
||||
mintUrl: mintToReceive || '',
|
||||
mintUrl: mintToReceive,
|
||||
transaction,
|
||||
message: e.message,
|
||||
error: WalletUtils.formatError(e),
|
||||
@@ -355,7 +278,7 @@ export const receiveOfflinePrepareTask = async function (
|
||||
error: WalletUtils.formatError(e),
|
||||
})
|
||||
|
||||
await transaction.setStatus(
|
||||
transaction.setStatus(
|
||||
TransactionStatus.ERROR,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
@@ -377,8 +300,7 @@ export const receiveOfflinePrepareTask = async function (
|
||||
export const receiveOfflineCompleteTask = async function (
|
||||
transactionId: number
|
||||
) {
|
||||
let mintToReceive = ''
|
||||
let lockedProofsCounter: MintProofsCounter | undefined = undefined
|
||||
let mintToReceive = ''
|
||||
const transaction = transactionsStore.findById(transactionId)
|
||||
|
||||
try {
|
||||
@@ -388,7 +310,6 @@ export const receiveOfflineCompleteTask = async function (
|
||||
|
||||
const transactionData = JSON.parse(transaction.data)
|
||||
|
||||
|
||||
if (!transaction.inputToken) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Could not find ecash token to redeem', {caller: 'receiveOfflineComplete'})
|
||||
}
|
||||
@@ -420,6 +341,95 @@ export const receiveOfflineCompleteTask = async function (
|
||||
} as TransactionTaskResult
|
||||
}
|
||||
|
||||
const {
|
||||
receivedAmount,
|
||||
outputToken,
|
||||
swapFeeReserve,
|
||||
swapFeePaid,
|
||||
counter
|
||||
} = await receiveSync(
|
||||
mintToReceive,
|
||||
token,
|
||||
token.memo || '',
|
||||
transaction.id
|
||||
)
|
||||
|
||||
// Update tx amount if full amount was not received
|
||||
if (receivedAmount !== transaction.amount) {
|
||||
transaction.setReceivedAmount(receivedAmount)
|
||||
}
|
||||
|
||||
// Finally, update completed transaction
|
||||
transactionData.push({
|
||||
status: TransactionStatus.COMPLETED,
|
||||
receivedAmount,
|
||||
swapFeeReserve,
|
||||
swapFeePaid,
|
||||
unit,
|
||||
counter,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.COMPLETED,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
|
||||
transaction.setOutputToken(outputToken)
|
||||
|
||||
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance!
|
||||
transaction.setBalanceAfter(balanceAfter)
|
||||
|
||||
if(swapFeePaid > 0) {
|
||||
transaction.setFee(swapFeePaid)
|
||||
}
|
||||
|
||||
return {
|
||||
taskFunction: RECEIVE_OFFLINE_COMPLETE,
|
||||
mintUrl: mintToReceive,
|
||||
transaction,
|
||||
message: `You've received ${formatCurrency(receivedAmount, getCurrency(unit).code)} ${getCurrency(unit).code} to your Minibits wallet.`,
|
||||
receivedAmount,
|
||||
} as TransactionTaskResult
|
||||
|
||||
} catch (e: any) {
|
||||
// release lock
|
||||
if(transaction) {
|
||||
|
||||
const transactionData = JSON.parse(transaction.data)
|
||||
transactionData.push({
|
||||
status: TransactionStatus.ERROR,
|
||||
error: WalletUtils.formatError(e),
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.ERROR,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
taskFunction: RECEIVE_OFFLINE_COMPLETE,
|
||||
mintUrl: mintToReceive,
|
||||
transaction,
|
||||
message: '',
|
||||
error: WalletUtils.formatError(e),
|
||||
} as TransactionTaskResult
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const receiveSync = async function (
|
||||
mintToReceive: string,
|
||||
token: TokenV3,
|
||||
memo: string,
|
||||
transactionId: number
|
||||
) {
|
||||
|
||||
const unit = token.unit as MintUnit || 'sat'
|
||||
let lockedProofsCounter: MintProofsCounter | undefined = undefined
|
||||
|
||||
try {
|
||||
// Handle missing mint, we add it automatically
|
||||
const alreadyExists = mintsStore.alreadyExists(mintToReceive)
|
||||
|
||||
@@ -434,31 +444,29 @@ export const receiveOfflineCompleteTask = async function (
|
||||
}
|
||||
|
||||
// Increase the proofs counter before the mint call so that in case the response
|
||||
// is not received our recovery index counts for sigs the mint has already issued (prevents duplicate b_b bug)
|
||||
|
||||
const tokenEntries: TokenEntryV3[] = token.token
|
||||
const proofsToReceive = tokenEntries[0].proofs as Proof[]
|
||||
const mintFeeReserve = mintInstance.getMintFeeReserve(proofsToReceive)
|
||||
// We will receive amount less fees paid for a swap for fresh ecash
|
||||
const amountPreferences = getDefaultAmountPreference(transaction.amount - mintFeeReserve)
|
||||
const countOfInFlightProofs = CashuUtils.getAmountPreferencesCount(amountPreferences)
|
||||
|
||||
log.trace('[receiveOfflineCompleteTask]', 'amountPreferences', amountPreferences)
|
||||
log.trace('[receiveOfflineCompleteTask]', 'countOfInFlightProofs', countOfInFlightProofs)
|
||||
// is not received our recovery index counts for sigs the mint has already issued
|
||||
const amountToReceive = CashuUtils.getTokenAmounts(token).totalAmount
|
||||
const amountPreferences = getDefaultAmountPreference(amountToReceive)
|
||||
const countOfInFlightProofs = CashuUtils.getAmountPreferencesCount(amountPreferences)
|
||||
const proofsToReceive = token.token.flatMap(entry => entry.proofs)
|
||||
const swapFeeReserve = mintInstance.getMintFeeReserve(proofsToReceive)
|
||||
|
||||
log.trace('[receiveSync]', 'amountPreferences', {amountPreferences, transactionId})
|
||||
log.trace('[receiveSync]', 'countOfInFlightProofs', {countOfInFlightProofs, transactionId})
|
||||
|
||||
// temp increase the counter + acquire lock and set inFlight values
|
||||
lockedProofsCounter = await WalletUtils.lockAndSetInFlight(
|
||||
mintInstance,
|
||||
unit,
|
||||
countOfInFlightProofs,
|
||||
transaction.id as number
|
||||
transactionId
|
||||
)
|
||||
|
||||
const receivedResult = await walletStore.receive(
|
||||
mintToReceive,
|
||||
unit as MintUnit,
|
||||
token,
|
||||
mintFeeReserve,
|
||||
swapFeeReserve,
|
||||
{
|
||||
preference: amountPreferences,
|
||||
counter: lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase
|
||||
@@ -466,110 +474,58 @@ export const receiveOfflineCompleteTask = async function (
|
||||
)
|
||||
|
||||
const receivedProofs = receivedResult.proofs
|
||||
const mintFeePaid = receivedResult.mintFeePaid
|
||||
const swapFeePaid = receivedResult.swapFeePaid
|
||||
|
||||
// log.trace('[receiveTask]', {receivedProofs})
|
||||
|
||||
// If we've got valid response, decrease proofsCounter and let it be increased back in next step when adding proofs
|
||||
lockedProofsCounter.decreaseProofsCounter(countOfInFlightProofs)
|
||||
|
||||
// store swapped proofs as encoded token in tx data
|
||||
const receivedTokenEntry: TokenEntryV3 = {
|
||||
mint: mintToReceive,
|
||||
proofs: receivedProofs,
|
||||
}
|
||||
|
||||
const outputToken = CashuUtils.encodeToken({
|
||||
token: [receivedTokenEntry],
|
||||
unit,
|
||||
})
|
||||
|
||||
// Update transaction status
|
||||
transactionData.push({
|
||||
status: TransactionStatus.PREPARED,
|
||||
mintFeeReserve,
|
||||
mintFeePaid,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.PREPARED,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
|
||||
const { addedAmount: receivedAmount } = WalletUtils.addCashuProofs(
|
||||
mintToReceive,
|
||||
receivedProofs,
|
||||
{
|
||||
unit,
|
||||
transactionId: transaction.id as number,
|
||||
transactionId: transactionId,
|
||||
isPending: false
|
||||
}
|
||||
)
|
||||
|
||||
// release lock
|
||||
lockedProofsCounter.resetInFlight(transaction.id as number)
|
||||
|
||||
// Update tx amount if full amount was not received
|
||||
if (receivedAmount !== transaction.amount) {
|
||||
transaction.setReceivedAmount(receivedAmount)
|
||||
lockedProofsCounter.resetInFlight(transactionId)
|
||||
|
||||
// store swapped proofs as encoded token in tx data
|
||||
const receivedTokenEntry = {
|
||||
mint: mintToReceive,
|
||||
proofs: receivedProofs,
|
||||
}
|
||||
|
||||
// Finally, update completed transaction
|
||||
transactionData.push({
|
||||
status: TransactionStatus.COMPLETED,
|
||||
const outputToken = CashuUtils.encodeToken({
|
||||
token: [receivedTokenEntry],
|
||||
unit,
|
||||
memo,
|
||||
})
|
||||
|
||||
return {
|
||||
receivedAmount,
|
||||
unit,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.COMPLETED,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
|
||||
transaction.setOutputToken(outputToken)
|
||||
|
||||
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance!
|
||||
transaction.setBalanceAfter(balanceAfter)
|
||||
|
||||
if(mintFeePaid > 0) {
|
||||
transaction.setFee(mintFeePaid)
|
||||
receivedProofs,
|
||||
outputToken,
|
||||
swapFeeReserve,
|
||||
swapFeePaid,
|
||||
counter: lockedProofsCounter?.counter
|
||||
}
|
||||
|
||||
return {
|
||||
taskFunction: RECEIVE_OFFLINE_COMPLETE,
|
||||
mintUrl: mintInstance.mintUrl,
|
||||
transaction,
|
||||
message: `You've received ${formatCurrency(receivedAmount, getCurrency(unit).code)} ${getCurrency(unit).code} to your Minibits wallet.`,
|
||||
receivedAmount,
|
||||
} as TransactionTaskResult
|
||||
|
||||
|
||||
} catch (e: any) {
|
||||
// release lock
|
||||
if(transaction) {
|
||||
if(lockedProofsCounter) {
|
||||
lockedProofsCounter.resetInFlight(transaction.id as number)
|
||||
if (transactionId) {
|
||||
if(lockedProofsCounter) {
|
||||
lockedProofsCounter.resetInFlight(transactionId)
|
||||
}
|
||||
|
||||
const transactionData = JSON.parse(transaction.data)
|
||||
transactionData.push({
|
||||
status: TransactionStatus.ERROR,
|
||||
error: WalletUtils.formatError(e),
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.ERROR,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
}
|
||||
|
||||
log.error(e.name, e.message)
|
||||
|
||||
return {
|
||||
taskFunction: RECEIVE_OFFLINE_COMPLETE,
|
||||
mintUrl: mintToReceive,
|
||||
transaction,
|
||||
message: '',
|
||||
error: WalletUtils.formatError(e),
|
||||
} as TransactionTaskResult
|
||||
if (e instanceof AppError) {
|
||||
throw e
|
||||
} else {
|
||||
throw new AppError(Err.WALLET_ERROR, e.message, e.stack.slice(0, 200))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import AppError, {Err} from '../../utils/AppError'
|
||||
import {
|
||||
MintKeyset,
|
||||
} from '@cashu/cashu-ts'
|
||||
import { getDefaultAmountPreference, isObj } from '@cashu/cashu-ts/src/utils'
|
||||
import { getDefaultAmountPreference } from '@cashu/cashu-ts/src/utils'
|
||||
import { MAX_SWAP_INPUT_SIZE, TransactionTaskResult, WalletTask } from '../walletService'
|
||||
import { Mint, MintBalance, MintProofsCounter } from '../../models/Mint'
|
||||
import { Proof } from '../../models/Proof'
|
||||
@@ -76,8 +76,8 @@ export const sendTask = async function (
|
||||
// get ready proofs to send and update proofs and pending proofs storage
|
||||
const {
|
||||
proofs: proofsToSend,
|
||||
mintFeePaid,
|
||||
mintFeeReserve,
|
||||
swapFeePaid,
|
||||
swapFeeReserve,
|
||||
isSwapNeeded,
|
||||
counter
|
||||
} = await sendFromMintSync(
|
||||
@@ -91,8 +91,8 @@ export const sendTask = async function (
|
||||
// Update transaction status
|
||||
transactionData.push({
|
||||
status: TransactionStatus.PREPARED,
|
||||
mintFeeReserve,
|
||||
mintFeePaid,
|
||||
swapFeeReserve,
|
||||
swapFeePaid,
|
||||
isSwapNeeded,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
@@ -134,8 +134,8 @@ export const sendTask = async function (
|
||||
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance!
|
||||
transaction.setBalanceAfter(balanceAfter)
|
||||
|
||||
if(mintFeePaid > 0) {
|
||||
transaction.setFee(mintFeePaid)
|
||||
if(swapFeePaid > 0) {
|
||||
transaction.setFee(swapFeePaid)
|
||||
}
|
||||
|
||||
log.trace('[send] totalBalance after', balanceAfter)
|
||||
@@ -165,7 +165,7 @@ export const sendTask = async function (
|
||||
transaction,
|
||||
message: '',
|
||||
encodedTokenToSend: outputToken,
|
||||
mintFeePaid
|
||||
swapFeePaid
|
||||
} as TransactionTaskResult
|
||||
|
||||
} catch (e: any) {
|
||||
@@ -263,13 +263,17 @@ export const sendFromMintSync = async function (
|
||||
)
|
||||
}
|
||||
|
||||
for (const proof of selectedProofs) {
|
||||
proof.setTransactionId(transactionId) // update txId
|
||||
}
|
||||
|
||||
// move sent proofs to pending
|
||||
proofsStore.removeProofs(selectedProofs)
|
||||
proofsStore.addProofs(selectedProofs, true) // pending true
|
||||
// move sent proofs to pending and add tx references
|
||||
proofsStore.removeProofs(selectedProofs)
|
||||
WalletUtils.addCashuProofs(
|
||||
mintUrl,
|
||||
selectedProofs,
|
||||
{
|
||||
unit,
|
||||
transactionId: transactionId,
|
||||
isPending: true
|
||||
}
|
||||
)
|
||||
|
||||
// Clean private properties to not to send them out. This returns plain js array, not model objects.
|
||||
const cleanedProofsToSend = selectedProofs.map(proof => {
|
||||
@@ -280,8 +284,8 @@ export const sendFromMintSync = async function (
|
||||
// We return cleaned proofs to be encoded as a sendable token
|
||||
return {
|
||||
proofs: cleanedProofsToSend,
|
||||
mintFeeReserve: 0,
|
||||
mintFeePaid: 0
|
||||
swapFeeReserve: 0,
|
||||
swapFeePaid: 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,9 +316,9 @@ export const sendFromMintSync = async function (
|
||||
|
||||
let proofsToSendFromAmount = CashuUtils.getProofsAmount(proofsToSendFrom)
|
||||
// swap will happen if we could not select proofs equal to amountToSend
|
||||
let mintFeeReserve: number = 0
|
||||
let swapFeeReserve: number = 0
|
||||
let returnedAmount = 0
|
||||
let mintFeePaid: number = 0
|
||||
let swapFeePaid: number = 0
|
||||
let proofsToSend: Proof[] = []
|
||||
let returnedProofs: Proof[] = []
|
||||
|
||||
@@ -327,26 +331,30 @@ export const sendFromMintSync = async function (
|
||||
*/
|
||||
if(isSwapNeeded) {
|
||||
// Calculate feeReserve from mint fee rate
|
||||
mintFeeReserve = mintInstance.getMintFeeReserve(proofsToSendFrom)
|
||||
// This is expected to get back from mint as a split remainder - we deduct fee that a mint will keep
|
||||
returnedAmount = proofsToSendFromAmount - amountToSend - mintFeeReserve
|
||||
swapFeeReserve = mintInstance.getMintFeeReserve(proofsToSendFrom)
|
||||
const amountWithFees = amountToSend + swapFeeReserve
|
||||
|
||||
log.debug('[sendFromMintSync] Swap is needed.', {mintFeeReserve, returnedAmount, transactionId})
|
||||
|
||||
// if we did not selected enough proofs to cover the fees we need some more
|
||||
if(returnedAmount < 0) {
|
||||
|
||||
const proofsToPayFees = getProofsToPayFees(
|
||||
Math.abs(returnedAmount),
|
||||
proofsFromMint,
|
||||
proofsToSendFrom
|
||||
if (totalAmountFromMint < amountWithFees) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
'There is not enough funds to send this amount.',
|
||||
{totalAmountFromMint, amountWithFees, transactionId, caller: 'transferTask'},
|
||||
)
|
||||
// add more proofs into inputs and recalculate amounts
|
||||
proofsToSendFrom.push(...proofsToPayFees)
|
||||
proofsToSendFromAmount = CashuUtils.getProofsAmount(proofsToSendFrom)
|
||||
returnedAmount = proofsToSendFromAmount - amountToSend - mintFeeReserve
|
||||
}
|
||||
|
||||
// exact match or min number of proofs that matches the amount
|
||||
proofsToSendFrom = CashuUtils.getProofsToSend(
|
||||
amountWithFees,
|
||||
proofsFromMint
|
||||
)
|
||||
|
||||
proofsToSendFromAmount = CashuUtils.getProofsAmount(proofsToSendFrom)
|
||||
|
||||
// This is expected to get back from mint as a split remainder - we deduct fee that a mint will keep
|
||||
returnedAmount = proofsToSendFromAmount - (amountToSend + swapFeeReserve)
|
||||
|
||||
log.debug('[sendFromMintSync] Swap is needed.', {swapFeeReserve, returnedAmount, transactionId})
|
||||
|
||||
// Output denominations we ask for to get back
|
||||
const amountPreferences = getDefaultAmountPreference(amountToSend)
|
||||
// Output denominations we are about to get as a split remainder
|
||||
@@ -355,8 +363,7 @@ export const sendFromMintSync = async function (
|
||||
const countOfProofsToSend = CashuUtils.getAmountPreferencesCount(amountPreferences)
|
||||
const countOfReturnedProofs = CashuUtils.getAmountPreferencesCount(returnedAmountPreferences)
|
||||
const countOfInFlightProofs = countOfProofsToSend + countOfReturnedProofs
|
||||
|
||||
log.trace('[sendFromMintSync]', 'amountPreferences', {amountPreferences, returnedAmountPreferences})
|
||||
|
||||
log.trace('[sendFromMintSync]', 'countOfInFlightProofs', countOfInFlightProofs)
|
||||
|
||||
// Increase the proofs counter before the mint call so that in case the response
|
||||
@@ -374,7 +381,7 @@ export const sendFromMintSync = async function (
|
||||
const sendResult = await walletStore.send(
|
||||
mintUrl,
|
||||
amountToSend,
|
||||
mintFeeReserve,
|
||||
swapFeeReserve,
|
||||
unit,
|
||||
proofsToSendFrom,
|
||||
{
|
||||
@@ -385,13 +392,14 @@ export const sendFromMintSync = async function (
|
||||
|
||||
returnedProofs = sendResult.returnedProofs // TODO types - these are ProofsV3 indeed
|
||||
proofsToSend = sendResult.proofsToSend // TODO types - these are ProofsV3 indeed
|
||||
mintFeePaid = sendResult.mintFeePaid
|
||||
swapFeePaid = sendResult.swapFeePaid
|
||||
|
||||
// If we've got valid response, decrease proofsCounter and let it be increased back in next step when adding proofs
|
||||
lockedProofsCounter.decreaseProofsCounter(countOfInFlightProofs)
|
||||
|
||||
// add proofs returned by the mint after the split
|
||||
log.trace('[sendFromMintSync] add returned proofs to spendable')
|
||||
|
||||
WalletUtils.addCashuProofs(
|
||||
mintUrl,
|
||||
returnedProofs,
|
||||
@@ -400,8 +408,7 @@ export const sendFromMintSync = async function (
|
||||
transactionId,
|
||||
isPending: false
|
||||
}
|
||||
)
|
||||
|
||||
)
|
||||
// release lock
|
||||
lockedProofsCounter.resetInFlight(transactionId)
|
||||
|
||||
@@ -410,7 +417,7 @@ export const sendFromMintSync = async function (
|
||||
* SWAP is NOT needed, we've found denominations that match exact amount
|
||||
*
|
||||
*/
|
||||
log.debug('[sendFromMintSync] Swap is not necessary, all proofsToSendFrom will be sent.', {transactionId})
|
||||
log.debug('[sendFromMintSync] Swap is not necessary.', {transactionId})
|
||||
|
||||
// If we selected whole balance, check if it is not above limit acceptable by wallet and mints.
|
||||
if(proofsToSendFrom.length > MAX_SWAP_INPUT_SIZE) {
|
||||
@@ -452,8 +459,8 @@ export const sendFromMintSync = async function (
|
||||
// We return cleaned proofs to be encoded as a sendable token + fees
|
||||
return {
|
||||
proofs: cleanedProofsToSend as ProofV3[],
|
||||
mintFeeReserve,
|
||||
mintFeePaid,
|
||||
swapFeeReserve,
|
||||
swapFeePaid,
|
||||
isSwapNeeded,
|
||||
counter: lockedProofsCounter?.counter
|
||||
}
|
||||
@@ -562,29 +569,4 @@ const getInactiveKeysetIds = function(mint: Mint) {
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
const getProofsToPayFees = function (
|
||||
feeAmount: number,
|
||||
proofsFromMint: Proof[],
|
||||
proofsToSendFrom: Proof[]
|
||||
) {
|
||||
|
||||
const remainingProofs = proofsStore.getProofsSubset(proofsFromMint, proofsToSendFrom)
|
||||
const remainingProofsAmount = CashuUtils.getProofsAmount(remainingProofs)
|
||||
|
||||
if(feeAmount > remainingProofsAmount) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
'There is not enough funds to cover the expected fee for this payment.',
|
||||
{feeAmount, remainingProofsAmount},
|
||||
)
|
||||
}
|
||||
|
||||
return CashuUtils.getProofsToSend(
|
||||
feeAmount,
|
||||
remainingProofs
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
+111
-134
@@ -1,4 +1,4 @@
|
||||
import {CashuUtils, ProofV3, TokenEntryV3} from '../cashu/cashuUtils'
|
||||
import {CashuUtils, TokenEntryV3} from '../cashu/cashuUtils'
|
||||
import AppError, {Err} from '../../utils/AppError'
|
||||
import {MeltQuoteResponse, MeltQuoteState} from '@cashu/cashu-ts'
|
||||
import {rootStoreInstance} from '../../models'
|
||||
@@ -9,7 +9,6 @@ import { Transaction, TransactionData, TransactionRecord, TransactionStatus, Tra
|
||||
import { log } from '../logService'
|
||||
import { WalletUtils } from './utils'
|
||||
import {isBefore} from 'date-fns'
|
||||
import { sendFromMintSync } from './sendTask'
|
||||
import { MintUnit, formatCurrency, getCurrency } from './currency'
|
||||
import { NostrEvent } from '../nostrService'
|
||||
|
||||
@@ -38,7 +37,8 @@ export const transferTask = async function (
|
||||
const mintInstance = mintsStore.findByUrl(mintUrl)
|
||||
let lockedProofsCounter: MintProofsCounter | undefined = undefined
|
||||
|
||||
log.debug('[transfer]', 'mintBalanceToTransferFrom', {mintBalanceToTransferFrom})
|
||||
// TODO refresh - balance might be outdated if it waits in queue before other txs
|
||||
log.debug('[transfer]', 'mintBalanceToTransferFrom', {mintBalanceToTransferFrom})
|
||||
log.debug('[transfer]', 'amountToTransfer', {amountToTransfer})
|
||||
log.debug('[transfer]', 'meltQuote', {meltQuote})
|
||||
|
||||
@@ -57,8 +57,8 @@ export const transferTask = async function (
|
||||
]
|
||||
|
||||
let transaction: Transaction | undefined = undefined
|
||||
let proofsToPay: ProofV3[] = []
|
||||
let proofsToPayAmount: number = 0
|
||||
let proofsToMeltFrom: Proof[] = []
|
||||
let proofsToMeltFromAmount: number = 0
|
||||
|
||||
try {
|
||||
const newTransaction = {
|
||||
@@ -98,101 +98,58 @@ export const transferTask = async function (
|
||||
'Could not find mint', {mintUrl, transactionId}
|
||||
)
|
||||
}
|
||||
|
||||
// if previous transaction got interrupted by android background processing, resync their proofs state with the mint
|
||||
// unfinished transactions cause outputs already signed error
|
||||
const previousTransaction = transactionsStore.findById(transactionId - 1)
|
||||
|
||||
// if zap remained stuck in DRAFT
|
||||
if(previousTransaction
|
||||
&& previousTransaction.status === TransactionStatus.DRAFT
|
||||
&& previousTransaction.type === TransactionType.TRANSFER
|
||||
) {
|
||||
log.error(`[transfer] Previous TRANSFER transaction ${previousTransaction.id} is stuck in DRAFT status.`)
|
||||
// we check if proofsCounter is locked so we very likely fired swap request
|
||||
const proofsCounter = mintInstance.findInFlightProofsCounterByTId(previousTransaction.id)
|
||||
|
||||
if(proofsCounter && proofsCounter.inFlightTid === previousTransaction.id) {
|
||||
log.error(`[transfer] Found previous DARFT TRANSFER locked proofsCounter.`, {proofsCounter, prevId: previousTransaction.id})
|
||||
await WalletTask.handleInFlightSync(mintInstance)
|
||||
}
|
||||
}
|
||||
|
||||
// if zap remained stuck in PREPARED we sync pending proofs so that we set proofsCounters to correct values
|
||||
if(previousTransaction
|
||||
&& previousTransaction.status === TransactionStatus.PREPARED
|
||||
&& previousTransaction.type === TransactionType.TRANSFER
|
||||
) {
|
||||
log.error(`[transfer] Previous TRANSFER transaction ${previousTransaction.id} is stuck in PREPARED status.`)
|
||||
const proofsCounter = mintInstance.findInFlightProofsCounterByTId(previousTransaction.id)
|
||||
|
||||
if(proofsCounter && proofsCounter.inFlightTid === previousTransaction.id) {
|
||||
log.error(`[transfer] Found previous PREPARED TRANSFER locked proofsCounter.`, {proofsCounter, prevId: previousTransaction.id})
|
||||
await WalletTask.handleInFlightSync(mintInstance)
|
||||
}
|
||||
|
||||
const proofsToSync = proofsStore.getByTransactionId(previousTransaction.id, true) // pending
|
||||
log.error(`[transfer] Syncing PREPARED TRANSFER pending proofs with the mint.`, {prevId: previousTransaction.id, proofsToSyncCount: proofsToSync.length})
|
||||
|
||||
if(proofsToSync.length > 0) {
|
||||
await WalletTask.syncStateWithMintSync({proofsToSync, mintUrl, isPending: true})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// calculate fees charged by mint for melt transaction to prepare enough proofs
|
||||
const proofsFromMint = proofsStore.getByMint(mintUrl, {isPending: false, unit})
|
||||
const totalAmountFromMint = CashuUtils.getProofsAmount(proofsFromMint)
|
||||
|
||||
let proofsToMelt = CashuUtils.getProofsToSend(
|
||||
const proofsToMelt = CashuUtils.getProofsToSend(
|
||||
amountToTransfer + meltQuote.fee_reserve,
|
||||
proofsFromMint
|
||||
)
|
||||
|
||||
let meltFeeReserve = mintInstance.getMintFeeReserve(proofsToMelt)
|
||||
let meltFeeReserve = mintInstance.getMintFeeReserve(proofsToMelt)
|
||||
const amountWithFees = amountToTransfer + meltQuote.fee_reserve + meltFeeReserve
|
||||
|
||||
log.trace('[transfer]', {
|
||||
meltFeeReserve,
|
||||
lightningFeeReserve: meltQuote.fee_reserve,
|
||||
amountWithFees: amountToTransfer + meltQuote.fee_reserve + meltFeeReserve,
|
||||
})
|
||||
if (totalAmountFromMint < amountWithFees) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
'There is not enough funds to send this amount.',
|
||||
{totalAmountFromMint, amountWithFees, transactionId, caller: 'transferTask'},
|
||||
)
|
||||
}
|
||||
|
||||
// get proofs ready to be paid to the mint
|
||||
const swapResult = await sendFromMintSync(
|
||||
mintBalanceToTransferFrom,
|
||||
amountToTransfer + meltQuote.fee_reserve + meltFeeReserve,
|
||||
unit,
|
||||
[],
|
||||
transactionId,
|
||||
// exact match or min number of proofs that matches the amount
|
||||
proofsToMeltFrom = CashuUtils.getProofsToSend(
|
||||
amountWithFees,
|
||||
proofsFromMint
|
||||
)
|
||||
|
||||
const {
|
||||
proofs: proofsToPay,
|
||||
mintFeePaid,
|
||||
mintFeeReserve,
|
||||
isSwapNeeded,
|
||||
counter
|
||||
} = swapResult
|
||||
|
||||
proofsToPayAmount = CashuUtils.getProofsAmount(proofsToPay)
|
||||
proofsToMeltFromAmount = CashuUtils.getProofsAmount(proofsToMeltFrom)
|
||||
|
||||
// TODO in case of swap from inactive keysets, different meltFees might apply than above calculated meltFeeReserve
|
||||
// In such case, we might need to add / substract the fee difference to / from proofsToPay
|
||||
proofsStore.removeProofs(proofsToMeltFrom)
|
||||
WalletUtils.addCashuProofs(
|
||||
mintUrl,
|
||||
proofsToMeltFrom,
|
||||
{
|
||||
unit,
|
||||
transactionId: transaction.id,
|
||||
isPending: true
|
||||
}
|
||||
)
|
||||
|
||||
log.info('[transfer]', 'Prepared poofsToPay proofs', {
|
||||
proofsToPayAmount,
|
||||
log.trace('[transfer]', 'Prepared proofsToMeltFrom proofs', {
|
||||
proofsToMeltFromAmount,
|
||||
transactionId,
|
||||
unit,
|
||||
transactionId,
|
||||
isSwapNeeded,
|
||||
counter
|
||||
})
|
||||
|
||||
// Update transaction status
|
||||
transactionData.push({
|
||||
status: TransactionStatus.PREPARED,
|
||||
mintFeeReserve,
|
||||
mintFeePaid,
|
||||
proofsToPayAmount,
|
||||
isSwapNeeded,
|
||||
counter,
|
||||
proofsToMeltFromAmount,
|
||||
lightningFeeReserve: meltQuote.fee_reserve,
|
||||
meltFeeReserve,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
@@ -203,7 +160,7 @@ export const transferTask = async function (
|
||||
|
||||
const inputTokenEntry: TokenEntryV3 = {
|
||||
mint: mintUrl,
|
||||
proofs: proofsToPay,
|
||||
proofs: proofsToMeltFrom,
|
||||
}
|
||||
|
||||
const inputToken = CashuUtils.encodeToken({
|
||||
@@ -213,10 +170,10 @@ export const transferTask = async function (
|
||||
|
||||
transaction.setInputToken(inputToken)
|
||||
|
||||
// number of outputs we can get back with returned lightning fees
|
||||
// number of outputs we can get back as a change
|
||||
let countOfInFlightProofs = 1
|
||||
if(meltQuote.fee_reserve > 1) {
|
||||
countOfInFlightProofs = Math.ceil(Math.log2(meltQuote.fee_reserve))
|
||||
if(proofsToMeltFromAmount - amountToTransfer > 1) {
|
||||
countOfInFlightProofs = Math.ceil(Math.log2(proofsToMeltFromAmount - amountToTransfer))
|
||||
}
|
||||
|
||||
// temp increase the counter + acquire lock and set inFlight values
|
||||
@@ -227,11 +184,11 @@ export const transferTask = async function (
|
||||
transaction.id
|
||||
)
|
||||
|
||||
const {state, preimage, change: feeSavedProofs} = await walletStore.payLightningMelt(
|
||||
const {state, preimage, change: returnedProofs} = await walletStore.payLightningMelt(
|
||||
mintUrl,
|
||||
unit,
|
||||
meltQuote,
|
||||
proofsToPay,
|
||||
proofsToMeltFrom,
|
||||
{
|
||||
counter: lockedProofsCounter.inFlightFrom as number
|
||||
}
|
||||
@@ -241,22 +198,26 @@ export const transferTask = async function (
|
||||
|
||||
if (state === MeltQuoteState.PAID) {
|
||||
|
||||
log.debug('[transfer] Invoice PAID', {
|
||||
state,
|
||||
meltQuote,
|
||||
preimage,
|
||||
log.debug('[transfer] Invoice PAID', {
|
||||
transactionId
|
||||
})
|
||||
|
||||
// Save preimage asap
|
||||
if(preimage) {
|
||||
transaction.setProof(preimage)
|
||||
}
|
||||
|
||||
// Spend pending proofs that were used to settle the lightning invoice
|
||||
proofsStore.removeProofs(proofsToPay as Proof[], true, false)
|
||||
proofsStore.removeProofs(proofsToMeltFrom as Proof[], true, false)
|
||||
|
||||
let totalFeePaid = proofsToMeltFromAmount - amountToTransfer
|
||||
let lightningFeePaid = totalFeePaid - meltFeeReserve
|
||||
let returnedAmount = CashuUtils.getProofsAmount(returnedProofs)
|
||||
|
||||
let lightningFeePaid = meltQuote.fee_reserve
|
||||
|
||||
if (feeSavedProofs.length > 0) {
|
||||
const {addedAmount: feeSaved} = WalletUtils.addCashuProofs(
|
||||
if(returnedProofs.length > 0) {
|
||||
WalletUtils.addCashuProofs(
|
||||
mintUrl,
|
||||
feeSavedProofs,
|
||||
returnedProofs,
|
||||
{
|
||||
unit,
|
||||
transactionId: transaction.id,
|
||||
@@ -264,45 +225,44 @@ export const transferTask = async function (
|
||||
}
|
||||
)
|
||||
|
||||
const feeSavedTokenEntry: TokenEntryV3 = {
|
||||
const returnedTokenEntry: TokenEntryV3 = {
|
||||
mint: mintUrl,
|
||||
proofs: feeSavedProofs,
|
||||
proofs: returnedProofs,
|
||||
}
|
||||
|
||||
const outputToken = CashuUtils.encodeToken({
|
||||
token: [feeSavedTokenEntry],
|
||||
token: [returnedTokenEntry],
|
||||
unit,
|
||||
})
|
||||
|
||||
transaction.setOutputToken(outputToken)
|
||||
|
||||
lightningFeePaid = meltQuote.fee_reserve - feeSaved
|
||||
transaction.setOutputToken(outputToken)
|
||||
|
||||
totalFeePaid = totalFeePaid - returnedAmount
|
||||
lightningFeePaid = totalFeePaid - meltFeeReserve
|
||||
} else {
|
||||
// Spend pending proofs that were used to settle the lightning invoice
|
||||
proofsStore.removeProofs(proofsToMeltFrom as Proof[], true, false)
|
||||
}
|
||||
|
||||
// release lock
|
||||
lockedProofsCounter.resetInFlight(transactionId)
|
||||
|
||||
// Save preimage
|
||||
if(preimage) {
|
||||
transaction.setProof(preimage)
|
||||
}
|
||||
|
||||
// Save final fee in db
|
||||
if(lightningFeePaid + mintFeePaid !== meltQuote.fee_reserve) {
|
||||
transaction.setFee(lightningFeePaid + mintFeePaid)
|
||||
if(totalFeePaid !== transaction.fee) {
|
||||
transaction.setFee(totalFeePaid)
|
||||
}
|
||||
|
||||
// Update transaction status
|
||||
transactionData.push({
|
||||
status: TransactionStatus.COMPLETED,
|
||||
lightningFeeReserve: meltQuote.fee_reserve,
|
||||
status: TransactionStatus.COMPLETED,
|
||||
lightningFeePaid,
|
||||
mintFeesPaid: totalFeePaid - lightningFeePaid,
|
||||
returnedAmount,
|
||||
preimage,
|
||||
counter: lockedProofsCounter.counter,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
// this overwrites transactionData and COMPLETED status already set by syncStateWithMintSync
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.COMPLETED,
|
||||
JSON.stringify(transactionData),
|
||||
@@ -315,9 +275,10 @@ export const transferTask = async function (
|
||||
taskFunction: TRANSFER,
|
||||
mintUrl,
|
||||
transaction,
|
||||
message: `Lightning invoice has been successfully paid and settled with your Minibits ecash. Fee has been ${formatCurrency(lightningFeePaid + mintFeePaid, getCurrency(unit).code)} ${getCurrency(unit).code}.`,
|
||||
lightningFeePaid,
|
||||
mintFeePaid,
|
||||
message: `Lightning invoice has been successfully paid and settled with your Minibits ecash. Fee has been ${formatCurrency(transaction.fee, getCurrency(unit).code)} ${getCurrency(unit).code}.`,
|
||||
lightningFeePaid,
|
||||
mintFees: totalFeePaid - lightningFeePaid,
|
||||
totalFeePaid,
|
||||
meltQuote,
|
||||
preimage,
|
||||
nwcEvent
|
||||
@@ -337,9 +298,7 @@ export const transferTask = async function (
|
||||
mintUrl,
|
||||
transaction,
|
||||
message: `Lightning payment did not complete in time. Your ecash will remain pending until the payment completes or fails.`,
|
||||
mintFeePaid,
|
||||
meltQuote,
|
||||
preimage,
|
||||
meltQuote,
|
||||
nwcEvent
|
||||
} as TransactionTaskResult
|
||||
|
||||
@@ -359,16 +318,12 @@ export const transferTask = async function (
|
||||
}
|
||||
|
||||
let message = e.message
|
||||
|
||||
// If Exception was trigerred most likely by walletStore.payLightningMelt()
|
||||
if (proofsToPay.length > 0) {
|
||||
// check with the mint if proofs are not pending by mint, if yes,
|
||||
// sync sets transaction status as PENDING (timeout-ed/hodled lightning payments)
|
||||
const proofsToSync = proofsStore.getByMint(mintUrl, {isPending: true})
|
||||
|
||||
|
||||
if (proofsToMeltFrom.length > 0) {
|
||||
// check with the mint the real status of the proofs involved in transaction
|
||||
await WalletTask.syncStateWithMintSync(
|
||||
{
|
||||
proofsToSync,
|
||||
proofsToSync: proofsToMeltFrom,
|
||||
mintUrl,
|
||||
isPending: true
|
||||
}
|
||||
@@ -378,27 +333,49 @@ export const transferTask = async function (
|
||||
const refreshed = transactionsStore.findById(transaction.id)
|
||||
|
||||
if(refreshed?.status === TransactionStatus.PENDING) {
|
||||
log.warn('[transfer]', 'proofsToPay from transfer with error are pending by mint', {
|
||||
proofsToPayAmount,
|
||||
log.warn('[transfer]', 'proofsToMeltFrom from transfer with error are pending by mint', {
|
||||
proofsToMeltFromAmount,
|
||||
unit,
|
||||
transactionId: transaction.id
|
||||
})
|
||||
|
||||
message = 'Lightning payment did not complete in time. Your ecash will remain pending until the payment completes or fails.'
|
||||
|
||||
} else if(refreshed?.status === TransactionStatus.COMPLETED) {
|
||||
// Likely receiving of change failed due to wallet error. We keep completed as status.
|
||||
log.error('[transfer]', 'Transfer throwed error but the payment suceeded', {
|
||||
error: e.message,
|
||||
proofsToMeltFromAmount,
|
||||
unit,
|
||||
transactionId: transaction.id
|
||||
})
|
||||
|
||||
const message = `Lightning invoice has been successfully paid, however some error occured: ${e.message}`
|
||||
|
||||
return {
|
||||
taskFunction: TRANSFER,
|
||||
mintUrl,
|
||||
transaction,
|
||||
message,
|
||||
meltQuote,
|
||||
preimage: meltQuote?.payment_preimage,
|
||||
nwcEvent
|
||||
} as TransactionTaskResult
|
||||
|
||||
} else {
|
||||
// syncStateWithMintSync returns to spendable only proofs that were pending by mint before so we need to take care of our transfer here.
|
||||
log.warn('[transfer]', 'proofsToPay from transfer with error to be returned to spendable wallet', {
|
||||
proofsToPayAmount,
|
||||
proofsToMeltFromAmount,
|
||||
unit,
|
||||
transactionId: transaction.id
|
||||
})
|
||||
|
||||
// remove it from pending proofs in the wallet
|
||||
proofsStore.removeProofs(proofsToPay as Proof[], true, true)
|
||||
proofsStore.removeProofs(proofsToMeltFrom, true, true)
|
||||
// add proofs back to the spendable wallet with internal references
|
||||
WalletUtils.addCashuProofs(
|
||||
mintUrl,
|
||||
proofsToPay,
|
||||
proofsToMeltFrom,
|
||||
{
|
||||
unit,
|
||||
transactionId: transaction.id,
|
||||
|
||||
@@ -29,7 +29,11 @@ const lockAndSetInFlight = async function (
|
||||
const walletInstance = await walletStore.getWallet(mint.mintUrl, unit, {withSeed: true})
|
||||
const currentCounter = mint.getProofsCounterByKeysetId!(walletInstance.keys.id)
|
||||
|
||||
log.info('[lockAndSetInFlight] Before lock', {transactionId, counter: currentCounter.counter})
|
||||
log.info('[lockAndSetInFlight] Before lock', {
|
||||
transactionId,
|
||||
counter: currentCounter.counter,
|
||||
countOfInFlightProofs
|
||||
})
|
||||
|
||||
if(!currentCounter) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Missing ProofsCounter.')
|
||||
|
||||
@@ -26,11 +26,13 @@ import { receiveTask, receiveOfflinePrepareTask, receiveOfflineCompleteTask} fro
|
||||
import { sendTask } from './wallet/sendTask'
|
||||
import { topupTask } from './wallet/topupTask'
|
||||
import { transferTask } from './wallet/transferTask'
|
||||
import { nwcTransferTask } from './wallet/nwcTransferTask'
|
||||
import { revertTask } from './wallet/revertTask'
|
||||
import { WalletUtils } from './wallet/utils'
|
||||
import { NotificationService } from './notificationService'
|
||||
import { MintUnit, formatCurrency, getCurrency } from './wallet/currency'
|
||||
import { MinibitsClient } from './minibitsService'
|
||||
import { revertTask } from './wallet/revertTask'
|
||||
|
||||
|
||||
|
||||
export const MAX_SWAP_INPUT_SIZE = 100
|
||||
@@ -71,6 +73,16 @@ type WalletTaskService = {
|
||||
encodedInvoice: string,
|
||||
nwcEvent?: NostrEvent
|
||||
) => Promise<void>
|
||||
nwcTransfer: (
|
||||
mintBalanceToTransferFrom: MintBalance,
|
||||
amountToTransfer: number,
|
||||
feeReserve: number,
|
||||
unit: MintUnit,
|
||||
memo: string,
|
||||
invoiceExpiry: Date,
|
||||
encodedInvoice: string,
|
||||
nwcEvent: NostrEvent
|
||||
) => Promise<void>
|
||||
receive: (
|
||||
token: TokenV3,
|
||||
amountToReceive: number,
|
||||
@@ -123,8 +135,9 @@ export interface WalletTaskResult {
|
||||
|
||||
export interface TransactionTaskResult extends WalletTaskResult {
|
||||
transaction?: Transaction
|
||||
swapFeePaid?: number
|
||||
lightningFeePaid?: number
|
||||
mintFeePaid?: number
|
||||
meltFeePaid?: number
|
||||
meltQuote?: MeltQuoteResponse
|
||||
nwcEvent?: NostrEvent
|
||||
}
|
||||
@@ -199,6 +212,34 @@ const transfer = async function (
|
||||
}
|
||||
|
||||
|
||||
const nwcTransfer = async function (
|
||||
mintBalanceToTransferFrom: MintBalance,
|
||||
amountToTransfer: number,
|
||||
feeReserve: number,
|
||||
unit: MintUnit,
|
||||
memo: string,
|
||||
invoiceExpiry: Date,
|
||||
encodedInvoice: string,
|
||||
nwcEvent: NostrEvent
|
||||
): Promise<void> {
|
||||
const now = new Date().getTime()
|
||||
SyncQueue.addPrioritizedTask(
|
||||
`nwcTransferTask-${now}`,
|
||||
async () => await nwcTransferTask(
|
||||
mintBalanceToTransferFrom,
|
||||
amountToTransfer,
|
||||
feeReserve,
|
||||
unit,
|
||||
memo,
|
||||
invoiceExpiry,
|
||||
encodedInvoice,
|
||||
nwcEvent
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const receive = async function (
|
||||
token: TokenV3,
|
||||
amountToReceive: number,
|
||||
@@ -1806,7 +1847,8 @@ export const WalletTask: WalletTaskService = {
|
||||
receiveOfflineComplete,
|
||||
send,
|
||||
sendAll,
|
||||
transfer,
|
||||
transfer,
|
||||
nwcTransfer,
|
||||
topup,
|
||||
revert
|
||||
}
|
||||
|
||||
@@ -1648,9 +1648,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cashu/cashu-ts@minibits-cash/cashu-ts#commit=1a60b7859627829dc29293d78a945095c335de63":
|
||||
"@cashu/cashu-ts@minibits-cash/cashu-ts#commit=8dcac7ce6d192c419aee5547f0db03c23b593a57":
|
||||
version: 1.0.0-minibits.6
|
||||
resolution: "@cashu/cashu-ts@https://github.com/minibits-cash/cashu-ts.git#commit=1a60b7859627829dc29293d78a945095c335de63"
|
||||
resolution: "@cashu/cashu-ts@https://github.com/minibits-cash/cashu-ts.git#commit=8dcac7ce6d192c419aee5547f0db03c23b593a57"
|
||||
dependencies:
|
||||
"@cashu/crypto": ^0.2.6
|
||||
"@noble/curves": ^1.3.0
|
||||
@@ -1658,7 +1658,7 @@ __metadata:
|
||||
"@scure/bip32": ^1.3.3
|
||||
"@scure/bip39": ^1.2.2
|
||||
buffer: ^6.0.3
|
||||
checksum: 79a83bca6644836268903d45ca5e43e863dbc48fc8ac6073fb218cf1c4ad05d9b70fcfa9e83a1e5edbe55bbc6128c281a6a769c490a36e7bc3d3ebcb5c48be4d
|
||||
checksum: dfeebbfe10b99429da21848257eb8a320cb9936909487686d2847e2cdcc61390140ca4fd8da87227c66e3e1c972dc0c1e5689f7d197e55b873f202138a28189c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9426,7 +9426,7 @@ __metadata:
|
||||
"@babel/core": ^7.20.0
|
||||
"@babel/preset-env": ^7.20.0
|
||||
"@babel/runtime": ^7.20.0
|
||||
"@cashu/cashu-ts": "minibits-cash/cashu-ts#commit=1a60b7859627829dc29293d78a945095c335de63"
|
||||
"@cashu/cashu-ts": "minibits-cash/cashu-ts#commit=8dcac7ce6d192c419aee5547f0db03c23b593a57"
|
||||
"@exodus/borc": ^2.1.1
|
||||
"@fortawesome/fontawesome-svg-core": ^6.6.0
|
||||
"@fortawesome/free-brands-svg-icons": ^6.6.0
|
||||
|
||||
Reference in New Issue
Block a user