Updated on 2026-08-14
This commit is contained in:
parent
9efe573323
commit
9af7bad7b8
22 changed files with 1680 additions and 238 deletions
|
|
@ -32,5 +32,7 @@ internal class TxHistoryInfoToTransactionItemUMConverter(
|
|||
is TransactionItemUM.Pill -> um.copy(onClick = { txHistoryUiActions.onTransactionClick(value) })
|
||||
else -> um
|
||||
}
|
||||
// todo txHistory: render standalone TangemPay on-chain rows when TangemPay is wired into the history
|
||||
is OnChainTx.TangemPay -> TODO("TangemPay on-chain row rendering is not implemented yet")
|
||||
}
|
||||
}
|
||||
|
|
@ -51,6 +51,8 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverter(
|
|||
|
||||
override fun convert(value: TxHistoryInfo): TxHistoryDetailsUM = when (value) {
|
||||
is OnChainTx.BSDK -> onChainConverter.convert(value.txInfo)
|
||||
// todo txHistory: build the details card for standalone TangemPay rows when TangemPay is wired in
|
||||
is OnChainTx.TangemPay -> TODO("TangemPay on-chain details rendering is not implemented yet")
|
||||
is ExpressTx -> expressConverter.convert(value)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
|||
import com.tangem.domain.txhistory.TxHistoryFeatureToggles
|
||||
import com.tangem.domain.txhistory.fetcher.AppTxHistoryFetcher
|
||||
import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger
|
||||
import com.tangem.domain.txhistory.list.HistoryTxListManager
|
||||
import com.tangem.domain.txhistory.list.txHistoryInfoFlow
|
||||
import com.tangem.domain.txhistory.model.TxHistoryInfo
|
||||
import com.tangem.domain.txhistory.model.explorerHash
|
||||
import com.tangem.domain.txhistory.models.TxHistoryStateError
|
||||
|
|
@ -30,9 +32,7 @@ import com.tangem.features.txhistory.entity.TxHistoryItemsUM
|
|||
import com.tangem.features.txhistory.entity.TxHistoryUpdateListener
|
||||
import com.tangem.features.txhistory.state.TxHistoryItemsSnapshot
|
||||
import com.tangem.features.txhistory.state.TxHistoryStateController
|
||||
import com.tangem.features.txhistory.utils.HistoryTxListManager
|
||||
import com.tangem.features.txhistory.utils.TxHistoryListManager
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
import com.tangem.features.txhistory.utils.*
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.annotations.RemoveWithToggle
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -99,6 +99,7 @@ internal class TxHistoryModel @Inject constructor(
|
|||
historyTxListManagerFactory.create(
|
||||
userWalletId = params.userWalletId,
|
||||
currency = params.currency,
|
||||
modelScope = modelScope,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
|
@ -133,23 +134,34 @@ internal class TxHistoryModel @Inject constructor(
|
|||
|
||||
if (historyTxListManager != null) {
|
||||
combine(
|
||||
flow = historyTxListManager.items,
|
||||
flow = historyTxListManager.state,
|
||||
flow2 = lookupDataFlow,
|
||||
transform = { merged, lookup -> merged to lookup },
|
||||
transform = { state, lookup -> state to lookup },
|
||||
)
|
||||
.onEach { (merged, lookup) ->
|
||||
stateController.setContent(
|
||||
snapshot = TxHistoryItemsSnapshot.Items(buildUiItems(merged, lookup)),
|
||||
loadMore = ::loadMoreItems,
|
||||
onExploreClick = ::openExplorer,
|
||||
)
|
||||
}
|
||||
.onEach { (state, lookup) -> applyHistoryState(state, lookup) }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
}
|
||||
|
||||
historyTxListManager.paginationStatus
|
||||
.onEach { paginationStatus -> handlePaginationStatus(paginationStatus) }
|
||||
.launchIn(modelScope)
|
||||
private fun applyHistoryState(state: HistoryTxListManager.HistoryState, lookup: TxHistoryLookupContext) {
|
||||
when (state) {
|
||||
HistoryTxListManager.HistoryState.Loading ->
|
||||
stateController.setLoadingIfNotContent(onExploreClick = ::openExplorer)
|
||||
HistoryTxListManager.HistoryState.Unavailable ->
|
||||
stateController.setNotSupported(onExploreClick = ::openExplorer)
|
||||
HistoryTxListManager.HistoryState.Empty ->
|
||||
stateController.setEmpty(onExploreClick = ::openExplorer)
|
||||
HistoryTxListManager.HistoryState.Error ->
|
||||
stateController.setError(onReloadClick = ::reload, onExploreClick = ::openExplorer)
|
||||
is HistoryTxListManager.HistoryState.Content -> {
|
||||
stateController.setContent(
|
||||
snapshot = TxHistoryItemsSnapshot.Items(buildUiItems(state.items, lookup)),
|
||||
loadMore = ::loadMoreItems,
|
||||
onExploreClick = ::openExplorer,
|
||||
)
|
||||
stateController.updateLoadingMore(isLoadingMore = state.isLoadingMore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -192,19 +204,17 @@ internal class TxHistoryModel @Inject constructor(
|
|||
private fun initListManager() {
|
||||
modelScope.launch {
|
||||
txHistoryListManager?.init()
|
||||
historyTxListManager?.init()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadTxInfo() {
|
||||
stateController.setLoadingIfNotContent(onExploreClick = ::openExplorer)
|
||||
modelScope.launch {
|
||||
txHistoryItemsCountUseCase.invoke(userWalletId = params.userWalletId, currency = params.currency)
|
||||
.onLeft(::handleErrorState)
|
||||
.onRight {
|
||||
txHistoryListManager?.startLoading()
|
||||
historyTxListManager?.startLoading()
|
||||
}
|
||||
txHistoryListManager?.let { legacy ->
|
||||
txHistoryItemsCountUseCase.invoke(userWalletId = params.userWalletId, currency = params.currency)
|
||||
.onLeft(::handleErrorState)
|
||||
.onRight { legacy.startLoading() }
|
||||
}
|
||||
}
|
||||
if (txHistoryFeatureToggle.isNewTxHistoryEnabled) {
|
||||
val trigger = TxHistoryFetchTrigger.TokenDetailsOpen(
|
||||
|
|
@ -219,13 +229,13 @@ internal class TxHistoryModel @Inject constructor(
|
|||
if (stateController.isNotSupported) return
|
||||
|
||||
stateController.setLoadingIfNotContent(onExploreClick = ::openExplorer)
|
||||
historyTxListManager?.reload()
|
||||
modelScope.launch {
|
||||
txHistoryItemsCountUseCase.invoke(userWalletId = params.userWalletId, currency = params.currency)
|
||||
.onLeft(::handleErrorState)
|
||||
.onRight {
|
||||
txHistoryListManager?.reload()
|
||||
historyTxListManager?.reload()
|
||||
}
|
||||
txHistoryListManager?.let { legacy ->
|
||||
txHistoryItemsCountUseCase.invoke(userWalletId = params.userWalletId, currency = params.currency)
|
||||
.onLeft(::handleErrorState)
|
||||
.onRight { legacy.reload() }
|
||||
}
|
||||
if (txHistoryFeatureToggle.isNewTxHistoryEnabled) {
|
||||
val trigger = TxHistoryFetchTrigger.TokenDetailsPTR(
|
||||
walletId = params.userWalletId,
|
||||
|
|
@ -245,9 +255,9 @@ internal class TxHistoryModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun loadMoreItems(): Boolean {
|
||||
historyTxListManager?.loadMore()
|
||||
modelScope.launch {
|
||||
txHistoryListManager?.loadMore(params.userWalletId, params.currency)
|
||||
historyTxListManager?.loadMore(params.userWalletId, params.currency)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,153 +0,0 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.txhistory.model.*
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.BatchListState
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
private typealias HistoryTxBatchAction = BatchAction<Int, TxHistoryListConfig, Nothing>
|
||||
|
||||
/**
|
||||
* Redesign-only transaction-history pipeline that merges the on-chain pagination backbone with the
|
||||
* express (swap/onramp) overlay. Unlike [TxHistoryListManager] there is no legacy branch.
|
||||
*
|
||||
* The express overlay is asset-scoped and time-windowed to the oldest loaded on-chain timestamp
|
||||
* (re-subscribed via [flatMapLatest] as more pages load) and re-emits live as the express DB updates,
|
||||
* so status changes render without depending on the transaction count.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class HistoryTxListManager @AssistedInject constructor(
|
||||
private val repository: TxHistoryRepositoryV2,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
@Assisted private val userWalletId: UserWalletId,
|
||||
@Assisted private val currency: CryptoCurrency,
|
||||
) {
|
||||
|
||||
private val jobHolder = JobHolder()
|
||||
private val actionsFlow: MutableSharedFlow<HistoryTxBatchAction> = MutableSharedFlow(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
private val state: MutableStateFlow<State> = MutableStateFlow(State())
|
||||
|
||||
val items: Flow<List<TxHistoryInfo>> = state
|
||||
.filter { it.hasContent }
|
||||
.map { it.items }
|
||||
.distinctUntilChanged()
|
||||
|
||||
val paginationStatus: Flow<PaginationStatus<*>> = state.map { it.status }.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Reactive stream of a single row tracked by its [TxHistoryInfo.txId], for the in-app details sheet.
|
||||
*
|
||||
* Seeded with the tapped [item] so the sheet always has an immediate snapshot, then re-emits the matching row from
|
||||
* the live merged list as its status changes. The seed also covers rows not present in [items] yet (e.g. a pending
|
||||
* tx surfaced from the currency status), which would otherwise never resolve.
|
||||
*/
|
||||
fun txHistoryInfoFlow(item: TxHistoryInfo): Flow<TxHistoryInfo> = items
|
||||
.mapNotNull { list -> list.firstOrNull { it.txId == item.txId } }
|
||||
.onStart { emit(item) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun init() {
|
||||
coroutineScope {
|
||||
val batchFlow = repository.getTxHistoryBatchFlow(
|
||||
context = TxHistoryListBatchingContext(actionsFlow = actionsFlow, coroutineScope = this),
|
||||
batchSize = BATCH_SIZE,
|
||||
)
|
||||
|
||||
val sharedBatchState = batchFlow.state.shareIn(scope = this, started = SharingStarted.Eagerly, replay = 1)
|
||||
|
||||
val expressFlow = sharedBatchState
|
||||
.map(::oldestLoadedTimestamp)
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { fromCreatedAtMillis ->
|
||||
repository.getExpressHistory(userWalletId, currency, fromCreatedAtMillis)
|
||||
}
|
||||
// Let the merge run on the first on-chain emission before the express query resolves.
|
||||
.onStart { emit(emptyList()) }
|
||||
|
||||
combine(sharedBatchState, expressFlow) { batchState, express ->
|
||||
buildState(batchState, express)
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.onEach { state.value = it }
|
||||
.launchIn(scope = this)
|
||||
.saveIn(jobHolder)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startLoading() {
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(requestParams = TxHistoryListConfig(userWalletId, currency, shouldRefresh = false)),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun reload() {
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(requestParams = TxHistoryListConfig(userWalletId, currency, shouldRefresh = true)),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadMore(userWalletId: UserWalletId, currency: CryptoCurrency) {
|
||||
actionsFlow.emit(
|
||||
BatchAction.LoadMore(requestParams = TxHistoryListConfig(userWalletId, currency, shouldRefresh = false)),
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildState(
|
||||
batchState: BatchListState<Int, PaginationWrapper<TxInfo>>,
|
||||
express: List<ExpressTx>,
|
||||
): State {
|
||||
val onChain = batchState.data.asSequence()
|
||||
.flatMap { it.data.items.asSequence() }
|
||||
.distinctBy(TxInfo::identityKey)
|
||||
.toList()
|
||||
|
||||
val merged = mergeTxHistoryInfos(onChain = onChain, express = express)
|
||||
|
||||
return State(status = batchState.status, items = merged)
|
||||
}
|
||||
|
||||
private fun oldestLoadedTimestamp(batchState: BatchListState<Int, PaginationWrapper<TxInfo>>): Long =
|
||||
batchState.data.asSequence()
|
||||
.flatMap { it.data.items.asSequence() }
|
||||
.minOfOrNull { it.timestampInMillis }
|
||||
?: NO_LOWER_BOUND
|
||||
|
||||
private data class State(
|
||||
val status: PaginationStatus<*> = PaginationStatus.None,
|
||||
val items: List<TxHistoryInfo> = emptyList(),
|
||||
) {
|
||||
val hasContent: Boolean
|
||||
get() = status !is PaginationStatus.None &&
|
||||
status !is PaginationStatus.InitialLoading &&
|
||||
status !is PaginationStatus.InitialLoadingError
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val BATCH_SIZE = 50
|
||||
const val NO_LOWER_BOUND = 0L
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(userWalletId: UserWalletId, currency: CryptoCurrency): HistoryTxListManager
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.txhistory.model.ExpressTx
|
||||
import com.tangem.domain.txhistory.model.OnChainTx
|
||||
import com.tangem.domain.txhistory.model.TxHistoryInfo
|
||||
|
||||
/**
|
||||
* Merges the on-chain pagination backbone with the express (swap/onramp) overlay into a single
|
||||
* timestamp-DESC timeline.
|
||||
*
|
||||
* Per express op (matched to on-chain by [ExpressTx.matchHash]):
|
||||
* - matched → enrich: emit the express row carrying its on-chain leg; the on-chain tx(es)
|
||||
* of that hash are collapsed into this row (not emitted standalone).
|
||||
* - unmatched → standalone row (status shown, no on-chain leg). Both in-progress and terminal
|
||||
* (finished/failed) express ops are shown so the user always sees their deals.
|
||||
*
|
||||
* On-chain transactions that no express op claimed pass through as [OnChainTx].
|
||||
* [onChain] is expected to be already de-duplicated (by `identityKey`) by the caller.
|
||||
*/
|
||||
internal fun mergeTxHistoryInfos(onChain: List<TxInfo>, express: List<ExpressTx>): List<TxHistoryInfo> {
|
||||
val onChainByHash = onChain.associateBy { it.txHash }
|
||||
val matchedHashes = mutableSetOf<String>()
|
||||
val result = mutableListOf<TxHistoryInfo>()
|
||||
|
||||
express.forEach { op ->
|
||||
val matched = op.matchHash?.let(onChainByHash::get)
|
||||
if (matched != null) {
|
||||
result += op.withMatchedTxInfo(matched)
|
||||
matchedHashes += matched.txHash
|
||||
} else {
|
||||
result += op
|
||||
}
|
||||
}
|
||||
|
||||
onChain.forEach { tx ->
|
||||
if (tx.txHash !in matchedHashes) {
|
||||
result += OnChainTx.BSDK(tx)
|
||||
}
|
||||
}
|
||||
|
||||
return result.sortedByDescending(TxHistoryInfo::timestampMillis)
|
||||
}
|
||||
|
||||
private fun ExpressTx.withMatchedTxInfo(txInfo: TxInfo): ExpressTx {
|
||||
val matched = OnChainTx.BSDK(txInfo)
|
||||
return when (this) {
|
||||
is ExpressTx.Swap -> copy(txInfo = matched)
|
||||
is ExpressTx.Onramp -> copy(txInfo = matched)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.express.models.ExchangeTransaction
|
||||
import com.tangem.domain.express.models.ExpressAsset.ID as ExpressAssetId
|
||||
import com.tangem.domain.express.models.ExpressExchangeStatus
|
||||
import com.tangem.domain.express.models.ExpressTransactionAsset
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.txhistory.model.ExpressTx
|
||||
import com.tangem.domain.txhistory.model.OnChainTx
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TxHistoryInfoMergerTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN express op matched to on-chain WHEN merge THEN enriched single row and on-chain not duplicated`() {
|
||||
// Arrange
|
||||
val onChain = listOf(createTxInfo(txHash = "h1", timestamp = 100))
|
||||
val express = listOf(createSwap(matchHash = "h1", status = ExpressExchangeStatus.Waiting))
|
||||
|
||||
// Act
|
||||
val result = mergeTxHistoryInfos(onChain, express)
|
||||
|
||||
// Assert
|
||||
assertThat(result).hasSize(1)
|
||||
val row = result.single()
|
||||
assertThat(row).isInstanceOf(ExpressTx.Swap::class.java)
|
||||
assertThat((row as ExpressTx).txInfo).isInstanceOf(OnChainTx.BSDK::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unmatched active express op WHEN merge THEN standalone live row kept`() {
|
||||
// Arrange
|
||||
val express = listOf(createSwap(matchHash = "missing", status = ExpressExchangeStatus.Waiting))
|
||||
|
||||
// Act
|
||||
val result = mergeTxHistoryInfos(onChain = emptyList(), express = express)
|
||||
|
||||
// Assert
|
||||
assertThat(result).hasSize(1)
|
||||
assertThat((result.single() as ExpressTx).txInfo).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unmatched terminal express op WHEN merge THEN standalone row kept`() {
|
||||
// Arrange
|
||||
val express = listOf(createSwap(matchHash = "missing", status = ExpressExchangeStatus.Finished))
|
||||
|
||||
// Act
|
||||
val result = mergeTxHistoryInfos(onChain = emptyList(), express = express)
|
||||
|
||||
// Assert
|
||||
assertThat(result).hasSize(1)
|
||||
val row = result.single()
|
||||
assertThat(row).isInstanceOf(ExpressTx.Swap::class.java)
|
||||
assertThat((row as ExpressTx).txInfo).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN on-chain tx unclaimed by express WHEN merge THEN passed through as OnChain`() {
|
||||
// Arrange
|
||||
val onChain = listOf(createTxInfo(txHash = "h1", timestamp = 100))
|
||||
|
||||
// Act
|
||||
val result = mergeTxHistoryInfos(onChain, express = emptyList())
|
||||
|
||||
// Assert
|
||||
assertThat(result).hasSize(1)
|
||||
assertThat(result.single()).isInstanceOf(OnChainTx.BSDK::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN rows of different timestamps WHEN merge THEN sorted by timestamp descending`() {
|
||||
// Arrange
|
||||
val onChain = listOf(createTxInfo(txHash = "h1", timestamp = 100))
|
||||
val express = listOf(
|
||||
createSwap(matchHash = "missing", createdAtMillis = 200, status = ExpressExchangeStatus.Waiting),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = mergeTxHistoryInfos(onChain, express)
|
||||
|
||||
// Assert
|
||||
assertThat(result.map { it.timestampMillis }).containsExactly(200L, 100L).inOrder()
|
||||
}
|
||||
|
||||
private fun createTxInfo(txHash: String, timestamp: Long) = TxInfo(
|
||||
txHash = txHash,
|
||||
timestampInMillis = timestamp,
|
||||
isOutgoing = true,
|
||||
destinationType = TxInfo.DestinationType.Single(TxInfo.AddressType.User("addr")),
|
||||
sourceType = TxInfo.SourceType.Single("addr"),
|
||||
interactionAddressType = null,
|
||||
status = TxInfo.TransactionStatus.Confirmed,
|
||||
type = TxInfo.TransactionType.Transfer,
|
||||
amount = BigDecimal.ONE,
|
||||
)
|
||||
|
||||
private fun createSwap(
|
||||
matchHash: String?,
|
||||
status: ExpressExchangeStatus,
|
||||
createdAtMillis: Long = 100,
|
||||
isOutgoing: Boolean = true,
|
||||
) = ExpressTx.Swap(
|
||||
tx = ExchangeTransaction(
|
||||
txId = "tx-1",
|
||||
status = status,
|
||||
createdAtMillis = createdAtMillis,
|
||||
provider = null,
|
||||
payinHash = matchHash.takeIf { isOutgoing },
|
||||
payoutHash = matchHash.takeUnless { isOutgoing },
|
||||
fromAddress = null,
|
||||
payoutAddress = null,
|
||||
fromAsset = ExpressTransactionAsset(
|
||||
id = ExpressAssetId(networkId = "eth", contractAddress = "0"),
|
||||
amount = BigDecimal("1.5"),
|
||||
decimals = 18,
|
||||
),
|
||||
toAsset = ExpressTransactionAsset(
|
||||
id = ExpressAssetId(networkId = "btc", contractAddress = "0xt"),
|
||||
amount = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
),
|
||||
isOutgoing = isOutgoing,
|
||||
txInfo = null,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue