Updated on 2026-08-14

This commit is contained in:
Tangem 2020-02-03 18:24:58 +03:00
parent 05cc0946f1
commit 0ee3346f20
4 changed files with 303 additions and 0 deletions

View file

@ -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
}
}
}
}

View file

@ -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<StellarResponse> {
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
)

View file

@ -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<ByteArray> {
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<ByteArray> {
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<ByteArray>(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()
}
}

View file

@ -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<List<Amount>> {
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()
}
}