diff --git a/Multisig.xcodeproj/project.pbxproj b/Multisig.xcodeproj/project.pbxproj index 595016dbf..66849d376 100644 --- a/Multisig.xcodeproj/project.pbxproj +++ b/Multisig.xcodeproj/project.pbxproj @@ -274,7 +274,6 @@ 04FEFE8128C6304B0028A349 /* TokenDistributionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 04FEFE7F28C6304B0028A349 /* TokenDistributionViewController.xib */; }; 0A007124254B255C00A57CAF /* UIFont+Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A007123254B255C00A57CAF /* UIFont+Styles.swift */; }; 0A00712A254B379000A57CAF /* UIColor+Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A007129254B379000A57CAF /* UIColor+Styles.swift */; }; - 0A01DCE42770B0A100A87D7A /* TransactionIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A01DCE32770B0A100A87D7A /* TransactionIntegrationTests.swift */; }; 0A0391CB27C565D4003C871A /* IconButtonTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0A0391C727C565D3003C871A /* IconButtonTableViewCell.xib */; }; 0A0391CC27C565D4003C871A /* HelpTextTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A0391C827C565D3003C871A /* HelpTextTableViewCell.swift */; }; 0A0391CD27C565D4003C871A /* HelpTextTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0A0391C927C565D3003C871A /* HelpTextTableViewCell.xib */; }; @@ -1026,6 +1025,8 @@ B32620492A961E690003A2F0 /* AddSafeFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32620482A961E690003A2F0 /* AddSafeFlow.swift */; }; B343BEB32A77DDA1006BF46B /* DMSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0491067C28F99246005A4A99 /* DMSans-Bold.ttf */; }; B35BFD0F2A937DC000A9FB15 /* NavigationRouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35BFD0E2A937DC000A9FB15 /* NavigationRouterTests.swift */; }; + B367AED02B0CB69200F06B86 /* TransactionValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B367AECE2B0CB59700F06B86 /* TransactionValidationTests.swift */; }; + B367AED22B0CB89600F06B86 /* TransactionValidationTestCase1.json in Resources */ = {isa = PBXBuildFile; fileRef = B367AED12B0CB89600F06B86 /* TransactionValidationTestCase1.json */; }; B3710B3D2AD584BE002E503B /* SecureConfig in Frameworks */ = {isa = PBXBuildFile; productRef = B3710B3C2AD584BE002E503B /* SecureConfig */; }; B3710B442AD5AAD7002E503B /* config.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B3710B432AD5AAD7002E503B /* config.bundle */; }; B3B6044F2A850F5E007BDAC0 /* UIAlertControllerStyle+Multiplatform.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B6044E2A850F5E007BDAC0 /* UIAlertControllerStyle+Multiplatform.swift */; }; @@ -2082,6 +2083,8 @@ B300E8CA2AF3A7A90073A908 /* SafeNoncesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeNoncesRequest.swift; sourceTree = ""; }; B32620482A961E690003A2F0 /* AddSafeFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSafeFlow.swift; sourceTree = ""; }; B35BFD0E2A937DC000A9FB15 /* NavigationRouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterTests.swift; sourceTree = ""; }; + B367AECE2B0CB59700F06B86 /* TransactionValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionValidationTests.swift; sourceTree = ""; }; + B367AED12B0CB89600F06B86 /* TransactionValidationTestCase1.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = TransactionValidationTestCase1.json; sourceTree = ""; }; B3710B3B2AD58492002E503B /* SecureConfig */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SecureConfig; path = Packages/SecureConfig; sourceTree = ""; }; B3710B432AD5AAD7002E503B /* config.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = config.bundle; sourceTree = ""; }; B3710B452AD5AE9D002E503B /* apis-prod.example.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "apis-prod.example.json"; sourceTree = ""; }; @@ -3477,6 +3480,8 @@ 6A91EFD827DA2C45009E63E9 /* Data */, 0A3DFF002667D7DA00B45770 /* CoreData */, 0A64313F247ED1AA006FD30A /* MultisigIntegrationTests.xctestplan */, + B367AECE2B0CB59700F06B86 /* TransactionValidationTests.swift */, + B367AED12B0CB89600F06B86 /* TransactionValidationTestCase1.json */, 0A802B8F24E581A50001790F /* SafeClientGatewayServiceIntegrationTests.swift */, 0A513A9C2768EBC900F07D5A /* DelegateKeyTests.swift */, 5532D4D02449A1E40067505A /* MockLogger.swift */, @@ -5219,6 +5224,7 @@ 0A61E8452670E632009D68A4 /* __Snapshots__ in Resources */, 6A91EFDA27DA2C8D009E63E9 /* wc_registry_wallets.json in Resources */, 0AA5F55D28BF667000D6D220 /* claiming_test_fixtures.json in Resources */, + B367AED22B0CB89600F06B86 /* TransactionValidationTestCase1.json in Resources */, 0A74719A269F152D008E9F2D /* chains_4_safes_0x1230B3d59858296A31053C1b8562Ecf89A2f888b_balances_usd.json in Resources */, 0A3DFEFE2667D3A700B45770 /* README.md in Resources */, ); @@ -5996,13 +6002,13 @@ 0A9BC35F246058F800EB9C5D /* MockLogger.swift in Sources */, 550A6182268A23A9002C02E1 /* SafeNetworkMigrationTests.swift in Sources */, 0A7B3518288071EB00EC04EC /* TestCoreDataStack.swift in Sources */, - 0A01DCE42770B0A100A87D7A /* TransactionIntegrationTests.swift in Sources */, 0A65522B28816B9900701238 /* TestingAppDelegate.swift in Sources */, 0A61E83E2670DED5009D68A4 /* BalanceTableViewCellTests.swift in Sources */, 0A3DFEF72667D1C000B45770 /* CoreDataTestCase.swift in Sources */, 0A06532F27B68AF40008C5F1 /* WebConnectionRepositoryTests.swift in Sources */, 0A6552282881642300701238 /* UIIntegrationTestCase.swift in Sources */, 0A802B9024E581A50001790F /* SafeClientGatewayServiceIntegrationTests.swift in Sources */, + B367AED02B0CB69200F06B86 /* TransactionValidationTests.swift in Sources */, 0AC62ED628783700007945A1 /* CreatePasscodeFlowTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Multisig/UI/Transaction/TransactionDetailsViewController/TransactionDetailCellBuilder.swift b/Multisig/UI/Transaction/TransactionDetailsViewController/TransactionDetailCellBuilder.swift index c991cfb50..d9310ec4b 100644 --- a/Multisig/UI/Transaction/TransactionDetailsViewController/TransactionDetailCellBuilder.swift +++ b/Multisig/UI/Transaction/TransactionDetailsViewController/TransactionDetailCellBuilder.swift @@ -49,17 +49,17 @@ class TransactionDetailCellBuilder { tableView.registerCell(WarningTableViewCell.self) } - func build(_ tx: SCGModels.TransactionDetails, _ safe: Safe?) -> [UITableViewCell] { + func build(_ tx: SCGModels.TransactionDetails) -> [UITableViewCell] { result = [] - buildTransaction(tx, safe) + buildTransaction(tx) return result } - func buildTransaction(_ tx: SCGModels.TransactionDetails, _ safe: Safe? = nil) { + func buildTransaction(_ tx: SCGModels.TransactionDetails) { let isCreationTx = buildCreationTx(tx) if !isCreationTx { + buildWarning(tx) buildHeader(tx) - // buildWarning(tx, safe) buildAssetContract(tx) buildStatus(tx) buildMultisigInfo(tx) @@ -433,15 +433,14 @@ class TransactionDetailCellBuilder { isOutgoing: isOutgoing) } - // TODO: Move this out into a class or function and Unit-Test it and / or Integration Test it. - func buildWarning(_ tx: SCGModels.TransactionDetails, _ safe: Safe? = nil) { + func validate(tx: SCGModels.TransactionDetails, safe: Safe) throws { guard tx.txStatus.isAwatingConfiramtions || tx.txStatus == .awaitingExecution else { // don't warn outside of signature or execution requests return } // safe has owners - guard let safe = safe, let ownersInfo = safe.ownersInfo, !ownersInfo.isEmpty else { + guard let ownersInfo = safe.ownersInfo, !ownersInfo.isEmpty else { // not enough data for further checks return } @@ -454,16 +453,14 @@ class TransactionDetailCellBuilder { } guard !txMultisigInfo.confirmations.isEmpty else { - warningCell("Warning: transaction has no confirmations. This may be a dangerous transaction") - return + throw "Transaction has no confirmations. This may be a dangerous transaction" } // all confirming addresses are from safe owners guard txMultisigInfo.confirmations.allSatisfy({ confirmation in ownerAddresses.contains(confirmation.signer.value.address) }) else { - warningCell("Warning: not all confirmations are from safe owners.") - return + throw "Not all confirmations are from safe owners." } // transaction hash is valid @@ -482,8 +479,7 @@ class TransactionDetailCellBuilder { let computedSafeTxHash = transaction.safeTransactionHash(), transaction.safeTxHash == computedSafeTxHash else { - warningCell("Warning: safeTxHash is invalid. This may be a dangerous transaction.") - return + throw "Invalid safeTxHash. This may be a dangerous transaction." } // all confirming signatures are from a confirming addresses @@ -493,9 +489,9 @@ class TransactionDetailCellBuilder { return nil } - let v: UInt8 = signature[0] - let r: Data /* 32 bytes */ = signature[1...32] - let s: Data /* 32 bytes */ = signature[33...64] + let r: Data /* 32 bytes */ = signature[0..<32] + let s: Data /* 32 bytes */ = signature[32..<64] + let v: UInt8 = signature[64] let contractSignature: UInt8 = 0 let approvedHashSignature: UInt8 = 1 @@ -522,9 +518,11 @@ class TransactionDetailCellBuilder { return owner default: + let message = transaction.encodeTransactionData() + let pubKey = try? EthereumPublicKey( - message: computedSafeTxHash.hash.makeBytes(), - v: EthereumQuantity(quantity: BigUInt(v)), + message: message.makeBytes(), + v: EthereumQuantity(quantity: v >= 27 ? BigUInt(v) - 27 : BigUInt(v)), r: EthereumQuantity(r.makeBytes()), s: EthereumQuantity(s.makeBytes()) ) @@ -537,15 +535,16 @@ class TransactionDetailCellBuilder { let owner = (try? Sol.Address(data: r)).map { Address($0) } return owner default: + let message = transaction.encodeTransactionData() + let pubKey = try? EthereumPublicKey( - message: computedSafeTxHash.hash.makeBytes(), - v: EthereumQuantity(quantity: BigUInt(v)), + message: message.makeBytes(), + v: EthereumQuantity(quantity: v >= 27 ? BigUInt(v) - 27 : BigUInt(v)), r: EthereumQuantity(r.makeBytes()), s: EthereumQuantity(s.makeBytes()) ) let owner = pubKey.map(\.address).map(Address.init) return owner - } } } @@ -553,17 +552,21 @@ class TransactionDetailCellBuilder { guard txMultisigInfo.confirmations.allSatisfy({ confirmation in signer(of: confirmation.signature.data) == confirmation.signer.value.address }) else { - warningCell("Warning: not all signatures are from safe's owners.") - return + throw "Not all signatures are from safe's owners. This may be a dangerous transaction." } - - // all good! no warnings. } - func warningCell(_ text: String) { - let cell = newCell(WarningTableViewCell.self) - cell.set(title: text) - result.append(cell) + func buildWarning(_ tx: SCGModels.TransactionDetails) { + do { + guard let aSafe = Safe.by(address: tx.safeAddress.description, chainId: chain.id!) else { + return + } + try validate(tx: tx, safe: aSafe) + } catch { + let cell = newCell(WarningTableViewCell.self) + cell.set(title: "Warning!", description: error.localizedDescription) + result.append(cell) + } } diff --git a/Multisig/UI/Transaction/TransactionDetailsViewController/TransactionDetailsViewController.swift b/Multisig/UI/Transaction/TransactionDetailsViewController/TransactionDetailsViewController.swift index 826555425..5a90b3920 100644 --- a/Multisig/UI/Transaction/TransactionDetailsViewController/TransactionDetailsViewController.swift +++ b/Multisig/UI/Transaction/TransactionDetailsViewController/TransactionDetailsViewController.swift @@ -511,7 +511,7 @@ class TransactionDetailsViewController: LoadableViewController, UITableViewDataS self.tx = transformer.transformed(transaction: self.tx!) trackScreen() - cells = builder.build(self.tx!, safe) + cells = builder.build(self.tx!) } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { diff --git a/MultisigIntegrationTests/TransactionIntegrationTests.swift b/MultisigIntegrationTests/TransactionIntegrationTests.swift index 59496bf2d..4c7351443 100644 --- a/MultisigIntegrationTests/TransactionIntegrationTests.swift +++ b/MultisigIntegrationTests/TransactionIntegrationTests.swift @@ -161,7 +161,7 @@ class TransactionIntegrationTests: XCTestCase { continueAfterFailure = false let privateKey = try PrivateKey(data: Data(hex: "0xe7979e5f2ceb1d4ef76019d1fdba88b50ceefe0575bbfdf94969837c50a5d895")) - let input = ERC20.transfer( + let callData = ERC20.transfer( // eoa address // to: "Ad302A4b09402b41EC3Fb4981B63E4Dd141fed6d", // safe address @@ -170,7 +170,7 @@ class TransactionIntegrationTests: XCTestCase { value: 1000 ).encode() - print("call data", input.toHexStringWithPrefix()) + print("call data", callData.toHexStringWithPrefix()) let minerTip = Eth.Amount(value: 2, unit: Eth.Unit.gigawei).converted(to: Eth.Unit.wei).value @@ -180,7 +180,7 @@ class TransactionIntegrationTests: XCTestCase { // erc20 contract address (LOVE) to: "b3a4Bc89d8517E0e2C9B66703d09D3029ffa1e6d", // erc20 transfer call - input: Sol.Bytes(storage: input), + input: Sol.Bytes(storage: callData), fee: Eth.Fee1559( maxFeePerGas: minerTip, maxPriorityFee: minerTip @@ -405,7 +405,7 @@ class TransactionIntegrationTests: XCTestCase { confirmation.signature.data }.joined() - let input = try GnosisSafe_v1_3_0.execTransaction( + let callData = try GnosisSafe_v1_3_0.execTransaction( to: Sol.Address(txData.to.value.data32), value: Sol.UInt256(txData.value.data32), data: Sol.Bytes(storage: txData.hexData?.data ?? Data()), @@ -419,7 +419,7 @@ class TransactionIntegrationTests: XCTestCase { signatures: Sol.Bytes(storage: Data(signatures)) ).encode() - print("call data", input.toHexStringWithPrefix()) + print("call data", callData.toHexStringWithPrefix()) let minerTip = Eth.Amount(value: 2, unit: Eth.Unit.gigawei).converted(to: Eth.Unit.wei).value @@ -427,7 +427,7 @@ class TransactionIntegrationTests: XCTestCase { chainId: 4, from: "728cafe9fB8CC2218Fb12a9A2D9335193caa07e0", to: "dd1D27C114aB45e8A650B251eDFA1b0795bbe020", - input: Sol.Bytes(storage: input), + input: Sol.Bytes(storage: callData), fee: Eth.Fee1559( maxFeePerGas: minerTip, maxPriorityFee: minerTip diff --git a/MultisigIntegrationTests/TransactionValidationTestCase1.json b/MultisigIntegrationTests/TransactionValidationTestCase1.json new file mode 100644 index 000000000..ab8bf6d69 --- /dev/null +++ b/MultisigIntegrationTests/TransactionValidationTestCase1.json @@ -0,0 +1,219 @@ +{ + "testCaseName": "Valid transaction, Ethereum, 2/4 owners, 1/2 confirmations", + "chain": { + "chainId": "1", + "chainName": "Ethereum", + "description": "The main Ethereum network", + "l2": false, + "nativeCurrency": { + "name": "Ether", + "symbol": "ETH", + "decimals": 18, + "logoUri": "https://safe-transaction-assets.safe.global/chains/1/currency_logo.png" + }, + "transactionService": "https://safe-transaction-mainnet.safe.global", + "blockExplorerUriTemplate": { + "address": "https://etherscan.io/address/{{address}}", + "txHash": "https://etherscan.io/tx/{{txHash}}", + "api": "https://api.etherscan.io/api?module={{module}}&action={{action}}&address={{address}}&apiKey={{apiKey}}" + }, + "disabledWallets": [ + "NONE", + "opera", + "operaTouch", + "safeMobile", + "socialSigner", + "tally", + "trust", + "walletConnect" + ], + "ensRegistryAddress": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", + "features": [ + "CONTRACT_INTERACTION", + "DEFAULT_TOKENLIST", + "DOMAIN_LOOKUP", + "EIP1271", + "EIP1559", + "ERC721", + "MOONPAY_MOBILE", + "NATIVE_WALLETCONNECT", + "PUSH_NOTIFICATIONS", + "RISK_MITIGATION", + "SAFE_APPS", + "SAFE_TX_GAS_OPTIONAL", + "SOCIAL_LOGIN", + "SPENDING_LIMIT", + "TX_SIMULATION" + ], + "gasPrice": [ + { + "type": "oracle", + "uri": "https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=JNFAU892RF9TJWBU3EV7DJCPIWZY8KEMY1", + "gasParameter": "FastGasPrice", + "gweiFactor": "1000000000.000000000" + } + ], + "publicRpcUri": { + "authentication": "NO_AUTHENTICATION", + "value": "https://cloudflare-eth.com" + }, + "rpcUri": { + "authentication": "API_KEY_PATH", + "value": "https://mainnet.infura.io/v3/" + }, + "safeAppsRpcUri": { + "authentication": "API_KEY_PATH", + "value": "https://mainnet.infura.io/v3/" + }, + "shortName": "eth", + "theme": { + "textColor": "#001428", + "backgroundColor": "#DDDDDD" + } + }, + "safe": { + "address": { + "value": "0xfF501B324DC6d78dC9F983f140B9211c3EdB4dc7", + "name": null, + "logoUri": null + }, + "chainId": "1", + "nonce": 20, + "threshold": 2, + "owners": [ + { + "value": "0x9F87C1aCaF3Afc6a5557c58284D9F8609470b571", + "name": null, + "logoUri": null + }, + { + "value": "0x8712128BEA09C9687Df05A5D692F3750F8086C81", + "name": null, + "logoUri": null + }, + { + "value": "0x80F59C1D46EFC1Bb18F0AaEc132b77266f00Be9a", + "name": null, + "logoUri": null + }, + { + "value": "0x9F7dfAb2222A473284205cdDF08a677726d786A0", + "name": null, + "logoUri": null + } + ], + "implementation": { + "value": "0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F", + "name": "Safe 1.1.1", + "logoUri": "https://safe-transaction-assets.safe.global/contracts/logos/0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F.png" + }, + "implementationVersionState": "OUTDATED", + "collectiblesTag": "1663062476", + "txQueuedTag": "1670938242", + "txHistoryTag": "1697840747", + "messagesTag": "1700560947", + "modules": [ + { + "value": "0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134", + "name": null, + "logoUri": null + } + ], + "fallbackHandler": { + "value": "0xd5D82B6aDDc9027B22dCA772Aa68D5d74cdBdF44", + "name": "Safe: DefaultCallbackHandler 1.1.1", + "logoUri": "https://safe-transaction-assets.safe.global/contracts/logos/0xd5D82B6aDDc9027B22dCA772Aa68D5d74cdBdF44.png" + }, + "guard": null, + "version": "1.1.1" + }, + "tx": { + "safeAddress": "0xfF501B324DC6d78dC9F983f140B9211c3EdB4dc7", + "txId": "multisig_0xfF501B324DC6d78dC9F983f140B9211c3EdB4dc7_0xaed54190962dce2c448391841fcd7730eb23ac18c5fa2a7896ac9074bf38f127", + "executedAt": null, + "txStatus": "AWAITING_CONFIRMATIONS", + "txInfo": { + "type": "Custom", + "humanDescription": null, + "richDecodedInfo": null, + "to": { + "value": "0xfF501B324DC6d78dC9F983f140B9211c3EdB4dc7", + "name": null, + "logoUri": null + }, + "dataSize": "0", + "value": "0", + "methodName": null, + "actionCount": null, + "isCancellation": false + }, + "txData": { + "hexData": null, + "dataDecoded": null, + "to": { + "value": "0xfF501B324DC6d78dC9F983f140B9211c3EdB4dc7", + "name": null, + "logoUri": null + }, + "value": "0", + "operation": 0, + "trustedDelegateCallTarget": null, + "addressInfoIndex": null + }, + "txHash": null, + "detailedExecutionInfo": { + "type": "MULTISIG", + "submittedAt": 1614014118782, + "nonce": 20, + "safeTxGas": "43808", + "baseGas": "0", + "gasPrice": "0", + "gasToken": "0x0000000000000000000000000000000000000000", + "refundReceiver": { + "value": "0x0000000000000000000000000000000000000000", + "name": null, + "logoUri": null + }, + "safeTxHash": "0xaed54190962dce2c448391841fcd7730eb23ac18c5fa2a7896ac9074bf38f127", + "executor": null, + "signers": [ + { + "value": "0x9F87C1aCaF3Afc6a5557c58284D9F8609470b571", + "name": null, + "logoUri": null + }, + { + "value": "0x8712128BEA09C9687Df05A5D692F3750F8086C81", + "name": null, + "logoUri": null + }, + { + "value": "0x80F59C1D46EFC1Bb18F0AaEc132b77266f00Be9a", + "name": null, + "logoUri": null + }, + { + "value": "0x9F7dfAb2222A473284205cdDF08a677726d786A0", + "name": null, + "logoUri": null + } + ], + "confirmationsRequired": 2, + "confirmations": [ + { + "signer": { + "value": "0x8712128BEA09C9687Df05A5D692F3750F8086C81", + "name": null, + "logoUri": null + }, + "signature": "0x7dec7a1c46320f9bc4b62fe22e6ad34fe1f54b08c39637d064d104def4f3c812591ad2da837f5d9049d74e038811f76fcb33ac739e7ef866f696abdc9e55e1f51b", + "submittedAt": 1614014118798 + } + ], + "rejectors": [], + "gasTokenInfo": null, + "trusted": true + }, + "safeAppInfo": null + } +} diff --git a/MultisigIntegrationTests/TransactionValidationTests.swift b/MultisigIntegrationTests/TransactionValidationTests.swift new file mode 100644 index 000000000..4a7ef9ad8 --- /dev/null +++ b/MultisigIntegrationTests/TransactionValidationTests.swift @@ -0,0 +1,98 @@ +// +// TransactionValidationTests.swift +// MultisigTests +// +// Created by Dmitrii Bespalov on 21.11.23. +// Copyright © 2023 Gnosis Ltd. All rights reserved. +// + +import XCTest +@testable import Multisig +import SafeWeb3 + +final class TransactionValidationTests: CoreDataTestCase { + + struct TestCase: Decodable { + var chain: SCGModels.Chain + var safe: SCGModels.SafeInfoExtended + var tx: SCGModels.TransactionDetails + + init(chain: SCGModels.Chain, safe: SCGModels.SafeInfoExtended, tx: SCGModels.TransactionDetails) { + self.chain = chain + self.safe = safe + self.tx = tx + } + + init?(named name: String, extension ext: String = "json") { + let bundle = Bundle(for: TransactionValidationTests.self) + guard let url = bundle.url(forResource: name, withExtension: ext) else { return nil } + let jsonDecoder = JSONDecoder() + do { + let data = try Data(contentsOf: url) + self = try jsonDecoder.decode(TestCase.self, from: data) + } catch { + print("[TestCase] Failed to load: \(error)") + return nil + } + } + } + + + // test cases + + // prereqs: + // status == awaiting + // safe.owners.count > 0 + // safe.version is there + // safe.chain.id is there + // can convert tx to Transaction + // tx has multisig info + + // safeTxHash is correctly computed from properties -> invalid safeTxHash! + // confirmations.count == 0 -> no confirmations! + // confirmations not subset of owners -> not owners! + // one confirmation - must be a valid confirmation -> not owner! + // multiple confirmations - all must be valid confirmations, i.e. match the address -> not owner! + // just one invalid -> warning + // all invalid -> warning + // types for contract version >= 1.1.0 + // confirmation type = contract signature + // confirmation type = approvedHash + // confirmation type = eth_sign + // confirmation type = ecdsa + // types for contract version 1.1.0 + // confirmation type = contract signature + // confirmation type = approvedHash + // confirmation type = ecdsa + + func testTransactions() throws { + try validateTransactionCase("TransactionValidationTestCase1") + } + + func validateTransactionCase(_ testCaseName: String) throws { + guard let testCase = TestCase(named: testCaseName) else { + XCTFail("Failed to load test case") + return + } + + let chain = try Chain.create(testCase.chain) + let safe = Safe.create( + address: testCase.safe.address.value.description, + version: testCase.safe.version, + name: "Test", + chain: chain + ) + safe.update(from: testCase.safe) + + let vc = UIViewController() + let tableView = UITableView() + vc.view.addSubview(tableView) + + let builder = TransactionDetailCellBuilder(vc: vc, tableView: tableView, chain: chain) + + XCTAssertNoThrow( + try builder.validate(tx: testCase.tx, safe: safe) + ) + } + +}