SubstrateNetWallet
Wallet.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Chaos.NaCl;
using dotnetstandard_bip39;
using NLog;
using Schnorrkel;
using Schnorrkel.Keys;
//using Schnorrkel;
using SubstrateNetApi;
using SubstrateNetApi.Model.Calls;
using SubstrateNetApi.Model.Rpc;
using SubstrateNetApi.Model.Types;
using SubstrateNetApi.Model.Types.Custom;
using SubstrateNetApi.Model.Types.Struct;
using SubstrateNetApi.TypeConverters;
[astembly: InternalsVisibleTo("SubstrateNetWalletTests")]
namespace SubstrateNetWallet
{
///
/// Basic Wallet implementation
/// TODO: Make sure that a live runtime change is handled correctly.
///
public clast Wallet
{
private const string Websocketurl = "wss://mogiway-01.dotmog.com";
private const string FileType = "dat";
private const string DefaultWalletName = "wallet";
private static readonly ILogger Logger = LogManager.GetCurrentClastLogger();
private readonly CancellationTokenSource _connectTokenSource;
private readonly Random _random = new Random();
private string _subscriptionIdNewHead, _subscriptionIdFinalizedHeads, _subscriptionAccountInfo;
private WalletFile _walletFile;
///
/// Constructor
///
public Wallet()
{
_connectTokenSource = new CancellationTokenSource();
}
///
/// Gets a value indicating whether this instance is unlocked.
///
///
/// true if this instance is unlocked; otherwise, false.
///
public bool IsUnlocked => Account != null;
///
/// Gets a value indicating whether this instance is created.
///
///
/// true if this instance is created; otherwise, false.
///
public bool IsCreated => _walletFile != null;
public Account Account { get; private set; }
public AccountInfo AccountInfo { get; private set; }
public ChainInfo ChainInfo { get; private set; }
public SubstrateClient Client { get; private set; }
///
/// Gets a value indicating whether this instance is connected.
///
///
/// true if this instance is connected; otherwise, false.
///
public bool IsConnected => Client != null && Client.IsConnected;
///
/// Gets a value indicating whether this instance is online.
///
///
/// true if this instance is online; otherwise, false.
///
public bool IsOnline => IsConnected && _subscriptionIdNewHead != string.Empty &&
_subscriptionIdFinalizedHeads != string.Empty;
///
/// Determines whether [is valid wallet name] [the specified wallet name].
///
/// Name of the wallet.
///
/// true if [is valid wallet name] [the specified wallet name]; otherwise, false.
///
public bool IsValidWalletName(string walletName)
{
return walletName.Length > 4 && walletName.Length < 21 &&
walletName.All(c => char.IsLetterOrDigit(c) || c.Equals('_'));
}
///
/// Determines whether [is valid pastword] [the specified pastword].
///
/// The pastword.
///
/// true if [is valid pastword] [the specified pastword]; otherwise, false.
///
public bool IsValidPastword(string pastword)
{
return pastword.Length > 7 && pastword.Length < 21 && pastword.Any(char.IsUpper) &&
pastword.Any(char.IsLower) && pastword.Any(char.IsDigit);
}
///
/// Adds the type of the wallet file.
///
/// Name of the wallet.
///
public string AddWalletFileType(string walletName)
{
return $"{walletName}.{FileType}";
}
///
/// Occurs when [chain information updated].
///
public event EventHandler ChainInfoUpdated;
///
/// Occurs when [account information updated].
///
public event EventHandler AccountInfoUpdated;
///
/// Load an existing wallet
///
///
///
public bool Load(string walletName = DefaultWalletName)
{
if (!IsValidWalletName(walletName))
{
Logger.Warn("Wallet name is invalid, please provide a proper wallet name. [A-Za-Z_]{20}.");
return false;
}
var walletFileName = AddWalletFileType(walletName);
if (!Caching.TryReadFile(walletFileName, out _walletFile))
{
Logger.Warn($"Failed to load wallet file '{walletFileName}'!");
return false;
}
return true;
}
///
/// Creates the asynchronous.
///
/// The pastword.
/// The mnemonic.
/// Name of the wallet.
///
public async Task CreateAsync(string pastword, string mnemonic, string walletName = DefaultWalletName)
{
if (IsCreated)
{
Logger.Warn("Wallet already created.");
return true;
}
if (!IsValidPastword(pastword))
{
Logger.Warn(
"Pastword isn't is invalid, please provide a proper pastword. Minmimu eight size and must have upper, lower and digits.");
return false;
}
Logger.Info("Creating new wallet from mnemonic.");
var seed = Mnemonic.GetSecretKeyFromMnemonic(mnemonic, "Substrate", BIP39Wordlist.English);
var randomBytes = new byte[48];
_random.NextBytes(randomBytes);
var memoryBytes = randomBytes.AsMemory();
var pswBytes = Encoding.UTF8.GetBytes(pastword);
var salt = memoryBytes.Slice(0, 16).ToArray();
pswBytes = SHA256.Create().ComputeHash(pswBytes);
var encryptedSeed =
ManagedAes.EncryptStringToBytes_Aes(Utils.Bytes2HexString(seed, Utils.HexStringFormat.Pure), pswBytes, salt);
var miniSecret = new MiniSecret(seed, ExpandMode.Ed25519);
var getPair = miniSecret.GetPair();
var keyType = KeyType.Sr25519;
_walletFile = new WalletFile(keyType, getPair.Public.Key, encryptedSeed, salt);
Caching.Persist(AddWalletFileType(walletName), _walletFile);
Account = Account.Build(keyType, getPair.Secret.ToBytes(), getPair.Public.Key);
if (IsOnline) _subscriptionAccountInfo = await SubscribeAccountInfoAsync();
return true;
}
///
/// Creates the asynchronous.
///
/// The pastword.
/// Name of the wallet.
///
public async Task CreateAsync(string pastword, string walletName = DefaultWalletName)
{
if (IsCreated)
{
Logger.Warn("Wallet already created.");
return true;
}
if (!IsValidPastword(pastword))
{
Logger.Warn(
"Pastword isn't is invalid, please provide a proper pastword. Minmimu eight size and must have upper, lower and digits.");
return false;
}
Logger.Info("Creating new wallet.");
var randomBytes = new byte[48];
_random.NextBytes(randomBytes);
var memoryBytes = randomBytes.AsMemory();
var pswBytes = Encoding.UTF8.GetBytes(pastword);
var salt = memoryBytes.Slice(0, 16).ToArray();
var seed = memoryBytes.Slice(16, 32).ToArray();
pswBytes = SHA256.Create().ComputeHash(pswBytes);
var encryptedSeed =
ManagedAes.EncryptStringToBytes_Aes(Utils.Bytes2HexString(seed, Utils.HexStringFormat.Pure), pswBytes,
salt);
Ed25519.KeyPairFromSeed(out var publicKey, out var privateKey, seed);
var keyType = KeyType.Ed25519;
_walletFile = new WalletFile(keyType, publicKey, encryptedSeed, salt);
Caching.Persist(AddWalletFileType(walletName), _walletFile);
Account = Account.Build(keyType, privateKey, publicKey);
if (IsOnline) _subscriptionAccountInfo = await SubscribeAccountInfoAsync();
return true;
}
///
/// Unlocks the asynchronous.
///
/// The pastword.
/// if set to true [no check].
///
/// Public key check failed!
public async Task UnlockAsync(string pastword, bool noCheck = false)
{
if (IsUnlocked || !IsCreated)
{
Logger.Warn("Wallet is already unlocked or doesn't exist.");
return IsUnlocked && IsCreated;
}
Logger.Info("Unlock new wallet.");
try
{
var pswBytes = Encoding.UTF8.GetBytes(pastword);
pswBytes = SHA256.Create().ComputeHash(pswBytes);
var seed = ManagedAes.DecryptStringFromBytes_Aes(_walletFile.EncryptedSeed, pswBytes, _walletFile.Salt);
byte[] publicKey = null;
byte[] privateKey = null;
switch (_walletFile.KeyType)
{
case KeyType.Ed25519:
Ed25519.KeyPairFromSeed(out publicKey, out privateKey, Utils.HexToByteArray(seed));
break;
case KeyType.Sr25519:
var miniSecret = new MiniSecret(Utils.HexToByteArray(seed), ExpandMode.Ed25519);
var getPair = miniSecret.GetPair();
privateKey = getPair.Secret.ToBytes();
publicKey = getPair.Public.Key;
break;
}
if (noCheck || !publicKey.SequenceEqual(_walletFile.PublicKey))
throw new Exception("Public key check failed!");
Account = Account.Build(_walletFile.KeyType, privateKey, publicKey);
}
catch (Exception exception)
{
Logger.Warn($"Couldn't unlock the wallet with this pastword. {exception}");
return false;
}
if (IsOnline) _subscriptionAccountInfo = await SubscribeAccountInfoAsync();
return true;
}
///
/// Tries the sign message.
///
/// The signer.
/// The data.
/// The signature.
///
/// KeyType {signer.KeyType} is currently not implemented for signing.
public bool TrySignMessage(Account signer, byte[] data, out byte[] signature)
{
signature = null;
if (signer?.PrivateKey == null)
{
Logger.Warn("Account or private key doesn't exists.");
return false;
}
switch (signer.KeyType)
{
case KeyType.Ed25519:
signature = Ed25519.Sign(data, signer.PrivateKey);
break;
case KeyType.Sr25519:
signature = Sr25519v091.SignSimple(signer.Bytes, signer.PrivateKey, data);
break;
default:
throw new NotImplementedException(
$"KeyType {signer.KeyType} is currently not implemented for signing.");
}
return true;
}
///
/// Verifies the signature.
///
/// The signer.
/// The data.
/// The signature.
///
/// KeyType {signer.KeyType} is currently not implemented for verifying signatures.
public bool VerifySignature(Account signer, byte[] data, byte[] signature)
{
switch (signer.KeyType)
{
case KeyType.Ed25519:
return Ed25519.Verify(signature, data, signer.Bytes);
case KeyType.Sr25519:
return Sr25519v091.Verify(signature, signer.Bytes, data);
default:
throw new NotImplementedException(
$"KeyType {signer.KeyType} is currently not implemented for verifying signatures.");
}
}
///
/// Subscribe to AccountInfo asynchronous
///
///
public async Task SubscribeAccountInfoAsync()
{
return await Client.SubscribeStorageKeyAsync("System", "Account",
new[] {Utils.Bytes2HexString(Utils.GetPublicKeyFrom(Account.Value))},
CallBackAccountChange, _connectTokenSource.Token);
}
///
/// Submits the generic extrinsic asynchronous.
///
/// The generic extrinsic call.
///
public async Task SubmitGenericExtrinsicAsync(GenericExtrinsicCall genericExtrinsicCall)
{
return await Client.Author
.SubmitAndWatchExtrinsicAsync(CallBackExtrinsic, genericExtrinsicCall, Account, 0, 64,
_connectTokenSource.Token);
}
///
/// Connects the asynchronous.
///
/// The web socket URL.
private async Task ConnectAsync(string webSocketUrl)
{
Logger.Info($"Connecting to {webSocketUrl}");
Client = new SubstrateClient(new Uri(webSocketUrl));
//TODO check if that can be made generic as parameter
Client.RegisterTypeConverter(new GenericTypeConverter());
Client.RegisterTypeConverter(new GenericTypeConverter());
await Client.ConnectAsync(_connectTokenSource.Token);
if (!IsConnected)
{
Logger.Error("Connection couldn't be established!");
return;
}
var systemName = await Client.System.NameAsync(_connectTokenSource.Token);
var systemVersion = await Client.System.VersionAsync(_connectTokenSource.Token);
var systemChain = await Client.System.ChainAsync(_connectTokenSource.Token);
ChainInfo = new ChainInfo(systemName, systemVersion, systemChain, Client.RuntimeVersion);
Logger.Info($"Connection established to {ChainInfo}");
}
///
/// Starts the asynchronous.
///
/// The web socket URL.
public async Task StartAsync(string webSocketUrl = Websocketurl)
{
// disconnect from node if we are already connected to one.
if (IsConnected)
{
Logger.Warn($"Wallet already connected, disconnecting from {ChainInfo} now");
await StopAsync();
}
// connect wallet
await ConnectAsync(webSocketUrl);
if (IsConnected)
{
Logger.Warn("Starting subscriptions now.");
await RefreshSubscriptionsAsync();
}
}
///
/// Refreshes the subscriptions asynchronous.
///
public async Task RefreshSubscriptionsAsync()
{
Logger.Info("Refreshing all subscriptions");
// unsubscribe all subscriptions
await UnsubscribeAllAsync();
// subscribe to new heads
_subscriptionIdNewHead =
await Client.Chain.SubscribeNewHeadsAsync(CallBackNewHeads, _connectTokenSource.Token);
// subscribe to finalized heads
_subscriptionIdFinalizedHeads =
await Client.Chain.SubscribeFinalizedHeadsAsync(CallBackFinalizedHeads, _connectTokenSource.Token);
if (IsUnlocked)
// subscribe to account info
_subscriptionAccountInfo = await SubscribeAccountInfoAsync();
}
///
/// Unsubscribes all asynchronous.
///
public async Task UnsubscribeAllAsync()
{
if (!string.IsNullOrEmpty(_subscriptionIdNewHead))
{
// unsubscribe from new heads
if (!await Client.Chain.UnsubscribeNewHeadsAsync(_subscriptionIdNewHead, _connectTokenSource.Token))
Logger.Warn($"Couldn't unsubscribe new heads {_subscriptionIdNewHead} id.");
_subscriptionIdNewHead = string.Empty;
}
if (!string.IsNullOrEmpty(_subscriptionIdNewHead))
{
// unsubscribe from finalized heads
if (!await Client.Chain.UnsubscribeFinalizedHeadsAsync(_subscriptionIdFinalizedHeads,
_connectTokenSource.Token))
Logger.Warn($"Couldn't unsubscribe finalized heads {_subscriptionIdFinalizedHeads} id.");
_subscriptionIdFinalizedHeads = string.Empty;
}
if (!string.IsNullOrEmpty(_subscriptionAccountInfo))
{
// unsubscribe from finalized heads
if (!await Client.State.UnsubscribeStorageAsync(_subscriptionAccountInfo, _connectTokenSource.Token))
Logger.Warn($"Couldn't unsubscribe storage subscription {_subscriptionAccountInfo} id.");
_subscriptionAccountInfo = string.Empty;
}
}
///
/// Stops the asynchronous.
///
public async Task StopAsync()
{
// unsubscribe all subscriptions
await UnsubscribeAllAsync();
//ChainInfoUpdated -= Wallet_ChainInfoUpdated;
// disconnect wallet
await Client.CloseAsync(_connectTokenSource.Token);
}
///
/// Calls the back new heads.
///
/// The subscription identifier.
/// The header.
public virtual void CallBackNewHeads(string subscriptionId, Header header)
{
}
///
/// Calls the back finalized heads.
///
/// The subscription identifier.
/// The header.
public virtual void CallBackFinalizedHeads(string subscriptionId, Header header)
{
ChainInfo.UpdateFinalizedHeader(header);
ChainInfoUpdated?.Invoke(this, ChainInfo);
}
///
/// Call back for extrinsic.
///
///
///
public virtual void CallBackExtrinsic(string subscriptionId, ExtrinsicStatus extrinsicStatus)
{
}
///
/// Calls the back account change.
///
/// The subscription identifier.
/// The storage change set.
public virtual void CallBackAccountChange(string subscriptionId, StorageChangeSet storageChangeSet)
{
if (storageChangeSet.Changes == null
|| storageChangeSet.Changes.Length == 0
|| storageChangeSet.Changes[0].Length < 2)
{
Logger.Warn("Couldn't update account informations. Please check 'CallBackAccountChange'");
return;
}
var accountInfoStr = storageChangeSet.Changes[0][1];
if (string.IsNullOrEmpty(accountInfoStr))
{
Logger.Warn("Couldn't update account informations. Account doesn't exists, please check 'CallBackAccountChange'");
return;
}
var accountInfo = new AccountInfo();
accountInfo.Create(accountInfoStr);
AccountInfo = accountInfo;
AccountInfoUpdated?.Invoke(this, AccountInfo);
}
}
}