diff --git a/blockchain-demo/src/main/java/com/tangem/demo/datastorage/DummyBlockchainDataStorage.kt b/blockchain-demo/src/main/java/com/tangem/demo/datastorage/DummyBlockchainDataStorage.kt index d5ec124e4..b00f50f7c 100644 --- a/blockchain-demo/src/main/java/com/tangem/demo/datastorage/DummyBlockchainDataStorage.kt +++ b/blockchain-demo/src/main/java/com/tangem/demo/datastorage/DummyBlockchainDataStorage.kt @@ -5,4 +5,5 @@ import com.tangem.blockchain.common.datastorage.BlockchainDataStorage internal object DummyBlockchainDataStorage : BlockchainDataStorage { override suspend fun getOrNull(key: String): String? = null override suspend fun store(key: String, value: String) = Unit + override suspend fun remove(key: String) = Unit } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/network/BinanceResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/network/BinanceResponse.kt index e52191fab..98c9d4137 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/network/BinanceResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/network/BinanceResponse.kt @@ -6,14 +6,14 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class BinanceFee( @Json(name = "fixed_fee_params") - var transactionFee: BinanceFeeData? = null, + val transactionFee: BinanceFeeData? = null, ) @JsonClass(generateAdapter = true) data class BinanceFeeData( @Json(name = "msg_type") - var messageType: String? = null, + val messageType: String? = null, @Json(name = "fee") - var value: Int? = null, + val value: Int? = null, ) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressService.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressService.kt index e163bd1f9..eeb869ee3 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressService.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressService.kt @@ -1,5 +1,6 @@ package com.tangem.blockchain.blockchains.bitcoin +import com.tangem.blockchain.blockchains.clore.CloreMainNetParams import com.tangem.blockchain.blockchains.dash.DashMainNetParams import com.tangem.blockchain.blockchains.ducatus.DucatusMainNetParams import com.tangem.blockchain.blockchains.radiant.RadiantMainNetParams @@ -39,6 +40,7 @@ open class BitcoinAddressService( Blockchain.Ravencoin -> RavencoinMainNetParams() Blockchain.RavencoinTestnet -> RavencoinTestNetParams() Blockchain.Radiant -> RadiantMainNetParams() + Blockchain.Clore -> CloreMainNetParams() else -> error( "${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}", ) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt index 5300ef012..bf13ad290 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt @@ -1,5 +1,6 @@ package com.tangem.blockchain.blockchains.bitcoin +import com.tangem.blockchain.blockchains.clore.CloreMainNetParams import com.tangem.blockchain.blockchains.dash.DashMainNetParams import com.tangem.blockchain.blockchains.ducatus.DucatusMainNetParams import com.tangem.blockchain.blockchains.ravencoin.RavencoinMainNetParams @@ -44,6 +45,7 @@ open class BitcoinTransactionBuilder( Blockchain.Dash -> DashMainNetParams() Blockchain.Ravencoin -> RavencoinMainNetParams() Blockchain.RavencoinTestnet -> RavencoinTestNetParams() + Blockchain.Clore -> CloreMainNetParams() else -> error("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}") } var unspentOutputs: List? = null diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/cardano/network/rosetta/response/RosettaCoinsResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/cardano/network/rosetta/response/RosettaCoinsResponse.kt index b61ce4572..510fecced 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/cardano/network/rosetta/response/RosettaCoinsResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/cardano/network/rosetta/response/RosettaCoinsResponse.kt @@ -34,8 +34,11 @@ internal data class RosettaCoinsResponse( @JsonClass(generateAdapter = true) data class Currency( + @Json(name = "symbol") val symbol: String?, + @Json(name = "decimals") val decimals: Int?, + @Json(name = "metadata") val metadata: Metadata?, ) { diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/network/response/CasperRpcResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/network/response/CasperRpcResponse.kt index 94b246359..dd909bfe1 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/network/response/CasperRpcResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/network/response/CasperRpcResponse.kt @@ -7,7 +7,11 @@ import com.squareup.moshi.JsonClass internal sealed interface CasperRpcResponse { /** Success response with [result] */ - data class Success(val result: Any) : CasperRpcResponse + @JsonClass(generateAdapter = true) + data class Success( + @Json(name = "result") + val result: Any, + ) : CasperRpcResponse /** Failure response with error [message] */ @JsonClass(generateAdapter = true) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/clore/CloreMainNetParams.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/clore/CloreMainNetParams.kt new file mode 100644 index 000000000..1345811c9 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/clore/CloreMainNetParams.kt @@ -0,0 +1,44 @@ +package com.tangem.blockchain.blockchains.clore + +import org.bitcoinj.core.Block +import org.bitcoinj.core.Utils +import org.bitcoinj.params.AbstractBitcoinNetParams + +@Suppress("MagicNumber") +internal class CloreMainNetParams : AbstractBitcoinNetParams() { + init { + interval = INTERVAL + targetTimespan = TARGET_TIMESPAN + + // 00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + maxTarget = Utils.decodeCompactBits(0x1e0fffffL) + addressHeader = 23 + p2shHeader = 122 + port = 8788 + bip32HeaderP2PKHpub = 0x0488b21e // The 4 byte header that serializes in base58 to "xpub". + bip32HeaderP2PKHpriv = 0x0488ade4 // The 4 byte header that serializes in base58 to "xprv" + majorityEnforceBlockUpgrade = MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE + majorityRejectBlockOutdated = MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED + majorityWindow = MAINNET_MAJORITY_WINDOW + id = ID_MAINNET + dnsSeeds = arrayOf( + "seed.clore.ai", + "seed1.clore.ai", + "seed2.clore.ai", + ) + } + + override fun getPaymentProtocolId(): String { + return PAYMENT_PROTOCOL_ID_MAINNET + } + + override fun getGenesisBlock(): Block { + return Block.createGenesis(this) // Stub genesis block + } + + private companion object { + private const val MAINNET_MAJORITY_WINDOW = 1000 + private const val MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED = 950 + private const val MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE = 750 + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/clore/CloreProvidersBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/clore/CloreProvidersBuilder.kt new file mode 100644 index 000000000..0dc40d225 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/clore/CloreProvidersBuilder.kt @@ -0,0 +1,27 @@ +package com.tangem.blockchain.blockchains.clore + +import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinNetworkProvider +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.BlockchainSdkConfig +import com.tangem.blockchain.common.network.providers.NetworkProvidersBuilder +import com.tangem.blockchain.common.network.providers.ProviderType +import com.tangem.blockchain.network.blockbook.BlockBookNetworkProviderFactory + +internal class CloreProvidersBuilder( + override val providerTypes: List, + private val config: BlockchainSdkConfig, +) : NetworkProvidersBuilder() { + + private val blockBookProviderFactory by lazy { BlockBookNetworkProviderFactory(config) } + + override fun createProviders(blockchain: Blockchain): List { + return providerTypes.mapNotNull { + when (it) { + is ProviderType.Public -> + blockBookProviderFactory + .createCloreBlockProvider(blockchain = blockchain, baseHost = it.url) + else -> null + } + } + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ducatus/network/bitcore/BitcoreResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ducatus/network/bitcore/BitcoreResponse.kt index 4f0d3d802..aeab26174 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ducatus/network/bitcore/BitcoreResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ducatus/network/bitcore/BitcoreResponse.kt @@ -6,29 +6,29 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class BitcoreBalance( @Json(name = "confirmed") - var confirmed: Long? = null, + val confirmed: Long? = null, @Json(name = "unconfirmed") - var unconfirmed: Long? = null, + val unconfirmed: Long? = null, ) @JsonClass(generateAdapter = true) data class BitcoreUtxo( @Json(name = "mintTxid") - var transactionHash: String? = null, + val transactionHash: String? = null, @Json(name = "mintIndex") - var index: Int? = null, + val index: Int? = null, @Json(name = "value") - var amount: Long? = null, + val amount: Long? = null, @Json(name = "script") - var script: String? = null, + val script: String? = null, ) @JsonClass(generateAdapter = true) data class BitcoreSendResponse( @Json(name = "txid") - var txid: String? = null, + val txid: String? = null, ) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ducatus/network/bitcore/BitcoreSendBody.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ducatus/network/bitcore/BitcoreSendBody.kt index e6076f46e..a0f9cce8d 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ducatus/network/bitcore/BitcoreSendBody.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ducatus/network/bitcore/BitcoreSendBody.kt @@ -1,6 +1,7 @@ package com.tangem.blockchain.blockchains.ducatus.network.bitcore +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class BitcoreSendBody(val rawTx: List) +data class BitcoreSendBody(@Json(name = "rawTx") val rawTx: List) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/Chain.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/Chain.kt index cff727446..ef0744706 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/Chain.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/Chain.kt @@ -79,4 +79,6 @@ enum class Chain(val id: Int, val blockchain: Blockchain?) { CoreTestnet(id = 1115, blockchain = Blockchain.CoreTestnet), Xodex(id = 2415, blockchain = Blockchain.Xodex), Canxium(id = 3003, blockchain = Blockchain.Canxium), + Chiliz(id = 88888, blockchain = Blockchain.Chiliz), + ChilizTestnet(id = 88882, blockchain = Blockchain.ChilizTestnet), } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/eip1559/EthereumLikeBlockchainExt.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/eip1559/EthereumLikeBlockchainExt.kt index 62fcfab11..ef4e95f1d 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/eip1559/EthereumLikeBlockchainExt.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/eip1559/EthereumLikeBlockchainExt.kt @@ -51,6 +51,7 @@ internal val Blockchain.isSupportEIP1559: Boolean Blockchain.EnergyWebChain, Blockchain.Mantle, Blockchain.Xodex, + Blockchain.Chiliz, -> false else -> error("Don't forget about evm here") } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/models/EthereumCompiledTransaction.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/models/EthereumCompiledTransaction.kt index 4deb46b06..2a34b7a18 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/models/EthereumCompiledTransaction.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/models/EthereumCompiledTransaction.kt @@ -1,33 +1,33 @@ package com.tangem.blockchain.blockchains.ethereum.models -import kotlinx.serialization.Serializable -import shadow.com.google.gson.annotations.SerializedName +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass /** * Model of filled Ethereum transaction used in staking */ -@Serializable +@JsonClass(generateAdapter = true) data class EthereumCompiledTransaction( - @SerializedName("from") + @Json(name = "from") val from: String, - @SerializedName("gasLimit") + @Json(name = "gasLimit") val gasLimit: String, - @SerializedName("value") + @Json(name = "value") val value: String?, - @SerializedName("to") + @Json(name = "to") val to: String, - @SerializedName("data") + @Json(name = "data") val data: String, - @SerializedName("nonce") + @Json(name = "nonce") val nonce: Int, - @SerializedName("type") + @Json(name = "type") val type: Int, - @SerializedName("gasPrice") + @Json(name = "gasPrice") val gasPrice: String?, - @SerializedName("maxFeePerGas") + @Json(name = "maxFeePerGas") val maxFeePerGas: String?, - @SerializedName("maxPriorityFeePerGas") + @Json(name = "maxPriorityFeePerGas") val maxPriorityFeePerGas: String?, - @SerializedName("chainId") + @Json(name = "chainId") val chainId: Int, ) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/providers/ChilizProvidersBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/providers/ChilizProvidersBuilder.kt new file mode 100644 index 000000000..831f68656 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/providers/ChilizProvidersBuilder.kt @@ -0,0 +1,18 @@ +package com.tangem.blockchain.blockchains.ethereum.providers + +import com.tangem.blockchain.blockchains.ethereum.network.EthereumJsonRpcProvider +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.network.providers.OnlyPublicProvidersBuilder +import com.tangem.blockchain.common.network.providers.ProviderType + +internal class ChilizProvidersBuilder( + override val providerTypes: List, +) : OnlyPublicProvidersBuilder( + providerTypes = providerTypes, + testnetProviders = listOf( + "https://spicy-rpc.chiliz.com/", + "https://chiliz-spicy.publicnode.com/", + ), +) { + override fun createProvider(url: String, blockchain: Blockchain) = EthereumJsonRpcProvider(url) +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/filecoin/network/response/FilecoinRpcResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/filecoin/network/response/FilecoinRpcResponse.kt index f99408bf7..428869fd1 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/filecoin/network/response/FilecoinRpcResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/filecoin/network/response/FilecoinRpcResponse.kt @@ -7,11 +7,16 @@ import com.squareup.moshi.JsonClass internal sealed interface FilecoinRpcResponse { /** Success response with [result] */ - data class Success(val result: Any) : FilecoinRpcResponse + @JsonClass(generateAdapter = true) + data class Success( + @Json(name = "result") + val result: Any, + ) : FilecoinRpcResponse /** Failure response with error [message] */ @JsonClass(generateAdapter = true) data class Failure( - @Json(name = "message") val message: String, + @Json(name = "message") + val message: String, ) : FilecoinRpcResponse } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/hedera/HederaWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/hedera/HederaWalletManager.kt index c35cbdbbc..4556d879f 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/hedera/HederaWalletManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/hedera/HederaWalletManager.kt @@ -87,7 +87,7 @@ internal class HederaWalletManager( if (error is BlockchainSdkError) error("Error isn't BlockchainSdkError") } - override suspend fun hasRequirements(currencyType: CryptoCurrencyType): Boolean { + private suspend fun assetRequiresAssociation(currencyType: CryptoCurrencyType): Boolean { return when (currencyType) { is CryptoCurrencyType.Coin -> false is CryptoCurrencyType.Token -> { @@ -99,7 +99,7 @@ internal class HederaWalletManager( } override suspend fun requirementsCondition(currencyType: CryptoCurrencyType): AssetRequirementsCondition? { - if (!hasRequirements(currencyType)) return null + if (!assetRequiresAssociation(currencyType)) return null return when (currencyType) { is CryptoCurrencyType.Coin -> null @@ -110,7 +110,10 @@ internal class HederaWalletManager( } else { val feeValue = exchangeRate * HBAR_TOKEN_ASSOCIATE_USD_COST val feeAmount = Amount(blockchain = wallet.blockchain, value = feeValue) - AssetRequirementsCondition.PaidTransactionWithFee(feeAmount) + AssetRequirementsCondition.PaidTransactionWithFee( + blockchain = blockchain, + feeAmount = feeAmount, + ) } } } @@ -120,7 +123,7 @@ internal class HederaWalletManager( currencyType: CryptoCurrencyType, signer: TransactionSigner, ): SimpleResult { - if (!hasRequirements(currencyType)) return SimpleResult.Success + if (!assetRequiresAssociation(currencyType)) return SimpleResult.Success return when (currencyType) { is CryptoCurrencyType.Coin -> SimpleResult.Success @@ -155,6 +158,10 @@ internal class HederaWalletManager( } } + override suspend fun discardRequirements(currencyType: CryptoCurrencyType): SimpleResult { + return SimpleResult.Success + } + override suspend fun send( transactionData: TransactionData, signer: TransactionSigner, diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransaction.java b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransaction.java index 335140894..e5d81ada0 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransaction.java +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransaction.java @@ -24,6 +24,7 @@ // based on BitcoinCashTransaction public class KaspaTransaction extends Transaction { private final byte[] TRANSACTION_SIGNING_DOMAIN = "TransactionSigningHash".getBytes(StandardCharsets.UTF_8); + private final byte[] TRANSACTION_ID = "TransactionID".getBytes(StandardCharsets.UTF_8); private final byte[] TRANSACTION_SIGNING_ECDSA_DOMAIN_HASH = Sha256Hash.of("TransactionSigningHashECDSA".getBytes(StandardCharsets.UTF_8)).getBytes(); private final int BLAKE2B_DIGEST_LENGTH = 32; @@ -130,6 +131,41 @@ public synchronized byte[] hashForSignatureWitness( return Sha256Hash.of(finalBos.toByteArray()).getBytes(); } + public synchronized byte[] transactionHash() { + ByteArrayOutputStream bos = new ByteArrayOutputStream(256); + try { + List inputs = getInputs(); + List outputs = getOutputs(); + + uint16ToByteStreamLE(0, bos); + + uint64ToByteStreamLE(BigInteger.valueOf(inputs.size()), bos); + for (TransactionInput input: inputs) { + bos.write(input.getOutpoint().getHash().getBytes()); + uint32ToByteStreamLE(input.getOutpoint().getIndex(), bos); + uint64ToByteStreamLE(BigInteger.valueOf(0), bos); + uint64ToByteStreamLE(BigInteger.valueOf(0), bos); + } + uint64ToByteStreamLE(BigInteger.valueOf(outputs.size()), bos); + for (TransactionOutput output : outputs) { + byte[] scriptBytes = output.getScriptBytes(); + uint64ToByteStreamLE(BigInteger.valueOf(output.getValue().value), bos); + uint16ToByteStreamLE(0, bos); // version + uint64ToByteStreamLE(BigInteger.valueOf(scriptBytes.length), bos); + bos.write(scriptBytes); + } + uint64ToByteStreamLE(BigInteger.valueOf(getLockTime()), bos); // lock time + bos.write(new byte[20]); // subnetwork id + uint64ToByteStreamLE(BigInteger.valueOf(0), bos); // gas + uint64ToByteStreamLE(BigInteger.valueOf(0), bos); // payload size + } catch (IOException e) { + throw new RuntimeException(e); + } + + Blake2b.Mac digest = Blake2b.Mac.newInstance(TRANSACTION_ID, BLAKE2B_DIGEST_LENGTH); + return digest.digest(bos.toByteArray()); + } + private byte[] blake2bDigestOf(byte[] input) { Blake2b.Mac digest = Blake2b.Mac.newInstance(TRANSACTION_SIGNING_DOMAIN, BLAKE2B_DIGEST_LENGTH); return digest.digest(input); diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransactionBuilder.kt index 92ad69da1..8d4184f5d 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransactionBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransactionBuilder.kt @@ -1,26 +1,41 @@ package com.tangem.blockchain.blockchains.kaspa +import com.squareup.moshi.adapter import com.tangem.blockchain.blockchains.kaspa.kaspacashaddr.KaspaAddressType import com.tangem.blockchain.blockchains.kaspa.kaspacashaddr.KaspaCashAddr +import com.tangem.blockchain.blockchains.kaspa.krc20.model.* import com.tangem.blockchain.blockchains.kaspa.network.* -import com.tangem.blockchain.common.BlockchainSdkError -import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.datastorage.BlockchainSavedData +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.network.moshi +import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.isZero +import com.tangem.common.extensions.toCompressedPublicKey import com.tangem.common.extensions.toHexString import org.bitcoinj.core.* import org.bitcoinj.core.Transaction.SigHash +import org.bitcoinj.script.Script import org.bitcoinj.script.ScriptBuilder import org.bitcoinj.script.ScriptOpCodes.* +import org.bouncycastle.jcajce.provider.digest.Blake2b import java.math.BigDecimal import java.math.BigInteger -class KaspaTransactionBuilder { - private lateinit var transaction: KaspaTransaction +class KaspaTransactionBuilder( + private val publicKey: Wallet.PublicKey, +) { private var networkParameters = KaspaMainNetParams() var unspentOutputs: List? = null - fun buildToSign(transactionData: TransactionData): Result> { + private val addressService = KaspaAddressService() + + @OptIn(ExperimentalStdlibApi::class) + private val envelopeAdapter by lazy { moshi.adapter() } + + @Suppress("MagicNumber") + fun buildToSign(transactionData: TransactionData): Result { transactionData.requireUncompiled() if (unspentOutputs.isNullOrEmpty()) { @@ -41,48 +56,231 @@ class KaspaTransactionBuilder { return Result.Failure(BlockchainSdkError.Kaspa.UtxoAmountError(MAX_INPUT_COUNT, maxAmount)) } - transaction = transactionData.toKaspaTransaction(networkParameters, unspentsToSpend, change) + val addressService = KaspaAddressService() + val sourceScript = ScriptBuilder().data(addressService.getPublicKey(transactionData.sourceAddress)).op( + OP_CODESEPARATOR, + ).build() - val hashesForSign: MutableList = MutableList(transaction.inputs.size) { byteArrayOf() } - for (input in transaction.inputs) { - val index = input.index - hashesForSign[index] = transaction.hashForSignatureWitness( - index, - input.scriptBytes, - input.value, - SigHash.ALL, - false, - ) + val destinationAddressDecoded = KaspaCashAddr.decodeCashAddress(transactionData.destinationAddress) + val destinationScript = when (destinationAddressDecoded.addressType) { + KaspaAddressType.P2PK_SCHNORR -> + ScriptBuilder.createP2PKOutputScript(destinationAddressDecoded.hash) + KaspaAddressType.P2PK_ECDSA -> + ScriptBuilder().data(destinationAddressDecoded.hash).op(OP_CODESEPARATOR).build() + KaspaAddressType.P2SH -> { + // known P2SH addresses won't throw + if (destinationAddressDecoded.hash.size != 32) error("Invalid hash length in P2SH address") + ScriptBuilder().op(OP_HASH256).data(destinationAddressDecoded.hash).op(OP_EQUAL).build() + } + null -> error("Null script type") // should never happen } - return Result.Success(hashesForSign) + + val transaction = createKaspaTransaction( + networkParameters = networkParameters, + unspentOutputs = unspentsToSpend, + transformer = { kaspaTransaction -> + kaspaTransaction.addOutput( + Coin.parseCoin(transactionData.amount.value.toPlainString()), + destinationScript, + ) + if (!change.isZero()) { + kaspaTransaction.addOutput( + Coin.parseCoin(change.toPlainString()), + sourceScript, + ) + } + kaspaTransaction + }, + ) + + return Result.Success(transaction) } - fun buildToSend(signatures: ByteArray): KaspaTransactionBody { + fun buildToSend(signatures: ByteArray, transaction: KaspaTransaction): KaspaTransactionBody { for (index in transaction.inputs.indices) { val signature = extractSignature(index, signatures) transaction.inputs[index].scriptSig = ScriptBuilder().data(signature).build() } - return KaspaTransactionBody( - KaspaTransactionData( - inputs = transaction.inputs.map { - KaspaInput( - previousOutpoint = KaspaPreviousOutpoint( - transactionId = it.outpoint.hash.toString(), - index = it.outpoint.index, - ), - signatureScript = it.scriptBytes.toHexString(), - ) - }, - outputs = transaction.outputs.map { - KaspaOutput( - amount = it.value.getValue(), - scriptPublicKey = KaspaScriptPublicKey(it.scriptBytes.toHexString()), + return buildForSendInternal(transaction) + } + + internal fun buildToSendKRC20Reveal( + signatures: ByteArray, + redeemScript: RedeemScript, + transaction: KaspaTransaction, + ): KaspaTransactionBody { + for (index in transaction.inputs.indices) { + val signature = extractSignature(index, signatures) + if (index == 0) { + transaction.inputs[index].scriptSig = ScriptBuilder() + .data(signature) + .data(redeemScript.script().program) + .build() + } else { + transaction.inputs[index].scriptSig = ScriptBuilder().data(signature).build() + } + } + return buildForSendInternal(transaction) + } + + @Suppress("LongMethod", "MagicNumber") + internal fun buildToSignKRC20Commit( + transactionData: TransactionData, + dust: BigDecimal?, + includeFee: Boolean = true, + ): Result { + transactionData.requireUncompiled() + + require(transactionData.amount.type is AmountType.Token) + + if (unspentOutputs.isNullOrEmpty()) { + return Result.Failure( + BlockchainSdkError.CustomError("Unspent outputs are missing"), + ) + } + + val unspentsToSpend = getUnspentsToSpend() + + val transactionFeeAmountValue = transactionData.fee?.amount?.value ?: BigDecimal.ZERO + + val revealFeeAmount = (transactionData.fee as? Fee.Kaspa) + ?.revealTransactionFee + ?.takeIf { includeFee } + ?.value + ?: BigDecimal.ZERO + + val commitFeeAmount = if (includeFee) { + transactionFeeAmountValue - revealFeeAmount + } else { + transactionFeeAmountValue + } + + val targetOutputAmountValue = revealFeeAmount + (dust ?: BigDecimal.ZERO) + + val resultChange = calculateChange( + amount = targetOutputAmountValue, + fee = commitFeeAmount, + unspentOutputs = getUnspentsToSpend(), + ) + + val envelope = Envelope( + p = "krc-20", + op = "transfer", + amt = transactionData.amount.longValueOrZero.toString(), + to = transactionData.destinationAddress, + tick = transactionData.amount.type.token.contractAddress, + ) + + val redeemScript = RedeemScript( + publicKey = publicKey.blockchainKey.toCompressedPublicKey(), + envelope = envelope, + ) + + val transaction = createKaspaTransaction( + networkParameters = networkParameters, + unspentOutputs = unspentsToSpend, + transformer = { kaspaTransaction -> + kaspaTransaction.addOutput( + Coin.parseCoin(targetOutputAmountValue.toPlainString()), + redeemScript.scriptHash(), + ) + if (!resultChange.isZero()) { + val addressService = KaspaAddressService() + val sourceScript = ScriptBuilder() + .data(addressService.getPublicKey(transactionData.sourceAddress)) + .op(OP_CODESEPARATOR) + .build() + kaspaTransaction.addOutput( + Coin.parseCoin(resultChange.toPlainString()), + sourceScript, ) - }, + } + kaspaTransaction + }, + ) + val commitTransaction = CommitTransaction( + transaction = transaction, + hashes = getHashesForSign(transaction), + redeemScript = redeemScript, + sourceAddress = transactionData.sourceAddress, + params = BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction( + transactionId = transaction.transactionHash().toHexString(), + amountValue = transactionData.amount.value ?: BigDecimal.ZERO, + feeAmountValue = targetOutputAmountValue, + envelope = envelope, ), ) + + return Result.Success(commitTransaction) } + internal fun buildToSignKRC20Reveal( + sourceAddress: String, + redeemScript: RedeemScript, + params: BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction, + revealFeeAmountValue: BigDecimal, + ): Result { + val utxo = listOf( + KaspaUnspentOutput( + amount = params.feeAmountValue, + outputIndex = 0, + transactionHash = params.transactionId.hexToBytes(), + outputScript = redeemScript.scriptHash().program, + ), + ) + + val change = calculateChange( + amount = BigDecimal.ZERO, + fee = revealFeeAmountValue, + unspentOutputs = utxo, + ) + + val transaction = createKaspaTransaction( + networkParameters = networkParameters, + unspentOutputs = utxo, + transformer = { kaspaTransaction -> + val sourceScript = ScriptBuilder() + .data(addressService.getPublicKey(sourceAddress)) + .op(OP_CODESEPARATOR) + .build() + kaspaTransaction.addOutput( + Coin.parseCoin(change.toPlainString()), + sourceScript, + ) + kaspaTransaction + }, + ) + + val revealTransaction = RevealTransaction( + transaction = transaction, + hashes = getHashesForSign(transaction), + redeemScript = redeemScript, + ) + + return Result.Success(revealTransaction) + } + + private fun buildForSendInternal(transaction: KaspaTransaction) = KaspaTransactionBody( + KaspaTransactionData( + inputs = transaction.inputs.map { + it.scriptSig.program + KaspaInput( + previousOutpoint = KaspaPreviousOutpoint( + transactionId = it.outpoint.hash.toString(), + index = it.outpoint.index, + ), + signatureScript = it.scriptBytes.toHexString(), + ) + }, + outputs = transaction.outputs.map { + KaspaOutput( + amount = it.value.getValue(), + scriptPublicKey = KaspaScriptPublicKey(it.scriptBytes.toHexString()), + ) + }, + ), + ) + @Suppress("MagicNumber") private fun extractSignature(index: Int, signatures: ByteArray): ByteArray { val r = BigInteger(1, signatures.copyOfRange(index * 64, 32 + index * 64)) @@ -95,7 +293,7 @@ class KaspaTransactionBuilder { } fun calculateChange(amount: BigDecimal, fee: BigDecimal, unspentOutputs: List): BigDecimal { - val fullAmount = unspentOutputs.map { it.amount }.reduce { acc, number -> acc + number } + val fullAmount = unspentOutputs.sumOf { it.amount } return fullAmount - (amount + fee) } @@ -106,19 +304,55 @@ class KaspaTransactionBuilder { fun getUnspentsToSpend() = unspentOutputs!!.sortedByDescending { it.amount }.take(getUnspentsToSpendCount()) + fun getHashesForSign(transaction: KaspaTransaction): List { + val hashesForSign: MutableList = MutableList(transaction.inputs.size) { byteArrayOf() } + for (input in transaction.inputs) { + val index = input.index + hashesForSign[index] = transaction.hashForSignatureWitness( + index, + input.scriptBytes, + input.value, + SigHash.ALL, + false, + ) + } + return hashesForSign + } + + private fun RedeemScript.script(): Script { + val kasplexId = "kasplex".toByteArray() + val payload = envelopeAdapter.toJson(envelope).toByteArray() + + return ScriptBuilder() + .data(publicKey) + .op(OP_CODESEPARATOR) + .opFalse() + .op(OP_IF) + .data(kasplexId) + .opTrue() + .opFalse() + .opFalse() + .data(payload) + .op(OP_ENDIF) + .build() + } + + private fun RedeemScript.scriptHash(): Script = ScriptBuilder() + .op(OP_HASH256) + .data(Blake2b.Blake2b256().digest(script().program)) + .op(OP_EQUAL) + .build() + companion object { const val MAX_INPUT_COUNT = 84 // Kaspa rejects transactions with more inputs } } -@Suppress("MagicNumber") -internal fun TransactionData.toKaspaTransaction( +internal fun createKaspaTransaction( networkParameters: NetworkParameters?, unspentOutputs: List, - change: BigDecimal, + transformer: (KaspaTransaction) -> KaspaTransaction, ): KaspaTransaction { - requireUncompiled() - val transaction = KaspaTransaction( networkParameters, ) @@ -138,33 +372,5 @@ internal fun TransactionData.toKaspaTransaction( input.sequenceNumber = 0 } - val addressService = KaspaAddressService() - val sourceScript = ScriptBuilder().data(addressService.getPublicKey(this.sourceAddress)).op(OP_CODESEPARATOR) - .build() - - val destinationAddressDecoded = KaspaCashAddr.decodeCashAddress(this.destinationAddress) - val destinationScript = when (destinationAddressDecoded.addressType) { - KaspaAddressType.P2PK_SCHNORR -> - ScriptBuilder.createP2PKOutputScript(destinationAddressDecoded.hash) - KaspaAddressType.P2PK_ECDSA -> - ScriptBuilder().data(destinationAddressDecoded.hash).op(OP_CODESEPARATOR).build() - KaspaAddressType.P2SH -> { - // known P2SH addresses won't throw - if (destinationAddressDecoded.hash.size != 32) error("Invalid hash length in P2SH address") - ScriptBuilder().op(OP_HASH256).data(destinationAddressDecoded.hash).op(OP_EQUAL).build() - } - null -> error("Null script type") // should never happen - } - - transaction.addOutput( - Coin.parseCoin(this.amount.value!!.toPlainString()), - destinationScript, - ) - if (!change.isZero()) { - transaction.addOutput( - Coin.parseCoin(change.toPlainString()), - sourceScript, - ) - } - return transaction + return transformer(transaction) } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaWalletManager.kt index 9afdf4ad2..0a43e5ea1 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaWalletManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/KaspaWalletManager.kt @@ -1,23 +1,39 @@ package com.tangem.blockchain.blockchains.kaspa import android.util.Log +import com.tangem.blockchain.blockchains.kaspa.krc20.KaspaKRC20InfoResponse +import com.tangem.blockchain.blockchains.kaspa.krc20.KaspaKRC20NetworkProvider +import com.tangem.blockchain.blockchains.kaspa.krc20.model.RedeemScript import com.tangem.blockchain.blockchains.kaspa.network.KaspaFeeBucketResponse import com.tangem.blockchain.blockchains.kaspa.network.KaspaInfoResponse import com.tangem.blockchain.blockchains.kaspa.network.KaspaNetworkProvider import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.datastorage.BlockchainSavedData +import com.tangem.blockchain.common.datastorage.implementations.AdvancedDataStorage import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.transaction.TransactionSendResult -import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.common.trustlines.AssetRequirementsCondition +import com.tangem.blockchain.common.trustlines.AssetRequirementsManager +import com.tangem.blockchain.extensions.* +import com.tangem.blockchain.extensions.map import com.tangem.common.CompletionResult +import com.tangem.common.extensions.toCompressedPublicKey +import com.tangem.common.extensions.toHexString +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import java.math.BigDecimal import java.math.BigInteger -class KaspaWalletManager( +@Suppress("LargeClass") +internal class KaspaWalletManager( wallet: Wallet, private val transactionBuilder: KaspaTransactionBuilder, private val networkProvider: KaspaNetworkProvider, -) : WalletManager(wallet), UtxoAmountLimitProvider, UtxoBlockchainManager { + private val krc20NetworkProvider: KaspaKRC20NetworkProvider, + private val dataStorage: AdvancedDataStorage, +) : WalletManager(wallet), UtxoAmountLimitProvider, UtxoBlockchainManager, AssetRequirementsManager { override val currentHost: String get() = networkProvider.baseUrl @@ -30,9 +46,25 @@ class KaspaWalletManager( override val allowConsolidation: Boolean = true override suspend fun updateInternal() { - when (val response = networkProvider.getInfo(wallet.address)) { - is Result.Success -> updateWallet(response.data) - is Result.Failure -> updateError(response.error) + coroutineScope { + val coinBalanceDeferred = async { networkProvider.getInfo(wallet.address) } + + val tokensBalances = if (cardTokens.isNotEmpty()) { + async { krc20NetworkProvider.getBalances(wallet.address, cardTokens.toList()) }.await() + } else { + Result.Success(emptyMap()) + } + + val coinBalance = coinBalanceDeferred.await() + + if (tokensBalances is Result.Success) { + updateWalletTokens(tokensBalances.data) + } + + when (coinBalance) { + is Result.Success -> updateWallet(coinBalance.data) + is Result.Failure -> updateError(coinBalance.error) + } } } @@ -46,6 +78,22 @@ class KaspaWalletManager( transactionBuilder.unspentOutputs = response.unspentOutputs } + private fun updateWalletTokens(tokensInfo: Map>) { + tokensInfo.forEach { result -> + val token = result.key + val amountType = AmountType.Token(token) + when (val response = result.value) { + is Result.Success -> { + val balance = response.data.balance + wallet.setAmount(balance, amountType) + } + is Result.Failure -> { + wallet.changeAmountValue(amountType, null, null) + } + } + } + } + private fun updateError(error: BlockchainError) { Log.e(this::class.java.simpleName, error.customMessage) if (error is BlockchainSdkError) throw error @@ -55,35 +103,35 @@ class KaspaWalletManager( transactionData: TransactionData, signer: TransactionSigner, ): Result { - when (val buildTransactionResult = transactionBuilder.buildToSign(transactionData)) { - is Result.Failure -> return buildTransactionResult - is Result.Success -> { - return when (val signerResult = signer.sign(buildTransactionResult.data, wallet.publicKey)) { - is CompletionResult.Success -> { - val transactionToSend = transactionBuilder.buildToSend( - signerResult.data.reduce { acc, bytes -> acc + bytes }, - ) - when (val sendResult = networkProvider.sendTransaction(transactionToSend)) { - is Result.Failure -> sendResult - is Result.Success -> { - val hash = sendResult.data - transactionData.hash = hash - wallet.addOutgoingTransaction(transactionData) - Result.Success(TransactionSendResult(hash ?: "")) - } - } - } - is CompletionResult.Failure -> Result.fromTangemSdkError(signerResult.error) + transactionData.requireUncompiled() + + return when (val type = transactionData.amount.type) { + is AmountType.Coin -> sendCoinTransaction(transactionData, signer) + is AmountType.Token -> { + val incompleteTokenTransaction = getIncompleteTokenTransaction(type.token) + if (incompleteTokenTransaction != null && + incompleteTokenTransaction.amountValue == transactionData.amount.value && + incompleteTokenTransaction.envelope.to == transactionData.destinationAddress + ) { + sendKRC20RevealOnlyTransaction( + transactionData = transactionData, + signer = signer, + incompleteTokenTransactionParams = incompleteTokenTransaction, + ) + } else { + sendKRC20Transaction(transactionData, signer) } } + else -> error("unknown amount type for fee estimation") } } + @Suppress("CyclomaticComplexMethod", "NestedBlockDepth") override suspend fun getFee(amount: Amount, destination: String): Result { val unspentOutputCount = transactionBuilder.getUnspentsToSpendCount() return if (unspentOutputCount == 0) { - Result.Failure(Exception("No unspent outputs found").toBlockchainSdkError()) // shouldn't happen + Result.Failure(BlockchainSdkError.Kaspa.ZeroUtxoError) } else { val source = wallet.address @@ -94,13 +142,31 @@ class KaspaWalletManager( fee = null, ) - when (val buildTransactionResult = transactionBuilder.buildToSign(transactionData)) { + val buildTransactionResult = when (amount.type) { + is AmountType.Coin -> transactionBuilder.buildToSign(transactionData) + is AmountType.Token -> transactionBuilder.buildToSignKRC20Commit( + transactionData = transactionData, + dust = dustValue, + includeFee = false, + ).let { + when (it) { + is Result.Failure -> it + is Result.Success -> Result.Success(it.data.transaction) + } + } + else -> error("unknown amount type for fee estimation") + } + + when (buildTransactionResult) { is Result.Failure -> return buildTransactionResult is Result.Success -> { - return when (val signerResult = dummySigner.sign(buildTransactionResult.data, wallet.publicKey)) { + val transaction = buildTransactionResult.data + val hashesToSign = transactionBuilder.getHashesForSign(transaction) + return when (val signerResult = dummySigner.sign(hashesToSign, wallet.publicKey)) { is CompletionResult.Success -> { val transactionToSend = transactionBuilder.buildToSend( - signerResult.data.reduce { acc, bytes -> acc + bytes }, + signatures = signerResult.data.reduce { acc, bytes -> acc + bytes }, + transaction = transaction, ) when (val sendResult = networkProvider.calculateFee(transactionToSend.transaction)) { is Result.Failure -> sendResult @@ -116,9 +182,9 @@ class KaspaWalletManager( Result.Success( TransactionFee.Choosable( - priority = allBuckets[0].toFee(mass), - normal = allBuckets[1].toFee(mass), - minimum = allBuckets[2].toFee(mass), + priority = allBuckets[0].toFee(mass, amount.type), + normal = allBuckets[1].toFee(mass, amount.type), + minimum = allBuckets[2].toFee(mass, amount.type), ), ) } @@ -144,9 +210,266 @@ class KaspaWalletManager( } } - private fun KaspaFeeBucketResponse.toFee(mass: BigInteger): Fee.Kaspa { + override suspend fun requirementsCondition(currencyType: CryptoCurrencyType): AssetRequirementsCondition? { + return when (currencyType) { + is CryptoCurrencyType.Coin -> null + is CryptoCurrencyType.Token -> { + getIncompleteTokenTransaction(currencyType.info)?.let { + AssetRequirementsCondition.IncompleteTransaction( + blockchain = blockchain, + amount = Amount( + value = it.amountValue, + token = currencyType.info, + ), + feeAmount = Amount( + value = it.feeAmountValue, + blockchain = blockchain, + ), + ) + } + } + } + } + + override suspend fun fulfillRequirements( + currencyType: CryptoCurrencyType, + signer: TransactionSigner, + ): SimpleResult { + return when (currencyType) { + is CryptoCurrencyType.Coin -> SimpleResult.Success + is CryptoCurrencyType.Token -> { + val incompleteTokenTransaction = getIncompleteTokenTransaction(currencyType.info) + ?: return SimpleResult.Success + + val result = sendKRC20RevealOnlyTransaction( + transactionData = incompleteTokenTransaction.toTransactionData( + type = AmountType.Token(currencyType.info), + ), + signer = signer, + incompleteTokenTransactionParams = incompleteTokenTransaction, + ) + + when (result) { + is Result.Success -> SimpleResult.Success + is Result.Failure -> SimpleResult.Failure(result.error) + } + } + } + } + + override suspend fun discardRequirements(currencyType: CryptoCurrencyType): SimpleResult { + when (currencyType) { + is CryptoCurrencyType.Coin -> Unit + is CryptoCurrencyType.Token -> { + removeIncompleteTokenTransaction(currencyType.info) + } + } + return SimpleResult.Success + } + + private suspend fun sendCoinTransaction( + transactionData: TransactionData, + signer: TransactionSigner, + ): Result { + when (val buildTransactionResult = transactionBuilder.buildToSign(transactionData)) { + is Result.Failure -> return buildTransactionResult + is Result.Success -> { + val transaction = buildTransactionResult.data + val hashesToSign = transactionBuilder.getHashesForSign(transaction) + return when (val signerResult = signer.sign(hashesToSign, wallet.publicKey)) { + is CompletionResult.Success -> { + val transactionToSend = transactionBuilder.buildToSend( + signatures = signerResult.data.reduce { acc, bytes -> acc + bytes }, + transaction = transaction, + ) + when (val sendResult = networkProvider.sendTransaction(transactionToSend)) { + is Result.Failure -> sendResult + is Result.Success -> { + val hash = sendResult.data + transactionData.hash = hash + wallet.addOutgoingTransaction(transactionData) + Result.Success(TransactionSendResult(hash ?: "")) + } + } + } + is CompletionResult.Failure -> Result.fromTangemSdkError(signerResult.error) + } + } + } + } + + @Suppress("NestedBlockDepth", "LongMethod") + private suspend fun sendKRC20Transaction( + transactionData: TransactionData, + signer: TransactionSigner, + ): Result { + transactionData.requireUncompiled() + + val token = (transactionData.amount.type as AmountType.Token).token + return when ( + val commitTransaction = transactionBuilder.buildToSignKRC20Commit( + transactionData = transactionData, + dust = dustValue, + ) + ) { + is Result.Success -> { + val revealTransaction = transactionBuilder.buildToSignKRC20Reveal( + sourceAddress = transactionData.sourceAddress, + redeemScript = commitTransaction.data.redeemScript, + revealFeeAmountValue = (transactionData.fee as Fee.Kaspa).revealTransactionFee?.value!!, + params = commitTransaction.data.params, + ) + + when (revealTransaction) { + is Result.Success -> { + val unionHashes = commitTransaction.data.hashes + revealTransaction.data.hashes + + return when (val signerResult = signer.sign(unionHashes, wallet.publicKey)) { + is CompletionResult.Success -> { + val commitSignaturesLength = commitTransaction.data.hashes.size + val commitSignatures = signerResult.data.take(commitSignaturesLength) + val revealSignatures = signerResult.data.drop(commitSignaturesLength) + val commitTransactionToSend = transactionBuilder.buildToSend( + signatures = commitSignatures.reduce { acc, bytes -> acc + bytes }, + transaction = commitTransaction.data.transaction, + ) + val revealTransactionToSend = transactionBuilder.buildToSendKRC20Reveal( + signatures = revealSignatures.reduce { acc, bytes -> acc + bytes }, + redeemScript = commitTransaction.data.redeemScript, + transaction = revealTransaction.data.transaction, + ) + when ( + val sendCommitResult = networkProvider.sendTransaction(commitTransactionToSend) + ) { + is Result.Failure -> sendCommitResult + is Result.Success -> { + storeIncompleteTokenTransaction( + token = token, + data = commitTransaction.data.params, + ) + delay(REVEAL_TRANSACTION_DELAY) + + when ( + val sendRevealResult = networkProvider.sendTransaction( + revealTransactionToSend, + ) + ) { + is Result.Failure -> { + updateUnspentOutputs() + sendRevealResult + } + is Result.Success -> { + updateUnspentOutputs() + val hash = sendRevealResult.data + transactionData.hash = hash + wallet.addOutgoingTransaction(transactionData) + removeIncompleteTokenTransaction(token) + Result.Success(TransactionSendResult(hash ?: "")) + } + } + } + } + } + is CompletionResult.Failure -> Result.fromTangemSdkError(signerResult.error) + } + } + is Result.Failure -> revealTransaction + } + } + is Result.Failure -> commitTransaction + } + } + + private suspend fun sendKRC20RevealOnlyTransaction( + transactionData: TransactionData, + signer: TransactionSigner, + incompleteTokenTransactionParams: BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction, + ): Result { + transactionData.requireUncompiled() + + val token = (transactionData.amount.type as AmountType.Token).token + val revealFeeAmountValue = (transactionData.fee as Fee.Kaspa).revealTransactionFee?.value ?: BigDecimal.ZERO + val redeemScript = RedeemScript( + wallet.publicKey.blockchainKey.toCompressedPublicKey(), + incompleteTokenTransactionParams.envelope, + ) + val transaction = transactionBuilder.buildToSignKRC20Reveal( + sourceAddress = transactionData.sourceAddress, + redeemScript = redeemScript, + params = incompleteTokenTransactionParams, + revealFeeAmountValue = revealFeeAmountValue, + ) + return when (transaction) { + is Result.Success -> { + return when (val signerResult = signer.sign(transaction.data.hashes, wallet.publicKey)) { + is CompletionResult.Success -> { + val transactionToSend = transactionBuilder.buildToSendKRC20Reveal( + signatures = signerResult.data.reduce { acc, bytes -> acc + bytes }, + redeemScript = redeemScript, + transaction = transaction.data.transaction, + ) + when (val sendResult = networkProvider.sendTransaction(transactionToSend)) { + is Result.Failure -> { + updateUnspentOutputs() + sendResult + } + is Result.Success -> { + updateUnspentOutputs() + val hash = sendResult.data + transactionData.hash = hash + wallet.addOutgoingTransaction(transactionData) + removeIncompleteTokenTransaction(token) + Result.Success(TransactionSendResult(hash ?: "")) + } + } + } + is CompletionResult.Failure -> Result.fromTangemSdkError(signerResult.error) + } + } + is Result.Failure -> transaction + } + } + + private fun BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction.toTransactionData( + type: AmountType.Token, + ): TransactionData.Uncompiled { + val token = type.token + val tokenValue = BigDecimal(envelope.amt) + + val transactionAmount = tokenValue.movePointLeft(token.decimals) + val fee = feeAmountValue - dustValue + val feeAmount = Amount( + value = fee, + blockchain = blockchain, + ) + + return TransactionData.Uncompiled( + amount = Amount( + value = transactionAmount, + blockchain = blockchain, + type = type, + ), + fee = Fee.Kaspa( + amount = feeAmount, + mass = BigInteger.ZERO, // we need only amount + feeRate = BigInteger.ZERO, // we need only amount + revealTransactionFee = feeAmount, + ), + sourceAddress = wallet.address, + destinationAddress = envelope.to, + status = TransactionStatus.Unconfirmed, + contractAddress = token.contractAddress, + ) + } + + private fun KaspaFeeBucketResponse.toFee(mass: BigInteger, type: AmountType): Fee.Kaspa { val feeRate = feeRate.toBigInteger() - val value = (mass * feeRate).toBigDecimal().movePointLeft(blockchain.decimals()) + val resultMass = if (type is AmountType.Token) { + mass + REVEAL_TRANSACTION_MASS + } else { + mass + } + val value = (resultMass * feeRate).toBigDecimal().movePointLeft(blockchain.decimals()) return Fee.Kaspa( amount = Amount( value = value, @@ -154,6 +477,47 @@ class KaspaWalletManager( ), mass = mass, feeRate = feeRate, + revealTransactionFee = type.takeIf { it is AmountType.Token }.let { + Amount( + value = (REVEAL_TRANSACTION_MASS * feeRate).toBigDecimal().movePointLeft(blockchain.decimals()), + blockchain = blockchain, + ) + }, ) } + + // we should update unspent outputs as soon as possible before create a new token transaction + private suspend fun updateUnspentOutputs() { + coroutineScope { + networkProvider.getInfo(wallet.address).map { + transactionBuilder.unspentOutputs = it.unspentOutputs + } + } + } + + private suspend fun getIncompleteTokenTransaction( + token: Token, + ): BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction? { + return dataStorage.getOrNull(token.createKey()) + } + + private suspend fun storeIncompleteTokenTransaction( + token: Token, + data: BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction, + ) { + dataStorage.store(token.createKey(), data) + } + + private suspend fun removeIncompleteTokenTransaction(token: Token) { + dataStorage.remove(token.createKey()) + } + + private fun Token.createKey(): String { + return "$symbol-${wallet.publicKey.blockchainKey.toCompressedPublicKey().toHexString()}" + } + + companion object { + private val REVEAL_TRANSACTION_MASS: BigInteger = 4100.toBigInteger() + private const val REVEAL_TRANSACTION_DELAY: Long = 2_000 + } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20Api.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20Api.kt new file mode 100644 index 000000000..798dd2316 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20Api.kt @@ -0,0 +1,8 @@ +package com.tangem.blockchain.blockchains.kaspa.krc20 + +import retrofit2.http.* + +interface KaspaKRC20Api { + @GET("krc20/address/{address}/token/{token}") + suspend fun getBalance(@Path("address") address: String, @Path("token") token: String): KaspaKRC20BalanceResponse +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20BalanceResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20BalanceResponse.kt new file mode 100644 index 000000000..289c6606d --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20BalanceResponse.kt @@ -0,0 +1,16 @@ +package com.tangem.blockchain.blockchains.kaspa.krc20 + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class KaspaKRC20BalanceResponse( + @Json(name = "result") + val result: List = emptyList(), +) { + @JsonClass(generateAdapter = true) + data class TokenBalance( + @Json(name = "balance") + val balance: Long? = null, + ) +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20NetworkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20NetworkProvider.kt new file mode 100644 index 000000000..94e8f36ad --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20NetworkProvider.kt @@ -0,0 +1,14 @@ +package com.tangem.blockchain.blockchains.kaspa.krc20 + +import com.tangem.blockchain.common.NetworkProvider +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.extensions.Result +import java.math.BigDecimal + +interface KaspaKRC20NetworkProvider : NetworkProvider { + suspend fun getBalances(address: String, tokens: List): Result>> +} + +data class KaspaKRC20InfoResponse( + val balance: BigDecimal, +) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20NetworkService.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20NetworkService.kt new file mode 100644 index 000000000..1c2256b54 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20NetworkService.kt @@ -0,0 +1,19 @@ +package com.tangem.blockchain.blockchains.kaspa.krc20 + +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.network.MultiNetworkProvider + +class KaspaKRC20NetworkService(providers: List) : KaspaKRC20NetworkProvider { + + private val multiNetworkProvider = MultiNetworkProvider(providers) + override val baseUrl: String + get() = multiNetworkProvider.currentProvider.baseUrl + + override suspend fun getBalances( + address: String, + tokens: List, + ): Result>> { + return multiNetworkProvider.performRequest { getBalances(address, tokens) } + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20RestApiNetworkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20RestApiNetworkProvider.kt new file mode 100644 index 000000000..3c8e801a4 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/KaspaKRC20RestApiNetworkProvider.kt @@ -0,0 +1,46 @@ +package com.tangem.blockchain.blockchains.kaspa.krc20 + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.toBlockchainSdkError +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.retryIO +import com.tangem.blockchain.network.createRetrofitInstance +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope + +open class KaspaKRC20RestApiNetworkProvider(override val baseUrl: String) : KaspaKRC20NetworkProvider { + + private val api: KaspaKRC20Api by lazy { + createRetrofitInstance(baseUrl).create(KaspaKRC20Api::class.java) + } + private val decimals = Blockchain.Kaspa.decimals() + + override suspend fun getBalances( + address: String, + tokens: List, + ): Result>> { + return coroutineScope { + val tokenBalancesDeferred = tokens.associateWith { token -> + async { + retryIO { + try { + val response = api.getBalance(address, token.contractAddress).result.first() + Result.Success( + KaspaKRC20InfoResponse( + balance = response.balance!!.toBigDecimal().movePointLeft(decimals), + ), + ) + } catch (e: Exception) { + Result.Failure(e.toBlockchainSdkError()) + } + } + } + } + + Result.Success( + tokenBalancesDeferred.mapValues { it.value.await() }, + ) + } + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/CommitTransaction.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/CommitTransaction.kt new file mode 100644 index 000000000..b493fac60 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/CommitTransaction.kt @@ -0,0 +1,12 @@ +package com.tangem.blockchain.blockchains.kaspa.krc20.model + +import com.tangem.blockchain.blockchains.kaspa.KaspaTransaction +import com.tangem.blockchain.common.datastorage.BlockchainSavedData + +internal data class CommitTransaction( + val transaction: KaspaTransaction, + val hashes: List, + val redeemScript: RedeemScript, + val sourceAddress: String, + val params: BlockchainSavedData.KaspaKRC20IncompleteTokenTransaction, +) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/Envelope.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/Envelope.kt new file mode 100644 index 000000000..da6456594 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/Envelope.kt @@ -0,0 +1,13 @@ +package com.tangem.blockchain.blockchains.kaspa.krc20.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class Envelope( + @Json(name = "p") val p: String, + @Json(name = "op") val op: String, + @Json(name = "amt") val amt: String, + @Json(name = "to") val to: String, + @Json(name = "tick") val tick: String, +) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/IncompleteTokenTransactionParams.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/IncompleteTokenTransactionParams.kt new file mode 100644 index 000000000..bf078cc8e --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/IncompleteTokenTransactionParams.kt @@ -0,0 +1,10 @@ +package com.tangem.blockchain.blockchains.kaspa.krc20.model + +import java.math.BigDecimal + +internal data class IncompleteTokenTransactionParams( + val transactionId: String, + val amountValue: BigDecimal, + val feeAmountValue: BigDecimal, + val envelope: Envelope, +) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/KaspaKRC20ProvidersBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/KaspaKRC20ProvidersBuilder.kt new file mode 100644 index 000000000..bc82ac8a5 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/KaspaKRC20ProvidersBuilder.kt @@ -0,0 +1,20 @@ +package com.tangem.blockchain.blockchains.kaspa.krc20.model + +import com.tangem.blockchain.blockchains.kaspa.krc20.KaspaKRC20NetworkProvider +import com.tangem.blockchain.blockchains.kaspa.krc20.KaspaKRC20RestApiNetworkProvider +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.network.providers.NetworkProvidersBuilder +import com.tangem.blockchain.common.network.providers.ProviderType + +internal class KaspaKRC20ProvidersBuilder( + override val providerTypes: List, +) : NetworkProvidersBuilder() { + + override fun createProviders(blockchain: Blockchain): List { + return listOf(KaspaKRC20RestApiNetworkProvider("https://api.kasplex.org/v1/")) + } + + override fun createTestnetProviders(blockchain: Blockchain): List { + return listOf(KaspaKRC20RestApiNetworkProvider("https://tn10api.kasplex.org/v1")) + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/RedeemScript.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/RedeemScript.kt new file mode 100644 index 000000000..380850844 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/RedeemScript.kt @@ -0,0 +1,6 @@ +package com.tangem.blockchain.blockchains.kaspa.krc20.model + +internal data class RedeemScript( + val publicKey: ByteArray, + val envelope: Envelope, +) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/RevealTransaction.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/RevealTransaction.kt new file mode 100644 index 000000000..a6eca070d --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/krc20/model/RevealTransaction.kt @@ -0,0 +1,9 @@ +package com.tangem.blockchain.blockchains.kaspa.krc20.model + +import com.tangem.blockchain.blockchains.kaspa.KaspaTransaction + +internal data class RevealTransaction( + val transaction: KaspaTransaction, + val hashes: List, + val redeemScript: RedeemScript, +) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/network/KaspaApi.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/network/KaspaApi.kt index 7061830ca..15fb4a03a 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/network/KaspaApi.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/network/KaspaApi.kt @@ -1,5 +1,6 @@ package com.tangem.blockchain.blockchains.kaspa.network +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import retrofit2.http.* @@ -24,40 +25,56 @@ interface KaspaApi { @JsonClass(generateAdapter = true) data class KaspaTransactionBody( + @Json(name = "transaction") val transaction: KaspaTransactionData, ) @JsonClass(generateAdapter = true) data class KaspaTransactionData( + @Json(name = "version") val version: Int = 0, + @Json(name = "inputs") val inputs: List, + @Json(name = "outputs") val outputs: List, + @Json(name = "lockTime") val lockTime: Int = 0, + @Json(name = "subnetworkId") val subnetworkId: String = "0000000000000000000000000000000000000000", ) @JsonClass(generateAdapter = true) data class KaspaInput( + @Json(name = "previousOutpoint") val previousOutpoint: KaspaPreviousOutpoint, + @Json(name = "signatureScript") val signatureScript: String, + @Json(name = "sequence") val sequence: Long = 0, + @Json(name = "sigOpCount") val sigOpCount: Int = 1, ) @JsonClass(generateAdapter = true) data class KaspaPreviousOutpoint( + @Json(name = "transactionId") val transactionId: String, + @Json(name = "index") val index: Long, ) @JsonClass(generateAdapter = true) data class KaspaOutput( + @Json(name = "amount") val amount: Long, + @Json(name = "scriptPublicKey") val scriptPublicKey: KaspaScriptPublicKey, ) @JsonClass(generateAdapter = true) data class KaspaScriptPublicKey( + @Json(name = "scriptPublicKey") val scriptPublicKey: String, + @Json(name = "version") val version: Int = 0, ) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/network/KaspaResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/network/KaspaResponse.kt index 7a00c4159..ef6955fa7 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/network/KaspaResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/kaspa/network/KaspaResponse.kt @@ -5,36 +5,46 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class KaspaBalanceResponse( - var balance: Long? = null, + @Json(name = "balance") + val balance: Long? = null, ) @JsonClass(generateAdapter = true) data class KaspaUnspentOutputResponse( - var outpoint: KaspaOutpoint? = null, - var utxoEntry: KaspaUtxoEntry? = null, + @Json(name = "outpoint") + val outpoint: KaspaOutpoint? = null, + @Json(name = "utxoEntry") + val utxoEntry: KaspaUtxoEntry? = null, ) @JsonClass(generateAdapter = true) data class KaspaSendTransactionResponse( - var transactionId: String? = null, - var error: String? = null, + @Json(name = "transactionId") + val transactionId: String? = null, + @Json(name = "error") + val error: String? = null, ) @JsonClass(generateAdapter = true) data class KaspaOutpoint( - var transactionId: String? = null, - var index: Long? = null, + @Json(name = "transactionId") + val transactionId: String? = null, + @Json(name = "index") + val index: Long? = null, ) @JsonClass(generateAdapter = true) data class KaspaUtxoEntry( - var amount: String? = null, - var scriptPublicKey: KaspaScriptPublicKeyResponse? = null, + @Json(name = "amount") + val amount: String? = null, + @Json(name = "scriptPublicKey") + val scriptPublicKey: KaspaScriptPublicKeyResponse? = null, ) @JsonClass(generateAdapter = true) data class KaspaScriptPublicKeyResponse( - var scriptPublicKey: String? = null, + @Json(name = "scriptPublicKey") + val scriptPublicKey: String? = null, ) @JsonClass(generateAdapter = true) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/sui/network/rpc/SuiJsonRpcRequestBody.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/sui/network/rpc/SuiJsonRpcRequestBody.kt index 62621e4f9..7f55086cb 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/sui/network/rpc/SuiJsonRpcRequestBody.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/sui/network/rpc/SuiJsonRpcRequestBody.kt @@ -1,8 +1,10 @@ package com.tangem.blockchain.blockchains.sui.network.rpc import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import java.util.UUID +@JsonClass(generateAdapter = true) internal data class SuiJsonRpcRequestBody( @Json(name = "id") val id: String = UUID.randomUUID().toString(), @Json(name = "jsonrpc") val jsonRpc: String = "2.0", diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/sui/network/rpc/SuiJsonRpcResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/sui/network/rpc/SuiJsonRpcResponse.kt index 12e359abc..f64f3b0f8 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/sui/network/rpc/SuiJsonRpcResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/sui/network/rpc/SuiJsonRpcResponse.kt @@ -1,8 +1,10 @@ package com.tangem.blockchain.blockchains.sui.network.rpc import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import java.math.BigDecimal +@JsonClass(generateAdapter = true) internal data class SuiJsonRpcResponse( @Json(name = "jsonrpc") val jsonRpc: String, @Json(name = "id") val id: String, @@ -10,17 +12,20 @@ internal data class SuiJsonRpcResponse( @Json(name = "error") val error: SuiJsonRpcError?, ) +@JsonClass(generateAdapter = true) internal data class SuiJsonRpcError( @Json(name = "code") val code: Int, @Json(name = "message") val message: String, ) +@JsonClass(generateAdapter = true) internal data class SuiCoinsResponse( @Json(name = "data") val data: List, @Json(name = "hasNextPage") val hasNextPage: Boolean, @Json(name = "nextCursor") val nextCursor: String?, ) { + @JsonClass(generateAdapter = true) data class Data( @Json(name = "balance") val balance: BigDecimal, @Json(name = "coinObjectId") val coinObjectId: String, @@ -31,6 +36,7 @@ internal data class SuiCoinsResponse( ) } +@JsonClass(generateAdapter = true) data class SuiDryRunTransactionResponse( @Json(name = "effects") val effects: Effects, @@ -38,6 +44,7 @@ data class SuiDryRunTransactionResponse( val input: Input, ) { + @JsonClass(generateAdapter = true) data class Effects( @Json(name = "gasUsed") val gasUsed: GasUsed, @@ -45,6 +52,7 @@ data class SuiDryRunTransactionResponse( val status: Status, ) { + @JsonClass(generateAdapter = true) data class GasUsed( @Json(name = "computationCost") val computationCost: BigDecimal, @@ -56,6 +64,7 @@ data class SuiDryRunTransactionResponse( val storageRebate: BigDecimal, ) + @JsonClass(generateAdapter = true) data class Status( @Json(name = "status") val value: String, @@ -66,11 +75,13 @@ data class SuiDryRunTransactionResponse( } } + @JsonClass(generateAdapter = true) data class Input( @Json(name = "gasData") val gasData: GasData, ) { + @JsonClass(generateAdapter = true) data class GasData( @Json(name = "budget") val budget: BigDecimal, @@ -82,6 +93,7 @@ data class SuiDryRunTransactionResponse( val price: BigDecimal, ) { + @JsonClass(generateAdapter = true) data class Payment( @Json(name = "digest") val digest: String, @@ -94,6 +106,7 @@ data class SuiDryRunTransactionResponse( } } +@JsonClass(generateAdapter = true) internal data class SuiExecuteTransactionBlockResponse( @Json(name = "digest") val digest: String, @@ -101,11 +114,13 @@ internal data class SuiExecuteTransactionBlockResponse( val effects: Effects, ) { + @JsonClass(generateAdapter = true) data class Effects( @Json(name = "status") val status: Status, ) { + @JsonClass(generateAdapter = true) data class Status( @Json(name = "status") val value: String, @@ -117,6 +132,7 @@ internal data class SuiExecuteTransactionBlockResponse( } } +@JsonClass(generateAdapter = true) internal data class SuiGetTransactionBlockResponse( @Json(name = "digest") val digest: String, diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosApi.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosApi.kt index dbd4168f1..4c1aeb594 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosApi.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosApi.kt @@ -29,7 +29,9 @@ interface TezosApi { @JsonClass(generateAdapter = true) data class TezosForgeBody( + @Json(name = "branch") val branch: String, + @Json(name = "contents") val contents: List, ) @@ -48,8 +50,12 @@ data class TezosOperationContent( @JsonClass(generateAdapter = true) data class TezosPreapplyBody( + @Json(name = "protocol") val protocol: String, + @Json(name = "branch") val branch: String, + @Json(name = "contents") val contents: List, + @Json(name = "signature") val signature: String, ) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosResponse.kt index a7ce1639e..84021de66 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosResponse.kt @@ -6,17 +6,17 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class TezosAddressResponse( @Json(name = "balance") - var balance: Long? = null, + val balance: Long? = null, @Json(name = "counter") - var counter: Long? = null, + val counter: Long? = null, ) @JsonClass(generateAdapter = true) data class TezosHeaderResponse( @Json(name = "protocol") - var protocol: String? = null, + val protocol: String? = null, @Json(name = "hash") - var hash: String? = null, + val hash: String? = null, ) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ton/network/TonNetworkModels.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ton/network/TonNetworkModels.kt index 570ed7432..b8d9b9cda 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ton/network/TonNetworkModels.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ton/network/TonNetworkModels.kt @@ -24,6 +24,7 @@ data class TonGetWalletInfoResponse( @Json(name = "seqno") val seqno: Int?, ) +@JsonClass(generateAdapter = false) enum class TonAccountState { @Json(name = "active") ACTIVE, diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/tron/network/TronNetworkModels.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tron/network/TronNetworkModels.kt index 304a4894a..e02e11e55 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/tron/network/TronNetworkModels.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tron/network/TronNetworkModels.kt @@ -19,39 +19,47 @@ data class TronChainParameters( @JsonClass(generateAdapter = true) data class TronGetAccountRequest( + @Json(name = "address") val address: String, + @Json(name = "visible") val visible: Boolean, ) @JsonClass(generateAdapter = true) data class TronEnergyFeeData( + @Json(name = "energyFee") val energyFee: Long, + @Json(name = "sunPerEnergyUnit") val sunPerEnergyUnit: Long, ) @JsonClass(generateAdapter = true) data class TronGetAccountResponse( + @Json(name = "balance") val balance: Long?, // We use [address] field to distinguish this response from // an empty JSON that we get if account hasn't been activated + @Json(name = "address") val address: String?, ) @JsonClass(generateAdapter = true) data class TronGetAccountResourceResponse( - val freeNetUsed: Long?, - val freeNetLimit: Long, + @Json(name = "freeNetUsed") val freeNetUsed: Long?, + @Json(name = "freeNetLimit") val freeNetLimit: Long, @Json(name = "EnergyLimit") val energyLimit: Long?, @Json(name = "EnergyUsed") val energyUsed: Long?, ) @JsonClass(generateAdapter = true) data class TronTransactionInfoRequest( + @Json(name = "value") val value: String, ) @JsonClass(generateAdapter = true) data class TronTransactionInfoResponse( + @Json(name = "id") val id: String, ) @@ -63,7 +71,9 @@ data class TronChainParametersResponse( @JsonClass(generateAdapter = true) data class TronChainParameter( + @Json(name = "key") val key: String, + @Json(name = "value") val value: Long? = null, ) @@ -81,23 +91,31 @@ data class BlockHeader( @JsonClass(generateAdapter = true) data class RawData( + @Json(name = "number") val number: Long, + @Json(name = "txTrieRoot") val txTrieRoot: String, @Json(name = "witness_address") val witnessAddress: String, + @Json(name = "parentHash") val parentHash: String, + @Json(name = "version") val version: Int, + @Json(name = "timestamp") val timestamp: Long, ) @JsonClass(generateAdapter = true) data class TronBroadcastRequest( + @Json(name = "transaction") val transaction: String, ) @JsonClass(generateAdapter = true) data class TronBroadcastResponse( + @Json(name = "result") val result: Boolean, + @Json(name = "txid") val txid: String, @Json(name = "message") val errorMessage: String?, diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/vechain/network/VechainNetworkModels.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/vechain/network/VechainNetworkModels.kt index 233efbc61..2cc9752ea 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/vechain/network/VechainNetworkModels.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/vechain/network/VechainNetworkModels.kt @@ -24,6 +24,7 @@ internal data class VeChainCommitTransactionResponse(@Json(name = "id") val txId @JsonClass(generateAdapter = true) internal data class VeChainTransactionInfoResponse(@Json(name = "id") val txId: String) +@JsonClass(generateAdapter = true) internal data class VeChainContractCallRequest( @Json(name = "clauses") val clauses: List, @Json(name = "caller") val caller: String?, @@ -37,6 +38,7 @@ internal data class VeChainClause( @Json(name = "data") val data: String, ) +@JsonClass(generateAdapter = true) internal data class VeChainContractCallResponse( @Json(name = "data") val data: String?, @Json(name = "gasUsed") val gasUsed: Long?, diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/xrp/XrpTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/xrp/XrpTransactionBuilder.kt index b42ff064b..ca2125535 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/xrp/XrpTransactionBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/xrp/XrpTransactionBuilder.kt @@ -18,7 +18,7 @@ import com.ripple.core.coretypes.Amount as XrpAmount class XrpTransactionBuilder(private val networkProvider: XrpNetworkProvider, publicKey: ByteArray) { var sequence: Long? = null // https://xrpl.org/blog/2021/reserves-lowered.html - var minReserve = 10.toBigDecimal() + var minReserve = 1.toBigDecimal() val blockchain = Blockchain.XRP private val canonicalPublicKey = XrpAddressService.canonizePublicKey(publicKey) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/xrp/XrpWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/xrp/XrpWalletManager.kt index 9f13dcc10..97ebd728f 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/xrp/XrpWalletManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/xrp/XrpWalletManager.kt @@ -34,14 +34,14 @@ class XrpWalletManager( private fun updateWallet(response: XrpInfoResponse) { Log.d(this::class.java.simpleName, "Balance is ${response.balance}") + wallet.setReserveValue(response.reserveTotal) if (!response.accountFound) { - updateError(BlockchainSdkError.AccountNotFound()) + updateError(BlockchainSdkError.AccountNotFound(response.reserveTotal)) return } wallet.setCoinValue(response.balance - response.reserveTotal) - wallet.setReserveValue(response.reserveTotal) transactionBuilder.sequence = response.sequence - transactionBuilder.minReserve = response.reserveTotal + transactionBuilder.minReserve = response.reserveBase if (response.hasUnconfirmed) { if (wallet.recentTransactions.isEmpty()) wallet.addTransactionDummy() diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/xrp/network/rippled/RippledResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/xrp/network/rippled/RippledResponse.kt index c060d5132..361407f2c 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/xrp/network/rippled/RippledResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/xrp/network/rippled/RippledResponse.kt @@ -7,102 +7,102 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class RippledAccountResponse( @Json(name = "result") - var result: RippledAccountResult? = null, + val result: RippledAccountResult? = null, ) @JsonClass(generateAdapter = true) data class RippledAccountResult( @Json(name = "account_data") - var accountData: RippledAccountData? = null, + val accountData: RippledAccountData? = null, @Json(name = "error_code") - var errorCode: Int? = null, + val errorCode: Int? = null, ) @JsonClass(generateAdapter = true) data class RippledAccountData( @Json(name = "Balance") - var balance: String? = null, + val balance: String? = null, @Json(name = "Sequence") - var sequence: Long? = null, + val sequence: Long? = null, @Json(name = "OwnerCount") - var ownerCount: Long? = null, + val ownerCount: Long? = null, ) // Rippled state @JsonClass(generateAdapter = true) data class RippledStateResponse( @Json(name = "result") - var result: RippledStateResult? = null, + val result: RippledStateResult? = null, ) @JsonClass(generateAdapter = true) data class RippledStateResult( @Json(name = "state") - var state: RippledState? = null, + val state: RippledState? = null, ) @JsonClass(generateAdapter = true) data class RippledState( @Json(name = "validated_ledger") - var validatedLedger: RippledLedger? = null, + val validatedLedger: RippledLedger? = null, ) @JsonClass(generateAdapter = true) data class RippledLedger( @Json(name = "reserve_base") - var reserveBase: Long? = null, + val reserveBase: Long? = null, @Json(name = "reserve_inc") - var reserveInc: Long? = null, + val reserveInc: Long? = null, ) // Rippled fee @JsonClass(generateAdapter = true) data class RippledFeeResponse( @Json(name = "result") - var result: RippledFeeResult? = null, + val result: RippledFeeResult? = null, ) @JsonClass(generateAdapter = true) data class RippledFeeResult( @Json(name = "drops") - var feeData: RippledFeeData? = null, + val feeData: RippledFeeData? = null, ) @JsonClass(generateAdapter = true) data class RippledFeeData( // enough to put tx to queue @Json(name = "minimum_fee") - var minimalFee: String? = null, + val minimalFee: String? = null, // enough to put tx to current ledger @Json(name = "open_ledger_fee") - var normalFee: String? = null, + val normalFee: String? = null, @Json(name = "median_fee") - var priorityFee: String? = null, + val priorityFee: String? = null, ) // Rippled submit @JsonClass(generateAdapter = true) data class RippledSubmitResponse( @Json(name = "result") - var result: RippledSubmitResult? = null, + val result: RippledSubmitResult? = null, ) @JsonClass(generateAdapter = true) data class RippledSubmitResult( @Json(name = "engine_result_code") - var resultCode: Int? = null, + val resultCode: Int? = null, @Json(name = "engine_result_message") - var resultMessage: String? = null, + val resultMessage: String? = null, @Json(name = "error") - var error: String? = null, + val error: String? = null, @Json(name = "error_exception") - var errorException: String? = null, + val errorException: String? = null, ) diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt index dfccca7a3..d26ef4add 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt @@ -171,6 +171,9 @@ enum class Blockchain( CoreTestnet("core/test", "tCORE", "Core Testnet"), Xodex("xodex", "XODEX", "Xodex"), Canxium("canxium", "CAU", "Canxium"), + Chiliz("chiliz", "CHZ", "Chiliz"), + ChilizTestnet("chiliz/test", "CHZ", "Chiliz Spicy Testnet"), + Clore("clore-ai", "CLORE", "Clore"), ; private val externalLinkProvider: ExternalLinkProvider by lazy { ExternalLinkProviderFactory.makeProvider(this) } @@ -215,6 +218,7 @@ enum class Blockchain( Radiant, Koinos, KoinosTestnet, InternetComputer, + Clore, -> 8 Solana, SolanaTestnet, @@ -268,6 +272,7 @@ enum class Blockchain( EnergyWebChain, EnergyWebChainTestnet, EnergyWebX, EnergyWebXTestnet, Core, CoreTestnet, + Chiliz, ChilizTestnet, Xodex, Canxium, -> 18 @@ -305,6 +310,7 @@ enum class Blockchain( Ducatus, Dash, Ravencoin, RavencoinTestnet, + Clore, -> BitcoinAddressService(this) BitcoinCash, BitcoinCashTestnet -> BitcoinCashAddressService(this) @@ -342,6 +348,7 @@ enum class Blockchain( Cyber, CyberTestnet, EnergyWebChain, EnergyWebChainTestnet, Core, CoreTestnet, + Chiliz, ChilizTestnet, Xodex, Canxium, -> EthereumAddressService() @@ -473,6 +480,7 @@ enum class Blockchain( EnergyWebX, EnergyWebXTestnet -> EnergyWebXTestnet Casper, CasperTestnet -> CasperTestnet Core, CoreTestnet -> CoreTestnet + Chiliz, ChilizTestnet -> ChilizTestnet else -> null } } @@ -546,8 +554,10 @@ enum class Blockchain( EnergyWebChain, EnergyWebChainTestnet, Core, CoreTestnet, Casper, CasperTestnet, + Chiliz, ChilizTestnet, Xodex, Canxium, + Clore, -> listOf(EllipticCurve.Secp256k1) Stellar, StellarTestnet, @@ -645,6 +655,8 @@ enum class Blockchain( CoreTestnet -> Chain.CoreTestnet.id Xodex -> Chain.Xodex.id Canxium -> Chain.Canxium.id + Chiliz -> Chain.Chiliz.id + ChilizTestnet -> Chain.ChilizTestnet.id else -> null } } @@ -676,6 +688,7 @@ enum class Blockchain( Hedera, HederaTestnet, TON, TONTestnet, Cardano, + Kaspa, -> true else -> false @@ -739,6 +752,17 @@ enum class Blockchain( else -> false } + /** + * Returns is fee in given network is zero + * For now actual only for one network + */ + fun isNetworkFeeZero(): Boolean { + return when (this) { + Xodex -> true + else -> false + } + } + companion object { private val values = values() diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/CryptoCurrencyType.kt b/blockchain/src/main/java/com/tangem/blockchain/common/CryptoCurrencyType.kt index 4bb6442cb..800a03447 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/CryptoCurrencyType.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/CryptoCurrencyType.kt @@ -3,6 +3,6 @@ package com.tangem.blockchain.common import com.tangem.blockchain.common.Token as BlockchainSdkToken sealed class CryptoCurrencyType { - object Coin : CryptoCurrencyType() + data object Coin : CryptoCurrencyType() data class Token(val info: BlockchainSdkToken) : CryptoCurrencyType() } diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/Exceptions.kt b/blockchain/src/main/java/com/tangem/blockchain/common/Exceptions.kt index b29b79a9e..96076981d 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/Exceptions.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/Exceptions.kt @@ -99,6 +99,7 @@ sealed class BlockchainSdkError( "Due to Kaspa limitations only $maxOutputs UTXOs can fit in a single transaction. This means you can only" + " send ${maxAmount.toPlainString()}. You need to reduce the amount", ) + data object ZeroUtxoError : Kaspa(3) } sealed class Ton( diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/Token.kt b/blockchain/src/main/java/com/tangem/blockchain/common/Token.kt index 62a593365..7e61c15d8 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/Token.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/Token.kt @@ -1,14 +1,20 @@ package com.tangem.blockchain.common +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import java.util.Locale @JsonClass(generateAdapter = true) data class Token( + @Json(name = "name") val name: String, + @Json(name = "symbol") val symbol: String, + @Json(name = "contractAddress") val contractAddress: String, + @Json(name = "decimals") val decimals: Int, + @Json(name = "id") val id: String? = null, ) { constructor( diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt index 97efeecef..08a74ab3e 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt @@ -118,6 +118,7 @@ class WalletManagerFactory( Blockchain.BitcoinCash, Blockchain.BitcoinCashTestnet -> BitcoinCashWalletManagerAssembly Blockchain.Ravencoin, Blockchain.RavencoinTestnet -> RavencoinWalletManagerAssembly Blockchain.Ducatus -> DucatusWalletManagerAssembly + Blockchain.Clore -> CloreWalletManagerAssembly // endregion // region ETH-like blockchains @@ -149,6 +150,7 @@ class WalletManagerFactory( Blockchain.Taraxa, Blockchain.TaraxaTestnet, Blockchain.EnergyWebChain, Blockchain.EnergyWebChainTestnet, Blockchain.Core, Blockchain.CoreTestnet, + Blockchain.Chiliz, Blockchain.ChilizTestnet, Blockchain.Xodex, Blockchain.Canxium, -> EthereumLikeWalletManagerAssembly @@ -183,7 +185,7 @@ class WalletManagerFactory( Blockchain.Binance, Blockchain.BinanceTestnet -> BinanceWalletManagerAssembly Blockchain.Tezos -> TezosWalletManagerAssembly Blockchain.Tron, Blockchain.TronTestnet -> TronWalletManagerAssembly - Blockchain.Kaspa -> KaspaWalletManagerAssembly + Blockchain.Kaspa -> KaspaWalletManagerAssembly(dataStorage) Blockchain.TON, Blockchain.TONTestnet -> TonWalletManagerAssembly Blockchain.Cosmos, Blockchain.CosmosTestnet -> CosmosWalletManagerAssembly Blockchain.TerraV1 -> TerraV1WalletManagerAssembly diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/address/EstimationFeeAddressFactory.kt b/blockchain/src/main/java/com/tangem/blockchain/common/address/EstimationFeeAddressFactory.kt index e3cb9c272..9bca8d875 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/address/EstimationFeeAddressFactory.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/address/EstimationFeeAddressFactory.kt @@ -18,32 +18,23 @@ class EstimationFeeAddressFactory { @Suppress("LongMethod", "CyclomaticComplexMethod") fun makeAddress(blockchain: Blockchain): String { return when (blockchain) { - Blockchain.Cardano -> CARDANO_ESTIMATION_ADDRESS - - Blockchain.Chia, Blockchain.ChiaTestnet -> { - // Can not generate and doesn't depend on destination - "" - } - + Blockchain.Unknown, // shouldn't get there + Blockchain.Ducatus, // doesn't depend on destination + Blockchain.Tezos, // doesn't depend on destination + Blockchain.Hedera, Blockchain.HederaTestnet, // doesn't depend on destination + Blockchain.Chia, Blockchain.ChiaTestnet, // doesn't depend on destination + Blockchain.InternetComputer, // fixed + Blockchain.Casper, Blockchain.CasperTestnet, // fixed Blockchain.XRP, Blockchain.Stellar, Blockchain.StellarTestnet, Blockchain.Binance, Blockchain.BinanceTestnet, Blockchain.SolanaTestnet, - Blockchain.Hedera, Blockchain.HederaTestnet, - -> { - // Doesn't depend on amount and destination - "" - } + -> "" - Blockchain.Tezos -> { - // Tezos has a fixed fee. - "" - } + Blockchain.Nexa, Blockchain.NexaTestnet, + -> TODO("Not implemented") - Blockchain.Ducatus, Blockchain.Unknown -> { - // Unsupported - "" - } + Blockchain.Cardano -> CARDANO_ESTIMATION_ADDRESS // We have to generate a new dummy address for UTXO-like Blockchain.Bitcoin, @@ -59,7 +50,6 @@ class EstimationFeeAddressFactory { Blockchain.RavencoinTestnet, -> "RT5qKgXdmh9pqtz71cgfL834VfeXFVH1sG" Blockchain.Solana -> "9wuDg6Y4H4j86Kg5aUGrUeaBa3sAUzjMs37KbeGFnRuM" - Blockchain.Nexa, Blockchain.NexaTestnet -> TODO("Not implemented") Blockchain.Radiant -> "1K8jBuCKzuwvFCjL7Qpqq69k1hnVXJ31Nc" // EVM-like Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet -> @@ -99,6 +89,7 @@ class EstimationFeeAddressFactory { Blockchain.Cyber, Blockchain.CyberTestnet, Blockchain.EnergyWebChain, Blockchain.EnergyWebChainTestnet, Blockchain.Core, Blockchain.CoreTestnet, + Blockchain.Chiliz, Blockchain.ChilizTestnet, Blockchain.Xodex, Blockchain.Canxium, -> "0x52bb4012854f808CF9BAbd855e44E506dAf6C077" @@ -132,20 +123,17 @@ class EstimationFeeAddressFactory { "0x4626b7ef23fb2800a0e224e8249f47e0db3579070262da2a7efb0bc52c882867" Blockchain.Algorand, Blockchain.AlgorandTestnet -> "CW6XDCKQAZUGAIOTGE2NEPYFFVW6H6IKFOTOF3W5WDUVHH4ZIDCIKYDPXY" - Blockchain.Koinos, Blockchain.KoinosTestnet -> "1C423Vbd44zjghhJR5fKJdLFS3rgVFUc9A" + Blockchain.Koinos, Blockchain.KoinosTestnet, + -> "1C423Vbd44zjghhJR5fKJdLFS3rgVFUc9A" Blockchain.Filecoin -> "f1wxdu6d25dc4hmebdfgriswooum22plhmmpxibzq" Blockchain.Kaspa -> "kaspa:qyp2f0ust8wyvuvqrzajvehx5jyh43vcjgessjdkw9vyw6rww4fdlsgzysspfuq" - Blockchain.Sei, Blockchain.SeiTestnet -> "sei1lhjvds604fvac32j4eygpr820lyc82dlfv0ea4" - Blockchain.InternetComputer -> { - // Doesn't depend on amount and destination - "" - } - Blockchain.Sui, - Blockchain.SuiTestnet, + Blockchain.Sei, Blockchain.SeiTestnet, + -> "sei1lhjvds604fvac32j4eygpr820lyc82dlfv0ea4" + Blockchain.Sui, Blockchain.SuiTestnet, -> "0xbca45e36a271e106546c89984108685215724e488570a0049a187c473cd521bc" Blockchain.EnergyWebX, Blockchain.EnergyWebXTestnet, -> "5CogUCbb5PYYbEHhDVGDN6JRRYBkd4sFRVc4wwP8oy5Su34Z" - Blockchain.Casper, Blockchain.CasperTestnet -> "" + Blockchain.Clore -> "AJfAu7RJxiTowM9qVaTbVuS5JCPCpV3p7M" } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/CloreWalletManagerAssembly.kt b/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/CloreWalletManagerAssembly.kt new file mode 100644 index 000000000..d4c64544d --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/CloreWalletManagerAssembly.kt @@ -0,0 +1,31 @@ +package com.tangem.blockchain.common.assembly.impl + +import com.tangem.blockchain.blockchains.bitcoin.BitcoinTransactionBuilder +import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinNetworkService +import com.tangem.blockchain.blockchains.clore.CloreProvidersBuilder +import com.tangem.blockchain.blockchains.ravencoin.RavencoinFeesCalculator +import com.tangem.blockchain.blockchains.ravencoin.RavencoinWalletManager +import com.tangem.blockchain.common.assembly.WalletManagerAssembly +import com.tangem.blockchain.common.assembly.WalletManagerAssemblyInput +import com.tangem.blockchain.transactionhistory.TransactionHistoryProviderFactory + +internal object CloreWalletManagerAssembly : WalletManagerAssembly() { + + override fun make(input: WalletManagerAssemblyInput): RavencoinWalletManager { + with(input.wallet) { + return RavencoinWalletManager( + wallet = this, + transactionBuilder = BitcoinTransactionBuilder( + walletPublicKey = publicKey.blockchainKey, + blockchain = blockchain, + walletAddresses = addresses, + ), + networkProvider = BitcoinNetworkService( + providers = CloreProvidersBuilder(input.providerTypes, input.config).build(blockchain), + ), + transactionHistoryProvider = TransactionHistoryProviderFactory.makeProvider(blockchain, input.config), + feesCalculator = RavencoinFeesCalculator(blockchain), + ) + } + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/EthereumLikeWalletManagerAssembly.kt b/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/EthereumLikeWalletManagerAssembly.kt index 6bf0abfc0..947e3fd40 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/EthereumLikeWalletManagerAssembly.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/EthereumLikeWalletManagerAssembly.kt @@ -68,6 +68,7 @@ internal object EthereumLikeWalletManagerAssembly : WalletManagerAssembly TaraxaProvidersBuilder(providerTypes) Blockchain.EnergyWebChain, Blockchain.EnergyWebChainTestnet -> EnergyWebChainProvidersBuilder(providerTypes) Blockchain.Core, Blockchain.CoreTestnet -> CoreProvidersBuilder(providerTypes) + Blockchain.Chiliz, Blockchain.ChilizTestnet -> ChilizProvidersBuilder(providerTypes) Blockchain.Xodex -> XodexProvidersBuilder(providerTypes) Blockchain.Canxium -> CanxiumProvidersBuilder(providerTypes) else -> error("Unsupported blockchain: $blockchain") diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/KaspaWalletManagerAssembly.kt b/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/KaspaWalletManagerAssembly.kt index bd34af174..d97f92475 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/KaspaWalletManagerAssembly.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/assembly/impl/KaspaWalletManagerAssembly.kt @@ -3,20 +3,31 @@ package com.tangem.blockchain.common.assembly.impl import com.tangem.blockchain.blockchains.kaspa.KaspaProvidersBuilder import com.tangem.blockchain.blockchains.kaspa.KaspaTransactionBuilder import com.tangem.blockchain.blockchains.kaspa.KaspaWalletManager +import com.tangem.blockchain.blockchains.kaspa.krc20.KaspaKRC20NetworkService +import com.tangem.blockchain.blockchains.kaspa.krc20.model.KaspaKRC20ProvidersBuilder import com.tangem.blockchain.blockchains.kaspa.network.KaspaNetworkService import com.tangem.blockchain.common.assembly.WalletManagerAssembly import com.tangem.blockchain.common.assembly.WalletManagerAssemblyInput +import com.tangem.blockchain.common.datastorage.implementations.AdvancedDataStorage -internal object KaspaWalletManagerAssembly : WalletManagerAssembly() { +internal class KaspaWalletManagerAssembly( + private val dataStorage: AdvancedDataStorage, +) : WalletManagerAssembly() { override fun make(input: WalletManagerAssemblyInput): KaspaWalletManager { return with(input.wallet) { KaspaWalletManager( wallet = this, - transactionBuilder = KaspaTransactionBuilder(), + transactionBuilder = KaspaTransactionBuilder( + publicKey = publicKey, + ), networkProvider = KaspaNetworkService( providers = KaspaProvidersBuilder(input.providerTypes, input.config).build(blockchain), ), + krc20NetworkProvider = KaspaKRC20NetworkService( + providers = KaspaKRC20ProvidersBuilder(input.providerTypes).build(blockchain), + ), + dataStorage = dataStorage, ) } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/datastorage/BlockchainDataStorage.kt b/blockchain/src/main/java/com/tangem/blockchain/common/datastorage/BlockchainDataStorage.kt index 480554118..2f26369bb 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/datastorage/BlockchainDataStorage.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/datastorage/BlockchainDataStorage.kt @@ -12,4 +12,7 @@ interface BlockchainDataStorage { /** Store [value] in JSON format by [key] */ suspend fun store(key: String, value: String) + + /** Remove [value] from storage by [key] */ + suspend fun remove(key: String) } diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/datastorage/BlockchainSavedData.kt b/blockchain/src/main/java/com/tangem/blockchain/common/datastorage/BlockchainSavedData.kt index c05dfffb1..402a7a529 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/datastorage/BlockchainSavedData.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/datastorage/BlockchainSavedData.kt @@ -2,6 +2,8 @@ package com.tangem.blockchain.common.datastorage import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.tangem.blockchain.blockchains.kaspa.krc20.model.Envelope +import java.math.BigDecimal internal sealed interface BlockchainSavedData { @@ -16,4 +18,12 @@ internal sealed interface BlockchainSavedData { // TODO: Remove this flag in future https://tangem.atlassian.net/browse/AND-7025 @Json(name = "cache_cleared") val isCacheCleared: Boolean = false, ) : BlockchainSavedData + + @JsonClass(generateAdapter = true) + data class KaspaKRC20IncompleteTokenTransaction( + @Json(name = "transactionId") val transactionId: String, + @Json(name = "amountValue") val amountValue: BigDecimal, + @Json(name = "feeAmountValue") val feeAmountValue: BigDecimal, + @Json(name = "envelope") val envelope: Envelope, + ) : BlockchainSavedData } diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/datastorage/implementations/AdvancedDataStorage.kt b/blockchain/src/main/java/com/tangem/blockchain/common/datastorage/implementations/AdvancedDataStorage.kt index e0db341f3..3429e6d3d 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/datastorage/implementations/AdvancedDataStorage.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/datastorage/implementations/AdvancedDataStorage.kt @@ -43,6 +43,17 @@ internal class AdvancedDataStorage( blockchainDataStorage.store(key = T::class.java.createKey(publicKey), value = adapter.toJson(value)) } + /** Store [value] by [key] */ + suspend inline fun store(key: String, value: T) { + val adapter = moshi.adapter(T::class.java) + blockchainDataStorage.store(key = key, value = adapter.toJson(value)) + } + + /** Remove [value] from storage by [key] */ + suspend inline fun remove(key: String) { + blockchainDataStorage.remove(key = key) + } + /** * Create a unique key by [publicKey] for [BlockchainSavedData]. * Example, Hedera-7BD63F5DE1BF539525C33367592949AE9B99D518BF78F26F3904BCD30CFCF018 diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV1.kt b/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV1.kt index 6b9a0ba76..1b49ebc38 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV1.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV1.kt @@ -14,6 +14,8 @@ import com.tangem.crypto.hdWallet.DerivationPath * https://cips.cardano.org/cips/cip1852/ * - `All else`. According to `BIP44` * https://github.com/satoshilabs/slips/blob/master/slip-0044.md + * + * For EVM if not found in slip-0044.md -> use 60 coin type */ object DerivationConfigV1 : DerivationConfig() { @@ -59,6 +61,8 @@ object DerivationConfigV1 : DerivationConfig() { Blockchain.Blast, Blockchain.Cyber, Blockchain.Canxium, + Blockchain.Chiliz, + Blockchain.Xodex, -> mapOf(AddressType.Default to DerivationPath("m/44'/60'/0'/0/0")) Blockchain.XDC -> mapOf(AddressType.Default to DerivationPath("m/44'/550'/0'/0/0")) Blockchain.EthereumClassic -> mapOf(AddressType.Default to DerivationPath("m/44'/61'/0'/0/0")) @@ -146,6 +150,7 @@ object DerivationConfigV1 : DerivationConfig() { Blockchain.CyberTestnet, Blockchain.EnergyWebChainTestnet, Blockchain.CoreTestnet, + Blockchain.ChilizTestnet, -> mapOf(AddressType.Default to DerivationPath("m/44'/1'/0'/0/0")) Blockchain.Aptos, Blockchain.AptosTestnet, @@ -175,8 +180,7 @@ object DerivationConfigV1 : DerivationConfig() { Blockchain.Casper, Blockchain.CasperTestnet, -> mapOf(AddressType.Default to DerivationPath("m/44'/506'/0'/0/0")) - Blockchain.Xodex, - -> mapOf(AddressType.Default to DerivationPath("m/44'/2415'/0'/0/0")) + Blockchain.Clore -> mapOf(AddressType.Default to DerivationPath("m/44'/1313'/0'/0/0")) } } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV2.kt b/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV2.kt index b286dd340..4c761d7a0 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV2.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV2.kt @@ -76,6 +76,7 @@ object DerivationConfigV2 : DerivationConfig() { Blockchain.Core, Blockchain.Xodex, Blockchain.Canxium, + Blockchain.Chiliz, -> mapOf(AddressType.Default to DerivationPath("m/44'/60'/0'/0/0")) Blockchain.XDC -> mapOf(AddressType.Default to DerivationPath("m/44'/550'/0'/0/0")) Blockchain.Binance -> mapOf(AddressType.Default to DerivationPath("m/44'/714'/0'/0/0")) @@ -149,6 +150,7 @@ object DerivationConfigV2 : DerivationConfig() { Blockchain.CyberTestnet, Blockchain.EnergyWebChainTestnet, Blockchain.CoreTestnet, + Blockchain.ChilizTestnet, -> mapOf(AddressType.Default to DerivationPath("m/44'/1'/0'/0/0")) Blockchain.Aptos, Blockchain.AptosTestnet, @@ -174,6 +176,7 @@ object DerivationConfigV2 : DerivationConfig() { Blockchain.Casper, Blockchain.CasperTestnet, -> mapOf(AddressType.Default to DerivationPath("m/44'/506'/0'/0/0")) + Blockchain.Clore -> mapOf(AddressType.Default to DerivationPath("m/44'/1313'/0'/0/0")) } } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV3.kt b/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV3.kt index 71322e5f8..f9a9c641e 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV3.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/derivation/DerivationConfigV3.kt @@ -71,6 +71,7 @@ object DerivationConfigV3 : DerivationConfig() { Blockchain.Core, Blockchain.Xodex, Blockchain.Canxium, + Blockchain.Chiliz, -> mapOf(AddressType.Default to DerivationPath("m/44'/60'/0'/0/0")) Blockchain.XDC -> mapOf(AddressType.Default to DerivationPath("m/44'/550'/0'/0/0")) Blockchain.EthereumClassic -> mapOf(AddressType.Default to DerivationPath("m/44'/61'/0'/0/0")) @@ -145,6 +146,7 @@ object DerivationConfigV3 : DerivationConfig() { Blockchain.CyberTestnet, Blockchain.EnergyWebChainTestnet, Blockchain.CoreTestnet, + Blockchain.ChilizTestnet, -> mapOf(AddressType.Default to DerivationPath("m/44'/1'/0'/0/0")) Blockchain.Aptos, Blockchain.AptosTestnet, @@ -170,6 +172,7 @@ object DerivationConfigV3 : DerivationConfig() { Blockchain.Casper, Blockchain.CasperTestnet, -> mapOf(AddressType.Default to DerivationPath("m/44'/506'/0'/0/0")) + Blockchain.Clore -> mapOf(AddressType.Default to DerivationPath("m/44'/1313'/0'/0/0")) } } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/transaction/Fee.kt b/blockchain/src/main/java/com/tangem/blockchain/common/transaction/Fee.kt index 8a8cf06aa..254bd092e 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/transaction/Fee.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/transaction/Fee.kt @@ -70,6 +70,7 @@ sealed class Fee { override val amount: Amount, val mass: BigInteger, val feeRate: BigInteger, + val revealTransactionFee: Amount? = null, ) : Fee() data class Filecoin( diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/trustlines/AssetRequirementsCondition.kt b/blockchain/src/main/java/com/tangem/blockchain/common/trustlines/AssetRequirementsCondition.kt index f3529de0c..4ff907ceb 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/trustlines/AssetRequirementsCondition.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/trustlines/AssetRequirementsCondition.kt @@ -1,16 +1,26 @@ package com.tangem.blockchain.common.trustlines import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Blockchain sealed class AssetRequirementsCondition { /** * The exact value of the fee for this type of condition is unknown. */ - object PaidTransaction : AssetRequirementsCondition() + data object PaidTransaction : AssetRequirementsCondition() /** * The exact value of the fee for this type of condition is stored in `feeAmount`. */ - data class PaidTransactionWithFee(val feeAmount: Amount) : AssetRequirementsCondition() + data class PaidTransactionWithFee( + val blockchain: Blockchain, + val feeAmount: Amount, + ) : AssetRequirementsCondition() + + data class IncompleteTransaction( + val blockchain: Blockchain, + val amount: Amount, + val feeAmount: Amount, + ) : AssetRequirementsCondition() } diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/trustlines/AssetRequirementsManager.kt b/blockchain/src/main/java/com/tangem/blockchain/common/trustlines/AssetRequirementsManager.kt index 27be094b1..e3107e6d3 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/trustlines/AssetRequirementsManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/trustlines/AssetRequirementsManager.kt @@ -4,14 +4,11 @@ import com.tangem.blockchain.common.CryptoCurrencyType import com.tangem.blockchain.common.TransactionSigner import com.tangem.blockchain.extensions.SimpleResult -/** - * Responsible for the token association creation (Hedera) and trust line setup (XRP, Stellar, Aptos, Algorand and other). - */ interface AssetRequirementsManager { - suspend fun hasRequirements(currencyType: CryptoCurrencyType): Boolean - suspend fun requirementsCondition(currencyType: CryptoCurrencyType): AssetRequirementsCondition? suspend fun fulfillRequirements(currencyType: CryptoCurrencyType, signer: TransactionSigner): SimpleResult + + suspend fun discardRequirements(currencyType: CryptoCurrencyType): SimpleResult } diff --git a/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/ExternalLinkProviderFactory.kt b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/ExternalLinkProviderFactory.kt index f81b64ac3..16513b5c8 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/ExternalLinkProviderFactory.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/ExternalLinkProviderFactory.kt @@ -90,8 +90,10 @@ internal object ExternalLinkProviderFactory { Blockchain.EnergyWebX, Blockchain.EnergyWebXTestnet -> EnergyWebXExternalLinkProvider(isTestnet) Blockchain.Casper, Blockchain.CasperTestnet -> CasperExternalLinkProvider(isTestnet) Blockchain.Core, Blockchain.CoreTestnet -> CoreExternalLinkProvider(isTestnet) + Blockchain.Chiliz, Blockchain.ChilizTestnet -> ChilizExternalLinkProvider(isTestnet) Blockchain.Xodex -> XodexExternalLinkProvider() Blockchain.Canxium -> CanxiumExternalLinkProvider() + Blockchain.Clore -> CloreExternalLinkProvider() } } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/ChilizExternalLinkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/ChilizExternalLinkProvider.kt new file mode 100644 index 000000000..0f56cf648 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/ChilizExternalLinkProvider.kt @@ -0,0 +1,23 @@ +package com.tangem.blockchain.externallinkprovider.providers + +import com.tangem.blockchain.externallinkprovider.ExternalLinkProvider +import com.tangem.blockchain.externallinkprovider.TxExploreState + +internal class ChilizExternalLinkProvider(isTestnet: Boolean) : ExternalLinkProvider { + + override val explorerBaseUrl: String = if (isTestnet) { + "https://testnet.chiliscan.com/" + } else { + "https://chiliscan.com/" + } + + override val testNetTopUpUrl: String = "https://tatum.io/faucets/chiliz/" + + override fun explorerUrl(walletAddress: String, contractAddress: String?): String { + return "${explorerBaseUrl}address/$walletAddress" + } + + override fun getExplorerTxUrl(transactionHash: String): TxExploreState { + return TxExploreState.Url("${explorerBaseUrl}tx/$transactionHash") + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/CloreExternalLinkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/CloreExternalLinkProvider.kt new file mode 100644 index 000000000..0883c5e21 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/externallinkprovider/providers/CloreExternalLinkProvider.kt @@ -0,0 +1,17 @@ +package com.tangem.blockchain.externallinkprovider.providers + +import com.tangem.blockchain.externallinkprovider.ExternalLinkProvider +import com.tangem.blockchain.externallinkprovider.TxExploreState + +internal class CloreExternalLinkProvider : ExternalLinkProvider { + + override val explorerBaseUrl: String = "https://clore.cryptoscope.io/" + + override fun explorerUrl(walletAddress: String, contractAddress: String?): String { + return explorerBaseUrl + "address/?address=$walletAddress" + } + + override fun getExplorerTxUrl(transactionHash: String): TxExploreState { + return TxExploreState.Url(explorerBaseUrl + "tx/?txid=$transactionHash") + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/BlockBookNetworkProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/BlockBookNetworkProvider.kt index cb575c9c3..a1eb2729c 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/BlockBookNetworkProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/BlockBookNetworkProvider.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressInfo import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinFee import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinNetworkProvider +import com.tangem.blockchain.blockchains.clore.CloreMainNetParams import com.tangem.blockchain.blockchains.dash.DashMainNetParams import com.tangem.blockchain.blockchains.ducatus.DucatusMainNetParams import com.tangem.blockchain.blockchains.ravencoin.RavencoinMainNetParams @@ -49,6 +50,7 @@ class BlockBookNetworkProvider( Blockchain.Dash -> DashMainNetParams() Blockchain.Ravencoin -> RavencoinMainNetParams() Blockchain.RavencoinTestnet -> RavencoinTestNetParams() + Blockchain.Clore -> CloreMainNetParams() else -> error("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}") } @@ -189,11 +191,16 @@ class BlockBookNetworkProvider( } private suspend fun getFeePerKb(param: Int): BigDecimal { - val getFeeResponse = withContext(Dispatchers.IO) { api.getFee(param) } + val feeRate = withContext(Dispatchers.IO) { + when (blockchain) { + Blockchain.Clore -> api.getFees(param).result + else -> api.getFee(param).result.feerate + } + } - if (getFeeResponse.result.feerate <= 0) throw BlockchainSdkError.FailedToLoadFee + if (feeRate <= 0) throw BlockchainSdkError.FailedToLoadFee - return getFeeResponse.result.feerate + return feeRate .toBigDecimal() .setScale(blockchain.decimals(), RoundingMode.UP) } diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/BlockBookNetworkProviderFactory.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/BlockBookNetworkProviderFactory.kt index 170bed57c..71dc9d12d 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/BlockBookNetworkProviderFactory.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/BlockBookNetworkProviderFactory.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinNetworkProvider import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.extensions.letNotBlank +import com.tangem.blockchain.network.blockbook.config.CloreBlockBookConfig import com.tangem.blockchain.network.blockbook.config.GetBlockConfig import com.tangem.blockchain.network.blockbook.config.NowNodesConfig @@ -42,4 +43,13 @@ internal class BlockBookNetworkProviderFactory( null } } + + fun createCloreBlockProvider(blockchain: Blockchain, baseHost: String): BitcoinNetworkProvider { + return BlockBookNetworkProvider( + config = CloreBlockBookConfig( + baseHost = baseHost, + ), + blockchain = blockchain, + ) + } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/config/CloreBlockBookConfig.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/config/CloreBlockBookConfig.kt new file mode 100644 index 000000000..899441380 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/config/CloreBlockBookConfig.kt @@ -0,0 +1,10 @@ +package com.tangem.blockchain.network.blockbook.config + +import com.tangem.blockchain.common.Blockchain + +internal class CloreBlockBookConfig(override val baseHost: String) : BlockBookConfig(credentials = null) { + + override fun getHost(blockchain: Blockchain, request: BlockBookRequest): String = baseHost + + override fun path(request: BlockBookRequest): String = "api/v2" +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/BlockBookApi.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/BlockBookApi.kt index 17e78692f..163cf11ed 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/BlockBookApi.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/BlockBookApi.kt @@ -7,10 +7,7 @@ import com.tangem.blockchain.common.logging.AddHeaderInterceptor import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.blockchain.network.blockbook.config.BlockBookConfig import com.tangem.blockchain.network.blockbook.config.BlockBookRequest -import com.tangem.blockchain.network.blockbook.network.responses.GetAddressResponse -import com.tangem.blockchain.network.blockbook.network.responses.GetFeeResponse -import com.tangem.blockchain.network.blockbook.network.responses.GetUtxoResponseItem -import com.tangem.blockchain.network.blockbook.network.responses.SendTransactionResponse +import com.tangem.blockchain.network.blockbook.network.responses.* import com.tangem.blockchain.network.moshi import com.tangem.blockchain.transactionhistory.models.TransactionHistoryRequest import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -92,6 +89,24 @@ internal class BlockBookApi(private val config: BlockBookConfig, private val blo .unpack() } + /** + * It is also a request to receive a fee for confirmation blocks. + * Some blockchains use this method. Such blockchains: + * - CloreAI + */ + suspend fun getFees(param: Int): GetFeesResponse { + val requestBaseUrl = config.getRequestBaseUrl(BlockBookRequest.GetFee, blockchain) + return client + .newCall( + request = Request.Builder() + .get() + .url("$requestBaseUrl/estimatefee/$param") + .build(), + ) + .await() + .unpack() + } + suspend fun sendTransaction(txHex: String): SendTransactionResponse { val requestBaseUrl = config.getRequestBaseUrl(BlockBookRequest.SendTransaction, blockchain) return client diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/responses/GetAddressResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/responses/GetAddressResponse.kt index 6a3e4b4a3..c1c23925e 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/responses/GetAddressResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/responses/GetAddressResponse.kt @@ -49,11 +49,21 @@ data class GetAddressResponse( * Tron blockchain specific info. * There are many more fields in this response, but we map only the required ones. */ - data class TronTXReceipt(val status: StatusType?) + @JsonClass(generateAdapter = true) + data class TronTXReceipt( + @Json(name = "status") + val status: StatusType?, + ) + @JsonClass(generateAdapter = false) enum class StatusType(override val value: Int) : EnumeratedEnum { + @Json(name = "PENDING") PENDING(-1), + + @Json(name = "FAILURE") FAILURE(0), + + @Json(name = "OK") OK(1), ; } diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/responses/GetFeesResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/responses/GetFeesResponse.kt new file mode 100644 index 000000000..33fd4c74e --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockbook/network/responses/GetFeesResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.blockchain.network.blockbook.network.responses + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetFeesResponse( + @Json(name = "result") val result: Double, +) diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairApi.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairApi.kt index 67a432ae4..93b45226b 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairApi.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairApi.kt @@ -1,5 +1,6 @@ package com.tangem.blockchain.network.blockchair +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.tangem.blockchain.network.blockchair.response.SendTransactionResponse import retrofit2.http.* @@ -53,4 +54,7 @@ interface BlockchairApi { } @JsonClass(generateAdapter = true) -data class BlockchairBody(val data: String) +data class BlockchairBody( + @Json(name = "data") + val data: String, +) diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairResponse.kt index f3fc9e1ae..cdc74d785 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairResponse.kt @@ -6,9 +6,11 @@ import com.tangem.blockchain.common.Token @JsonClass(generateAdapter = true) data class BlockchairAddress( + @Json(name = "data") val data: Map? = null, ) +@JsonClass(generateAdapter = true) data class BlockchairAddressData( @Json(name = "address") val addressInfo: BlockchairAddressInfo? = null, @@ -16,12 +18,16 @@ data class BlockchairAddressData( @Json(name = "utxo") val unspentOutputs: List? = null, // btc-like only + @Json(name = "transactions") val transactions: List? = null, // btc-like only + @Json(name = "calls") val calls: List? = null, // eth-like only ) +@JsonClass(generateAdapter = true) data class BlockchairAddressInfo( + @Json(name = "balance") val balance: Long? = null, @Json(name = "script_hex") @@ -34,6 +40,7 @@ data class BlockchairAddressInfo( val unspentOutputCount: Int? = null, ) +@JsonClass(generateAdapter = true) data class BlockchairUnspentOutput( @Json(name = "block_id") val block: Int? = null, @@ -41,49 +48,65 @@ data class BlockchairUnspentOutput( @Json(name = "transaction_hash") val transactionHash: String? = null, + @Json(name = "index") val index: Int? = null, @Json(name = "value") val amount: Long? = null, ) +@JsonClass(generateAdapter = true) data class BlockchairTransaction( + @Json(name = "data") val data: Map? = null, ) +@JsonClass(generateAdapter = true) data class BlockchairTransactionData( + @Json(name = "transaction") val transaction: BlockchairTransactionInfo? = null, ) +@JsonClass(generateAdapter = true) data class BlockchairTransactionInfo( @Json(name = "block_id") val block: Int? = null, + @Json(name = "hash") val hash: String? = null, + @Json(name = "time") val time: String? = null, @Json(name = "balance_change") val balanceDif: Long? = null, ) +@JsonClass(generateAdapter = true) data class BlockchairStats( + @Json(name = "data") val data: BlockchairStatsData? = null, ) +@JsonClass(generateAdapter = true) data class BlockchairStatsData( @Json(name = "suggested_transaction_fee_per_byte_sat") val feePerByte: Int? = null, ) +@JsonClass(generateAdapter = true) data class BlockchairTokenHolder( + @Json(name = "data") val data: Map, ) +@JsonClass(generateAdapter = true) data class TokenHolderData( + @Json(name = "transactions") val transactions: List? = null, ) +@JsonClass(generateAdapter = true) data class BlockchairCallInfo( @Json(name = "block_id") val block: Int? = null, @@ -91,12 +114,16 @@ data class BlockchairCallInfo( @Json(name = "transaction_hash") val hash: String? = null, + @Json(name = "time") val time: String? = null, + @Json(name = "sender") val sender: String? = null, + @Json(name = "recipient") val recipient: String? = null, + @Json(name = "value") val value: String? = null, @Json(name = "token_symbol") @@ -111,20 +138,24 @@ data class BlockchairCallInfo( @JsonClass(generateAdapter = true) data class BlockchairTokensResponse( + @Json(name = "data") val data: Map? = null, ) +@JsonClass(generateAdapter = true) data class BlockchairTokensData( @Json(name = "layer_2") val tokensInfo: TokensInfo, ) +@JsonClass(generateAdapter = true) data class TokensInfo( @Json(name = "erc_20") val tokens: List, ) // data class Erc20Tokens(val tokens: List) +@JsonClass(generateAdapter = true) data class BlockchairToken( @Json(name = "token_address") val address: String, @@ -141,6 +172,7 @@ data class BlockchairToken( @Json(name = "balance_approximate") val approximateBalance: Double, + @Json(name = "balance") val balance: String, ) { fun toToken(): Token { diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockcypher/BlockcypherApi.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockcypher/BlockcypherApi.kt index fcdd2c700..ff98ab845 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/blockcypher/BlockcypherApi.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockcypher/BlockcypherApi.kt @@ -1,5 +1,6 @@ package com.tangem.blockchain.network.blockcypher +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.tangem.blockchain.network.blockcypher.response.SendTransactionResponse import retrofit2.http.* @@ -21,4 +22,4 @@ interface BlockcypherApi { } @JsonClass(generateAdapter = true) -data class BlockcypherSendBody(val tx: String) +data class BlockcypherSendBody(@Json(name = "tx") val tx: String) diff --git a/blockchain/src/main/java/com/tangem/blockchain/transactionhistory/blockchains/polygon/network/PolygonTransactionHistoryResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/transactionhistory/blockchains/polygon/network/PolygonTransactionHistoryResponse.kt index 988b5ebf0..76974af25 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/transactionhistory/blockchains/polygon/network/PolygonTransactionHistoryResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/transactionhistory/blockchains/polygon/network/PolygonTransactionHistoryResponse.kt @@ -15,7 +15,10 @@ internal sealed class PolygonScanResult { internal data class Description(val description: String) : PolygonScanResult() @JsonClass(generateAdapter = true) - internal data class Transactions(val txs: List) : PolygonScanResult() + internal data class Transactions( + @Json(name = "txs") + val txs: List, + ) : PolygonScanResult() val transactions: List? get() = when (this) { diff --git a/blockchain/src/test/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransactionTest.kt b/blockchain/src/test/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransactionTest.kt index 823d83bad..f010f0c13 100644 --- a/blockchain/src/test/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransactionTest.kt +++ b/blockchain/src/test/java/com/tangem/blockchain/blockchains/kaspa/KaspaTransactionTest.kt @@ -2,18 +2,19 @@ package com.tangem.blockchain.blockchains.kaspa import com.google.common.truth.Truth import com.tangem.blockchain.blockchains.kaspa.network.* -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.hexToBytes +import org.bitcoinj.core.Coin +import org.bitcoinj.core.TransactionOutput import org.junit.Test +import java.math.BigDecimal class KaspaTransactionTest { private val blockchain = Blockchain.Kaspa + private val networkParameters = KaspaMainNetParams() private val decimals = blockchain.decimals() private val addressService = KaspaAddressService() @@ -37,7 +38,12 @@ class KaspaTransactionTest { val sourceAddress = addressService.makeAddress(walletPublicKey) - val transactionBuilder = KaspaTransactionBuilder() + val transactionBuilder = KaspaTransactionBuilder( + publicKey = Wallet.PublicKey( + seedKey = walletPublicKey, + derivationType = null, + ), + ) transactionBuilder.unspentOutputs = listOf( KaspaUnspentOutput( transactionHash = "deb88e7dd734437c6232a636085ef917d1d13cc549fe14749765508b2782f2fb".hexToBytes(), @@ -132,10 +138,11 @@ class KaspaTransactionTest { // act val buildToSignResult = transactionBuilder.buildToSign(transactionData) as Result.Success - val signedTransaction = transactionBuilder.buildToSend(signature) + val hashes = transactionBuilder.getHashesForSign(buildToSignResult.data) + val signedTransaction = transactionBuilder.buildToSend(signature, buildToSignResult.data) // assert - Truth.assertThat(buildToSignResult.data.map { it.toList() }) + Truth.assertThat(hashes.map { it.toList() }) .containsExactly(expectedHashToSign1, expectedHashToSign2, expectedHashToSign3) Truth.assertThat(signedTransaction).isEqualTo(expectedSignedTransaction) } @@ -158,7 +165,12 @@ class KaspaTransactionTest { val sourceAddress = addressService.makeAddress(walletPublicKey) - val transactionBuilder = KaspaTransactionBuilder() + val transactionBuilder = KaspaTransactionBuilder( + publicKey = Wallet.PublicKey( + seedKey = walletPublicKey, + derivationType = null, + ), + ) transactionBuilder.unspentOutputs = listOf( KaspaUnspentOutput( transactionHash = "ae96e819429e9da538e84cb213f62fbc8ad32e932d7c7f1fb9bd2fedf8fd7b4a".hexToBytes(), @@ -217,10 +229,49 @@ class KaspaTransactionTest { // act val buildToSignResult = transactionBuilder.buildToSign(transactionData) as Result.Success - val signedTransaction = transactionBuilder.buildToSend(signature) + val hashes = transactionBuilder.getHashesForSign(buildToSignResult.data) + val signedTransaction = transactionBuilder.buildToSend(signature, buildToSignResult.data) // assert - Truth.assertThat(buildToSignResult.data.map { it.toList() }).containsExactly(expectedHashToSign1) + Truth.assertThat(hashes.map { it.toList() }).containsExactly(expectedHashToSign1) Truth.assertThat(signedTransaction).isEqualTo(expectedSignedTransaction) } + + @Test + fun buildCorrectKaspaKRC20Transaction() { + val commitTransaction = createKaspaTransaction( + networkParameters = networkParameters, + unspentOutputs = listOf( + KaspaUnspentOutput( + transactionHash = "4DF1F7923708F6FA98F8D192CDB511666FC93C858D86FB7BC61BC7C13D54C9F4".hexToBytes(), + outputIndex = 2, + amount = BigDecimal.ZERO, + outputScript = "415BFC0DDE408A06EC6A39AE850986B49C2D0D5B83E47233B43012DE3AEDCECDE75EBC239008060BD50633E8E1AEBA891300CA74E8279DD591D8CEDA60609AFA6001".hexToBytes(), + ), + ), + transformer = { + it.addOutput( + TransactionOutput( + networkParameters, + null, + Coin.valueOf(500003000), + "AA207B1CFEE1AA9CB2AB4EFF9FF9593F88D3F0453F02E02790AC493F8EB712DCE17787".hexToBytes(), + ), + ) + it.addOutput( + TransactionOutput( + networkParameters, + null, + Coin.valueOf(3764387352), + "2035C82AA416591A1AFB84D10B6D225899F27CE6B51381C03B8CF104C3906258D3AC".hexToBytes(), + ), + ) + it + }, + ) + + Truth + .assertThat(commitTransaction.transactionHash()) + .isEqualTo("C2CB9D865F5085CD6F7F23365545C68D1EACA7E3CDE9D231A64812BE2C989A30".hexToBytes()) + } } diff --git a/blockchain/src/test/java/com/tangem/blockchain/common/WalletManagerFactoryTest.kt b/blockchain/src/test/java/com/tangem/blockchain/common/WalletManagerFactoryTest.kt index 1c2116423..f141db37d 100644 --- a/blockchain/src/test/java/com/tangem/blockchain/common/WalletManagerFactoryTest.kt +++ b/blockchain/src/test/java/com/tangem/blockchain/common/WalletManagerFactoryTest.kt @@ -145,6 +145,7 @@ internal class WalletManagerFactoryTest { blockchainDataStorage = object : BlockchainDataStorage { override suspend fun getOrNull(key: String): String? = null override suspend fun store(key: String, value: String) = Unit + override suspend fun remove(key: String) = Unit }, accountCreator = accountCreator, featureToggles = BlockchainFeatureToggles(isEthereumEIP1559Enabled = true), @@ -211,6 +212,7 @@ internal class WalletManagerFactoryTest { blockchainDataStorage = object : BlockchainDataStorage { override suspend fun getOrNull(key: String): String? = null override suspend fun store(key: String, value: String) = Unit + override suspend fun remove(key: String) = Unit }, accountCreator = accountCreator, featureToggles = BlockchainFeatureToggles(isEthereumEIP1559Enabled = true), diff --git a/blockchain/src/test/java/com/tangem/blockchain/common/address/EstimationFeeAddressFactoryTest.kt b/blockchain/src/test/java/com/tangem/blockchain/common/address/EstimationFeeAddressFactoryTest.kt index d2967cc8e..bbcb3c2fd 100644 --- a/blockchain/src/test/java/com/tangem/blockchain/common/address/EstimationFeeAddressFactoryTest.kt +++ b/blockchain/src/test/java/com/tangem/blockchain/common/address/EstimationFeeAddressFactoryTest.kt @@ -8,20 +8,22 @@ class EstimationFeeAddressFactoryTest { private val factory = EstimationFeeAddressFactory() private val hardcodedAddressesForBlockchain = mapOf( - Blockchain.Cardano to "addr1q95pg4z9tf26r5dwf72vmh62u3pr9sewq2waahyhpjzm3enz43pvhh0us3z0z5xen2skq200e67eu89s5v2s0sdh3fnsm9lknu", Blockchain.Chia to "", Blockchain.ChiaTestnet to "", + Blockchain.Hedera to "", + Blockchain.HederaTestnet to "", + Blockchain.Tezos to "", + Blockchain.Ducatus to "", + Blockchain.InternetComputer to "", + Blockchain.Casper to "", + Blockchain.CasperTestnet to "", + Blockchain.Cardano to "addr1q95pg4z9tf26r5dwf72vmh62u3pr9sewq2waahyhpjzm3enz43pvhh0us3z0z5xen2skq200e67eu89s5v2s0sdh3fnsm9lknu", Blockchain.XRP to "", Blockchain.Stellar to "", Blockchain.StellarTestnet to "", Blockchain.Binance to "", Blockchain.BinanceTestnet to "", Blockchain.SolanaTestnet to "", - Blockchain.Hedera to "", - Blockchain.HederaTestnet to "", - Blockchain.Tezos to "", - Blockchain.Kaspa to "", - Blockchain.Ducatus to "", Blockchain.Bitcoin to "bc1qkrc5kmpq546wr2xk0errg58yw9jjq7thvhdk5k", Blockchain.BitcoinTestnet to "bc1qkrc5kmpq546wr2xk0errg58yw9jjq7thvhdk5k", Blockchain.Litecoin to "MSqjXH6toL4kHqsRo3mWaWMkhmiH9GQxLR", @@ -120,7 +122,6 @@ class EstimationFeeAddressFactoryTest { Blockchain.Kaspa to "kaspa:qyp2f0ust8wyvuvqrzajvehx5jyh43vcjgessjdkw9vyw6rww4fdlsgzysspfuq", Blockchain.Sei to "sei1lhjvds604fvac32j4eygpr820lyc82dlfv0ea4", Blockchain.SeiTestnet to "sei1lhjvds604fvac32j4eygpr820lyc82dlfv0ea4", - Blockchain.InternetComputer to "", Blockchain.Sui to "0xbca45e36a271e106546c89984108685215724e488570a0049a187c473cd521bc", ) diff --git a/tangem-android-tools b/tangem-android-tools index 0e4934995..0ed07b85e 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 0e4934995b8e7c69862ef80362e86bd166bd1288 +Subproject commit 0ed07b85e64805707b2ef6a92f42bc2c4e8b7753