From 0ee3346f2098bf24df6005d367c5825a5ab096fb Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 3 Feb 2020 18:24:58 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../blockchain/stellar/StellarAddress.kt | 24 +++++ .../stellar/StellarNetworkManager.kt | 100 ++++++++++++++++++ .../stellar/StellarTransactionBuilder.kt | 84 +++++++++++++++ .../stellar/StellarWalletManager.kt | 95 +++++++++++++++++ 4 files changed, 303 insertions(+) create mode 100644 blockchain/src/main/java/com/tangem/blockchain/stellar/StellarAddress.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/stellar/StellarNetworkManager.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/stellar/StellarTransactionBuilder.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/stellar/StellarWalletManager.kt diff --git a/blockchain/src/main/java/com/tangem/blockchain/stellar/StellarAddress.kt b/blockchain/src/main/java/com/tangem/blockchain/stellar/StellarAddress.kt new file mode 100644 index 0000000000..605d8f3e2e --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/stellar/StellarAddress.kt @@ -0,0 +1,24 @@ +package com.tangem.blockchain.stellar + +import org.stellar.sdk.KeyPair + +class StellarAddressFactory { + companion object { + fun makeAddress(cardPublicKey: ByteArray): String { + val kp = KeyPair.fromPublicKey(cardPublicKey) + return kp.accountId + } + } +} + +class StellarAddressValidator { + companion object { + fun validate(address: String): Boolean { + return try { + KeyPair.fromAccountId(address) != null + } catch (exception: IllegalArgumentException) { + false + } + } + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/stellar/StellarNetworkManager.kt b/blockchain/src/main/java/com/tangem/blockchain/stellar/StellarNetworkManager.kt new file mode 100644 index 0000000000..87102f42e6 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/stellar/StellarNetworkManager.kt @@ -0,0 +1,100 @@ +package com.tangem.blockchain.stellar + +import com.tangem.blockchain.common.extensions.Result +import com.tangem.blockchain.common.extensions.SimpleResult +import com.tangem.blockchain.common.network.API_STELLAR +import com.tangem.blockchain.common.network.API_STELLAR_TESTNET +import com.tangem.blockchain.stellar.StellarWalletManager.Companion.STROOPS_IN_XLM +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import org.stellar.sdk.Network +import org.stellar.sdk.Server +import org.stellar.sdk.Transaction +import org.stellar.sdk.requests.ErrorResponse +import java.io.IOException +import java.math.BigDecimal + +class StellarNetworkManager(isTestNet: Boolean) { + + val network: Network = if (isTestNet) Network.TESTNET else Network.PUBLIC + private val stellarServer by lazy { + Server(if (isTestNet) API_STELLAR_TESTNET else API_STELLAR) + } + + suspend fun sendTransaction(transaction: String): SimpleResult { + return try { + val response = stellarServer.submitTransaction(Transaction.fromEnvelopeXdr(transaction, network)) + if (response.isSuccess) { + SimpleResult.Success + } else { + val trResult: String? = response.extras?.resultCodes?.transactionResultCode + + (response.extras?.resultCodes?.operationsResultCodes?.getOrNull(0) ?: "") + SimpleResult.Failure(Exception(trResult ?: "transaction failed")) + } + } catch (error: Exception) { + SimpleResult.Failure(error) + } + } + + suspend fun checkIsAccountCreated(address: String): Boolean { + try { + stellarServer.accounts().account(address) + return true + } catch (errorResponse: ErrorResponse) { + if (errorResponse.code == 404) return false + return false + } catch (exception: IOException) { + return false + } + } + + suspend fun getInfo(accountId: String, assetCode: String? = null): Result { + return try { + coroutineScope { + val accountResponseDefered = async(Dispatchers.IO) { stellarServer.accounts().account(accountId) } + val ledgerResponseDeferred = async(Dispatchers.IO) { + val latestLedger: Int = stellarServer.root().coreLatestLedger + stellarServer.ledgers().ledger(latestLedger.toLong()) + } + + val accountResponse = accountResponseDefered.await() + val balance = accountResponse.balances + .find { it.assetType == "native" } + ?.balance?.toBigDecimal() + ?: return@coroutineScope Result.Failure(Exception("Stellar Balance not found")) + val assetBalance = if (assetCode == null) { + null + } else { + accountResponse.balances + .find { it.assetType != "native" && it.assetCode == assetCode } + ?.balance?.toBigDecimal() + ?: return@coroutineScope Result.Failure(Exception("Stellar Balance not found")) + } + val sequence = accountResponse.sequenceNumber + + val ledgerResponse = ledgerResponseDeferred.await() + val baseFee = ledgerResponse.baseFeeInStroops.toBigDecimal().divide(STROOPS_IN_XLM) + val baseReserve = ledgerResponse.baseReserveInStroops.toBigDecimal().divide(STROOPS_IN_XLM) + + Result.Success(StellarResponse( + baseFee, + baseReserve, + assetBalance, + balance, + sequence + )) + } + } catch (error: Exception) { + Result.Failure(error) + } + } +} + +data class StellarResponse( + val baseFee: BigDecimal, + val baseReserve: BigDecimal, + val assetBalance: BigDecimal?, + val balance: BigDecimal, + val sequence: Long +) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/stellar/StellarTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/stellar/StellarTransactionBuilder.kt new file mode 100644 index 0000000000..b234ef4f60 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/stellar/StellarTransactionBuilder.kt @@ -0,0 +1,84 @@ +package com.tangem.blockchain.stellar + +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.stellar.StellarWalletManager.Companion.BASE_FEE +import com.tangem.blockchain.stellar.StellarWalletManager.Companion.STROOPS_IN_XLM +import com.tangem.common.extensions.hexToBytes +import org.stellar.sdk.* +import org.stellar.sdk.xdr.AccountID +import org.stellar.sdk.xdr.DecoratedSignature +import org.stellar.sdk.xdr.Signature +import org.stellar.sdk.xdr.SignatureHint +import java.util.* + +class StellarTransactionBuilder(private val newtorkManager: StellarNetworkManager, private val publicKey: ByteArray) { + + private lateinit var transaction: Transaction + + suspend fun buildToSign(transactionData: TransactionData, sequence: Long, fee: Int): List { + + val destinationKeyPair = KeyPair.fromAccountId(transactionData.destinationAddress) + val sourceKeyPair = KeyPair.fromAccountId(transactionData.sourceAddress) + + if (transactionData.amount.type == AmountType.Coin) { + val operation = if (newtorkManager.checkIsAccountCreated(transactionData.sourceAddress)) { + PaymentOperation.Builder(destinationKeyPair.accountId, + AssetTypeNative(), + transactionData.amount.value.toString()) + .build() + } else { + CreateAccountOperation.Builder(destinationKeyPair.accountId, transactionData.amount.value.toString()).build() + } + return serializeOperation(operation, sourceKeyPair, sequence, fee) + + } else if (transactionData.amount.type == AmountType.Token) { + val keyPair = KeyPair.fromAccountId(transactionData.amount.address) + val asset = Asset.createNonNativeAsset(transactionData.amount.currencySymbol, keyPair.accountId) + val operation: Operation = if (transactionData.amount.value != null) { + PaymentOperation.Builder( + destinationKeyPair.accountId, + asset, + transactionData.amount.value!!.toPlainString()) + .build() + } else { + ChangeTrustOperation.Builder(asset, "900000000000.0000000") + .setSourceAccount(sourceKeyPair.accountId) + .build() + } + return serializeOperation(operation, sourceKeyPair, sequence, fee) + } else { + return emptyList() + } + } + + private fun serializeOperation( + operation: Operation, sourceKeyPair: KeyPair, + sequence: Long, fee: Int + ): List { + + val accountID = AccountID() + accountID.accountID = sourceKeyPair.xdrPublicKey + val currentTime = Calendar.getInstance().timeInMillis / 1000 + val minTime = 0L + val maxTime = currentTime + 60 + + transaction = Transaction.Builder( + Account(sourceKeyPair.accountId, sequence), newtorkManager.network) + .addOperation(operation) + .addTimeBounds(TimeBounds(minTime, maxTime)) + .setOperationFee(fee) + .build() + return listOf(transaction.hash()) + } + + fun buildToSend(signature: ByteArray): String { + val hint = publicKey.takeLast(4).toByteArray() + val decoratedSignature = DecoratedSignature().apply { + this.hint = SignatureHint().apply { signatureHint = hint } + this.signature = Signature().apply { this.signature = signature } + } + transaction.signatures.add(decoratedSignature) + return transaction.toEnvelopeXdrBase64() + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/stellar/StellarWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/stellar/StellarWalletManager.kt new file mode 100644 index 0000000000..a8b02ccf97 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/stellar/StellarWalletManager.kt @@ -0,0 +1,95 @@ +package com.tangem.blockchain.stellar + +import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.extensions.Result +import com.tangem.blockchain.common.extensions.SimpleResult +import com.tangem.blockchain.wallets.CurrencyWallet +import com.tangem.tasks.TaskEvent +import java.math.BigDecimal +import java.util.* + +class StellarWalletManager( + private val cardId: String, + walletPublicKey: ByteArray, + walletConfig: WalletConfig, + token: Token? = null, + isTestNet: Boolean = false +) : WalletManager, + TransactionSender, + FeeProvider { + + override val blockchain: Blockchain = Blockchain.Stellar + private val address = blockchain.makeAddress(walletPublicKey) + private val currencyWallet = CurrencyWallet(walletConfig, address) + override var wallet: Wallet = currencyWallet + private val networkManager = StellarNetworkManager(isTestNet) + private val builder = StellarTransactionBuilder(networkManager, walletPublicKey) + private var baseFee = BASE_FEE + private var baseReserve = BASE_RESERVE + private var sequence = 0L + + init { + if (token != null) currencyWallet.balances[AmountType.Token] = + Amount( + token.symbol, + null, + token.contractAddress, + token.decimals, + AmountType.Token) + } + + 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: StellarResponse) { + currencyWallet.balances[AmountType.Coin]?.value = data.balance + currencyWallet.balances[AmountType.Token]?.value = data.assetBalance + currencyWallet.balances[AmountType.Reserve]?.value = data.baseReserve + sequence = data.sequence + baseFee = data.baseFee + baseReserve = data.baseReserve + + val currentTime = Calendar.getInstance().timeInMillis + currencyWallet.pendingTransactions.forEach { transaction -> + if (transaction.date?.timeInMillis ?: 0 - currentTime > 10) { + transaction.status = TransactionStatus.Confirmed + } + } + } + + private fun updateError(error: Throwable?) { + + } + + override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { + val hashes = builder.buildToSign(transactionData, sequence, baseFee.toStroops()) + when (val signerResponse = signer.sign(hashes.toTypedArray(), cardId)) { + is TaskEvent.Event -> { + val transactionToSend = builder.buildToSend(signerResponse.data.signature) + return networkManager.sendTransaction(transactionToSend) + } + is TaskEvent.Completion -> return SimpleResult.Failure(signerResponse.error) + } + } + + override suspend fun getFee(amount: Amount, source: String, destination: String): Result> { + return Result.Success(listOf( + Amount(baseFee, blockchain) + )) + } + + private fun BigDecimal.toStroops(): Int { + return this.multiply(STROOPS_IN_XLM).toInt() + } + + companion object { + val STROOPS_IN_XLM = 10000000.toBigDecimal() + val BASE_FEE = 0.00001.toBigDecimal() + val BASE_RESERVE = 0.5.toBigDecimal() + } +}