diff --git a/BlockchainSdk.xcodeproj/project.pbxproj b/BlockchainSdk.xcodeproj/project.pbxproj index 10daba92b..ac9b09977 100644 --- a/BlockchainSdk.xcodeproj/project.pbxproj +++ b/BlockchainSdk.xcodeproj/project.pbxproj @@ -700,6 +700,8 @@ DC87B9F02B8F4D9700C61C5A /* BitcoreResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC87B9ED2B8F4D9700C61C5A /* BitcoreResponse.swift */; }; DC87B9F12B8F4D9700C61C5A /* BitcoreTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC87B9EE2B8F4D9700C61C5A /* BitcoreTarget.swift */; }; DC886FCD2AF5637D0098669C /* CommonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC886FCC2AF5637D0098669C /* CommonTests.swift */; }; + DCF5E0E52C6A361A00062F28 /* TronTransactionParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF5E0E42C6A361A00062F28 /* TronTransactionParams.swift */; }; + DCF5E0FB2C6A455300062F28 /* TronFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF5E0FA2C6A455300062F28 /* TronFunction.swift */; }; E93532CE292D0E86008FD979 /* BlockBookUtxoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9941ACA2927B07300399DE3 /* BlockBookUtxoProvider.swift */; }; E9941AC92927846B00399DE3 /* BlockBookTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9941AC82927846B00399DE3 /* BlockBookTarget.swift */; }; EF0DA78228523FAC0081092A /* StellarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DFCE549274BB88700C1B1A0 /* StellarTests.swift */; }; @@ -1613,6 +1615,8 @@ DC87B9EE2B8F4D9700C61C5A /* BitcoreTarget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitcoreTarget.swift; sourceTree = ""; }; DC886FCC2AF5637D0098669C /* CommonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonTests.swift; sourceTree = ""; }; DCE3379A28F2B7260029A6F5 /* BlockchainSdkExample.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = BlockchainSdkExample.entitlements; sourceTree = ""; }; + DCF5E0E42C6A361A00062F28 /* TronTransactionParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronTransactionParams.swift; sourceTree = ""; }; + DCF5E0FA2C6A455300062F28 /* TronFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronFunction.swift; sourceTree = ""; }; E9941AC82927846B00399DE3 /* BlockBookTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockBookTarget.swift; sourceTree = ""; }; E9941ACA2927B07300399DE3 /* BlockBookUtxoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockBookUtxoProvider.swift; sourceTree = ""; }; EF1339B52AB4B2A600B78BA3 /* TransferERC20TokenMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferERC20TokenMethod.swift; sourceTree = ""; }; @@ -3491,11 +3495,13 @@ DA9F76E827EC8AEB00F0665C /* TronAddressService.swift */, B00B58C72BCEB9F4007475F7 /* TronAPIResolver.swift */, EF3B19452AA85E620084AA1C /* TronExternalLinkProvider.swift */, + DCF5E0FA2C6A455300062F28 /* TronFunction.swift */, DA9F76F027EC9C8300F0665C /* TronJsonRpcProvider.swift */, DA9F76FD27ECA78800F0665C /* TronNetworkModels.swift */, DA9F76EE27EC9BCD00F0665C /* TronNetworkService.swift */, DA9F76F427EC9CB400F0665C /* TronTarget.swift */, DA3081312817EC1A00DE41F1 /* TronTransactionBuilder.swift */, + DCF5E0E42C6A361A00062F28 /* TronTransactionParams.swift */, 2DFD3AA32BE560BD00FCA7DC /* TronUtils.swift */, 2DDE5B9C29C4F8D200A5B708 /* TronWalletAssembly.swift */, DA9F76EB27EC9A2900F0665C /* TronWalletManager.swift */, @@ -4994,6 +5000,7 @@ DA5ACCC02C05B7D1005892AC /* KoinosTransferEvent.swift in Sources */, DA570CF927A82322007F34EB /* PolkadotBlockchainMeta.swift in Sources */, 5D14E4972397B87000C15FC8 /* WalletManager.swift in Sources */, + DCF5E0FB2C6A455300062F28 /* TronFunction.swift in Sources */, B6F3AA6D2ADD92230059C99A /* Publisher+.swift in Sources */, 0AEFED102C3D5B0E00C0F400 /* ICPProviderTarget.swift in Sources */, DC5E64E12B1650F400E81AA5 /* OP_CHECKSIG.swift in Sources */, @@ -5076,6 +5083,7 @@ 0AF90CDD2B87658600772F04 /* AuroraExternalLinkProvider.swift in Sources */, 0A0E225D2B9B366400075FF1 /* MoonbeamExternalLinkProvider.swift in Sources */, B6F3AA722ADD925D0059C99A /* NEARTarget.swift in Sources */, + DCF5E0E52C6A361A00062F28 /* TronTransactionParams.swift in Sources */, EFD717D02A2619C400E5430D /* Wallet+PublicKey.swift in Sources */, B698247F2B71A8D300E1333D /* HederaNetworkResult.swift in Sources */, 5DFF005F2521F772005203FB /* DucatusWalletManager.swift in Sources */, diff --git a/BlockchainSdk/Blockchains/Tron/TronAddressService.swift b/BlockchainSdk/Blockchains/Tron/TronAddressService.swift index e1d4f8336..b88534cda 100644 --- a/BlockchainSdk/Blockchains/Tron/TronAddressService.swift +++ b/BlockchainSdk/Blockchains/Tron/TronAddressService.swift @@ -13,29 +13,6 @@ import CryptoSwift public struct TronAddressService { private let prefix: UInt8 = 0x41 private let addressLength = 21 - - public init() {} - - static func toByteForm(_ base58String: String) -> Data? { - guard let bytes = base58String.base58CheckDecodedBytes else { - return nil - } - - return Data(bytes) - } - - static func toHexForm(_ base58String: String, length: Int?) -> String? { - guard let data = toByteForm(base58String) else { - return nil - } - - let hex = data.hexString.lowercased() - if let length = length { - return String(repeating: "0", count: length - hex.count) + hex - } else { - return hex - } - } } // MARK: - AddressProvider diff --git a/BlockchainSdk/Blockchains/Tron/TronFunction.swift b/BlockchainSdk/Blockchains/Tron/TronFunction.swift new file mode 100644 index 000000000..5383969fd --- /dev/null +++ b/BlockchainSdk/Blockchains/Tron/TronFunction.swift @@ -0,0 +1,20 @@ +// +// TronFunction.swift +// BlockchainSdk +// +// Created by Alexander Osokin on 12.08.2024. +// Copyright © 2024 Tangem AG. All rights reserved. +// + +import Foundation +import CryptoSwift + +enum TronFunction: String { + case transfer = "transfer(address,uint256)" + case approve = "approve(address,uint256)" + case balanceOf = "balanceOf(address)" + + var prefix: Data { + Data(rawValue.bytes).sha3(.keccak256).prefix(4) + } +} diff --git a/BlockchainSdk/Blockchains/Tron/TronJsonRpcProvider.swift b/BlockchainSdk/Blockchains/Tron/TronJsonRpcProvider.swift index 5b9283938..3b9294db1 100644 --- a/BlockchainSdk/Blockchains/Tron/TronJsonRpcProvider.swift +++ b/BlockchainSdk/Blockchains/Tron/TronJsonRpcProvider.swift @@ -43,8 +43,8 @@ class TronJsonRpcProvider: HostProvider { requestPublisher(for: .broadcastHex(data: data)) } - func tokenBalance(address: String, contractAddress: String) -> AnyPublisher { - requestPublisher(for: .tokenBalance(address: address, contractAddress: contractAddress)) + func tokenBalance(address: String, contractAddress: String, parameter: String) -> AnyPublisher { + requestPublisher(for: .tokenBalance(address: address, contractAddress: contractAddress, parameter: parameter)) } func contractEnergyUsage(sourceAddress: String, contractAddress: String, parameter: String) -> AnyPublisher { diff --git a/BlockchainSdk/Blockchains/Tron/TronNetworkService.swift b/BlockchainSdk/Blockchains/Tron/TronNetworkService.swift index fe17d6f2a..343ba7b60 100644 --- a/BlockchainSdk/Blockchains/Tron/TronNetworkService.swift +++ b/BlockchainSdk/Blockchains/Tron/TronNetworkService.swift @@ -50,10 +50,10 @@ class TronNetworkService: MultiNetworkProvider { } } - func accountInfo(for address: String, tokens: [Token], transactionIDs: [String]) -> AnyPublisher { + func accountInfo(for address: String, tokens: [Token], transactionIDs: [String], encodedAddress: String) -> AnyPublisher { Publishers.Zip3( getAccount(for: address), - tokenBalances(address: address, tokens: tokens), + tokenBalances(address: address, tokens: tokens, parameter: encodedAddress), confirmedTransactionIDs(ids: transactionIDs) ) .map { [blockchain] (accountInfo, tokenBalances, confirmedTransactionIDs) in @@ -136,8 +136,8 @@ class TronNetworkService: MultiNetworkProvider { .eraseToAnyPublisher() } } - - private func tokenBalances(address: String, tokens: [Token]) -> AnyPublisher<[Token: Decimal], Error> { + + private func tokenBalances(address: String, tokens: [Token], parameter: String) -> AnyPublisher<[Token: Decimal], Error> { tokens .publisher .setFailureType(to: Error.self) @@ -146,7 +146,7 @@ class TronNetworkService: MultiNetworkProvider { return .anyFail(error: WalletError.empty) } return self - .tokenBalance(address: address, token: token) + .tokenBalance(address: address, token: token, parameter: parameter) .setFailureType(to: Error.self) .eraseToAnyPublisher() } @@ -159,9 +159,9 @@ class TronNetworkService: MultiNetworkProvider { .eraseToAnyPublisher() } - private func tokenBalance(address: String, token: Token) -> AnyPublisher<(Token, Decimal), Never> { + private func tokenBalance(address: String, token: Token, parameter: String) -> AnyPublisher<(Token, Decimal), Never> { providerPublisher { - $0.tokenBalance(address: address, contractAddress: token.contractAddress) + $0.tokenBalance(address: address, contractAddress: token.contractAddress, parameter: parameter) .tryMap { response in let bigUIntValue = try TronUtils().combineBigUIntValueAtBalance(response: response.constant_result) diff --git a/BlockchainSdk/Blockchains/Tron/TronTarget.swift b/BlockchainSdk/Blockchains/Tron/TronTarget.swift index a9985a3e0..a0eac306e 100644 --- a/BlockchainSdk/Blockchains/Tron/TronTarget.swift +++ b/BlockchainSdk/Blockchains/Tron/TronTarget.swift @@ -16,7 +16,7 @@ struct TronTarget: TargetType { case getAccountResource(address: String) case getNowBlock case broadcastHex(data: Data) - case tokenBalance(address: String, contractAddress: String) + case tokenBalance(address: String, contractAddress: String, parameter: String) case contractEnergyUsage(sourceAddress: String, contractAddress: String, parameter: String) case getTransactionInfoById(transactionID: String) } @@ -68,14 +68,12 @@ struct TronTarget: TargetType { case .broadcastHex(let data): let request = TronBroadcastRequest(transaction: data.hexString.lowercased()) return .requestJSONEncodable(request) - case .tokenBalance(let address, let contractAddress): - let hexAddress = TronAddressService.toHexForm(address, length: 64) ?? "" - + case .tokenBalance(let address, let contractAddress, let parameter): let request = TronTriggerSmartContractRequest( owner_address: address, contract_address: contractAddress, - function_selector: "balanceOf(address)", - parameter: hexAddress, + function_selector: TronFunction.balanceOf.rawValue, + parameter: parameter, visible: true ) return .requestJSONEncodable(request) @@ -83,7 +81,7 @@ struct TronTarget: TargetType { let request = TronTriggerSmartContractRequest( owner_address: sourceAddress, contract_address: contractAddress, - function_selector: "transfer(address,uint256)", + function_selector: TronFunction.transfer.rawValue, parameter: parameter, visible: true ) diff --git a/BlockchainSdk/Blockchains/Tron/TronTransactionBuilder.swift b/BlockchainSdk/Blockchains/Tron/TronTransactionBuilder.swift index e2645f8b6..3a845e93a 100644 --- a/BlockchainSdk/Blockchains/Tron/TronTransactionBuilder.swift +++ b/BlockchainSdk/Blockchains/Tron/TronTransactionBuilder.swift @@ -11,17 +11,12 @@ import SwiftProtobuf import CryptoSwift class TronTransactionBuilder { - private let blockchain: Blockchain - private let smartContractFeeLimit: Int64 = 100_000_000 - - init(blockchain: Blockchain) { - self.blockchain = blockchain - } - - func buildForSign(amount: Amount, source: String, destination: String, block: TronBlock) throws -> Protocol_Transaction.raw { - let contract = try self.contract(amount: amount, source: source, destination: destination) - let feeLimit = (amount.type == .coin) ? 0 : smartContractFeeLimit - + private let utils = TronUtils() + + func buildForSign(transaction: Transaction, block: TronBlock) throws -> TronPresignedInput { + let contract = try self.contract(transaction: transaction) + let feeLimit = (transaction.amount.type == .coin) ? 0 : Constants.smartContractFeeLimit + let blockHeaderRawData = block.block_header.raw_data let blockHeader = Protocol_BlockHeader.raw.with { $0.timestamp = blockHeaderRawData.timestamp @@ -31,17 +26,17 @@ class TronTransactionBuilder { $0.parentHash = Data(hex: blockHeaderRawData.parentHash) $0.witnessAddress = Data(hex: blockHeaderRawData.witness_address) } - + let blockData = try blockHeader.serializedData() let blockHash = blockData.getSha256() let refBlockHash = blockHash[8..<16] - + let number = blockHeader.number let numberData = Data(Data(from: number).reversed()) let refBlockBytes = numberData[6..<8] - + let tenHours: Int64 = 10 * 60 * 60 * 1000 // same as WalletCore - + let rawData = Protocol_Transaction.raw.with { $0.timestamp = blockHeader.timestamp $0.expiration = blockHeader.timestamp + tenHours @@ -52,73 +47,129 @@ class TronTransactionBuilder { ] $0.feeLimit = feeLimit } - - return rawData + + let hash = try rawData.serializedData().sha256() + return TronPresignedInput(rawData: rawData, hash: hash) } - - func buildForSend(rawData: Protocol_Transaction.raw, signature: Data) -> Protocol_Transaction { + + func buildForSend(rawData: Protocol_Transaction.raw, signature: Data) throws -> Data { let transaction = Protocol_Transaction.with { $0.rawData = rawData $0.signature = [signature] } - return transaction + + return try transaction.serializedData() + } + + func buildContractEnergyUsageParameter(amount: Amount, destinationAddress: String) throws -> String { + let addressData = try utils.convertAddressToBytes(destinationAddress).leadingZeroPadding(toLength: 32) + + guard let amountData = amount.encoded?.leadingZeroPadding(toLength: 32) else { + throw WalletError.failedToGetFee + } + + let parameter = (addressData + amountData).hexString.lowercased() + return parameter } - - private func contract(amount: Amount, source: String, destination: String) throws -> Protocol_Transaction.Contract { + + private func contract(transaction: Transaction) throws -> Protocol_Transaction.Contract { + let amount = transaction.amount + let sourceAddress = transaction.sourceAddress + let destinationAddress = transaction.destinationAddress + switch amount.type { case .coin: - let parameter = Protocol_TransferContract.with { - $0.ownerAddress = TronAddressService.toByteForm(source) ?? Data() - $0.toAddress = TronAddressService.toByteForm(destination) ?? Data() - $0.amount = integerValue(from: amount).int64Value + let parameter = try Protocol_TransferContract.with { + $0.ownerAddress = try utils.convertAddressToBytes(sourceAddress) + $0.toAddress = try utils.convertAddressToBytes(destinationAddress) + $0.amount = amount.int64Value } - + return try Protocol_Transaction.Contract.with { $0.type = .transferContract $0.parameter = try Google_Protobuf_Any(message: parameter) } case .token(let token): - let functionSelector = "transfer(address,uint256)" - let functionSelectorHash = Data(functionSelector.bytes).sha3(.keccak256).prefix(4) - - let addressData = TronAddressService.toByteForm(destination)?.leadingZeroPadding(toLength: 32) ?? Data() - - guard - let bigIntValue = EthereumUtils.parseToBigUInt("\(amount.value)", decimals: token.decimalCount) - else { - throw WalletError.failedToBuildTx - } - - let amountData = bigIntValue.serialize().leadingZeroPadding(toLength: 32) - let contractData = functionSelectorHash + addressData + amountData - - let parameter = Protocol_TriggerSmartContract.with { - $0.contractAddress = TronAddressService.toByteForm(token.contractAddress) ?? Data() + let contractData = try buildContractData(transaction: transaction) + + let parameter = try Protocol_TriggerSmartContract.with { + $0.contractAddress = try utils.convertAddressToBytes(token.contractAddress) $0.data = contractData - $0.ownerAddress = TronAddressService.toByteForm(source) ?? Data() + $0.ownerAddress = try utils.convertAddressToBytes(sourceAddress) } - + return try Protocol_Transaction.Contract.with { $0.type = .triggerSmartContract $0.parameter = try Google_Protobuf_Any(message: parameter) } - case .reserve, .feeResource: - fatalError() + default: + assertionFailure("Not impkemented") + throw BlockchainSdkError.notImplemented } } - - private func integerValue(from amount: Amount) -> NSDecimalNumber { - let decimalValue: Decimal - switch amount.type { - case .coin: - decimalValue = blockchain.decimalValue - case .token(let token): - decimalValue = token.decimalValue - case .reserve, .feeResource: - fatalError() + + private func buildContractData(transaction: Transaction) throws -> Data { + let params = try getParams(from: transaction) + let transactionType = params?.transactionType ?? .transfer + + switch transactionType { + case .transfer: + return try buildTransferContractData(amount: transaction.amount, destinationAddress: transaction.destinationAddress) + case .approval(let data): + return buildApprovalContractData(data: data) } - - let decimalAmount = amount.value * decimalValue - return (decimalAmount.rounded() as NSDecimalNumber) } + + private func buildTransferContractData(amount: Amount, destinationAddress: String) throws -> Data { + guard let amountData = amount.encoded?.leadingZeroPadding(toLength: 32) else { + throw WalletError.failedToBuildTx + } + + let destinationData = try utils.convertAddressToBytes(destinationAddress).leadingZeroPadding(toLength: 32) + + let contractData = TronFunction.transfer.prefix + destinationData + amountData + return contractData + + } + + private func buildApprovalContractData(data: Data) -> Data { + let contractData = TronFunction.approve.prefix + data + return contractData + } + + private func getParams(from transaction: Transaction) throws -> TronTransactionParams? { + guard let params = transaction.params else { + return nil + } + + guard let tronParams = params as? TronTransactionParams else { + throw WalletError.failedToBuildTx + } + + return tronParams + } +} + +// MARK: - Constants + +private extension TronTransactionBuilder { + enum Constants { + static let smartContractFeeLimit: Int64 = 100_000_000 + } +} + +// MARK: - Amount+ + +fileprivate extension Amount { + var int64Value: Int64 { + let decimalAmount = value * pow(Decimal(10), decimals) + return (decimalAmount.rounded() as NSDecimalNumber).int64Value + } +} + +// MARK: - TronPresignedInput + +struct TronPresignedInput { + let rawData: Protocol_Transaction.raw + let hash: Data } diff --git a/BlockchainSdk/Blockchains/Tron/TronTransactionParams.swift b/BlockchainSdk/Blockchains/Tron/TronTransactionParams.swift new file mode 100644 index 000000000..5f14912ce --- /dev/null +++ b/BlockchainSdk/Blockchains/Tron/TronTransactionParams.swift @@ -0,0 +1,24 @@ +// +// TronTransactionParams.swift +// BlockchainSdk +// +// Created by Alexander Osokin on 12.08.2024. +// Copyright © 2024 Tangem AG. All rights reserved. +// + +import Foundation + +public struct TronTransactionParams: TransactionParams { + public let transactionType: TransactionType + + public init(transactionType: TransactionType) { + self.transactionType = transactionType + } +} + +public extension TronTransactionParams { + enum TransactionType { + case transfer + case approval(data: Data) + } +} diff --git a/BlockchainSdk/Blockchains/Tron/TronUtils.swift b/BlockchainSdk/Blockchains/Tron/TronUtils.swift index 04df08c67..8caa8fe2e 100644 --- a/BlockchainSdk/Blockchains/Tron/TronUtils.swift +++ b/BlockchainSdk/Blockchains/Tron/TronUtils.swift @@ -21,4 +21,16 @@ struct TronUtils { return bigIntValue } + + func convertAddressToBytes(_ base58String: String) throws -> Data { + guard let bytes = base58String.base58CheckDecodedBytes else { + throw TronUtilsError.failedToDecodeAddress + } + + return Data(bytes) + } +} + +enum TronUtilsError: Error { + case failedToDecodeAddress } diff --git a/BlockchainSdk/Blockchains/Tron/TronWalletAssembly.swift b/BlockchainSdk/Blockchains/Tron/TronWalletAssembly.swift index e1b163081..4d7fd0dd2 100644 --- a/BlockchainSdk/Blockchains/Tron/TronWalletAssembly.swift +++ b/BlockchainSdk/Blockchains/Tron/TronWalletAssembly.swift @@ -22,7 +22,7 @@ struct TronWalletAssembly: WalletManagerAssembly { }) $0.networkService = TronNetworkService(isTestnet: blockchain.isTestnet, providers: providers) - $0.txBuilder = TronTransactionBuilder(blockchain: blockchain) + $0.txBuilder = TronTransactionBuilder() } } } diff --git a/BlockchainSdk/Blockchains/Tron/TronWalletManager.swift b/BlockchainSdk/Blockchains/Tron/TronWalletManager.swift index 133465fc3..d4618c9db 100644 --- a/BlockchainSdk/Blockchains/Tron/TronWalletManager.swift +++ b/BlockchainSdk/Blockchains/Tron/TronWalletManager.swift @@ -25,9 +25,22 @@ class TronWalletManager: BaseManager, WalletManager { private let feeSigner = DummySigner() override func update(completion: @escaping (Result) -> Void) { - let transactionIDs = wallet.pendingTransactions.map { $0.hash } - - cancellable = networkService.accountInfo(for: wallet.address, tokens: cardTokens, transactionIDs: transactionIDs) + let encodedAddressPublisher = Result { + let bytes = try TronUtils().convertAddressToBytes(wallet.address) + let hex = bytes.leadingZeroPadding(toLength: 32).hexString.lowercased() + return hex + }.publisher + + cancellable = encodedAddressPublisher + .withWeakCaptureOf(self) + .flatMap{ manager, encodedAddress in + manager.networkService.accountInfo( + for: manager.wallet.address, + tokens: manager.cardTokens, + transactionIDs: manager.wallet.pendingTransactions.map { $0.hash }, + encodedAddress: encodedAddress + ) + } .sink { [weak self] in switch $0 { case .failure(let error): @@ -43,31 +56,26 @@ class TronWalletManager: BaseManager, WalletManager { func send(_ transaction: Transaction, signer: TransactionSigner) -> AnyPublisher { return signedTransactionData( - amount: transaction.amount, - source: wallet.address, - destination: transaction.destinationAddress, + transaction: transaction, signer: signer, publicKey: wallet.publicKey ) - .flatMap { [weak self] data -> AnyPublisher in - guard let self = self else { - return .anyFail(error: WalletError.empty) - } - - return self.networkService + .withWeakCaptureOf(self) + .flatMap { manager, data in + manager.networkService .broadcastHex(data) .mapSendError(tx: data.hexString) - .eraseToAnyPublisher() } - .tryMap { [weak self] broadcastResponse -> TransactionSendResult in + .withWeakCaptureOf(self) + .tryMap { manager, broadcastResponse -> TransactionSendResult in guard broadcastResponse.result == true else { throw WalletError.failedToSendTx } - + let hash = broadcastResponse.txid let mapper = PendingTransactionRecordMapper() let record = mapper.mapToPendingTransactionRecord(transaction: transaction, hash: hash) - self?.wallet.addPendingTransaction(record) + manager.wallet.addPendingTransaction(record) return TransactionSendResult(hash: hash) } .eraseSendError() @@ -76,18 +84,28 @@ class TronWalletManager: BaseManager, WalletManager { func getFee(amount: Amount, destination: String) -> AnyPublisher<[Fee], Error> { let energyFeePublisher = energyFee(amount: amount, destination: destination) - - let transactionDataPublisher = signedTransactionData( + + let blockchain = wallet.blockchain + + let dummyTransaction = Transaction( amount: amount, - source: wallet.address, - destination: destination, + fee: Fee(.zeroCoin(for: blockchain)), + sourceAddress: wallet.address, + destinationAddress: destination, + changeAddress: wallet.address) + + let transactionDataPublisher = signedTransactionData( + transaction: dummyTransaction, signer: feeSigner, publicKey: feeSigner.publicKey ) - let blockchain = self.wallet.blockchain - - return Publishers.Zip4(energyFeePublisher, networkService.accountExists(address: destination), transactionDataPublisher, networkService.getAccountResource(for: wallet.address)) + return Publishers.Zip4( + energyFeePublisher, + networkService.accountExists(address: destination), + transactionDataPublisher, + networkService.getAccountResource(for: wallet.address) + ) .map { energyFee, destinationExists, transactionData, resources -> [Fee] in if !destinationExists && amount.type == .coin { let amount = Amount(with: blockchain, value: 1.1) @@ -115,32 +133,26 @@ class TronWalletManager: BaseManager, WalletManager { } .eraseToAnyPublisher() } - + private func energyFee(amount: Amount, destination: String) -> AnyPublisher { - let token: Token - switch amount.type { - case .reserve, .feeResource: - return .anyFail(error: WalletError.failedToGetFee) - case .coin: - return Just(0).setFailureType(to: Error.self).eraseToAnyPublisher() - case .token(let amountToken): - token = amountToken + guard let contractAddress = amount.type.token?.contractAddress else { + return .justWithError(output: 0) } - - let addressData = TronAddressService.toByteForm(destination)?.leadingZeroPadding(toLength: 32) ?? Data() - guard let amountData = amount.encoded?.leadingZeroPadding(toLength: 32) else { - return .anyFail(error: WalletError.failedToGetFee) + + let energyUsePublisher = Result { + try txBuilder.buildContractEnergyUsageParameter(amount: amount, destinationAddress: destination) } - - let parameter = (addressData + amountData).hexString.lowercased() - - let energyUsePublisher = networkService.contractEnergyUsage( - sourceAddress: wallet.address, - contractAddress: token.contractAddress, - parameter: parameter - ) - - return Publishers.Zip(energyUsePublisher, networkService.chainParameters()) + .publisher + .withWeakCaptureOf(self) + .flatMap { manager, parameter in + manager.networkService.contractEnergyUsage( + sourceAddress: manager.wallet.address, + contractAddress: contractAddress, + parameter: parameter + ) + } + + return energyUsePublisher.zip(networkService.chainParameters()) .map { energyUse, chainParameters in // Contract's energy fee changes every maintenance period (6 hours) and // since we don't know what period the transaction is going to be executed in @@ -157,76 +169,30 @@ class TronWalletManager: BaseManager, WalletManager { .eraseToAnyPublisher() } - private func signedTransactionData(amount: Amount, source: String, destination: String, signer: TransactionSigner, publicKey: Wallet.PublicKey) -> AnyPublisher { - return networkService.getNowBlock() - .tryMap { [weak self] block -> Protocol_Transaction.raw in - guard let self = self else { - throw WalletError.empty - } - - return try self.txBuilder.buildForSign(amount: amount, source: source, destination: destination, block: block) - } - .flatMap { [weak self] transactionRaw -> AnyPublisher in - guard let self = self else { - return .anyFail(error: WalletError.empty) - } - - return Just(()) - .setFailureType(to: Error.self) - .flatMap { [weak self] _ -> AnyPublisher in - guard let self = self else { - return .anyFail(error: WalletError.empty) - } - - return self.sign(transactionRaw, with: signer, publicKey: publicKey) - } - .tryMap { [weak self] signature -> Protocol_Transaction in - guard let self = self else { - throw WalletError.empty - } - - return self.txBuilder.buildForSend(rawData: transactionRaw, signature: signature) - } - .tryMap { - try $0.serializedData() - } - .eraseToAnyPublisher() - } - - .eraseToAnyPublisher() - } - - private func sign(_ transactionRaw: Protocol_Transaction.raw, with signer: TransactionSigner, publicKey: Wallet.PublicKey) -> AnyPublisher { - Just(()) - .setFailureType(to: Error.self) - .tryMap { - try transactionRaw.serializedData().sha256() + private func signedTransactionData(transaction: Transaction, signer: TransactionSigner, publicKey: Wallet.PublicKey) -> AnyPublisher { + networkService.getNowBlock() + .withWeakCaptureOf(self) + .tryMap { manager, block in + try manager.txBuilder.buildForSign(transaction: transaction, block: block) } - .flatMap { hash -> AnyPublisher in - Just(hash) - .setFailureType(to: Error.self) - .flatMap { - signer.sign(hash: $0, walletPublicKey: publicKey) - } - .tryMap { [weak self] signature -> Data in - guard let self = self else { - throw WalletError.empty - } - - return self.unmarshal(signature, hash: hash, publicKey: publicKey) + .flatMap { presignedInput in + signer.sign(hash: presignedInput.hash, walletPublicKey: publicKey) + .withWeakCaptureOf(self) + .tryMap { manager, signature in + let unmarshalledSignature = manager.unmarshal(signature, hash: presignedInput.hash, publicKey: publicKey) + return try manager.txBuilder.buildForSend(rawData: presignedInput.rawData, signature: unmarshalledSignature) } - .eraseToAnyPublisher() } .eraseToAnyPublisher() } - + private func updateWallet(_ accountInfo: TronAccountInfo) { wallet.add(amount: Amount(with: wallet.blockchain, value: accountInfo.balance)) - + for (token, balance) in accountInfo.tokenBalances { wallet.add(tokenValue: balance, for: token) } - + wallet.removePendingTransaction { hash in accountInfo.confirmedTransactionIDs.contains(hash) } @@ -259,8 +225,8 @@ fileprivate class DummySigner: TransactionSigner { init() { let keyPair = try! Secp256k1Utils().generateKeyPair() let compressedPublicKey = try! Secp256k1Key(with: keyPair.publicKey).compress() - self.publicKey = Wallet.PublicKey(seedKey: compressedPublicKey, derivationType: .none) - self.privateKey = keyPair.privateKey + publicKey = Wallet.PublicKey(seedKey: compressedPublicKey, derivationType: .none) + privateKey = keyPair.privateKey } func sign(hash: Data, walletPublicKey: Wallet.PublicKey) -> AnyPublisher { diff --git a/BlockchainSdkTests/ICP/ICPTests.swift b/BlockchainSdkTests/ICP/ICPTests.swift index 6c855eabf..10adf40f9 100644 --- a/BlockchainSdkTests/ICP/ICPTests.swift +++ b/BlockchainSdkTests/ICP/ICPTests.swift @@ -13,8 +13,6 @@ import TangemSdk @testable import BlockchainSdk final class ICPTests: XCTestCase { - private let blockchain = Blockchain.internetComputer - private let sizeTester = TransactionSizeTesterUtility() func testTransactionBuild() throws { @@ -26,18 +24,18 @@ final class ICPTests: XCTestCase { let nonce = Data(hex: "5b4210ba3969eff9b64163012d48935cf72bb86e0e444c431d28f64888af41f5") let txBuilder = ICPTransactionBuilder( - decimalValue: blockchain.decimalValue, + decimalValue: Blockchain.internetComputer.decimalValue, publicKey: publicKey.data, nonce: nonce ) let amounValueDecimal = Decimal(stringValue: "0.0001")! - let amountValue = Amount(with: blockchain, value: amounValueDecimal) - let feeValue = Amount(with: blockchain, value: .init(stringValue: "0.0001")!) - + let amountValue = Amount(with: .internetComputer, value: amounValueDecimal) + let feeValue = Amount(with: .internetComputer, value: .init(stringValue: "0.0001")!) + - let addressService = WalletCoreAddressService(blockchain: blockchain) + let addressService = WalletCoreAddressService(blockchain: .internetComputer) let sourceAddress = try addressService.makeAddress( for: Wallet.PublicKey(seedKey: publicKey.data, derivationType: nil), with: .default @@ -65,7 +63,7 @@ final class ICPTests: XCTestCase { hashesToSign.forEach { sizeTester.testTxSize($0) } - let curve = try Curve(blockchain: blockchain) + let curve = try Curve(blockchain: .internetComputer) let signatures = try hashesToSign.map { digest in let signature = try XCTUnwrap(privateKey.sign(digest: digest, curve: curve)) diff --git a/BlockchainSdkTests/Tron/TronTests.swift b/BlockchainSdkTests/Tron/TronTests.swift index 7e4d591a6..80c6dec11 100644 --- a/BlockchainSdkTests/Tron/TronTests.swift +++ b/BlockchainSdkTests/Tron/TronTests.swift @@ -29,22 +29,28 @@ class TronTests: XCTestCase { override func setUp() { self.blockchain = Blockchain.tron(testnet: true) - self.txBuilder = TronTransactionBuilder(blockchain: blockchain) + self.txBuilder = TronTransactionBuilder() } - func testTrxTransfer() { - let transactionRaw = try! txBuilder.buildForSign(amount: Amount(with: blockchain, value: 1), source: "TU1BRXbr6EmKmrLL4Kymv7Wp18eYFkRfAF", destination: "TXXxc9NsHndfQ2z9kMKyWpYa5T3QbhKGwn", block: tronBlock) - + func testTrxTransfer() throws { + let transaction = Transaction( + amount: Amount(with: blockchain, value: 1), + fee: Fee(.zeroCoin(for: blockchain)), + sourceAddress: "TU1BRXbr6EmKmrLL4Kymv7Wp18eYFkRfAF", + destinationAddress: "TXXxc9NsHndfQ2z9kMKyWpYa5T3QbhKGwn", + changeAddress: "TU1BRXbr6EmKmrLL4Kymv7Wp18eYFkRfAF" + ) + + let presignedInput = try txBuilder.buildForSign(transaction: transaction, block: tronBlock) let signature = Data(hex: "6b5de85a80b2f4f02351f691593fb0e49f14c5cb42451373485357e42d7890cd77ad7bfcb733555c098b992da79dabe5050f5e2db77d9d98f199074222de037701") - let transaction = txBuilder.buildForSend(rawData: transactionRaw, signature: signature) - let transactionData = try! transaction.serializedData() + let transactionData = try txBuilder.buildForSend(rawData: presignedInput.rawData, signature: signature) let expectedTransactionData = Data(hex: "0a85010a027b3b2208b21ace8d6ac20e7e40d8abb9bae62c5a67080112630a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412320a1541c5d1c75825b30bb2e2e655798209d56448eb6b5e121541ec8c5a0fcbb28f14418eed9cf582af0d77e4256e18c0843d70d889a4a9e62c12416b5de85a80b2f4f02351f691593fb0e49f14c5cb42451373485357e42d7890cd77ad7bfcb733555c098b992da79dabe5050f5e2db77d9d98f199074222de037701") XCTAssertEqual(transactionData, expectedTransactionData) } - func testTrc20TransferUSDT() { + func testTrc20TransferUSDT() throws { let token = Token(name: "Tether", symbol: "USDT", contractAddress: "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj", @@ -55,13 +61,20 @@ class TronTests: XCTestCase { 1000000000000000000, ] - let transactionDataList = amountValues.map { amountValue -> Data in - let transactionRaw = try! txBuilder.buildForSign(amount: Amount(with: token, value: amountValue), source: "TU1BRXbr6EmKmrLL4Kymv7Wp18eYFkRfAF", destination: "TXXxc9NsHndfQ2z9kMKyWpYa5T3QbhKGwn", block: tronBlock) - + let transactionDataList = try amountValues.map { amountValue -> Data in + + let transaction = Transaction( + amount: Amount(with: token, value: amountValue), + fee: Fee(.zeroCoin(for: blockchain)), + sourceAddress: "TU1BRXbr6EmKmrLL4Kymv7Wp18eYFkRfAF", + destinationAddress: "TXXxc9NsHndfQ2z9kMKyWpYa5T3QbhKGwn", + changeAddress: "TU1BRXbr6EmKmrLL4Kymv7Wp18eYFkRfAF" + ) + + let presignedInput = try txBuilder.buildForSign(transaction: transaction, block: tronBlock) let signature = Data(hex: "6b5de85a80b2f4f02351f691593fb0e49f14c5cb42451373485357e42d7890cd77ad7bfcb733555c098b992da79dabe5050f5e2db77d9d98f199074222de037701") - let transaction = txBuilder.buildForSend(rawData: transactionRaw, signature: signature) - let transactionData = try! transaction.serializedData() - + let transactionData = try txBuilder.buildForSend(rawData: presignedInput.rawData, signature: signature) + return transactionData } @@ -75,7 +88,7 @@ class TronTests: XCTestCase { XCTAssertEqual(transactionDataList, expectedTransactionDataList) } - func testTrc20TransferJST() { + func testTrc20TransferJST() throws { let token = Token(name: "JST", symbol: "JST", contractAddress: "TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3", @@ -87,13 +100,20 @@ class TronTests: XCTestCase { Decimal(string: "123456789123456789.123456789")!, ] - let transactionDataList = amountValues.map { amountValue -> Data in - let transactionRaw = try! txBuilder.buildForSign(amount: Amount(with: token, value: amountValue), source: "TU1BRXbr6EmKmrLL4Kymv7Wp18eYFkRfAF", destination: "TXXxc9NsHndfQ2z9kMKyWpYa5T3QbhKGwn", block: tronBlock) - + let transactionDataList = try amountValues.map { amountValue -> Data in + + let transaction = Transaction( + amount: Amount(with: token, value: amountValue), + fee: Fee(.zeroCoin(for: blockchain)), + sourceAddress: "TU1BRXbr6EmKmrLL4Kymv7Wp18eYFkRfAF", + destinationAddress: "TXXxc9NsHndfQ2z9kMKyWpYa5T3QbhKGwn", + changeAddress: "TU1BRXbr6EmKmrLL4Kymv7Wp18eYFkRfAF" + ) + + let presignedInput = try txBuilder.buildForSign(transaction: transaction, block: tronBlock) let signature = Data(hex: "6b5de85a80b2f4f02351f691593fb0e49f14c5cb42451373485357e42d7890cd77ad7bfcb733555c098b992da79dabe5050f5e2db77d9d98f199074222de037701") - let transaction = txBuilder.buildForSend(rawData: transactionRaw, signature: signature) - let transactionData = try! transaction.serializedData() - + let transactionData = try txBuilder.buildForSend(rawData: presignedInput.rawData, signature: signature) + return transactionData }