diff --git a/Example/myWeb3Wallet/myWeb3Wallet.xcodeproj/project.pbxproj b/Example/myWeb3Wallet/myWeb3Wallet.xcodeproj/project.pbxproj index 224ad4a5b..9db196a6e 100644 --- a/Example/myWeb3Wallet/myWeb3Wallet.xcodeproj/project.pbxproj +++ b/Example/myWeb3Wallet/myWeb3Wallet.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -18,7 +18,7 @@ FA5308422721D59D002C1F06 /* myWeb3WalletUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA5308412721D59D002C1F06 /* myWeb3WalletUITests.swift */; }; FA5308442721D59D002C1F06 /* myWeb3WalletUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA5308432721D59D002C1F06 /* myWeb3WalletUITestsLaunchTests.swift */; }; FA5308512721F5BC002C1F06 /* SplashViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA5308502721F5BC002C1F06 /* SplashViewController.swift */; }; - FA5308562721F647002C1F06 /* WalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA5308552721F647002C1F06 /* WalletViewController.swift */; }; + FA5308562721F647002C1F06 /* AuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA5308552721F647002C1F06 /* AuthViewController.swift */; }; FAF01E912722D848002CEE01 /* DashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF01E902722D848002CEE01 /* DashboardViewController.swift */; }; FAF01E942722E6F1002CEE01 /* simulator_screenshot_9B13C8F6-BE7A-4919-966A-9E69A2953F31.png in Resources */ = {isa = PBXBuildFile; fileRef = FAF01E932722E6F1002CEE01 /* simulator_screenshot_9B13C8F6-BE7A-4919-966A-9E69A2953F31.png */; }; FAF01E962722E713002CEE01 /* simulator_screenshot_38EB4733-C3DB-4280-BAF2-9ED97440A8AD.png in Resources */ = {isa = PBXBuildFile; fileRef = FAF01E952722E713002CEE01 /* simulator_screenshot_38EB4733-C3DB-4280-BAF2-9ED97440A8AD.png */; }; @@ -61,7 +61,7 @@ FA5308412721D59D002C1F06 /* myWeb3WalletUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = myWeb3WalletUITests.swift; sourceTree = ""; }; FA5308432721D59D002C1F06 /* myWeb3WalletUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = myWeb3WalletUITestsLaunchTests.swift; sourceTree = ""; }; FA5308502721F5BC002C1F06 /* SplashViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewController.swift; sourceTree = ""; }; - FA5308552721F647002C1F06 /* WalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletViewController.swift; sourceTree = ""; }; + FA5308552721F647002C1F06 /* AuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewController.swift; sourceTree = ""; }; FAF01E902722D848002CEE01 /* DashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardViewController.swift; sourceTree = ""; }; FAF01E932722E6F1002CEE01 /* simulator_screenshot_9B13C8F6-BE7A-4919-966A-9E69A2953F31.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "simulator_screenshot_9B13C8F6-BE7A-4919-966A-9E69A2953F31.png"; sourceTree = ""; }; FAF01E952722E713002CEE01 /* simulator_screenshot_38EB4733-C3DB-4280-BAF2-9ED97440A8AD.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "simulator_screenshot_38EB4733-C3DB-4280-BAF2-9ED97440A8AD.png"; sourceTree = ""; }; @@ -69,6 +69,12 @@ FAF01E992722E73D002CEE01 /* simulator_screenshot_10F04849-E85A-41AB-8A17-D3FC394285AE.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "simulator_screenshot_10F04849-E85A-41AB-8A17-D3FC394285AE.png"; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 59490A822D3820A5004908B6 /* Core */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Core; sourceTree = ""; }; + 59490A9F2D3836D7004908B6 /* WalletViewController */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = WalletViewController; sourceTree = ""; }; + 59490AA22D385847004908B6 /* SendViewController */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SendViewController; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ FA53081A2721D59B002C1F06 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -129,6 +135,7 @@ FA53081F2721D59B002C1F06 /* myWeb3Wallet */ = { isa = PBXGroup; children = ( + 59490A822D3820A5004908B6 /* Core */, FAF01E922722E673002CEE01 /* ScreenShot */, FA5308532721F615002C1F06 /* ViewControllers */, FA5308522721F5C8002C1F06 /* Appdelegate */, @@ -170,19 +177,21 @@ FA5308532721F615002C1F06 /* ViewControllers */ = { isa = PBXGroup; children = ( - FA5308542721F62E002C1F06 /* WalletController */, + 59490AA22D385847004908B6 /* SendViewController */, + 59490A9F2D3836D7004908B6 /* WalletViewController */, + FA5308542721F62E002C1F06 /* AuthController */, ); path = ViewControllers; sourceTree = ""; }; - FA5308542721F62E002C1F06 /* WalletController */ = { + FA5308542721F62E002C1F06 /* AuthController */ = { isa = PBXGroup; children = ( FA5308502721F5BC002C1F06 /* SplashViewController.swift */, - FA5308552721F647002C1F06 /* WalletViewController.swift */, + FA5308552721F647002C1F06 /* AuthViewController.swift */, FAF01E902722D848002CEE01 /* DashboardViewController.swift */, ); - path = WalletController; + path = AuthController; sourceTree = ""; }; FAF01E922722E673002CEE01 /* ScreenShot */ = { @@ -211,6 +220,11 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 59490A822D3820A5004908B6 /* Core */, + 59490A9F2D3836D7004908B6 /* WalletViewController */, + 59490AA22D385847004908B6 /* SendViewController */, + ); name = myWeb3Wallet; packageProductDependencies = ( D6DD90D12991966100EE140E /* web3swift */, @@ -342,7 +356,7 @@ FA5308212721D59B002C1F06 /* AppDelegate.swift in Sources */, FA5308232721D59B002C1F06 /* SceneDelegate.swift in Sources */, FA5308512721F5BC002C1F06 /* SplashViewController.swift in Sources */, - FA5308562721F647002C1F06 /* WalletViewController.swift in Sources */, + FA5308562721F647002C1F06 /* AuthViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Example/myWeb3Wallet/myWeb3Wallet/Base.lproj/Main.storyboard b/Example/myWeb3Wallet/myWeb3Wallet/Base.lproj/Main.storyboard index 59341573b..1bb490a86 100644 --- a/Example/myWeb3Wallet/myWeb3Wallet/Base.lproj/Main.storyboard +++ b/Example/myWeb3Wallet/myWeb3Wallet/Base.lproj/Main.storyboard @@ -57,10 +57,10 @@ - + - + diff --git a/Example/myWeb3Wallet/myWeb3Wallet/Core/Network.swift b/Example/myWeb3Wallet/myWeb3Wallet/Core/Network.swift new file mode 100644 index 000000000..169be32f0 --- /dev/null +++ b/Example/myWeb3Wallet/myWeb3Wallet/Core/Network.swift @@ -0,0 +1,22 @@ +// +// Network.swift +// myWeb3Wallet +// +// Created by 6od9i on 15/01/25. +// + +import Foundation + +struct Network { + /// Id of chain + let chainId: Int + /// Name of the network + let name: String + /// Some rpc api paths - for network provider + let networkRPC: String + /// Path to network explorer like https://bscscan.com/ + let explorer: String? + + /// list of tokens added in this network + var tokens: [Token] +} diff --git a/Example/myWeb3Wallet/myWeb3Wallet/Core/Token.swift b/Example/myWeb3Wallet/myWeb3Wallet/Core/Token.swift new file mode 100644 index 000000000..d59e98e8c --- /dev/null +++ b/Example/myWeb3Wallet/myWeb3Wallet/Core/Token.swift @@ -0,0 +1,18 @@ +// +// Token.swift +// myWeb3Wallet +// +// Created by 6od9i on 15/01/25. +// + +import Foundation + +struct Token { + var isNative: Bool = false + /// Token symbol, for example - "ETH"/"USDT" + let symbol: String + /// Token contract address + let address: String + /// Decimals number + let decimals: Int +} diff --git a/Example/myWeb3Wallet/myWeb3Wallet/Core/WalletChainsModel.swift b/Example/myWeb3Wallet/myWeb3Wallet/Core/WalletChainsModel.swift new file mode 100644 index 000000000..a1a3b30cf --- /dev/null +++ b/Example/myWeb3Wallet/myWeb3Wallet/Core/WalletChainsModel.swift @@ -0,0 +1,64 @@ +// +// WalletChainsModel.swift +// myWeb3Wallet +// +// Created by 6od9i on 15/01/25. +// + +import Foundation + +struct WalletChainsModel { + static let networks: [Network] = [ + Network(chainId: 1, name: "Ethereum", + networkRPC: "https://ethereum-rpc.publicnode.com", + explorer: "https://etherscan.io/", tokens: [ + Token(isNative: true, + symbol: "ETH", + address: "0x0", + decimals: 18), + Token(symbol: "USDT", + address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + decimals: 6), + Token(symbol: "USDC", + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + decimals: 6), + Token(symbol: "BTC", + address: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + decimals: 8) + ]), + Network(chainId: 56, name: "Binance Smart Chain", + networkRPC: "https://bsc-dataseed.binance.org/", + explorer: "https://bscscan.com/", tokens: [ + Token(isNative: true, + symbol: "BNB", + address: "0x0", + decimals: 18), + Token(symbol: "USDT", + address: "0x55d398326f99059fF775485246999027B3197955", + decimals: 18), + Token(symbol: "USDC", + address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + decimals: 18), + Token(symbol: "BTC", + address: "0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c", + decimals: 18) + ]), + Network(chainId: 137, name: "Polygon", + networkRPC: "https://polygon.llamarpc.com", + explorer: "https://polygonscan.com/", tokens: [ + Token(isNative: true, + symbol: "POL", + address: "0x0", + decimals: 18), + Token(symbol: "USDT", + address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + decimals: 6), + Token(symbol: "USDC", + address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + decimals: 6), + Token(symbol: "WBTC", + address: "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6", + decimals: 8) + ]) + ] +} diff --git a/Example/myWeb3Wallet/myWeb3Wallet/Core/WalletManager.swift b/Example/myWeb3Wallet/myWeb3Wallet/Core/WalletManager.swift new file mode 100644 index 000000000..2f8ab62c1 --- /dev/null +++ b/Example/myWeb3Wallet/myWeb3Wallet/Core/WalletManager.swift @@ -0,0 +1,56 @@ +// +// WalletManager.swift +// myWeb3Wallet +// +// Created by 6od9i on 15/01/25. +// + +import Foundation +import web3swift +import Web3Core +import BigInt + +final class WalletManager { + static let keystorePassword = "password" + + /// Container with private keys + private let keystoreManager: KeystoreManager + + private(set) var networks: [Web3Network] = [] + + let address: EthereumAddress + + init(keystoreManager: KeystoreManager) async { + self.keystoreManager = keystoreManager + self.address = keystoreManager.addresses!.first! + + for model in WalletChainsModel.networks { + let network = Networks.Custom(networkID: BigUInt(model.chainId)) + guard let providerURL = URL(string: model.networkRPC), + let provider = try? await Web3HttpProvider(url: providerURL, network: network, + keystoreManager: keystoreManager) + else { continue } + + let web3 = web3swift.Web3(provider: provider) + networks.append(Web3Network(network: model, web3: web3)) + } + } + + func loadBalances() async { + for network in networks { + if let nativeBalance = try? await network.web3.eth.getBalance(for: address), + let nativeSymbol = network.network.tokens.first(where: { $0.isNative })?.symbol { + network.tokensBalances[nativeSymbol] = nativeBalance + } + for token in network.network.tokens { + guard token.isNative == false, + let contract = network.web3.contract(Web3.Utils.erc20ABI, at: EthereumAddress(token.address)), + let operation = contract.createReadOperation("balanceOf", parameters: [address]), + let result = try? await operation.callContractMethod(), + let balance = result["balance"] as? BigUInt + else { continue } + network.tokensBalances[token.symbol] = balance + } + } + } +} diff --git a/Example/myWeb3Wallet/myWeb3Wallet/Core/Web3Network.swift b/Example/myWeb3Wallet/myWeb3Wallet/Core/Web3Network.swift new file mode 100644 index 000000000..10da9bfac --- /dev/null +++ b/Example/myWeb3Wallet/myWeb3Wallet/Core/Web3Network.swift @@ -0,0 +1,25 @@ +// +// Web3Network.swift +// myWeb3Wallet +// +// Created by 6od9i on 20/01/25. +// + +import Foundation +import web3swift +import Web3Core +import BigInt + +final class Web3Network { + let network: Network + + /// web3 - sign, request and etc + let web3: Web3 + + var tokensBalances: [String: BigUInt] = [:] + + init(network: Network, web3: Web3) { + self.network = network + self.web3 = web3 + } +} diff --git a/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/AuthController/AuthViewController.swift b/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/AuthController/AuthViewController.swift new file mode 100644 index 000000000..a02fa19b6 --- /dev/null +++ b/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/AuthController/AuthViewController.swift @@ -0,0 +1,139 @@ +// +// AuthViewController.swift +// myWeb3Wallet +// +// Created by Ravi Ranjan on 22/10/21. +// + +import UIKit +import web3swift +import Web3Core + +class AuthViewController: UIViewController { + + @IBOutlet weak var continueButton: UIButton! + @IBOutlet weak var walletAddressLabel: UILabel! + @IBOutlet weak var importWalletButton: UIButton! + @IBOutlet weak var createWalletButton: UIButton! + + var walletAddress: String? { + didSet { + self.walletAddressLabel.text = walletAddress + } + } + + override func viewDidLoad() { + super.viewDidLoad() + self.createWalletButton.layer.cornerRadius = 5.0 + self.importWalletButton.layer.cornerRadius = 5.0 + } + + @IBAction func onClickCreateWallet(_ sender: UIButton) { + self.createMnemonics() + + } + @IBAction func onClickImportWalletButton(_ sender: UIButton) { + self.showImportAlert() + } + + @IBAction func onClickContinueButton(_ sender: UIButton) { + } +} + +extension AuthViewController { + func showImportAlert() { + let alert = UIAlertController(title: "MyWeb3Wallet", message: "", preferredStyle: .alert) + alert.addTextField { textfied in + textfied.placeholder = "Enter mnemonics/private Key" + } + let mnemonicsAction = UIAlertAction(title: "Mnemonics", style: .default) { _ in + guard let mnemonics = alert.textFields?[0].text else { return } + self.importWalletWith(mnemonics: mnemonics) + } + let privateKeyAction = UIAlertAction(title: "Private Key", style: .default) { _ in + guard let privateKey = alert.textFields?[0].text else { return } + self.importWalletWith(privateKey: privateKey) + + } + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) + alert.addAction(mnemonicsAction) + alert.addAction(privateKeyAction) + alert.addAction(cancelAction) + self.present(alert, animated: true, completion: nil) + } + + fileprivate func createMnemonics() { + guard let mnemonics = try? BIP39.generateMnemonics(bitsOfEntropy: 256, language: .english) else { + self.showAlertMessage(title: "", message: "We are unable to create wallet", actionName: "Ok") + return + } + print(mnemonics) + + guard let keystore = try? BIP32Keystore(mnemonics: mnemonics, password: WalletManager.keystorePassword), + let walletAddress = keystore.addresses?.first else { + self.showAlertMessage(title: "", message: "Unable to create wallet", actionName: "Ok") + return + } + self.walletAddress = walletAddress.address + let privateKey = try! keystore.UNSAFE_getPrivateKeyData(password: WalletManager.keystorePassword, + account: walletAddress) + print(privateKey) + + Task { + let walletManager = await WalletManager(keystoreManager: KeystoreManager([keystore])) + openWallet(walletManager: walletManager) + } + } + + func importWalletWith(privateKey: String) { + let formattedKey = privateKey.trimmingCharacters(in: .whitespacesAndNewlines) + guard let dataKey = Data.fromHex(formattedKey) else { + self.showAlertMessage(title: "Error", message: "Please enter a valid Private key ", actionName: "Ok") + return + } + do { + guard let keystore = try EthereumKeystoreV3(privateKey: dataKey, password: WalletManager.keystorePassword), + let address = keystore.addresses?.first?.address else { + throw NSError(domain: "Unknown", code: 400) + } + self.walletAddress = address + Task { @MainActor in + let walletManager = await WalletManager(keystoreManager: KeystoreManager([keystore])) + openWallet(walletManager: walletManager) + } + } catch { + let alert = UIAlertController(title: "Error", message: "Please enter correct Private key", preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .destructive) + alert.addAction(okAction) + self.present(alert, animated: true) + } + + } + + func importWalletWith(mnemonics: String) { + guard let keystore = try? BIP32Keystore(mnemonics: mnemonics, password: WalletManager.keystorePassword), + let walletAddress = keystore.addresses?.first else { + self.showAlertMessage(title: "", message: "Unable to create wallet", actionName: "Ok") + return + } + self.walletAddress = walletAddress.address + Task { @MainActor in + let walletManager = await WalletManager(keystoreManager: KeystoreManager([keystore])) + openWallet(walletManager: walletManager) + } + } + + func openWallet(walletManager: WalletManager) { + let walletVC = WalletViewController(walletManager: walletManager) + navigationController?.setViewControllers([walletVC], animated: true) + } +} + +extension UIViewController { + func showAlertMessage(title: String = "MyWeb3Wallet", message: String = "Message is empty", actionName: String = "OK") { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let action = UIAlertAction.init(title: actionName, style: .destructive) + alertController.addAction(action) + self.present(alertController, animated: true) + } +} diff --git a/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletController/DashboardViewController.swift b/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/AuthController/DashboardViewController.swift similarity index 100% rename from Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletController/DashboardViewController.swift rename to Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/AuthController/DashboardViewController.swift diff --git a/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletController/SplashViewController.swift b/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/AuthController/SplashViewController.swift similarity index 91% rename from Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletController/SplashViewController.swift rename to Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/AuthController/SplashViewController.swift index 593b1281e..23c4db8b9 100644 --- a/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletController/SplashViewController.swift +++ b/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/AuthController/SplashViewController.swift @@ -23,7 +23,7 @@ class SplashViewController: UIViewController { } @objc func moveToWalletView() { - guard let walletScreen = self.storyboard?.instantiateViewController(withIdentifier: "WalletViewController") as? WalletViewController else { + guard let walletScreen = self.storyboard?.instantiateViewController(withIdentifier: "WalletViewController") as? AuthViewController else { #if DEBUG printContent("Unable to get Wallet controller") #endif diff --git a/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/SendViewController/SendViewController.swift b/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/SendViewController/SendViewController.swift new file mode 100644 index 000000000..b45b1d227 --- /dev/null +++ b/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/SendViewController/SendViewController.swift @@ -0,0 +1,231 @@ +// +// SendViewController.swift +// myWeb3Wallet +// +// Created by 6od9i on 15/01/25. +// + +import UIKit +import Web3Core +import web3swift +import BigInt + +final class SendViewController: UIViewController { + private let walletManager: WalletManager + private let network: Web3Network + private let token: Token + + private let sendButton: UIButton = { + let button = UIButton() + button.setTitle("Send", for: .normal) + button.setTitleColor(.white, for: .normal) + button.backgroundColor = .systemBlue + button.layer.cornerRadius = 10 + return button + }() + + private let closeButton: UIButton = { + let button = UIButton() + button.setTitle("Close", for: .normal) + button.setTitleColor(.white, for: .normal) + button.backgroundColor = .systemBlue + button.layer.cornerRadius = 10 + return button + }() + + var address: EthereumAddress? { + didSet { + sendButton.isEnabled = amount != nil && address != nil + } + } + + var amount: BigUInt? { + didSet { + sendButton.isEnabled = amount != nil && address != nil + } + } + + let addressField: UITextField = { + let field = UITextField() + field.placeholder = "Address" + field.borderStyle = .roundedRect + return field + }() + + let amountField: UITextField = { + let field = UITextField() + field.placeholder = "Amount" + field.borderStyle = .roundedRect + field.keyboardType = .decimalPad + return field + }() + + init(walletManager: WalletManager, network: Web3Network, token: Token) { + self.walletManager = walletManager + self.network = network + self.token = token + super.init(nibName: nil, bundle: nil) + sendButton.isEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + + [closeButton, addressField, amountField, sendButton].forEach { + view.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + } + + closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16).isActive = true + closeButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true + + addressField.topAnchor.constraint(equalTo: closeButton.bottomAnchor, constant: 16).isActive = true + addressField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true + addressField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16).isActive = true + + amountField.topAnchor.constraint(equalTo: addressField.bottomAnchor, constant: 16).isActive = true + amountField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true + amountField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16).isActive = true + + sendButton.topAnchor.constraint(equalTo: amountField.bottomAnchor, + constant: 24).isActive = true + sendButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + sendButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.3).isActive = true + + [addressField, amountField].forEach { + $0.addTarget(self, action: #selector(textChanged), for: .editingChanged) + } + + sendButton.addTarget(self, action: #selector(send), for: .touchUpInside) + closeButton.addTarget(self, action: #selector(close), for: .touchUpInside) + } + + @objc func close() { + navigationController?.popViewController(animated: true) + } + + @objc func textChanged() { + let address = addressField.text?.trimmingCharacters(in: .whitespacesAndNewlines) + let amount = amountField.text?.trimmingCharacters(in: .whitespacesAndNewlines) + + self.address = if let address { EthereumAddress(address) } else { nil } + self.amount = if let amount, + let amountBigInt = Utilities.parseToBigUInt(amount, decimals: token.decimals) { + amountBigInt + } else { + nil + } + } + + @objc func send() { + Task { @MainActor in + do { + try await sendToken() + } catch { + let alertController = UIAlertController(title: "Error", message: error.localizedDescription, + preferredStyle: .alert) + let action = UIAlertAction.init(title: "Cancel", style: .destructive) + alertController.addAction(action) + self.present(alertController, animated: true) + } + } + } + + func sendToken() async throws { + guard let address, let amount else { return } + + var transaction: CodableTransaction = .emptyTransaction + /// from = address of your wallet for sending + transaction.from = walletManager.address + transaction.gasPrice = try await network.web3.eth.gasPrice() + + var writeOperation: WriteOperation + + if token.isNative { + transaction.to = address + transaction.value = amount /// for native token use token amount here + + /// for native token could be created "fallback" transaction from coldWalletABI + /// in this case address for contract is your address, because native tokens not used contracts and stored on your native wallet + + let contract = network.web3.contract(Web3.Utils.coldWalletABI, at: address, abiVersion: 2) + + transaction.gasPrice = nil + transaction.gasLimit = 0 + contract?.transaction = transaction + + /// have no parameters in this function + guard let nativeOperation = contract?.createWriteOperation("fallback", parameters: []) else { + throw NSError(domain: "Unknown", code: 400) + } + writeOperation = nativeOperation + } else { + guard let tokenAddress = EthereumAddress(token.address) else { + throw NSError(domain: "Unknown", code: 400) + } + + transaction.to = tokenAddress /// Address of token contract for call transfer + transaction.value = 0 /// Value is value of native token, when call contract for transfer - should be 0 + + /// Tokens in EVM used erc20 should be sended with "transfer" metnod + /// it should be called on contract by address of token, ! not owner address and not target address ! + let contract = network.web3.contract(Web3.Utils.erc20ABI, at: EthereumAddress(token.address), + abiVersion: 2) + + transaction.gasPrice = nil + transaction.gasLimit = 0 + contract?.transaction = transaction + + /// Write operation of "transfer" for estimate gas should be created with 2 params - target address and amount of token + guard let tokenOperation = contract?.createWriteOperation( + "transfer", + parameters: [address as AnyObject, amount as AnyObject]) else { + throw NSError(domain: "Unknown", code: 400) + } + writeOperation = tokenOperation + } + + transaction.gasLimit = try await network.web3.eth.estimateGas(for: writeOperation.transaction) + transaction.gasPrice = try await network.web3.eth.gasPrice() + + let policies = Policies(noncePolicy: .latest, + gasLimitPolicy: .manual(transaction.gasLimit), + gasPricePolicy: .manual(transaction.gasPrice ?? 0), + maxFeePerGasPolicy: .automatic, + maxPriorityFeePerGasPolicy: .automatic) + + let hash = try await writeOperation.writeToChain(password: WalletManager.keystorePassword, + policies: policies).hash + print(hash) + try await checkTransaction(hash: hash) + } + + func checkTransaction(hash: String) async throws { + var txStatus = TransactionReceipt.TXStatus.notYetProcessed + while txStatus == .notYetProcessed { + sleep(1) + guard let receipt = try? await network.web3.eth.transactionReceipt(Data(hex: hash)) else { continue } + txStatus = receipt.status + } + + let alertController = UIAlertController(title: txStatus == .ok ? "Complete" : "Failed", + message: "TX Hash: \(hash)", + preferredStyle: .alert) + let copyAction = UIAlertAction.init(title: "Copy hash and close", style: .default) { _ in + UIPasteboard.general.string = hash + self.navigationController?.popViewController(animated: true) + } + alertController.addAction(copyAction) + + let action = UIAlertAction.init(title: "Close", style: .destructive) { _ in + self.navigationController?.popViewController(animated: true) + } + alertController.addAction(action) + self.present(alertController, animated: true) + } +} diff --git a/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletController/WalletViewController.swift b/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletController/WalletViewController.swift deleted file mode 100644 index 12e560853..000000000 --- a/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletController/WalletViewController.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// WalletViewController.swift -// myWeb3Wallet -// -// Created by Ravi Ranjan on 22/10/21. -// - -import UIKit -import web3swift -import Web3Core - -class WalletViewController: UIViewController { - - @IBOutlet weak var continueButton: UIButton! - @IBOutlet weak var walletAddressLabel: UILabel! - @IBOutlet weak var importWalletButton: UIButton! - @IBOutlet weak var createWalletButton: UIButton! - var _walletAddress: String { - set { - self.continueButton.isHidden = false - self.walletAddressLabel.text = newValue - } - get { - return self._walletAddress - } - } - var _mnemonics: String = "" - override func viewDidLoad() { - super.viewDidLoad() - self.createWalletButton.layer.cornerRadius = 5.0 - self.importWalletButton.layer.cornerRadius = 5.0 - - // Do any additional setup after loading the view. - } - - @IBAction func onClickCreateWallet(_ sender: UIButton) { -#if DEBUG - print("Clicked on Create Wallet Option") -#endif - self.createMnemonics() - - } - @IBAction func onClickImportWalletButton(_ sender: UIButton) { - print("Clicked on import Wallet Option") - self.showImportALert() - } - - @IBAction func onClickContinueButton(_ sender: UIButton) { - print("Clicked on COntinue button") - guard let dashboardScreen = self.storyboard?.instantiateViewController(withIdentifier: "DashboardViewController") as? DashboardViewController else { -#if DEBUG - printContent("Unable to get Wallet controller") -#endif - return - } - self.navigationController?.pushViewController(dashboardScreen, animated: true) - } - fileprivate func showImportALert() { - let alert = UIAlertController(title: "MyWeb3Wallet", message: "", preferredStyle: .alert) - alert.addTextField { textfied in - textfied.placeholder = "Enter mnemonics/private Key" - } - let mnemonicsAction = UIAlertAction(title: "Mnemonics", style: .default) { _ in - print("Clicked on Mnemonics Option") - guard let mnemonics = alert.textFields?[0].text else { return } - print(mnemonics) - } - let privateKeyAction = UIAlertAction(title: "Private Key", style: .default) { _ in - print("Clicked on Private Key Wallet Option") - guard let privateKey = alert.textFields?[0].text else { return } - print(privateKey) - self.importWalletWith(privateKey: privateKey) - - } - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) - alert.addAction(mnemonicsAction) - alert.addAction(privateKeyAction) - alert.addAction(cancelAction) - self.present(alert, animated: true, completion: nil) - } - func importWalletWith(privateKey: String) { - let formattedKey = privateKey.trimmingCharacters(in: .whitespacesAndNewlines) - guard let dataKey = Data.fromHex(formattedKey) else { - self.showAlertMessage(title: "Error", message: "Please enter a valid Private key ", actionName: "Ok") - return - } - do { - let keystore = try EthereumKeystoreV3(privateKey: dataKey, password: "") - if let myWeb3KeyStore = keystore { - let manager = KeystoreManager([myWeb3KeyStore]) - let address = keystore?.addresses?.first -#if DEBUG - print("Address :::>>>>> ", address) - print("Address :::>>>>> ", manager.addresses) -#endif - let walletAddress = manager.addresses?.first?.address - self.walletAddressLabel.text = walletAddress ?? "0x" - - print(walletAddress) - } else { - print("error") - } - } catch { -#if DEBUG - print("error creating keyStore") - print("Private key error.") -#endif - let alert = UIAlertController(title: "Error", message: "Please enter correct Private key", preferredStyle: .alert) - let okAction = UIAlertAction(title: "OK", style: .destructive) - alert.addAction(okAction) - self.present(alert, animated: true) - } - - } - - func importWalletWith(mnemonics: String) { - let walletAddress = try? BIP32Keystore(mnemonics: mnemonics, password: "", prefixPath: "m/44'/77777'/0'/0") - self.walletAddressLabel.text = "\(walletAddress?.addresses?.first?.address ?? "0x")" - } -} - -extension WalletViewController { - - fileprivate func createMnemonics() { - let userDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] - let web3KeystoreManager = KeystoreManager.managerForPath(userDir + "/keystore") - do { - if web3KeystoreManager?.addresses?.count ?? 0 >= 0 { - let tempMnemonics = try? BIP39.generateMnemonics(bitsOfEntropy: 256, language: .english) - guard let tMnemonics = tempMnemonics else { - self.showAlertMessage(title: "", message: "We are unable to create wallet", actionName: "Ok") - return - } - self._mnemonics = tMnemonics - print(_mnemonics) - - let tempWalletAddress = try? BIP32Keystore(mnemonics: self._mnemonics, password: "", prefixPath: "m/44'/77777'/0'/0") - guard let walletAddress = tempWalletAddress?.addresses?.first else { - self.showAlertMessage(title: "", message: "Unable to create wallet", actionName: "Ok") - return - } - self._walletAddress = walletAddress.address - let privateKey = try tempWalletAddress?.UNSAFE_getPrivateKeyData(password: "", account: walletAddress) -#if DEBUG - print(privateKey, "Is the private key") -#endif - let keyData = try? JSONEncoder().encode(tempWalletAddress?.keystoreParams) - FileManager.default.createFile(atPath: userDir + "/keystore" + "/key.json", contents: keyData, attributes: nil) - } - } catch { - - } - - } -} -extension UIViewController { - func showAlertMessage(title: String = "MyWeb3Wallet", message: String = "Message is empty", actionName: String = "OK") { - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let action = UIAlertAction.init(title: actionName, style: .destructive) - alertController.addAction(action) - self.present(alertController, animated: true) - } - -} diff --git a/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletViewController/Cells/TokenCell.swift b/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletViewController/Cells/TokenCell.swift new file mode 100644 index 000000000..6d9375a61 --- /dev/null +++ b/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletViewController/Cells/TokenCell.swift @@ -0,0 +1,77 @@ +// +// TokenCell.swift +// myWeb3Wallet +// +// Created by 6od9i on 15/01/25. +// + +import UIKit + +final class TokenCell: UITableViewCell { + var name: String = "" { + didSet { nameLabel.text = name } + } + + var network: String = "" { + didSet { networkLabel.text = network } + } + + var balance: String = "" { + didSet { balanceLabel.text = balance } + } + + private let nameLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 16) + label.textColor = .black + label.numberOfLines = 1 + return label + }() + + private let networkLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 14) + label.textColor = .darkGray + label.numberOfLines = 1 + return label + }() + + private let balanceLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 16) + label.textColor = .black + label.textAlignment = .right + label.numberOfLines = 1 + return label + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupViews() { + [nameLabel, networkLabel, balanceLabel].forEach { + addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + } + + nameLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16).isActive = true + nameLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16).isActive = true + nameLabel.trailingAnchor.constraint(equalTo: balanceLabel.trailingAnchor, constant: -16).isActive = true + + balanceLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + balanceLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) + + balanceLabel.topAnchor.constraint(equalTo: nameLabel.topAnchor).isActive = true + balanceLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16).isActive = true + + networkLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8).isActive = true + networkLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor).isActive = true + networkLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16).isActive = true + } +} diff --git a/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletViewController/Cells/WalletCell.swift b/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletViewController/Cells/WalletCell.swift new file mode 100644 index 000000000..574e9b618 --- /dev/null +++ b/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletViewController/Cells/WalletCell.swift @@ -0,0 +1,56 @@ +// +// WalletCell.swift +// myWeb3Wallet +// +// Created by 6od9i on 15/01/25. +// + +import UIKit + +class WalletCell: UITableViewCell { + var address: String = "" { + didSet { addressLabel.text = address } + } + + private let addressLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 16) + label.textColor = .black + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 16, weight: .semibold) + label.text = "Wallet" + label.textColor = .black + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupViews() { + [titleLabel, addressLabel].forEach { + addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + } + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16).isActive = true + titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true + + addressLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8).isActive = true + addressLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16).isActive = true + addressLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16).isActive = true + addressLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16).isActive = true + } +} diff --git a/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletViewController/WalletViewController.swift b/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletViewController/WalletViewController.swift new file mode 100644 index 000000000..43fe7de07 --- /dev/null +++ b/Example/myWeb3Wallet/myWeb3Wallet/ViewControllers/WalletViewController/WalletViewController.swift @@ -0,0 +1,154 @@ +// +// WalletViewController.swift +// myWeb3Wallet +// +// Created by 6od9i on 15/01/25. +// + +import UIKit +import Web3Core +import BigInt + +final class WalletViewController: UIViewController { + private let walletManager: WalletManager + + private let sendButton: UIButton = { + let button = UIButton() + button.setTitle("Send", for: .normal) + button.setTitleColor(.white, for: .normal) + button.backgroundColor = .systemBlue + button.layer.cornerRadius = 10 + return button + }() + + private let tableView: UITableView = { + let tableView = UITableView() + tableView.register(TokenCell.self, forCellReuseIdentifier: "TokenCell") + tableView.register(WalletCell.self, forCellReuseIdentifier: "WalletCell") + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 100 + tableView.allowsSelection = false + tableView.refreshControl = UIRefreshControl() + return tableView + }() + + init(walletManager: WalletManager) { + self.walletManager = walletManager + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + + [tableView, sendButton].forEach { + view.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + } + + tableView.bottomAnchor.constraint(equalTo: sendButton.topAnchor).isActive = true + tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + + sendButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, + constant: -16).isActive = true + sendButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + sendButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.3).isActive = true + + tableView.delegate = self + tableView.dataSource = self + tableView.refreshControl?.addTarget(self, action: #selector(loadBalances), for: .valueChanged) + loadBalances() + + sendButton.addTarget(self, action: #selector(send), for: .touchUpInside) + } + + @objc func send() { + selectChain() + } + func selectChain() { + let alertController = UIAlertController(title: "Select chain", message: "", preferredStyle: .actionSheet) + for network in walletManager.networks { + let action = UIAlertAction(title: network.network.name, style: .default) { [weak self] _ in + self?.selectToken(network: network) + } + alertController.addAction(action) + } + + let action = UIAlertAction.init(title: "Cancel", style: .destructive) + alertController.addAction(action) + + self.present(alertController, animated: true) + } + + func selectToken(network: Web3Network) { + let alertController = UIAlertController(title: "Select token", message: "", preferredStyle: .actionSheet) + for token in network.network.tokens { + let action = UIAlertAction(title: token.symbol, style: .default) { [weak self] _ in + self?.sendToken(token, from: network) + } + alertController.addAction(action) + } + + let action = UIAlertAction.init(title: "Cancel", style: .destructive) + alertController.addAction(action) + + self.present(alertController, animated: true) + } + + func sendToken(_ token: Token, from network: Web3Network) { + let sendVC = SendViewController(walletManager: walletManager, network: network, token: token) + navigationController?.pushViewController(sendVC, animated: true) + } +} + +extension WalletViewController { + @objc func loadBalances() { + Task { @MainActor in + await walletManager.loadBalances() + tableView.refreshControl?.endRefreshing() + tableView.reloadData() + } + } +} + +extension WalletViewController: UITableViewDelegate, UITableViewDataSource { + func numberOfSections(in tableView: UITableView) -> Int { + walletManager.networks.count + 1 + } + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 0 { return 1 } + return walletManager.networks[section - 1].network.tokens.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if indexPath.section == 0 { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "WalletCell", for: indexPath) as? WalletCell + else { fatalError() } + cell.address = walletManager.address.address + return cell + } else { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "TokenCell", for: indexPath) as? TokenCell + else { fatalError() } + let network = walletManager.networks[indexPath.section - 1] + let token = network.network.tokens[indexPath.row] + cell.network = network.network.name + cell.name = token.symbol + + if let balance = network.tokensBalances[token.symbol] { + let doubleString = Utilities.formatToPrecision(balance, units: .custom(token.decimals), + formattingDecimals: 10) + cell.balance = doubleString + } else { + cell.balance = "Loading..." + } + return cell + } + + } +}