Updated on 2026-08-14
This commit is contained in:
parent
a21044feee
commit
05cc0946f1
8 changed files with 376 additions and 145 deletions
|
|
@ -1,16 +0,0 @@
|
|||
package com.tangem.blockchain.eth
|
||||
|
||||
import org.kethereum.crypto.toAddress
|
||||
import org.kethereum.functions.isValid
|
||||
import org.kethereum.model.Address
|
||||
import org.kethereum.model.PublicKey
|
||||
import org.kethereum.wallet.model.WalletCrypto
|
||||
|
||||
object EthereumAddressFactory {
|
||||
fun makeAddress(cardPublicKey: ByteArray, testNet: Boolean = false): String =
|
||||
PublicKey(cardPublicKey).toAddress().hex
|
||||
}
|
||||
|
||||
object EthereumAddressValidator {
|
||||
fun validate(address: String, testNet: Boolean = false): Boolean = Address(address).isValid()
|
||||
}
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
package com.tangem.blockchain.eth
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.wallets.CurrencyWallet
|
||||
import com.tangem.tasks.TaskEvent
|
||||
import org.kethereum.DEFAULT_GAS_LIMIT
|
||||
import org.kethereum.ETH_IN_WEI
|
||||
import org.kethereum.crypto.api.ec.ECDSASignature
|
||||
import org.kethereum.crypto.determineRecId
|
||||
import org.kethereum.crypto.impl.ec.canonicalise
|
||||
import org.kethereum.functions.encodeRLP
|
||||
import org.kethereum.keccakshortcut.keccak
|
||||
import org.kethereum.model.*
|
||||
import java.math.BigInteger
|
||||
|
||||
class EthereumWalletManager(
|
||||
private val cardId: String,
|
||||
private val walletPublicKey: ByteArray,
|
||||
private val isTestNet: Boolean,
|
||||
walletConfig: WalletConfig
|
||||
) : WalletManager,
|
||||
TransactionSender,
|
||||
FeeProvider {
|
||||
|
||||
override val blockchain: Blockchain = Blockchain.Ethereum
|
||||
private val address = blockchain.makeAddress(walletPublicKey)
|
||||
override var wallet: Wallet = CurrencyWallet(walletConfig, address)
|
||||
|
||||
|
||||
override fun update() {
|
||||
|
||||
}
|
||||
|
||||
override fun send(transactionData: TransactionData, signer: TransactionSigner) {
|
||||
val builder = EthereumTransactionBuilder()
|
||||
val hashes = builder.buildToSign(transactionData, walletPublicKey)
|
||||
signer.sign(hashes.toTypedArray(), cardId) {
|
||||
when (it) {
|
||||
is TaskEvent.Event -> builder.buildToSend(it.data.signature, walletPublicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFee(amount: Amount, source: String, destination: String): List<Amount> {
|
||||
val gasPrices: List<Long> = getEthGasPrices()
|
||||
val gasLimit = getGasLimit(amount).value
|
||||
|
||||
val fees = mutableListOf<Amount>()
|
||||
for (gasPrice in gasPrices) {
|
||||
val feeValue = (gasPrice * gasLimit).toBigDecimal().divide(ETH_IN_WEI.toBigDecimal())
|
||||
fees.add(Amount(blockchain.currency, feeValue, address, blockchain.decimals))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private class EthereumTransactionBuilder {
|
||||
var nonce: BigInteger? = null
|
||||
var transaction: Transaction? = null
|
||||
var hashToSign: ByteArray? = null
|
||||
|
||||
fun buildToSign(transactionData: TransactionData, publicKey: ByteArray): List<ByteArray> {
|
||||
val from = Address(transactionData.sourceAddress)
|
||||
val to = Address(transactionData.destinationAddress)
|
||||
val value = transactionData.amount.value!!
|
||||
.movePointRight(transactionData.amount.decimals.toInt()).toBigInteger()
|
||||
val fee = transactionData.fee!!.value!!
|
||||
.movePointRight(transactionData.fee.decimals.toInt()).toBigInteger()
|
||||
val gasPrice = fee.divide(DEFAULT_GAS_LIMIT)
|
||||
|
||||
transaction = createTransactionWithDefaults(
|
||||
from = from,
|
||||
to = to,
|
||||
value = value,
|
||||
gasPrice = gasPrice,
|
||||
gasLimit = DEFAULT_GAS_LIMIT,
|
||||
nonce = nonce,
|
||||
chain = ChainId(Chain.Mainnet.id)
|
||||
)
|
||||
|
||||
val transactionToSign = transaction!!
|
||||
.encodeRLP(SignatureData().apply { v = transaction!!.chain!! })
|
||||
hashToSign = transactionToSign.keccak()
|
||||
|
||||
return listOf(hashToSign!!)
|
||||
}
|
||||
|
||||
fun buildToSend(signature: ByteArray, walletPublicKey: ByteArray): ByteArray {
|
||||
val r = BigInteger(1, signature.copyOfRange(0, 32))
|
||||
var s = BigInteger(1, signature.copyOfRange(32, 64))
|
||||
|
||||
val ecdsaSignature = ECDSASignature(r, s).canonicalise()
|
||||
|
||||
val recId = ecdsaSignature.determineRecId(hashToSign!!, PublicKey(walletPublicKey))
|
||||
val v = (recId + 27 + Chain.Mainnet.id).toBigInteger() // TODO: where to put chainId?
|
||||
val signatureData = SignatureData(ecdsaSignature.r, ecdsaSignature.s, v)
|
||||
|
||||
return transaction!!.encodeRLP(signatureData)
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Chain(val id: Long) {
|
||||
Mainnet(1),
|
||||
Morden(2),
|
||||
Ropsten(3),
|
||||
Rinkeby(4),
|
||||
Rootstock_mainnet(30),
|
||||
Rootstock_testnet(31),
|
||||
Kovan(42),
|
||||
Ethereum_Classic_mainnet(61),
|
||||
Ethereum_Classic_testnet(62),
|
||||
Geth_private_chains(1337),
|
||||
Matic_Testnet(8995);
|
||||
}
|
||||
|
||||
private enum class GasLimit(val value: Long) {
|
||||
Default(210000),
|
||||
Token(60000),
|
||||
High(300000)
|
||||
}
|
||||
|
||||
private fun getGasLimit(amount: Amount): GasLimit {
|
||||
return when (amount.currencySymbol) {
|
||||
"ETH" -> GasLimit.Default
|
||||
"DGX" -> GasLimit.High
|
||||
else -> GasLimit.Token
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.blockchain.ethereum
|
||||
|
||||
import org.kethereum.crypto.toAddress
|
||||
import org.kethereum.functions.isValid
|
||||
import org.kethereum.model.Address
|
||||
import org.kethereum.model.PublicKey
|
||||
|
||||
class EthereumAddressFactory {
|
||||
companion object {
|
||||
fun makeAddress(walletPublicKey: ByteArray): String =
|
||||
PublicKey(walletPublicKey.sliceArray(1..64)).toAddress().hex
|
||||
}
|
||||
}
|
||||
|
||||
class EthereumAddressValidator {
|
||||
companion object {
|
||||
fun validate(address: String): Boolean = Address(address).isValid()
|
||||
}
|
||||
}
|
||||
|
||||
enum class Chain(val id: Int) {
|
||||
Mainnet(1),
|
||||
Morden(2),
|
||||
Ropsten(3),
|
||||
Rinkeby(4),
|
||||
RootstockMainnet(30),
|
||||
RootstockTestnet(31),
|
||||
Kovan(42),
|
||||
EthereumClassicMainnet(61),
|
||||
EthereumClassicTestnet(62),
|
||||
Geth_private_chains(1337),
|
||||
MaticTestnet(8995);
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
package com.tangem.blockchain.ethereum
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.extensions.Result
|
||||
import com.tangem.blockchain.common.extensions.SimpleResult
|
||||
import com.tangem.blockchain.ethereum.network.EthereumNetworkManager
|
||||
import com.tangem.blockchain.ethereum.network.EthereumResponse
|
||||
import com.tangem.blockchain.wallets.CurrencyWallet
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.tasks.TaskEvent
|
||||
import org.kethereum.DEFAULT_GAS_LIMIT
|
||||
import org.kethereum.crypto.api.ec.ECDSASignature
|
||||
import org.kethereum.crypto.determineRecId
|
||||
import org.kethereum.crypto.impl.ec.canonicalise
|
||||
import org.kethereum.functions.encodeRLP
|
||||
import org.kethereum.keccakshortcut.keccak
|
||||
import org.kethereum.model.*
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
class EthereumWalletManager(
|
||||
private val cardId: String,
|
||||
private val walletPublicKey: ByteArray,
|
||||
chain: Chain,
|
||||
walletConfig: WalletConfig
|
||||
) : WalletManager,
|
||||
TransactionSender,
|
||||
FeeProvider {
|
||||
|
||||
override val blockchain: Blockchain = Blockchain.Ethereum
|
||||
private val address = blockchain.makeAddress(walletPublicKey)
|
||||
private val currencyWallet = CurrencyWallet(walletConfig, address)
|
||||
override var wallet: Wallet = currencyWallet
|
||||
private val builder = EthereumTransactionBuilder(chain)
|
||||
private val networkManager = EthereumNetworkManager()
|
||||
private var pendingTxCount = -1L
|
||||
private var txCount = -1L
|
||||
|
||||
override suspend fun update() {
|
||||
val result = networkManager.getInfo(address, currencyWallet.balances[AmountType.Token]?.address)
|
||||
when (result) {
|
||||
is Result.Failure -> updateError(result.error)
|
||||
is Result.Success -> updateWallet(result.data)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWallet(data: EthereumResponse) {
|
||||
currencyWallet.balances[AmountType.Coin]?.value = data.balance
|
||||
currencyWallet.balances[AmountType.Token]?.value = data.tokenBalance
|
||||
txCount = data.txCount
|
||||
pendingTxCount = data.pendingTxCount
|
||||
if (txCount == pendingTxCount) {
|
||||
currencyWallet.pendingTransactions.forEach { it.status = TransactionStatus.Confirmed }
|
||||
} else if (currencyWallet.pendingTransactions.isEmpty()) {
|
||||
currencyWallet.pendingTransactions.add(TransactionData(
|
||||
Amount(blockchain.currency, decimals = blockchain.decimals),
|
||||
null,
|
||||
"unknown",
|
||||
currencyWallet.address))
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateError(error: Throwable?) {
|
||||
|
||||
}
|
||||
|
||||
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
|
||||
val transactionToSign = builder.buildToSign(transactionData, txCount.toBigInteger())
|
||||
?: return SimpleResult.Failure(Exception("Not enough data"))
|
||||
when (val signerResponse = signer.sign(transactionToSign.hashes.toTypedArray(), cardId)) {
|
||||
is TaskEvent.Event -> {
|
||||
val transactionToSend = builder.buildToSend(signerResponse.data.signature, transactionToSign, walletPublicKey)
|
||||
return networkManager.sendTransaction(String.format("0x%s", transactionToSend.toHexString()))
|
||||
}
|
||||
is TaskEvent.Completion -> return SimpleResult.Failure(signerResponse.error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
|
||||
val result = networkManager.getFee(getGasLimit(amount).value)
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
val feeValues: List<BigDecimal> = result.data
|
||||
return Result.Success(
|
||||
feeValues.map { Amount(blockchain.currency, it, address, blockchain.decimals) })
|
||||
}
|
||||
is Result.Failure -> return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class EthereumTransactionBuilder(private val chain: Chain) {
|
||||
|
||||
fun buildToSign(transactionData: TransactionData, nonce: BigInteger?): TransactionToSign? {
|
||||
|
||||
val amount: BigDecimal = transactionData.amount.value ?: return null
|
||||
val transactionFee: BigDecimal = transactionData.fee?.value ?: return null
|
||||
|
||||
val value = amount.movePointRight(transactionData.amount.decimals.toInt()).toBigInteger()
|
||||
val fee = transactionFee.movePointRight(transactionData.fee.decimals.toInt()).toBigInteger()
|
||||
|
||||
val transaction = createTransactionWithDefaults(
|
||||
from = Address(transactionData.sourceAddress),
|
||||
to = Address(transactionData.destinationAddress),
|
||||
value = value,
|
||||
gasPrice = fee.divide(DEFAULT_GAS_LIMIT),
|
||||
gasLimit = DEFAULT_GAS_LIMIT,
|
||||
nonce = nonce,
|
||||
chain = ChainId(chain.id.toLong())
|
||||
)
|
||||
val hash = transaction.encodeRLP(SignatureData(v = chain.id.toBigInteger())).keccak()
|
||||
return TransactionToSign(transaction, listOf(hash))
|
||||
}
|
||||
|
||||
fun buildToSend(signature: ByteArray, transactionToSign: TransactionToSign, walletPublicKey: ByteArray): ByteArray {
|
||||
val r = BigInteger(1, signature.copyOfRange(0, 32))
|
||||
val s = BigInteger(1, signature.copyOfRange(32, 64))
|
||||
|
||||
val ecdsaSignature = ECDSASignature(r, s).canonicalise()
|
||||
|
||||
val recId = ecdsaSignature.determineRecId(transactionToSign.hashes[0], PublicKey(walletPublicKey.sliceArray(1..64)))
|
||||
val v = (recId + 27 + 8 + (chain.id * 2)).toBigInteger()
|
||||
val signatureData = SignatureData(ecdsaSignature.r, ecdsaSignature.s, v)
|
||||
|
||||
return transactionToSign.transaction.encodeRLP(signatureData)
|
||||
}
|
||||
}
|
||||
|
||||
private class TransactionToSign(val transaction: Transaction, val hashes: List<ByteArray>)
|
||||
|
||||
enum class GasLimit(val value: Long) {
|
||||
Default(21000),
|
||||
Token(60000),
|
||||
High(300000)
|
||||
}
|
||||
|
||||
private fun getGasLimit(amount: Amount): GasLimit {
|
||||
return when (amount.currencySymbol) {
|
||||
Blockchain.Ethereum.currency -> GasLimit.Default
|
||||
"DGX" -> GasLimit.High
|
||||
else -> GasLimit.Token
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package com.tangem.blockchain.ethereum.network
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.extensions.Result
|
||||
import com.tangem.blockchain.common.extensions.SimpleResult
|
||||
import com.tangem.blockchain.common.extensions.retryIO
|
||||
import com.tangem.blockchain.common.network.API_INFURA
|
||||
import com.tangem.blockchain.common.network.createRetrofitInstance
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import org.kethereum.ETH_IN_WEI
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import java.math.RoundingMode
|
||||
|
||||
|
||||
class EthereumNetworkManager {
|
||||
|
||||
private val api: InfuraApi by lazy {
|
||||
createRetrofitInstance(API_INFURA).create(InfuraApi::class.java)
|
||||
}
|
||||
|
||||
private val provider: InfuraProvider by lazy { InfuraProvider(api) }
|
||||
|
||||
suspend fun sendTransaction(transaction: String): SimpleResult {
|
||||
return try {
|
||||
val response = retryIO { provider.sendTransaction(transaction) }
|
||||
if (response.error == null) {
|
||||
SimpleResult.Success
|
||||
} else {
|
||||
SimpleResult.Failure(Exception("Code: ${(response.error.code)}, ${(response.error.message)}"))
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
SimpleResult.Failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getFee(gasLimit: Long): Result<List<BigDecimal>> {
|
||||
return try {
|
||||
Result.Success(
|
||||
provider.getGasPrice().result!!.parseFee(gasLimit)
|
||||
)
|
||||
} catch (error: Exception) {
|
||||
Result.Failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getInfo(address: String, contractAddress: String? = null): Result<EthereumResponse> {
|
||||
return try {
|
||||
coroutineScope {
|
||||
val balanceResponse = retryIO { async { provider.getBalance(address) } }
|
||||
val txCountResponse = retryIO { async { provider.getTxCount(address) } }
|
||||
val pendingTxCountResponse = retryIO { async { provider.getPendingTxCount(address) } }
|
||||
var tokenBalanceResponse: Deferred<InfuraResponse>? = null
|
||||
if (contractAddress != null) {
|
||||
tokenBalanceResponse = retryIO { async { provider.getTokenBalance(address, contractAddress) } }
|
||||
}
|
||||
Result.Success(EthereumResponse(
|
||||
balanceResponse.await().result!!.parseAmount(),
|
||||
tokenBalanceResponse?.await()?.result?.parseAmount(),
|
||||
txCountResponse.await().result?.responseToNumber()?.toLong() ?: 0,
|
||||
pendingTxCountResponse.await().result?.responseToNumber()?.toLong() ?: 0
|
||||
))
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
Result.Failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.parseFee(gasLimit: Long): List<BigDecimal> {
|
||||
val gasPrice = this.responseToNumber().toBigDecimal()
|
||||
val minFee = gasPrice.multiply(gasLimit.toBigDecimal())
|
||||
val normalFee = minFee.multiply(BigDecimal(1.2))
|
||||
val priorityFee = minFee.multiply(BigDecimal(1.5))
|
||||
return listOf(
|
||||
minFee.convertFeeToEth(),
|
||||
normalFee.convertFeeToEth(),
|
||||
priorityFee.convertFeeToEth()
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.responseToNumber(): BigInteger = this.substring(2).toBigInteger(16)
|
||||
|
||||
private fun String.parseAmount(): BigDecimal =
|
||||
this.responseToNumber().toBigDecimal().divide(ETH_IN_WEI.toBigDecimal())
|
||||
|
||||
private fun BigDecimal.convertFeeToEth(): BigDecimal {
|
||||
return this.divide(ETH_IN_WEI.toBigDecimal())
|
||||
.setScale(12, Blockchain.Ethereum.roundingMode()).stripTrailingZeros()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class EthereumResponse(
|
||||
val balance: BigDecimal,
|
||||
val tokenBalance: BigDecimal?,
|
||||
val txCount: Long,
|
||||
val pendingTxCount: Long
|
||||
)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.blockchain.ethereum.network
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface InfuraApi {
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST("v3/613a0b14833145968b1f656240c7d245")
|
||||
suspend fun postToInfura(@Body body: InfuraBody?): InfuraResponse
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InfuraBody(
|
||||
val jsonrpc: String = "2.0",
|
||||
val id: Int = 67,
|
||||
val method: String? = null,
|
||||
val params: List<Any> = listOf()
|
||||
)
|
||||
|
||||
data class EthCallParams(private val data: String, private val to: String)
|
||||
|
||||
enum class InfuraMethod(val value: String) {
|
||||
GET_BALANCE("eth_getBalance"),
|
||||
GET_TRANSACTION_COUNT("eth_getTransactionCount"),
|
||||
GET_PENDING_COUNT("eth_getPendingCount"),
|
||||
CALL("eth_call"),
|
||||
SEND_RAW_TRANSACTION("eth_sendRawTransaction"),
|
||||
GAS_PRICE("eth_gasPrice")
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.blockchain.ethereum.network
|
||||
|
||||
class InfuraProvider(private val api: InfuraApi) {
|
||||
suspend fun getBalance(address: String) = api.postToInfura(createInfuraBody(InfuraMethod.GET_BALANCE, address))
|
||||
suspend fun getTokenBalance(address: String, contractAddress: String) = api.postToInfura(createInfuraBody(InfuraMethod.CALL, address, contractAddress))
|
||||
suspend fun getTxCount(address: String) = api.postToInfura(createInfuraBody(InfuraMethod.GET_TRANSACTION_COUNT, address))
|
||||
suspend fun getPendingTxCount(address: String) = api.postToInfura(createInfuraBody(InfuraMethod.GET_PENDING_COUNT, address))
|
||||
suspend fun getGasPrice() = api.postToInfura(createInfuraBody(InfuraMethod.GAS_PRICE))
|
||||
suspend fun sendTransaction(transaction: String) = api.postToInfura(createInfuraBody(InfuraMethod.SEND_RAW_TRANSACTION, transaction = transaction))
|
||||
}
|
||||
|
||||
private fun createInfuraBody(
|
||||
method: InfuraMethod,
|
||||
address: String? = null,
|
||||
contractAddress: String? = null,
|
||||
transaction: String? = null): InfuraBody {
|
||||
|
||||
return when (method) {
|
||||
InfuraMethod.GET_BALANCE ->
|
||||
InfuraBody(method = InfuraMethod.GET_BALANCE.value, params = listOf(address ?: "", "latest"))
|
||||
InfuraMethod.GET_TRANSACTION_COUNT ->
|
||||
InfuraBody(method = InfuraMethod.GET_TRANSACTION_COUNT.value, params = listOf(address ?: "", "latest"))
|
||||
InfuraMethod.GET_PENDING_COUNT ->
|
||||
InfuraBody(method = InfuraMethod.GET_TRANSACTION_COUNT.value, params = listOf(address ?: "", "pending"))
|
||||
InfuraMethod.GAS_PRICE ->
|
||||
InfuraBody(method = InfuraMethod.GAS_PRICE.value)
|
||||
InfuraMethod.SEND_RAW_TRANSACTION ->
|
||||
InfuraBody(method = InfuraMethod.SEND_RAW_TRANSACTION.value, params = listOf(transaction ?: ""))
|
||||
InfuraMethod.CALL -> {
|
||||
InfuraBody(
|
||||
method = InfuraMethod.CALL.value,
|
||||
params = listOf(EthCallParams(
|
||||
"0x70a08231000000000000000000000000" + address?.substring(2), contractAddress
|
||||
?: ""),
|
||||
"latest"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.blockchain.ethereum.network
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InfuraResponse(
|
||||
@Json(name = "jsonrpc")
|
||||
val jsonrpc: String = "",
|
||||
|
||||
@Json(name = "id")
|
||||
val id: Int? = null,
|
||||
|
||||
@Json(name = "result")
|
||||
val result: String? = null,
|
||||
|
||||
@Json(name = "error")
|
||||
val error: InfuraError? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InfuraError(
|
||||
@Json(name = "code")
|
||||
val code: Int? = null,
|
||||
|
||||
@Json(name = "message")
|
||||
val message: String? = null
|
||||
)
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue