Revamp mint keysets and keys management, melt swap fees

This commit is contained in:
minibits-cash
2024-07-23 15:46:17 +02:00
parent fdf2ca7ea7
commit b03850761f
13 changed files with 622 additions and 394 deletions
+1 -1
View File
@@ -19,7 +19,7 @@
"postinstall": "patch-package --exclude 'nothing'"
},
"dependencies": {
"@cashu/cashu-ts": "minibits-cash/cashu-ts#1.0.0-minibits",
"@cashu/cashu-ts": "minibits-cash/cashu-ts#1.0.0-minibits.2",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-regular-svg-icons": "^6.5.2",
+187 -76
View File
@@ -1,6 +1,11 @@
import {cast, flow, Instance, SnapshotIn, SnapshotOut, types} from 'mobx-state-tree'
import {cast, flow, getSnapshot, Instance, SnapshotIn, SnapshotOut, types} from 'mobx-state-tree'
import {withSetPropAction} from './helpers/withSetPropAction'
import type {CashuWallet, GetInfoResponse, MintKeys, MintKeyset} from '@cashu/cashu-ts'
import type {
CashuWallet,
GetInfoResponse,
MintKeys as CashuMintKeys,
MintKeyset as CashuMintKeyset
} from '@cashu/cashu-ts'
import {colors, getRandomIconColor} from '../theme'
import { log, MintClient } from '../services'
@@ -33,8 +38,6 @@ export type Balances = {
unitBalances: UnitBalance[]
}
export enum MintStatus {
ONLINE = 'ONLINE',
OFFLINE = 'OFFLINE'
@@ -43,13 +46,21 @@ export enum MintStatus {
export type MintProofsCounter = {
keyset: string
counter: number
unit: MintUnit
input_fee_ppk: number
unit: MintUnit
inFlightFrom?: number // starting counter index for pending split request sent to mint (for recovery from failure to receive proofs)
inFlightTo?: number // last counter index for pending split request sent to mint
inFlightTid?: number // related tx id
}
export const MintProofsCounterModel = types
.model('MintProofsCounter', {
keyset: types.string,
unit: types.optional(types.frozen<MintUnit>(), 'sat'),
counter: types.number,
inFlightFrom: types.maybe(types.number),
inFlightTo: types.maybe(types.number),
inFlightTid: types.maybe(types.number)
})
/**
* This represents a Cashu mint
*/
@@ -59,24 +70,61 @@ export const MintModel = types
mintUrl: types.string,
hostname: types.maybe(types.string),
shortname: types.maybe(types.string),
units: types.array(types.frozen<MintUnit>()),
proofsCounters: types.array(
types.model('MintProofsCounter', {
keyset: types.string,
unit: types.optional(types.frozen<MintUnit>(), 'sat'),
counter: types.number,
input_fee_ppk: types.optional(types.number, 0),
inFlightFrom: types.maybe(types.number),
inFlightTo: types.maybe(types.number),
inFlightTid: types.maybe(types.number)
})
),
units: types.array(types.frozen<MintUnit>()),
keysets: types.array(types.frozen<CashuMintKeyset>()),
keys: types.array(types.frozen<CashuMintKeys>()),
proofsCounters: types.array(MintProofsCounterModel),
color: types.optional(types.string, colors.palette.iconBlue200),
status: types.optional(types.frozen<MintStatus>(), MintStatus.ONLINE),
createdAt: types.optional(types.Date, new Date()),
})
.actions(withSetPropAction) // TODO? start to use across app to avoid pure setter methods, e.g. mint.setProp('color', '#ccc')
.actions(self => ({
addKeyset(keyset: CashuMintKeyset) {
const alreadyExists = self.keysets.some(k => k.id === keyset.id)
if(!alreadyExists) {
self.keysets.push(keyset)
}
self.keysets = cast(self.keysets)
},
removeKeyset(keyset: CashuMintKeyset) {
const index = self.keysets.findIndex(k => k.id === keyset.id)
if(index) {
self.keysets.splice(index, 0)
}
self.keysets = cast(self.keysets)
},
setIsActive(freshKeyset: CashuMintKeyset) {
const keyset = self.keysets.find(k => k.id === freshKeyset.id)
if(keyset) {
keyset.active = freshKeyset.active
}
self.keysets = cast(self.keysets)
},
addKeys(keys: CashuMintKeys) {
const alreadyExists = self.keys.some(k => k.id === keys.id)
if(!alreadyExists) {
self.keys.push(keys)
}
self.keys = cast(self.keys)
},
removeKeys(keys: CashuMintKeys) {
const index = self.keys.findIndex(k => k.id === keys.id)
if(index) {
self.keys.splice(index, 0)
}
self.keys = cast(self.keys)
},
addUnit(unit: MintUnit) {
const alreadyExists = self.units.some(u => u === unit)
@@ -95,43 +143,99 @@ export const MintModel = types
self.units = cast(self.units)
},
addProofsCounter(counter: MintProofsCounter) {
const alreadyExists = self.proofsCounters.some(p => p.keyset === counter.keyset)
if(!alreadyExists) {
self.proofsCounters.push(counter)
}
self.proofsCounters = cast(self.proofsCounters)
},
removeProofsCounter(counter: MintProofsCounter) {
const index = self.proofsCounters.findIndex(p => p.keyset === counter.keyset)
if(index) {
self.proofsCounters.splice(index, 0)
}
self.proofsCounters = cast(self.proofsCounters)
},
getProofsCounter(keysetId: string) {
const counter = self.proofsCounters.find(c => c.keyset === keysetId)
return counter
},
isUnitSupported(unit: MintUnit): boolean {
return MintUnits.includes(unit) ? true : false
},
keysetExists(keyset: CashuMintKeyset): boolean {
return self.keysets.some(k => k.id === keyset.id)
},
}))
.actions(self => ({
createProofsCounter(keyset: MintKeyset) {
// Do not add unit the wallet does not have configured
if (!MintUnits.includes(keyset.unit as MintUnit)) {
throw new AppError(Err.VALIDATION_ERROR, `Unsupported unit provided by the mint: ${keyset.unit}`)
}
const existing = self.getProofsCounter(keyset.id)
if(existing) { // update fees if they can change (?)
if (existing.unit !== keyset.unit) {
throw new AppError(Err.VALIDATION_ERROR, `Keyset unit mismatch, got ${keyset.unit}, expected ${existing.unit}`)
}
existing.input_fee_ppk = keyset.input_fee_ppk || 0
self.proofsCounters = cast(self.proofsCounters)
return existing
}
.actions(self => ({
createProofsCounter(keyset: CashuMintKeyset) {
const newCounter: MintProofsCounter = {
keyset: keyset.id,
unit: keyset.unit as MintUnit,
input_fee_ppk: keyset.input_fee_ppk || 0,
unit: keyset.unit as MintUnit,
counter: 0,
}
self.proofsCounters.push(newCounter)
self.addUnit(keyset.unit as MintUnit)
const instance = self.proofsCounters.find(c => c.keyset === keyset.id) as MintProofsCounter
const proofsCounterInstance = MintProofsCounterModel.create(newCounter)
self.addProofsCounter(newCounter)
log.trace('[ceateProofsCounter]', {newCounter: getSnapshot(proofsCounterInstance)})
return proofsCounterInstance
}
}))
.actions(self => ({
initKeyset(keyset: CashuMintKeyset) {
// Do not add unit the wallet does not have configured
if(!self.isUnitSupported(keyset.unit as MintUnit)) {
throw new AppError(Err.VALIDATION_ERROR, `Unsupported unit provided by the mint: ${keyset.unit}`)
}
const existing = self.keysets.find(k => k.id === keyset.id)
log.trace('[ceateProofsCounter]', {newCounter: instance})
return instance
if(existing) {
if (existing.unit !== keyset.unit) {
throw new AppError(Err.VALIDATION_ERROR, `Keyset unit mismatch, got ${keyset.unit}, expected ${existing.unit}`)
}
return existing
}
if(!keyset.input_fee_ppk) {
keyset.input_fee_ppk = 0
}
if(!keyset.unit) {
keyset.unit = 'sat'
}
self.addKeyset(keyset)
self.addUnit(keyset.unit as MintUnit)
self.createProofsCounter(keyset)
log.trace('[initKeyset]', {newKeyset: keyset})
},
initKeys(key: CashuMintKeys) {
// Do not add unit the wallet does not have configured
if(!self.isUnitSupported(key.unit as MintUnit)) {
throw new AppError(Err.VALIDATION_ERROR, `Unsupported unit provided by the mint: ${key.unit}`)
}
const existing = self.keys.find(k => k.id === key.id)
if(existing) {
if (existing.unit !== key.unit) {
throw new AppError(Err.VALIDATION_ERROR, `Keyset unit mismatch, got ${key.unit}, expected ${existing.unit}`)
}
return existing
}
self.addKeys(key)
log.trace('[initKeys]', {newKeys: key.id})
},
findInFlightProofsCounter() {
const counter = self.proofsCounters.find(c => c.inFlightFrom && c.inFlightTo && c.inFlightTid)
@@ -151,32 +255,39 @@ export const MintModel = types
},
}))
.actions(self => ({
getProofsCounterByUnit: flow(function* getProofsCounterByUnit(unit: MintUnit) {
try {
// Refresh current keys for this unit from new or existing in-memory wallet instance
const wallet: CashuWallet = yield MintClient.getWallet(self.mintUrl, unit)
const keyset = wallet.keyset
// Get or create new proofs counter for this keyset
const counter = self.getProofsCounter(keyset.id)
if(!counter) {
return self.createProofsCounter(keyset)
}
return counter
} catch(e: any) {
log.warn('[getProofsCounterByUnit] Could not refresh keyset, using mint proofsCounter for unit', {unit})
const proofsCounterInstance = self.proofsCounters.find(c => c.unit === unit)
if(!proofsCounterInstance) {
throw new AppError(Err.NOTFOUND_ERROR, 'Could not get keyset to create new mint proofsCounter for unit', {unit})
}
return proofsCounterInstance
refreshKeysets(freshKeysets: CashuMintKeyset[]) {
// add new keyset if not exists
for (const keyset of freshKeysets) {
self.initKeyset(keyset)
self.setIsActive(keyset)
}
}),
},
refreshKeys(freshKeys: CashuMintKeys[]) {
for (const key of freshKeys) {
self.initKeys(key)
}
},
getProofsCounterByUnit(unit: MintUnit, useActiveKeyset: boolean = true) {
let keyset: CashuMintKeyset | undefined
if(useActiveKeyset) {
keyset = self.keysets.find(k => k.active === true && k.unit === unit)
} else {
keyset = self.keysets.find(k => k.active === false && k.unit === unit)
}
if(!keyset) {
throw new AppError(Err.NOTFOUND_ERROR, 'Mint has no keyset for this unit', {unit})
}
const counter = self.proofsCounters.find(p => p.keyset === keyset?.id)
if(!counter) {
return self.createProofsCounter(keyset)
}
return counter
},
setHostname() {
try {
self.hostname = new URL(self.mintUrl).hostname
@@ -299,10 +410,10 @@ export const MintModel = types
self.proofsCounters = cast(self.proofsCounters)
},
getFeesForProofs(proofs: Proof[]): number {
// Find the corresponding MintProofsCounter for each proof and sum the input fees
// Find the corresponding keyset for each proof and sum the input fees
const totalInputFees = proofs.reduce((sum, proof) => {
const counter = self.proofsCounters.find(pc => pc.keyset === proof.id)
return counter ? sum + counter.input_fee_ppk : sum
const keyset = self.keysets.find(k => k.id === proof.id)
return keyset && keyset.input_fee_ppk ? sum + keyset.input_fee_ppk : sum
}, 0)
// Calculate the fees
@@ -317,15 +428,15 @@ export const MintModel = types
const mintBalance: MintBalance | undefined = getRootStore(self).proofsStore.getMintBalance(self.mintUrl)
return mintBalance
},
get keysets(): string[] {
return self.proofsCounters.map(c => c.keyset)
}
get keysetIds(): string[] {
return self.keysets.map(k => k.id)
}
}))
export type Mint = {
mintUrl: string
mintUrl: string
} & Partial<Instance<typeof MintModel>>
export interface MintSnapshotOut extends SnapshotOut<typeof MintModel> {}
export interface MintSnapshotIn extends SnapshotIn<typeof MintModel> {}
+158 -151
View File
@@ -12,157 +12,164 @@ import {
import {log} from '../services/logService'
import { MintClient } from '../services'
import AppError, { Err } from '../utils/AppError'
import { MintKeyset } from '@cashu/cashu-ts'
import type {
MintKeys as CashuMintKeys,
MintKeyset as CashuMintKeyset
} from '@cashu/cashu-ts'
import { MintUnit, MintUnits } from '../services/wallet/currency'
export type MintsByHostname = {
hostname: string
mints: Mint[]
}
export type MintsByHostname = {
hostname: string
mints: Mint[]
}
export type MintsByUnit = {
export type MintsByUnit = {
unit: MintUnit
mints: Mint[]
}
export const MintsStoreModel = types
.model('MintsStore', {
mints: types.array(MintModel),
blockedMintUrls: types.array(types.string),
})
.views(self => ({
findByUrl: (mintUrl: string | URL) => {
const mint = self.mints.find(m => m.mintUrl === mintUrl)
return mint ? mint : undefined
},
}))
.actions(withSetPropAction)
.actions(self => ({
addMint: flow(function* addMint(mintUrl: string) {
if(!mintUrl.includes('.onion') && !mintUrl.startsWith('https')) {
throw new AppError(Err.VALIDATION_ERROR, 'Mint URL needs to start with https')
}
// create default wallet instance then download and cache up to date mint keys in that instance
const allKeysets: MintKeyset[] = yield MintClient.getMintKeysets(mintUrl)
const activeKeysets = allKeysets.filter(keyset => keyset.active === true)
}
if(!activeKeysets || activeKeysets.length === 0) {
throw new AppError(Err.VALIDATION_ERROR, 'Mint has no active keysets and is not operational', {mintUrl})
}
const newMint: Mint = {
mintUrl,
}
const mintInstance = MintModel.create(newMint)
mintInstance.setHostname()
mintInstance.setRandomColor()
export const MintsStoreModel = types
.model('MintsStore', {
mints: types.array(MintModel),
blockedMintUrls: types.array(types.string),
})
.views(self => ({
findByUrl: (mintUrl: string | URL) => {
const mint = self.mints.find(m => m.mintUrl === mintUrl)
return mint ? mint : undefined
},
}))
.actions(withSetPropAction)
.actions(self => ({
addMint: flow(function* addMint(mintUrl: string) {
if(!mintUrl.includes('.onion') && !mintUrl.startsWith('https')) {
throw new AppError(Err.VALIDATION_ERROR, 'Mint URL needs to start with https.')
}
for(const keyset of activeKeysets) {
try {
if(keyset.active === true) {
mintInstance.createProofsCounter(keyset)
}
} catch(e: any) {
continue
}
// create default wallet instance then download and cache up to date mint keys in that instance
const keysets: CashuMintKeyset[] = yield MintClient.getMintKeysets(mintUrl)
const keys: CashuMintKeys[] = yield MintClient.getMintKeys(mintUrl)
if(!keysets || keysets.length === 0 || !keys || keys.length === 0) {
throw new AppError(Err.VALIDATION_ERROR, 'Mint has no keysets and is not operational.', {mintUrl})
}
const mintInstance = MintModel.create({mintUrl})
for(const keyset of keysets) {
if (!mintInstance.isUnitSupported(keyset.unit as MintUnit)) {
continue
}
yield mintInstance.setShortname()
self.mints.push(mintInstance)
}),
updateMint: flow(function* updateMint(mintUrl: string) {
mintInstance.initKeyset(keyset)
}
for(const key of keys) {
if (!mintInstance.isUnitSupported(key.unit as MintUnit)) {
continue
}
mintInstance.initKeys(key)
}
mintInstance.setHostname()
yield mintInstance.setShortname()
self.mints.push(mintInstance)
}),
updateMint: flow(function* updateMint(mintUrl: string) {
const mintInstance = self.findByUrl(mintUrl)
if(!mintInstance) {
throw new AppError(Err.VALIDATION_ERROR, 'Could not find mint to update', {mintUrl})
}
// refresh up to date mint keys
const allKeysets: MintKeyset[] = yield MintClient.getMintKeysets(mintUrl)
const keysets: CashuMintKeyset[] = yield MintClient.getMintKeysets(mintUrl)
const keys: CashuMintKeys[] = yield MintClient.getMintKeys(mintUrl)
if(!allKeysets || allKeysets.length === 0) {
throw new AppError(Err.VALIDATION_ERROR, 'Mint has no active keysets and is not operational', {mintUrl})
if(!keysets || keysets.length === 0 || !keys || keys.length === 0) {
throw new AppError(Err.VALIDATION_ERROR, 'Mint has no keysets and is not operational', {mintUrl})
}
for(const keyset of allKeysets) {
if(keyset.active === true) {
// Do not add unit the wallet does not have configured
if (!MintUnits.includes(keyset.unit as MintUnit)) {
log.error(`Unsupported unit provided by the mint: ${keyset.unit}`)
continue
}
mintInstance.addUnit(keyset.unit as MintUnit) // add supported units by mint if not yet exist
mintInstance.createProofsCounter(keyset) // create proofsCounters or update fees if exists
for(const keyset of keysets) {
if (!mintInstance.isUnitSupported(keyset.unit as MintUnit)) {
continue
}
mintInstance.initKeyset(keyset)
}
for(const key of keys) {
if (!mintInstance.isUnitSupported(key.unit as MintUnit)) {
continue
}
mintInstance.initKeys(key)
}
yield mintInstance.setShortname()
}),
removeMint(mintToBeRemoved: Mint) {
if (self.blockedMintUrls.some(m => m === mintToBeRemoved.mintUrl)) {
self.blockedMintUrls.remove(mintToBeRemoved.mintUrl)
log.debug('[removeMint]', 'Mint removed from blockedMintUrls')
}
let mintInstance: Mint | undefined
if (isStateTreeNode(mintToBeRemoved)) {
mintInstance = mintToBeRemoved
} else {
mintInstance = self.findByUrl((mintToBeRemoved as Mint).mintUrl)
}
if (mintInstance) {
detach(mintInstance)
destroy(mintInstance)
log.info('[removeMint]', 'Mint removed from MintsStore')
}
},
blockMint(mintToBeBlocked: Mint) {
if(self.blockedMintUrls.some(url => url === mintToBeBlocked.mintUrl)) {
return
}
self.blockedMintUrls.push(mintToBeBlocked.mintUrl)
log.debug('[blockMint]', 'Mint blocked in MintsStore')
},
unblockMint(blockedMint: Mint) {
self.blockedMintUrls.remove(blockedMint.mintUrl)
log.debug('[unblockMint]', 'Mint unblocked in MintsStore')
}
}))
.views(self => ({
get mintCount() {
return self.mints.length
},
get allMints() {
return self.mints
},
get groupedByHostname() {
const grouped: Record<string, MintsByHostname> = {}
self.mints.forEach((mint: Mint) => {
const {hostname} = mint
if (!grouped[hostname as string]) {
grouped[hostname as string] = {
hostname,
mints: [],
}
}
grouped[hostname as string].mints.push(mint)
})
return Object.values(grouped) as MintsByHostname[]
},
get groupedByUnit() {
}),
removeMint(mintToBeRemoved: Mint) {
if (self.blockedMintUrls.some(m => m === mintToBeRemoved.mintUrl)) {
self.blockedMintUrls.remove(mintToBeRemoved.mintUrl)
log.debug('[removeMint]', 'Mint removed from blockedMintUrls')
}
let mintInstance: Mint | undefined
if (isStateTreeNode(mintToBeRemoved)) {
mintInstance = mintToBeRemoved
} else {
mintInstance = self.findByUrl((mintToBeRemoved as Mint).mintUrl)
}
if (mintInstance) {
detach(mintInstance)
destroy(mintInstance)
log.info('[removeMint]', 'Mint removed from MintsStore')
}
},
blockMint(mintToBeBlocked: Mint) {
if(self.blockedMintUrls.some(url => url === mintToBeBlocked.mintUrl)) {
return
}
self.blockedMintUrls.push(mintToBeBlocked.mintUrl)
log.debug('[blockMint]', 'Mint blocked in MintsStore')
},
unblockMint(blockedMint: Mint) {
self.blockedMintUrls.remove(blockedMint.mintUrl)
log.debug('[unblockMint]', 'Mint unblocked in MintsStore')
}
}))
.views(self => ({
get mintCount() {
return self.mints.length
},
get allMints() {
return self.mints
},
get groupedByHostname() {
const grouped: Record<string, MintsByHostname> = {}
self.mints.forEach((mint: Mint) => {
const {hostname} = mint
if (!grouped[hostname as string]) {
grouped[hostname as string] = {
hostname,
mints: [],
}
}
grouped[hostname as string].mints.push(mint)
})
return Object.values(grouped) as MintsByHostname[]
},
get groupedByUnit() {
const groupedByUnit: Record<string, MintsByUnit> = {}
self.mints.forEach(mint => {
@@ -175,29 +182,29 @@ import { MintUnit, MintUnits } from '../services/wallet/currency'
})
return Object.values(groupedByUnit) as MintsByUnit[]
},
alreadyExists(mintUrl: string) {
return self.mints.some(m => m.mintUrl === mintUrl) ? true : false
},
isBlocked(mintUrl: string) {
return self.blockedMintUrls.some(m => m === mintUrl) ? true : false
},
getBlockedFromList(mintUrls: string[]) {
return mintUrls.filter(mintUrl =>
self.blockedMintUrls.some(blockedUrl => blockedUrl === mintUrl),
)
},
getMissingMints: (mintUrls: string[]) => {
const missingMints: string[] = []
for (const url of mintUrls) {
if (!self.mints.find(mint => mint.mintUrl === url)) {
missingMints.push(url)
}
}
return missingMints
},
}))
},
alreadyExists(mintUrl: string) {
return self.mints.some(m => m.mintUrl === mintUrl) ? true : false
},
isBlocked(mintUrl: string) {
return self.blockedMintUrls.some(m => m === mintUrl) ? true : false
},
getBlockedFromList(mintUrls: string[]) {
return mintUrls.filter(mintUrl =>
self.blockedMintUrls.some(blockedUrl => blockedUrl === mintUrl),
)
},
getMissingMints: (mintUrls: string[]) => {
const missingMints: string[] = []
for (const url of mintUrls) {
if (!self.mints.find(mint => mint.mintUrl === url)) {
missingMints.push(url)
}
}
return missingMints
},
}))
export interface MintsStore extends Instance<typeof MintsStoreModel> {}
export interface MintsStoreSnapshot
export interface MintsStore extends Instance<typeof MintsStoreModel> {}
export interface MintsStoreSnapshot
extends SnapshotOut<typeof MintsStoreModel> {}
+1 -2
View File
@@ -17,7 +17,7 @@ import {
import * as Sentry from '@sentry/react-native'
import RNExitApp from 'react-native-exit-app'
import type {RootStore} from '../RootStore'
import {KeyChain, MinibitsClient, MMKVStorage, NostrClient} from '../../services'
import {KeyChain, MinibitsClient, MintClient, MMKVStorage, NostrClient} from '../../services'
import {Database} from '../../services'
import { log } from '../../services/logService'
import { rootStoreModelVersion } from '../RootStore'
@@ -285,7 +285,6 @@ async function _runMigrations(rootStore: RootStore) {
log.warn('[setupRootStore] Migration error', {message: e.name})
}
}
} catch (e: any) {
throw new AppError(
Err.STORAGE_ERROR,
+2 -2
View File
@@ -142,8 +142,8 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
toggleAddMintModal() // close
setIsLoading(true)
const activeKeysets: MintKeyset[] = await MintClient.getMintKeysets(mintUrl)
const matchingKeyset = activeKeysets.find(keyset => selectedMint.keysets?.some(k => k === keyset.id))
const keysets: MintKeyset[] = await MintClient.getMintKeysets(mintUrl)
const matchingKeyset = keysets.find(keyset => selectedMint.keysets?.some(k => k.id === keyset.id))
if(!matchingKeyset) {
throw new AppError(Err.VALIDATION_ERROR, 'No keyset match, provided URL likely points to different mint.')
+1 -1
View File
@@ -261,9 +261,9 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
recoveredMint.increaseProofsCounter(selectedKeyset.id as string, Math.abs(endIndex - startIndex))
const {spent, pending} = await MintClient.getSpentOrPendingProofsFromMint(
proofs as Proof[],
recoveredMint.mintUrl,
selectedKeyset.unit as MintUnit,
proofs as Proof[],
)
log.debug('[restore]', `Spent and pending proofs`, {spent: spent.length, pending: pending.length})
+182 -94
View File
@@ -8,6 +8,9 @@ import {
deriveKeysetId,
setGlobalRequestOptions,
type Proof as CashuProof,
type MintKeyset as CashuMintKeyset,
type MintKeys as CashuMintKeys,
MintKeys
} from '@cashu/cashu-ts'
import {rootStoreInstance} from '../models'
import { KeyChain } from '../services'
@@ -22,7 +25,7 @@ import { deriveSeedFromMnemonic } from '@cashu/cashu-ts'
import { isObj } from '@cashu/cashu-ts/src/utils'
import { JS_BUNDLE_VERSION } from '@env'
import { MintUnit } from './wallet/currency'
import { getSnapshot } from 'mobx-state-tree'
let _mints: CashuMint[] = []
let _wallets: CashuWallet[] = [] // used where seed is not required (perf)
@@ -37,8 +40,8 @@ function isOnionMint(mintUrl: string) {
}
const TorVPNSetupInstructions = `
Is your Tor VPN running?
Mints on Tor require a Tor VPN like Orbot. You can get it on Google Play or Github (guardianproject/orbot).
Tor can sometimes be slow, so requests might time out. If Orbot is running, try again in a bit.`
Mints on Tor require a Tor VPN application like Orbot. You can get it on Google Play or Github.`
const getOrCreateMnemonic = async function (): Promise<string> {
let mnemonic: string | undefined = undefined
@@ -93,7 +96,7 @@ const getSeed = async function (): Promise<Uint8Array | undefined> {
}
const getMint = function (mintUrl: string): CashuMint {
const getMint = async function (mintUrl: string): Promise<CashuMint> {
const mint = _mints.find(m => m.mintUrl === mintUrl)
@@ -104,8 +107,30 @@ const getMint = function (mintUrl: string): CashuMint {
setGlobalRequestOptions({
headers: {'User-Agent': `Minibits/${JS_BUNDLE_VERSION}`}
})
// create cashu-ts mint instance
const newMint = new CashuMint(mintUrl)
// get fresh keysets
const {keysets} = await newMint.getKeySets()
// get mint model from wallet state
const mintInstance = mintsStore.findByUrl(mintUrl)
if(!mintInstance) {
throw new AppError(Err.NOTFOUND_ERROR, 'Missing mint in the wallet state', {mintUrl})
}
// check if we've got new keysets not existing in wallet state
const newKeysets = keysets.filter(freshKeyset => {
return !mintInstance.keysets.some(keyset => keyset.id === freshKeyset.id);
})
if(newKeysets.length > 0) {
// if we heve new keysets, get and sync new keys
const {keysets} = await newMint.getKeys()
mintInstance.refreshKeys(keysets)
}
// sync wallet state with fresh keysets, active statuses and keys
mintInstance.refreshKeysets(keysets)
// store cashu-ts mint instance in memory
_mints.push(newMint)
return newMint
@@ -117,13 +142,72 @@ const getWallet = async function (
mintUrl: string,
unit: MintUnit,
options?: {
keysetId?: string // force specific (inactive) keys to swap from
withSeed: boolean
}
): Promise<CashuWallet> {
const cashuMint = getMint(mintUrl)
// syncs mint model in wallet state and returns cashu-ts mint class instance
const cashuMint = await getMint(mintUrl)
// mint model from wallet state
const mintInstance = mintsStore.findByUrl(mintUrl)
if(!mintInstance) {
throw new AppError(Err.NOTFOUND_ERROR, 'Mint not found in the wallet state.', {
mintUrl
})
}
// select keys to be used to find or create new cashu-ts wallet instance
let walletKeys: MintKeys
if(options && options.keysetId) {
const requestedKeys = mintInstance.keys.find(k => k.id === options.keysetId)
if(!requestedKeys) {
throw new AppError(Err.NOTFOUND_ERROR, 'Wallet has not keys with provided keyset id.', {
mintUrl,
keysetId: options.keysetId
})
}
if(requestedKeys.unit !== unit) {
throw new AppError(Err.VALIDATION_ERROR, 'Wallet unit and keys mismatch.', {
mintUrl,
keysetId: options.keysetId,
unit
})
}
walletKeys = requestedKeys
} else {
// if not we find first active keyset and related keys
const activeKeyset = mintInstance.keysets.find(k => k.unit === unit && k.active === true)
if(!activeKeyset) {
throw new AppError(Err.VALIDATION_ERROR, 'Wallet has not any active keyset for the selected unit.', {
mintUrl,
unit
})
}
const activeKeys = mintInstance.keys.find(k => k.id === activeKeyset.id)
if(!activeKeys) {
throw new AppError(Err.VALIDATION_ERROR, 'Wallet has not any keys for the selected unit.', {
mintUrl,
unit
})
}
walletKeys = getSnapshot(activeKeys)
}
if (options && options.withSeed) {
const seedWallet = _seedWallets.find(w => w.mint.mintUrl === mintUrl && w.unit === unit)
const seedWallet = _seedWallets.find(
w => w.mint.mintUrl === mintUrl &&
w.keys.id === walletKeys.id
)
if (seedWallet) {
return seedWallet
@@ -131,62 +215,37 @@ const getWallet = async function (
let seed: Uint8Array | undefined = undefined
seed = await getSeed()
// Handle legacy pre-0.1.5 created wallets
if(!seed) {
const mnemonic = await getOrCreateMnemonic()
seed = await getSeed()
resetCachedWallets() // force all wallet instances to be recreated with seed
}
const newSeedWallet = new CashuWallet(cashuMint, {
unit,
keys: walletKeys,
mnemonicOrSeed: seed
})
try {
// make sure we have keys for wallet unit cached in wallet instance
const keys = await newSeedWallet.getKeys(undefined, unit)
_seedWallets.push(newSeedWallet)
if(!keys || keys.unit !== unit) {
throw new AppError(Err.VALIDATION_ERROR, `This mint does not currently support unit ${unit}`)
}
_seedWallets.push(newSeedWallet)
log.trace('[getWallet]', 'Returning new cashuWallet instance with seed')
return newSeedWallet
} catch (e: any) {
let message = 'Could not connect to the selected mint.'
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
throw new AppError(Err.NETWORK_ERROR, message, {message: e.message, caller: 'getWallet'})
}
log.trace('[getWallet]', 'Returning new cashuWallet instance with seed')
return newSeedWallet
}
const wallet = _wallets.find(w => w.mint.mintUrl === mintUrl && w.unit === unit)
const wallet = _wallets.find(
w => w.mint.mintUrl === mintUrl &&
w.keys.id === walletKeys.id
)
if (wallet) {
return wallet
}
const newWallet = new CashuWallet(cashuMint, {
unit,
keys: walletKeys,
mnemonicOrSeed: undefined
})
try {
await newWallet.getKeys(undefined, unit)
_wallets.push(newWallet)
_wallets.push(newWallet)
log.trace('[getWallet]', 'Returning new cashuWallet instance')
return newWallet
} catch (e: any) {
let message = 'Could not connect to the selected mint.'
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
throw new AppError(Err.NETWORK_ERROR, message, {message: e.message, caller: 'getWallet'})
}
log.trace('[getWallet]', 'Returning new cashuWallet instance')
return newWallet
}
@@ -198,7 +257,7 @@ const resetCachedWallets = function () {
const getMintKeysets = async function (mintUrl: string) {
const cashuMint = getMint(mintUrl)
const cashuMint = await getMint(mintUrl)
try {
const {keysets} = await cashuMint.getKeySets() // all
@@ -211,23 +270,46 @@ const getMintKeysets = async function (mintUrl: string) {
}
const receiveFromMint = async function (
const getMintKeys = async function (mintUrl: string) {
const cashuMint = await getMint(mintUrl)
try {
const {keysets} = await cashuMint.getKeys() // all
return keysets
} catch (e: any) {
let message = 'Could not connect to the selected mint.'
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
throw new AppError(Err.CONNECTION_ERROR, message, { message: e.message, mintUrl })
}
}
const receive = async function (
mintUrl: string,
unit: MintUnit,
decodedToken: CashuToken,
amountPreferences: AmountPreference[],
counter: number
options: {
preference: AmountPreference[],
counter: number
}
) {
try {
const cashuWallet = await getWallet(mintUrl, unit, {withSeed: true}) // with seed
const cashuWallet = await getWallet(
mintUrl,
unit,
{
withSeed: true,
})
const proofs = await cashuWallet.receive(decodedToken, {
keysetId: cashuWallet.keys.id,
preference: amountPreferences,
counter,
pubkey: undefined,
privkey: undefined
})
const proofs = await cashuWallet.receive(
decodedToken,
{
keysetId: cashuWallet.keys.id,
preference: options.preference,
counter: options.counter,
pubkey: undefined,
privkey: undefined
})
return proofs
} catch (e: any) {
@@ -237,59 +319,60 @@ const receiveFromMint = async function (
const sendFromMint = async function (
const send = async function (
mintUrl: string,
unit: MintUnit,
amountToSend: number,
unit: MintUnit,
proofsToSendFrom: Proof[],
amountPreferences: AmountPreference[],
counter: number
options: {
preference: AmountPreference[],
counter: number
}
) {
try {
const cashuWallet = await getWallet(mintUrl, unit, {withSeed: true}) // with seed
const cashuWallet = await getWallet(
mintUrl,
unit,
{
withSeed: true,
})
log.debug('[MintClient.sendFromMint] counter', counter)
log.debug('[MintClient.sendFromMint] counter', options.counter)
const {returnChange, send} = await cashuWallet.send(
amountToSend,
proofsToSendFrom,
{
keysetId: cashuWallet.keys.id,
preference: amountPreferences,
counter,
preference: options.preference,
counter: options.counter,
pubkey: undefined,
privkey: undefined
}
)
log.debug('[MintClient.sendFromMint] returnedProofs', returnChange)
log.debug('[MintClient.sendFromMint] sentProofs', send)
log.debug('[MintClient.sendFromMint] proofsToSend', send)
// do some basic validations that proof amounts from mints match
const totalAmountToSendFrom = CashuUtils.getProofsAmount(proofsToSendFrom)
const returnedAmount = CashuUtils.getProofsAmount(returnChange as Proof[])
const proofsAmount = CashuUtils.getProofsAmount(send as Proof[])
const sendAmount = CashuUtils.getProofsAmount(send as Proof[])
if (proofsAmount !== amountToSend) {
if (sendAmount !== amountToSend) {
throw new AppError(
Err.VALIDATION_ERROR,
`Amount to be sent does not equal requested original amount. Original is ${amountToSend}, mint returned ${proofsAmount}`,
`Amount to be sent provided by mint does not equal requested amount. Original is ${amountToSend}, mint returned ${sendAmount}`,
)
}
if (totalAmountToSendFrom !== returnedAmount + proofsAmount) {
throw new AppError(
Err.VALIDATION_ERROR,
`Amount returned by the mint as a change ${returnedAmount} is incorrect, it should be ${
totalAmountToSendFrom - proofsAmount
}`,
)
}
const mintFeePaid = totalAmountToSendFrom - amountToSend - returnedAmount
// we normalize naming of returned parameters
return {
returnedProofs: returnChange as Proof[],
proofsToSend: send as Proof[],
proofsToSend: send as Proof[],
mintFeePaid
}
} catch (e: any) {
let message = 'The mint could not return signatures necessary for this transaction'
@@ -310,9 +393,9 @@ const sendFromMint = async function (
const getSpentOrPendingProofsFromMint = async function (
proofs: Proof[],
mintUrl: string,
unit: MintUnit,
proofs: Proof[],
) {
try {
@@ -350,7 +433,7 @@ const createLightningMeltQuote = async function (
encodedInvoice: string,
) {
try {
const cashuMint = getMint(mintUrl)
const cashuMint = await getMint(mintUrl)
const lightningQuote: MeltQuoteResponse = await cashuMint.createMeltQuote({
unit,
request: encodedInvoice
@@ -382,7 +465,9 @@ const payLightningMelt = async function (
unit: MintUnit,
lightningMeltQuote: MeltQuoteResponse, // invoice is stored by mint by quote
proofsToPayFrom: CashuProof[], // proofAmount >= amount + fee_reserve
counter: number
options: {
counter: number
}
) {
try {
const cashuWallet = await getWallet(mintUrl, unit, {withSeed: true}) // with seed
@@ -393,7 +478,7 @@ const payLightningMelt = async function (
proofsToPayFrom,
{
keysetId: cashuWallet.keys.id,
counter
counter: options.counter
}
)
@@ -427,7 +512,7 @@ const createLightningMintQuote = async function (
amount: number,
) {
try {
const cashuMint = getMint(mintUrl)
const cashuMint = await getMint(mintUrl)
const {
request: encodedInvoice,
quote: mintQuote,
@@ -463,7 +548,7 @@ const checkLightningMintQuote = async function (
quote: string,
) {
try {
const cashuMint = getMint(mintUrl)
const cashuMint = await getMint(mintUrl)
const {
request: encodedInvoice,
quote: mintQuote,
@@ -498,11 +583,13 @@ const checkLightningMintQuote = async function (
const mintProofs = async function (
mintUrl: string,
unit: MintUnit,
amount: number,
unit: MintUnit,
mintQuote: string,
amountPreferences: AmountPreference[],
counter: number
options: {
preference: AmountPreference[],
counter: number
}
) {
try {
const cashuWallet = await getWallet(mintUrl, unit, {withSeed: true}) // with seed
@@ -512,8 +599,8 @@ const mintProofs = async function (
mintQuote,
{
keysetId: cashuWallet.keys.id,
preference: amountPreferences,
counter,
preference: options.preference,
counter: options.counter,
pubkey: undefined
}
)
@@ -557,7 +644,7 @@ const restore = async function (
try {
const {indexFrom, indexTo, keysetId} = options
// need special wallet instance to pass seed and keysetId directly
const cashuMint = getMint(mintUrl)
const cashuMint = await getMint(mintUrl)
const seedWallet = new CashuWallet(cashuMint, {
unit: 'sat', // just use default unit as we restore by keyset
@@ -589,7 +676,7 @@ const getMintInfo = async function (
mintUrl: string,
) {
try {
const cashuMint = getMint(mintUrl)
const cashuMint = await getMint(mintUrl)
const info = await cashuMint.getInfo()
log.trace('[getMintInfo]', {info})
return info
@@ -616,8 +703,9 @@ export const MintClient = {
getSeed,
resetCachedWallets,
getMintKeysets,
receiveFromMint,
sendFromMint,
getMintKeys,
receive,
send,
getSpentOrPendingProofsFromMint,
createLightningMintQuote,
checkLightningMintQuote,
+17 -13
View File
@@ -17,7 +17,7 @@ import {
import { getDefaultAmountPreference, getEncodedToken } from '@cashu/cashu-ts/src/utils'
import { TransactionTaskResult } from '../walletService'
import { WalletUtils } from './utils'
import { formatCurrency, getCurrency } from './currency'
import { MintUnit, formatCurrency, getCurrency } from './currency'
const {
mintsStore,
@@ -124,7 +124,7 @@ export const receiveTask = async function (
const amountPreferences = getDefaultAmountPreference(amountToReceive)
const countOfInFlightProofs = CashuUtils.getAmountPreferencesCount(amountPreferences)
log.trace('[receiveTask]', 'proofsCounter initial state', {proofsCounter: await mintInstance.getProofsCounterByUnit?.(unit)})
log.trace('[receiveTask]', 'proofsCounter initial state', {proofsCounter: mintInstance.getProofsCounterByUnit?.(unit)})
log.trace('[receiveTask]', 'amountPreferences', {amountPreferences, transactionId})
log.trace('[receiveTask]', 'countOfInFlightProofs', {countOfInFlightProofs, transactionId})
@@ -132,14 +132,16 @@ export const receiveTask = async function (
await WalletUtils.lockAndSetInFlight(mintInstance, unit, countOfInFlightProofs, transactionId)
// get locked counter values
const lockedProofsCounter = await mintInstance.getProofsCounterByUnit?.(unit)
const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit)
const receivedProofs = await MintClient.receiveFromMint(
const receivedProofs = await MintClient.receive(
mintToReceive,
unit,
unit as MintUnit,
token,
amountPreferences,
lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase
{
preference: amountPreferences,
counter: lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase
}
)
// If we've got valid response, decrease proofsCounter and let it be increased back in next step when adding proofs
@@ -443,15 +445,17 @@ export const receiveOfflineCompleteTask = async function (
await WalletUtils.lockAndSetInFlight(mintInstance, unit, countOfInFlightProofs, transaction.id as number)
// get locked counter values
const lockedProofsCounter = await mintInstance.getProofsCounterByUnit?.(unit)
const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit)
const receivedProofs = await MintClient.receiveFromMint(
const receivedProofs = await MintClient.receive(
mintToReceive,
unit,
unit as MintUnit,
token,
amountPreferences,
lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase
)
{
preference: amountPreferences,
counter: lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase
}
)
// If we've got valid response, decrease proofsCounter and let it be increased back in next step when adding proofs
mintInstance.decreaseProofsCounter(lockedProofsCounter.keyset, countOfInFlightProofs)
+35 -25
View File
@@ -76,7 +76,7 @@ export const sendTask = async function (
transactionId = storedTransaction.id as number
// get ready proofs to send and update proofs and pending proofs storage
const proofsToSend = await sendFromMint(
const {proofs: proofsToSend, mintFeePaid, mintFeeReserve} = await sendFromMint(
mintBalanceToSendFrom,
amountToSend,
unit,
@@ -87,7 +87,8 @@ export const sendTask = async function (
// Update transaction status
transactionData.push({
status: TransactionStatus.PREPARED,
//proofsToSend,
mintFeeReserve,
mintFeePaid,
createdAt: new Date(),
})
@@ -115,7 +116,7 @@ export const sendTask = async function (
// Update transaction status
transactionData.push({
status: TransactionStatus.PENDING,
status: TransactionStatus.PENDING,
encodedTokenToSend,
createdAt: new Date(),
})
@@ -153,6 +154,7 @@ export const sendTask = async function (
transaction: pendingTransaction,
message: '',
encodedTokenToSend,
mintFeePaid
} as TransactionTaskResult
} catch (e: any) {
// Update transaction status if we have any
@@ -249,7 +251,11 @@ export const sendFromMint = async function (
})
// We return cleaned proofs to be encoded as a sendable token
return cleanedProofsToSend
return {
proofs: cleanedProofsToSend,
mintFeeReserve: 0,
mintFeePaid: 0
}
}
/*
@@ -261,28 +267,27 @@ export const sendFromMint = async function (
proofsFromMint,
)
let proofsToSendFromAmount = CashuUtils.getProofsAmount(proofsToSendFrom)
let proofsToSendFromAmount = CashuUtils.getProofsAmount(proofsToSendFrom)
// swap will happen if we could not select proofs equal to amountToSend
let returnedAmount = proofsToSendFromAmount - amountToSend
let feesAmount: number = 0
let mintFeeReserve: number = 0
if(returnedAmount > 0) {
feesAmount = mintInstance.getFeesForProofs(proofsToSendFrom)
mintFeeReserve = mintInstance.getFeesForProofs(proofsToSendFrom)
// if we did not selected enough proofs to cover the fees we need some more
if(feesAmount > returnedAmount) {
const missingFeesAmount = feesAmount - returnedAmount
if(mintFeeReserve > returnedAmount) {
const missingFeesAmount = mintFeeReserve - returnedAmount
const remainingProofs = proofsStore.getProofsSubset(proofsFromMint, proofsToSendFrom)
const remainingProofsAmount = CashuUtils.getProofsAmount(remainingProofs)
log.warn('[sendFromMint]', 'Not enough proofs to cover feeAmount', {feesAmount, returnedAmount})
log.warn('[sendFromMint]', 'Not enough proofs to cover feeReserve', {mintFeeReserve, returnedAmount})
if(missingFeesAmount > remainingProofsAmount) {
throw new AppError(
Err.VALIDATION_ERROR,
'There is not enough funds to send this payment with expected fees',
{totalAmountFromMint, amountToSend, feesAmount},
{totalAmountFromMint, amountToSend, mintFeeReserve},
)
}
@@ -297,8 +302,8 @@ export const sendFromMint = async function (
returnedAmount = proofsToSendFromAmount - amountToSend
}
// decrease requested returned outputs by fees so mint can charge them
returnedAmount -= feesAmount
// decrease requested returned outputs by fees so that the mint can charge them
returnedAmount -= mintFeeReserve
}
// Inputs we are about to send
@@ -319,19 +324,20 @@ export const sendFromMint = async function (
await WalletUtils.lockAndSetInFlight(mintInstance, unit, countOfInFlightProofs, transactionId)
// get locked counter values
const lockedProofsCounter = await mintInstance.getProofsCounterByUnit?.(unit)
const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit)
// if split to required denominations was necessary, this gets it done with the mint and we get the return
const {returnedProofs, proofsToSend} = await MintClient.sendFromMint(
const {returnedProofs, proofsToSend, mintFeePaid} = await MintClient.send(
mintUrl,
unit,
amountToSend,
unit,
proofsToSendFrom,
amountPreferences,
lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase
)
{
preference: amountPreferences,
counter: lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase
}
)
// If we've got valid response, decrease proofsCounter and let it be increased back in next step when adding proofs
mintInstance.decreaseProofsCounter(lockedProofsCounter.keyset, countOfInFlightProofs)
@@ -339,7 +345,7 @@ export const sendFromMint = async function (
// add proofs returned by the mint after the split
if (returnedProofs.length > 0) {
log.trace('[sendFromMint] add returned proofs to spendable')
const { addedProofs, addedAmount } = WalletUtils.addCashuProofs(
WalletUtils.addCashuProofs(
mintUrl,
returnedProofs,
{
@@ -356,7 +362,7 @@ export const sendFromMint = async function (
// these might be original proofToSendFrom if they matched the exact amount and split was not necessary
log.trace('[sendFromMint] add proofsToSend to pending')
const { addedProofs, addedAmount } = WalletUtils.addCashuProofs(
WalletUtils.addCashuProofs(
mintUrl,
proofsToSend,
{
@@ -380,8 +386,12 @@ export const sendFromMint = async function (
}
})
// We return cleaned proofs to be encoded as a sendable token
return cleanedProofsToSend
// We return cleaned proofs to be encoded as a sendable token + fees
return {
proofs: cleanedProofsToSend as CashuProof[],
mintFeeReserve,
mintFeePaid
}
} catch (e: any) {
// release lock
mintInstance?.resetInFlight(transactionId)
+19 -14
View File
@@ -55,7 +55,7 @@ export const transferTask = async function (
try {
if (amountToTransfer + meltQuote.fee_reserve > mintBalanceToTransferFrom.balances[unit]!) {
throw new AppError(Err.VALIDATION_ERROR, 'Mint balance is insufficient to cover the amount to transfer with expected Lightning fees.')
throw new AppError(Err.VALIDATION_ERROR, 'Mint balance is insufficient to cover the amount to transfer with the expected Lightning fees.')
}
if(isBefore(invoiceExpiry, new Date())) {
@@ -87,7 +87,7 @@ export const transferTask = async function (
transactionId = storedTransaction.id as number
// get proofs ready to be paid to the mint
proofsToPay = await sendFromMint(
const {proofs: proofsToPay, mintFeePaid, mintFeeReserve} = await sendFromMint(
mintBalanceToTransferFrom,
amountToTransfer + meltQuote.fee_reserve,
unit,
@@ -101,6 +101,8 @@ export const transferTask = async function (
// Update transaction status
transactionData.push({
status: TransactionStatus.PREPARED,
mintFeeReserve,
mintFeePaid,
createdAt: new Date(),
})
@@ -116,14 +118,16 @@ export const transferTask = async function (
await WalletUtils.lockAndSetInFlight(mintInstance, unit, countOfInFlightProofs, transactionId)
// get locked counter values
const lockedProofsCounter = await mintInstance.getProofsCounterByUnit?.(unit)
const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit)
const {isPaid, preimage, feeSavedProofs} = await MintClient.payLightningMelt(
mintUrl,
unit,
meltQuote,
proofsToPay,
lockedProofsCounter.inFlightFrom as number
proofsToPay,
{
counter: lockedProofsCounter.inFlightFrom as number
}
)
mintInstance.decreaseProofsCounter(lockedProofsCounter.keyset, countOfInFlightProofs)
@@ -178,10 +182,9 @@ export const transferTask = async function (
}
// If real fees were less then estimated, cash the returned savings.
let finalFee = meltQuote.fee_reserve
let lightningFeePaid = meltQuote.fee_reserve
if (feeSavedProofs.length) {
if (feeSavedProofs.length) {
const {addedAmount: feeSaved} = WalletUtils.addCashuProofs(
mintUrl,
feeSavedProofs,
@@ -193,19 +196,20 @@ export const transferTask = async function (
)
finalFee = meltQuote.fee_reserve - feeSaved
lightningFeePaid = meltQuote.fee_reserve - feeSaved
}
// release lock
mintInstance.resetInFlight(transactionId)
// Save final fee in db
await transactionsStore.updateFee(transactionId, finalFee)
await transactionsStore.updateFee(transactionId, lightningFeePaid + mintFeePaid)
// Update transaction status
transactionData.push({
status: TransactionStatus.COMPLETED,
finalFee,
lightningFeeReserve: meltQuote.fee_reserve,
lightningFeePaid,
preimage, // TODO add to tx details
createdAt: new Date(),
})
@@ -226,7 +230,8 @@ export const transferTask = async function (
mintUrl,
transaction: completedTransaction,
message: `Lightning invoice has been successfully paid and settled with your Minibits ecash. Final network fee has been ${formatCurrency(finalFee, getCurrency(unit).code)} ${getCurrency(unit).code}.`,
finalFee,
lightningFeePaid,
mintFeePaid
} as TransactionTaskResult
} catch (e: any) {
// Update transaction status if we have any
@@ -324,9 +329,9 @@ const _moveProofsFromPending = async function (
// due to the timeout but mint's node keeps the payment as in-flight (e.g. receiving node holds the invoice)
// In this case we need to keep such proofs as pending and not move them back to wallet as in other payment failures.
const {pending: pendingByMint} = await MintClient.getSpentOrPendingProofsFromMint(
proofsToMove as Proof[],
mintUrl,
unit,
unit,
proofsToMove as Proof[]
)
let amountPendingByMint: number = 0
+1 -1
View File
@@ -31,7 +31,7 @@ const lockAndSetInFlight = async function (
retryCount: number = 0,
): Promise<void> {
const currentCounter = await mint.getProofsCounterByUnit?.(unit)
const currentCounter = mint.getProofsCounterByUnit?.(unit)
log.trace('[lockAndSetInFlight] proofsCounter', {currentCounter})
if(!retryCount) {
+13 -9
View File
@@ -100,6 +100,8 @@ export interface WalletTaskResult {
export interface TransactionTaskResult extends WalletTaskResult {
transaction?: Transaction
lightningFeePaid?: number
mintFeePaid?: number
}
export type ReceivedEventResult = {
@@ -362,9 +364,9 @@ const _handleSpentByMintTask = async function (
spent: spentProofs,
pending: pendingProofs
} = await MintClient.getSpentOrPendingProofsFromMint(
proofsFromMint,
mintUrl,
mint && mint.units ? mint.units[0] : 'sat'
mint && mint.units ? mint.units[0] : 'sat',
proofsFromMint
)
if(mint) {
@@ -603,9 +605,9 @@ const _handleInFlightByMintTask = async function (mint: Mint, seed: Uint8Array):
}
const {spent, pending} = await MintClient.getSpentOrPendingProofsFromMint(
proofs as Proof[],
mint.mintUrl,
mint.units ? mint.units[0] : 'sat'
mint.units ? mint.units[0] : 'sat',
proofs as Proof[]
)
const spentCount = spent.length
@@ -819,17 +821,19 @@ const _handlePendingTopupTask = async function (params: {paymentRequest: Payment
)
// get locked counter values
const lockedProofsCounter = await mintInstance.getProofsCounterByUnit(unit)
const lockedProofsCounter = mintInstance.getProofsCounterByUnit(unit)
let proofs: CashuProof[] = []
proofs = (await MintClient.mintProofs(
mint as string,
unit,
amount,
mintQuote as string,
amountPreferences,
lockedProofsCounter.inFlightFrom as number
unit,
mintQuote,
{
preference: amountPreferences,
counter: lockedProofsCounter.inFlightFrom as number
}
)) as CashuProof[]
mintInstance.decreaseProofsCounter(lockedProofsCounter.keyset, countOfInFlightProofs)
+5 -5
View File
@@ -1202,9 +1202,9 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@cashu/cashu-ts@minibits-cash/cashu-ts#1.0.0-minibits":
"@cashu/cashu-ts@minibits-cash/cashu-ts#1.0.0-minibits.2":
version "1.0.0-minibits"
resolved "https://codeload.github.com/minibits-cash/cashu-ts/tar.gz/95cd37801f54700bed1b84113a774bd23f36aecb"
resolved "https://codeload.github.com/minibits-cash/cashu-ts/tar.gz/03b26c1db3a72d3d8e975a72f63b0ed06dc1be34"
dependencies:
"@cashu/crypto" "^0.2.6"
"@noble/curves" "^1.3.0"
@@ -1214,9 +1214,9 @@
buffer "^6.0.3"
"@cashu/crypto@^0.2.6":
version "0.2.6"
resolved "https://registry.yarnpkg.com/@cashu/crypto/-/crypto-0.2.6.tgz#fbc173f1a5c380085f62b5141ae62d309a427426"
integrity sha512-qjytcY26MRntG6nJc9U2tSeDw+BApKQaIch58POjEiTuc7MbIxgR/l/xU5NzXa/nGrSLdNZQwl/o5RQDhc2otw==
version "0.2.7"
resolved "https://registry.yarnpkg.com/@cashu/crypto/-/crypto-0.2.7.tgz#26467fd2f5ecfeabcc74b9c65189a1e8c6ce85ce"
integrity sha512-1aaDfUjiHNXoJqg8nW+341TLWV9W28DsVNXJUKcHL0yAmwLs5+56SSnb8LLDJzPamLVoYL0U0bda91klAzptig==
dependencies:
"@noble/curves" "^1.3.0"
"@noble/hashes" "^1.3.3"