Skip to content

Commit

Permalink
feat(wallet-mobile): new tx review foreign utxo info (#3746)
Browse files Browse the repository at this point in the history
  • Loading branch information
banklesss authored Nov 26, 2024
1 parent 27f6446 commit cc8da86
Show file tree
Hide file tree
Showing 18 changed files with 406 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {CredKind} from '@emurgo/cross-csl-core'
import {isNonNullable} from '@yoroi/common'
import {infoExtractName} from '@yoroi/portfolio'
import {Portfolio} from '@yoroi/types'
import {ApiUtxoData, Portfolio} from '@yoroi/types'
import {NetworkApi} from '@yoroi/types/lib/typescript/network/manager'
import _ from 'lodash'
import {useQuery} from 'react-query'

Expand All @@ -11,6 +12,7 @@ import {wrappedCsl} from '../../../../yoroi-wallets/cardano/wrappedCsl'
import {formatTokenWithText} from '../../../../yoroi-wallets/utils/format'
import {asQuantity} from '../../../../yoroi-wallets/utils/utils'
import {usePortfolioTokenInfos} from '../../../Portfolio/common/hooks/usePortfolioTokenInfos'
import {useSelectedNetwork} from '../../../WalletManager/common/hooks/useSelectedNetwork'
import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet'
import {
FormattedCertificate,
Expand All @@ -28,10 +30,19 @@ export const useFormattedTx = (data: TransactionBody): FormattedTx => {

const inputs = data?.inputs ?? []
const outputs = data?.outputs ?? []
const referenceInputs = data?.reference_inputs ?? []

const inputUtxos = useUtxos(inputs, wallet)
const referenceInputUtxos = useUtxos(referenceInputs, wallet)

const inputTokenIds = inputs.flatMap((i) => {
const receiveUTxO = getUtxoByTxIdAndIndex(wallet, i.transaction_id, i.index)
return receiveUTxO?.assets.map((a) => `${a.policyId}.${a.assetId}` as Portfolio.Token.Id) ?? []
const utxo = inputUtxos.find((utxo) => utxo?.tx_hash === i.transaction_id && utxo?.tx_index === i.index)
return utxo?.assets.map((a) => `${a.policyId}.${a.assetId}` as Portfolio.Token.Id) ?? []
})

const referenceInputTokenIds = referenceInputs.flatMap((i) => {
const utxo = referenceInputUtxos.find((utxo) => utxo?.tx_hash === i.transaction_id && utxo?.tx_index === i.index)
return utxo?.assets.map((a) => `${a.policyId}.${a.assetId}` as Portfolio.Token.Id) ?? []
})

const outputTokenIds = outputs.flatMap((o) => {
Expand All @@ -47,10 +58,16 @@ export const useFormattedTx = (data: TransactionBody): FormattedTx => {
const mintTokenIds =
data.mint?.map(([policyId, asset]) => `${policyId}.${Object.keys(asset)[0] ?? ''}` as Portfolio.Token.Id) ?? []

const tokenIds = _.uniq<Portfolio.Token.Id>([...inputTokenIds, ...outputTokenIds, ...mintTokenIds])
const tokenIds = _.uniq<Portfolio.Token.Id>([
...inputTokenIds,
...outputTokenIds,
...mintTokenIds,
...referenceInputTokenIds,
])
const portfolioTokenInfos = usePortfolioTokenInfos({wallet, tokenIds}, {suspense: true})

const formattedInputs = useFormattedInputs(wallet, inputs, portfolioTokenInfos)
const formattedInputs = useFormattedInputs(wallet, inputs, portfolioTokenInfos, inputUtxos)
const formattedReferenceInputs = useFormattedInputs(wallet, inputs, portfolioTokenInfos, referenceInputUtxos)
const formattedOutputs = useFormattedOutputs(wallet, outputs, portfolioTokenInfos)
const formattedFee = formatFee(wallet, data)
const formattedCertificates = formatCertificates(data.certs)
Expand All @@ -62,18 +79,19 @@ export const useFormattedTx = (data: TransactionBody): FormattedTx => {
fee: formattedFee,
certificates: formattedCertificates,
mint: formattedMintData,
referenceInputs: data.reference_inputs,
referenceInputs: formattedReferenceInputs,
}
}

export const useFormattedInputs = (
wallet: YoroiWallet,
inputs: TransactionInputs,
tokenInfosResult: ReturnType<typeof usePortfolioTokenInfos>,
inputUtxos: ReturnType<typeof useUtxos>,
) => {
const query = useQuery<FormattedInputs>(
['useFormattedInputs', inputs],
async () => formatInputs(wallet, inputs, tokenInfosResult),
async () => formatInputs(wallet, inputs, tokenInfosResult, inputUtxos),
{
suspense: true,
},
Expand Down Expand Up @@ -104,10 +122,13 @@ const formatInputs = async (
wallet: YoroiWallet,
inputs: TransactionInputs,
portfolioTokenInfos: ReturnType<typeof usePortfolioTokenInfos>,
inputUtxos: ReturnType<typeof useUtxos>,
): Promise<FormattedInputs> => {
return Promise.all(
inputs.map(async (input) => {
const receiveUTxO = getUtxoByTxIdAndIndex(wallet, input.transaction_id, input.index)
const receiveUTxO = inputUtxos.find(
(utxo) => utxo?.tx_hash === input.transaction_id && utxo?.tx_index === input.index,
)
const address = receiveUTxO?.receiver
const coin = receiveUTxO?.amount != null ? asQuantity(receiveUTxO.amount) : null

Expand Down Expand Up @@ -268,8 +289,50 @@ const getAddressKind = async (addressBech32: string): Promise<CredKind | null> =
}
}

const getUtxoByTxIdAndIndex = (wallet: YoroiWallet, txId: string, index: number) => {
return wallet.utxos.find((u) => u.tx_hash === txId && u.tx_index === index)
export const useUtxos = (inputs: TransactionInputs, wallet: YoroiWallet) => {
const {networkManager} = useSelectedNetwork()

const query = useQuery(['useUtxos', inputs], async () => getAllUtxos(inputs, wallet, networkManager.api.utxoData), {
suspense: true,
})

if (!query.data) throw new Error('invalid formatted inputs')
return query.data
}

const getAllUtxos = async (inputs: TransactionInputs, wallet: YoroiWallet, getUtxoData: NetworkApi['utxoData']) => {
return Promise.all(inputs.map((input) => getUtxo(wallet, input.transaction_id, input.index, getUtxoData))) ?? []
}

const getUtxo = async (wallet: YoroiWallet, txHash: string, txIndex: number, getUtxoData: NetworkApi['utxoData']) => {
const internalUtxo = wallet.utxos.find((u) => u.tx_hash === txHash && u.tx_index === txIndex)

if (!internalUtxo) {
const externalUtxo = await getUtxoData({txHash, txIndex})
return externalUtxo != null ? toRawUtxo(externalUtxo, txHash, txIndex) : null
}

return internalUtxo
}

function toRawUtxo(utxosData: ApiUtxoData, txHash: string, txIndex: number) {
const {address, amount, assets} = utxosData.output

const mappedAssets = assets.map((asset) => ({
amount: asset.amount,
assetId: asset.assetId,
policyId: asset.policyId,
name: asset.name,
}))

return {
amount: amount,
receiver: address,
tx_hash: txHash,
tx_index: txIndex,
utxo_id: `${txHash}:${txIndex}`,
assets: mappedAssets,
}
}

const isOwnedAddress = (wallet: YoroiWallet, bech32Address: string) => {
Expand Down
48 changes: 37 additions & 11 deletions apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export const onlyAdaOneReceiver: FormattedTx = {
},
certificates: null,
mint: null,
referenceInputs: null,
referenceInputs: [],
}

export const onlyAdaOneReceiverReferenceInputs: FormattedTx = {
Expand Down Expand Up @@ -331,12 +331,38 @@ export const onlyAdaOneReceiverReferenceInputs: FormattedTx = {
mint: null,
referenceInputs: [
{
transaction_id: 'f38eb82a583b61d914d6b838509ab38c4bd398c410c9218682fb69fd702df1c4',
index: 1,
},
{
transaction_id: 'f38eb82a583b61d914d6b838509ab38c4bd398c410c9218682fb69fd702df1c4',
index: 0,
assets: [
{
tokenInfo: {
id: '.',
nature: Portfolio.Token.Nature.Primary,
type: Portfolio.Token.Type.FT,
application: Portfolio.Token.Application.Coin,
status: Portfolio.Token.Status.Valid,
fingerprint: '',
decimals: 6,
name: 'ADA',
ticker: 'ADA',
symbol: '₳',
reference: '',
tag: '',
website: 'https://www.cardano.org/',
originalImage: '',
description: 'Cardano',
},
name: 'ADA',
label: '10.618074 ADA',
quantity: '10618074',
isPrimary: true,
},
],
address:
'addr1qykrmfm7qmhpvmt6xkapegwun67wf75pcghm7p3a78gmm470ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwqrqg5yh',
addressKind: 0,
rewardAddress: 'stake1u88sshxrn2j075k7r6nqdmjcr2kpm2upvmtgxrn6ndkwehqyy4w9s',
ownAddress: true,
txIndex: 0,
txHash: '968c8b93fa086cb09fca400d2fe11b52e3b551a0527840c2dbb02796379467ca',
},
],
}
Expand Down Expand Up @@ -544,7 +570,7 @@ export const onlyAdaOneReceiverMint: FormattedTx = {
'1',
],
],
referenceInputs: null,
referenceInputs: [],
}

export const multiAssetOneReceiver: FormattedTx = {
Expand Down Expand Up @@ -862,7 +888,7 @@ export const multiAssetOneReceiver: FormattedTx = {
},
certificates: null,
mint: null,
referenceInputs: null,
referenceInputs: [],
}

const onlyAdaMultiReceiver: FormattedTx = {
Expand Down Expand Up @@ -1082,7 +1108,7 @@ const onlyAdaMultiReceiver: FormattedTx = {
},
certificates: null,
mint: null,
referenceInputs: null,
referenceInputs: [],
}

const multiAssetMultiReceiver: FormattedTx = {
Expand Down Expand Up @@ -1325,7 +1351,7 @@ const multiAssetMultiReceiver: FormattedTx = {
},
certificates: null,
mint: null,
referenceInputs: null,
referenceInputs: [],
}

export const mocks = {
Expand Down
2 changes: 1 addition & 1 deletion apps/wallet-mobile/src/features/ReviewTx/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export type FormattedTx = {
fee: FormattedFee
certificates: FormattedCertificate[] | null
mint: Array<[Portfolio.Token.Info, string]> | null
referenceInputs: TransactionBodyJSON['reference_inputs']
referenceInputs: FormattedInputs
}

export type FormattedMetadata = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -465,8 +465,8 @@ const useStyles = () => {
color: color.text_gray_medium,
},
tokenSectionLabel: {
...atoms.body_2_md_regular,
color: color.gray_900,
...atoms.body_1_lg_medium,
color: color.text_gray_medium,
},
tokenItems: {
...atoms.flex_wrap,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {useTheme} from '@yoroi/theme'
import * as React from 'react'
import {StyleSheet, Text, View} from 'react-native'
import {StyleSheet, View} from 'react-native'

import {Space} from '../../../../../../components/Space/Space'
import {Accordion} from '../../../../common/Accordion'
import {CopiableText} from '../../../../common/CopiableText'
import {useStrings} from '../../../../common/hooks/useStrings'
import {FormattedTx} from '../../../../common/types'
import {Inputs} from '../UTxOs/UTxOsTab'

export const ReferenceInputsTab = ({referenceInputs}: {referenceInputs: FormattedTx['referenceInputs']}) => {
const {styles} = useStyles()
Expand All @@ -16,22 +16,8 @@ export const ReferenceInputsTab = ({referenceInputs}: {referenceInputs: Formatte
<View style={styles.root}>
<Space height="lg" />

<Accordion label={`${strings.utxosInputsLabel} (${referenceInputs?.length ?? 0})`}>
{referenceInputs?.map((input, index) => (
<View key={index}>
<Space height="lg" />

<CopiableText textToCopy={input.transaction_id}>
<Text style={styles.input}>{input.transaction_id}</Text>

<Space width="sm" />

<Text style={styles.index}>{`#${input.index}`}</Text>

<Space width="sm" />
</CopiableText>
</View>
))}
<Accordion label={`${strings.utxosInputsLabel} (${referenceInputs.length})`}>
<Inputs inputs={referenceInputs} />
</Accordion>
</View>
)
Expand All @@ -45,15 +31,6 @@ const useStyles = () => {
...atoms.px_lg,
backgroundColor: color.bg_color_max,
},
input: {
...atoms.flex_1,
...atoms.body_2_md_regular,
color: color.text_gray_medium,
},
index: {
...atoms.body_2_md_medium,
color: color.text_gray_medium,
},
})

return {styles} as const
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const ReviewTx = ({

const showMetadataTab = !isEmptyString(formattedMetadata?.hash) && formattedMetadata?.metadata != null
const showMintTab = !!formattedTx.mint
const showReferenceInoutsTab = !!formattedTx.referenceInputs
const showReferenceInoutsTab = formattedTx.referenceInputs.length > 0

if (showMetadataTab) tabsData.push([strings.metadataTab, 'metadata'])
if (showMintTab) tabsData.push([strings.mintTab, 'mint'])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const UTxOsTab = ({tx}: {tx: FormattedTx}) => {
)
}

const Inputs = ({inputs}: {inputs: FormattedInputs}) => {
export const Inputs = ({inputs}: {inputs: FormattedInputs}) => {
return inputs.map((input, index) => <Input key={`${input.address}-${index}`} input={input} />)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,14 +254,15 @@ export function buildNetworkManagers({
const networkRootStorage = mountMMKVStorage({path: `/`, id: `${network}.manager.v1`})
const rootStorage = observableStorageMaker(networkRootStorage)
const legacyRootStorage = observableStorageMaker(mountAsyncStorage({path: `/legacy/${network}/v1/`}))
const {getProtocolParams, getBestBlock} = apiMaker({network: config.network})
const {getProtocolParams, getBestBlock, getUtxoData} = apiMaker({network: config.network})
const api = {
protocolParams: () =>
getProtocolParams().catch((error) => {
logger.error(`networkManager: ${network} protocolParams has failed, using hardcoded`, {error})
return Promise.resolve(protocolParamsPlaceholder)
}),
bestBlock: getBestBlock,
utxoData: getUtxoData,
}

const info = dateToEpochInfo(config.eras)
Expand Down
12 changes: 12 additions & 0 deletions packages/api/src/cardano/api/cardano-api-maker.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {Api} from '@yoroi/types'
import {protocolParamsMockResponse} from './protocol-params.mocks'
import {bestBlockMockResponse} from './best-block.mocks'
import {mockUtxoData} from './utxo-data.mocks'

const loading = () => new Promise(() => {})
const unknownError = () => Promise.reject(new Error('Unknown error'))
Expand Down Expand Up @@ -38,7 +39,18 @@ const getBestBlock = {
},
}

const getUtxoData = {
success: () => Promise.resolve(mockUtxoData),
delayed: (timeout?: number) => delayedResponse({data: mockUtxoData, timeout}),
empty: () => Promise.resolve({}),
loading,
error: {
unknown: unknownError,
},
}

export const mockCardanoApi: Api.Cardano.Api = {
getProtocolParams: getProtocolParams.success,
getBestBlock: getBestBlock.success,
getUtxoData: getUtxoData.success,
} as const
4 changes: 4 additions & 0 deletions packages/api/src/cardano/api/cardano-api-maker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Api, Chain} from '@yoroi/types'

import {getProtocolParams as getProtocolParamsWrapper} from './protocol-params'
import {getBestBlock as getBestBlockWrapper} from './best-block'
import {getUtxoData as getUtxoDataWapper} from './utxo-data'
import {API_ENDPOINTS} from './config'

export const cardanoApiMaker = ({
Expand All @@ -14,11 +15,14 @@ export const cardanoApiMaker = ({
request?: Fetcher
}): Readonly<Api.Cardano.Api> => {
const baseUrl = API_ENDPOINTS[network].root
const legacyBaseUrl = API_ENDPOINTS[network].legacy
const getProtocolParams = getProtocolParamsWrapper(baseUrl, request)
const getBestBlock = getBestBlockWrapper(baseUrl, request)
const getUtxoData = getUtxoDataWapper(legacyBaseUrl, request)

return freeze({
getProtocolParams,
getBestBlock,
getUtxoData,
} as const)
}
Loading

0 comments on commit cc8da86

Please sign in to comment.