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

IOS-7797 [Staking] StakeKitTransactionSender use async/await #822

Merged
6 changes: 5 additions & 1 deletion BlockchainSdk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,7 @@
EF5C753E2BAC4D6B00811198 /* URLSessionTask.State+CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF5C753D2BAC4D6B00811198 /* URLSessionTask.State+CustomStringConvertible.swift */; };
EF5C75402BAC4DB500811198 /* URLSessionWebSocketTask.CloseCode+CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF5C753F2BAC4DB500811198 /* URLSessionWebSocketTask.CloseCode+CustomStringConvertible.swift */; };
EF5F1B9729C9C0500093307B /* Fee.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF5F1B9629C9C0500093307B /* Fee.swift */; };
EF5F545F2C8718F000B7D1E7 /* StakeKitTransactionSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF5F545E2C8718F000B7D1E7 /* StakeKitTransactionSender.swift */; };
EF666C202A288AEF0044986F /* AdaliteTokenDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF666C1F2A288AEF0044986F /* AdaliteTokenDTO.swift */; };
EF666C2A2A2DFB120044986F /* WalletManagerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF666C292A2DFB120044986F /* WalletManagerFactory.swift */; };
EF666C2E2A2F3EE90044986F /* BitcoinLegacyAddressService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF666C2D2A2F3EE90044986F /* BitcoinLegacyAddressService.swift */; };
Expand Down Expand Up @@ -1757,6 +1758,7 @@
EF5C753D2BAC4D6B00811198 /* URLSessionTask.State+CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionTask.State+CustomStringConvertible.swift"; sourceTree = "<group>"; };
EF5C753F2BAC4DB500811198 /* URLSessionWebSocketTask.CloseCode+CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionWebSocketTask.CloseCode+CustomStringConvertible.swift"; sourceTree = "<group>"; };
EF5F1B9629C9C0500093307B /* Fee.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fee.swift; sourceTree = "<group>"; };
EF5F545E2C8718F000B7D1E7 /* StakeKitTransactionSender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StakeKitTransactionSender.swift; sourceTree = "<group>"; };
EF666C1F2A288AEF0044986F /* AdaliteTokenDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaliteTokenDTO.swift; sourceTree = "<group>"; };
EF666C292A2DFB120044986F /* WalletManagerFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletManagerFactory.swift; sourceTree = "<group>"; };
EF666C2D2A2F3EE90044986F /* BitcoinLegacyAddressService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinLegacyAddressService.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4133,12 +4135,13 @@
EF9E380E2B7A3A41004B694C /* Interfaces */ = {
isa = PBXGroup;
children = (
EF5F545E2C8718F000B7D1E7 /* StakeKitTransactionSender.swift */,
5D14E48F2397B87000C15FC8 /* WalletManager.swift */,
DA2E76FF29769BF5001FF957 /* TransactionCreator.swift */,
EF23FBD72BB6EB45008DBCDF /* TransactionFeeProvider.swift */,
DA4B80242BBA66F900CE50B7 /* BitcoinTransactionFeeCalculator.swift */,
EF937BD92B90EED0009B8374 /* BlockchainNotifications */,
EF9E382E2B7A4CF3004B694C /* TransactionValidator */,
DA4B80242BBA66F900CE50B7 /* BitcoinTransactionFeeCalculator.swift */,
);
path = Interfaces;
sourceTree = "<group>";
Expand Down Expand Up @@ -4585,6 +4588,7 @@
DA0E98D42C0472DB00C92985 /* KoinosTarget.swift in Sources */,
5DF9151D253DB85500B927DD /* TezosNetworkService.swift in Sources */,
5DEAFA9224447D350032E316 /* ThenProcessable.swift in Sources */,
EF5F545F2C8718F000B7D1E7 /* StakeKitTransactionSender.swift in Sources */,
2DA13A022A8AD83B00E3C374 /* ChiaNetworkProvider.swift in Sources */,
DAB9BE722BBD902C00E77545 /* TaraxaExternalLinkProvider.swift in Sources */,
5DCC817623FAE6D500DFC460 /* AnyPublisher+Response.swift in Sources */,
Expand Down
54 changes: 18 additions & 36 deletions BlockchainSdk/Blockchains/Cosmos/CosmosWalletManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,42 +198,24 @@ class CosmosWalletManager: BaseManager, WalletManager {

extension CosmosWalletManager: ThenProcessable { }

extension CosmosWalletManager: StakeKitTransactionSender {
func sendStakeKit(
transaction: StakeKitTransaction,
signer: any TransactionSigner
) -> AnyPublisher<TransactionSendResult, SendTxError> {
let helper = CosmosStakeKitTransactionHelper(builder: txBuilder)
// MARK: - StakeKitTransactionSender, StakeKitTransactionSenderProvider

return Result {
try helper.prepareForSign(stakingTransaction: transaction)
}
.publisher
.withWeakCaptureOf(self)
.flatMap { manager, hash in
signer
.sign(hash: hash, walletPublicKey: self.wallet.publicKey)
.tryMap { signature -> Data in
let signature = try Secp256k1Signature(with: signature)
return try signature.unmarshal(with: manager.wallet.publicKey.blockchainKey, hash: hash).data
}
}
.tryMap { signature -> Data in
try helper.buildForSend(stakingTransaction: transaction, signature: signature)
}
.withWeakCaptureOf(self)
.flatMap { manager, transaction in
manager.networkService
.send(transaction: transaction)
.mapSendError(tx: transaction.hexString.lowercased())
}
.handleEvents(receiveOutput: { [weak self] hash in
let mapper = PendingTransactionRecordMapper()
let record = mapper.mapToPendingTransactionRecord(stakeKitTransaction: transaction, hash: hash)
self?.wallet.addPendingTransaction(record)
})
.map { TransactionSendResult(hash: $0) }
.eraseSendError()
.eraseToAnyPublisher()
extension CosmosWalletManager: StakeKitTransactionSender, StakeKitTransactionSenderProvider {
typealias RawTransaction = Data

func prepareDataForSign(transaction: StakeKitTransaction) throws -> Data {
try CosmosStakeKitTransactionHelper(builder: txBuilder)
.prepareForSign(stakingTransaction: transaction)
}

func prepareDataForSend(transaction: StakeKitTransaction, signature: SignatureInfo) throws -> RawTransaction {
let unmarshal = try signature.unmarshal()

return try CosmosStakeKitTransactionHelper(builder: txBuilder)
.buildForSend(stakingTransaction: transaction, signature: unmarshal)
}

func broadcast(transaction: StakeKitTransaction, rawTransaction: RawTransaction) async throws -> String {
try await networkService.send(transaction: rawTransaction).async()
}
}
52 changes: 16 additions & 36 deletions BlockchainSdk/Blockchains/Ethereum/EthereumWalletManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -406,44 +406,24 @@ extension EthereumWalletManager: EthereumTransactionDataBuilder {
}
}

extension EthereumWalletManager: StakeKitTransactionSender {
func sendStakeKit(
transaction: StakeKitTransaction,
signer: any TransactionSigner
) -> AnyPublisher<TransactionSendResult, SendTxError> {
let helper = EthereumStakeKitTransactionHelper(transactionBuilder: txBuilder)
return Result {
try helper.prepareForSign(transaction)
}
.publisher
.withWeakCaptureOf(self)
.flatMap { walletManager, hashToSign in
signer.sign(hash: hashToSign, walletPublicKey: walletManager.wallet.publicKey)
}
.withWeakCaptureOf(self)
.tryMap { walletManager, signatureInfo -> String in
try helper.prepareForSend(
stakingTransaction: transaction,
signatureInfo: signatureInfo
)
// MARK: - StakeKitTransactionSender, StakeKitTransactionSenderProvider

extension EthereumWalletManager: StakeKitTransactionSender, StakeKitTransactionSenderProvider {
typealias RawTransaction = String

func prepareDataForSign(transaction: StakeKitTransaction) throws -> Data {
try EthereumStakeKitTransactionHelper(transactionBuilder: txBuilder).prepareForSign(transaction)
}

func prepareDataForSend(transaction: StakeKitTransaction, signature: SignatureInfo) throws -> RawTransaction {
try EthereumStakeKitTransactionHelper(transactionBuilder: txBuilder)
.prepareForSend(stakingTransaction: transaction, signatureInfo: signature)
.hexString
.lowercased()
.addHexPrefix()
}
.withWeakCaptureOf(self)
.flatMap { walletManager, rawTransaction in
walletManager.networkService.send(transaction: rawTransaction)
.mapSendError(tx: rawTransaction)
}
.withWeakCaptureOf(self)
.tryMap { walletManager, hash in
let mapper = PendingTransactionRecordMapper()
let record = mapper.mapToPendingTransactionRecord(stakeKitTransaction: transaction, hash: hash)
walletManager.wallet.addPendingTransaction(record)

return TransactionSendResult(hash: hash)
}
.eraseSendError()
.eraseToAnyPublisher()
}

func broadcast(transaction: StakeKitTransaction, rawTransaction: RawTransaction) async throws -> String {
try await networkService.send(transaction: rawTransaction).async()
}
}
37 changes: 12 additions & 25 deletions BlockchainSdk/Blockchains/Solana/SolanaWalletManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -307,33 +307,20 @@ private extension SolanaWalletManager {
}
}

// MARK: - StakeKitTransactionSender, StakeKitTransactionSenderProvider

// MARK: - StakeKitTransactionSender
extension SolanaWalletManager: StakeKitTransactionSender, StakeKitTransactionSenderProvider {
typealias RawTransaction = String

extension SolanaWalletManager: StakeKitTransactionSender {
func sendStakeKit(transaction: StakeKitTransaction, signer: any TransactionSigner) -> AnyPublisher<TransactionSendResult, SendTxError> {
let helper = SolanaStakeKitTransactionHelper()
func prepareDataForSign(transaction: StakeKitTransaction) throws -> Data {
SolanaStakeKitTransactionHelper().prepareForSign(transaction.unsignedData)
}

func prepareDataForSend(transaction: StakeKitTransaction, signature: SignatureInfo) throws -> RawTransaction {
SolanaStakeKitTransactionHelper().prepareForSend(transaction.unsignedData, signature: signature.signature)
}

return Just(helper.prepareForSign(transaction.unsignedData))
.withWeakCaptureOf(self)
.flatMap { manager, txToSign in
signer.sign(hash: txToSign, walletPublicKey: manager.wallet.publicKey)
}
.map { signature in
helper.prepareForSend(transaction.unsignedData, signature: signature)
}
.withWeakCaptureOf(self)
.flatMap { manager, txToSend in
manager.networkService.sendRaw(base64serializedTransaction: txToSend)
}
.withWeakCaptureOf(self)
.map { manager, hash in
let mapper = PendingTransactionRecordMapper()
let record = mapper.mapToPendingTransactionRecord(stakeKitTransaction: transaction, hash: hash)
manager.wallet.addPendingTransaction(record)
return TransactionSendResult(hash: hash)
}
.eraseSendError()
.eraseToAnyPublisher()
func broadcast(transaction: StakeKitTransaction, rawTransaction: RawTransaction) async throws -> String {
try await networkService.sendRaw(base64serializedTransaction: rawTransaction).async()
}
}
78 changes: 13 additions & 65 deletions BlockchainSdk/Blockchains/Tron/TronWalletManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,75 +259,23 @@ extension TronWalletManager: TronTransactionDataBuilder {
}
}

// MARK: - StakeKitTransactionSender
// MARK: - StakeKitTransactionSender, StakeKitTransactionSenderProvider

extension TronWalletManager: StakeKitTransactionSender {
func sendStakeKit(transactions: [StakeKitTransaction], signer: TransactionSigner) -> AnyPublisher<[TransactionSendResult], SendTxError> {
extension TronWalletManager: StakeKitTransactionSender, StakeKitTransactionSenderProvider {
typealias RawTransaction = Data

let presignedInputsPublisher = Result {
try transactions.map {
try TronStakeKitTransactionHelper().prepareForSign($0.unsignedData)
}
}.publisher

let readyToSendTransactionsPublisher = presignedInputsPublisher
.withWeakCaptureOf(self)
.flatMap { manager, inputs -> AnyPublisher<[Data], Error> in
signer
.sign(hashes: inputs.map { $0.hash }, walletPublicKey: manager.wallet.publicKey)
.tryMap { signatures -> [Data] in
try zip(inputs, signatures).map { (input, signature) in
try manager.txBuilder.buildForSend(rawData: input.rawData, signature: signature)
}
}
.eraseToAnyPublisher()
}

let sentTransactionsPublisher = readyToSendTransactionsPublisher
.flatMap { [weak self] transactionsData -> AnyPublisher<[TronBroadcastResponse], Error> in
guard let self else {
return .anyFail(error: WalletError.empty)
}

let sendPublishers = transactionsData
.map { data in
self.networkService
.broadcastHex(data)
.mapSendError(tx: data.hexString)
.eraseToAnyPublisher()
}

return Publishers.Sequence(sequence: sendPublishers)
.flatMap(maxPublishers: .max(1)) { $0 }
.collect()
.eraseToAnyPublisher()
}

return sentTransactionsPublisher
.withWeakCaptureOf(self)
.tryMap { manager, broadcastResponses -> [TransactionSendResult] in
guard broadcastResponses.count == transactions.count,
broadcastResponses.allSatisfy({ $0.result }) else {
throw WalletError.failedToSendTx
}

var results: [TransactionSendResult] = []

for (transaction, broadcastResponse) in zip(transactions, broadcastResponses) {
let hash = broadcastResponse.txid
let mapper = PendingTransactionRecordMapper()
let record = mapper.mapToPendingTransactionRecord(stakeKitTransaction: transaction, hash: hash)
manager.wallet.addPendingTransaction(record)
results.append(TransactionSendResult(hash: hash))
}
func prepareDataForSign(transaction: StakeKitTransaction) throws -> Data {
try TronStakeKitTransactionHelper().prepareForSign(transaction.unsignedData).hash
}

return results
}
.eraseSendError()
.eraseToAnyPublisher()
func prepareDataForSend(transaction: StakeKitTransaction, signature: SignatureInfo) throws -> RawTransaction {
let rawData = try TronStakeKitTransactionHelper().prepareForSign(transaction.unsignedData).rawData
let unmarshalled = unmarshal(signature.signature, hash: signature.hash, publicKey: wallet.publicKey)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а тут это точно надо? В оригинальном коде не было

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ага, я же писал тебе даже там ошибка была от ноды )

return try txBuilder.buildForSend(rawData: rawData, signature: unmarshalled)
}

func sendStakeKit(transaction: StakeKitTransaction, signer: any TransactionSigner) -> AnyPublisher<TransactionSendResult, SendTxError> {
return .anyFail(error: .init(error: BlockchainSdkError.notImplemented))
func broadcast(transaction: StakeKitTransaction, rawTransaction: RawTransaction) async throws -> String {
try await networkService.broadcastHex(rawTransaction).async().txid
}
}

96 changes: 96 additions & 0 deletions BlockchainSdk/Common/Interfaces/StakeKitTransactionSender.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// StakeKitTransactionSender.swift
// BlockchainSdk
//
// Created by Sergey Balashov on 02.09.2024.
//

import Foundation
import TangemSdk

public protocol StakeKitTransactionSender {
/// Return stream with tx which was sent one by one
/// If catch error stream will be stopped
/// In case when manager already implemented the `StakeKitTransactionSenderProvider` method will be not required
func sendStakeKit(transactions: [StakeKitTransaction], signer: TransactionSigner, delay second: UInt64?) -> AsyncThrowingStream<StakeKitTransactionSendResult, Error>
}

protocol StakeKitTransactionSenderProvider {
associatedtype RawTransaction

func prepareDataForSign(transaction: StakeKitTransaction) throws -> Data
func prepareDataForSend(transaction: StakeKitTransaction, signature: SignatureInfo) throws -> RawTransaction
func broadcast(transaction: StakeKitTransaction, rawTransaction: RawTransaction) async throws -> String
}

// MARK: - Common implementation for StakeKitTransactionSenderProvider

extension StakeKitTransactionSender where Self: StakeKitTransactionSenderProvider, Self: WalletProvider, RawTransaction: CustomStringConvertible {
func sendStakeKit(transactions: [StakeKitTransaction], signer: TransactionSigner, delay second: UInt64?) -> AsyncThrowingStream<StakeKitTransactionSendResult, Error> {
.init { [weak self] continuation in
let task = Task { [weak self] in
guard let self else {
continuation.finish()
return
}

do {
tureck1y marked this conversation as resolved.
Show resolved Hide resolved
let preparedHashes = try transactions.map { try self.prepareDataForSign(transaction: $0) }
let signatures: [SignatureInfo] = try await signer.sign(hashes: preparedHashes, walletPublicKey: wallet.publicKey).async()

for (transaction, signature) in zip(transactions, signatures) {
try Task.checkCancellation()
let rawTransaction = try prepareDataForSend(transaction: transaction, signature: signature)

do {
let result: TransactionSendResult = try await broadcast(transaction: transaction, rawTransaction: rawTransaction)
try Task.checkCancellation()
continuation.yield(.init(transaction: transaction, result: result))
m3g0byt3 marked this conversation as resolved.
Show resolved Hide resolved
} catch {
throw StakeKitTransactionSendError(transaction: transaction, error: error)
}

if transactions.count > 1, let second {
Log.log("\(self) start \(second) second delay between the transactions sending")
try await Task.sleep(nanoseconds: second * NSEC_PER_SEC)
}
}

continuation.finish()

} catch {
Log.log("\(self) catch \(error) when sent staking transaction")
continuation.finish(throwing: error)
}
}

continuation.onTermination = { termination in
task.cancel()
}
}
}

/// Convenience method with adding the `PendingTransaction` to the wallet and `SendTxError` mapping
private func broadcast(transaction: StakeKitTransaction, rawTransaction: RawTransaction) async throws -> TransactionSendResult {
do {
let hash: String = try await broadcast(transaction: transaction, rawTransaction: rawTransaction)
let mapper = PendingTransactionRecordMapper()
let record = mapper.mapToPendingTransactionRecord(
stakeKitTransaction: transaction,
source: wallet.defaultAddress.value,
hash: hash
)

await addPendingTransaction(record)

return TransactionSendResult(hash: hash)
} catch {
throw SendTxErrorFactory().make(error: error, with: rawTransaction.description)
}
}

@MainActor
private func addPendingTransaction(_ record: PendingTransactionRecord) {
wallet.addPendingTransaction(record)
}
}
Loading
Loading