diff --git a/src/Neo/Wallets/AssetDescriptor.cs b/src/Neo/Wallets/AssetDescriptor.cs index 7b9be7b16b..7fdfadc90e 100644 --- a/src/Neo/Wallets/AssetDescriptor.cs +++ b/src/Neo/Wallets/AssetDescriptor.cs @@ -50,22 +50,29 @@ public class AssetDescriptor /// The id of the asset. public AssetDescriptor(DataCache snapshot, ProtocolSettings settings, UInt160 asset_id) { - var contract = NativeContract.ContractManagement.GetContract(snapshot, asset_id); - if (contract is null) throw new ArgumentException(null, nameof(asset_id)); + try + { + var contract = NativeContract.ContractManagement.GetContract(snapshot, asset_id); + if (contract is null) throw new WalletException(WalletErrorType.ContractNotFound, nameof(asset_id)); - byte[] script; - using (ScriptBuilder sb = new()) + byte[] script; + using (ScriptBuilder sb = new()) + { + sb.EmitDynamicCall(asset_id, "decimals", CallFlags.ReadOnly); + sb.EmitDynamicCall(asset_id, "symbol", CallFlags.ReadOnly); + script = sb.ToArray(); + } + using ApplicationEngine engine = ApplicationEngine.Run(script, snapshot, settings: settings, gas: 0_30000000L); + if (engine.State != VMState.HALT) throw new WalletException(WalletErrorType.ExecutionFault, nameof(asset_id)); + AssetId = asset_id; + AssetName = contract.Manifest.Name; + Symbol = engine.ResultStack.Pop().GetString(); + Decimals = (byte)engine.ResultStack.Pop().GetInteger(); + } + catch (Exception ex) when (ex is not WalletException) { - sb.EmitDynamicCall(asset_id, "decimals", CallFlags.ReadOnly); - sb.EmitDynamicCall(asset_id, "symbol", CallFlags.ReadOnly); - script = sb.ToArray(); + throw WalletException.FromException(ex); } - using ApplicationEngine engine = ApplicationEngine.Run(script, snapshot, settings: settings, gas: 0_30000000L); - if (engine.State != VMState.HALT) throw new ArgumentException(null, nameof(asset_id)); - AssetId = asset_id; - AssetName = contract.Manifest.Name; - Symbol = engine.ResultStack.Pop().GetString(); - Decimals = (byte)engine.ResultStack.Pop().GetInteger(); } public override string ToString() diff --git a/src/Neo/Wallets/Helper.cs b/src/Neo/Wallets/Helper.cs index b8427d078d..46e455d7a9 100644 --- a/src/Neo/Wallets/Helper.cs +++ b/src/Neo/Wallets/Helper.cs @@ -36,7 +36,14 @@ public static class Helper /// The signature for the . public static byte[] Sign(this IVerifiable verifiable, KeyPair key, uint network) { - return Crypto.Sign(verifiable.GetSignData(network), key.PrivateKey); + try + { + return Crypto.Sign(verifiable.GetSignData(network), key.PrivateKey); + } + catch (Exception ex) + { + throw WalletException.FromException(ex); + } } /// @@ -61,17 +68,24 @@ public static string ToAddress(this UInt160 scriptHash, byte version) /// The converted script hash. public static UInt160 ToScriptHash(this string address, byte version) { - byte[] data = address.Base58CheckDecode(); - if (data.Length != 21) - throw new FormatException(); - if (data[0] != version) - throw new FormatException(); - return new UInt160(data.AsSpan(1)); + try + { + byte[] data = address.Base58CheckDecode(); + if (data.Length != 21) + throw new WalletException(WalletErrorType.FormatError, "Invalid address format: incorrect length."); + if (data[0] != version) + throw new WalletException(WalletErrorType.FormatError, "Invalid address version."); + return new UInt160(data.AsSpan(1)); + } + catch (Exception e) when (e is not WalletException) + { + throw new WalletException(WalletErrorType.FormatError, "Invalid address format."); + } } internal static byte[] XOR(byte[] x, byte[] y) { - if (x.Length != y.Length) throw new ArgumentException(); + if (x.Length != y.Length) throw new WalletException(WalletErrorType.InvalidOperation, "Arrays must have the same length."); byte[] r = new byte[x.Length]; for (int i = 0; i < r.Length; i++) r[i] = (byte)(x[i] ^ y[i]); @@ -90,78 +104,90 @@ internal static byte[] XOR(byte[] x, byte[] y) /// The network fee of the transaction. public static long CalculateNetworkFee(this Transaction tx, DataCache snapshot, ProtocolSettings settings, Func accountScript, long maxExecutionCost = ApplicationEngine.TestModeGas) { - UInt160[] hashes = tx.GetScriptHashesForVerifying(snapshot); - - // base size for transaction: includes const_header + signers + attributes + script + hashes - int size = Transaction.HeaderSize + tx.Signers.GetVarSize() + tx.Attributes.GetVarSize() + tx.Script.GetVarSize() + IO.Helper.GetVarSize(hashes.Length), index = -1; - uint exec_fee_factor = NativeContract.Policy.GetExecFeeFactor(snapshot); - long networkFee = 0; - foreach (UInt160 hash in hashes) + try { - index++; - byte[] witnessScript = accountScript(hash); - byte[] invocationScript = null; + UInt160[] hashes = tx.GetScriptHashesForVerifying(snapshot); - if (tx.Witnesses != null && witnessScript is null) + // base size for transaction: includes const_header + signers + attributes + script + hashes + int size = Transaction.HeaderSize + tx.Signers.GetVarSize() + tx.Attributes.GetVarSize() + tx.Script.GetVarSize() + IO.Helper.GetVarSize(hashes.Length), index = -1; + uint exec_fee_factor = NativeContract.Policy.GetExecFeeFactor(snapshot); + long networkFee = 0; + foreach (UInt160 hash in hashes) { - // Try to find the script in the witnesses - Witness witness = tx.Witnesses[index]; - witnessScript = witness?.VerificationScript.ToArray(); + index++; + byte[] witnessScript = accountScript(hash); + byte[] invocationScript = null; - if (witnessScript is null || witnessScript.Length == 0) + if (tx.Witnesses != null && witnessScript is null) { - // Then it's a contract-based witness, so try to get the corresponding invocation script for it - invocationScript = witness?.InvocationScript.ToArray(); - } - } + // Try to find the script in the witnesses + Witness witness = tx.Witnesses[index]; + witnessScript = witness?.VerificationScript.ToArray(); - if (witnessScript is null || witnessScript.Length == 0) - { - var contract = NativeContract.ContractManagement.GetContract(snapshot, hash); - if (contract is null) - throw new ArgumentException($"The smart contract or address {hash} is not found"); - var md = contract.Manifest.Abi.GetMethod(ContractBasicMethod.Verify, ContractBasicMethod.VerifyPCount); - if (md is null) - throw new ArgumentException($"The smart contract {contract.Hash} haven't got verify method"); - if (md.ReturnType != ContractParameterType.Boolean) - throw new ArgumentException("The verify method doesn't return boolean value."); - if (md.Parameters.Length > 0 && invocationScript is null) - throw new ArgumentException("The verify method requires parameters that need to be passed via the witness' invocation script."); + if (witnessScript is null || witnessScript.Length == 0) + { + // Then it's a contract-based witness, so try to get the corresponding invocation script for it + invocationScript = witness?.InvocationScript.ToArray(); + } + } + if (witnessScript is null || witnessScript.Length == 0) + { + var contract = NativeContract.ContractManagement.GetContract(snapshot, hash); + if (contract is null) + throw new WalletException(WalletErrorType.ContractNotFound, $"The smart contract or address {hash} is not found."); + var md = contract.Manifest.Abi.GetMethod(ContractBasicMethod.Verify, ContractBasicMethod.VerifyPCount); + if (md is null) + throw new WalletException(WalletErrorType.ContractError, $"The smart contract {contract.Hash} hasn't got a verify method."); + if (md.ReturnType != ContractParameterType.Boolean) + throw new WalletException(WalletErrorType.ContractError, "The verify method doesn't return boolean value."); + if (md.Parameters.Length > 0 && invocationScript is null) + throw new WalletException(WalletErrorType.ContractError, "The verify method requires parameters that need to be passed via the witness' invocation script."); - // Empty verification and non-empty invocation scripts - var invSize = invocationScript?.GetVarSize() ?? Array.Empty().GetVarSize(); - size += Array.Empty().GetVarSize() + invSize; + // Empty verification and non-empty invocation scripts + var invSize = invocationScript?.GetVarSize() ?? Array.Empty().GetVarSize(); + size += Array.Empty().GetVarSize() + invSize; - // Check verify cost - using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshot.CloneCache(), settings: settings, gas: maxExecutionCost); - engine.LoadContract(contract, md, CallFlags.ReadOnly); - if (invocationScript != null) engine.LoadScript(invocationScript, configureState: p => p.CallFlags = CallFlags.None); - if (engine.Execute() == VMState.FAULT) throw new ArgumentException($"Smart contract {contract.Hash} verification fault."); - if (!engine.ResultStack.Pop().GetBoolean()) throw new ArgumentException($"Smart contract {contract.Hash} returns false."); + // Check verify cost + using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshot.CloneCache(), settings: settings, gas: maxExecutionCost); + engine.LoadContract(contract, md, CallFlags.ReadOnly); + if (invocationScript != null) engine.LoadScript(invocationScript, configureState: p => p.CallFlags = CallFlags.None); + if (engine.Execute() == VMState.FAULT) throw new WalletException(WalletErrorType.ExecutionFault, $"Smart contract {contract.Hash} verification fault."); + if (!engine.ResultStack.Pop().GetBoolean()) throw new WalletException(WalletErrorType.VerificationFailed, $"Smart contract {contract.Hash} returns false."); - maxExecutionCost -= engine.FeeConsumed; - if (maxExecutionCost <= 0) throw new InvalidOperationException("Insufficient GAS."); - networkFee += engine.FeeConsumed; - } - else if (IsSignatureContract(witnessScript)) - { - size += 67 + witnessScript.GetVarSize(); - networkFee += exec_fee_factor * SignatureContractCost(); + maxExecutionCost -= engine.FeeConsumed; + if (maxExecutionCost <= 0) throw new WalletException(WalletErrorType.InsufficientFunds, "Insufficient GAS."); + networkFee += engine.FeeConsumed; + } + else if (IsSignatureContract(witnessScript)) + { + size += 67 + witnessScript.GetVarSize(); + networkFee += exec_fee_factor * SignatureContractCost(); + } + else if (IsMultiSigContract(witnessScript, out int m, out int n)) + { + int size_inv = 66 * m; + size += IO.Helper.GetVarSize(size_inv) + size_inv + witnessScript.GetVarSize(); + networkFee += exec_fee_factor * MultiSignatureContractCost(m, n); + } + // We can support more contract types in the future. } - else if (IsMultiSigContract(witnessScript, out int m, out int n)) + networkFee += size * NativeContract.Policy.GetFeePerByte(snapshot); + foreach (TransactionAttribute attr in tx.Attributes) { - int size_inv = 66 * m; - size += IO.Helper.GetVarSize(size_inv) + size_inv + witnessScript.GetVarSize(); - networkFee += exec_fee_factor * MultiSignatureContractCost(m, n); + networkFee += attr.CalculateNetworkFee(snapshot, tx); } - // We can support more contract types in the future. + return networkFee; } - networkFee += size * NativeContract.Policy.GetFeePerByte(snapshot); - foreach (TransactionAttribute attr in tx.Attributes) + catch (Exception ex) when (ex is not WalletException) { - networkFee += attr.CalculateNetworkFee(snapshot, tx); + throw new WalletException(WalletErrorType.UnknownError, ex.Message, ex); } - return networkFee; + } + + internal static void ThrowIfNull(object argument, string paramName) + { + if (argument == null) + throw new WalletException(WalletErrorType.ArgumentNull, $"Argument {paramName} cannot be null."); } } } diff --git a/src/Neo/Wallets/KeyPair.cs b/src/Neo/Wallets/KeyPair.cs index 7b15c0bbd0..cfc7546f93 100644 --- a/src/Neo/Wallets/KeyPair.cs +++ b/src/Neo/Wallets/KeyPair.cs @@ -47,7 +47,7 @@ public class KeyPair : IEquatable public KeyPair(byte[] privateKey) { if (privateKey.Length != 32 && privateKey.Length != 96 && privateKey.Length != 104) - throw new ArgumentException(null, nameof(privateKey)); + throw new WalletException(WalletErrorType.InvalidPrivateKey, nameof(privateKey)); PrivateKey = privateKey[^32..]; if (privateKey.Length == 32) { @@ -55,7 +55,15 @@ public KeyPair(byte[] privateKey) } else { - PublicKey = Cryptography.ECC.ECPoint.FromBytes(privateKey, Cryptography.ECC.ECCurve.Secp256r1); + try + { + PublicKey = Cryptography.ECC.ECPoint.FromBytes(privateKey, Cryptography.ECC.ECCurve.Secp256r1); + } + catch + { + throw new WalletException(WalletErrorType.InvalidPrivateKey, $"{nameof(privateKey)}."); + } + } } @@ -119,30 +127,44 @@ public string Export(string passphrase, byte version, int N = 16384, int r = 8, /// The private key in NEP-2 format. public string Export(byte[] passphrase, byte version, int N = 16384, int r = 8, int p = 8) { - UInt160 script_hash = Contract.CreateSignatureRedeemScript(PublicKey).ToScriptHash(); - string address = script_hash.ToAddress(version); - byte[] addresshash = Encoding.ASCII.GetBytes(address).Sha256().Sha256()[..4]; - byte[] derivedkey = SCrypt.Generate(passphrase, addresshash, N, r, p, 64); - byte[] derivedhalf1 = derivedkey[..32]; - byte[] derivedhalf2 = derivedkey[32..]; - byte[] encryptedkey = Encrypt(XOR(PrivateKey, derivedhalf1), derivedhalf2); - Span buffer = stackalloc byte[39]; - buffer[0] = 0x01; - buffer[1] = 0x42; - buffer[2] = 0xe0; - addresshash.CopyTo(buffer[3..]); - encryptedkey.CopyTo(buffer[7..]); - return Base58.Base58CheckEncode(buffer); + try + { + UInt160 script_hash = Contract.CreateSignatureRedeemScript(PublicKey).ToScriptHash(); + string address = script_hash.ToAddress(version); + byte[] addresshash = Encoding.ASCII.GetBytes(address).Sha256().Sha256()[..4]; + byte[] derivedkey = SCrypt.Generate(passphrase, addresshash, N, r, p, 64); + byte[] derivedhalf1 = derivedkey[..32]; + byte[] derivedhalf2 = derivedkey[32..]; + byte[] encryptedkey = Encrypt(XOR(PrivateKey, derivedhalf1), derivedhalf2); + Span buffer = stackalloc byte[39]; + buffer[0] = 0x01; + buffer[1] = 0x42; + buffer[2] = 0xe0; + addresshash.CopyTo(buffer[3..]); + encryptedkey.CopyTo(buffer[7..]); + return Base58.Base58CheckEncode(buffer); + } + catch (Exception e) + { + throw WalletException.FromException(e); + } } private static byte[] Encrypt(byte[] data, byte[] key) { - using Aes aes = Aes.Create(); - aes.Key = key; - aes.Mode = CipherMode.ECB; - aes.Padding = PaddingMode.None; - using ICryptoTransform encryptor = aes.CreateEncryptor(); - return encryptor.TransformFinalBlock(data, 0, data.Length); + try + { + using Aes aes = Aes.Create(); + aes.Key = key; + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + using ICryptoTransform encryptor = aes.CreateEncryptor(); + return encryptor.TransformFinalBlock(data, 0, data.Length); + } + catch (Exception e) + { + throw WalletException.FromException(e); + } } public override int GetHashCode() diff --git a/src/Neo/Wallets/NEP6/NEP6Account.cs b/src/Neo/Wallets/NEP6/NEP6Account.cs index 7998b9ea29..477ea8464d 100644 --- a/src/Neo/Wallets/NEP6/NEP6Account.cs +++ b/src/Neo/Wallets/NEP6/NEP6Account.cs @@ -41,14 +41,21 @@ public NEP6Account(NEP6Wallet wallet, UInt160 scriptHash, KeyPair key, string pa public static NEP6Account FromJson(JObject json, NEP6Wallet wallet) { - return new NEP6Account(wallet, json["address"].GetString().ToScriptHash(wallet.ProtocolSettings.AddressVersion), json["key"]?.GetString()) + try { - Label = json["label"]?.GetString(), - IsDefault = json["isDefault"].GetBoolean(), - Lock = json["lock"].GetBoolean(), - Contract = NEP6Contract.FromJson((JObject)json["contract"]), - Extra = json["extra"] - }; + return new NEP6Account(wallet, json["address"].GetString().ToScriptHash(wallet.ProtocolSettings.AddressVersion), json["key"]?.GetString()) + { + Label = json["label"]?.GetString(), + IsDefault = json["isDefault"].GetBoolean(), + Lock = json["lock"].GetBoolean(), + Contract = NEP6Contract.FromJson((JObject)json["contract"]), + Extra = json["extra"] + }; + } + catch (Exception e) when (e is not WalletException) + { + throw WalletException.FromException(e); + } } public override KeyPair GetKey() @@ -66,7 +73,14 @@ public KeyPair GetKey(string password) if (nep2key == null) return null; if (key == null) { - key = new KeyPair(Wallet.GetPrivateKeyFromNEP2(nep2key, password, ProtocolSettings.AddressVersion, wallet.Scrypt.N, wallet.Scrypt.R, wallet.Scrypt.P)); + try + { + key = new KeyPair(Wallet.GetPrivateKeyFromNEP2(nep2key, password, ProtocolSettings.AddressVersion, wallet.Scrypt.N, wallet.Scrypt.R, wallet.Scrypt.P)); + } + catch (Exception e) when (e is not WalletException) + { + throw new WalletException(WalletErrorType.PasswordIncorrect); + } } return key; } @@ -91,7 +105,7 @@ public bool VerifyPassword(string password) Wallet.GetPrivateKeyFromNEP2(nep2key, password, ProtocolSettings.AddressVersion, wallet.Scrypt.N, wallet.Scrypt.R, wallet.Scrypt.P); return true; } - catch (FormatException) + catch { return false; } diff --git a/src/Neo/Wallets/NEP6/NEP6Contract.cs b/src/Neo/Wallets/NEP6/NEP6Contract.cs index cecc80be7e..5df6bfab5c 100644 --- a/src/Neo/Wallets/NEP6/NEP6Contract.cs +++ b/src/Neo/Wallets/NEP6/NEP6Contract.cs @@ -24,28 +24,42 @@ internal class NEP6Contract : Contract public static NEP6Contract FromJson(JObject json) { if (json == null) return null; - return new NEP6Contract + try { - Script = Convert.FromBase64String(json["script"].AsString()), - ParameterList = ((JArray)json["parameters"]).Select(p => p["type"].GetEnum()).ToArray(), - ParameterNames = ((JArray)json["parameters"]).Select(p => p["name"].AsString()).ToArray(), - Deployed = json["deployed"].AsBoolean() - }; + return new NEP6Contract + { + Script = Convert.FromBase64String(json["script"].AsString()), + ParameterList = ((JArray)json["parameters"]).Select(p => p["type"].GetEnum()).ToArray(), + ParameterNames = ((JArray)json["parameters"]).Select(p => p["name"].AsString()).ToArray(), + Deployed = json["deployed"].AsBoolean() + }; + } + catch (Exception e) + { + throw WalletException.FromException(e); + } } public JObject ToJson() { - JObject contract = new(); - contract["script"] = Convert.ToBase64String(Script); - contract["parameters"] = new JArray(ParameterList.Zip(ParameterNames, (type, name) => + try + { + JObject contract = new() + { + ["script"] = Convert.ToBase64String(Script), + ["parameters"] = new JArray(ParameterList.Zip(ParameterNames, (type, name) => + { + JObject parameter = new() { ["name"] = name, ["type"] = type }; + return parameter; + })), + ["deployed"] = Deployed + }; + return contract; + } + catch (Exception e) { - JObject parameter = new(); - parameter["name"] = name; - parameter["type"] = type; - return parameter; - })); - contract["deployed"] = Deployed; - return contract; + throw WalletException.FromException(e); + } } } } diff --git a/src/Neo/Wallets/NEP6/NEP6Wallet.cs b/src/Neo/Wallets/NEP6/NEP6Wallet.cs index 17a59fc830..c60b382177 100644 --- a/src/Neo/Wallets/NEP6/NEP6Wallet.cs +++ b/src/Neo/Wallets/NEP6/NEP6Wallet.cs @@ -58,8 +58,15 @@ public NEP6Wallet(string path, string password, ProtocolSettings settings, strin this.password = password; if (File.Exists(path)) { - JObject wallet = (JObject)JToken.Parse(File.ReadAllBytes(path)); - LoadFromJson(wallet, out Scrypt, out accounts, out extra); + try + { + JObject wallet = (JObject)JToken.Parse(File.ReadAllBytes(path)); + LoadFromJson(wallet, out Scrypt, out accounts, out extra); + } + catch (Exception ex) + { + throw new WalletException(WalletErrorType.LoadWalletError, "Failed to load wallet file.", ex); + } } else { @@ -86,13 +93,20 @@ public NEP6Wallet(string path, string password, ProtocolSettings settings, JObje private void LoadFromJson(JObject wallet, out ScryptParameters scrypt, out Dictionary accounts, out JToken extra) { - version = Version.Parse(wallet["version"].AsString()); - name = wallet["name"]?.AsString(); - scrypt = ScryptParameters.FromJson((JObject)wallet["scrypt"]); - accounts = ((JArray)wallet["accounts"]).Select(p => NEP6Account.FromJson((JObject)p, this)).ToDictionary(p => p.ScriptHash); - extra = wallet["extra"]; - if (!VerifyPasswordInternal(password)) - throw new InvalidOperationException("Wrong password."); + try + { + version = Version.Parse(wallet["version"].AsString()); + name = wallet["name"]?.AsString(); + scrypt = ScryptParameters.FromJson((JObject)wallet["scrypt"]); + accounts = ((JArray)wallet["accounts"]).Select(p => NEP6Account.FromJson((JObject)p, this)).ToDictionary(p => p.ScriptHash); + extra = wallet["extra"]; + if (!VerifyPasswordInternal(password)) + throw new WalletException(WalletErrorType.PasswordIncorrect, "Wrong password."); + } + catch (Exception ex) when (ex is not WalletException) + { + throw new WalletException(WalletErrorType.LoadWalletError, "Failed to parse wallet JSON.", ex); + } } private void AddAccount(NEP6Account account) @@ -134,22 +148,29 @@ public override bool Contains(UInt160 scriptHash) public override WalletAccount CreateAccount(byte[] privateKey) { - if (privateKey is null) throw new ArgumentNullException(nameof(privateKey)); - KeyPair key = new(privateKey); - if (key.PublicKey.IsInfinity) throw new ArgumentException(null, nameof(privateKey)); - NEP6Contract contract = new() + Helper.ThrowIfNull(privateKey, nameof(privateKey)); + try { - Script = Contract.CreateSignatureRedeemScript(key.PublicKey), - ParameterList = new[] { ContractParameterType.Signature }, - ParameterNames = new[] { "signature" }, - Deployed = false - }; - NEP6Account account = new(this, contract.ScriptHash, key, password) + KeyPair key = new(privateKey); + if (key.PublicKey.IsInfinity) throw new WalletException(WalletErrorType.InvalidPrivateKey, "Invalid private key."); + NEP6Contract contract = new() + { + Script = Contract.CreateSignatureRedeemScript(key.PublicKey), + ParameterList = new[] { ContractParameterType.Signature }, + ParameterNames = new[] { "signature" }, + Deployed = false + }; + NEP6Account account = new(this, contract.ScriptHash, key, password) + { + Contract = contract + }; + AddAccount(account); + return account; + } + catch (Exception ex) when (!(ex is WalletException)) { - Contract = contract - }; - AddAccount(account); - return account; + throw new WalletException(WalletErrorType.CreateAccountError, "Failed to create account.", ex); + } } public override WalletAccount CreateAccount(Contract contract, KeyPair key = null) @@ -226,7 +247,7 @@ public override WalletAccount Import(X509Certificate2 cert) { if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - throw new PlatformNotSupportedException("Importing certificates is not supported on macOS."); + throw new WalletException(WalletErrorType.UnsupportedOperation, "Importing certificates is not supported on macOS."); } KeyPair key; using (ECDsa ecdsa = cert.GetECDsaPrivateKey()) diff --git a/src/Neo/Wallets/NEP6/NEP6WalletFactory.cs b/src/Neo/Wallets/NEP6/NEP6WalletFactory.cs index 759ab98de6..2b64b7a6d2 100644 --- a/src/Neo/Wallets/NEP6/NEP6WalletFactory.cs +++ b/src/Neo/Wallets/NEP6/NEP6WalletFactory.cs @@ -26,7 +26,7 @@ public bool Handle(string path) public Wallet CreateWallet(string name, string path, string password, ProtocolSettings settings) { if (File.Exists(path)) - throw new InvalidOperationException("The wallet file already exists."); + throw new WalletException(WalletErrorType.FileAlreadyExists, "The wallet file already exists."); NEP6Wallet wallet = new NEP6Wallet(path, password, settings, name); wallet.Save(); return wallet; diff --git a/src/Neo/Wallets/NEP6/ScryptParameters.cs b/src/Neo/Wallets/NEP6/ScryptParameters.cs index d8e076272a..7f45973baf 100644 --- a/src/Neo/Wallets/NEP6/ScryptParameters.cs +++ b/src/Neo/Wallets/NEP6/ScryptParameters.cs @@ -10,6 +10,7 @@ // modifications are permitted. using Neo.Json; +using System; namespace Neo.Wallets.NEP6 { @@ -58,7 +59,14 @@ public ScryptParameters(int n, int r, int p) /// The converted parameters. public static ScryptParameters FromJson(JObject json) { - return new ScryptParameters((int)json["n"].AsNumber(), (int)json["r"].AsNumber(), (int)json["p"].AsNumber()); + try + { + return new ScryptParameters((int)json["n"].AsNumber(), (int)json["r"].AsNumber(), (int)json["p"].AsNumber()); + } + catch (Exception e) + { + throw WalletException.FromException(e); + } } /// @@ -67,10 +75,7 @@ public static ScryptParameters FromJson(JObject json) /// The parameters represented by a JSON object. public JObject ToJson() { - JObject json = new(); - json["n"] = N; - json["r"] = R; - json["p"] = P; + JObject json = new() { ["n"] = N, ["r"] = R, ["p"] = P }; return json; } } diff --git a/src/Neo/Wallets/Wallet.cs b/src/Neo/Wallets/Wallet.cs index fc71a9135c..c8f3575164 100644 --- a/src/Neo/Wallets/Wallet.cs +++ b/src/Neo/Wallets/Wallet.cs @@ -141,6 +141,7 @@ public WalletAccount CreateAccount() { var privateKey = new byte[32]; using var rng = RandomNumberGenerator.Create(); + var maxTry = 30; do { @@ -149,16 +150,18 @@ public WalletAccount CreateAccount() rng.GetBytes(privateKey); return CreateAccount(privateKey); } - catch (ArgumentException) + catch { // Try again + maxTry--; } finally { Array.Clear(privateKey, 0, privateKey.Length); } } - while (true); + while (maxTry > 0); + throw new WalletException(WalletErrorType.UnknownError, "Failed to create account."); } /// @@ -280,34 +283,48 @@ public BigDecimal GetAvailable(DataCache snapshot, UInt160 asset_id) /// The balance for the specified asset. public BigDecimal GetBalance(DataCache snapshot, UInt160 asset_id, params UInt160[] accounts) { - byte[] script; - using (ScriptBuilder sb = new()) + try { - sb.EmitPush(0); - foreach (UInt160 account in accounts) + byte[] script; + using (ScriptBuilder sb = new()) { - sb.EmitDynamicCall(asset_id, "balanceOf", CallFlags.ReadOnly, account); - sb.Emit(OpCode.ADD); + sb.EmitPush(0); + foreach (UInt160 account in accounts) + { + sb.EmitDynamicCall(asset_id, "balanceOf", CallFlags.ReadOnly, account); + sb.Emit(OpCode.ADD); + } + sb.EmitDynamicCall(asset_id, "decimals", CallFlags.ReadOnly); + script = sb.ToArray(); } - sb.EmitDynamicCall(asset_id, "decimals", CallFlags.ReadOnly); - script = sb.ToArray(); + using ApplicationEngine engine = ApplicationEngine.Run(script, snapshot, settings: ProtocolSettings, gas: 0_60000000L * accounts.Length); + if (engine.State == VMState.FAULT) + return new BigDecimal(BigInteger.Zero, 0); + byte decimals = (byte)engine.ResultStack.Pop().GetInteger(); + BigInteger amount = engine.ResultStack.Pop().GetInteger(); + return new BigDecimal(amount, decimals); + } + catch (Exception e) when (e is not WalletException) + { + throw WalletException.FromException(e); } - using ApplicationEngine engine = ApplicationEngine.Run(script, snapshot, settings: ProtocolSettings, gas: 0_60000000L * accounts.Length); - if (engine.State == VMState.FAULT) - return new BigDecimal(BigInteger.Zero, 0); - byte decimals = (byte)engine.ResultStack.Pop().GetInteger(); - BigInteger amount = engine.ResultStack.Pop().GetInteger(); - return new BigDecimal(amount, decimals); } private static byte[] Decrypt(byte[] data, byte[] key) { - using Aes aes = Aes.Create(); - aes.Key = key; - aes.Mode = CipherMode.ECB; - aes.Padding = PaddingMode.None; - using ICryptoTransform decryptor = aes.CreateDecryptor(); - return decryptor.TransformFinalBlock(data, 0, data.Length); + try + { + using Aes aes = Aes.Create(); + aes.Key = key; + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + using ICryptoTransform decryptor = aes.CreateDecryptor(); + return decryptor.TransformFinalBlock(data, 0, data.Length); + } + catch (Exception e) when (e is not WalletException) + { + throw WalletException.FromException(e); + } } /// @@ -322,11 +339,16 @@ private static byte[] Decrypt(byte[] data, byte[] key) /// The decoded private key. public static byte[] GetPrivateKeyFromNEP2(string nep2, string passphrase, byte version, int N = 16384, int r = 8, int p = 8) { + ThrowIfNull(passphrase, nameof(passphrase)); byte[] passphrasedata = Encoding.UTF8.GetBytes(passphrase); try { return GetPrivateKeyFromNEP2(nep2, passphrasedata, version, N, r, p); } + catch (Exception e) when (e is not WalletException) + { + throw WalletException.FromException(e); + } finally { passphrasedata.AsSpan().Clear(); @@ -345,29 +367,37 @@ public static byte[] GetPrivateKeyFromNEP2(string nep2, string passphrase, byte /// The decoded private key. public static byte[] GetPrivateKeyFromNEP2(string nep2, byte[] passphrase, byte version, int N = 16384, int r = 8, int p = 8) { - if (nep2 == null) throw new ArgumentNullException(nameof(nep2)); - if (passphrase == null) throw new ArgumentNullException(nameof(passphrase)); - byte[] data = nep2.Base58CheckDecode(); - if (data.Length != 39 || data[0] != 0x01 || data[1] != 0x42 || data[2] != 0xe0) - throw new FormatException(); - byte[] addresshash = new byte[4]; - Buffer.BlockCopy(data, 3, addresshash, 0, 4); - byte[] derivedkey = SCrypt.Generate(passphrase, addresshash, N, r, p, 64); - byte[] derivedhalf1 = derivedkey[..32]; - byte[] derivedhalf2 = derivedkey[32..]; - Array.Clear(derivedkey, 0, derivedkey.Length); - byte[] encryptedkey = new byte[32]; - Buffer.BlockCopy(data, 7, encryptedkey, 0, 32); - Array.Clear(data, 0, data.Length); - byte[] prikey = XOR(Decrypt(encryptedkey, derivedhalf2), derivedhalf1); - Array.Clear(derivedhalf1, 0, derivedhalf1.Length); - Array.Clear(derivedhalf2, 0, derivedhalf2.Length); - ECPoint pubkey = Cryptography.ECC.ECCurve.Secp256r1.G * prikey; - UInt160 script_hash = Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash(); - string address = script_hash.ToAddress(version); - if (!Encoding.ASCII.GetBytes(address).Sha256().Sha256().AsSpan(0, 4).SequenceEqual(addresshash)) - throw new FormatException(); - return prikey; + + ThrowIfNull(nep2, nameof(nep2)); + ThrowIfNull(passphrase, nameof(passphrase)); + try + { + byte[] data = nep2.Base58CheckDecode(); + if (data.Length != 39 || data[0] != 0x01 || data[1] != 0x42 || data[2] != 0xe0) + throw new WalletException(WalletErrorType.FormatError, "Invalid NEP-2 format."); + byte[] addresshash = new byte[4]; + Buffer.BlockCopy(data, 3, addresshash, 0, 4); + byte[] derivedkey = SCrypt.Generate(passphrase, addresshash, N, r, p, 64); + byte[] derivedhalf1 = derivedkey[..32]; + byte[] derivedhalf2 = derivedkey[32..]; + Array.Clear(derivedkey, 0, derivedkey.Length); + byte[] encryptedkey = new byte[32]; + Buffer.BlockCopy(data, 7, encryptedkey, 0, 32); + Array.Clear(data, 0, data.Length); + byte[] prikey = XOR(Decrypt(encryptedkey, derivedhalf2), derivedhalf1); + Array.Clear(derivedhalf1, 0, derivedhalf1.Length); + Array.Clear(derivedhalf2, 0, derivedhalf2.Length); + ECPoint pubkey = Cryptography.ECC.ECCurve.Secp256r1.G * prikey; + UInt160 script_hash = Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash(); + string address = script_hash.ToAddress(version); + if (!Encoding.ASCII.GetBytes(address).Sha256().Sha256().AsSpan(0, 4).SequenceEqual(addresshash)) + throw new WalletException(WalletErrorType.FormatError); + return prikey; + } + catch (Exception e) + { + throw WalletException.FromException(e); + } } /// @@ -377,14 +407,21 @@ public static byte[] GetPrivateKeyFromNEP2(string nep2, byte[] passphrase, byte /// The decoded private key. public static byte[] GetPrivateKeyFromWIF(string wif) { - if (wif is null) throw new ArgumentNullException(nameof(wif)); - byte[] data = wif.Base58CheckDecode(); - if (data.Length != 34 || data[0] != 0x80 || data[33] != 0x01) - throw new FormatException(); - byte[] privateKey = new byte[32]; - Buffer.BlockCopy(data, 1, privateKey, 0, privateKey.Length); - Array.Clear(data, 0, data.Length); - return privateKey; + try + { + ThrowIfNull(wif, nameof(wif)); + byte[] data = wif.Base58CheckDecode(); + if (data.Length != 34 || data[0] != 0x80 || data[33] != 0x01) + throw new WalletException(WalletErrorType.InvalidPrivateKey, "Invalid WIF format."); + byte[] privateKey = new byte[32]; + Buffer.BlockCopy(data, 1, privateKey, 0, privateKey.Length); + Array.Clear(data, 0, data.Length); + return privateKey; + } + catch (Exception e) when (e is not WalletException) + { + throw new WalletException(WalletErrorType.InvalidPrivateKey, "Invalid WIF format.", e); + } } private static Signer[] GetSigners(UInt160 sender, Signer[] cosigners) @@ -416,16 +453,24 @@ public virtual WalletAccount Import(X509Certificate2 cert) { if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - throw new PlatformNotSupportedException("Importing certificates is not supported on macOS."); + throw new WalletException(WalletErrorType.UnsupportedOperation, "Importing certificates is not supported on macOS."); + } + + try + { + byte[] privateKey; + using (ECDsa ecdsa = cert.GetECDsaPrivateKey()) + { + privateKey = ecdsa.ExportParameters(true).D; + } + WalletAccount account = CreateAccount(privateKey); + Array.Clear(privateKey, 0, privateKey.Length); + return account; } - byte[] privateKey; - using (ECDsa ecdsa = cert.GetECDsaPrivateKey()) + catch (Exception e) when (e is not WalletException) { - privateKey = ecdsa.ExportParameters(true).D; + throw WalletException.FromException(e); } - WalletAccount account = CreateAccount(privateKey); - Array.Clear(privateKey, 0, privateKey.Length); - return account; } /// @@ -435,10 +480,17 @@ public virtual WalletAccount Import(X509Certificate2 cert) /// The imported account. public virtual WalletAccount Import(string wif) { - byte[] privateKey = GetPrivateKeyFromWIF(wif); - WalletAccount account = CreateAccount(privateKey); - Array.Clear(privateKey, 0, privateKey.Length); - return account; + try + { + byte[] privateKey = GetPrivateKeyFromWIF(wif); + WalletAccount account = CreateAccount(privateKey); + Array.Clear(privateKey, 0, privateKey.Length); + return account; + } + catch (Exception e) when (e is not WalletException) + { + throw WalletException.FromException(e); + } } /// @@ -452,10 +504,17 @@ public virtual WalletAccount Import(string wif) /// The imported account. public virtual WalletAccount Import(string nep2, string passphrase, int N = 16384, int r = 8, int p = 8) { - byte[] privateKey = GetPrivateKeyFromNEP2(nep2, passphrase, ProtocolSettings.AddressVersion, N, r, p); - WalletAccount account = CreateAccount(privateKey); - Array.Clear(privateKey, 0, privateKey.Length); - return account; + try + { + byte[] privateKey = GetPrivateKeyFromNEP2(nep2, passphrase, ProtocolSettings.AddressVersion, N, r, p); + WalletAccount account = CreateAccount(privateKey); + Array.Clear(privateKey, 0, privateKey.Length); + return account; + } + catch (Exception e) when (e is not WalletException) + { + throw WalletException.FromException(e); + } } /// @@ -492,13 +551,14 @@ public Transaction MakeTransaction(DataCache snapshot, TransferOutput[] outputs, sb2.EmitDynamicCall(assetId, "balanceOf", CallFlags.ReadOnly, account); using ApplicationEngine engine = ApplicationEngine.Run(sb2.ToArray(), snapshot, settings: ProtocolSettings, persistingBlock: persistingBlock); if (engine.State != VMState.HALT) - throw new InvalidOperationException($"Execution for {assetId}.balanceOf('{account}' fault"); + throw new WalletException(WalletErrorType.ContractError, $"Execution for {assetId}.balanceOf('{account}') failed."); BigInteger value = engine.ResultStack.Pop().GetInteger(); if (value.Sign > 0) balances.Add((account, value)); } BigInteger sum_balance = balances.Select(p => p.Value).Sum(); if (sum_balance < sum) - throw new InvalidOperationException($"It does not have enough balance, expected: {sum} found: {sum_balance}"); + throw new WalletException(WalletErrorType.InsufficientFunds, $"Insufficient balance for asset {assetId}. Required: {sum}, Available: {sum_balance}."); + foreach (TransferOutput output in group) { balances = balances.OrderBy(p => p.Value).ToList(); @@ -530,7 +590,15 @@ public Transaction MakeTransaction(DataCache snapshot, TransferOutput[] outputs, if (balances_gas is null) balances_gas = accounts.Select(p => (Account: p, Value: NativeContract.GAS.BalanceOf(snapshot, p))).Where(p => p.Value.Sign > 0).ToList(); - return MakeTransaction(snapshot, script, cosignerList.Values.ToArray(), Array.Empty(), balances_gas, persistingBlock: persistingBlock); + try + { + return MakeTransaction(snapshot, script, cosignerList.Values.ToArray(), Array.Empty(), balances_gas, persistingBlock: persistingBlock); + } + catch (Exception ex) + { + throw new WalletException(WalletErrorType.TransactionCreationError, "Failed to create transaction.", ex); + } + } /// @@ -579,7 +647,7 @@ private Transaction MakeTransaction(DataCache snapshot, ReadOnlyMemory scr { if (engine.State == VMState.FAULT) { - throw new InvalidOperationException($"Failed execution for '{Convert.ToBase64String(script.Span)}'", engine.FaultException); + throw new WalletException(WalletErrorType.ExecutionFault, $"Failed execution for '{Convert.ToBase64String(script.Span)}'.", engine.FaultException); } tx.SystemFee = engine.FeeConsumed; } @@ -587,7 +655,7 @@ private Transaction MakeTransaction(DataCache snapshot, ReadOnlyMemory scr tx.NetworkFee = tx.CalculateNetworkFee(snapshot, ProtocolSettings, (a) => GetAccount(a)?.Contract?.Script, maxGas); if (value >= tx.SystemFee + tx.NetworkFee) return tx; } - throw new InvalidOperationException("Insufficient GAS"); + throw new WalletException(WalletErrorType.InsufficientFunds, "Insufficient GAS."); } /// @@ -597,61 +665,68 @@ private Transaction MakeTransaction(DataCache snapshot, ReadOnlyMemory scr /// if the signature is successfully added to the context; otherwise, . public bool Sign(ContractParametersContext context) { - if (context.Network != ProtocolSettings.Network) return false; - bool fSuccess = false; - foreach (UInt160 scriptHash in context.ScriptHashes) + try { - WalletAccount account = GetAccount(scriptHash); - - if (account != null) + if (context.Network != ProtocolSettings.Network) return false; + bool fSuccess = false; + foreach (UInt160 scriptHash in context.ScriptHashes) { - // Try to sign self-contained multiSig + WalletAccount account = GetAccount(scriptHash); - Contract multiSigContract = account.Contract; - - if (multiSigContract != null && - IsMultiSigContract(multiSigContract.Script, out int m, out ECPoint[] points)) + if (account != null) { - foreach (var point in points) + // Try to sign self-contained multiSig + + Contract multiSigContract = account.Contract; + + if (multiSigContract != null && + IsMultiSigContract(multiSigContract.Script, out int m, out ECPoint[] points)) { - account = GetAccount(point); - if (account?.HasKey != true) continue; + foreach (var point in points) + { + account = GetAccount(point); + if (account?.HasKey != true) continue; + KeyPair key = account.GetKey(); + byte[] signature = context.Verifiable.Sign(key, context.Network); + fSuccess |= context.AddSignature(multiSigContract, key.PublicKey, signature); + if (fSuccess) m--; + if (context.Completed || m <= 0) break; + } + continue; + } + else if (account.HasKey) + { + // Try to sign with regular accounts KeyPair key = account.GetKey(); byte[] signature = context.Verifiable.Sign(key, context.Network); - fSuccess |= context.AddSignature(multiSigContract, key.PublicKey, signature); - if (fSuccess) m--; - if (context.Completed || m <= 0) break; + fSuccess |= context.AddSignature(account.Contract, key.PublicKey, signature); + continue; } - continue; - } - else if (account.HasKey) - { - // Try to sign with regular accounts - KeyPair key = account.GetKey(); - byte[] signature = context.Verifiable.Sign(key, context.Network); - fSuccess |= context.AddSignature(account.Contract, key.PublicKey, signature); - continue; } - } - // Try Smart contract verification + // Try Smart contract verification - var contract = NativeContract.ContractManagement.GetContract(context.SnapshotCache, scriptHash); + var contract = NativeContract.ContractManagement.GetContract(context.SnapshotCache, scriptHash); - if (contract != null) - { - var deployed = new DeployedContract(contract); + if (contract != null) + { + var deployed = new DeployedContract(contract); - // Only works with verify without parameters + // Only works with verify without parameters - if (deployed.ParameterList.Length == 0) - { - fSuccess |= context.Add(deployed); + if (deployed.ParameterList.Length == 0) + { + fSuccess |= context.Add(deployed); + } } } - } - return fSuccess; + return fSuccess; + } + catch (Exception e) when (e is not WalletException) + { + throw WalletException.FromException(e); + } } /// @@ -686,20 +761,51 @@ public static Wallet Open(string path, string password, ProtocolSettings setting /// The created new wallet. public static Wallet Migrate(string path, string oldPath, string password, ProtocolSettings settings) { + ThrowIfNull(path, nameof(path)); + ThrowIfNull(oldPath, nameof(oldPath)); + ThrowIfNull(password, nameof(password)); + ThrowIfNull(settings, nameof(settings)); + IWalletFactory factoryOld = GetFactory(oldPath); if (factoryOld is null) - throw new InvalidOperationException("The old wallet file format is not supported."); + throw new WalletException(WalletErrorType.UnsupportedWalletFormat, "The old wallet file format is not supported."); + IWalletFactory factoryNew = GetFactory(path); if (factoryNew is null) - throw new InvalidOperationException("The new wallet file format is not supported."); + throw new WalletException(WalletErrorType.UnsupportedWalletFormat, "The new wallet file format is not supported."); - Wallet oldWallet = factoryOld.OpenWallet(oldPath, password, settings); - Wallet newWallet = factoryNew.CreateWallet(oldWallet.Name, path, password, settings); + Wallet oldWallet; + try + { + oldWallet = factoryOld.OpenWallet(oldPath, password, settings); + } + catch (Exception ex) + { + throw new WalletException(WalletErrorType.OpenWalletError, "Failed to open the old wallet.", ex); + } + + Wallet newWallet; + try + { + newWallet = factoryNew.CreateWallet(oldWallet.Name, path, password, settings); + } + catch (Exception ex) + { + throw new WalletException(WalletErrorType.CreateWalletError, "Failed to create the new wallet.", ex); + } foreach (WalletAccount account in oldWallet.GetAccounts()) { - newWallet.CreateAccount(account.Contract, account.GetKey()); + try + { + newWallet.CreateAccount(account.Contract, account.GetKey()); + } + catch (Exception ex) + { + throw new WalletException(WalletErrorType.MigrateAccountError, $"Failed to migrate account {account.Address}.", ex); + } } + return newWallet; } diff --git a/src/Neo/Wallets/WalletErrorType.cs b/src/Neo/Wallets/WalletErrorType.cs new file mode 100644 index 0000000000..6678e78aa7 --- /dev/null +++ b/src/Neo/Wallets/WalletErrorType.cs @@ -0,0 +1,40 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// WalletErrorType.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Wallets; + +public enum WalletErrorType +{ + OpenWalletError, + CreateWalletError, + MigrateAccountError, + LoadWalletError, + CreateAccountError, + InvalidPrivateKey, + ChangePasswordError, + InsufficientFunds, + InvalidTransaction, + AccountLocked, + TransactionCreationError, + ExecutionFault, + FormatError, + InvalidOperation, + FileAlreadyExists, + ArgumentException, + ContractNotFound, + ContractError, + VerificationFailed, + UnsupportedOperation, + PasswordIncorrect, + UnsupportedWalletFormat, + ArgumentNull, + UnknownError +} diff --git a/src/Neo/Wallets/WalletException.cs b/src/Neo/Wallets/WalletException.cs new file mode 100644 index 0000000000..5d6ca4f728 --- /dev/null +++ b/src/Neo/Wallets/WalletException.cs @@ -0,0 +1,91 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// WalletException.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.IO; +using System.Runtime.CompilerServices; + +namespace Neo.Wallets; + +public class WalletException : Exception +{ + public WalletErrorType ErrorType { get; } + public string CustomErrorMessage { get; } + + public WalletException(WalletErrorType errorType, string customErrorMessage = null) + : base($"{errorType}: {customErrorMessage ?? GetDefaultMessage(errorType)}") + { + ErrorType = errorType; + CustomErrorMessage = customErrorMessage; + } + + public WalletException(WalletErrorType errorType, string customErrorMessage, Exception innerException) + : base($"{errorType}: {customErrorMessage ?? GetDefaultMessage(errorType)}", innerException) + { + ErrorType = errorType; + CustomErrorMessage = customErrorMessage; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static WalletException FromException(Exception exception) + { + if (exception is WalletException walletException) + { + return walletException; + } + + var errorType = exception switch + { + FormatException => WalletErrorType.FormatError, + InvalidOperationException => WalletErrorType.InvalidOperation, + ArgumentException => WalletErrorType.ArgumentException, + UnauthorizedAccessException => WalletErrorType.AccountLocked, + IOException => WalletErrorType.ExecutionFault, + _ => WalletErrorType.UnknownError + }; + + return new WalletException(errorType, exception.Message, exception); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string GetDefaultMessage(WalletErrorType errorType) + { + return errorType switch + { + WalletErrorType.OpenWalletError => "Failed to open the wallet.", + WalletErrorType.CreateWalletError => "Failed to create the wallet.", + WalletErrorType.MigrateAccountError => "Failed to migrate the account.", + WalletErrorType.LoadWalletError => "Failed to load the wallet.", + WalletErrorType.CreateAccountError => "Failed to create the account.", + WalletErrorType.InvalidPrivateKey => "Invalid private key provided.", + WalletErrorType.ChangePasswordError => "Failed to change the password.", + WalletErrorType.InsufficientFunds => "Insufficient funds in the wallet.", + WalletErrorType.InvalidTransaction => "Invalid transaction.", + WalletErrorType.AccountLocked => "The account is locked.", + WalletErrorType.TransactionCreationError => "Failed to create the transaction.", + WalletErrorType.ExecutionFault => "Execution fault.", + WalletErrorType.FormatError => "Format error occurred.", + WalletErrorType.InvalidOperation => "Invalid operation.", + WalletErrorType.FileAlreadyExists => "The wallet file already exists.", + WalletErrorType.ArgumentException => "Invalid argument provided.", + WalletErrorType.UnknownError => "An unknown error occurred.", + WalletErrorType.ContractNotFound => "The contract was not found.", + WalletErrorType.ContractError => "A contract error occurred.", + WalletErrorType.VerificationFailed => "Verification failed.", + WalletErrorType.UnsupportedOperation => "Unsupported operation.", + WalletErrorType.PasswordIncorrect => "Incorrect password.", + WalletErrorType.UnsupportedWalletFormat => "Unsupported wallet format.", + _ => "A wallet error occurred." + }; + } + + public override string Message => CustomErrorMessage ?? base.Message; +} diff --git a/src/Plugins/RpcServer/RpcServer.Wallet.cs b/src/Plugins/RpcServer/RpcServer.Wallet.cs index 50f3c7a1e0..63fca7cdfd 100644 --- a/src/Plugins/RpcServer/RpcServer.Wallet.cs +++ b/src/Plugins/RpcServer/RpcServer.Wallet.cs @@ -229,6 +229,10 @@ protected internal virtual JToken OpenWallet(JArray _params) { throw new RpcException(RpcError.WalletNotSupported.WithData("Invalid password.")); } + catch (WalletException e) + { + throw new RpcException(RpcError.WalletNotSupported.WithData(e.Message)); + } return true; } diff --git a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs index 2db71ae570..efb207ad44 100644 --- a/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs +++ b/tests/Neo.Plugins.RpcServer.Tests/UT_RpcServer.Wallet.cs @@ -63,7 +63,7 @@ public void TestOpenInvalidWallet() File.WriteAllText(Path, "{\"name\":null,\"version\":\"1.0\",\"scrypt\":{\"n\":16384,\"r\":8,\"p\":8},\"accounts\":[{\"address\":\"NVizn8DiExdmnpTQfjiVY3dox8uXg3Vrxv\",\"label\":null,\"isDefault\":false,\"lock\":false,\"key\":\"6PYPMrsCJ3D4AXJCFWYT2WMSBGF7dLoaNipW14t4UFAkZw3Z9vQRQV1bEU\",\"contract\":{\"script\":\"DCEDaR\\u002BFVb8lOdiMZ/wCHLiI\\u002Bzuf17YuGFReFyHQhB80yMpBVuezJw==\",\"parameters\":[{\"name\":\"signature\",\"type\":\"Signature\"}],\"deployed\":false},\"extra\":null}],\"extra\":null}"); exception = Assert.ThrowsException(() => _rpcServer.OpenWallet(paramsArray), "Should throw RpcException for unsupported wallet"); Assert.AreEqual(RpcError.WalletNotSupported.Code, exception.HResult); - Assert.AreEqual(exception.Message, "Wallet not supported - Invalid password."); + Assert.AreEqual(exception.Message, "Wallet not supported - Failed to load wallet file."); File.Delete(Path); } diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Transaction.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Transaction.cs index dc910c5142..1022a04a4f 100644 --- a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Transaction.cs +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Transaction.cs @@ -580,7 +580,8 @@ public void FeeIsSignatureContract_TestScope_CurrentHash_NEO_FAULT() // expects FAULT on execution of 'transfer' Application script // due to lack of a valid witness validation Transaction tx = null; - Assert.ThrowsException(() => tx = wallet.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers)); + var exception = Assert.ThrowsException(() => tx = wallet.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers)); + Assert.AreEqual(exception.ErrorType, WalletErrorType.ExecutionFault); Assert.IsNull(tx); } @@ -711,7 +712,8 @@ public void FeeIsSignatureContract_TestScope_NoScopeFAULT() // expects FAULT on execution of 'transfer' Application script // due to lack of a valid witness validation Transaction tx = null; - Assert.ThrowsException(() => tx = wallet.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers, attributes)); + var exception = Assert.ThrowsException(() => tx = wallet.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers, attributes)); + Assert.AreEqual(exception.ErrorType, WalletErrorType.ExecutionFault); Assert.IsNull(tx); } @@ -761,7 +763,8 @@ public void FeeIsSignatureContract_UnexistingVerificationContractFAULT() // expects ArgumentException on execution of 'CalculateNetworkFee' due to // null witness_script (no account in the wallet, no corresponding witness // and no verification contract for the signer) - Assert.ThrowsException(() => walletWithoutAcc.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers)); + var exception = Assert.ThrowsException(() => walletWithoutAcc.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers)); + Assert.AreEqual(exception.ErrorType, WalletErrorType.ContractNotFound); Assert.IsNull(tx); } @@ -1016,7 +1019,8 @@ public void FeeIsSignatureContract_TestScope_FeeOnly_Default() Scopes = WitnessScope.None } }; - Assert.ThrowsException(() => wallet.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers)); + var exception = Assert.ThrowsException(() => wallet.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers)); + Assert.AreEqual(exception.ErrorType, WalletErrorType.ExecutionFault); // change to global scope signers[0].Scopes = WitnessScope.Global; diff --git a/tests/Neo.UnitTests/Wallets/NEP6/UT_NEP6Wallet.cs b/tests/Neo.UnitTests/Wallets/NEP6/UT_NEP6Wallet.cs index a914b235b7..d9f623f27d 100644 --- a/tests/Neo.UnitTests/Wallets/NEP6/UT_NEP6Wallet.cs +++ b/tests/Neo.UnitTests/Wallets/NEP6/UT_NEP6Wallet.cs @@ -93,8 +93,10 @@ public void TestCreateAccount() Assert.IsTrue(uut.Sign(ctx)); tx.Witnesses = ctx.GetWitnesses(); Assert.IsTrue(tx.VerifyWitnesses(TestProtocolSettings.Default, TestBlockchain.GetTestSnapshotCache(), long.MaxValue)); - Assert.ThrowsException(() => uut.CreateAccount((byte[])null)); - Assert.ThrowsException(() => uut.CreateAccount("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551".HexToBytes())); + var exception = Assert.ThrowsException(() => uut.CreateAccount((byte[])null)); + Assert.AreEqual(exception.ErrorType, WalletErrorType.ArgumentNull); + exception = Assert.ThrowsException(() => uut.CreateAccount("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551".HexToBytes())); + Assert.AreEqual(exception.ErrorType, WalletErrorType.InvalidPrivateKey); } [TestMethod] @@ -305,7 +307,8 @@ public void TestImportCert() Assert.AreEqual(true, cert.HasPrivateKey); if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - Assert.ThrowsException(() => uut.Import(cert)); + var exception = Assert.ThrowsException(() => uut.Import(cert)); + Assert.AreEqual(exception.ErrorType, WalletErrorType.UnsupportedOperation); return; } WalletAccount account = uut.Import(cert); diff --git a/tests/Neo.UnitTests/Wallets/UT_AssetDescriptor.cs b/tests/Neo.UnitTests/Wallets/UT_AssetDescriptor.cs index ff2386c466..da7a9cef71 100644 --- a/tests/Neo.UnitTests/Wallets/UT_AssetDescriptor.cs +++ b/tests/Neo.UnitTests/Wallets/UT_AssetDescriptor.cs @@ -12,6 +12,7 @@ using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Neo.SmartContract.Native; +using Neo.Wallets; using System; namespace Neo.UnitTests.Wallets @@ -23,11 +24,11 @@ public class UT_AssetDescriptor public void TestConstructorWithNonexistAssetId() { var snapshotCache = TestBlockchain.GetTestSnapshotCache(); - Action action = () => + var exception = Assert.ThrowsException(() => { var descriptor = new Neo.Wallets.AssetDescriptor(snapshotCache, TestProtocolSettings.Default, UInt160.Parse("01ff00ff00ff00ff00ff00ff00ff00ff00ff00a4")); - }; - action.Should().Throw(); + }); + Assert.AreEqual(exception.ErrorType, WalletErrorType.ContractNotFound); } [TestMethod] diff --git a/tests/Neo.UnitTests/Wallets/UT_KeyPair.cs b/tests/Neo.UnitTests/Wallets/UT_KeyPair.cs index 7ae84960c7..77bd4a3f16 100644 --- a/tests/Neo.UnitTests/Wallets/UT_KeyPair.cs +++ b/tests/Neo.UnitTests/Wallets/UT_KeyPair.cs @@ -46,7 +46,7 @@ public void TestConstructor() for (int i = 0; i < privateKey31.Length; i++) privateKey31[i] = (byte)random.Next(256); Action action = () => new KeyPair(privateKey31); - action.Should().Throw(); + action.Should().Throw(); } [TestMethod] diff --git a/tests/Neo.UnitTests/Wallets/UT_Wallet.cs b/tests/Neo.UnitTests/Wallets/UT_Wallet.cs index bba7ff05a1..4d71e14440 100644 --- a/tests/Neo.UnitTests/Wallets/UT_Wallet.cs +++ b/tests/Neo.UnitTests/Wallets/UT_Wallet.cs @@ -252,11 +252,11 @@ public void TestGetBalance() [TestMethod] public void TestGetPrivateKeyFromNEP2() { - Action action = () => Wallet.GetPrivateKeyFromNEP2("3vQB7B6MrGQZaxCuFg4oh", "TestGetPrivateKeyFromNEP2", ProtocolSettings.Default.AddressVersion, 2, 1, 1); - action.Should().Throw(); + var exception = Assert.ThrowsException(() => Wallet.GetPrivateKeyFromNEP2("3vQB7B6MrGQZaxCuFg4oh", "TestGetPrivateKeyFromNEP2", ProtocolSettings.Default.AddressVersion, 2, 1, 1)); + Assert.AreEqual(exception.ErrorType, WalletErrorType.FormatError); - action = () => Wallet.GetPrivateKeyFromNEP2(nep2Key, "Test", ProtocolSettings.Default.AddressVersion, 2, 1, 1); - action.Should().Throw(); + exception = Assert.ThrowsException(() => Wallet.GetPrivateKeyFromNEP2(nep2Key, "Test", ProtocolSettings.Default.AddressVersion, 2, 1, 1)); + Assert.AreEqual(exception.ErrorType, WalletErrorType.FormatError); Wallet.GetPrivateKeyFromNEP2(nep2Key, "pwd", ProtocolSettings.Default.AddressVersion, 2, 1, 1).Should().BeEquivalentTo(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 }); } @@ -264,11 +264,11 @@ public void TestGetPrivateKeyFromNEP2() [TestMethod] public void TestGetPrivateKeyFromWIF() { - Action action = () => Wallet.GetPrivateKeyFromWIF(null); - action.Should().Throw(); + var exception = Assert.ThrowsException(() => Wallet.GetPrivateKeyFromWIF(null)); + Assert.AreEqual(exception.ErrorType, WalletErrorType.ArgumentNull); - action = () => Wallet.GetPrivateKeyFromWIF("3vQB7B6MrGQZaxCuFg4oh"); - action.Should().Throw(); + exception = Assert.ThrowsException(() => Wallet.GetPrivateKeyFromWIF("3vQB7B6MrGQZaxCuFg4oh")); + Assert.AreEqual(exception.ErrorType, WalletErrorType.InvalidPrivateKey); Wallet.GetPrivateKeyFromWIF("L3tgppXLgdaeqSGSFw1Go3skBiy8vQAM7YMXvTHsKQtE16PBncSU").Should().BeEquivalentTo(new byte[] { 199, 19, 77, 111, 216, 231, 61, 129, 158, 130, 117, 92, 100, 201, 55, 136, 216, 219, 9, 97, 146, 158, 2, 90, 83, 54, 60, 76, 192, 42, 105, 98 }); } @@ -296,7 +296,7 @@ public void TestMakeTransaction1() WalletAccount account = wallet.CreateAccount(contract, glkey.PrivateKey); account.Lock = false; - Action action = () => wallet.MakeTransaction(snapshotCache, new TransferOutput[] + var exception = Assert.ThrowsException(() => wallet.MakeTransaction(snapshotCache, new TransferOutput[] { new TransferOutput() { @@ -305,10 +305,10 @@ public void TestMakeTransaction1() Value = new BigDecimal(BigInteger.One,8), Data = "Dec 12th" } - }, UInt160.Zero); - action.Should().Throw(); + }, UInt160.Zero)); + Assert.AreEqual(exception.ErrorType, WalletErrorType.InsufficientFunds); - action = () => wallet.MakeTransaction(snapshotCache, new TransferOutput[] + exception = Assert.ThrowsException(() => wallet.MakeTransaction(snapshotCache, new TransferOutput[] { new TransferOutput() { @@ -317,10 +317,9 @@ public void TestMakeTransaction1() Value = new BigDecimal(BigInteger.One,8), Data = "Dec 12th" } - }, account.ScriptHash); - action.Should().Throw(); - - action = () => wallet.MakeTransaction(snapshotCache, new TransferOutput[] + }, account.ScriptHash)); + Assert.AreEqual(exception.ErrorType, WalletErrorType.InsufficientFunds); + exception = Assert.ThrowsException(() => wallet.MakeTransaction(snapshotCache, new TransferOutput[] { new TransferOutput() { @@ -329,8 +328,8 @@ public void TestMakeTransaction1() Value = new BigDecimal(BigInteger.One,8), Data = "Dec 12th" } - }, account.ScriptHash); - action.Should().Throw(); + }, account.ScriptHash)); + Assert.AreEqual(exception.ErrorType, WalletErrorType.ContractError); // Fake balance var key = NativeContract.GAS.CreateStorageKey(20, account.ScriptHash); @@ -375,8 +374,8 @@ public void TestMakeTransaction2() { var snapshotCache = TestBlockchain.GetTestSnapshotCache(); MyWallet wallet = new(); - Action action = () => wallet.MakeTransaction(snapshotCache, Array.Empty(), null, null, Array.Empty()); - action.Should().Throw(); + var exception = Assert.ThrowsException(() => wallet.MakeTransaction(snapshotCache, Array.Empty(), null, null, Array.Empty())); + Assert.AreEqual(exception.ErrorType, WalletErrorType.InsufficientFunds); Contract contract = Contract.Create(new ContractParameterType[] { ContractParameterType.Boolean }, new byte[] { 1 }); WalletAccount account = wallet.CreateAccount(contract, glkey.PrivateKey); diff --git a/tests/Neo.UnitTests/Wallets/UT_Wallets_Helper.cs b/tests/Neo.UnitTests/Wallets/UT_Wallets_Helper.cs index 843eeae192..26196fb549 100644 --- a/tests/Neo.UnitTests/Wallets/UT_Wallets_Helper.cs +++ b/tests/Neo.UnitTests/Wallets/UT_Wallets_Helper.cs @@ -26,10 +26,12 @@ public void TestToScriptHash() { byte[] array = { 0x01 }; UInt160 scriptHash = new UInt160(Crypto.Hash160(array)); - "NdtB8RXRmJ7Nhw1FPTm7E6HoDZGnDw37nf".ToScriptHash(TestProtocolSettings.Default.AddressVersion).Should().Be(scriptHash); + "NdtB8RXRmJ7Nhw1FPTm7E6HoDZGnDw37nf".ToScriptHash(TestProtocolSettings.Default.AddressVersion).Should() + .Be(scriptHash); - Action action = () => "3vQB7B6MrGQZaxCuFg4oh".ToScriptHash(TestProtocolSettings.Default.AddressVersion); - action.Should().Throw(); + var exception = Assert.ThrowsException(() => + "3vQB7B6MrGQZaxCuFg4oh".ToScriptHash(TestProtocolSettings.Default.AddressVersion)); + Assert.AreEqual(exception.ErrorType, WalletErrorType.FormatError); var address = scriptHash.ToAddress(ProtocolSettings.Default.AddressVersion); Span data = stackalloc byte[21]; @@ -37,8 +39,9 @@ public void TestToScriptHash() data[0] = 0x01; scriptHash.ToArray().CopyTo(data[1..]); address = Base58.Base58CheckEncode(data); - action = () => address.ToScriptHash(ProtocolSettings.Default.AddressVersion); - action.Should().Throw(); + exception = Assert.ThrowsException(() => + address.ToScriptHash(ProtocolSettings.Default.AddressVersion)); + Assert.AreEqual(exception.ErrorType, WalletErrorType.FormatError); } } }