From 53e54cafa73428feb8f5f172f37d47e63c5f2fd5 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 14 Jan 2026 16:00:13 -0800 Subject: [PATCH 1/7] Fix lint errors in SwapDetailsCard.tsx --- eslint.config.mjs | 1 - src/components/cards/SwapDetailsCard.tsx | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 2bbc3a7d65e..18ec0b3342a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -129,7 +129,6 @@ export default [ 'src/components/cards/StakingOptionCard.tsx', 'src/components/cards/StakingReturnsCard.tsx', 'src/components/cards/SupportCard.tsx', - 'src/components/cards/SwapDetailsCard.tsx', 'src/components/cards/TappableAccountCard.tsx', 'src/components/cards/TappableCard.tsx', 'src/components/cards/UnderlinedNumInputCard.tsx', diff --git a/src/components/cards/SwapDetailsCard.tsx b/src/components/cards/SwapDetailsCard.tsx index a0df0c33f4a..5ea561de0de 100644 --- a/src/components/cards/SwapDetailsCard.tsx +++ b/src/components/cards/SwapDetailsCard.tsx @@ -58,7 +58,7 @@ const upgradeSwapData = ( return swapData } -export function SwapDetailsCard(props: Props) { +export const SwapDetailsCard: React.FC = (props: Props) => { const { swapData, transaction, wallet } = props const theme = useTheme() const styles = getStyles(theme) @@ -124,12 +124,12 @@ export function SwapDetailsCard(props: Props) { return } - if (error) showError(error) + if (error != null) showError(error) } ) }) - const handleLink = async () => { + const handleLink = async (): Promise => { if (formattedOrderUri == null) return // Replace {{TXID}} with actual transaction ID if present @@ -140,9 +140,9 @@ export function SwapDetailsCard(props: Props) { if (available) await SafariView.show({ url: formattedOrderUri }) else await Linking.openURL(formattedOrderUri) }) - .catch(error => { + .catch((error: unknown) => { showError(error) - Linking.openURL(formattedOrderUri).catch(err => { + Linking.openURL(formattedOrderUri).catch((err: unknown) => { showError(err) }) }) @@ -192,7 +192,7 @@ export function SwapDetailsCard(props: Props) { ? walletDefaultDenom.symbol : transaction.currencyCode - const createExchangeDataString = (newline: string = '\n') => { + const createExchangeDataString = (newline: string = '\n'): string => { const uniqueIdentifier = memos .map( (memo, index) => @@ -281,7 +281,7 @@ export function SwapDetailsCard(props: Props) { ) } -const getStyles = cacheStyles((theme: Theme) => ({ +const getStyles = cacheStyles((_: Theme) => ({ tileColumn: { flexDirection: 'column', justifyContent: 'center' From 465c39cbcb524b76999c694e56f8fcbd9a1495d6 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 16 Jul 2025 14:15:01 -0700 Subject: [PATCH 2/7] Simplify exchange details tile --- src/components/cards/SwapDetailsCard.tsx | 46 ++++++------------------ 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/src/components/cards/SwapDetailsCard.tsx b/src/components/cards/SwapDetailsCard.tsx index 5ea561de0de..c1e1dfc4c8f 100644 --- a/src/components/cards/SwapDetailsCard.tsx +++ b/src/components/cards/SwapDetailsCard.tsx @@ -5,7 +5,7 @@ import type { EdgeTxSwap } from 'edge-core-js' import * as React from 'react' -import { Linking, Platform, View } from 'react-native' +import { Linking, Platform } from 'react-native' import Mailer from 'react-native-mail' import SafariView from 'react-native-safari-view' import { sprintf } from 'sprintf-js' @@ -25,7 +25,6 @@ import { convertNativeToDisplay, unixToLocaleDateTime } from '../../util/utils' import { RawTextModal } from '../modals/RawTextModal' import { EdgeRow } from '../rows/EdgeRow' import { Airship, showError } from '../services/AirshipInstance' -import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' import { EdgeCard } from './EdgeCard' @@ -60,11 +59,8 @@ const upgradeSwapData = ( export const SwapDetailsCard: React.FC = (props: Props) => { const { swapData, transaction, wallet } = props - const theme = useTheme() - const styles = getStyles(theme) const { memos = [], spendTargets = [], tokenId } = transaction - const { currencyInfo } = wallet const walletName = useWalletName(wallet) const walletDefaultDenom = useSelector(state => transaction.tokenId === null @@ -186,12 +182,6 @@ export const SwapDetailsCard: React.FC = (props: Props) => { getExchangeDenom(destinationWallet.currencyConfig, null).name })` - const symbolString = - currencyInfo.currencyCode === transaction.currencyCode && - walletDefaultDenom.symbol != null - ? walletDefaultDenom.symbol - : transaction.currencyCode - const createExchangeDataString = (newline: string = '\n'): string => { const uniqueIdentifier = memos .map( @@ -243,23 +233,16 @@ export const SwapDetailsCard: React.FC = (props: Props) => { title={lstrings.transaction_details_exchange_details} onPress={handleExchangeDetails} > - - - {lstrings.title_exchange + ' ' + sourceAmount + ' ' + symbolString} - - - {lstrings.string_to_capitalize + - ' ' + - destinationAmount + - ' ' + - destinationAssetName} - - - {swapData.isEstimate - ? lstrings.estimated_quote - : lstrings.fixed_quote} - - + + {`${sourceAmount} ${sourceAssetName}` + + ' → ' + + `${destinationAmount} ${destinationAssetName}`} + + + {swapData.isEstimate + ? lstrings.estimated_quote + : lstrings.fixed_quote} + {orderUri == null ? null : ( = (props: Props) => { ) } - -const getStyles = cacheStyles((_: Theme) => ({ - tileColumn: { - flexDirection: 'column', - justifyContent: 'center' - } -})) From 15477d9482366d60cb90c729ed130dff8c02bdda Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 14 Jan 2026 16:23:18 -0800 Subject: [PATCH 3/7] Introduce DataSheetModal for exchange details --- src/components/cards/SwapDetailsCard.tsx | 124 +++++++++++++++++------ src/components/modals/DataSheetModal.tsx | 67 ++++++++++++ src/locales/en_US.ts | 3 + src/locales/strings/enUS.json | 3 + 4 files changed, 165 insertions(+), 32 deletions(-) create mode 100644 src/components/modals/DataSheetModal.tsx diff --git a/src/components/cards/SwapDetailsCard.tsx b/src/components/cards/SwapDetailsCard.tsx index c1e1dfc4c8f..374a538c19d 100644 --- a/src/components/cards/SwapDetailsCard.tsx +++ b/src/components/cards/SwapDetailsCard.tsx @@ -22,7 +22,7 @@ import { useSelector } from '../../types/reactRedux' import { getTokenId } from '../../util/CurrencyInfoHelpers' import { getWalletName } from '../../util/CurrencyWalletHelpers' import { convertNativeToDisplay, unixToLocaleDateTime } from '../../util/utils' -import { RawTextModal } from '../modals/RawTextModal' +import { DataSheetModal, type DataSheetSection } from '../modals/DataSheetModal' import { EdgeRow } from '../rows/EdgeRow' import { Airship, showError } from '../services/AirshipInstance' import { EdgeText } from '../themed/EdgeText' @@ -92,16 +92,26 @@ export const SwapDetailsCard: React.FC = (props: Props) => { const handleExchangeDetails = useHandler(async () => { await Airship.show(bridge => ( - )) }) const handleEmail = useHandler(() => { - const body = createExchangeDataString('
') + // Serialize the data sheet sections to a string: + const sections = createExchangeDataSheetSections() + const body = sections + .map(section => + // Separate rows with a newline + section.rows.map(row => row.title + ': ' + row.body).join('\n') + ) + // Separate sections with two newlines + .join('\n\n') + // Replace newlines with
tags + .replaceAll('\n', '
') Mailer.mail( { @@ -182,48 +192,98 @@ export const SwapDetailsCard: React.FC = (props: Props) => { getExchangeDenom(destinationWallet.currencyConfig, null).name })` - const createExchangeDataString = (newline: string = '\n'): string => { + const createExchangeDataSheetSections = (): DataSheetSection[] => { const uniqueIdentifier = memos .map( (memo, index) => - `${memo.value}${index + 1 !== memos.length ? newline : ''}` + `${memo.value}${index + 1 !== memos.length ? '\n' : ''}` ) .toString() const exchangeAddresses = spendTargets .map( (target, index) => `${target.publicAddress}${ - index + 1 !== spendTargets.length ? newline : '' + index + 1 !== spendTargets.length ? '\n' : '' }` ) .toString() const { dateTime } = unixToLocaleDateTime(transaction.date) - return `${lstrings.fio_date_label}: ${dateTime}${newline}${ - lstrings.transaction_details_exchange_service - }: ${plugin.displayName}${newline}${ - lstrings.transaction_details_exchange_order_id - }: ${orderId ?? ''}${newline}${ - lstrings.transaction_details_exchange_source_wallet - }: ${walletName}${newline}${ - lstrings.fragment_send_from_label - }: ${sourceAmount} ${sourceAssetName}${newline}${ - lstrings.string_to_capitalize - }: ${destinationAmount} ${destinationAssetName}${newline}${ - lstrings.transaction_details_exchange_destination_wallet - }: ${destinationWalletName}${newline}${ - isEstimate ? lstrings.estimated_quote : lstrings.fixed_quote - }${newline}${newline}${lstrings.transaction_details_tx_id_modal_title}: ${ - transaction.txid - }${newline}${newline}${ - lstrings.transaction_details_exchange_exchange_address - }:${newline}${exchangeAddresses}${newline}${newline}${ - lstrings.transaction_details_exchange_exchange_unique_id - }:${newline}${uniqueIdentifier}${newline}${newline}${ - lstrings.transaction_details_exchange_payout_address - }:${newline}${payoutAddress}${newline}${newline}${ - lstrings.transaction_details_exchange_refund_address - }:${newline}${refundAddress ?? ''}${newline}` + return [ + { + rows: [ + { + title: lstrings.fio_date_label, + body: dateTime + }, + { + title: lstrings.transaction_details_exchange_service, + body: plugin.displayName + }, + { + title: lstrings.transaction_details_exchange_order_id, + body: orderId ?? '' + }, + { + title: lstrings.quote_type, + body: isEstimate ? lstrings.estimated_quote : lstrings.fixed_quote + } + ] + }, + { + rows: [ + { + title: lstrings.transaction_details_exchange_source_wallet, + body: walletName + }, + { + title: lstrings.string_send_amount, + body: `${sourceAmount} ${sourceAssetName}` + } + ] + }, + { + rows: [ + { + title: lstrings.transaction_details_exchange_destination_wallet, + body: destinationWalletName + }, + { + title: lstrings.string_receive_amount, + body: `${destinationAmount} ${destinationAssetName}` + } + ] + }, + { + rows: [ + { + title: lstrings.transaction_details_tx_id_modal_title, + body: transaction.txid + }, + { + title: lstrings.transaction_details_exchange_exchange_address, + body: exchangeAddresses + }, + ...(uniqueIdentifier !== '' + ? [ + { + title: + lstrings.transaction_details_exchange_exchange_unique_id, + body: uniqueIdentifier + } + ] + : []), + { + title: lstrings.transaction_details_exchange_payout_address, + body: payoutAddress + }, + { + title: lstrings.transaction_details_exchange_refund_address, + body: refundAddress ?? '' + } + ] + } + ] } return ( diff --git a/src/components/modals/DataSheetModal.tsx b/src/components/modals/DataSheetModal.tsx new file mode 100644 index 00000000000..e841299c63a --- /dev/null +++ b/src/components/modals/DataSheetModal.tsx @@ -0,0 +1,67 @@ +import * as React from 'react' +import { Fragment } from 'react' +import { ScrollView } from 'react-native' +import type { AirshipBridge } from 'react-native-airship' + +import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' +import { EdgeCard } from '../cards/EdgeCard' +import { SectionHeader } from '../common/SectionHeader' +import { EdgeRow } from '../rows/EdgeRow' +import { EdgeModal } from './EdgeModal' + +interface Props { + bridge: AirshipBridge + + /** The sections data to display in the modal. */ + sections: DataSheetSection[] + + /** The title of the modal. */ + title?: string +} + +export interface DataSheetSection { + /** The title of the section. */ + title?: string + + /** The rows of the section. */ + rows: DataSheetRow[] +} + +export interface DataSheetRow { + /** The title or label of the row. */ + title: string + + /** The body text of the row. */ + body: string +} + +export const DataSheetModal: React.FC = (props: Props) => { + const { bridge, sections, title } = props + + const handleCancel = (): void => { + bridge.resolve(undefined) + } + + return ( + + + {sections.map((section, index) => ( + + {section.title == null ? null : ( + + )} + + {section.rows.map((row, index) => ( + + ))} + + + ))} + + + ) +} diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 51c353695a6..86cd4e29a4c 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -735,6 +735,8 @@ const strings = { string_save: 'Save', string_share: 'Share', string_to_capitalize: 'To', + string_send_amount: 'Send Amount', + string_receive_amount: 'Receive Amount', string_show_balance: 'Show Balance', string_amount: 'Amount', string_value: 'Value', @@ -1126,6 +1128,7 @@ const strings = { 'This swap will create an order to exchange funds at the quoted rate but might only fulfill a portion of your order.\n\nFunds that fail to swap will remain in your source wallet or be returned.', fixed_quote: 'Fixed Quote', estimated_quote: 'Estimated Quote', + quote_type: 'Quote Type', estimated_exchange_message: 'The amount above is an estimate. This exchange may result in less funds received than quoted.', buy_sell_crypto_select_country_button: 'Select your region', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index cb33ea0f5ef..4b77dffa340 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -571,6 +571,8 @@ "string_save": "Save", "string_share": "Share", "string_to_capitalize": "To", + "string_send_amount": "Send Amount", + "string_receive_amount": "Receive Amount", "string_show_balance": "Show Balance", "string_amount": "Amount", "string_value": "Value", @@ -894,6 +896,7 @@ "can_be_partial_quote_body": "This swap will create an order to exchange funds at the quoted rate but might only fulfill a portion of your order.\n\nFunds that fail to swap will remain in your source wallet or be returned.", "fixed_quote": "Fixed Quote", "estimated_quote": "Estimated Quote", + "quote_type": "Quote Type", "estimated_exchange_message": "The amount above is an estimate. This exchange may result in less funds received than quoted.", "buy_sell_crypto_select_country_button": "Select your region", "search_region": "Search region", From 3875e898d52d0d1d2ebf7cb9c6b5c285e6612b53 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 16 Jul 2025 15:26:38 -0700 Subject: [PATCH 4/7] Make SwapDetailsCard sourceWallet optional In preparation for swapData to be available on receive transactions, we may not have the source wallet to pass to this component. --- src/components/cards/SwapDetailsCard.tsx | 78 ++++++++++++------- .../scenes/TransactionDetailsScene.tsx | 2 +- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/src/components/cards/SwapDetailsCard.tsx b/src/components/cards/SwapDetailsCard.tsx index 374a538c19d..fa1f98d16ae 100644 --- a/src/components/cards/SwapDetailsCard.tsx +++ b/src/components/cards/SwapDetailsCard.tsx @@ -11,7 +11,6 @@ import SafariView from 'react-native-safari-view' import { sprintf } from 'sprintf-js' import { useHandler } from '../../hooks/useHandler' -import { useWalletName } from '../../hooks/useWalletName' import { useWatch } from '../../hooks/useWatch' import { lstrings } from '../../locales/strings' import { @@ -31,7 +30,7 @@ import { EdgeCard } from './EdgeCard' interface Props { swapData: EdgeTxSwap transaction: EdgeTransaction - wallet: EdgeCurrencyWallet + sourceWallet?: EdgeCurrencyWallet } const TXID_PLACEHOLDER = '{{TXID}}' @@ -39,9 +38,10 @@ const TXID_PLACEHOLDER = '{{TXID}}' // Metadata may have been created and saved before tokenId was required. // If tokenId is missing it defaults to null so we can try upgrading it. const upgradeSwapData = ( - destinationWallet: EdgeCurrencyWallet, + destinationWallet: EdgeCurrencyWallet | undefined, swapData: EdgeTxSwap ): EdgeTxSwap => { + if (destinationWallet == null) return swapData if ( swapData.payoutTokenId === undefined && destinationWallet.currencyInfo.currencyCode !== swapData.payoutCurrencyCode @@ -58,15 +58,9 @@ const upgradeSwapData = ( } export const SwapDetailsCard: React.FC = (props: Props) => { - const { swapData, transaction, wallet } = props + const { swapData, transaction, sourceWallet } = props const { memos = [], spendTargets = [], tokenId } = transaction - const walletName = useWalletName(wallet) - const walletDefaultDenom = useSelector(state => - transaction.tokenId === null - ? getExchangeDenom(wallet.currencyConfig, tokenId) - : selectDisplayDenom(state, wallet.currencyConfig, tokenId) - ) // The wallet may have been deleted: const account = useSelector(state => state.core.account) @@ -84,7 +78,7 @@ export const SwapDetailsCard: React.FC = (props: Props) => { payoutTokenId, plugin, refundAddress - } = upgradeSwapData(wallet, swapData) + } = upgradeSwapData(sourceWallet, swapData) const formattedOrderUri = orderUri == null ? undefined @@ -166,25 +160,37 @@ export const SwapDetailsCard: React.FC = (props: Props) => { payoutTokenId ) ) - if (destinationDenomination == null) return null const sourceNativeAmount = sub( abs(transaction.nativeAmount), transaction.networkFee ) - const sourceAmount = convertNativeToDisplay(walletDefaultDenom.multiplier)( - sourceNativeAmount + const sourceWalletDenom = useSelector(state => + sourceWallet?.currencyInfo.currencyCode === transaction.currencyCode + ? getExchangeDenom(sourceWallet.currencyConfig, tokenId) + : sourceWallet != null + ? selectDisplayDenom(state, sourceWallet.currencyConfig, tokenId) + : undefined ) + const sourceAmount = + sourceWalletDenom == null + ? undefined + : convertNativeToDisplay(sourceWalletDenom.multiplier)(sourceNativeAmount) const sourceAssetName = - tokenId == null - ? walletDefaultDenom.name - : `${walletDefaultDenom.name} (${ - getExchangeDenom(wallet.currencyConfig, null).name + sourceWalletDenom == null || sourceWallet == null + ? undefined + : tokenId == null + ? sourceWalletDenom.name + : `${sourceWalletDenom.name} (${ + getExchangeDenom(sourceWallet.currencyConfig, null).name })` - const destinationAmount = convertNativeToDisplay( - destinationDenomination.multiplier - )(swapData.payoutNativeAmount) + const destinationAmount = + destinationDenomination == null + ? undefined + : convertNativeToDisplay(destinationDenomination.multiplier)( + swapData.payoutNativeAmount + ) const destinationAssetName = payoutTokenId == null ? payoutCurrencyCode @@ -232,14 +238,22 @@ export const SwapDetailsCard: React.FC = (props: Props) => { }, { rows: [ - { - title: lstrings.transaction_details_exchange_source_wallet, - body: walletName - }, - { - title: lstrings.string_send_amount, - body: `${sourceAmount} ${sourceAssetName}` - } + ...(sourceWallet?.name == null + ? [] + : [ + { + title: lstrings.transaction_details_exchange_source_wallet, + body: sourceWallet.name + } + ]), + ...(sourceAmount == null || sourceAssetName == null + ? [] + : [ + { + title: lstrings.string_send_amount, + body: `${sourceAmount} ${sourceAssetName}` + } + ]) ] }, { @@ -286,6 +300,10 @@ export const SwapDetailsCard: React.FC = (props: Props) => { ] } + if (destinationAmount == null) { + return null + } + return ( = (props: Props) => { onPress={handleExchangeDetails} > - {`${sourceAmount} ${sourceAssetName}` + + {(sourceAmount == null ? '' : `${sourceAmount} ${sourceAssetName}`) + ' → ' + `${destinationAmount} ${destinationAssetName}`} diff --git a/src/components/scenes/TransactionDetailsScene.tsx b/src/components/scenes/TransactionDetailsScene.tsx index 5715d755c1a..3c0acf73e99 100644 --- a/src/components/scenes/TransactionDetailsScene.tsx +++ b/src/components/scenes/TransactionDetailsScene.tsx @@ -610,7 +610,7 @@ export const TransactionDetailsComponent: React.FC = props => { )} From 8b0aa2315a5c2517a17195e70a25b3e3ccdff441 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 14 Jan 2026 16:24:29 -0800 Subject: [PATCH 5/7] Remove indirection caused by destructuring --- src/components/cards/SwapDetailsCard.tsx | 63 +++++++++++------------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/src/components/cards/SwapDetailsCard.tsx b/src/components/cards/SwapDetailsCard.tsx index fa1f98d16ae..e3b81a490bc 100644 --- a/src/components/cards/SwapDetailsCard.tsx +++ b/src/components/cards/SwapDetailsCard.tsx @@ -58,31 +58,14 @@ const upgradeSwapData = ( } export const SwapDetailsCard: React.FC = (props: Props) => { - const { swapData, transaction, sourceWallet } = props - + const { transaction, sourceWallet } = props const { memos = [], spendTargets = [], tokenId } = transaction - // The wallet may have been deleted: - const account = useSelector(state => state.core.account) - const currencyWallets = useWatch(account, 'currencyWallets') - const destinationWallet = currencyWallets[swapData.payoutWalletId] - const destinationWalletName = - destinationWallet == null ? '' : getWalletName(destinationWallet) - - const { - isEstimate, - orderId, - orderUri, - payoutAddress, - payoutCurrencyCode, - payoutTokenId, - plugin, - refundAddress - } = upgradeSwapData(sourceWallet, swapData) + const swapData = upgradeSwapData(sourceWallet, props.swapData) const formattedOrderUri = - orderUri == null + swapData.orderUri == null ? undefined - : orderUri.replace(TXID_PLACEHOLDER, transaction.txid) + : swapData.orderUri.replace(TXID_PLACEHOLDER, transaction.txid) const handleExchangeDetails = useHandler(async () => { await Airship.show(bridge => ( @@ -111,10 +94,12 @@ export const SwapDetailsCard: React.FC = (props: Props) => { { subject: sprintf( lstrings.transaction_details_exchange_support_request, - plugin.displayName + swapData.plugin.displayName ), recipients: - plugin.supportEmail != null ? [plugin.supportEmail] : undefined, + swapData.plugin.supportEmail != null + ? [swapData.plugin.supportEmail] + : undefined, body, isHTML: true }, @@ -151,13 +136,19 @@ export const SwapDetailsCard: React.FC = (props: Props) => { } } + // The wallet may have been deleted: + const account = useSelector(state => state.core.account) + const currencyWallets = useWatch(account, 'currencyWallets') + const destinationWallet = currencyWallets[swapData.payoutWalletId] + const destinationWalletName = + destinationWallet == null ? '' : getWalletName(destinationWallet) const destinationDenomination = useSelector(state => - destinationWallet == null || payoutTokenId === undefined + destinationWallet == null || swapData.payoutTokenId === undefined ? undefined : selectDisplayDenom( state, destinationWallet.currencyConfig, - payoutTokenId + swapData.payoutTokenId ) ) @@ -192,9 +183,9 @@ export const SwapDetailsCard: React.FC = (props: Props) => { swapData.payoutNativeAmount ) const destinationAssetName = - payoutTokenId == null - ? payoutCurrencyCode - : `${payoutCurrencyCode} (${ + swapData.payoutTokenId == null + ? swapData.payoutCurrencyCode + : `${swapData.payoutCurrencyCode} (${ getExchangeDenom(destinationWallet.currencyConfig, null).name })` @@ -224,15 +215,17 @@ export const SwapDetailsCard: React.FC = (props: Props) => { }, { title: lstrings.transaction_details_exchange_service, - body: plugin.displayName + body: swapData.plugin.displayName }, { title: lstrings.transaction_details_exchange_order_id, - body: orderId ?? '' + body: swapData.orderId ?? '' }, { title: lstrings.quote_type, - body: isEstimate ? lstrings.estimated_quote : lstrings.fixed_quote + body: swapData.isEstimate + ? lstrings.estimated_quote + : lstrings.fixed_quote } ] }, @@ -289,11 +282,11 @@ export const SwapDetailsCard: React.FC = (props: Props) => { : []), { title: lstrings.transaction_details_exchange_payout_address, - body: payoutAddress + body: swapData.payoutAddress }, { title: lstrings.transaction_details_exchange_refund_address, - body: refundAddress ?? '' + body: swapData.refundAddress ?? '' } ] } @@ -322,7 +315,7 @@ export const SwapDetailsCard: React.FC = (props: Props) => { : lstrings.fixed_quote} - {orderUri == null ? null : ( + {swapData.orderUri == null ? null : ( = (props: Props) => { body={formattedOrderUri} /> )} - {plugin.supportEmail == null ? null : ( + {swapData.plugin.supportEmail == null ? null : ( Date: Wed, 14 Jan 2026 16:27:10 -0800 Subject: [PATCH 6/7] Fix lint errors --- eslint.config.mjs | 6 +- .../__snapshots__/Row.test.tsx.snap | 28 ++++---- .../AdvancedDetailsCard.test.tsx.snap | 2 +- .../FioAddressListScene.test.tsx.snap | 4 +- .../FioAddressRegisterScene.test.tsx.snap | 4 +- .../FioDomainRegisterScene.test.tsx.snap | 2 +- .../__snapshots__/RequestScene.test.tsx.snap | 2 +- .../__snapshots__/SendScene2.ui.test.tsx.snap | 68 +++++++++---------- .../TransactionDetailsScene.test.tsx.snap | 32 ++++----- src/components/rows/EdgeRow.tsx | 6 +- .../services/AccountCallbackManager.tsx | 26 +++---- 11 files changed, 89 insertions(+), 91 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 18ec0b3342a..5fdf223937c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -229,8 +229,6 @@ export default [ 'src/components/rows/CryptoFiatAmountRow.tsx', 'src/components/rows/CurrencyRow.tsx', - 'src/components/rows/EdgeRow.tsx', - 'src/components/rows/PaymentMethodRow.tsx', 'src/components/rows/SwapProviderRow.tsx', 'src/components/rows/TxCryptoAmountRow.tsx', @@ -307,8 +305,6 @@ export default [ 'src/components/scenes/SweepPrivateKeyCompletionScene.tsx', 'src/components/scenes/SweepPrivateKeyProcessingScene.tsx', - 'src/components/scenes/TransactionDetailsScene.tsx', - 'src/components/scenes/TransactionsExportScene.tsx', 'src/components/scenes/UpgradeUsernameScreen.tsx', @@ -317,7 +313,7 @@ export default [ 'src/components/scenes/WcConnectScene.tsx', 'src/components/scenes/WcDisconnectScene.tsx', 'src/components/scenes/WebViewScene.tsx', - 'src/components/services/AccountCallbackManager.tsx', + 'src/components/services/ActionQueueService.ts', 'src/components/services/AirshipInstance.tsx', 'src/components/services/AutoLogout.ts', diff --git a/src/__tests__/components/__snapshots__/Row.test.tsx.snap b/src/__tests__/components/__snapshots__/Row.test.tsx.snap index 0cbdda861d3..282aef143b0 100644 --- a/src/__tests__/components/__snapshots__/Row.test.tsx.snap +++ b/src/__tests__/components/__snapshots__/Row.test.tsx.snap @@ -391,7 +391,7 @@ exports[`RowUi4 renders correctly with flex: 1 children 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -525,7 +525,7 @@ exports[`RowUi4 renders correctly with flex: 1 children 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -659,7 +659,7 @@ exports[`RowUi4 renders correctly with flex: 1 children 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -793,7 +793,7 @@ exports[`RowUi4 renders correctly with flex: 1 children 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -927,7 +927,7 @@ exports[`RowUi4 renders correctly with flex: 1 children 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -1262,7 +1262,7 @@ exports[`RowUi4 renders correctly with multiple rows in a flex: 1 View 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -1412,7 +1412,7 @@ exports[`RowUi4 renders correctly with multiple rows in a flex: 1 View 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -1562,7 +1562,7 @@ exports[`RowUi4 renders correctly with multiple rows in a flex: 1 View 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -1719,7 +1719,7 @@ exports[`RowUi4 renders correctly with multiple rows in a flex: 1 View 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -1872,7 +1872,7 @@ exports[`RowUi4 should handle press events 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -2243,7 +2243,7 @@ exports[`RowUi4 should render a row with a right button 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -2392,7 +2392,7 @@ exports[`RowUi4 should render a row with a right button 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -2541,7 +2541,7 @@ exports[`RowUi4 should render a row with a right button 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -2697,7 +2697,7 @@ exports[`RowUi4 should render a row with a right button 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } diff --git a/src/__tests__/modals/__snapshots__/AdvancedDetailsCard.test.tsx.snap b/src/__tests__/modals/__snapshots__/AdvancedDetailsCard.test.tsx.snap index 76aaf0cc2ed..4f8ccc2809d 100644 --- a/src/__tests__/modals/__snapshots__/AdvancedDetailsCard.test.tsx.snap +++ b/src/__tests__/modals/__snapshots__/AdvancedDetailsCard.test.tsx.snap @@ -139,7 +139,7 @@ exports[`AdvancedDetailsCard should render with loading props 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } diff --git a/src/__tests__/scenes/__snapshots__/FioAddressListScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/FioAddressListScene.test.tsx.snap index 81897f61bd9..b4b61877d89 100644 --- a/src/__tests__/scenes/__snapshots__/FioAddressListScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/FioAddressListScene.test.tsx.snap @@ -496,7 +496,7 @@ exports[`FioAddressList should render with loading props 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -824,7 +824,7 @@ exports[`FioAddressList should render with loading props 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } diff --git a/src/__tests__/scenes/__snapshots__/FioAddressRegisterScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/FioAddressRegisterScene.test.tsx.snap index 0c712dbe809..4afd8d51975 100644 --- a/src/__tests__/scenes/__snapshots__/FioAddressRegisterScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/FioAddressRegisterScene.test.tsx.snap @@ -633,7 +633,7 @@ exports[`FioAddressRegister should render with loading props 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -815,7 +815,7 @@ exports[`FioAddressRegister should render with loading props 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } diff --git a/src/__tests__/scenes/__snapshots__/FioDomainRegisterScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/FioDomainRegisterScene.test.tsx.snap index f22e469e412..99d3fde1378 100644 --- a/src/__tests__/scenes/__snapshots__/FioDomainRegisterScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/FioDomainRegisterScene.test.tsx.snap @@ -599,7 +599,7 @@ exports[`FioDomainRegister should render with loading props 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } diff --git a/src/__tests__/scenes/__snapshots__/RequestScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/RequestScene.test.tsx.snap index 4ff78549a01..d4ab91d2182 100644 --- a/src/__tests__/scenes/__snapshots__/RequestScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/RequestScene.test.tsx.snap @@ -1144,7 +1144,7 @@ exports[`Request should render with loaded props 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } diff --git a/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap b/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap index 6db59a4cfba..156f01fd43a 100644 --- a/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap @@ -388,7 +388,7 @@ exports[`SendScene2 1 spendTarget 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -919,7 +919,7 @@ exports[`SendScene2 1 spendTarget 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -1138,7 +1138,7 @@ exports[`SendScene2 1 spendTarget 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -1385,7 +1385,7 @@ exports[`SendScene2 1 spendTarget 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -2142,7 +2142,7 @@ exports[`SendScene2 1 spendTarget with info tiles 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -2673,7 +2673,7 @@ exports[`SendScene2 1 spendTarget with info tiles 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -2892,7 +2892,7 @@ exports[`SendScene2 1 spendTarget with info tiles 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -3139,7 +3139,7 @@ exports[`SendScene2 1 spendTarget with info tiles 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -4128,7 +4128,7 @@ exports[`SendScene2 2 spendTargets 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -4412,7 +4412,7 @@ exports[`SendScene2 2 spendTargets 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -4884,7 +4884,7 @@ exports[`SendScene2 2 spendTargets 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -5103,7 +5103,7 @@ exports[`SendScene2 2 spendTargets 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -5350,7 +5350,7 @@ exports[`SendScene2 2 spendTargets 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -6107,7 +6107,7 @@ exports[`SendScene2 2 spendTargets hide tiles 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -6391,7 +6391,7 @@ exports[`SendScene2 2 spendTargets hide tiles 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -6637,7 +6637,7 @@ exports[`SendScene2 2 spendTargets hide tiles 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -6917,7 +6917,7 @@ exports[`SendScene2 2 spendTargets hide tiles 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -7674,7 +7674,7 @@ exports[`SendScene2 2 spendTargets hide tiles 2`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -7958,7 +7958,7 @@ exports[`SendScene2 2 spendTargets hide tiles 2`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -8464,7 +8464,7 @@ exports[`SendScene2 2 spendTargets hide tiles 2`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -9221,7 +9221,7 @@ exports[`SendScene2 2 spendTargets hide tiles 3`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -9505,7 +9505,7 @@ exports[`SendScene2 2 spendTargets hide tiles 3`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -9785,7 +9785,7 @@ exports[`SendScene2 2 spendTargets hide tiles 3`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -10542,7 +10542,7 @@ exports[`SendScene2 2 spendTargets lock tiles 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -10826,7 +10826,7 @@ exports[`SendScene2 2 spendTargets lock tiles 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -11232,7 +11232,7 @@ exports[`SendScene2 2 spendTargets lock tiles 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -11512,7 +11512,7 @@ exports[`SendScene2 2 spendTargets lock tiles 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -12269,7 +12269,7 @@ exports[`SendScene2 2 spendTargets lock tiles 2`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -12471,7 +12471,7 @@ exports[`SendScene2 2 spendTargets lock tiles 2`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -13197,7 +13197,7 @@ exports[`SendScene2 2 spendTargets lock tiles 2`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -13954,7 +13954,7 @@ exports[`SendScene2 2 spendTargets lock tiles 3`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -14156,7 +14156,7 @@ exports[`SendScene2 2 spendTargets lock tiles 3`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -14816,7 +14816,7 @@ exports[`SendScene2 2 spendTargets lock tiles 3`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -15573,7 +15573,7 @@ exports[`SendScene2 Render SendScene 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } diff --git a/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap index 1bcef38aaf2..8a8d277577e 100644 --- a/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap @@ -428,7 +428,7 @@ exports[`TransactionDetailsScene should render 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -849,7 +849,7 @@ exports[`TransactionDetailsScene should render 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -1486,7 +1486,7 @@ exports[`TransactionDetailsScene should render 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -1668,7 +1668,7 @@ exports[`TransactionDetailsScene should render 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -1988,7 +1988,7 @@ exports[`TransactionDetailsScene should render 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -2230,7 +2230,7 @@ exports[`TransactionDetailsScene should render 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -2419,7 +2419,7 @@ exports[`TransactionDetailsScene should render 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -2601,7 +2601,7 @@ exports[`TransactionDetailsScene should render 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -3355,7 +3355,7 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -3776,7 +3776,7 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -4413,7 +4413,7 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -4595,7 +4595,7 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -4915,7 +4915,7 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -5157,7 +5157,7 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -5346,7 +5346,7 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -5528,7 +5528,7 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi { "busy": undefined, "checked": undefined, - "disabled": undefined, + "disabled": false, "expanded": undefined, "selected": undefined, } diff --git a/src/components/rows/EdgeRow.tsx b/src/components/rows/EdgeRow.tsx index ba39bb26ef8..3d692e09ad3 100644 --- a/src/components/rows/EdgeRow.tsx +++ b/src/components/rows/EdgeRow.tsx @@ -50,13 +50,13 @@ interface Props { marginRem?: number[] | number } -export const EdgeRow = (props: Props) => { +export const EdgeRow: React.FC = (props: Props) => { const { body, children, error, icon, - loading, + loading = false, marginRem, maximumHeight = 'medium', testID, @@ -118,7 +118,7 @@ export const EdgeRow = (props: Props) => { {title == null ? null : ( {title} diff --git a/src/components/services/AccountCallbackManager.tsx b/src/components/services/AccountCallbackManager.tsx index 170416e3ef6..8a53b7dcc1e 100644 --- a/src/components/services/AccountCallbackManager.tsx +++ b/src/components/services/AccountCallbackManager.tsx @@ -44,7 +44,7 @@ const notDirty: DirtyList = { walletList: false } -export function AccountCallbackManager(props: Props) { +export const AccountCallbackManager: React.FC = (props: Props) => { const { account, navigation } = props const dispatch = useDispatch() const exchangeRates = useSelector(state => state.exchangeRates) @@ -52,7 +52,7 @@ export function AccountCallbackManager(props: Props) { const numWallets = React.useRef(0) // Helper for marking wallets dirty: - function setRatesDirty() { + function setRatesDirty(): void { setDirty(dirty => ({ ...dirty, rates: true @@ -109,7 +109,7 @@ export function AccountCallbackManager(props: Props) { cacheEntries.forEach(cacheEntry => { const { currencyCode, metadata } = cacheEntry if (tx.currencyCode !== currencyCode) return - wallet.saveTx({ ...tx, metadata }).catch(err => { + wallet.saveTx({ ...tx, metadata }).catch((err: unknown) => { console.warn(err) }) }) @@ -127,9 +127,11 @@ export function AccountCallbackManager(props: Props) { // Check for incoming FIO requests: const receivedTxs = transactions.filter(tx => !tx.isSend) if (receivedTxs.length > 0) - dispatch(checkFioObtData(wallet, receivedTxs)).catch(err => { - console.warn(err) - }) + dispatch(checkFioObtData(wallet, receivedTxs)).catch( + (err: unknown) => { + console.warn(err) + } + ) // Review triggers: deposit & transaction count for (const tx of transactions) { @@ -137,7 +139,7 @@ export function AccountCallbackManager(props: Props) { tx.savedAction?.actionType ?? tx.chainAction?.actionType if (!tx.isSend) { - dispatch(updateTransactionCount()).catch(err => { + dispatch(updateTransactionCount()).catch((err: unknown) => { console.warn(err) }) const exchangeDenom = getExchangeDenom( @@ -161,12 +163,12 @@ export function AccountCallbackManager(props: Props) { ) ) if (usdAmount > 0) { - dispatch(updateDepositAmount(usdAmount)).catch(err => { + dispatch(updateDepositAmount(usdAmount)).catch((err: unknown) => { console.warn(err) }) } } else if (actionType !== 'swap' && actionType !== 'fiat') { - dispatch(updateTransactionCount()).catch(err => { + dispatch(updateTransactionCount()).catch((err: unknown) => { console.warn(err) }) } @@ -181,11 +183,11 @@ export function AccountCallbackManager(props: Props) { if (account.username == null) { // Avoid showing modal for FIO wallets since the first transaction may be the handle creation if (wallet.currencyInfo.pluginId === 'fio') { - dispatch(refreshAllFioAddresses()).catch(err => { + dispatch(refreshAllFioAddresses()).catch((err: unknown) => { console.warn(err) }) } else { - showBackupModal({ navigation }).catch(error => { + showBackupModal({ navigation }).catch((error: unknown) => { showDevError(error) }) } @@ -244,7 +246,7 @@ export function AccountCallbackManager(props: Props) { if (dirty.walletList) { // Update all wallets (hammer mode): datelog('Updating wallet list') - await dispatch(refreshConnectedWallets).catch(err => { + await dispatch(refreshConnectedWallets).catch((err: unknown) => { console.warn(err) }) await snooze(1000) From 21db2175534aecb06a14de77bd8c99aafa92afbf Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Mon, 14 Jul 2025 13:18:44 -0700 Subject: [PATCH 7/7] Integrate getTxInfo reports server API --- CHANGELOG.md | 1 + src/components/cards/SwapDetailsCard.tsx | 41 ++- .../progress-indicators/ShimmerText.tsx | 137 ++++++++++ src/components/rows/EdgeRow.tsx | 7 +- .../scenes/TransactionDetailsScene.tsx | 65 ++++- .../services/AccountCallbackManager.tsx | 12 + src/envConfig.ts | 1 + src/locales/en_US.ts | 2 + src/locales/strings/enUS.json | 2 + src/util/pickRandom.ts | 9 + src/util/reportsServer.ts | 258 ++++++++++++++++++ 11 files changed, 529 insertions(+), 6 deletions(-) create mode 100644 src/components/progress-indicators/ShimmerText.tsx create mode 100644 src/util/pickRandom.ts create mode 100644 src/util/reportsServer.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 709a3d15553..82fc0ca8ccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased (develop) - added: `chooseCaip19Asset` EdgeProvider API for precise wallet selection using CAIP-19 identifiers +- added: Integrated reports server order status to transaction details. - changed: Append chain names to token codes in RampCreateScene ## 4.42.0 (staging) diff --git a/src/components/cards/SwapDetailsCard.tsx b/src/components/cards/SwapDetailsCard.tsx index e3b81a490bc..1955c2f32bc 100644 --- a/src/components/cards/SwapDetailsCard.tsx +++ b/src/components/cards/SwapDetailsCard.tsx @@ -20,8 +20,10 @@ import { import { useSelector } from '../../types/reactRedux' import { getTokenId } from '../../util/CurrencyInfoHelpers' import { getWalletName } from '../../util/CurrencyWalletHelpers' +import type { ReportsTxInfo } from '../../util/reportsServer' import { convertNativeToDisplay, unixToLocaleDateTime } from '../../util/utils' import { DataSheetModal, type DataSheetSection } from '../modals/DataSheetModal' +import { ShimmerText } from '../progress-indicators/ShimmerText' import { EdgeRow } from '../rows/EdgeRow' import { Airship, showError } from '../services/AirshipInstance' import { EdgeText } from '../themed/EdgeText' @@ -31,6 +33,15 @@ interface Props { swapData: EdgeTxSwap transaction: EdgeTransaction sourceWallet?: EdgeCurrencyWallet + + /** The transaction info from the reports server. */ + reportsTxInfo?: ReportsTxInfo + + /** + * Whether the transaction info from the reports server is loading. + * If not provided, the card will not show the status. + * */ + isReportsTxInfoLoading?: boolean } const TXID_PLACEHOLDER = '{{TXID}}' @@ -58,7 +69,12 @@ const upgradeSwapData = ( } export const SwapDetailsCard: React.FC = (props: Props) => { - const { transaction, sourceWallet } = props + const { + transaction, + sourceWallet, + reportsTxInfo, + isReportsTxInfoLoading = false + } = props const { memos = [], spendTargets = [], tokenId } = transaction const swapData = upgradeSwapData(sourceWallet, props.swapData) @@ -226,7 +242,15 @@ export const SwapDetailsCard: React.FC = (props: Props) => { body: swapData.isEstimate ? lstrings.estimated_quote : lstrings.fixed_quote - } + }, + ...(reportsTxInfo == null + ? [] + : [ + { + title: lstrings.transaction_details_exchange_status, + body: reportsTxInfo.swapInfo.status + } + ]) ] }, { @@ -315,6 +339,19 @@ export const SwapDetailsCard: React.FC = (props: Props) => { : lstrings.fixed_quote} + {isReportsTxInfoLoading == null ? null : ( + + {isReportsTxInfoLoading ? ( + + ) : ( + + {reportsTxInfo == null + ? lstrings.string_unknown + : reportsTxInfo.swapInfo.status} + + )} + + )} {swapData.orderUri == null ? null : ( = (props: Props) => { + const { isShown = true, characters, lines = 1 } = props + const theme = useTheme() + + const containerHeight: DimensionValue = React.useMemo( + () => theme.rem(lines * 1.5), + [lines, theme] + ) + const containerWidth: DimensionValue = React.useMemo( + () => + characters != null + ? characters * theme.rem(0.75) + : ((Math.floor(Math.random() * 80) + 20 + '%') as DimensionValue), + [characters, theme] + ) + + const [containerLayout, handleContainerLayout] = useLayout() + const containerLayoutWidth = containerLayout.width + const gradientWidth = containerLayoutWidth * 6 + + const offset = useSharedValue(0) + + const startAnimation = useHandler(() => { + const duration = 2000 + const startPosition = -gradientWidth + const endPosition = containerLayoutWidth + offset.value = startPosition + offset.value = withRepeat( + withSequence( + withTiming(startPosition, { duration: duration / 2 }), + withTiming(endPosition, { duration }) + ), + -1, + false + ) + }) + + React.useEffect(() => { + if (gradientWidth > 0) startAnimation() + }, [startAnimation, gradientWidth]) + + return isShown ? ( + + + + + + + ) : null +} + +/** + * This is the track of the component that contains a gradient that overflows + * in width. + */ +const ContainerView = styled(View)<{ + width: DimensionValue + height: DimensionValue +}>(theme => props => ({ + width: props.width, + maxWidth: '100%', + height: props.height, + borderRadius: theme.rem(0.25), + backgroundColor: theme.shimmerBackgroundColor, + overflow: 'hidden' +})) + +/** + * This is the animated view that within the {@link ContainerView}. It animates + * by an offset value which represents the horizontal position of the shimmer. + */ +const Shimmer = styled(Animated.View)<{ + width: DimensionValue + offset: SharedValue +}>(_ => props => [ + { + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + display: 'flex', + flexDirection: 'row', + width: props.width + }, + useAnimatedStyle(() => ({ + transform: [{ translateX: props.offset.value }] + })) +]) + +/** + * This is gradient nested within the {@link Shimmer}. + */ +const Gradient = styled(LinearGradient)({ + flex: 1, + width: '100%', + height: '100%' +}) diff --git a/src/components/rows/EdgeRow.tsx b/src/components/rows/EdgeRow.tsx index 3d692e09ad3..8ff95d7bccc 100644 --- a/src/components/rows/EdgeRow.tsx +++ b/src/components/rows/EdgeRow.tsx @@ -15,6 +15,7 @@ import { triggerHaptic } from '../../util/haptic' import { fixSides, mapSides, sidesToMargin } from '../../util/sides' import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity' import { ChevronRightIcon } from '../icons/ThemedIcons' +import { ShimmerText } from '../progress-indicators/ShimmerText' import { showToast } from '../services/AirshipInstance' import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' @@ -39,6 +40,7 @@ interface Props { error?: boolean icon?: React.ReactNode loading?: boolean + shimmer?: boolean maximumHeight?: 'small' | 'medium' | 'large' rightButtonType?: RowActionIcon title?: string @@ -57,6 +59,7 @@ export const EdgeRow: React.FC = (props: Props) => { error, icon, loading = false, + shimmer = false, marginRem, maximumHeight = 'medium', testID, @@ -123,7 +126,9 @@ export const EdgeRow: React.FC = (props: Props) => { {title} )} - {loading ? ( + {shimmer ? ( + + ) : loading ? ( = props => { const styles = getStyles(theme) const iconColor = useIconColor({ pluginId: currencyInfo.pluginId, tokenId }) + const transactionSwapData = (): EdgeTxSwap | undefined => + convertActionToSwapData(account, transaction) ?? transaction.swapData + + // Query for transaction info from reports server only if the transaction is + // a receive (we need to get potential swap data) or the transaction has + // swap data (we need to get the status) + const shouldShowTradeDetails = + !transaction.isSend || transactionSwapData() != null + const { data: reportsTxInfo, isLoading: isReportsTxInfoLoading } = useQuery({ + queryKey: ['txInfo', transaction.txid], + queryFn: async () => { + return await queryReportsTxInfo(wallet, transaction) + }, + staleTime: query => + // Only cache if the status has resolved, otherwise we'll always consider + // the data to be stale: + ['processing', 'pending', undefined].includes( + query.state.data?.swapInfo.status + ) + ? 0 // No cache + : Infinity, // Cache forever + enabled: shouldShowTradeDetails, + retry: false + }) + + const swapDataFromReports = useMemo( + () => + reportsTxInfo == null + ? undefined + : toEdgeTxSwap(account, wallet, transaction, reportsTxInfo), + [account, reportsTxInfo, transaction, wallet] + ) + + const edgeTxActionSwapFromReports = useMemo(() => { + if (reportsTxInfo == null) return + return toEdgeTxActionSwap(account, transaction, reportsTxInfo) + }, [account, reportsTxInfo, transaction]) + + // Update the transaction object with saveAction data from reports server: + if ( + edgeTxActionSwapFromReports != null && + transaction.savedAction !== edgeTxActionSwapFromReports + ) { + transaction.savedAction = edgeTxActionSwapFromReports + transaction.assetAction = { + assetActionType: 'swap' + } + } + // Choose a default category based on metadata or the txAction const { action, @@ -96,8 +152,7 @@ export const TransactionDetailsComponent: React.FC = props => { savedData } = getTxActionDisplayInfo(transaction, account, wallet) - const swapData = - convertActionToSwapData(account, transaction) ?? transaction.swapData + const swapData = transactionSwapData() ?? swapDataFromReports const thumbnailPath = useContactThumbnail(mergedData.name) ?? pluginIdIcons[iconPluginId ?? ''] @@ -610,7 +665,11 @@ export const TransactionDetailsComponent: React.FC = props => { )} diff --git a/src/components/services/AccountCallbackManager.tsx b/src/components/services/AccountCallbackManager.tsx index 8a53b7dcc1e..9eef9e3b14b 100644 --- a/src/components/services/AccountCallbackManager.tsx +++ b/src/components/services/AccountCallbackManager.tsx @@ -23,6 +23,7 @@ import { convertCurrency } from '../../selectors/WalletSelectors' import { useDispatch, useSelector } from '../../types/reactRedux' import type { NavigationBase } from '../../types/routerTypes' import { makePeriodicTask } from '../../util/PeriodicTask' +import { mergeReportsTxInfo } from '../../util/reportsServer' import { convertNativeToExchange, datelog, snooze } from '../../util/utils' import { Airship, showDevError } from './AirshipInstance' @@ -133,6 +134,17 @@ export const AccountCallbackManager: React.FC = (props: Props) => { } ) + const txsNeedingSwapData = transactions.filter( + tx => tx.swapData == null && !tx.isSend + ) + if (txsNeedingSwapData.length > 0) { + mergeReportsTxInfo(account, wallet, txsNeedingSwapData).catch( + (err: unknown) => { + console.warn(err) + } + ) + } + // Review triggers: deposit & transaction count for (const tx of transactions) { const actionType = diff --git a/src/envConfig.ts b/src/envConfig.ts index cdb4fc9d5d5..71e27fbfc0f 100644 --- a/src/envConfig.ts +++ b/src/envConfig.ts @@ -515,6 +515,7 @@ export const asEnvConfig = asObject({ port: asOptional(asString, '8008') }) ), + REPORTS_SERVERS: asOptional(asArray(asString)), THEME_SERVER: asOptional( asObject({ host: asOptional(asString, 'localhost'), diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 86cd4e29a4c..44fd29e66a1 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -864,6 +864,7 @@ const strings = { transaction_details_empty_note_placeholder: 'Tap to Add Note (Optional)', transaction_details_exchange_details: 'Exchange Details', transaction_details_exchange_service: 'Exchange Service', + transaction_details_exchange_status: 'Exchange Status', transaction_details_exchange_order_id: 'Order ID', transaction_details_exchange_source_wallet: 'Source Wallet', transaction_details_exchange_destination_wallet: 'Destination Wallet', @@ -1398,6 +1399,7 @@ const strings = { string_deny: 'Deny', string_wallet_balance: 'Wallet Balance', string_max_cap: 'MAX', + string_unknown: 'Unknown', string_warning: 'Warning', // Generic string. Same with wc_smartcontract_warning_title string_report_error: 'Report Error', string_report_sent: 'Report sent.', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 4b77dffa340..031cc44da49 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -690,6 +690,7 @@ "transaction_details_empty_note_placeholder": "Tap to Add Note (Optional)", "transaction_details_exchange_details": "Exchange Details", "transaction_details_exchange_service": "Exchange Service", + "transaction_details_exchange_status": "Exchange Status", "transaction_details_exchange_order_id": "Order ID", "transaction_details_exchange_source_wallet": "Source Wallet", "transaction_details_exchange_destination_wallet": "Destination Wallet", @@ -1103,6 +1104,7 @@ "string_deny": "Deny", "string_wallet_balance": "Wallet Balance", "string_max_cap": "MAX", + "string_unknown": "Unknown", "string_warning": "Warning", "string_report_error": "Report Error", "string_report_sent": "Report sent.", diff --git a/src/util/pickRandom.ts b/src/util/pickRandom.ts new file mode 100644 index 00000000000..7519311067a --- /dev/null +++ b/src/util/pickRandom.ts @@ -0,0 +1,9 @@ +/** + * Pick a random element from an array. + * + * @param arr - The array to pick from. + * @returns A random element from the array. + */ +export function pickRandom(arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)] +} diff --git a/src/util/reportsServer.ts b/src/util/reportsServer.ts new file mode 100644 index 00000000000..e4a149c6a63 --- /dev/null +++ b/src/util/reportsServer.ts @@ -0,0 +1,258 @@ +import { mul } from 'biggystring' +import { + asArray, + asEither, + asJSON, + asNumber, + asObject, + asString +} from 'cleaners' +import type { + EdgeAccount, + EdgeCurrencyWallet, + EdgeTransaction, + EdgeTxActionSwap, + EdgeTxSwap +} from 'edge-core-js' + +import { ENV } from '../env' +import { getExchangeDenom } from '../selectors/DenominationSelectors' +import { asEdgeTokenId } from '../types/types' +import { cleanFetch } from './cleanFetch' +import { getCurrencyCode } from './CurrencyInfoHelpers' +import { pickRandom } from './pickRandom' + +// Constants +export const REPORTS_SERVERS = ENV.REPORTS_SERVERS ?? [ + 'https://reports1.edge.app' +] + +// Types +export type ReportsTxInfo = ReturnType + +// Cleaners +const asAssetInfo = asObject({ + address: asString, + pluginId: asString, + tokenId: asEdgeTokenId, + amount: asNumber +}) + +const asSwapInfo = asObject({ + orderId: asString, + pluginId: asString, + status: asString +}) + +export const asTxInfo = asObject({ + isoDate: asString, + swapInfo: asSwapInfo, + deposit: asAssetInfo, + payout: asAssetInfo +}) + +const asGetTxInfoSuccessResponse = asJSON( + asObject({ + txs: asArray(asTxInfo) + }) +) + +const asGetTxInfoFailureResponse = asJSON( + asObject({ + error: asObject({ + message: asString + }) + }) +) + +interface GetTxInfoRequest { + addressPrefix: string + startIsoDate: string + endIsoDate: string +} + +type GetTxInfoResponse = ReturnType +const asGetTxInfoResponse = asEither( + asGetTxInfoSuccessResponse, + asGetTxInfoFailureResponse +) + +const fetchGetTxInfo = cleanFetch({ + resource: input => input.endpoint, + asResponse: asGetTxInfoResponse +}) + +/** + * Fetches transaction information from the reports server for a given wallet + * and transaction. + * + * This function: + * - Extracts the first receive address from the transaction and truncates it + * to the first 5 characters (address prefix). + * - Sets a time window of 24 hours before and after the transaction date. + * - Queries the reports server for transactions matching the address prefix and + * time window. + * - Returns the first ReportsTxInfo where the destinationAddress matches the + * full address and the destinationAmount (in native units) matches the + * transaction's nativeAmount. + * + * @param wallet - The EdgeCurrencyWallet containing the transaction. + * @param transaction - The EdgeTransaction to look up. + * @returns The matching ReportsTxInfo if found, otherwise undefined. + * @throws If the reports server returns an error. + */ +export async function queryReportsTxInfo( + wallet: EdgeCurrencyWallet, + transaction: EdgeTransaction +): Promise { + const transactionDate = new Date(transaction.date * 1000) + const address = transaction.ourReceiveAddresses?.[0] + + if (address == null) { + return null + } + + // Get first 5 characters of the address + const addressHashfix = hashfix(address) + + // Set time range: 24 hours before and after transaction + const startDate = new Date(transactionDate) + startDate.setHours(startDate.getHours() - 24) + const endDate = new Date(transactionDate) + endDate.setHours(endDate.getHours() + 24) + + // Convert dates to ISO strings + const startIsoDate = startDate.toISOString() + const endIsoDate = endDate.toISOString() + + // Query the reports server: + const baseUrl = pickRandom(REPORTS_SERVERS) + const endpoint = new URL(`${baseUrl}/v1/getTxInfo`) + endpoint.searchParams.set('addressHashfix', addressHashfix.toString()) + endpoint.searchParams.set('startIsoDate', startIsoDate) + endpoint.searchParams.set('endIsoDate', endIsoDate) + const response = await fetchGetTxInfo({ + endpoint + }) + + if ('error' in response) { + throw new Error(response.error.message) + } + + // Find the first transaction where destinationAddress matches the full address + const denom = getExchangeDenom(wallet.currencyConfig, transaction.tokenId) + const matchingTx = response.txs.find(tx => { + const destinationNativeAmount = mul(tx.payout.amount, denom.multiplier) + return ( + tx.payout.address === address && + destinationNativeAmount === transaction.nativeAmount + ) + }) + + return matchingTx ?? null +} + +/** + * Converts a ReportsTxInfo to an EdgeTxSwap. + */ +export const toEdgeTxSwap = ( + account: EdgeAccount, + wallet: EdgeCurrencyWallet, + transaction: EdgeTransaction, + txInfo: ReportsTxInfo +): EdgeTxSwap | undefined => { + const swapPlugin = account.swapConfig[txInfo.swapInfo.pluginId] + if (swapPlugin == null) { + return + } + const payoutCurrencyCode = getCurrencyCode(wallet, transaction.tokenId) + const swapData: EdgeTxSwap = { + orderId: txInfo.swapInfo.orderId, + isEstimate: false, + + // The EdgeSwapInfo from the swap plugin: + plugin: { + pluginId: swapPlugin.swapInfo.pluginId, + displayName: swapPlugin.swapInfo.displayName, + supportEmail: swapPlugin.swapInfo.supportEmail + }, + + // Address information: + payoutAddress: txInfo.payout.address, + payoutCurrencyCode, + payoutNativeAmount: txInfo.payout.amount.toString(), + payoutWalletId: transaction.walletId + } + return swapData +} + +export const toEdgeTxActionSwap = ( + account: EdgeAccount, + transaction: EdgeTransaction, + txInfo: ReportsTxInfo +): EdgeTxActionSwap | undefined => { + const swapPlugin = account.swapConfig[txInfo.swapInfo.pluginId] + if (swapPlugin == null) { + return + } + return { + actionType: 'swap', + swapInfo: swapPlugin.swapInfo, + orderId: txInfo.swapInfo.orderId, + // orderUri, isEstimate, canBePartial, refundAddress are not available in + // txInfo, so we leave them undefined. + fromAsset: { + pluginId: txInfo.deposit.pluginId, + tokenId: txInfo.deposit.tokenId, + nativeAmount: txInfo.deposit.amount.toString() + }, + toAsset: { + pluginId: txInfo.payout.pluginId, + tokenId: txInfo.payout.tokenId, + nativeAmount: txInfo.payout.amount.toString() + }, + payoutAddress: txInfo.payout.address, + payoutWalletId: transaction.walletId + } +} + +/** + * This will merge reports-server txInfo data into receive transactions which + * are missing swap metadata. + */ +export async function mergeReportsTxInfo( + account: EdgeAccount, + wallet: EdgeCurrencyWallet, + transactions: EdgeTransaction[] +): Promise { + for (const transaction of transactions) { + const reportsTxInfo = await queryReportsTxInfo(wallet, transaction) + if (reportsTxInfo == null) continue + const swapData = toEdgeTxSwap(account, wallet, transaction, reportsTxInfo) + const swapAction = toEdgeTxActionSwap(account, transaction, reportsTxInfo) + if (swapData != null) { + transaction.swapData = swapData + transaction.savedAction = swapAction + wallet.saveTx(transaction).catch((err: unknown) => { + console.warn(err) + }) + } + } +} + +/** + * The hashfix is a 5 byte value that is used to identify the payout address + * semi-uniquely. + * + * It's named hashfix because it's not a prefix or suffix but a _hash_fix. + */ +function hashfix(address: string): number { + const space = 1099511627776 // 5 bytes of space; 2^40 + const prime = 769 // large prime number + let hashfix = 0 // the final hashfix + for (let i = 0; i < address.length; i++) { + const byte = address.charCodeAt(i) + hashfix = (hashfix * prime + byte) % space + } + return hashfix +}