Finalize first fees implementation

This commit is contained in:
minibits-cash
2024-07-25 21:44:06 +02:00
parent b03850761f
commit a9da94338b
12 changed files with 392 additions and 236 deletions
+1
View File
@@ -468,6 +468,7 @@
"invoice": "Lightning invoice to pay",
"isOffline": "Redeem online",
"lightningFee": "Lightning network fee",
"fee": "Fee",
"memoFromInvoice": "Memo from invoice",
"memoFromSender": "Memo from sender",
"receiveOfflineComplete": "Redeem to wallet",
+45 -33
View File
@@ -15,6 +15,7 @@ import { getRootStore } from './helpers/getRootStore'
import { generateId } from '../utils/utils'
import { Proof } from './Proof'
// used as a helper type across app
/* export type Balance = {
balance: number
@@ -52,7 +53,7 @@ export type MintProofsCounter = {
inFlightTid?: number // related tx id
}
export const MintProofsCounterModel = types
/* export const MintProofsCounterModel = types
.model('MintProofsCounter', {
keyset: types.string,
unit: types.optional(types.frozen<MintUnit>(), 'sat'),
@@ -60,7 +61,7 @@ export const MintProofsCounterModel = types
inFlightFrom: types.maybe(types.number),
inFlightTo: types.maybe(types.number),
inFlightTid: types.maybe(types.number)
})
})*/
/**
* This represents a Cashu mint
*/
@@ -73,7 +74,16 @@ export const MintModel = types
units: types.array(types.frozen<MintUnit>()),
keysets: types.array(types.frozen<CashuMintKeyset>()),
keys: types.array(types.frozen<CashuMintKeys>()),
proofsCounters: types.array(MintProofsCounterModel),
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()),
@@ -147,6 +157,7 @@ export const MintModel = types
const alreadyExists = self.proofsCounters.some(p => p.keyset === counter.keyset)
if(!alreadyExists) {
log.trace('[addProofsCounter]', {counter})
self.proofsCounters.push(counter)
}
@@ -180,43 +191,44 @@ export const MintModel = types
counter: 0,
}
const proofsCounterInstance = MintProofsCounterModel.create(newCounter)
self.addProofsCounter(newCounter)
log.trace('[ceateProofsCounter]', {newCounter: getSnapshot(proofsCounterInstance)})
return proofsCounterInstance
return self.getProofsCounter(keyset.id)
}
}))
.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)
if(existing) {
if (existing.unit !== keyset.unit) {
throw new AppError(Err.VALIDATION_ERROR, `Keyset unit mismatch, got ${keyset.unit}, expected ${existing.unit}`)
}
try {
if(!self.isUnitSupported(keyset.unit as MintUnit)) {
throw new AppError(Err.VALIDATION_ERROR, `Unsupported unit provided by the mint: ${keyset.unit}`)
}
return existing
}
const existing = self.keysets.find(k => k.id === keyset.id)
if(!keyset.input_fee_ppk) {
keyset.input_fee_ppk = 0
}
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.unit) {
keyset.unit = 'sat'
}
if(!keyset.input_fee_ppk) {
keyset.input_fee_ppk = 0
}
self.addKeyset(keyset)
self.addUnit(keyset.unit as MintUnit)
self.createProofsCounter(keyset)
if(!keyset.unit) {
keyset.unit = 'sat'
}
log.trace('[initKeyset]', {newKeyset: keyset})
self.addKeyset(keyset)
self.addUnit(keyset.unit as MintUnit)
self.createProofsCounter(keyset)
log.trace('[initKeyset]', {newKeyset: keyset})
} catch (e: any) {
throw new AppError(Err.WALLET_ERROR, '[initKeyset] ' + e.message)
}
},
initKeys(key: CashuMintKeys) {
// Do not add unit the wallet does not have configured
@@ -409,7 +421,7 @@ export const MintModel = types
self.proofsCounters = cast(self.proofsCounters)
},
getFeesForProofs(proofs: Proof[]): number {
getMintFeeReserve(proofs: Proof[]): number {
// Find the corresponding keyset for each proof and sum the input fees
const totalInputFees = proofs.reduce((sum, proof) => {
const keyset = self.keysets.find(k => k.id === proof.id)
@@ -417,10 +429,10 @@ export const MintModel = types
}, 0)
// Calculate the fees
const fees = Math.max(Math.floor((totalInputFees + 999) / 1000), 0)
const feeReserve = Math.max(Math.floor((totalInputFees + 999) / 1000), 0)
log.debug('*** [getFeesForProofs]', {fees})
return fees
log.debug('[getMintFeeReserve]', {feeReserve})
return feeReserve
}
}))
.views(self => ({
+16 -6
View File
@@ -45,15 +45,25 @@ export const ProofsStoreModel = types
getByMint(
mintUrl: string,
options: {
unit?: MintUnit,
isPending: boolean,
unit?: MintUnit,
keysetIds?: string[]
}
): Proof[] | undefined {
const proofs = options.isPending ? self.pendingProofs : self.proofs
let proofs: Proof[] = []
if(options.keysetIds && options.keysetIds.length > 0) {
proofs = options.isPending ?
self.pendingProofs.filter(p => options.keysetIds?.includes(p.id)) :
self.proofs.filter(p => options.keysetIds?.includes(p.id))
} else {
proofs = options.isPending ? self.pendingProofs : self.proofs
}
if (options.unit) {
return proofs.filter(proof => proof.mintUrl === mintUrl && proof.unit === options.unit)
return proofs.filter(
proof => proof.mintUrl === mintUrl &&
proof.unit === options.unit
)
.slice()
.sort((a, b) => b.amount - a.amount)
}
@@ -62,7 +72,7 @@ export const ProofsStoreModel = types
.slice()
.sort((a, b) => b.amount - a.amount)
},
},
getProofInstance(proof: Proof, isPending: boolean = false) {
let proofInstance: Proof | undefined
if (isStateTreeNode(proof)) {
+3 -3
View File
@@ -389,9 +389,6 @@ export const ReceiveScreen: FC<WalletStackScreenProps<'Receive'>> = observer(
)}
</>
)}
{isLoading && <Loading />}
{error && <ErrorModal error={error} />}
{info && <InfoModal message={info} />}
</View>
<BottomModal
isVisible={isResultModalVisible ? true : false}
@@ -454,6 +451,9 @@ export const ReceiveScreen: FC<WalletStackScreenProps<'Receive'>> = observer(
onBackButtonPress={toggleResultModal}
onBackdropPress={toggleResultModal}
/>
{isLoading && <Loading />}
{error && <ErrorModal error={error} />}
{info && <InfoModal message={info} />}
</Screen>
)
},
+4 -4
View File
@@ -304,7 +304,7 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
contactToSendTo?.picture
)
return navigation.navigate('Wallet', {})
// return navigation.navigate('Wallet', {})
} catch(e: any) {
log.error(e.name, e.message) // silent
}
@@ -687,9 +687,6 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
</View>
</View>
)}
{isLoading && <Loading />}
{error && <ErrorModal error={error} />}
{info && <InfoModal message={info} />}
</View>
<BottomModal
isVisible={isProofSelectorModalVisible}
@@ -781,6 +778,9 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
onBackButtonPress={toggleResultModal}
onBackdropPress={toggleResultModal}
/>
{isLoading && <Loading />}
{error && <ErrorModal error={error} />}
{info && <InfoModal message={info} />}
</Screen>
)
}
+30 -4
View File
@@ -556,6 +556,12 @@ const ReceiveInfoBlock = function (props: {
label="tranDetailScreen.type"
value={transaction.type as string}
/>
<TranItem
label="tranDetailScreen.fee"
value={transaction.fee}
unit={transaction.unit}
isCurrency={true}
/>
{encodedTokenToRetry ? (
<View
style={{flexDirection: 'row', justifyContent: 'space-between'}}>
@@ -808,6 +814,12 @@ const ReceiveOfflineInfoBlock = function (props: {
label="tranDetailScreen.type"
value={transaction.type as string}
/>
<TranItem
label="tranDetailScreen.fee"
value={transaction.fee}
unit={transaction.unit}
isCurrency={true}
/>
<TranItem
label="tranDetailScreen.status"
value={transaction.status as string}
@@ -962,6 +974,12 @@ const SendInfoBlock = 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.PENDING ? (
<View
style={{flexDirection: 'row', justifyContent: 'space-between'}}>
@@ -1202,6 +1220,12 @@ const TopupInfoBlock = function (props: {
label="tranDetailScreen.type"
value={transaction.type as string}
/>
<TranItem
label="tranDetailScreen.fee"
value={transaction.fee}
unit={transaction.unit}
isCurrency={true}
/>
{paymentRequest && isInternetReachable ? (
<View
style={{flexDirection: 'row', justifyContent: 'space-between'}}>
@@ -1408,10 +1432,6 @@ const TransferInfoBlock = function (props: {
isCurrency={true}
isFirst={true}
/>
<TranItem
label="tranDetailScreen.lightningFee"
value={`${transaction.fee}`}
/>
{transaction.memo && (
<TranItem
label="tranDetailScreen.memoFromInvoice"
@@ -1428,6 +1448,12 @@ const TransferInfoBlock = function (props: {
label="tranDetailScreen.type"
value={transaction.type as string}
/>
<TranItem
label="tranDetailScreen.fee"
value={transaction.fee}
unit={transaction.unit}
isCurrency={true}
/>
<TranItem
label="tranDetailScreen.status"
value={transaction.status as string}
+60 -72
View File
@@ -151,31 +151,6 @@ const getMintsFromToken = function (token: Token): string[] {
}
const updateMintProofs = function (
token: Token,
mintUrl: string,
updatedProofs: Proof[],
): Token {
// Find the index of the mint to update
const mintIndex = token.token.findIndex(mint => mint.mint === mintUrl)
if (mintIndex === -1) {
throw new AppError(
Err.VALIDATION_ERROR,
`Mint ${mintUrl} not found in token`,
)
}
// Clone the token instance
const updatedToken: Token = cloneDeep(token)
// Update the proofs for the specified mint
updatedToken.token[mintIndex].proofs = updatedProofs
return updatedToken
}
const getProofsFromTokenEntries = (tokenEntries: TokenEntry[]) => {
const proofs: Proof[] = []
@@ -187,6 +162,63 @@ const getProofsFromTokenEntries = (tokenEntries: TokenEntry[]) => {
}
const findExactMatch = function (requestedAmount: number, proofs: Proof[]): Proof[] | null {
const result: Proof[] = [];
function backtrack(start: number, remaining: number): boolean {
if (remaining === 0) {
return true;
}
for (let i = start; i < proofs.length; i++) {
if (proofs[i].amount > remaining) continue;
result.push(proofs[i]);
if (backtrack(i + 1, remaining - proofs[i].amount)) {
return true;
}
result.pop();
}
return false;
}
proofs.sort((a, b) => b.amount - a.amount);
if (backtrack(0, requestedAmount)) {
return result;
}
return null;
}
const findMinExcess = function (requestedAmount: number, proofs: Proof[]): Proof[] {
proofs.sort((a, b) => b.amount - a.amount);
const selectedProofs: Proof[] = [];
let currentAmount = 0;
for (const proof of proofs) {
if (currentAmount >= requestedAmount) {
break;
}
selectedProofs.push(proof);
currentAmount += proof.amount;
}
return selectedProofs;
}
const getProofsToSend = function (requestedAmount: number, proofs: Proof[]): Proof[] {
const proofsAmount = getProofsAmount(proofs)
if(requestedAmount > proofsAmount) {
throw new AppError(
Err.VALIDATION_ERROR,
'There is not enough funds to send this amount',
{requestedAmount, proofsAmount, caller: 'getProofsToSend'})
}
const exactMatch = findExactMatch(requestedAmount, proofs);
if (exactMatch) {
return exactMatch;
}
return findMinExcess(requestedAmount, proofs);
}
/**
* returns a subset of tokens, so that not all tokens are sent to mint for smaller amounts.
* @param amount
@@ -204,51 +236,6 @@ const getProofsFromTokenEntries = (tokenEntries: TokenEntry[]) => {
return proofSubset
} */
export const getProofsToSend = (amount: number, proofs: Proof[]) => {
if (proofs.reduce((s, t) => (s += t.amount), 0) < amount) {
// there are not enough proofs to pay the amount
throw new AppError(Err.VALIDATION_ERROR, 'Not enough proofs to match requested amount', {amount})
}
// sort proofs by amount ascending
proofs = proofs.slice().sort((a, b) => a.amount - b.amount);
// remember next bigger proof as a fallback
const nextBigger = proofs.find((p) => p.amount > amount);
// go through smaller proofs until sum is bigger than amount
const smallerProofs = proofs.filter((p) => p.amount <= amount);
// sort by amount descending
smallerProofs.sort((a, b) => b.amount - a.amount);
let selectedProofs: Proof[] = [];
if (smallerProofs.length == 0 && nextBigger) {
// if there are no smaller proofs, take the next bigger proof as a fallback
return [nextBigger];
} else if (smallerProofs.length == 0 && !nextBigger) {
// no proofs available
return [];
}
// recursively select the largest proof of smallerProofs, subtract the amount from the remainder
// and call coinSelect again with the remainder and the rest of the smallerProofs (without the largest proof)
let remainder = amount;
selectedProofs = [smallerProofs[0]];
remainder -= smallerProofs[0].amount;
if (remainder > 0) {
selectedProofs = selectedProofs.concat(getProofsToSend(remainder, smallerProofs.slice(1)));
}
let sum = selectedProofs.reduce((s, t) => (s += t.amount), 0);
// if sum of selectedProofs is smaller than amount, take next bigger proof instead as a fallback
if (sum < amount && nextBigger) {
selectedProofs = [nextBigger];
}
log.trace("[getProofsToSend] ### selected amounts", "sum", selectedProofs.reduce((s, t) => (s += t.amount), 0), selectedProofs.map(p => p.amount));
return selectedProofs
}
/**
* removes a set of tokens from another set of tokens, and returns the remaining.
@@ -303,7 +290,7 @@ const getMintFromProof = function (
): Mint | undefined {
let mint: Mint | undefined
mints.forEach(m => {
if (m.keysets?.includes(proof.id)) {
if (m.keysetIds?.includes(proof.id)) {
mint = m
}
})
@@ -322,7 +309,8 @@ export const CashuUtils = {
getProofsAmount,
getAmountPreferencesCount,
getMintsFromToken,
updateMintProofs,
findMinExcess,
// updateMintProofs,
getProofsFromTokenEntries,
getProofsToSend,
getProofsSubset,
+39 -29
View File
@@ -26,6 +26,7 @@ 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'
import { Token } from '../models/Token'
let _mints: CashuMint[] = []
let _wallets: CashuWallet[] = [] // used where seed is not required (perf)
@@ -114,22 +115,22 @@ const getMint = async function (mintUrl: string): Promise<CashuMint> {
// 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})
// skip checks if this is new mint being added
if(mintInstance) {
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)
}
// 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)
@@ -142,13 +143,13 @@ const getWallet = async function (
mintUrl: string,
unit: MintUnit,
options?: {
keysetId?: string // force specific (inactive) keys to swap from
keysetId?: string
withSeed: boolean
}
): Promise<CashuWallet> {
// 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) {
@@ -156,7 +157,7 @@ const getWallet = async function (
mintUrl
})
}
// select keys to be used to find or create new cashu-ts wallet instance
let walletKeys: MintKeys
if(options && options.keysetId) {
@@ -181,7 +182,9 @@ const getWallet = async function (
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)
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.', {
@@ -199,7 +202,7 @@ const getWallet = async function (
})
}
walletKeys = getSnapshot(activeKeys)
walletKeys = activeKeys
}
if (options && options.withSeed) {
@@ -288,6 +291,7 @@ const receive = async function (
mintUrl: string,
unit: MintUnit,
decodedToken: CashuToken,
mintFeeReserve: number,
options: {
preference: AmountPreference[],
counter: number
@@ -301,8 +305,11 @@ const receive = async function (
withSeed: true,
})
const amountToReceive = CashuUtils.getTokenAmounts(decodedToken as Token).totalAmount
const proofs = await cashuWallet.receive(
decodedToken,
decodedToken,
mintFeeReserve,
{
keysetId: cashuWallet.keys.id,
preference: options.preference,
@@ -311,7 +318,10 @@ const receive = async function (
privkey: undefined
})
return proofs
const receivedAmount = CashuUtils.getProofsAmount(proofs as Proof[])
const mintFeePaid = amountToReceive - receivedAmount
return {proofs, mintFeePaid}
} catch (e: any) {
throw new AppError(Err.MINT_ERROR, e.message)
}
@@ -322,6 +332,7 @@ const receive = async function (
const send = async function (
mintUrl: string,
amountToSend: number,
mintFeeReserve: number,
unit: MintUnit,
proofsToSendFrom: Proof[],
options: {
@@ -337,10 +348,11 @@ const send = async function (
withSeed: true,
})
log.debug('[MintClient.sendFromMint] counter', options.counter)
log.debug('[MintClient.send] counter', options.counter)
const {returnChange, send} = await cashuWallet.send(
amountToSend,
mintFeeReserve,
proofsToSendFrom,
{
keysetId: cashuWallet.keys.id,
@@ -351,8 +363,8 @@ const send = async function (
}
)
log.debug('[MintClient.sendFromMint] returnedProofs', returnChange)
log.debug('[MintClient.sendFromMint] proofsToSend', send)
log.debug('[MintClient.send] returnedProofs', returnChange)
log.debug('[MintClient.send] proofsToSend', send)
// do some basic validations that proof amounts from mints match
const totalAmountToSendFrom = CashuUtils.getProofsAmount(proofsToSendFrom)
@@ -397,10 +409,8 @@ const getSpentOrPendingProofsFromMint = async function (
unit: MintUnit,
proofs: Proof[],
) {
try {
const cashuWallet = await getWallet(mintUrl, unit, {withSeed: true})
try {
const cashuWallet = await getWallet(mintUrl, unit, {withSeed: true})
const spentPendingProofs = await cashuWallet.checkProofsSpent(proofs)
log.trace('[CashuMintClient.getSpentOrPendingProofsFromMint]', {mintUrl, spentPendingProofs})
+41 -10
View File
@@ -12,12 +12,15 @@ import {CashuUtils} from '../cashu/cashuUtils'
import AppError, {Err} from '../../utils/AppError'
import {Token} from '../../models/Token'
import {
type Token as CashuToken
TokenEntry,
type Token as CashuToken,
type Proof as CashuProof
} from '@cashu/cashu-ts'
import { getDefaultAmountPreference, getEncodedToken } from '@cashu/cashu-ts/src/utils'
import { TransactionTaskResult } from '../walletService'
import { WalletUtils } from './utils'
import { MintUnit, formatCurrency, getCurrency } from './currency'
import { Proof } from '../../models/Proof'
const {
mintsStore,
@@ -120,9 +123,12 @@ export const receiveTask = 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)
// is not received our recovery index counts for sigs the mint has already issued
const amountPreferences = getDefaultAmountPreference(amountToReceive)
const countOfInFlightProofs = CashuUtils.getAmountPreferencesCount(amountPreferences)
const tokenEntries: Array<TokenEntry> = token.token
const proofsToReceive = tokenEntries[0].proofs as Proof[]
const mintFeeReserve = mintInstance.getMintFeeReserve(proofsToReceive)
log.trace('[receiveTask]', 'proofsCounter initial state', {proofsCounter: mintInstance.getProofsCounterByUnit?.(unit)})
log.trace('[receiveTask]', 'amountPreferences', {amountPreferences, transactionId})
@@ -132,17 +138,21 @@ export const receiveTask = async function (
await WalletUtils.lockAndSetInFlight(mintInstance, unit, countOfInFlightProofs, transactionId)
// get locked counter values
const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit)
const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit)!
const receivedProofs = await MintClient.receive(
const receivedResult = await MintClient.receive(
mintToReceive,
unit as MintUnit,
token,
mintFeeReserve,
{
preference: amountPreferences,
counter: lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase
}
)
)
const receivedProofs = receivedResult.proofs
const mintFeePaid = receivedResult.mintFeePaid
// 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)
@@ -163,6 +173,8 @@ export const receiveTask = async function (
transactionData.push({
status: TransactionStatus.PREPARED,
updatedToken,
mintFeeReserve,
mintFeePaid,
createdAt: new Date(),
})
@@ -210,6 +222,10 @@ export const receiveTask = async function (
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance!
await transactionsStore.updateBalanceAfter(transactionId, balanceAfter)
if(mintFeePaid > 0) {
await transactionsStore.updateFee(transactionId, mintFeePaid)
}
return {
taskFunction: RECEIVE,
mintUrl: mintInstance.mintUrl,
@@ -435,7 +451,12 @@ 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 amountPreferences = getDefaultAmountPreference(transaction.amount)
const tokenEntries: Array<TokenEntry> = 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)
@@ -445,23 +466,27 @@ export const receiveOfflineCompleteTask = async function (
await WalletUtils.lockAndSetInFlight(mintInstance, unit, countOfInFlightProofs, transaction.id as number)
// get locked counter values
const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit)
const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit)!
const receivedProofs = await MintClient.receive(
const receivedResult = await MintClient.receive(
mintToReceive,
unit as MintUnit,
token,
mintFeeReserve,
{
preference: amountPreferences,
counter: lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase
}
)
)
const receivedProofs = receivedResult.proofs
const mintFeePaid = receivedResult.mintFeePaid
// 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)
// store swapped proofs as encoded token in tx data
const tokenEntryToSend = {
const tokenEntryToSend: TokenEntry = {
mint: mintToReceive,
proofs: receivedProofs,
}
@@ -475,6 +500,8 @@ export const receiveOfflineCompleteTask = async function (
transactionData.push({
status: TransactionStatus.PREPARED,
updatedToken,
mintFeeReserve,
mintFeePaid,
createdAt: new Date(),
})
@@ -522,6 +549,10 @@ export const receiveOfflineCompleteTask = async function (
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance!
await transactionsStore.updateBalanceAfter(transaction.id as number, balanceAfter)
if(mintFeePaid > 0) {
await transactionsStore.updateFee(transaction.id as number, mintFeePaid)
}
return {
taskFunction: RECEIVE_OFFLINE_COMPLETE,
mintUrl: mintInstance.mintUrl,
+133 -66
View File
@@ -18,13 +18,13 @@ import {
} from '@cashu/cashu-ts'
import { getDefaultAmountPreference, isObj } from '@cashu/cashu-ts/src/utils'
import { TransactionTaskResult, WalletTask } from '../walletService'
import { MintBalance } from '../../models/Mint'
import { MintBalance, MintProofsCounter } from '../../models/Mint'
import { Proof } from '../../models/Proof'
import { poller } from '../../utils/poller'
import { WalletUtils } from './utils'
import { getSnapshot, isStateTreeNode } from 'mobx-state-tree'
import { MintUnit } from './currency'
import { getProofsToSend } from '../../services/cashu/cashuUtils'
import { boolean } from 'mobx-state-tree/dist/internal'
const {
mintsStore,
@@ -129,6 +129,10 @@ export const sendTask = async function (
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance!
await transactionsStore.updateBalanceAfter(transactionId, balanceAfter)
if(mintFeePaid > 0) {
await transactionsStore.updateFee(transactionId, mintFeePaid)
}
log.trace('[send] totalBalance after', balanceAfter)
@@ -203,9 +207,11 @@ export const sendFromMint = async function (
)
}
const proofsFromMint = proofsStore.getByMint(mintUrl, {isPending: false, unit}) as Proof[]
log.debug('[sendFromMint]', 'proofsFromMint count', {proofsCount: proofsFromMint.length, amountToSend})
const proofsFromMint = proofsStore.getByMint(mintUrl, {isPending: false, unit}) as Proof[]
log.debug('[sendFromMint]', 'proofsFromMint count', {mintBalance: mintBalance.balances[unit], amountToSend})
if (proofsFromMint.length < 1) {
throw new AppError(
@@ -219,8 +225,8 @@ export const sendFromMint = async function (
if (totalAmountFromMint < amountToSend) {
throw new AppError(
Err.VALIDATION_ERROR,
'There is not enough funds to send this payment',
{totalAmountFromMint, amountToSend},
'There is not enough funds to send this amount',
{totalAmountFromMint, amountToSend, caller: 'sendFromMint'},
)
}
@@ -259,25 +265,78 @@ export const sendFromMint = async function (
}
/*
* if we did not selected ecash but amount and we might need a swap of ecash by the mint to match exact amount
*/
* SWAP or DIRECT SEND
* if we did not selected ecash but amount and we might need a swap of ecash by the mint to match exact amount
*/
// Prioritize send from inactive keysets
let proofsToSendFrom: Proof[] = []
const inactiveKeysetIds = mintInstance?.keysets.filter(k => k.active === false).map(k => k.id)
const activeKeysetIds = mintInstance?.keysets.filter(k => k.active === true).map(k => k.id)
const proofsToSendFrom = getProofsToSend(
amountToSend,
proofsFromMint,
)
log.trace('[sendFromMint]', {inactiveKeysetIds, activeKeysetIds})
if(inactiveKeysetIds.length > 0) {
const proofsFromInactiveKeysets = proofsStore.getByMint(mintUrl, {isPending: false, unit, keysetIds: inactiveKeysetIds})
const proofsFromActiveKeysets = proofsStore.getByMint(mintUrl, {isPending: false, unit, keysetIds: activeKeysetIds})
let proofsToSendFromAmount = CashuUtils.getProofsAmount(proofsToSendFrom)
if(proofsFromInactiveKeysets && proofsFromInactiveKeysets.length > 0) {
let proofsFromInactiveKeysetsAmount = CashuUtils.getProofsAmount(proofsFromInactiveKeysets)
// swap will happen if we could not select proofs equal to amountToSend
let returnedAmount = proofsToSendFromAmount - amountToSend
log.trace('[sendFromMint]', {proofsFromInactiveKeysetsAmount})
if(proofsFromInactiveKeysetsAmount >= amountToSend) {
proofsToSendFrom = CashuUtils.getProofsToSend(
amountToSend,
proofsFromInactiveKeysets
)
} else {
const remainingAmount = amountToSend - proofsFromInactiveKeysetsAmount
const remainingProofs = CashuUtils.findMinExcess(remainingAmount, proofsFromActiveKeysets!)
proofsToSendFrom = CashuUtils.getProofsToSend(
amountToSend,
[...proofsFromInactiveKeysets, ...remainingProofs]
)
}
} else {
proofsToSendFrom = CashuUtils.getProofsToSend(
amountToSend,
proofsFromMint
)
}
} else {
proofsToSendFrom = CashuUtils.getProofsToSend(
amountToSend,
proofsFromMint
)
}
let proofsToSendFromAmount = CashuUtils.getProofsAmount(proofsToSendFrom)
// swap will happen if we could not select proofs equal to amountToSend
let mintFeeReserve: number = 0
let mintFeePaid: number = 0
let proofsToSend: Proof[] = []
let returnedProofs: Proof[] = []
let isSwapNeeded: boolean = proofsToSendFromAmount - amountToSend > 0 ? true : false
let returnedAmount = 0
if(returnedAmount > 0) {
mintFeeReserve = mintInstance.getFeesForProofs(proofsToSendFrom)
log.trace('[sendFromMint]', {proofsToSendFromAmount, amountToSend})
/*
* SWAP is needed, could involve a fee
* if we did not selected ecash but amount and we might need a swap of ecash by the mint to match exact amount
*/
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 mint will keep
returnedAmount = proofsToSendFromAmount - amountToSend - mintFeeReserve
log.debug('[sendFromMint] Swap is needed.', {mintFeeReserve, returnedAmount})
// if we did not selected enough proofs to cover the fees we need some more
if(mintFeeReserve > returnedAmount) {
const missingFeesAmount = mintFeeReserve - returnedAmount
if(returnedAmount < 0) {
const missingFeesAmount = Math.abs(returnedAmount)
const remainingProofs = proofsStore.getProofsSubset(proofsFromMint, proofsToSendFrom)
const remainingProofsAmount = CashuUtils.getProofsAmount(remainingProofs)
@@ -290,8 +349,8 @@ export const sendFromMint = async function (
{totalAmountFromMint, amountToSend, mintFeeReserve},
)
}
const proofsToPayFees = getProofsToSend(
// select additional proof(s) to pay fees
const proofsToPayFees = CashuUtils.getProofsToSend(
missingFeesAmount,
remainingProofs
)
@@ -299,51 +358,51 @@ export const sendFromMint = async function (
// add more proofs into inputs and recalculate amounts
proofsToSendFrom.push(...proofsToPayFees)
proofsToSendFromAmount = CashuUtils.getProofsAmount(proofsToSendFrom)
returnedAmount = proofsToSendFromAmount - amountToSend
returnedAmount = proofsToSendFromAmount - amountToSend - mintFeeReserve
}
// decrease requested returned outputs by fees so that the mint can charge them
returnedAmount -= mintFeeReserve
}
// Outputs denominations we asked for to get
const amountPreferences = getDefaultAmountPreference(amountToSend)
// Output denominations we are about to get as a split remainder
const returnedAmountPreferences = getDefaultAmountPreference(returnedAmount)
// Inputs we are about to send
const amountPreferences = getDefaultAmountPreference(amountToSend)
// Outputs we are about to get back if swap occurs
const returnedAmountPreferences = getDefaultAmountPreference(returnedAmount)
const countOfProofsToSend = CashuUtils.getAmountPreferencesCount(amountPreferences)
const countOfReturnedProofs = CashuUtils.getAmountPreferencesCount(returnedAmountPreferences)
const countOfInFlightProofs = countOfProofsToSend + countOfReturnedProofs
const countOfProofsToSend = CashuUtils.getAmountPreferencesCount(amountPreferences)
const countOfReturnedProofs = CashuUtils.getAmountPreferencesCount(returnedAmountPreferences)
const countOfInFlightProofs = countOfProofsToSend + countOfReturnedProofs
log.trace('[sendFromMint]', 'amountPreferences', {amountPreferences, returnedAmountPreferences})
log.trace('[sendFromMint]', 'countOfInFlightProofs', countOfInFlightProofs)
// 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)
// + acquire lock and set inFlight values
await WalletUtils.lockAndSetInFlight(mintInstance, unit, countOfInFlightProofs, transactionId)
// get locked counter values
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, mintFeePaid} = await MintClient.send(
mintUrl,
amountToSend,
unit,
proofsToSendFrom,
{
preference: amountPreferences,
counter: lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase
}
)
log.trace('[sendFromMint]', 'amountPreferences', {amountPreferences, returnedAmountPreferences})
log.trace('[sendFromMint]', 'countOfInFlightProofs', countOfInFlightProofs)
// 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)
// 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)
// + acquire lock and set inFlight values
await WalletUtils.lockAndSetInFlight(mintInstance, unit, countOfInFlightProofs, transactionId)
// add proofs returned by the mint after the split
if (returnedProofs.length > 0) {
// get locked counter values
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 sendResult = await MintClient.send(
mintUrl,
amountToSend,
mintFeeReserve,
unit,
proofsToSendFrom,
{
preference: amountPreferences,
counter: lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase
}
)
returnedProofs = sendResult.returnedProofs
proofsToSend = sendResult.proofsToSend
mintFeePaid = sendResult.mintFeePaid
// 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)
// add proofs returned by the mint after the split
log.trace('[sendFromMint] add returned proofs to spendable')
WalletUtils.addCashuProofs(
mintUrl,
@@ -352,16 +411,24 @@ export const sendFromMint = async function (
unit,
transactionId,
isPending: false
}
)
}
})
} else if (returnedAmount === 0) {
/*
* SWAP not needed, all selected proofs will be sent
*/
log.trace('[sendFromMint] Swap is not necessary, all proofsToSendFrom will be sent.')
proofsToSend = [...proofsToSendFrom]
} else {
throw new AppError(Err.VALIDATION_ERROR, 'Amount to keep can not be negative')
}
// remove used proofs and move sent proofs to pending
log.trace('[sendFromMint] remove proofsToSendFrom from spendable')
proofsStore.removeProofs(proofsToSendFrom)
// these might be original proofToSendFrom if they matched the exact amount and split was not necessary
log.trace('[sendFromMint] add proofsToSend to pending')
// these might be original proofToSendFrom if they matched the exact amount and split was not necessary
WalletUtils.addCashuProofs(
mintUrl,
proofsToSend,
+19 -8
View File
@@ -86,10 +86,19 @@ export const transferTask = async function (
transactionId = storedTransaction.id as number
// calculate fees charged by mint for melt transaction to prepare enough proofs
const proofsFromMint = proofsStore.getByMint(mintUrl, {isPending: false, unit}) as Proof[]
let proofsToSendFrom = CashuUtils.getProofsToSend(
amountToTransfer + meltQuote.fee_reserve,
proofsFromMint
)
let meltFeeReserve = mintInstance.getMintFeeReserve(proofsToSendFrom)
log.trace('[transfer]', {meltFeeReserve, amountWithFees: amountToTransfer + meltQuote.fee_reserve + meltFeeReserve,})
// get proofs ready to be paid to the mint
const {proofs: proofsToPay, mintFeePaid, mintFeeReserve} = await sendFromMint(
mintBalanceToTransferFrom,
amountToTransfer + meltQuote.fee_reserve,
amountToTransfer + meltQuote.fee_reserve + meltFeeReserve,
unit,
[],
transactionId,
@@ -110,21 +119,21 @@ export const transferTask = async function (
transactionId,
TransactionStatus.PREPARED,
JSON.stringify(transactionData),
)
)
// we do not know how much we will get so use big enough constant to increase and lock
const countOfInFlightProofs = 10
// number of outputs we can get back with returned lightning fees
const countOfInFlightProofs = Math.ceil(Math.log2(meltQuote.fee_reserve)) || 1
// temp increase the counter + acquire lock and set inFlight values
await WalletUtils.lockAndSetInFlight(mintInstance, unit, countOfInFlightProofs, transactionId)
// get locked counter values
const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit)
const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit)!
const {isPaid, preimage, feeSavedProofs} = await MintClient.payLightningMelt(
mintUrl,
unit,
meltQuote,
proofsToPay,
proofsToPay,
{
counter: lockedProofsCounter.inFlightFrom as number
}
@@ -203,7 +212,9 @@ export const transferTask = async function (
mintInstance.resetInFlight(transactionId)
// Save final fee in db
await transactionsStore.updateFee(transactionId, lightningFeePaid + mintFeePaid)
if(lightningFeePaid + mintFeePaid !== meltQuote.fee_reserve) {
await transactionsStore.updateFee(transactionId, lightningFeePaid + mintFeePaid)
}
// Update transaction status
transactionData.push({
@@ -229,7 +240,7 @@ export const transferTask = async function (
taskFunction: TRANSFER,
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}.`,
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
} as TransactionTaskResult
+1 -1
View File
@@ -821,7 +821,7 @@ const _handlePendingTopupTask = async function (params: {paymentRequest: Payment
)
// get locked counter values
const lockedProofsCounter = mintInstance.getProofsCounterByUnit(unit)
const lockedProofsCounter = mintInstance.getProofsCounterByUnit(unit)!
let proofs: CashuProof[] = []