Skip to content

Commit

Permalink
chore(wallet-mobile): more helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
stackchain committed May 3, 2024
1 parent aaa430d commit 1c9e72c
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -1,54 +1,58 @@
import {splitBigInt} from '@yoroi/common'
import {isPrimaryToken} from '@yoroi/portfolio'
import {useTheme} from '@yoroi/theme'
import {useTransfer} from '@yoroi/transfer'
import {Balance} from '@yoroi/types'
import * as React from 'react'
import {ScrollView, StyleSheet, Text, TouchableOpacity, View, ViewProps} from 'react-native'

import {Button, KeyboardAvoidingView, Spacer, TextInput} from '../../../../../components'
import {AmountItem} from '../../../../../components/AmountItem/AmountItem'
import {selectFtOrThrow} from '../../../../../yoroi-wallets/cardano/utils'
import {useTokenInfo} from '../../../../../yoroi-wallets/hooks'
import {Logger} from '../../../../../yoroi-wallets/logging'
import {asQuantity, editedFormatter, pastedFormatter, Quantities} from '../../../../../yoroi-wallets/utils'
import {usePortfolioBalances} from '../../../../Portfolio/common/hooks/usePortfolioBalances'
import {usePortfolioPrimaryBreakdown} from '../../../../Portfolio/common/hooks/usePortfolioPrimaryBreakdown'
import {useSelectedWallet} from '../../../../WalletManager/Context'
import {useNavigateTo, useOverridePreviousSendTxRoute} from '../../../common/navigation'
import {useStrings} from '../../../common/strings'
import {useTokenQuantities} from '../../../common/useTokenQuantities'
import {NoBalance} from './ShowError/NoBalance'
import {UnableToSpend} from './ShowError/UnableToSpend'

export const EditAmountScreen = () => {
const strings = useStrings()
const {styles} = useStyles()
const navigateTo = useNavigateTo()
const {selectedTokenId, amountChanged} = useTransfer()
const {available, spendable, initialQuantity} = useTokenQuantities(selectedTokenId)

const wallet = useSelectedWallet()
const tokenInfo = useTokenInfo({wallet, tokenId: selectedTokenId}, {select: selectFtOrThrow})
const isPrimary = tokenInfo.id === wallet.primaryTokenInfo.id
const balances = usePortfolioBalances({wallet})
const primaryBreakdown = usePortfolioPrimaryBreakdown({wallet})

const [quantity, setQuantity] = React.useState<Balance.Quantity>(initialQuantity)
const [inputValue, setInputValue] = React.useState<string>(
Quantities.denominated(initialQuantity, tokenInfo.decimals ?? 0),
)
const {selectedTokenId, amountChanged, allocated, selectedTargetIndex, targets} = useTransfer()

useOverridePreviousSendTxRoute(
Quantities.isZero(initialQuantity) ? 'send-select-token-from-list' : 'send-list-amounts-to-send',
)
const amount = targets[selectedTargetIndex].entry.amounts[selectedTokenId]
const initialQuantity = amount.quantity
const available =
(balances.records.get(selectedTokenId)?.quantity ?? 0n) -
(allocated.get(selectedTargetIndex)?.get(selectedTokenId) ?? 0n)
const isPrimary = isPrimaryToken(amount.info)

const [quantity, setQuantity] = React.useState(initialQuantity)
const [inputValue, setInputValue] = React.useState(splitBigInt(initialQuantity, amount.info.decimals).bn.toFormat())
const spendable = available - primaryBreakdown.lockedAsStorageCost

useOverridePreviousSendTxRoute(initialQuantity === 0n ? 'send-select-token-from-list' : 'send-list-amounts-to-send')

React.useEffect(() => {
setQuantity(initialQuantity)
setInputValue(Quantities.denominated(initialQuantity, tokenInfo.decimals ?? 0))
}, [initialQuantity, tokenInfo.decimals])
setInputValue(splitBigInt(initialQuantity, amount.info.decimals).bn.toFormat())
}, [amount.info.decimals, initialQuantity])

const hasBalance = !Quantities.isGreaterThan(quantity, available)
const isUnableToSpend = isPrimary && Quantities.isGreaterThan(quantity, spendable)
const isZero = Quantities.isZero(quantity)
const hasBalance = available >= quantity
const isUnableToSpend = isPrimary && quantity > spendable
const isZero = quantity === 0n

const onChangeQuantity = (text: string) => {
try {
const quantity = asQuantity(text.length > 0 ? text : '0')
const newQuantity = asQuantity(text.length > 0 ? text : '0')
setInputValue(text)
setQuantity(Quantities.integer(quantity, tokenInfo.decimals ?? 0))
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {useNavigateTo, useOverridePreviousSendTxRoute} from '../../common/naviga
import {toYoroiEntry} from '../../common/toYoroiEntry'
import {AddTokenButton} from './AddToken/AddToken'
import {RemoveAmountButton} from './RemoveAmount'
import {TokenAmountItem} from '../../../Portfolio/common/TokenAmountItem/TokenAmountItem'

export const ListAmountsToSendScreen = () => {
const {styles} = useStyles()
Expand Down Expand Up @@ -94,14 +95,14 @@ export const ListAmountsToSendScreen = () => {
return (
<View style={styles.container}>
<AmountsList
data={tokens}
renderItem={({item: {id}}: {item: Balance.TokenInfo}) => (
data={Object.values(amounts)}
renderItem={({item: amount}) => (
<Boundary>
<ActionableAmount amount={Amounts.getAmount(amounts, id)} onRemove={onRemove} onEdit={onEdit} />
<ActionableAmount amount={amount} onRemove={onRemove} onEdit={onEdit} />
</Boundary>
)}
bounces={false}
keyExtractor={({id}) => id}
keyExtractor={({info}) => info.id}
testID="selectedTokens"
/>

Expand All @@ -126,24 +127,22 @@ export const ListAmountsToSendScreen = () => {
}

type ActionableAmountProps = {
amount: Balance.Amount
onEdit(tokenId: string): void
onRemove(tokenId: string): void
amount: Portfolio.Token.Amount
onEdit(tokenId: Portfolio.Token.Id): void
onRemove(tokenId: Portfolio.Token.Id): void
}
const ActionableAmount = ({amount, onRemove, onEdit}: ActionableAmountProps) => {
const wallet = useSelectedWallet()
const {styles} = useStyles()
const {tokenId} = amount
const tokenInfo = useTokenInfo({wallet, tokenId})

const handleRemove = () => onRemove(tokenId)
const handleEdit = () => (tokenInfo.kind === 'nft' ? null : onEdit(tokenId))
const handleRemove = () => onRemove(amount.info.id)
const handleEdit = () => (isNft(amount.info) ? null : onEdit(amount.info.id))

return (
<View style={styles.amountItem} testID="amountItem">
<Left>
<EditAmountButton onPress={handleEdit}>
<AmountItem amount={amount} wallet={wallet} />
<TokenAmountItem amount={amount} privacyPlaceholder="" network={wallet.network} isPrivacyOff />
</EditAmountButton>
</Left>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
"defaultMessage": "!!!Add token",
"file": "src/features/Send/useCases/ListAmountsToSend/ListAmountsToSendScreen.tsx",
"start": {
"line": 195,
"line": 190,
"column": 12,
"index": 6152
"index": 6156
},
"end": {
"line": 198,
"line": 193,
"column": 3,
"index": 6229
"index": 6233
}
}
]
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export * from './utils/strings'
export * from './numbers/to-bigint'
export * from './numbers/split-bigint'
export * from './numbers/bigint-formatter'
export * from './numbers/parse-input-to-bigint'

export * from './observer/observer'

Expand Down
119 changes: 119 additions & 0 deletions packages/common/src/numbers/parse-input-to-bigint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import BigNumber from 'bignumber.js'

import {parseInputToBigInt} from './parse-input-to-bigint'

describe('parse-input-to-bigint', () => {
it('parseInputToBigInt', () => {
const english = {
prefix: '',
decimalSeparator: '.',
groupSeparator: ',',
groupSize: 3,
secondaryGroupSize: 0,
fractionGroupSize: 0,
fractionGroupSeparator: ' ',
suffix: '',
}

const italian = {
...english,
decimalSeparator: ',',
groupSeparator: ' ',
}

BigNumber.config({
FORMAT: italian,
})

expect(
parseInputToBigInt({input: '', decimalPlaces: 3, format: italian}),
).toEqual(['', 0n])
expect(
parseInputToBigInt({input: ',', decimalPlaces: 3, format: italian}),
).toEqual(['0,', 0n])
expect(
parseInputToBigInt({input: '1', decimalPlaces: 3, format: italian}),
).toEqual(['1', 1000n])
expect(
parseInputToBigInt({input: '123,55', decimalPlaces: 3, format: italian}),
).toEqual(['123,55', 123550n])
expect(
parseInputToBigInt({
input: '1234,6666',
decimalPlaces: 3,
format: italian,
}),
).toEqual(['1 234,666', 1234666n])
expect(
parseInputToBigInt({input: '55,', decimalPlaces: 3, format: italian}),
).toEqual(['55,', 55000n])
expect(
parseInputToBigInt({input: '55,0', decimalPlaces: 3, format: italian}),
).toEqual(['55,0', 55000n])
expect(
parseInputToBigInt({input: '55,10', decimalPlaces: 3, format: italian}),
).toEqual(['55,10', 55100n])
expect(
parseInputToBigInt({
input: 'ab1.5c,6.5',
decimalPlaces: 3,
format: italian,
}),
).toEqual(['15,65', 15650n])

BigNumber.config({FORMAT: english})

expect(
parseInputToBigInt({input: '', decimalPlaces: 3, format: english}),
).toEqual(['', 0n])
expect(
parseInputToBigInt({input: '1', decimalPlaces: 3, format: english}),
).toEqual(['1', 1000n])
expect(
parseInputToBigInt({input: '123.55', decimalPlaces: 3, format: english}),
).toEqual(['123.55', 123550n])
expect(
parseInputToBigInt({
input: '1234.6666',
decimalPlaces: 3,
format: english,
}),
).toEqual(['1,234.666', 1234666n])
expect(
parseInputToBigInt({input: '55.', decimalPlaces: 3, format: english}),
).toEqual(['55.', 55000n])
expect(
parseInputToBigInt({input: '55.0', decimalPlaces: 3, format: english}),
).toEqual(['55.0', 55000n])
expect(
parseInputToBigInt({input: '55.10', decimalPlaces: 3, format: english}),
).toEqual(['55.10', 55100n])
expect(
parseInputToBigInt({
input: 'ab1.5c,6.5',
decimalPlaces: 3,
format: english,
}),
).toEqual(['1.56', 1560n])

expect(
parseInputToBigInt({
input: '1.23456',
decimalPlaces: 0,
format: english,
precision: 3,
}),
).toEqual(['1.234', 1n])
expect(
parseInputToBigInt({
input: '1.23456',
decimalPlaces: 2,
format: english,
precision: 3,
}),
// how? simple, precision of 3 keep 1.234, 2 decimals drop the 4, toFixed 0
// 123 with 2 decimals = 1.23
// so be mindfull when using precision and decimalPlaces together
).toEqual(['1.234', 123n])
})
})
85 changes: 85 additions & 0 deletions packages/common/src/numbers/parse-input-to-bigint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {Numbers} from '@yoroi/types'
import BigNumber from 'bignumber.js'

/**
* Parses a localized numeric input into a formatted string and a corresponding
* BigInt representation, adjusting for specified decimal places and precision
* Don't use this to parse non-inputs, use toBigInt instead
* @note It will append 0 if starting with a decimal separator
*
* @param options - The options for parsing the input
* @param options.input - The input string to be parsed
* @param options.decimalPlaces - The number of decimal places to consider
* @param options.format - The locale format for numbers
* @param [options.precision] - The precision for rounding the number (optional, defaults to decimalPlaces)
*
* @returns {[string, bigint]} - A tuple containing the formatted string and BigInt value
*
* @example
* parseInputToBigInt({input: '', decimalPlaces: 3, format: italian}) // => ['', 0n]
* parseInputToBigInt({input: '1', decimalPlaces: 3, format: italian}) // => ['1', 1000n]
* parseInputToBigInt({input: '123,55', decimalPlaces: 3, format: italian}) // => ['123,55', 123550n]
*/
export function parseInputToBigInt({
input,
decimalPlaces,
format,
precision = decimalPlaces,
}: {
input: string
decimalPlaces: number
format: Numbers.Locale
precision?: number
}): [string, bigint] {
const {decimalSeparator} = format
const invalidCharsRegex = new RegExp(`[^0-9${decimalSeparator}]`, 'g')
const sanitizedInput =
input === '' ? '' : input.replace(invalidCharsRegex, '')

if (sanitizedInput === '') return ['', 0n]
if (sanitizedInput.startsWith(decimalSeparator))
return [`0${decimalSeparator}`, 0n]

const parts = sanitizedInput.split(decimalSeparator)
let formattedValue = sanitizedInput
let numericalValue = sanitizedInput

if (parts.length <= 1) {
const quantity = BigInt(
new BigNumber(numericalValue.replace(decimalSeparator, '.'))
.decimalPlaces(precision)
.shiftedBy(decimalPlaces)
.toFixed(0, BigNumber.ROUND_DOWN),
)
return [
new BigNumber(formattedValue.replace(decimalSeparator, '.')).toFormat(),
quantity,
]
}

const [integerPart, decimalPart] = parts
formattedValue = `${integerPart}${decimalSeparator}${decimalPart?.slice(
0,
precision,
)}1`
numericalValue = `${integerPart}${decimalSeparator}${decimalPart?.slice(
0,
precision,
)}`

const formattedNumber = new BigNumber(
formattedValue.replace(decimalSeparator, '.'),
).toFormat()

// Remove the temporary character used for formatting purposes
const textOutput = formattedNumber.slice(0, -1)

const quantity = BigInt(
new BigNumber(numericalValue.replace(decimalSeparator, '.'))
.decimalPlaces(precision)
.shiftedBy(decimalPlaces)
.toFixed(0, BigNumber.ROUND_DOWN),
)

return [textOutput, quantity]
}
10 changes: 6 additions & 4 deletions packages/common/src/numbers/to-bigint.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import BigNumber from 'bignumber.js' // Make sure to install this package
import BigNumber from 'bignumber.js'

/**
* @description Converts a number to a bigint in atomic units
* @param input string | number | BigNumber
* don't use this to format inputs use parseInputToBigInt instead
*
* @param quantity string | number | BigNumber
* @param decimalPlaces
* @returns bigint with atomic units
*
Expand All @@ -12,10 +14,10 @@ import BigNumber from 'bignumber.js' // Make sure to install this package
* toBigInt('1', 18) // => 1000000000000000000n
*/
export function toBigInt(
input: string | number | BigNumber,
quantity: string | number | BigNumber,
decimalPlaces: number,
): bigint {
const bigNumber = BigNumber(input || 0)
const bigNumber = BigNumber(quantity || 0)

const scaledNumber = bigNumber.shiftedBy(decimalPlaces)

Expand Down

0 comments on commit 1c9e72c

Please sign in to comment.