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));
+ }
}
}