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