mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-06 11:48:24 +00:00
276 lines
9.9 KiB
TypeScript
276 lines
9.9 KiB
TypeScript
import {cast, flow, Instance, SnapshotIn, SnapshotOut, types} from 'mobx-state-tree'
|
|
import {withSetPropAction} from './helpers/withSetPropAction'
|
|
import type {GetInfoResponse, MintKeys, MintKeyset} from '@cashu/cashu-ts'
|
|
import {colors, getRandomIconColor} from '../theme'
|
|
import { log, MintClient } from '../services'
|
|
|
|
import AppError, { Err } from '../utils/AppError'
|
|
import { MintUnit } from '../services/wallet/currency'
|
|
import { getRootStore } from './helpers/getRootStore'
|
|
import { generateId } from '../utils/utils'
|
|
|
|
// used as a helper type across app
|
|
/* export type Balance = {
|
|
balance: number
|
|
unit: MintUnit
|
|
}*/
|
|
|
|
export type MintBalance = {
|
|
mintUrl: string
|
|
balances: {
|
|
[key in MintUnit]?: number
|
|
}
|
|
}
|
|
|
|
export type UnitBalance = {
|
|
unitBalance: number
|
|
unit: MintUnit
|
|
}
|
|
|
|
export type Balances = {
|
|
mintBalances: MintBalance[]
|
|
unitBalances: UnitBalance[]
|
|
}
|
|
|
|
|
|
|
|
export enum MintStatus {
|
|
ONLINE = 'ONLINE',
|
|
OFFLINE = 'OFFLINE'
|
|
}
|
|
|
|
export type MintProofsCounter = {
|
|
keyset: string
|
|
counter: 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
|
|
}
|
|
|
|
/**
|
|
* This represents a Cashu mint
|
|
*/
|
|
export const MintModel = types
|
|
.model('Mint', {
|
|
id: types.optional(types.identifier, () => generateId(8)),
|
|
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,
|
|
inFlightFrom: types.maybe(types.number),
|
|
inFlightTo: types.maybe(types.number),
|
|
inFlightTid: types.maybe(types.number)
|
|
})
|
|
),
|
|
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 => ({
|
|
addUnit(unit: MintUnit) {
|
|
const alreadyExists = self.units.some(u => u === unit)
|
|
|
|
if(!alreadyExists) {
|
|
self.units.push(unit)
|
|
}
|
|
|
|
self.units = cast(self.units)
|
|
},
|
|
removeUnit(unit: MintUnit) {
|
|
const index = self.units.findIndex(u => u === unit)
|
|
|
|
if(index) {
|
|
self.units.splice(index, 0)
|
|
}
|
|
|
|
self.units = cast(self.units)
|
|
},
|
|
}))
|
|
.actions(self => ({
|
|
getOrCreateProofsCounter(keysetId: string, unit?: MintUnit) {
|
|
log.trace('[getOrCreateProofsCounter]', keysetId, unit)
|
|
const counter = self.proofsCounters.find(c => c.keyset === keysetId)
|
|
|
|
if(!counter) {
|
|
if (!unit) {
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Can not create proofs counter: missing unit')
|
|
}
|
|
|
|
const newCounter = {
|
|
keyset: keysetId,
|
|
unit,
|
|
counter: 0,
|
|
}
|
|
|
|
self.proofsCounters.push(newCounter)
|
|
self.addUnit(unit)
|
|
const instance = self.proofsCounters.find(c => c.keyset === keysetId) as MintProofsCounter
|
|
|
|
log.trace('[getOrCreateProofsCounter] new', {newCounter: instance})
|
|
return instance
|
|
}
|
|
|
|
if(unit && counter.unit !== unit) {
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Mismatch of proofsCounter keyset and passed unit', {counter, unit})
|
|
}
|
|
|
|
return counter as MintProofsCounter
|
|
},
|
|
findInFlightProofsCounter() {
|
|
const counter = self.proofsCounters.find(c => c.inFlightFrom && c.inFlightTo && c.inFlightTid)
|
|
return counter as MintProofsCounter | undefined
|
|
},
|
|
findInFlightProofsCounterByTId(tId: number) {
|
|
const counter = self.proofsCounters.find(c => c.inFlightTid === tId)
|
|
return counter as MintProofsCounter | undefined
|
|
},
|
|
validateURL(url: string) {
|
|
try {
|
|
new URL(url)
|
|
return true
|
|
} catch (e) {
|
|
return false
|
|
}
|
|
},
|
|
}))
|
|
.actions(self => ({
|
|
getProofsCounterByUnit: flow(function* getProofsCounterByUnit(unit: MintUnit) {
|
|
// Retrieve current keys for this unit from new or existing cashu-ts wallet instance
|
|
const keys: MintKeys = (yield MintClient.getWallet(self.mintUrl, unit)).keys
|
|
|
|
// Get or create new proofs counter for this keyset
|
|
const counter = self.getOrCreateProofsCounter(keys.id, unit)
|
|
return counter
|
|
}),
|
|
setHostname() {
|
|
try {
|
|
self.hostname = new URL(self.mintUrl).hostname
|
|
} catch (e) {
|
|
return false
|
|
}
|
|
},
|
|
setMintUrl(url: string) {
|
|
if(self.validateURL(url)) {
|
|
const mintsStore = getRootStore(self).mintsStore
|
|
|
|
if(!mintsStore.alreadyExists(url)) {
|
|
|
|
const proofsStore = getRootStore(self).proofsStore
|
|
proofsStore.updateMintUrl(self.mintUrl, url) // update mintUrl on mint's proofs
|
|
self.mintUrl = url
|
|
|
|
return true
|
|
}
|
|
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Mint URL already exists.', {url})
|
|
} else {
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Invalid Mint URL.', {url})
|
|
}
|
|
},
|
|
setId() { // migration
|
|
self.id = generateId(8)
|
|
},
|
|
setShortname: flow(function* setShortname() {
|
|
// get name from URL as a fallback
|
|
const lastSlashIndex = self.mintUrl.lastIndexOf('/')
|
|
let shortname = self.mintUrl.substring(lastSlashIndex + 1).slice(0, 25)
|
|
|
|
try {
|
|
const info: GetInfoResponse = yield MintClient.getMintInfo(self.mintUrl)
|
|
|
|
if(info.name.length > 0) {
|
|
shortname = info.name
|
|
}
|
|
|
|
self.shortname = shortname
|
|
|
|
} catch (e) {
|
|
self.shortname = shortname
|
|
}
|
|
}),
|
|
setRandomColor() {
|
|
self.color = getRandomIconColor()
|
|
},
|
|
setColor(color: string) {
|
|
self.color = color
|
|
},
|
|
setStatus(status: MintStatus) {
|
|
self.status = status
|
|
},
|
|
setInFlight(keyset: string, options: {inFlightFrom: number, inFlightTo: number, inFlightTid: number}) {
|
|
const counter = self.getOrCreateProofsCounter(keyset)
|
|
|
|
counter.inFlightFrom = options.inFlightFrom
|
|
counter.inFlightTo = options.inFlightTo
|
|
counter.counter = options.inFlightTo // temp increase of main counter value
|
|
counter.inFlightTid = options.inFlightTid
|
|
|
|
log.trace('[setInFlight]', 'Lock and inflight indexes were set', counter)
|
|
|
|
self.proofsCounters = cast(self.proofsCounters)
|
|
},
|
|
resetInFlight(inFlightTid: number) {
|
|
const counter = self.findInFlightProofsCounterByTId(inFlightTid)
|
|
|
|
if(!counter) {
|
|
log.warn('[resetInFlight]', 'Could not find counter locked by inFlightTid', {inFlightTid})
|
|
return
|
|
}
|
|
|
|
counter.inFlightFrom = undefined
|
|
counter.inFlightTo = undefined
|
|
counter.inFlightTid = undefined
|
|
|
|
log.trace('[resetInFlight]', 'Lock and inflight indexes were reset', {inFlightTid})
|
|
|
|
self.proofsCounters = cast(self.proofsCounters)
|
|
},
|
|
increaseProofsCounter(keyset: string, numberOfProofs: number) {
|
|
const counter = self.getOrCreateProofsCounter(keyset)
|
|
counter.counter += numberOfProofs
|
|
log.trace('[increaseProofsCounter]', 'Increased proofsCounter', {numberOfProofs, counter})
|
|
|
|
// Make sure to cast the frozen array back to a mutable array
|
|
self.proofsCounters = cast(self.proofsCounters)
|
|
},
|
|
decreaseProofsCounter(keyset: string, numberOfProofs: number) {
|
|
const counter = self.getOrCreateProofsCounter(keyset)
|
|
counter.counter -= numberOfProofs
|
|
Math.max(0, counter.counter)
|
|
log.trace('[decreaseProofsCounter]', 'Decreased proofsCounter', {numberOfProofs, counter})
|
|
|
|
self.proofsCounters = cast(self.proofsCounters)
|
|
},
|
|
resetCounters() {
|
|
for(const counter of self.proofsCounters) {
|
|
log.warn('Resetting counter', counter.keyset)
|
|
counter.counter = 0
|
|
}
|
|
|
|
self.proofsCounters = cast(self.proofsCounters)
|
|
},
|
|
}))
|
|
.views(self => ({
|
|
get balances(): MintBalance | undefined {
|
|
const mintBalance: MintBalance | undefined = getRootStore(self).proofsStore.getMintBalance(self.mintUrl)
|
|
return mintBalance
|
|
},
|
|
get keysets(): string[] {
|
|
return self.proofsCounters.map(c => c.keyset)
|
|
}
|
|
}))
|
|
|
|
|
|
|
|
export type Mint = {
|
|
mintUrl: string
|
|
} & Partial<Instance<typeof MintModel>>
|
|
export interface MintSnapshotOut extends SnapshotOut<typeof MintModel> {}
|
|
export interface MintSnapshotIn extends SnapshotIn<typeof MintModel> {} |