diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt index 1efb779b34..6e10964419 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt @@ -121,8 +121,8 @@ internal class DefaultNFTRuntimeStore( }, ) } - val assetsCount = when (assets) { - is NFTCollection.Assets.Value -> assets.items.size + val assetsCount = when { + assets is NFTCollection.Assets.Value && assets.items.isNotEmpty() -> assets.items.size else -> data.count } data.copy( diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt index 6b3694be92..24effa1faa 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt @@ -3,6 +3,7 @@ package com.tangem.datasource.local.nft import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi import com.tangem.blockchain.nft.models.NFTCollection @@ -58,6 +59,7 @@ class NFTPersistenceStoreFactory @Inject constructor( types = types, defaultValue = defaultValue, ), + corruptionHandler = ReplaceFileCorruptionHandler { defaultValue }, produceFile = { context.dataStoreFile(fileName = fileName) }, scope = appScope, ) diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/local/nft/DefaultNFTRuntimeStoreTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/local/nft/DefaultNFTRuntimeStoreTest.kt new file mode 100644 index 0000000000..f09b40c825 --- /dev/null +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/local/nft/DefaultNFTRuntimeStoreTest.kt @@ -0,0 +1,136 @@ +package com.tangem.datasource.local.nft + +import com.google.common.truth.Truth.assertThat +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.network.Network +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.nft.models.NFTCollection +import com.tangem.domain.nft.models.NFTCollections +import com.tangem.domain.nft.models.NFTSalePrice +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import java.math.BigInteger + +class DefaultNFTRuntimeStoreTest { + + private val network = createNetwork() + + private val store = DefaultNFTRuntimeStore( + network = network, + collectionsRuntimeStore = RuntimeSharedStore(), + pricesRuntimeStore = RuntimeSharedStore(), + ) + + @Test + fun `GIVEN collection with empty loaded assets WHEN getCollections THEN collection is kept`() = runTest { + // Arrange + val collection = createCollection( + count = 1, + assets = NFTCollection.Assets.Value(items = emptyList(), source = StatusSource.ACTUAL), + ) + store.initialize(collections = createCollections(collection), prices = emptyMap()) + + // Act + val content = store.getCollections().first().content + + // Assert + assertThat(content).isInstanceOf(NFTCollections.Content.Collections::class.java) + content as NFTCollections.Content.Collections + val actual = content.collections.orEmpty().single() + assertThat(actual.count).isEqualTo(1) + assertThat(actual.assets).isInstanceOf(NFTCollection.Assets.Value::class.java) + } + + @Test + fun `GIVEN collection with loaded assets WHEN getCollections THEN count recalculated from assets`() = runTest { + // Arrange + val collection = createCollection( + count = 5, + assets = NFTCollection.Assets.Value(items = listOf(createAsset()), source = StatusSource.ACTUAL), + ) + store.initialize(collections = createCollections(collection), prices = emptyMap()) + + // Act + val content = store.getCollections().first().content + + // Assert + content as NFTCollections.Content.Collections + assertThat(content.collections.orEmpty().single().count).isEqualTo(1) + } + + @Test + fun `GIVEN collection with zero count and not loaded assets WHEN getCollections THEN collection filtered out`() = + runTest { + // Arrange + val collection = createCollection(count = 0, assets = NFTCollection.Assets.Empty) + store.initialize(collections = createCollections(collection), prices = emptyMap()) + + // Act + val content = store.getCollections().first().content + + // Assert + content as NFTCollections.Content.Collections + assertThat(content.collections.orEmpty()).isEmpty() + } + + private fun createCollections(vararg collections: NFTCollection) = NFTCollections( + network = network, + content = NFTCollections.Content.Collections( + collections = collections.toList(), + source = StatusSource.ACTUAL, + ), + ) + + private fun createCollection(count: Int, assets: NFTCollection.Assets) = NFTCollection( + id = NFTCollection.Identifier.EVM(tokenAddress = TOKEN_ADDRESS), + network = network, + name = "Test collection", + description = null, + logoUrl = null, + count = count, + assets = assets, + ) + + private fun createAsset(): NFTAsset { + val assetId = NFTAsset.Identifier.EVM( + tokenAddress = TOKEN_ADDRESS, + tokenId = BigInteger.ONE, + contractType = NFTAsset.Identifier.EVM.ContractType.ERC721, + ) + return NFTAsset( + id = assetId, + collectionId = NFTCollection.Identifier.EVM(tokenAddress = TOKEN_ADDRESS), + network = network, + contractType = "ERC721", + owner = null, + name = "Test asset", + description = null, + amount = null, + decimals = 0, + salePrice = NFTSalePrice.Empty(assetId), + rarity = null, + media = null, + traits = emptyList(), + source = StatusSource.ACTUAL, + ) + } + + private fun createNetwork() = Network( + id = Network.ID(rawId = Network.RawID("ethereum"), derivationPath = Network.DerivationPath.None), + name = "Ethereum", + currencySymbol = "ETH", + derivationPath = Network.DerivationPath.None, + isTestnet = false, + standardType = Network.StandardType.ERC20, + hasFiatFeeRate = true, + canHandleTokens = true, + transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, + ) + + private companion object { + const val TOKEN_ADDRESS = "0x0000000000000000000000000000000000000001" + } +} \ No newline at end of file diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index d5e0ee4f95..dcca338ef7 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -12,6 +12,7 @@ import com.tangem.datasource.local.nft.NFTPersistenceStore import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory import com.tangem.datasource.local.nft.NFTRuntimeStore import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory +import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter import com.tangem.datasource.local.nft.converter.NFTSdkAssetIdentifierConverter import com.tangem.datasource.local.nft.converter.NFTSdkAssetSalePriceConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter @@ -181,29 +182,27 @@ internal class DefaultNFTRepository @Inject constructor( } } - getNFTPersistenceStore(userWalletId, network) - .getCollectionsSync() - ?.map { collection -> - if (collection.identifier == sdkCollectionId) { - collection.copy(assets = assets) - } else { - collection - } - } - ?.let { collections -> - saveCollectionsInRuntime( - userWalletId = userWalletId, - network = network, - collections = collections, - ) - saveCollectionsInPersistence( - userWalletId = userWalletId, - network = network, - collections = collections, - ) - } + saveAssetsInRuntime( + userWalletId = userWalletId, + network = network, + collectionId = collectionId, + assets = assets, + ) + + // local cache failures must not affect the runtime state which is already up to date + runSuspendCatching { + updateAssetsInPersistence( + userWalletId = userWalletId, + network = network, + sdkCollectionId = sdkCollectionId, + assets = assets, + ) + }.onFailure { error -> + TangemLogger.e("Failed to persist NFT assets for $network", error) + } }.onLeft { throwable -> if (throwable !is UnsupportedOperationException) { + TangemLogger.e("Failed to refresh NFT assets for $network", throwable) saveFailedStateInRuntime( userWalletId = userWalletId, network = network, @@ -269,18 +268,29 @@ internal class DefaultNFTRepository @Inject constructor( expireCollections(userWalletId, network) val collections = walletManagersFacade.getNFTCollections(userWalletId, network) - val mergedCollections = collections.mergeWithStoredAssets(userWalletId, network) + + // local cache failures must not affect successfully fetched collections + val mergedCollections = runSuspendCatching { + collections.mergeWithStoredAssets(userWalletId, network) + }.getOrElse { error -> + TangemLogger.e("Failed to merge NFT collections with stored assets for $network", error) + collections + } saveCollectionsInRuntime( userWalletId = userWalletId, network = network, collections = mergedCollections, ) - saveCollectionsInPersistence( - userWalletId = userWalletId, - network = network, - collections = mergedCollections, - ) + runSuspendCatching { + saveCollectionsInPersistence( + userWalletId = userWalletId, + network = network, + collections = mergedCollections, + ) + }.onFailure { error -> + TangemLogger.e("Failed to persist NFT collections for $network", error) + } if (refreshAssets) { mergedCollections.forEach { collection -> @@ -292,6 +302,7 @@ internal class DefaultNFTRepository @Inject constructor( } } }.onLeft { throwable -> + TangemLogger.e("Failed to refresh NFT collections for $network", throwable) saveFailedStateInRuntime( userWalletId = userWalletId, network = network, @@ -391,12 +402,72 @@ internal class DefaultNFTRepository @Inject constructor( } } + private suspend fun saveAssetsInRuntime( + userWalletId: UserWalletId, + network: Network, + collectionId: NFTCollection.Identifier, + assets: List, + ) { + val store = getNFTRuntimeStore(userWalletId, network) + val storedCollections = store.getCollectionsSync() + val content = storedCollections.content as? NFTCollections.Content.Collections ?: return + + val convertedAssets = assets + .map { asset -> NFTSdkAssetConverter.convert(network to asset) } + .filter { it.id !is NFTAsset.Identifier.Unknown } + + val updatedCollections = content.collections + ?.map { collection -> + if (collection.id == collectionId) { + collection.copy( + assets = NFTCollection.Assets.Value( + items = convertedAssets, + source = StatusSource.ACTUAL, + ), + ) + } else { + collection + } + } + + store.saveCollections( + storedCollections.copy(content = content.copy(collections = updatedCollections)), + ) + } + + private suspend fun updateAssetsInPersistence( + userWalletId: UserWalletId, + network: Network, + sdkCollectionId: SdkNFTCollection.Identifier, + assets: List, + ) { + val storedCollections = getNFTPersistenceStore(userWalletId, network).getCollectionsSync() ?: return + val updatedCollections = storedCollections.map { collection -> + if (collection.identifier == sdkCollectionId) { + collection.copy(assets = assets) + } else { + collection + } + } + saveCollectionsInPersistence(userWalletId, network, updatedCollections) + } + private suspend fun saveCollectionsInPersistence( userWalletId: UserWalletId, network: Network, collections: List, ) { - getNFTPersistenceStore(userWalletId, network).saveCollections(collections) + val serializableCollections = collections + .filter { it.identifier !is SdkNFTCollection.Identifier.Unknown } + .map { collection -> + collection.copy( + assets = collection.assets.filter { asset -> + asset.identifier !is SdkNFTAsset.Identifier.Unknown && + asset.collectionIdentifier !is SdkNFTCollection.Identifier.Unknown + }, + ) + } + getNFTPersistenceStore(userWalletId, network).saveCollections(serializableCollections) } private suspend fun saveSalePriceInRuntime(userWalletId: UserWalletId, network: Network, salePrice: NFTSalePrice) { diff --git a/data/nft/src/test/kotlin/com/tangem/data/nft/DefaultNFTRepositoryTest.kt b/data/nft/src/test/kotlin/com/tangem/data/nft/DefaultNFTRepositoryTest.kt new file mode 100644 index 0000000000..250cf17d20 --- /dev/null +++ b/data/nft/src/test/kotlin/com/tangem/data/nft/DefaultNFTRepositoryTest.kt @@ -0,0 +1,288 @@ +package com.tangem.data.nft + +import android.content.Context +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.datasource.local.nft.NFTPersistenceStore +import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory +import com.tangem.datasource.local.nft.NFTRuntimeStore +import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory +import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.nft.models.NFTCollection +import com.tangem.domain.nft.models.NFTCollections +import com.tangem.domain.nft.models.NFTSalePrice +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.io.IOException +import java.math.BigInteger +import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset +import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DefaultNFTRepositoryTest { + + private val nftPersistenceStoreFactory: NFTPersistenceStoreFactory = mockk() + private val nftRuntimeStoreFactory: NFTRuntimeStoreFactory = mockk() + private val walletManagersFacade: WalletManagersFacade = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk() + private val context: Context = mockk() + + private val userWalletId = UserWalletId("011") + private val userWallet = mockk { + every { walletId } returns userWalletId + } + + private val network: Network = MockCryptoCurrencyFactory().ethereum.network + + @BeforeEach + fun resetMocks() { + clearMocks(nftPersistenceStoreFactory, nftRuntimeStoreFactory, walletManagersFacade, userWalletsListRepository) + every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(userWallet)) + every { context.resources } returns mockk() + } + + private fun createRepository() = DefaultNFTRepository( + nftPersistenceStoreFactory = nftPersistenceStoreFactory, + nftRuntimeStoreFactory = nftRuntimeStoreFactory, + walletManagersFacade = walletManagersFacade, + dispatchers = TestingCoroutineDispatcherProvider(), + userWalletsListRepository = userWalletsListRepository, + networkFactory = mockk(), + excludedBlockchains = ExcludedBlockchains(), + context = context, + ) + + @Test + fun `GIVEN collections fetched WHEN persistence write fails THEN runtime keeps actual data`() = runTest { + // Arrange + val runtimeStore = FakeNFTRuntimeStore(network) + val persistenceStore = mockk { + coEvery { getCollectionsSync() } returns null + coEvery { getSalePricesSync() } returns null + coEvery { saveCollections(any()) } throws IOException("Failed to write to disk") + } + every { nftPersistenceStoreFactory.provide(userWalletId, network) } returns persistenceStore + every { nftRuntimeStoreFactory.provide(network) } returns runtimeStore + coEvery { walletManagersFacade.getNFTCollections(userWalletId, network) } returns listOf(createSdkCollection()) + + // Act + createRepository().refreshCollections(userWalletId, listOf(network)) + + // Assert + val content = runtimeStore.getCollectionsSync().content + assertThat(content).isInstanceOf(NFTCollections.Content.Collections::class.java) + content as NFTCollections.Content.Collections + assertThat(content.source).isEqualTo(StatusSource.ACTUAL) + assertThat(content.collections).hasSize(1) + } + + @Test + fun `GIVEN runtime has collection missing in persistence WHEN refreshAssets THEN assets saved to runtime`() = + runTest { + // Arrange + val sdkCollection = createSdkCollection() + val collectionId = NFTSdkCollectionIdentifierConverter.convert(sdkCollection.identifier) + val runtimeStore = FakeNFTRuntimeStore(network) + val persistenceStore = mockk { + coEvery { getCollectionsSync() } returns null + coEvery { getSalePricesSync() } returns null + coEvery { saveCollections(any()) } returns Unit + } + every { nftPersistenceStoreFactory.provide(userWalletId, network) } returns persistenceStore + every { nftRuntimeStoreFactory.provide(network) } returns runtimeStore + coEvery { + walletManagersFacade.getNFTCollections(userWalletId, network) + } returns listOf(sdkCollection) + coEvery { + walletManagersFacade.getNFTAssets(userWalletId, network, sdkCollection.identifier) + } returns listOf(createSdkAsset()) + coEvery { walletManagersFacade.getNFTSalePrice(userWalletId, network, any(), any()) } returns null + + val repository = createRepository() + // seed runtime store with the fetched collection, persistence stays empty + repository.refreshCollections(userWalletId, listOf(network)) + + // Act + repository.refreshAssets(userWalletId, network, collectionId) + + // Assert + val content = runtimeStore.getCollectionsSync().content + assertThat(content).isInstanceOf(NFTCollections.Content.Collections::class.java) + content as NFTCollections.Content.Collections + val assets = content.collections.orEmpty().single().assets + assertThat(assets).isInstanceOf(NFTCollection.Assets.Value::class.java) + assets as NFTCollection.Assets.Value + assertThat(assets.items).hasSize(1) + } + + @Test + fun `GIVEN fetch returns no assets WHEN refreshAssets THEN empty loaded value saved to runtime`() = runTest { + // Arrange + val sdkCollection = createSdkCollection() + val collectionId = NFTSdkCollectionIdentifierConverter.convert(sdkCollection.identifier) + val runtimeStore = FakeNFTRuntimeStore(network) + val persistenceStore = mockk { + coEvery { getCollectionsSync() } returns null + coEvery { getSalePricesSync() } returns null + coEvery { saveCollections(any()) } returns Unit + } + every { nftPersistenceStoreFactory.provide(userWalletId, network) } returns persistenceStore + every { nftRuntimeStoreFactory.provide(network) } returns runtimeStore + coEvery { + walletManagersFacade.getNFTCollections(userWalletId, network) + } returns listOf(sdkCollection) + coEvery { + walletManagersFacade.getNFTAssets(userWalletId, network, sdkCollection.identifier) + } returns emptyList() + + val repository = createRepository() + repository.refreshCollections(userWalletId, listOf(network)) + + // Act + repository.refreshAssets(userWalletId, network, collectionId) + + // Assert + val content = runtimeStore.getCollectionsSync().content + assertThat(content).isInstanceOf(NFTCollections.Content.Collections::class.java) + content as NFTCollections.Content.Collections + val assets = content.collections.orEmpty().single().assets + assertThat(assets).isInstanceOf(NFTCollection.Assets.Value::class.java) + assets as NFTCollection.Assets.Value + assertThat(assets.items).isEmpty() + } + + @Test + fun `GIVEN no cached collections WHEN fetch fails THEN error state saved to runtime`() = runTest { + // Arrange + val runtimeStore = FakeNFTRuntimeStore(network) + val persistenceStore = mockk { + coEvery { getCollectionsSync() } returns null + coEvery { getSalePricesSync() } returns null + } + every { nftPersistenceStoreFactory.provide(userWalletId, network) } returns persistenceStore + every { nftRuntimeStoreFactory.provide(network) } returns runtimeStore + coEvery { walletManagersFacade.getNFTCollections(userWalletId, network) } throws IOException("HTTP 500") + + // Act + createRepository().refreshCollections(userWalletId, listOf(network)) + + // Assert + val content = runtimeStore.getCollectionsSync().content + assertThat(content).isInstanceOf(NFTCollections.Content.Error::class.java) + } + + @Test + fun `GIVEN cached collections WHEN fetch fails THEN cache marked as only cache`() = runTest { + // Arrange + val runtimeStore = FakeNFTRuntimeStore(network) + val persistenceStore = mockk { + coEvery { getCollectionsSync() } returns listOf(createSdkCollection()) + coEvery { getSalePricesSync() } returns null + } + every { nftPersistenceStoreFactory.provide(userWalletId, network) } returns persistenceStore + every { nftRuntimeStoreFactory.provide(network) } returns runtimeStore + coEvery { walletManagersFacade.getNFTCollections(userWalletId, network) } throws IOException("HTTP 500") + + // Act + createRepository().refreshCollections(userWalletId, listOf(network)) + + // Assert + val content = runtimeStore.getCollectionsSync().content + assertThat(content).isInstanceOf(NFTCollections.Content.Collections::class.java) + content as NFTCollections.Content.Collections + assertThat(content.source).isEqualTo(StatusSource.ONLY_CACHE) + assertThat(content.collections).hasSize(1) + } + + private fun createSdkCollection(assets: List = emptyList()) = SdkNFTCollection( + identifier = SdkNFTCollection.Identifier.EVM(tokenAddress = TOKEN_ADDRESS), + blockchainId = Blockchain.Ethereum.id, + name = "Test collection", + description = null, + logoUrl = null, + count = 1, + assets = assets, + ) + + private fun createSdkAsset() = SdkNFTAsset( + identifier = SdkNFTAsset.Identifier.EVM( + tokenId = BigInteger.ONE, + tokenAddress = TOKEN_ADDRESS, + contractType = SdkNFTAsset.Identifier.EVM.ContractType.ERC721, + ), + collectionIdentifier = SdkNFTCollection.Identifier.EVM(tokenAddress = TOKEN_ADDRESS), + blockchainId = Blockchain.Ethereum.id, + contractType = "ERC721", + owner = null, + name = "Test asset", + description = null, + amount = BigInteger.ONE, + decimals = 0, + salePrice = null, + rarity = null, + media = null, + traits = emptyList(), + ) + + private class FakeNFTRuntimeStore(private val network: Network) : NFTRuntimeStore { + + private var collections: NFTCollections = NFTCollections.empty(network) + private var prices: Map = emptyMap() + + override suspend fun initialize(collections: NFTCollections, prices: Map) { + this.collections = collections + this.prices = prices + } + + override fun getCollections(): Flow = flowOf(collections) + + override suspend fun getCollectionsSync(): NFTCollections = collections + + override fun getAsset( + collectionId: NFTCollection.Identifier, + assetId: NFTAsset.Identifier, + ): Flow = flowOf(null) + + override fun getSalePrice(assetId: NFTAsset.Identifier): Flow = + flowOf(prices[assetId] ?: NFTSalePrice.Empty(assetId)) + + override suspend fun getSalePriceSync(assetId: NFTAsset.Identifier): NFTSalePrice = + prices[assetId] ?: NFTSalePrice.Empty(assetId) + + override suspend fun saveCollections(collections: NFTCollections) { + this.collections = collections + } + + override suspend fun saveSalePrice(salePrice: NFTSalePrice) { + prices = prices + (salePrice.assetId to salePrice) + } + + override suspend fun clear() { + collections = NFTCollections.empty(network) + prices = emptyMap() + } + } + + private companion object { + const val TOKEN_ADDRESS = "0x0000000000000000000000000000000000000001" + } +} \ No newline at end of file