Skip to content
Snippets Groups Projects
substrate_sdk.dart 21.5 KiB
Newer Older
Hugo Trentesaux's avatar
Hugo Trentesaux committed
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gecko/globals.dart';
poka's avatar
poka committed
import 'package:gecko/models/chest_data.dart';
import 'package:gecko/models/wallet_data.dart';
import 'package:gecko/providers/home.dart';
import 'package:gecko/providers/my_wallets.dart';
poka's avatar
poka committed
import 'package:polkawallet_sdk/api/apiKeyring.dart';
import 'package:polkawallet_sdk/api/types/networkParams.dart';
poka's avatar
poka committed
import 'package:polkawallet_sdk/api/types/txInfoData.dart';
import 'package:polkawallet_sdk/polkawallet_sdk.dart';
import 'package:polkawallet_sdk/storage/keyring.dart';
poka's avatar
poka committed
import 'package:polkawallet_sdk/storage/types/keyPairData.dart';
import 'package:provider/provider.dart';
poka's avatar
poka committed
import 'package:truncate/truncate.dart';
// import 'package:web_socket_channel/io.dart';

class SubstrateSdk with ChangeNotifier {
  final WalletSDK sdk = WalletSDK();
  final Keyring keyring = Keyring();
poka's avatar
poka committed
  String generatedMnemonic = '';
poka's avatar
poka committed
  bool sdkLoading = false;
poka's avatar
poka committed
  bool importIsLoading = false;
  bool isLoadingEndpoint = false;
poka's avatar
poka committed
  String debugConnection = '';
  String transactionStatus = '';
  /////////////////////////////////////
  ////////// 1: API METHODS ///////////
  /////////////////////////////////////

poka's avatar
poka committed
  Future<String> executeCall(TxInfoData txInfo, txOptions, String password,
      [String? rawParams]) async {
    try {
      final hash = await sdk.api.tx
poka's avatar
poka committed
          .signAndSend(txInfo, txOptions, password, rawParam: rawParams)
          .timeout(
            const Duration(seconds: 12),
            onTimeout: () => {},
          );
      log.d(hash);
      if (hash.isEmpty) {
        transactionStatus = 'timeout';
        notifyListeners();

        return 'timeout';
      } else {
        transactionStatus = hash.toString();
        notifyListeners();
        return hash.toString();
      }
    } catch (e) {
      transactionStatus = e.toString();
      notifyListeners();
      return e.toString();
    }
  }

  Future getStorage(String call) async {
    return await sdk.webView!.evalJavascript('api.query.$call');
  }

  TxSenderData _setSender() {
    return TxSenderData(
      keyring.current.address,
      keyring.current.pubKey,
    );
  }

  ////////////////////////////////////////////
  ////////// 2: GET ONCHAIN STORAGE //////////
  ////////////////////////////////////////////

  Future<int> getIdentityIndexOf(String address) async {
    return await getStorage('identity.identityIndexOf("$address")') ?? 0;
  }

  Future<List<int>> getCerts(String address) async {
    final idtyIndex = await getIdentityIndexOf(address);
    final certsReceiver =
        await getStorage('cert.storageIdtyCertMeta($idtyIndex)') ?? [];

    return [certsReceiver['receivedCount'], certsReceiver['issuedCount']];
  }

  Future<int> getCertValidityPeriod(String from, String to) async {
    final idtyIndexFrom = await getIdentityIndexOf(from);
    final idtyIndexTo = await getIdentityIndexOf(to);

    if (idtyIndexFrom == 0 || idtyIndexTo == 0) return 0;

    final List certData =
        await getStorage('cert.certsByReceiver($idtyIndexTo)') ?? [];

    if (certData.isEmpty) return 0;
    for (List certInfo in certData) {
      if (certInfo[0] == idtyIndexFrom) {
        return certInfo[1];
      }
    }

    return 0;
  }

  Future<Map<String, dynamic>> getParameters() async {
    final currencyParameters =
        await getStorage('parameters.parametersStorage()') ?? {};
    return currencyParameters;
  }

  Future<bool> hasAccountConsumers(String address) async {
    final accountInfo = await getStorage('system.account("$address")');
    final consumers = accountInfo['consumers'];
    return consumers == 0 ? false : true;
  }

  // Future<double> getBalance(String address) async {
  //   double balance = 0.0;

  //   if (nodeConnected) {
  //     final brutBalance = await sdk.api.account.queryBalance(address);
  //     // log.d(brutBalance?.toJson());
  //     balance = int.parse(brutBalance!.freeBalance) / 100;
  //   } else {
  //     balance = -1;
  //   }

  //   await getUnclaimedUd(address);
  //   return balance;
  // }

  Future<Map<String, double>> getBalance(String address) async {
    if (!nodeConnected) {
      return {
        'transferableBalance': 0,
        'free': 0,
        'unclaimedUds': 0,
        'reserved': 0,
      };
    }

    // Get onchain storage values
    final Map balanceGlobal = await getStorage('system.account("$address")');
    final int? idtyIndex =
        await getStorage('identity.identityIndexOf("$address")');
    final Map? idtyData = idtyIndex == null
        ? null
        : await getStorage('identity.identities($idtyIndex)');
    final int currentUdIndex =
        int.parse(await getStorage('universalDividend.currentUdIndex()'));
    final List pastReevals =
        await getStorage('universalDividend.pastReevals()');

    // Compute amount of claimable UDs
    final int unclaimedUds = _computeUnclaimUds(currentUdIndex,
        idtyData?['data']?['firstEligibleUd'] ?? 0, pastReevals);

    // Calculate transferable and potential balance
    final int transferableBalance =
        (balanceGlobal['data']['free'] + unclaimedUds);

    Map<String, double> finalBalances = {
      'transferableBalance': transferableBalance / 100,
      'free': balanceGlobal['data']['free'] / 100,
      'unclaimedUds': unclaimedUds / 100,
      'reserved': balanceGlobal['data']['reserved'] / 100,
    };
    // log.i(finalBalances);
    return finalBalances;
  int _computeUnclaimUds(
      int currentUdIndex, int firstEligibleUd, List pastReevals) {
    int totalAmount = 0;

    if (firstEligibleUd == 0) return 0;

    for (final List reval in pastReevals.reversed) {
      final int revalNbr = reval[0];
      final int revalValue = reval[1];

      // Loop each UDs revaluations and sum unclaimed balance
      if (revalNbr <= firstEligibleUd) {
        final count = currentUdIndex - firstEligibleUd;
        totalAmount += count * revalValue;
        break;
      } else {
        final count = currentUdIndex - revalNbr;
        totalAmount += count * revalValue;
        currentUdIndex = revalNbr;
      }
    }

    return totalAmount;
  }

  Future<int> getSs58Prefix() async {
    final List res = await sdk.webView!.evalJavascript(
            'api.consts.system.ss58Prefix.words',
            wrapPromise: false) ??
        [42];

    ss58 = res[0];
    log.d(ss58);
    return ss58;
  }

  Future<bool> isMemberGet(String address) async {
    return await idtyStatus(address) == 'Validated';
  }

  Future<Map<String, int>> certState(String from, String to) async {
    Map<String, int> result = {};
    if (from != to && await isMemberGet(from)) {
      final removableOn = await getCertValidityPeriod(from, to);
      final certMeta = await getCertMeta(from);
      final int nextIssuableOn = certMeta['nextIssuableOn'] ?? 0;
      final certRemovableDuration = (removableOn - blocNumber) * 6;
      const int renewDelay = 2 * 30 * 24 * 3600; // 2 months

      if (certRemovableDuration >= renewDelay) {
        final certRenewDuration = certRemovableDuration - renewDelay;
        result.putIfAbsent('certRenewable', () => certRenewDuration);
      } else if (nextIssuableOn > blocNumber) {
        final certDelayDuration = (nextIssuableOn - blocNumber) * 6;
        result.putIfAbsent('certDelay', () => certDelayDuration);
      } else {
        result.putIfAbsent('canCert', () => 0);
      }
poka's avatar
poka committed
      // log.d('tatatatata: ${nextIssuableOn - blocNumber}');
    return result;
  }

  Future<Map> getCertMeta(String address) async {
    var idtyIndex = await getIdentityIndexOf(address);

    final certMeta =
        await getStorage('cert.storageIdtyCertMeta($idtyIndex)') ?? '';

    return certMeta;
  }

  Future<String> idtyStatus(String address, [bool smooth = true]) async {
    var idtyIndex = await getIdentityIndexOf(address);

    if (idtyIndex == 0) {
      return 'noid';
    }

    final idtyStatus = await getStorage('identity.identities($idtyIndex)');

    if (idtyStatus != null) {
      final String status = idtyStatus['status'];

      return (status);
    } else {
      return 'expired';
    }
  }

  Future getCurencyName() async {}

  /////////////////////////////////////
  ////// 3: SUBSTRATE CONNECTION //////
  /////////////////////////////////////

poka's avatar
poka committed
    sdkLoading = true;
poka's avatar
poka committed
    await keyring.init([ss58]);
    keyring.setSS58(ss58);

    await sdk.init(keyring);
    sdkReady = true;
poka's avatar
poka committed
    sdkLoading = false;
  String? getConnectedEndpoint() {
    return sdk.api.connectedNode?.endpoint;
  }

  Future<void> connectNode(BuildContext ctx) async {
    HomeProvider homeProvider = Provider.of<HomeProvider>(ctx, listen: false);
    homeProvider.changeMessage("connectionPending".tr(), 0);
poka's avatar
poka committed

poka's avatar
poka committed
    // configBox.delete('customEndpoint');
    final List<NetworkParams> listEndpoints =
        configBox.containsKey('customEndpoint')
            ? [getDuniterCustomEndpoint()]
            : getDuniterBootstrap();

poka's avatar
poka committed
    int timeout = 10000;
    if (sdk.api.connectedNode?.endpoint != null) {
      await sdk.api.setting.unsubscribeBestNumber();
poka's avatar
poka committed
    }

    isLoadingEndpoint = true;
    notifyListeners();
poka's avatar
poka committed
    final res = await sdk.api.connectNode(keyring, listEndpoints).timeout(
          Duration(milliseconds: timeout),
poka's avatar
poka committed
          onTimeout: () => null,
        );
    isLoadingEndpoint = false;
    notifyListeners();
    if (res != null) {
      nodeConnected = true;
poka's avatar
poka committed

      // Subscribe bloc number
      sdk.api.setting.subscribeBestNumber((res) {
        blocNumber = int.parse(res.toString());
        // log.d(sdk.api.connectedNode?.endpoint);
poka's avatar
poka committed
        if (sdk.api.connectedNode?.endpoint == null) {
          homeProvider.changeMessage("networkLost".tr(), 0);
poka's avatar
poka committed
        }
poka's avatar
poka committed
        notifyListeners();
      });
poka's avatar
poka committed
      // currencyName = await getCurencyName();
      homeProvider.changeMessage(
poka's avatar
poka committed
          "wellConnectedToNode"
              .tr(args: [getConnectedEndpoint()!.split('/')[2]]),
poka's avatar
poka committed
          5);
      // snackNode(ctx, true);
    } else {
      nodeConnected = false;
poka's avatar
poka committed
      debugConnection = res.toString();
      notifyListeners();
      homeProvider.changeMessage("noDuniterEndointAvailable".tr(), 0);
      // snackNode(ctx, false);
    log.d(sdk.api.connectedNode?.endpoint);
poka's avatar
poka committed

poka's avatar
poka committed
  List<NetworkParams> getDuniterBootstrap() {
    List<NetworkParams> node = [];

    for (String endpoint in configBox.get('endpoint')) {
poka's avatar
poka committed
      final n = NetworkParams();
      n.name = currencyName;
      n.endpoint = endpoint;
poka's avatar
poka committed
      n.ss58 = ss58;
      node.add(n);
    }
    return node;
  }

  NetworkParams getDuniterCustomEndpoint() {
    final nodeParams = NetworkParams();
    nodeParams.name = currencyName;
    nodeParams.endpoint = configBox.get('customEndpoint');
    nodeParams.ss58 = ss58;
    return nodeParams;
  }

poka's avatar
poka committed
  Future<String> importAccount(
      {String mnemonic = '',
      String derivePath = '',
poka's avatar
poka committed
      required String password}) async {
    const keytype = KeyType.mnemonic;
    if (mnemonic != '') generatedMnemonic = mnemonic;
poka's avatar
poka committed

poka's avatar
poka committed
    importIsLoading = true;
    notifyListeners();
poka's avatar
poka committed

    final json = await sdk.api.keyring
        .importAccount(keyring,
            keyType: keytype,
poka's avatar
poka committed
            key: generatedMnemonic,
            name: derivePath,
            password: password,
poka's avatar
poka committed
            derivePath: derivePath,
            cryptoType: CryptoType.sr25519)
poka's avatar
poka committed
        .catchError((e) {
      importIsLoading = false;
      notifyListeners();
    });
poka's avatar
poka committed
    if (json == null) return '';
poka's avatar
poka committed
    try {
      await sdk.api.keyring.addAccount(
poka's avatar
poka committed
        keyring,
poka's avatar
poka committed
        keyType: keytype,
poka's avatar
poka committed
        acc: json,
        password: password,
poka's avatar
poka committed
      );
    } catch (e) {
poka's avatar
poka committed
      importIsLoading = false;
      notifyListeners();
poka's avatar
poka committed
    }
poka's avatar
poka committed

poka's avatar
poka committed
    importIsLoading = false;
poka's avatar
poka committed
    notifyListeners();
    return keyring.allAccounts.last.address!;
poka's avatar
poka committed
  }

  //////////////////////////////////
  /////// 4: CRYPTOGRAPHY //////////
  //////////////////////////////////
poka's avatar
poka committed

  KeyPairData getKeypair(String address) {
    return keyring.keyPairs.firstWhere((kp) => kp.address == address,
        orElse: (() => KeyPairData()));
  }

  Future<bool> checkPassword(String address, String pass) async {
    final account = getKeypair(address);
    return await sdk.api.keyring.checkPassword(account, pass);
  }

  Future<String> getSeed(String address, String pin) async {
poka's avatar
poka committed
    final account = getKeypair(address);
    keyring.setCurrent(account);

    final seed = await sdk.api.keyring.getDecryptedSeed(keyring, pin);
    String seedText;
    if (seed == null) {
      seedText = '';
poka's avatar
poka committed
    } else {
      seedText = seed.seed!.split('//')[0];
    log.d(seedText);
    return seedText;
  int getDerivationNumber(String address) {
    final account = getKeypair(address);
    final deriveNbr = account.name!.split('//')[1];
    return int.parse(deriveNbr);
  }

  Future<KeyPairData?> changePassword(BuildContext context, String address,
      String passOld, String? passNew) async {
    final account = getKeypair(address);
    MyWalletsProvider myWalletProvider =
        Provider.of<MyWalletsProvider>(context, listen: false);
    keyring.setCurrent(account);
    myWalletProvider.resetPinCode();

    return await sdk.api.keyring.changePassword(keyring, passOld, passNew);
  }

poka's avatar
poka committed
  Future<void> deleteAllAccounts() async {
    for (var account in keyring.allAccounts) {
      await sdk.api.keyring.deleteAccount(keyring, account);
    }
  }

poka's avatar
poka committed
  Future<void> deleteAccounts(List<String> address) async {
    for (var a in address) {
      final account = getKeypair(a);
      await sdk.api.keyring.deleteAccount(keyring, account);
    }
  }

  Future<String> generateMnemonic({String lang = appLang}) async {
poka's avatar
poka committed
    final gen = await sdk.api.keyring.generateMnemonic(ss58);
poka's avatar
poka committed
    generatedMnemonic = gen.mnemonic!;
poka's avatar
poka committed

poka's avatar
poka committed
    return gen.mnemonic!;
poka's avatar
poka committed
  }
poka's avatar
poka committed

  Future<String> setCurrentWallet(WalletData wallet) async {
poka's avatar
poka committed
    final currentChestNumber = configBox.get('currentChest');
    ChestData newChestData = chestBox.get(currentChestNumber)!;
    newChestData.defaultWallet = wallet.number;
    await chestBox.put(currentChestNumber, newChestData);
poka's avatar
poka committed

poka's avatar
poka committed
    try {
      final acc = getKeypair(wallet.address!);
poka's avatar
poka committed
      keyring.setCurrent(acc);
      return acc.address!;
    } catch (e) {
      return (e.toString());
    }
  }

  KeyPairData getCurrentWallet() {
    try {
      final acc = keyring.current;
      return acc;
    } catch (e) {
      return KeyPairData();
    }
  }

  Future<String> derive(
      BuildContext context, String address, int number, String password) async {
    final keypair = getKeypair(address);

poka's avatar
poka committed
    //TODO: fix null keypair after used chest import
    log.d('tatatata $address $number $password ${keypair.encoded}');

    final seedMap =
        await keyring.store.getDecryptedSeed(keypair.pubKey, password);

    if (seedMap?['type'] != 'mnemonic') return '';
    final List seedList = seedMap!['seed'].split('//');
    generatedMnemonic = seedList[0];

    return await importAccount(
        mnemonic: generatedMnemonic,
        derivePath: '//$number',
        password: password);
  }

  Future<String> generateRootKeypair(String address, String password) async {
    final keypair = getKeypair(address);

    final seedMap =
        await keyring.store.getDecryptedSeed(keypair.pubKey, password);

    if (seedMap?['type'] != 'mnemonic') return '';
    final List seedList = seedMap!['seed'].split('//');
    generatedMnemonic = seedList[0];

poka's avatar
poka committed
    return await importAccount(password: password);
  }

  Future<bool> isMnemonicValid(String mnemonic) async {
    // Needed for bad encoding of UTF-8
    mnemonic = mnemonic.replaceAll('é', 'é');
    mnemonic = mnemonic.replaceAll('è', 'è');

    return await sdk.api.keyring.checkMnemonicValid(mnemonic);
  }

  //////////////////////////////////////
  ///////// 5: CALLS EXECUTION /////////
  //////////////////////////////////////

  Future<String> pay(
poka's avatar
poka committed
      {required String fromAddress,
      required String destAddress,
      required double amount,
      required String password}) async {
    transactionStatus = '';
    final fromPubkey = await sdk.api.account.decodeAddress([fromAddress]);
    final int amountUnit = (amount * 100).toInt();

poka's avatar
poka committed
    final sender = TxSenderData(
poka's avatar
poka committed
    );
    final globalBalance = await getBalance(fromAddress);
    TxInfoData txInfo;
poka's avatar
poka committed
    List txOptions = [];
    String? rawParams;

    if (globalBalance['unclaimedUds'] == 0) {
      txInfo = TxInfoData('balances',
          amount == -1 ? 'transferAll' : 'transferKeepAlive', sender);
      txOptions = [destAddress, amount == -1 ? false : amountUnit];
    } else {
      txInfo = TxInfoData(
        'utility',
        'batchAll',
        sender,
      );
poka's avatar
poka committed
      const tx1 = 'api.tx.universalDividend.claimUds()';
      final tx2 = amount == -1
          ? 'api.tx.balances.transferAll(false)'
          : 'api.tx.balances.transferKeepAlive("$destAddress", $amountUnit)';
poka's avatar
poka committed
      rawParams = '[[$tx1, $tx2]]';
    // log.d('pay args:  ${txInfo.module}, ${txInfo.call}, $txOptions, $rawParams');
poka's avatar
poka committed
    return await executeCall(txInfo, txOptions, password, rawParams);
  }

  Future<String> certify(
      String fromAddress, String password, String toAddress) async {
    transactionStatus = '';

    final myIdtyStatus = await idtyStatus(fromAddress);
    final toIdtyStatus = await idtyStatus(toAddress);
    final fromIndex = await getIdentityIndexOf(fromAddress);
    final toIndex = await getIdentityIndexOf(toAddress);

    if (myIdtyStatus != 'Validated') {
      transactionStatus = 'notMember';
      notifyListeners();
      return 'notMember';
    }

    TxInfoData txInfo;
poka's avatar
poka committed
    List txOptions = [];
poka's avatar
poka committed
    String? rawParams;
    final toCerts = await getCerts(toAddress);
    final currencyParameters = await getParameters();

    if (toIdtyStatus == 'noid') {
      txInfo = TxInfoData(
        'identity',
        'createIdentity',
        sender,
      );
poka's avatar
poka committed
      txOptions = [toAddress];
    } else if (toIdtyStatus == 'Validated' ||
        toIdtyStatus == 'ConfirmedByOwner') {
      if (toCerts[0] >= currencyParameters['wotMinCertForMembership'] &&
          toIdtyStatus != 'Validated') {
        log.i('Batch cert and membership validation');
poka's avatar
poka committed
        txInfo = TxInfoData(
          'utility',
          'batchAll',
          sender,
        );
        final tx1 = 'cert.addCert($fromIndex, $toIndex)';
        final tx2 = 'identity.validateIdentity($toIndex)';

        rawParams = '[[$tx1, $tx2]]';
      } else {
        txInfo = TxInfoData(
          'cert',
          'addCert',
          sender,
        );
poka's avatar
poka committed
        txOptions = [fromIndex, toIndex];
    } else {
      transactionStatus = 'cantBeCert';
      notifyListeners();
      return 'cantBeCert';
    }

    log.d('Cert action: ${txInfo.call!}');
poka's avatar
poka committed
    return await executeCall(txInfo, txOptions, password, rawParams);
poka's avatar
poka committed
  }
poka's avatar
poka committed
  // Future claimUDs(String password) async {
  //   final sender = TxSenderData(
  //     keyring.current.address,
  //     keyring.current.pubKey,
  //   );
poka's avatar
poka committed
  //   final txInfo = TxInfoData(
  //     'universalDividend',
  //     'claimUds',
  //     sender,
  //   );
poka's avatar
poka committed
  //   return await executeCall(txInfo, [], password);
  // }
poka's avatar
poka committed

  Future<String> confirmIdentity(
      String fromAddress, String name, String password) async {
    log.d('me: ${keyring.current.address!}');
poka's avatar
poka committed

    final sender = TxSenderData(
      keyring.current.address,
      keyring.current.pubKey,
    );

    final txInfo = TxInfoData(
      'identity',
      'confirmIdentity',
      sender,
    );
    return await executeCall(txInfo, txOptions, password);
poka's avatar
poka committed
  Future revokeIdentity(String address, String password) async {
    final idtyIndex = await getIdentityIndexOf(address);
poka's avatar
poka committed

    final sender = TxSenderData(
      keyring.current.address,
      keyring.current.pubKey,
    );

    log.d(sender.address);
    TxInfoData txInfo;

    txInfo = TxInfoData(
      'membership',
      'revokeMembership',
      sender,
    );

poka's avatar
poka committed

    return await executeCall(txInfo, txOptions, password);
////////////////////////////////////////////
/////// 6: UI ELEMENTS (off class) /////////
////////////////////////////////////////////

poka's avatar
poka committed
void snack(BuildContext context, String message, {int duration = 2}) {
  final snackBar =
      SnackBar(content: Text(message), duration: Duration(seconds: duration));
  ScaffoldMessenger.of(context).showSnackBar(snackBar);
}

class AddressInfo {
  final String? address;
  double balance;

  AddressInfo({@required this.address, this.balance = 0});
}

void snackNode(BuildContext context, bool isConnected) {
  String message;
  if (!isConnected) {
    message =
        "${"noDuniterNodeAvailableTryLater".tr()}:\n${configBox.get('endpoint').first}";
    SubstrateSdk sub = Provider.of<SubstrateSdk>(context, listen: false);
    message =
        "${"youAreConnectedToNode".tr()}\n${sub.getConnectedEndpoint()!.split('//')[1]}";
poka's avatar
poka committed
  final snackBar = SnackBar(
      padding: const EdgeInsets.all(20),
      content: Text(message, style: const TextStyle(fontSize: 16)),
poka's avatar
poka committed
      duration: const Duration(seconds: 4));
  ScaffoldMessenger.of(context).showSnackBar(snackBar);
}

poka's avatar
poka committed
String getShortPubkey(String pubkey) {
poka's avatar
poka committed
  String pubkeyShort = truncate(pubkey, 7,
poka's avatar
poka committed
          omission: String.fromCharCode(0x2026),
          position: TruncatePosition.end) +
poka's avatar
poka committed
      truncate(pubkey, 6, omission: "", position: TruncatePosition.start);
poka's avatar
poka committed
  return pubkeyShort;
poka's avatar
poka committed

class PasswordException implements Exception {
  String cause;
  PasswordException(this.cause);
}