Updated on 2026-08-14
This commit is contained in:
parent
05cc0946f1
commit
0ee3346f20
4 changed files with 303 additions and 0 deletions
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue