diff --git a/src/Neo/Wallets/Wallet.cs b/src/Neo/Wallets/Wallet.cs index 39a1c3756f..032d2bbc4b 100644 --- a/src/Neo/Wallets/Wallet.cs +++ b/src/Neo/Wallets/Wallet.cs @@ -136,6 +136,54 @@ protected Wallet(string path, ProtocolSettings settings) Path = path; } + /// + /// Constructs a special contract with empty script, will get the script with + /// scriptHash from blockchain when doing the verification. + /// + /// Note: + /// Creates "m" out of "n" type verification script using length + /// with the default BFT assumptions of Ceiling(n - (n-1) / 3) for "m". + /// + /// + /// The public keys of the contract. + /// Multi-Signature contract . + /// + /// is empty or length is greater than 1024. + /// + /// + public WalletAccount CreateMultiSigAccount(params ECPoint[] publicKeys) => + CreateMultiSigAccount((int)Math.Ceiling((2 * publicKeys.Length + 1) / 3m), publicKeys); + + /// + /// Constructs a special contract with empty script, will get the script with + /// scriptHash from blockchain when doing the verification. + /// + /// The number of correct signatures that need to be provided in order for the verification to pass. + /// The public keys of the contract. + /// Multi-Signature contract . + /// + /// is empty or is greater than length or + /// is less than 1 or is greater than 1024. + /// + /// + public WalletAccount CreateMultiSigAccount(int m, params ECPoint[] publicKeys) + { + ArgumentOutOfRangeException.ThrowIfEqual(publicKeys.Length, 0, nameof(publicKeys)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(m, publicKeys.Length, nameof(publicKeys)); + ArgumentOutOfRangeException.ThrowIfLessThan(m, 1, nameof(m)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(m, 1024, nameof(m)); + + var contract = Contract.CreateMultiSigContract(m, publicKeys); + var account = GetAccounts() + .FirstOrDefault( + f => + f.HasKey && + f.Lock == false && + publicKeys.Contains(f.GetKey().PublicKey)); + + return CreateAccount(contract, account?.GetKey()); + } + /// /// Creates a standard account for the wallet. /// @@ -237,6 +285,12 @@ public WalletAccount CreateAccount(Contract contract, byte[] privateKey) return result; } + public IEnumerable GetMultiSigAccounts() => + GetAccounts() + .Where(static w => + w.Lock == false && + IsMultiSigContract(w.Contract.Script)); + /// /// Gets the account with the specified public key. /// diff --git a/tests/Neo.UnitTests/Wallets/UT_Wallet.cs b/tests/Neo.UnitTests/Wallets/UT_Wallet.cs index 7eb8f54b16..54520fd2f9 100644 --- a/tests/Neo.UnitTests/Wallets/UT_Wallet.cs +++ b/tests/Neo.UnitTests/Wallets/UT_Wallet.cs @@ -13,6 +13,7 @@ using Neo.Cryptography; using Neo.Cryptography.ECC; using Neo.Extensions; +using Neo.Extensions.Factories; using Neo.Network.P2P; using Neo.Network.P2P.Payloads; using Neo.Sign; @@ -22,7 +23,9 @@ using Neo.Wallets; using System; using System.Collections.Generic; +using System.Linq; using System.Numerics; +using Helper = Neo.SmartContract.Helper; namespace Neo.UnitTests.Wallets { @@ -511,5 +514,43 @@ public void TestContainsKeyPair() contains = wallet.ContainsSignable(pair.PublicKey); Assert.IsFalse(contains); // locked } + + [TestMethod] + public void TestMultiSigAccount() + { + var expectedWallet = new MyWallet(); + var expectedPrivateKey1 = RandomNumberFactory.NextBytes(32, cryptography: true); + var expectedPrivateKey2 = RandomNumberFactory.NextBytes(32, cryptography: true); + var expectedPrivateKey3 = RandomNumberFactory.NextBytes(32, cryptography: true); + + var expectedWalletAccount1 = expectedWallet.CreateAccount(expectedPrivateKey1); + var expectedWalletAccount2 = expectedWallet.CreateAccount(expectedPrivateKey2); + var expectedWalletAccount3 = expectedWallet.CreateAccount(expectedPrivateKey3); + + var expectedAccountKey1 = expectedWalletAccount1.GetKey(); + var expectedAccountKey2 = expectedWalletAccount2.GetKey(); + var expectedAccountKey3 = expectedWalletAccount3.GetKey(); + + var actualMultiSigAccount1 = expectedWallet.CreateMultiSigAccount([expectedAccountKey1.PublicKey]); + var actualMultiSigAccount2 = expectedWallet.CreateMultiSigAccount([expectedAccountKey1.PublicKey, expectedAccountKey2.PublicKey, expectedAccountKey3.PublicKey]); + + Assert.IsNotNull(actualMultiSigAccount1); + Assert.AreNotEqual(expectedWalletAccount1.ScriptHash, actualMultiSigAccount1.ScriptHash); + Assert.AreEqual(expectedAccountKey1.PublicKey, actualMultiSigAccount1.GetKey().PublicKey); + Assert.IsTrue(Helper.IsMultiSigContract(actualMultiSigAccount1.Contract.Script)); + Assert.IsTrue(expectedWallet.GetMultiSigAccounts().Contains(actualMultiSigAccount1)); + + var notExpectedAccountKeys = new ECPoint[1025]; + Assert.ThrowsExactly(() => expectedWallet.CreateMultiSigAccount()); + Assert.ThrowsExactly(() => expectedWallet.CreateMultiSigAccount(2, [expectedAccountKey1.PublicKey])); + Assert.ThrowsExactly(() => expectedWallet.CreateMultiSigAccount(0, [expectedAccountKey1.PublicKey])); + Assert.ThrowsExactly(() => expectedWallet.CreateMultiSigAccount(1025, notExpectedAccountKeys)); + + Assert.IsNotNull(actualMultiSigAccount2); + Assert.AreNotEqual(expectedWalletAccount2.ScriptHash, actualMultiSigAccount2.ScriptHash); + Assert.Contains(actualMultiSigAccount2.GetKey().PublicKey, [expectedAccountKey1.PublicKey, expectedAccountKey2.PublicKey, expectedAccountKey3.PublicKey]); + Assert.IsTrue(Helper.IsMultiSigContract(actualMultiSigAccount2.Contract.Script)); + Assert.IsTrue(expectedWallet.GetMultiSigAccounts().Contains(actualMultiSigAccount2)); + } } }