Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AND-9119 [Staking] Implemented staking Polkadot. BSDK part #848

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.tangem.blockchain.blockchains.polkadot

import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.squareup.moshi.adapter
import com.tangem.blockchain.blockchains.polkadot.extensions.makeEraFromBlockNumber
import com.tangem.blockchain.blockchains.polkadot.models.PolkadotCompiledTransaction
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.hexToBigInteger
import com.tangem.blockchain.extensions.hexToInt
import com.tangem.blockchain.network.moshi
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.hexToBytes
Expand Down Expand Up @@ -44,14 +47,17 @@ class PolkadotTransactionBuilder(private val blockchain: Blockchain) {
)
}

@OptIn(ExperimentalStdlibApi::class)
private val compiledTransactionAdapter by lazy { moshi.adapter<PolkadotCompiledTransaction>() }

fun buildForSign(destinationAddress: String, amount: Amount, context: ExtrinsicContext): ByteArray {
val buffer = ByteArrayOutputStream()
val codecWriter = ScaleCodecWriter(buffer)

encodeCall(codecWriter, amount, Address.from(destinationAddress), context.runtimeVersion)
encodeEraNonceTip(codecWriter, context)

encodeCheckMetadataHashMode(codecWriter, context)
encodeCheckMetadataHashMode(codecWriter, context.runtimeVersion)

codecWriter.writeUint32(context.runtimeVersion)
codecWriter.writeUint32(context.txVersion)
Expand All @@ -62,7 +68,30 @@ class PolkadotTransactionBuilder(private val blockchain: Blockchain) {
codecWriter.writeUint256(context.eraBlockHash.bytes)
}

encodeCheckMetadataHash(codecWriter, context)
encodeCheckMetadataHash(codecWriter, context.runtimeVersion)

return buffer.toByteArray()
}

fun buildForSignCompiled(transaction: TransactionData.Compiled): ByteArray {
val compiledTransaction = (transaction.value as? TransactionData.Compiled.Data.RawString)?.data
?: error("Compiled transaction must be in hex format")

val parsed = compiledTransactionAdapter.fromJson(compiledTransaction)
?: error("Unable to parse compiled transaction")
val tx = parsed.tx

val buffer = ByteArrayOutputStream()
val codecWriter = ScaleCodecWriter(buffer)

codecWriter.writeByteArray(tx.method.hexToBytes())
encodeEraNonceTip(codecWriter, tx)
encodeCheckMetadataHashMode(codecWriter, tx.specVersion.hexToInt())
codecWriter.writeUint32(tx.specVersion.hexToInt())
codecWriter.writeUint32(tx.transactionVersion.hexToInt())
codecWriter.writeByteArray(tx.genesisHash.hexToBytes())
codecWriter.writeByteArray(tx.blockHash.hexToBytes())
encodeCheckMetadataHash(codecWriter, tx.specVersion.hexToInt())

return buffer.toByteArray()
}
Expand All @@ -88,7 +117,7 @@ class PolkadotTransactionBuilder(private val blockchain: Blockchain) {
codecWriter.writeByteArray(signature.value.bytes)

encodeEraNonceTip(codecWriter, context)
encodeCheckMetadataHashMode(codecWriter, context)
encodeCheckMetadataHashMode(codecWriter, context.runtimeVersion)
encodeCall(codecWriter, amount, Address.from(destinationAddress), context.runtimeVersion)

val prefixBuffer = ByteArrayOutputStream()
Expand All @@ -97,6 +126,37 @@ class PolkadotTransactionBuilder(private val blockchain: Blockchain) {
return prefixBuffer.toByteArray() + txBuffer.toByteArray()
}

@Suppress("MagicNumber")
fun buildForSendCompiled(transaction: TransactionData.Compiled, signedPayload: ByteArray): ByteArray {
val compiledTransaction = (transaction.value as? TransactionData.Compiled.Data.RawString)?.data
?: error("Compiled transaction must be in hex format")

val parsed = compiledTransactionAdapter.fromJson(compiledTransaction)
?: error("Unable to parse compiled transaction")

val tx = parsed.tx

val type = Extrinsic.TYPE_BIT_SIGNED + (Extrinsic.TYPE_UNMASK_VERSION and 4)
val hash512 = Hash512(signedPayload)
val signature = Extrinsic.ED25519Signature(hash512)

val buffer = ByteArrayOutputStream()
val codecWriter = ScaleCodecWriter(buffer)

codecWriter.writeByte(type)
encodeAddress(codecWriter, Address.from(tx.address), tx.specVersion.hexToInt())
codecWriter.writeByte(signature.type.code)
codecWriter.writeByteArray(signature.value.bytes)
encodeEraNonceTip(codecWriter, tx)
encodeCheckMetadataHashMode(codecWriter, tx.specVersion.hexToInt())
codecWriter.writeByteArray(tx.method.hexToBytes())

val prefixBuffer = ByteArrayOutputStream()
ScaleCodecWriter(prefixBuffer).write(ScaleCodecWriter.COMPACT_UINT, buffer.size())

return prefixBuffer.toByteArray() + buffer.toByteArray()
}

private fun encodeCall(
codecWriter: ScaleCodecWriter,
amount: Amount,
Expand All @@ -116,14 +176,21 @@ class PolkadotTransactionBuilder(private val blockchain: Blockchain) {
codecWriter.write(ScaleCodecWriter.COMPACT_BIGINT, context.tip.value)
}

private fun encodeCheckMetadataHashMode(codecWriter: ScaleCodecWriter, context: ExtrinsicContext) {
if (shouldUseCheckMetadataHash(specVersion = context.runtimeVersion)) {
private fun encodeEraNonceTip(codecWriter: ScaleCodecWriter, compiledTx: PolkadotCompiledTransaction.Inner) {
dbaturin marked this conversation as resolved.
Show resolved Hide resolved
val era = makeEraFromBlockNumber(compiledTx.blockNumber.hexToBigInteger().toLong())
codecWriter.write(EraWriter(), era.toInteger())
codecWriter.write(ScaleCodecWriter.COMPACT_BIGINT, compiledTx.nonce.hexToBigInteger())
codecWriter.write(ScaleCodecWriter.COMPACT_BIGINT, compiledTx.tip.hexToBigInteger())
}

private fun encodeCheckMetadataHashMode(codecWriter: ScaleCodecWriter, specVersion: Int) {
iMaks99 marked this conversation as resolved.
Show resolved Hide resolved
if (shouldUseCheckMetadataHash(specVersion = specVersion)) {
codecWriter.write(ScaleCodecWriter.BOOL, false)
}
}

private fun encodeCheckMetadataHash(codecWriter: ScaleCodecWriter, context: ExtrinsicContext) {
if (shouldUseCheckMetadataHash(specVersion = context.runtimeVersion)) {
private fun encodeCheckMetadataHash(codecWriter: ScaleCodecWriter, specVersion: Int) {
iMaks99 marked this conversation as resolved.
Show resolved Hide resolved
if (shouldUseCheckMetadataHash(specVersion = specVersion)) {
codecWriter.writeByte(0x0.toByte())
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.tangem.blockchain.blockchains.polkadot

import com.tangem.blockchain.blockchains.polkadot.extensions.makeEraFromBlockNumber
import com.tangem.blockchain.blockchains.polkadot.network.PolkadotNetworkProvider
import com.tangem.blockchain.blockchains.polkadot.network.accounthealthcheck.AccountResponse
import com.tangem.blockchain.blockchains.polkadot.network.accounthealthcheck.ExtrinsicDetailResponse
Expand All @@ -10,13 +11,15 @@ import com.tangem.blockchain.common.BlockchainSdkError.UnsupportedOperation
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.common.transaction.TransactionsSendResult
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.successOr
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import io.emeraldpay.polkaj.tx.Era
import io.emeraldpay.polkaj.tx.ExtrinsicContext
import io.emeraldpay.polkaj.types.Hash256
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import java.math.BigDecimal
import java.util.Calendar
import java.util.EnumSet
Expand Down Expand Up @@ -135,6 +138,52 @@ internal class PolkadotWalletManager(
}
}

override suspend fun sendMultiple(
transactionDataList: List<TransactionData>,
signer: TransactionSigner,
): Result<TransactionsSendResult> {
val compiledTransactionList = transactionDataList.map {
it.requireCompiled()
}

val signResult = signMultipleCompiledTransactionData(
compiledTransactionList = compiledTransactionList,
signer = signer,
publicKey = wallet.publicKey,
)

return when (signResult) {
is Result.Failure -> Result.Failure(signResult.error)
is Result.Success -> {
coroutineScope {
val sendResults = signResult.data.mapIndexed { index, signedData ->
if (index != 0) {
delay(SEND_TRANSACTIONS_DELAY)
}

when (val sendResult = networkProvider.sendTransaction(signedData)) {
is Result.Failure -> sendResult
is Result.Success -> {
val hash = sendResult.data.formattedHash()
transactionDataList[index].hash = hash
wallet.addOutgoingTransaction(transactionDataList[index].updateHash(hash = hash))
Result.Success(TransactionSendResult(hash))
}
}
}
val failedResult = sendResults.firstOrNull { it is Result.Failure }
if (failedResult != null) {
Result.Failure((failedResult as Result.Failure).error)
} else {
Result.Success(
TransactionsSendResult(sendResults.mapNotNull { (it as? Result.Success)?.data?.hash }),
)
}
}
}
}
}

override suspend fun getExtrinsicList(afterExtrinsicId: Long?): ExtrinsicListResponse {
if (extrinsicCheckNetworkProvider == null) {
error("extrinsicCheckNetworkProvider is not supported")
Expand Down Expand Up @@ -164,7 +213,7 @@ internal class PolkadotWalletManager(
private suspend fun updateEra() {
val latestBlockHash = networkProvider.getLatestBlockHash().successOr { error("latestBlockHash error") }
val blockNumber = networkProvider.getBlockNumber(latestBlockHash).successOr { error("blockNumber error") }
currentContext.era = Era.Mortal.forCurrent(TRANSACTION_LIFE_PERIOD, blockNumber.toLong())
currentContext.era = makeEraFromBlockNumber(blockNumber.toLong())
currentContext.eraBlockHash = Hash256(latestBlockHash.hexToBytes())
}

Expand Down Expand Up @@ -227,6 +276,32 @@ internal class PolkadotWalletManager(
}
}

private suspend fun signMultipleCompiledTransactionData(
compiledTransactionList: List<TransactionData.Compiled>,
signer: TransactionSigner,
publicKey: Wallet.PublicKey,
): Result<List<ByteArray>> {
val builtTxForSignList = compiledTransactionList.map {
txBuilder.buildForSignCompiled(it)
}

return when (val signResults = signer.sign(builtTxForSignList, publicKey)) {
is CompletionResult.Success -> {
val builtForSend = signResults.data.mapIndexed { index, hash ->
txBuilder.buildForSendCompiled(
transaction = compiledTransactionList[index],
signedPayload = hash,
)
}
Result.Success(builtForSend)
}

is CompletionResult.Failure -> {
Result.fromTangemSdkError(signResults.error)
}
}
}

private suspend fun isAccountUnderfunded(address: String): Result<Boolean> {
val destinationBalance = networkProvider.getBalance(address).successOr { return it }
val isUnderfunded =
Expand All @@ -242,7 +317,6 @@ internal class PolkadotWalletManager(
}

private companion object {

const val TRANSACTION_LIFE_PERIOD = 128L
const val SEND_TRANSACTIONS_DELAY = 5_000L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.tangem.blockchain.blockchains.polkadot.extensions

import io.emeraldpay.polkaj.tx.Era

private const val TRANSACTION_LIFE_PERIOD = 128L

internal fun makeEraFromBlockNumber(blockNumber: Long): Era {
return Era.Mortal.forCurrent(TRANSACTION_LIFE_PERIOD, blockNumber)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.tangem.blockchain.blockchains.polkadot.models

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* Model of filled Polkadot transaction used in staking
*/
@JsonClass(generateAdapter = true)
internal data class PolkadotCompiledTransaction(
@Json(name = "tx")
val tx: Inner,
@Json(name = "specName")
val specName: String,
@Json(name = "specVersion")
val specVersion: String,
@Json(name = "metadataRpc")
val metadataRpc: String,
) {
@JsonClass(generateAdapter = true)
data class Inner(
@Json(name = "address")
val address: String,
@Json(name = "blockHash")
val blockHash: String,
@Json(name = "blockNumber")
val blockNumber: String,
@Json(name = "era")
val era: String,
@Json(name = "genesisHash")
val genesisHash: String,
@Json(name = "metadataRpc")
val metadataRpc: String,
@Json(name = "method")
val method: String,
@Json(name = "nonce")
val nonce: String,
@Json(name = "signedExtensions")
val signedExtensions: List<String>,
@Json(name = "specVersion")
val specVersion: String,
@Json(name = "tip")
val tip: String,
@Json(name = "transactionVersion")
val transactionVersion: String,
@Json(name = "version")
val version: Int,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.logging.AddHeaderInterceptor
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.orZero
import com.tangem.blockchain.extensions.successOr
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.common.extensions.toHexString
Expand Down Expand Up @@ -41,11 +42,29 @@ internal class PolkadotCombinedProvider(
private val commands = StandardCommands.getInstance()
private val ss58Network = PolkadotAddressService(blockchain).ss58Network

/**
* **Free** is the balance that can be used for on-chain activity like staking,
* participating in governance etc. but is not necessarily spendable (or transferrable)
*
* **Spendable** is the free balance that can be spent
*
* spendable = free - max(frozen - on_hold, ED)
* In our case Existential deposit (ED) will be calculated via ExistentialDepositProvider
*
* For more info https://wiki.polkadot.network/docs/learn-account-balances
*/
override suspend fun getBalance(address: String): Result<BigDecimal> = withContext(Dispatchers.IO) {
val accountInfo = getAccountInfo(Address.from(address)).successOr { return@withContext it }

val amount = accountInfo?.data?.free?.toBigDecimal(decimals) ?: BigDecimal.ZERO
Result.Success(amount)
val balance = accountInfo?.data?.let { data ->
val freeAmount = data.free?.toBigDecimal(decimals).orZero()
val frozenAmount = data.miscFrozen?.toBigDecimal(decimals).orZero()
val heldAmount = data.reserved?.toBigDecimal(decimals).orZero()

(freeAmount - (frozenAmount - heldAmount)).coerceAtLeast(BigDecimal.ZERO)
} ?: BigDecimal.ZERO

Result.Success(balance)
}

private suspend fun getAccountInfo(address: Address): Result<AccountInfo?> = withContext(Dispatchers.IO) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ import java.math.BigDecimal
fun max(a: BigDecimal, b: BigDecimal): BigDecimal {
return if (a > b) a else b
}

fun BigDecimal?.orZero() = this ?: BigDecimal.ZERO
Loading
Loading