Files
minibits_wallet/src/services/wallet/topupTask.ts
T

210 lines
6.5 KiB
TypeScript

import {rootStoreInstance} from '../../models'
import { TransactionTaskResult, WalletTask } from '../walletService'
import { MintBalance } from '../../models/Mint'
import { poller } from '../../utils/poller'
import { Transaction, TransactionData, TransactionStatus, TransactionType } from '../../models/Transaction'
import { log } from '../logService'
import { Contact } from '../../models/Contact'
import { LightningUtils } from '../lightning/lightningUtils'
import { getSnapshot, isStateTreeNode } from 'mobx-state-tree'
import { WalletUtils } from './utils'
import { MintUnit } from './currency'
import { NostrEvent } from '../nostrService'
import { Err } from '../../utils/AppError'
import { CashuMint, CashuWallet, MintQuoteResponse } from '@cashu/cashu-ts'
import { addSeconds } from 'date-fns/addSeconds'
const {
transactionsStore,
walletProfileStore,
walletStore
} = rootStoreInstance
// const {walletStore} = nonPersistedStores
export const TOPUP_TASK = 'topupTask'
export const topupTask = async function (
mintBalanceToTopup: MintBalance,
amountToTopup: number,
unit: MintUnit,
memo: string,
contactToSendTo?: Contact,
nwcEvent?: NostrEvent
) : Promise<TransactionTaskResult> {
log.info('[topupTask]', {mintBalanceToTopup})
log.info('[topupTask]', {amountToTopup, unit})
// create draft transaction
const transactionData: TransactionData[] = [
{
status: TransactionStatus.DRAFT,
amountToTopup,
unit,
createdAt: new Date(),
},
]
let transaction: Transaction | undefined = undefined
const mintUrl = mintBalanceToTopup.mintUrl
try {
const newTransaction = {
type: TransactionType.TOPUP,
amount: amountToTopup,
fee: 0,
unit,
data: JSON.stringify(transactionData),
memo,
mint: mintUrl,
status: TransactionStatus.DRAFT,
}
// store tx in db and in the model
transaction = await transactionsStore.addTransaction(newTransaction)
const {
encodedInvoice,
mintQuote
} = await walletStore.createLightningMintQuote(
mintUrl,
unit,
amountToTopup,
memo
)
const decodedInvoice = LightningUtils.decodeInvoice(encodedInvoice)
const {
amount,
payment_hash: paymentHash,
expiry,
timestamp
} = LightningUtils.getInvoiceData(decodedInvoice)
// sender is current wallet profile
const {
pubkey,
npub,
name,
nip05,
picture,
} = walletProfileStore
const contactFrom: Contact = {
pubkey,
npub,
name,
nip05,
picture
}
// Private contacts are stored in model, public ones are plain objects
// contactToSendTo is to whom to send the request
const contactTo = isStateTreeNode(contactToSendTo) ? getSnapshot(contactToSendTo) : contactToSendTo
// Bulk update quote, paymentId, paymentRequest, expiresAt, sentFrom, sentTo
let expiresAtDate: Date | undefined = undefined
if(expiry && expiry > 0) {
expiresAtDate = addSeconds(new Date(timestamp * 1000), expiry)
} else {
expiresAtDate = addSeconds(new Date(timestamp * 1000), 86400)
}
const sentFromValue = contactTo?.nip05 || contactTo?.name || ''
const sentToValue = walletProfileStore.nip05 || ''
log.trace('[topupTask]', expiresAtDate)
transaction.update({
quote: mintQuote,
paymentId: paymentHash,
paymentRequest: encodedInvoice,
expiresAt: expiresAtDate,
sentFrom: sentFromValue,
sentTo: sentToValue
})
log.trace('[topupTask] invoice', {amount, paymentHash, expiry, timestamp, expiresAtDate})
transactionData.push({
status: TransactionStatus.PENDING,
quote: mintQuote,
createdAt: new Date()
})
// Update status and data in one call
transaction.update({
status: TransactionStatus.PENDING,
data: JSON.stringify(transactionData)
})
if(!nwcEvent) {
const wsMint = new CashuMint(mintUrl)
const wsWallet = new CashuWallet(wsMint)
try {
const unsub = await wsWallet.onMintQuotePaid(
mintQuote,
async (m: MintQuoteResponse) => {
log.trace(`Websocket: mint quote PAID: ${m.quote}`)
WalletTask.handlePendingTopupQueue({transaction})
unsub()
},
async (error: any) => {
throw error
}
)
} catch (error: any) {
log.error(Err.NETWORK_ERROR,
"Error in websocket subscription. Starting poller.",
error.message
)
poller(
`handlePendingTopupPoller-${paymentHash}`,
WalletTask.handlePendingTopupQueue,
{
interval: 10 * 1000,
maxPolls: 6,
maxErrors: 2
},
{transaction})
.then(() => log.trace('Polling completed', [], `handlePendingTopupPoller`))
}
}
return {
taskFunction: TOPUP_TASK,
mintUrl,
transaction,
message: '',
encodedInvoice,
nwcEvent
} as TransactionTaskResult
} catch (e: any) {
if (transaction) {
transactionData.push({
status: TransactionStatus.ERROR,
error: WalletUtils.formatError(e),
createdAt: new Date()
})
// Update status and data on error
transaction.update({
status: TransactionStatus.ERROR,
data: JSON.stringify(transactionData)
})
}
log.error(e.name, e.message)
return {
taskFunction: TOPUP_TASK,
transaction,
message: e.message,
error: WalletUtils.formatError(e),
} as TransactionTaskResult
}
}