diff --git a/BlockchainSdk.xcodeproj/project.pbxproj b/BlockchainSdk.xcodeproj/project.pbxproj index 66442d900..aebb43fda 100644 --- a/BlockchainSdk.xcodeproj/project.pbxproj +++ b/BlockchainSdk.xcodeproj/project.pbxproj @@ -539,6 +539,8 @@ DA82434127A2B0AD00CFC2C0 /* PolkadotTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA82434027A2B0AD00CFC2C0 /* PolkadotTarget.swift */; }; DA82434327A2B0C100CFC2C0 /* PolkadotResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA82434227A2B0C100CFC2C0 /* PolkadotResponse.swift */; }; DA9EA73F29EE958500CAE6F2 /* CosmosTransactionParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA73E29EE958500CAE6F2 /* CosmosTransactionParams.swift */; }; + DA9F15F22C80B3B800EA7FAF /* FilecoinTransactionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9F15F12C80B3B800EA7FAF /* FilecoinTransactionBuilder.swift */; }; + DA9F15F42C80BDC700EA7FAF /* FilecoinTransactionBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9F15F32C80BDC700EA7FAF /* FilecoinTransactionBuilderTests.swift */; }; DA9F76E927EC8AEB00F0665C /* TronAddressService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9F76E827EC8AEB00F0665C /* TronAddressService.swift */; }; DA9F76EC27EC9A2900F0665C /* TronWalletManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9F76EB27EC9A2900F0665C /* TronWalletManager.swift */; }; DA9F76EF27EC9BCD00F0665C /* TronNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9F76EE27EC9BCD00F0665C /* TronNetworkService.swift */; }; @@ -583,6 +585,7 @@ DAE657E62BFC732400D7D63A /* value.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE657E22BFC732400D7D63A /* value.pb.swift */; }; DAE657E72BFC732400D7D63A /* token.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE657E32BFC732400D7D63A /* token.pb.swift */; }; DAE657E92BFCA3E400D7D63A /* KoinosTransactionBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE657E82BFCA3E400D7D63A /* KoinosTransactionBuilderTests.swift */; }; + DAE864BB2C81CF1700A2D51A /* FilecoinFeeParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE864BA2C81CF1700A2D51A /* FilecoinFeeParameters.swift */; }; DAED18A22C7DF3D900522056 /* FilecoinNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAED18A12C7DF3D900522056 /* FilecoinNetworkService.swift */; }; DAED921F27A150E500F188D7 /* PolkadotAddressService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAED921E27A150E500F188D7 /* PolkadotAddressService.swift */; }; DAF0866E27A942D60024312E /* PolkadotWalletManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF0866D27A942D60024312E /* PolkadotWalletManager.swift */; }; @@ -1478,6 +1481,8 @@ DA82434027A2B0AD00CFC2C0 /* PolkadotTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolkadotTarget.swift; sourceTree = ""; }; DA82434227A2B0C100CFC2C0 /* PolkadotResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolkadotResponse.swift; sourceTree = ""; }; DA9EA73E29EE958500CAE6F2 /* CosmosTransactionParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CosmosTransactionParams.swift; sourceTree = ""; }; + DA9F15F12C80B3B800EA7FAF /* FilecoinTransactionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilecoinTransactionBuilder.swift; sourceTree = ""; }; + DA9F15F32C80BDC700EA7FAF /* FilecoinTransactionBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilecoinTransactionBuilderTests.swift; sourceTree = ""; }; DA9F76E827EC8AEB00F0665C /* TronAddressService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronAddressService.swift; sourceTree = ""; }; DA9F76EB27EC9A2900F0665C /* TronWalletManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronWalletManager.swift; sourceTree = ""; }; DA9F76EE27EC9BCD00F0665C /* TronNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronNetworkService.swift; sourceTree = ""; }; @@ -1523,6 +1528,7 @@ DAE657E22BFC732400D7D63A /* value.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = value.pb.swift; sourceTree = ""; }; DAE657E32BFC732400D7D63A /* token.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = token.pb.swift; sourceTree = ""; }; DAE657E82BFCA3E400D7D63A /* KoinosTransactionBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinosTransactionBuilderTests.swift; sourceTree = ""; }; + DAE864BA2C81CF1700A2D51A /* FilecoinFeeParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilecoinFeeParameters.swift; sourceTree = ""; }; DAED18A12C7DF3D900522056 /* FilecoinNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilecoinNetworkService.swift; sourceTree = ""; }; DAED921E27A150E500F188D7 /* PolkadotAddressService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolkadotAddressService.swift; sourceTree = ""; }; DAF0866D27A942D60024312E /* PolkadotWalletManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PolkadotWalletManager.swift; sourceTree = ""; }; @@ -3402,6 +3408,8 @@ DAD1565A2C7DCFAD00DE52B3 /* Network */, DA15D1E92C77830E00FD733B /* FilecoinExternalLinkProvider.swift */, DA20BD692C7BC5E9000F02DF /* FilecoinWalletAssembly.swift */, + DA9F15F12C80B3B800EA7FAF /* FilecoinTransactionBuilder.swift */, + DAE864BA2C81CF1700A2D51A /* FilecoinFeeParameters.swift */, ); path = Filecoin; sourceTree = ""; @@ -3457,6 +3465,7 @@ isa = PBXGroup; children = ( DA20BD6C2C7BC7AF000F02DF /* FilecoinAddressTests.swift */, + DA9F15F32C80BDC700EA7FAF /* FilecoinTransactionBuilderTests.swift */, ); path = Filecoin; sourceTree = ""; @@ -4836,6 +4845,7 @@ B0C2C89D25FBCF0200A61622 /* RosettaTarget.swift in Sources */, B62011042B7AAC2100155235 /* Collection+.swift in Sources */, EFD717DF2A27310E00E5430D /* AddressType.swift in Sources */, + DAE864BB2C81CF1700A2D51A /* FilecoinFeeParameters.swift in Sources */, B69F21E62B86CD4A00A1177B /* UnixTimestamp.swift in Sources */, EFF607C72BD000D000C37210 /* EthereumAddressService.swift in Sources */, 0A7624E32C296969002FA139 /* ICPWalletManager.swift in Sources */, @@ -4890,6 +4900,7 @@ 5D88C80B256BCDBB00020028 /* StellarTransactionParams.swift in Sources */, B69824772B7175EA00E1333D /* HederaTransactionParams.swift in Sources */, 5D88838927D3BB8B008744E1 /* WalletError.swift in Sources */, + DA9F15F22C80B3B800EA7FAF /* FilecoinTransactionBuilder.swift in Sources */, B64A680D2BCE9E82009ED960 /* EthereumOptimisticRollupWalletAssembly.swift in Sources */, EF72577E2A8D42A100EA8CB2 /* TransactionHistory.swift in Sources */, DA4B80252BBA66F900CE50B7 /* BitcoinTransactionFeeCalculator.swift in Sources */, @@ -5471,6 +5482,7 @@ DA1D3B7B2B57B8FB00247393 /* BigUInt+.swift in Sources */, 2D535E872A0CC5FA0081EB76 /* AddressesValidationTests.swift in Sources */, DAD62DE62C467718008509BE /* MantleTests.swift in Sources */, + DA9F15F42C80BDC700EA7FAF /* FilecoinTransactionBuilderTests.swift in Sources */, B62BCBAC2B3D978F007494CF /* VeChainTests.swift in Sources */, EF0DA78928523FAC0081092A /* LitecoinTests.swift in Sources */, EF0DA78828523FAC0081092A /* BitcoinTests.swift in Sources */, diff --git a/BlockchainSdk/Blockchains/Filecoin/FilecoinFeeParameters.swift b/BlockchainSdk/Blockchains/Filecoin/FilecoinFeeParameters.swift new file mode 100644 index 000000000..876b7de3c --- /dev/null +++ b/BlockchainSdk/Blockchains/Filecoin/FilecoinFeeParameters.swift @@ -0,0 +1,15 @@ +// +// FilecoinFeeParameters.swift +// BlockchainSdk +// +// Created by Aleksei Muraveinik on 30.08.24. +// Copyright © 2024 Tangem AG. All rights reserved. +// + +import BigInt + +struct FilecoinFeeParameters: FeeParameters { + let gasUnitPrice: BigUInt + let gasLimit: Int64 + let gasPremium: BigUInt +} diff --git a/BlockchainSdk/Blockchains/Filecoin/FilecoinTransactionBuilder.swift b/BlockchainSdk/Blockchains/Filecoin/FilecoinTransactionBuilder.swift new file mode 100644 index 000000000..534b9e274 --- /dev/null +++ b/BlockchainSdk/Blockchains/Filecoin/FilecoinTransactionBuilder.swift @@ -0,0 +1,95 @@ +// +// FilecoinTransactionBuilder.swift +// BlockchainSdk +// +// Created by Aleksei Muraveinik on 29.08.24. +// Copyright © 2024 Tangem AG. All rights reserved. +// + +import TangemSdk +import WalletCore + +enum FilecoinTransactionBuilderError: Error { + case filecoinFeeParametersNotFound + case failedToConvertAmountToBigUInt + case failedToGetDataFromJSON +} + +final class FilecoinTransactionBuilder { + private let wallet: Wallet + + init(wallet: Wallet) { + self.wallet = wallet + } + + func buildForSign(transaction: Transaction, nonce: UInt64) throws -> Data { + guard let feeParameters = transaction.fee.parameters as? FilecoinFeeParameters else { + throw FilecoinTransactionBuilderError.filecoinFeeParametersNotFound + } + + let input = try makeSigningInput(transaction: transaction, nonce: nonce, feeParameters: feeParameters) + let txInputData = try input.serializedData() + + let preImageHashes = TransactionCompiler.preImageHashes(coinType: .filecoin, txInputData: txInputData) + let preSigningOutput = try TxCompilerPreSigningOutput(serializedData: preImageHashes) + + return preSigningOutput.dataHash + } + + func buildForSend( + transaction: Transaction, + nonce: UInt64, + signatureInfo: SignatureInfo + ) throws -> FilecoinSignedTransactionBody { + guard let feeParameters = transaction.fee.parameters as? FilecoinFeeParameters else { + throw FilecoinTransactionBuilderError.filecoinFeeParametersNotFound + } + + let signatures = DataVector() + signatures.add(data: signatureInfo.signature) + + let publicKeys = DataVector() + publicKeys.add(data: try Secp256k1Key(with: signatureInfo.publicKey).decompress()) + + let input = try makeSigningInput(transaction: transaction, nonce: nonce, feeParameters: feeParameters) + let txInputData = try input.serializedData() + + let compiledWithSignatures = TransactionCompiler.compileWithSignatures( + coinType: .filecoin, + txInputData: txInputData, + signatures: signatures, + publicKeys: publicKeys + ) + + let signingOutput = try FilecoinSigningOutput(serializedData: compiledWithSignatures) + + guard let jsonData = signingOutput.json.data(using: .utf8) else { + throw FilecoinTransactionBuilderError.failedToGetDataFromJSON + } + + return try JSONDecoder().decode(FilecoinSignedTransactionBody.self, from: jsonData) + } + + private func makeSigningInput( + transaction: Transaction, + nonce: UInt64, + feeParameters: FilecoinFeeParameters + ) throws -> FilecoinSigningInput { + guard let value = transaction.amount.bigUIntValue else { + throw FilecoinTransactionBuilderError.failedToConvertAmountToBigUInt + } + + return try FilecoinSigningInput.with { input in + input.to = transaction.destinationAddress + input.nonce = nonce + + input.value = value.serialize() + + input.gasFeeCap = feeParameters.gasUnitPrice.serialize() + input.gasLimit = feeParameters.gasLimit + input.gasPremium = feeParameters.gasPremium.serialize() + + input.publicKey = try Secp256k1Key(with: wallet.publicKey.blockchainKey).decompress() + } + } +} diff --git a/BlockchainSdk/Blockchains/Filecoin/Network/DTO/FilecoinSignedTransactionBody.swift b/BlockchainSdk/Blockchains/Filecoin/Network/DTO/FilecoinSignedTransactionBody.swift index 23a834d8e..e58d36ddb 100644 --- a/BlockchainSdk/Blockchains/Filecoin/Network/DTO/FilecoinSignedTransactionBody.swift +++ b/BlockchainSdk/Blockchains/Filecoin/Network/DTO/FilecoinSignedTransactionBody.swift @@ -8,8 +8,8 @@ import Foundation -struct FilecoinSignedTransactionBody: Encodable { - struct Signature: Encodable { +struct FilecoinSignedTransactionBody: Codable, Equatable { + struct Signature: Codable, Equatable { let type: Int let signature: String diff --git a/BlockchainSdk/Blockchains/Filecoin/Network/DTO/FilecoinTransactionBody.swift b/BlockchainSdk/Blockchains/Filecoin/Network/DTO/FilecoinTransactionBody.swift index 21187eb85..f0127535e 100644 --- a/BlockchainSdk/Blockchains/Filecoin/Network/DTO/FilecoinTransactionBody.swift +++ b/BlockchainSdk/Blockchains/Filecoin/Network/DTO/FilecoinTransactionBody.swift @@ -8,7 +8,7 @@ import Foundation -struct FilecoinTransactionBody: Encodable { +struct FilecoinTransactionBody: Codable, Equatable { let sourceAddress: String let destinationAddress: String let amount: String diff --git a/BlockchainSdkTests/Filecoin/FilecoinTransactionBuilderTests.swift b/BlockchainSdkTests/Filecoin/FilecoinTransactionBuilderTests.swift new file mode 100644 index 000000000..9ca0e5564 --- /dev/null +++ b/BlockchainSdkTests/Filecoin/FilecoinTransactionBuilderTests.swift @@ -0,0 +1,103 @@ +// +// FilecoinTransactionBuilderTests.swift +// BlockchainSdkTests +// +// Created by Aleksei Muraveinik on 29.08.24. +// Copyright © 2024 Tangem AG. All rights reserved. +// + +import XCTest +@testable import BlockchainSdk + +final class FilecoinTransactionBuilderTests: XCTestCase { + private let transactionBuilder = FilecoinTransactionBuilder( + wallet: Wallet( + blockchain: .filecoin, + addresses: [ + .default: PlainAddress( + value: Constants.sourceAddress, + publicKey: Wallet.PublicKey( + seedKey: Constants.publicKey, + derivationType: nil + ), + type: .default + ) + ] + ) + ) + + private var transaction: Transaction { + Transaction( + amount: Amount( + with: .filecoin, + type: .coin, + value: 0.01 + ), + fee: Fee( + Amount( + with: .filecoin, + type: .coin, + value: (101225 * 1526328) / Blockchain.filecoin.decimalValue + ), + parameters: FilecoinFeeParameters( + gasUnitPrice: 101225, + gasLimit: 1526328, + gasPremium: 50612 + ) + ), + sourceAddress: Constants.sourceAddress, + destinationAddress: Constants.destinationAddress, + changeAddress: Constants.sourceAddress + ) + } + + func testBuildForSign() throws { + let expected = Data(hex: "BEB93CCF5C85273B327AC5DCDD58CBF3066F57FC84B87CD20DC67DF69EC2D0A9") + let actual = try transactionBuilder.buildForSign(transaction: transaction, nonce: 2) + + XCTAssertEqual(expected, actual) + } + + func testBuildForSend() throws { + let nonce: UInt64 = 2 + let expected = FilecoinSignedTransactionBody( + transactionBody: FilecoinTransactionBody( + sourceAddress: Constants.sourceAddress, + destinationAddress: Constants.destinationAddress, + amount: "10000000000000000", + nonce: nonce, + gasUnitPrice: "101225", + gasLimit: 1526328, + gasPremium: "50612" + ), + signature: FilecoinSignedTransactionBody.Signature( + type: 1, + signature: "Bogel9o9zvXUT+sC+nVpciGyHfBxWG6V4+xOawP6YrAU1OIbifvEHpRT/Elakv2X6mfUkbQzparvc2HyJBbXRwE=" + ) + ) + + let hashToSign = try transactionBuilder.buildForSign(transaction: transaction, nonce: nonce) + + let actual = try transactionBuilder.buildForSend( + transaction: transaction, + nonce: nonce, + signatureInfo: SignatureInfo( + signature: Constants.signature, + publicKey: Constants.publicKey, + hash: hashToSign + ) + ) + + XCTAssertEqual(expected, actual) + } +} + +private extension FilecoinTransactionBuilderTests { + enum Constants { + static let publicKey = Data(hex: "0374D0F81F42DDFE34114D533E95E6AE5FE6EA271C96F1FA505199FDC365AE9720") + static let signature = Data(hex: "06881E97DA3DCEF5D44FEB02FA75697221B21DF071586E95E3EC4E6B03FA62B014D4E21B89FBC41E9453FC495A92FD97EA67D491B433A5AAEF7361F22416D74701") + + static let sourceAddress = "f1flbddhx4vwox3y3ux5bwgsgq2frzeiuvvdrjo7i" + static let destinationAddress = "f1rluskhwvv5b3z36skltu4noszbc5stfihevbf2i" + } +}