Units for topup and paymentRequest WIP

This commit is contained in:
minibits-cash
2024-04-28 23:56:43 +02:00
parent 1287017370
commit 69475b5ab0
12 changed files with 213 additions and 71 deletions
+14 -6
View File
@@ -4,7 +4,7 @@ import {withSetPropAction} from './helpers/withSetPropAction'
import {Contact, ContactModel} from './Contact'
import {log} from '../services/logService'
import addSeconds from 'date-fns/addSeconds'
import { MintUnit } from '../services'
import { MintUnit, MintUnits } from '../services/wallet/currency'
/**
* This represents incoming lightning invoice to pay (by transfer tx)
@@ -16,6 +16,9 @@ export enum PaymentRequestStatus {
EXPIRED = 'EXPIRED',
}
export enum MintMethod {
BOLT11 = 'bolt11',
}
export enum PaymentRequestType {
INCOMING = 'INCOMING',
@@ -27,10 +30,13 @@ export const PaymentRequestModel = types
type: types.frozen<PaymentRequestType>(),
status: types.frozen<PaymentRequestStatus>(),
mint: types.maybe(types.string),
mintQuote: types.maybe(types.string),
mintQuote: types.maybe(types.string),
mintUnit: types.frozen<MintUnit>(), // TODO migration to set default unit for existing PRs
mintMethod: types.optional(types.frozen<MintMethod>(), MintMethod.BOLT11),
amountToTopup: types.number,
encodedInvoice: types.string,
amount: types.number,
unit: types.frozen<MintUnit>(), // TODO migration to set default unit for existing PRs
invoicedAmount: types.number,
invoicedUnit: types.optional(types.frozen<MintUnit>(), 'sat'),
description: types.optional(types.string, ''),
paymentHash: types.string,
contactFrom: types.frozen<Contact>(),
@@ -52,10 +58,12 @@ export type PaymentRequest = {
type: PaymentRequestType
status: PaymentRequestStatus
mint?: string
mintUnit: MintUnit
mintQuote?: string
mintMethod?: MintMethod
amountToTopup: number
encodedInvoice: string
amount: number
unit: MintUnit
invoicedAmount: number
description?: string
paymentHash: string
contactFrom: Contact
+16 -1
View File
@@ -262,7 +262,7 @@ export const ProofsStoreModel = types
unitPendingBalances,
}
log.debug('[getBalances]', balances)
// log.debug('[getBalances]', balances)
// console.log(balances)
return balances
@@ -292,6 +292,21 @@ export const ProofsStoreModel = types
return filteredMintBalances
},
getMintBalancesWithUnit: (unit: MintUnit) => {
const balances = self.getBalances().mintBalances
const filteredMintBalances = balances
.slice()
.filter((balance: MintBalance) => {
if(Object.keys(balance.balances).includes(unit)) {
return true
}
return false
})
.sort((a, b) => b.balances[unit]! - a.balances[unit]!)
return filteredMintBalances
},
getMintBalanceWithMaxBalance: (unit: MintUnit) => {
const balances = self.getBalances().mintBalances
+21 -3
View File
@@ -47,6 +47,7 @@ import { MintBalance } from "../models/Mint"
import { PaymentRequest } from "../models/PaymentRequest"
import { LNURLPayParams, LNURLWithdrawParams } from "js-lnurl"
import { IncomingDataType } from "../services/incomingParser"
import { MintUnit } from "../services/wallet/currency"
export type TabsParamList = {
@@ -124,12 +125,29 @@ export type WalletStackParamList = {
SendOptions: undefined
Send: {contact?: Contact, relays?: string[], paymentOption?: SendOption}
Scan: undefined
LightningPay: undefined
LightningPay: {
mintUrl?: string,
unit?: MintUnit
}
TranDetail: {id: number}
TranHistory: undefined
PaymentRequests: undefined
Transfer: {encodedInvoice?: string, paymentRequest?: PaymentRequest, lnurlParams?: LNURLPayParams & {address?: string}, paymentOption?: SendOption,}
Topup: {contact?: Contact, relays?: string[], paymentOption?: ReceiveOption, lnurlParams?: LNURLWithdrawParams}
Transfer: {
encodedInvoice?: string,
paymentRequest?: PaymentRequest,
lnurlParams?: LNURLPayParams & {address?: string},
paymentOption?: SendOption,
mintUrl?: string,
unit?: MintUnit
}
Topup: {
contact?: Contact,
relays?: string[],
paymentOption?: ReceiveOption,
lnurlParams?: LNURLWithdrawParams,
mintUrl?: string,
unit?: MintUnit
}
ContactsNavigator: {screen: string, params: any}
SettingsNavigator: {screen: string, params: any}
}
+1 -1
View File
@@ -36,7 +36,7 @@ export const MintListItem = observer(function(props: {
const {mint, mintBalance, selectedUnit, onMintSelect, isSelected, isSelectable, isBlocked, isUnitVisible, separator} = props
log.trace('[MintListItem]', props)
// log.trace('[MintListItem]', props)
return (
<ListItem
@@ -12,7 +12,7 @@ import { getImageSource } from '../../utils/utils'
import { ContactListItem } from "../Contacts/ContactListItem"
import { SendOption } from "../SendOptionsScreen"
import { CurrencySign } from "../Wallet/CurrencySign"
import { CurrencyCode } from "../../services/wallet/currency"
import { CurrencyCode, MintUnitCurrencyPairs } from "../../services/wallet/currency"
export interface PaymentRequestListProps {
@@ -29,7 +29,7 @@ export const PaymentRequestListItem = observer(function (props: PaymentRequestLi
const secToExpiry = differenceInSeconds(pr.expiresAt as Date, new Date())
const expiryBg = ( secToExpiry < 0 ? colors.palette.angry500 : secToExpiry < 60 ? colors.palette.orange400 : colors.palette.success300)
const separatorColor = useThemeColor('separator')
const onGotoContactDetail = function() {
log.trace(pr)
navigation.navigate('ContactsNavigator', {
@@ -63,10 +63,10 @@ export const PaymentRequestListItem = observer(function (props: PaymentRequestLi
/>
<Text
preset='heading'
text={pr.amount.toLocaleString()}
text={pr.type === PaymentRequestType.INCOMING ? pr.invoicedAmount.toLocaleString() : pr.amountToTopup.toLocaleString()}
/>
<CurrencySign
currencyCode={CurrencyCode.SATS}
<CurrencySign
mintUnit={pr.type === PaymentRequestType.INCOMING ? pr.invoicedUnit : pr.mintUnit}
containerStyle={{marginBottom: spacing.small}}
/>
</View>
@@ -95,7 +95,7 @@ export const PaymentRequestListItem = observer(function (props: PaymentRequestLi
size={spacing.medium}
color={dim}
/>
<Text size='xxs' style={{color: dim, marginBottom: -10}} text={`pay ${pr.amount} SATS to`} />
<Text size='xxs' style={{color: dim, marginBottom: -10}} text={`pay ${pr.invoicedAmount} ${pr.invoicedUnit} to`} />
</View>
<Text size='xxs' style={{color: dim, textAlign: 'center', marginRight: 30, marginBottom: 20}} text='...........' />
<View style={{flexDirection: 'column', alignItems: 'center', width: 100}}>
+26 -8
View File
@@ -48,11 +48,11 @@ import { Contact } from '../models/Contact'
import { getImageSource, infoMessage } from '../utils/utils'
import { ReceiveOption } from './ReceiveOptionsScreen'
import { LNURLWithdrawParams } from 'js-lnurl'
import { roundDown } from '../utils/number'
import { roundDown, roundUp, toNumber } from '../utils/number'
import { LnurlClient, LnurlWithdrawResult } from '../services/lnurlService'
import { moderateVerticalScale, verticalScale } from '@gocodingnow/rn-size-matters'
import { CurrencySign } from './Wallet/CurrencySign'
import { CurrencyCode, MintUnit, MintUnitCurrencyPairs, MintUnits } from "../services/wallet/currency"
import { Currencies, CurrencyCode, MintUnit, MintUnitCurrencyPairs, MintUnits } from "../services/wallet/currency"
if (
Platform.OS === 'android' &&
@@ -120,6 +120,19 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
}
}, [])
useEffect(() => {
const { mintUrl, unit } = route.params
const setSelectedMintandUnit = () => {
setUnit(unit!)
setMintBalanceToTopup(proofsStore.getMintBalance(mintUrl!))
}
if(mintUrl && unit) {
setSelectedMintandUnit()
}
}, [])
// Send to contact
useFocusEffect(
useCallback(() => {
@@ -288,7 +301,11 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
const onAmountEndEditing = function () {
try {
const amount = parseInt(amountToTopup)
const precision = Currencies[MintUnitCurrencyPairs[unit]]!.precision
const mantissa = Currencies[MintUnitCurrencyPairs[unit]]!.mantissa
const amount = toNumber(amountToTopup) * precision
log.trace('[onAmountEndEditing]', amount)
if (!amount || amount === 0) {
infoMessage('Amount should be positive number.')
@@ -300,17 +317,18 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
return
}
const availableBalances = proofsStore.getBalances().mintBalances
const availableBalances = proofsStore.getMintBalancesWithUnit(unit)
if (availableBalances.length === 0) {
infoMessage('Add the mint first.', 'There is no mint connected to your wallet that you would receive your ecash from.')
infoMessage('Add the mint first.', 'There is no mint connected to your wallet that you would receive your ecash to.')
return
}
setAmountToTopup(`${roundUp(toNumber(amountToTopup), mantissa)}`) // round amount based on currency format
setAvailableMintBalances(availableBalances)
// Default mint with highest balance to topup
setMintBalanceToTopup(availableBalances[0])
// Default mint is the one with the highest balance to topup
if(!mintBalanceToTopup) setMintBalanceToTopup(availableBalances[0])
setIsAmountEndEditing(true)
// We do not make memo focus mandatory
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
@@ -365,7 +383,7 @@ export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
WalletTask.topup(
mintBalanceToTopup as MintBalance,
parseInt(amountToTopup),
toNumber(amountToTopup) * Currencies[MintUnitCurrencyPairs[unit]]!.precision,
unit,
memo,
contactToSendTo
+7 -10
View File
@@ -19,14 +19,14 @@ export const CurrencyAmount = observer(function (props: {
})
{
const {currencyCode, mintUnit, amount, symbolStyle, amountStyle, containerStyle, size} = props
let currencySymbol: string = Currencies.SATS?.symbol!
let currencySymbol: string = Currencies.SATS!.symbol
if(!!currencyCode) {
currencySymbol = Currencies[currencyCode]?.symbol!
currencySymbol = Currencies[currencyCode]!.symbol
}
if(!!mintUnit) {
currencySymbol = Currencies[MintUnitCurrencyPairs[mintUnit]]?.symbol!
currencySymbol = Currencies[MintUnitCurrencyPairs[mintUnit]]!.symbol
}
const amountColor = useThemeColor('amount')
@@ -34,14 +34,11 @@ export const CurrencyAmount = observer(function (props: {
return (
<View
style={[{
// alignSelf:'center',
style={[{
paddingHorizontal: spacing.tiny,
// borderRadius: spacing.tiny,
borderColor: 'red',
borderWidth: 1,
flexDirection: 'row',
// justifyContent: 'space-between',
// borderColor: 'red',
// borderWidth: 1,
flexDirection: 'row',
}, containerStyle || {}]}
>
<Text
+11 -4
View File
@@ -3,16 +3,23 @@ import { TextStyle, View, ViewStyle } from "react-native"
import { SvgXml } from "react-native-svg"
import { Text } from "../../components"
import { Spacing, colors, spacing, typography, useThemeColor } from "../../theme"
import { CurrencyCode, Currencies } from "../../services/wallet/currency"
import { CurrencyCode, Currencies, MintUnit, MintUnitCurrencyPairs } from "../../services/wallet/currency"
export const CurrencySign = function(props: {
currencyCode: CurrencyCode,
currencyCode?: CurrencyCode,
mintUnit?: MintUnit,
containerStyle?: ViewStyle,
textStyle?: TextStyle
size?: Spacing
}
) {
const {currencyCode, mintUnit, containerStyle, textStyle, size} = props
let code = currencyCode || CurrencyCode.SATS
if(!!mintUnit) {
code = Currencies[MintUnitCurrencyPairs[mintUnit]]!.code
}
const textColor = useThemeColor('amount')
const bgColor = colors.palette.primary200
@@ -34,10 +41,10 @@ export const CurrencySign = function(props: {
width={props.size && spacing[props.size] * 1.5 || spacing.small * 1.5}
height={props.size && spacing[props.size] * 1.5 || spacing.small * 1.5}
style={{marginRight: spacing.tiny}}
xml={Currencies[props.currencyCode]?.icon || null}
xml={Currencies[code]?.icon || null}
/>
<Text
text={Currencies[props.currencyCode]?.code}
text={Currencies[code]?.code}
style={[{
color: textColor,
fontSize: props.size && spacing[props.size] || spacing.small,
+92 -19
View File
@@ -11,7 +11,8 @@ import {
Animated,
FlatList,
Pressable,
Linking
Linking,
LayoutAnimation
} from 'react-native'
import codePush, { RemotePackage } from 'react-native-code-push'
import {moderateVerticalScale, verticalScale} from '@gocodingnow/rn-size-matters'
@@ -57,6 +58,7 @@ import useIsInternetReachable from '../utils/useIsInternetReachable'
import { CurrencySign } from './Wallet/CurrencySign'
import { Currencies, CurrencyCode, MintUnit, MintUnitCurrencyPairs, MintUnits } from "../services/wallet/currency"
import { CurrencyAmount } from './Wallet/CurrencyAmount'
import { ReceiveOption } from './ReceiveOptionsScreen'
// refresh
@@ -287,9 +289,9 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
navigation.navigate('Scan')
}
const gotoMintInfo = function (mintUrl: string) {
/* const gotoMintInfo = function (mintUrl: string) {
navigation.navigate('SettingsNavigator', {screen: 'MintInfo', params: {mintUrl}})
}
} */
const gotoTranHistory = function () {
navigation.navigate('TranHistory')
@@ -305,7 +307,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
/* Mints pager */
const groupedMints = mintsStore.groupedByUnit
log.trace('[WalletScreen]', {groupedByUnit: groupedMints})
// log.trace('[WalletScreen]', {groupedByUnit: groupedMints})
const width = spacing.screenWidth
const pagerRef = useRef<PagerView>(null)
const scrollOffsetAnimatedValue = React.useRef(new Animated.Value(0)).current
@@ -404,7 +406,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
<View style={$contentContainer}>
<MintsByUnitListItem
mintsByUnit={mints}
gotoMintInfo={gotoMintInfo}
navigation={navigation}
/>
{/*transactionsStore.recentByHostname(mints.hostname).length > 0 && (
<Card
@@ -587,20 +589,44 @@ const PromoBlock = function (props: {addMint: any}) {
const MintsByUnitListItem = observer(function (props: {
mintsByUnit: MintsByUnit
gotoMintInfo: any
navigation: any
}) {
/*const [isMenuVisible, setIsMenuVisible] = useState<boolean>(false)
const [selectedMintUrl, setSelectedMintUrl] = useState<string | undefined>(undefined)
const toggleMenu = function () {
if (isMenuVisible) {
log.trace('[toggleMenu]', !isMenuVisible)
setIsMenuVisible(false)
} else {
log.trace('[toggleMenu]', !isMenuVisible)
setIsMenuVisible(true)
const onSelectedMint = function (mintUrl: string) {
if (selectedMintUrl && selectedMintUrl === mintUrl) {
LayoutAnimation.easeInEaseOut()
setSelectedMintUrl(undefined)
return
}
}*/
LayoutAnimation.easeInEaseOut()
setSelectedMintUrl(mintUrl)
}
const gotoMintInfo = function (mintUrl: string) {
props.navigation.navigate('SettingsNavigator', {screen: 'MintInfo', params: {mintUrl}})
}
const gotoTopup = function (mintUrl: string, unit: MintUnit) {
props.navigation.navigate('Topup', {
paymentOption: ReceiveOption.SHOW_INVOICE,
mintUrl,
unit
})
}
const gotoLightningPay = async function (mintUrl: string, unit: MintUnit) {
props.navigation.navigate('LightningPay', {
mintUrl,
unit
})
}
const color = useThemeColor('textDim')
const balanceColor = useThemeColor('amount')
const {mintsByUnit} = props
@@ -615,11 +641,11 @@ const MintsByUnitListItem = observer(function (props: {
ContentComponent={
<>
{mintsByUnit.mints.map((mint: Mint) => (
<ListItem
key={mint.mintUrl}
<View key={mint.mintUrl}>
<ListItem
text={mint.shortname}
subText={mint.hostname}
leftIcon='faCoins'
leftIcon={mint.status === MintStatus.OFFLINE ? 'faTriangleExclamation' : 'faCoins'}
leftIconInverse={true}
leftIconColor={mint.color}
RightComponent={
@@ -631,8 +657,55 @@ const MintsByUnitListItem = observer(function (props: {
}
//topSeparator={true}
style={$item}
onPress={() => props.gotoMintInfo(mint.mintUrl)}
onPress={() => onSelectedMint(mint.mintUrl)}
/>
{selectedMintUrl === mint.mintUrl && (
<View style={{flexDirection: 'row', marginBottom: spacing.small}}>
<Button
text={'Add funds'}
LeftAccessory={() => (
<Icon
icon='faPlus'
color={color}
size={spacing.medium}
/>
)}
preset='tertiary'
textStyle={{fontSize: 14, color}}
onPress={() => gotoTopup(mint.mintUrl, mintsByUnit.unit)}
style={{minHeight: moderateVerticalScale(40), paddingVertical: moderateVerticalScale(spacing.tiny)}}
/>
{/*<Button
text={'Exchange'}
LeftAccessory={() => (
<Icon
icon='faRotate'
color={color}
size={spacing.medium}
/>
)}
textStyle={{fontSize: 14, color}}
preset='tertiary'
onPress={props.gotoMintInfo}
style={{minHeight: moderateVerticalScale(40), paddingVertical: moderateVerticalScale(spacing.tiny)}}
/>*/}
<Button
text={'Cash out'}
LeftAccessory={() => (
<Icon
icon='faArrowTurnUp'
color={color}
size={spacing.medium}
/>
)}
textStyle={{fontSize: 14, color}}
preset='tertiary'
onPress={() => gotoLightningPay(mint.mintUrl, mintsByUnit.unit)}
style={{minHeight: moderateVerticalScale(40), paddingVertical: moderateVerticalScale(spacing.tiny)}}
/>
</View>
)}
</View>
))}
</>
}
+3 -1
View File
@@ -2,6 +2,8 @@ import { decode } from "@gandlaf21/bolt11-decode"
import AppError, { Err } from '../../utils/AppError'
import { addSeconds } from 'date-fns'
import { log } from '../logService'
import numbro from "numbro"
import { roundUp, toNumber } from "../../utils/number"
// TODO refactor all this into own module
@@ -99,7 +101,7 @@ const getInvoiceData = function (decoded: DecodedLightningInvoice) {
for (const item of decoded.sections) {
switch (item.name) {
case 'amount':
result.amount = parseInt(item.value) / 1000 //sats
result.amount = roundUp(toNumber(item.value) / 1000, 0) // round to whole sats
break
case 'description':
result.description = (item.value as string) || ''
+7 -7
View File
@@ -64,7 +64,7 @@ export const Currencies: CurrencyList = {
code: CurrencyCode.USD,
mintUnit: 'usd',
icon: UsdIcon,
precision: 1,
precision: 100,
mantissa: 2,
},
EUR: {
@@ -73,42 +73,42 @@ export const Currencies: CurrencyList = {
code: CurrencyCode.EUR,
mintUnit: 'eur',
icon: EurIcon,
precision: 1,
precision: 100,
mantissa: 2,
},
GBP: {
symbol: '£',
title: 'Pound sterling',
code: CurrencyCode.GBP,
precision: 1,
precision: 100,
mantissa: 2,
},
CZK: {
symbol: 'Kč',
title: 'Česká koruna',
code: CurrencyCode.CZK,
precision: 1,
precision: 100,
mantissa: 2,
},
PLN: {
symbol: 'zł',
title: 'Złoty',
code: CurrencyCode.PLN,
precision: 1,
precision: 100,
mantissa: 2,
},
HUF: {
symbol: 'Ft',
title: 'Magyar forint',
code: CurrencyCode.HUF,
precision: 1,
precision: 100,
mantissa: 2,
},
RON: {
symbol: 'L',
title: 'Leu românesc',
code: CurrencyCode.RON,
precision: 1,
precision: 100,
mantissa: 2,
},
} as const
+9 -5
View File
@@ -64,12 +64,14 @@ export const topupTask = async function (
const decodedInvoice = LightningUtils.decodeInvoice(encodedInvoice)
const {amount, payment_hash, expiry, timestamp} = LightningUtils.getInvoiceData(decodedInvoice)
if (amount !== amountToTopup) {
log.trace('[topupTask] invoice', {amount, payment_hash, expiry, timestamp})
/* if (amount !== amountToTopup) {
throw new AppError(
Err.MINT_ERROR,
'Received lightning invoice amount does not equal requested top-up amount.',
)
}
} */
// sender is current wallet profile
const {
@@ -97,10 +99,11 @@ export const topupTask = async function (
type: PaymentRequestType.OUTGOING,
status: PaymentRequestStatus.ACTIVE,
mint: mintUrl,
unit,
mintQuote,
mintUnit: unit,
amountToTopup,
encodedInvoice,
amount,
invoicedAmount: amount,
description: memo ? memo : contactTo ? `Pay to ${walletProfileStore.nip05}` : '',
paymentHash: payment_hash,
contactFrom,
@@ -129,7 +132,7 @@ export const topupTask = async function (
WalletTask.handlePendingTopup,
{
interval: 6 * 1000,
maxPolls: 20,
maxPolls: 1, // TODO 20!!!
maxErrors: 5
},
{paymentRequest})
@@ -163,6 +166,7 @@ export const topupTask = async function (
log.error(e.name, e.message)
return {
taskFunction: TOPUP,
transaction: errorTransaction || undefined,
message: '',
error: WalletUtils.formatError(e),