diff --git a/.github/workflows/znn_cli_builder.yml b/.github/workflows/znn_cli_builder.yml index eaaac8b..00a3ad4 100644 --- a/.github/workflows/znn_cli_builder.yml +++ b/.github/workflows/znn_cli_builder.yml @@ -3,7 +3,6 @@ name: Build and release znn-cli on: push: branches: - - cicd - master # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -30,7 +29,7 @@ jobs: - name: Install dependencies run: dart pub get - name: Build znn-cli - run: dart compile exe cli_handler.dart -o ${{ matrix.output-name }} + run: dart compile exe znn-cli.dart -o ${{ matrix.output-name }} - name: Upload artifacts uses: actions/upload-artifact@v3 with: @@ -62,12 +61,17 @@ jobs: - name: Download libpow_links uses: robinraju/release-downloader@v1.8 with: - repository: "alienc0der/znn-pow-links-cpp" + repository: "zenon-network/znn-pow-links-cpp" latest: true - name: Download libargon2 uses: robinraju/release-downloader@v1.8 with: - repository: "alienc0der/argon2_ffi" + repository: "zenon-network/argon2_ffi" + latest: true + - name: Download libledger_ffi + uses: robinraju/release-downloader@v1.8 + with: + repository: "zenon-network/ledger_ffi_rs" latest: true - name: Package Linux binaries run: | @@ -75,26 +79,29 @@ jobs: chmod +x releases/znn-cli unzip -j libpow_links-linux-amd64.zip -d releases/ unzip -j libargon2_ffi-linux-amd64.zip -d releases/ - zip -jr releases/znn-cli-linux-amd64.zip releases/znn-cli releases/libpow_links.so releases/libargon2_ffi_plugin.so - rm releases/znn-cli releases/libpow_links.so releases/libargon2_ffi_plugin.so + unzip -j libledger_ffi-linux-amd64.zip -d releases/ + zip -jr releases/znn-cli-linux-amd64.zip releases/znn-cli releases/libpow_links.so releases/libargon2_ffi_plugin.so releases/libledger_ffi.so + rm releases/znn-cli releases/libpow_links.so releases/libargon2_ffi_plugin.so releases/libledger_ffi.so - name: Package Windows binaries run: | mv znn-cli-windows.exe releases/znn-cli.exe chmod +x releases/znn-cli.exe unzip -j libpow_links-windows-amd64.zip -d releases/ unzip -j libargon2_ffi-windows-amd64.zip -d releases/ + unzip -j libledger_ffi-windows-amd64.zip -d releases/ rm releases/generator.exe - zip -jr releases/znn-cli-windows-amd64.zip releases/znn-cli.exe releases/libpow_links.dll releases/argon2_ffi_plugin.dll - rm releases/znn-cli.exe releases/libpow_links.dll releases/argon2_ffi_plugin.dll + zip -jr releases/znn-cli-windows-amd64.zip releases/znn-cli.exe releases/libpow_links.dll releases/argon2_ffi_plugin.dll releases/libledger_ffi.dll + rm releases/znn-cli.exe releases/libpow_links.dll releases/argon2_ffi_plugin.dll releases/libledger_ffi.dll - name: Package macOS binaries run: | mv znn-cli-macos releases/znn-cli chmod +x releases/znn-cli unzip -j libpow_links-darwin-universal.zip -d releases/ unzip -j libargon2_ffi-darwin-universal.zip -d releases/ + unzip -j libledger_ffi-darwin-arch64.zip -d releases/ rm releases/generator - zip -jr releases/znn-cli-darwin-universal.zip releases/znn-cli releases/libpow_links.dylib releases/libargon2_ffi.dylib - rm releases/znn-cli releases/libpow_links.dylib releases/libargon2_ffi.dylib + zip -jr releases/znn-cli-darwin-universal.zip releases/znn-cli releases/libpow_links.dylib releases/libargon2_ffi.dylib releases/libledger_ffi.dylib + rm releases/znn-cli releases/libpow_links.dylib releases/libargon2_ffi.dylib releases/libledger_ffi.dylib - name: Generate checksums run: | cd releases/ diff --git a/LICENSE b/LICENSE index a1a4b0b..62f1426 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 The Zenon developers +Copyright (c) 2023 The Zenon developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index a529eab..6eb221a 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,17 @@ default: all all: + echo Use 'make windows' or 'make linux' to build the znn-cli binary + +windows: + -mkdir build + dart pub get + dart compile exe znn-cli.dart -o build\znn-cli.exe + copy .\Resources\* .\build\ + +linux: mkdir -p build - dart compile exe cli_handler.dart -o build/znn-cli + dart pub get + dart compile exe znn-cli.dart -o build/znn-cli cp ./Resources/* ./build + diff --git a/README.md b/README.md index 9f0d1e1..5728b4a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ # Zenon CLI tool -The Zenon CLI tool is designed to interact with Alphanet - Network of Momentum Phase 0. +The Zenon CLI tool is designed to interact with Alphanet - Network of Momentum Phase 1. ## Building -`make` +Windows: `make windows` +Linux: `make linux` + +## Installation +Download and extract the [latest version](https://github.com/hypercore-one/znn_cli_dart/releases/). ## Notes All the required dynamic libraries were copied in the `build/` folder. When Moving the `znn-cli` binary, don't forget to move the libraries as well. diff --git a/Resources/libledger_ffi.dll b/Resources/libledger_ffi.dll new file mode 100644 index 0000000..76017b3 Binary files /dev/null and b/Resources/libledger_ffi.dll differ diff --git a/Resources/libledger_ffi.dylib b/Resources/libledger_ffi.dylib new file mode 100644 index 0000000..7df1630 Binary files /dev/null and b/Resources/libledger_ffi.dylib differ diff --git a/Resources/libledger_ffi.so b/Resources/libledger_ffi.so new file mode 100644 index 0000000..fb91a7e Binary files /dev/null and b/Resources/libledger_ffi.so differ diff --git a/cli_handler.dart b/cli_handler.dart deleted file mode 100644 index 2e343dc..0000000 --- a/cli_handler.dart +++ /dev/null @@ -1,1165 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:bip39/bip39.dart' as bip39; -import 'package:dcli/dcli.dart'; -import 'package:path/path.dart' as path; -import 'package:znn_sdk_dart/znn_sdk_dart.dart'; - -import 'init_znn.dart'; - -Future main(List args) async { - return initZnn(args, handleCli); -} - -Future handleCli(List args) async { - final Zenon znnClient = Zenon(); - Address? address = (await znnClient.defaultKeyPair?.address); - - switch (args[0]) { - case 'version': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('version'); - break; - } - print('$znnCli v$znnCliVersion using Zenon SDK v$znnSdkVersion'); - print(getZnndVersion()); - break; - - case 'send': - if (!(args.length == 4 || args.length == 5)) { - print('Incorrect number of arguments. Expected:'); - print( - 'send toAddress amount [${green('ZNN')}/${blue('QSR')}/${magenta('ZTS')}]'); - break; - } - Address newAddress = Address.parse(args[1]); - late BigInt amount; - TokenStandard tokenStandard; - if (args[3] == 'znn' || args[3] == 'ZNN') { - tokenStandard = znnZts; - } else if (args[3] == 'qsr' || args[3] == 'QSR') { - tokenStandard = qsrZts; - } else { - tokenStandard = TokenStandard.parse(args[3]); - } - - AccountInfo info = - await znnClient.ledger.getAccountInfoByAddress(address!); - bool ok = true; - bool found = false; - for (BalanceInfoListItem entry in info.balanceInfoList!) { - if (entry.token!.tokenStandard.toString() == tokenStandard.toString()) { - amount = AmountUtils.extractDecimals( - num.parse(args[2]), entry.token!.decimals); - if (entry.balance! < amount) { - print( - '${red("Error!")} You only have ${AmountUtils.addDecimals(entry.balance!, entry.token!.decimals)} ${entry.token!.symbol} tokens'); - ok = false; - break; - } - found = true; - } - } - - if (!ok) break; - if (!found) { - print( - '${red("Error!")} You only have ${AmountUtils.addDecimals(BigInt.zero, 0)} ${tokenStandard.toString()} tokens'); - break; - } - Token? token = await znnClient.embedded.token.getByZts(tokenStandard); - var block = AccountBlockTemplate.send(newAddress, tokenStandard, amount); - - if (args.length == 5) { - block.data = AsciiEncoder().convert(args[4]); - print( - 'Sending ${AmountUtils.addDecimals(amount, token!.decimals)} ${args[3]} to ${args[1]} with a message "${args[4]}"'); - } else { - print( - 'Sending ${AmountUtils.addDecimals(amount, token!.decimals)} ${args[3]} to ${args[1]}'); - } - - await znnClient.send(block); - print('Done'); - break; - - case 'receive': - if (args.length != 2) { - print('Incorrect number of arguments. Expected:'); - print('receive blockHash'); - break; - } - Hash sendBlockHash = Hash.parse(args[1]); - print('Please wait ...'); - await znnClient.send(AccountBlockTemplate.receive(sendBlockHash)); - print('Done'); - break; - - case 'receiveAll': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('receiveAll'); - break; - } - var unreceived = (await znnClient.ledger - .getUnreceivedBlocksByAddress(address!, pageIndex: 0, pageSize: 5)); - if (unreceived.count == 0) { - print('Nothing to receive'); - break; - } else { - if (unreceived.more!) { - print( - 'You have ${red("more")} than ${green(unreceived.count.toString())} transaction(s) to receive'); - } else { - print( - 'You have ${green(unreceived.count.toString())} transaction(s) to receive'); - } - } - - print('Please wait ...'); - while (unreceived.count! > 0) { - for (var block in unreceived.list!) { - await znnClient.send(AccountBlockTemplate.receive(block.hash)); - } - unreceived = (await znnClient.ledger - .getUnreceivedBlocksByAddress(address, pageIndex: 0, pageSize: 5)); - } - print('Done'); - break; - - case 'autoreceive': - znnClient.wsClient - .addOnConnectionEstablishedCallback((broadcaster) async { - print('Subscribing for account-block events ...'); - await znnClient.subscribe.toAllAccountBlocks(); - print('Subscribed successfully!'); - - broadcaster.listen((json) async { - if (json!["method"] == "ledger.subscription") { - for (var i = 0; i < json["params"]["result"].length; i += 1) { - var tx = json["params"]["result"][i]; - if (tx["toAddress"] != address.toString()) { - continue; - } - var hash = tx["hash"]; - print("receiving transaction with hash $hash"); - var template = await znnClient - .send(AccountBlockTemplate.receive(Hash.parse(hash))); - print( - "successfully received $hash. Receive-block-hash ${template.hash}"); - await Future.delayed(Duration(seconds: 1)); - } - } - }); - }); - - for (;;) { - await Future.delayed(Duration(seconds: 1)); - } - - case 'unreceived': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('unreceived'); - break; - } - var unreceived = await znnClient.ledger - .getUnreceivedBlocksByAddress(address!, pageIndex: 0, pageSize: 5); - - if (unreceived.count == 0) { - print('Nothing to receive'); - } else { - if (unreceived.more!) { - print( - 'You have ${red("more")} than ${green(unreceived.count.toString())} transaction(s) to receive'); - } else { - print( - 'You have ${green(unreceived.count.toString())} transaction(s) to receive'); - } - print('Showing the first ${unreceived.list!.length}'); - } - - for (var block in unreceived.list!) { - print( - 'Unreceived ${AmountUtils.addDecimals(block.amount, block.token!.decimals)} ${block.token!.symbol} from ${block.address.toString()}. Use the hash ${block.hash} to receive'); - } - break; - - case 'unconfirmed': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('unconfirmed'); - break; - } - var unconfirmed = await znnClient.ledger - .getUnconfirmedBlocksByAddress(address!, pageIndex: 0, pageSize: 5); - - if (unconfirmed.count == 0) { - print('No unconfirmed transactions'); - } else { - print( - 'You have ${green(unconfirmed.count.toString())} unconfirmed transaction(s)'); - print('Showing the first ${unconfirmed.list!.length}'); - } - - var encoder = JsonEncoder.withIndent(" "); - for (var block in unconfirmed.list!) { - print(encoder.convert(block.toJson())); - } - break; - - case 'balance': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('balance'); - break; - } - AccountInfo info = - await znnClient.ledger.getAccountInfoByAddress(address!); - print( - 'Balance for account-chain ${info.address!.toString()} having height ${info.blockCount}'); - if (info.balanceInfoList!.isEmpty) { - print(' No coins or tokens at address ${address.toString()}'); - } - for (BalanceInfoListItem entry in info.balanceInfoList!) { - print( - ' ${AmountUtils.addDecimals(entry.balance!, entry.token!.decimals)} ${entry.token!.symbol} ' - '${entry.token!.domain} ${entry.token!.tokenStandard.toString()}'); - } - break; - - case 'frontierMomentum': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('frontierMomentum'); - break; - } - Momentum currentFrontierMomentum = - await znnClient.ledger.getFrontierMomentum(); - print('Momentum height: ${currentFrontierMomentum.height.toString()}'); - print('Momentum hash: ${currentFrontierMomentum.hash.toString()}'); - print( - 'Momentum previousHash: ${currentFrontierMomentum.previousHash.toString()}'); - print( - 'Momentum timestamp: ${currentFrontierMomentum.timestamp.toString()}'); - break; - - case 'plasma.fuse': - if (args.length != 3) { - print('Incorrect number of arguments. Expected:'); - print('plasma.fuse toAddress amount (in ${blue('QSR')})'); - break; - } - Address beneficiary = Address.parse(args[1]); - BigInt amount = AmountUtils.extractDecimals( - num.parse(args[2] * oneQsr), coinDecimals); - if (amount < fuseMinQsrAmount) { - print( - '${red('Invalid amount')}: ${AmountUtils.addDecimals(amount, coinDecimals)} ${blue('QSR')}. Minimum amount for fusing is ${AmountUtils.addDecimals(fuseMinQsrAmount, coinDecimals)}'); - break; - } else if (amount % BigInt.from(oneQsr) != BigInt.zero) { - print('${red('Error!')} Amount has to be integer'); - break; - } - print( - 'Fusing ${AmountUtils.addDecimals(amount, coinDecimals)} ${blue('QSR')} to ${args[1]}'); - await znnClient.send(znnClient.embedded.plasma.fuse(beneficiary, amount)); - print('Done'); - break; - - case 'plasma.get': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('plasma.get'); - break; - } - PlasmaInfo plasmaInfo = await znnClient.embedded.plasma.get(address!); - print( - '${green(address.toString())} has ${plasmaInfo.currentPlasma} / ${plasmaInfo.maxPlasma}' - ' plasma with ${AmountUtils.addDecimals(plasmaInfo.qsrAmount, coinDecimals)} ${blue('QSR')} fused.'); - break; - - case 'plasma.list': - if (!(args.length == 1 || args.length == 3)) { - print('Incorrect number of arguments. Expected:'); - print('plasma.list [pageIndex pageSize]'); - break; - } - int pageIndex = 0; - int pageSize = 25; - if (args.length == 3) { - pageIndex = int.parse(args[1]); - pageSize = int.parse(args[2]); - } - FusionEntryList fusionEntryList = (await znnClient.embedded.plasma - .getEntriesByAddress(address!, - pageIndex: pageIndex, pageSize: pageSize)); - - if (fusionEntryList.count > 0) { - print( - 'Fusing ${AmountUtils.addDecimals(fusionEntryList.qsrAmount, coinDecimals)} ${blue('QSR')} for Plasma in ${fusionEntryList.count} entries'); - } else { - print('No Plasma fusion entries found'); - } - - for (FusionEntry entry in fusionEntryList.list) { - print( - ' ${AmountUtils.addDecimals(entry.qsrAmount, coinDecimals)} ${blue('QSR')} for ${entry.beneficiary.toString()}'); - print( - 'Can be canceled at momentum height: ${entry.expirationHeight}. Use id ${entry.id} to cancel'); - } - break; - - case 'plasma.cancel': - if (args.length != 2) { - print('Incorrect number of arguments. Expected:'); - print('plasma.cancel id'); - break; - } - Hash id = Hash.parse(args[1]); - - int pageIndex = 0; - bool found = false; - bool gotError = false; - - FusionEntryList fusions = - await znnClient.embedded.plasma.getEntriesByAddress(address!); - while (fusions.list.isNotEmpty) { - var index = fusions.list.indexWhere((entry) => entry.id == id); - if (index != -1) { - found = true; - if (fusions.list[index].expirationHeight > - (await znnClient.ledger.getFrontierMomentum()).height) { - print('${red('Error!')} Fuse entry can not be cancelled yet'); - gotError = true; - } - break; - } - pageIndex++; - fusions = await znnClient.embedded.plasma - .getEntriesByAddress(address, pageIndex: pageIndex); - } - - if (!found) { - print('${red('Error!')} Fuse entry was not found'); - break; - } - if (gotError) { - break; - } - print('Canceling Plasma fuse entry with id ${args[1]}'); - await znnClient.send(znnClient.embedded.plasma.cancel(id)); - print('Done'); - break; - - case 'sentinel.list': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('sentinel.list'); - break; - } - SentinelInfoList sentinels = - (await znnClient.embedded.sentinel.getAllActive()); - bool one = false; - for (SentinelInfo entry in sentinels.list) { - if (entry.owner.toString() == address!.toString()) { - if (entry.isRevocable) { - print( - 'Revocation window will close in ${formatDuration(entry.revokeCooldown)}'); - } else { - print( - 'Revocation window will open in ${formatDuration(entry.revokeCooldown)}'); - } - one = true; - } - } - if (!one) { - print('No Sentinel registered at address ${address!.toString()}'); - } - break; - - case 'sentinel.register': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('sentinel.register'); - break; - } - AccountInfo accountInfo = - await znnClient.ledger.getAccountInfoByAddress(address!); - var depositedQsr = - await znnClient.embedded.sentinel.getDepositedQsr(address); - print('You have $depositedQsr ${blue('QSR')} deposited for the Sentinel'); - if (accountInfo.znn()! < sentinelRegisterZnnAmount || - accountInfo.qsr()! < sentinelRegisterQsrAmount) { - print('Cannot register Sentinel with address ${address.toString()}'); - print( - 'Required ${AmountUtils.addDecimals(sentinelRegisterZnnAmount, coinDecimals)} ${green('ZNN')} and ${AmountUtils.addDecimals(sentinelRegisterQsrAmount, coinDecimals)} ${blue('QSR')}'); - print( - 'Available ${AmountUtils.addDecimals(accountInfo.znn()!, coinDecimals)} ${green('ZNN')} and ${AmountUtils.addDecimals(accountInfo.qsr()!, coinDecimals)} ${blue('QSR')}'); - break; - } - - if (depositedQsr < sentinelRegisterQsrAmount) { - await znnClient.send(znnClient.embedded.sentinel - .depositQsr(sentinelRegisterQsrAmount - depositedQsr)); - } - await znnClient.send(znnClient.embedded.sentinel.register()); - print('Done'); - print( - 'Check after 2 momentums if the Sentinel was successfully registered using ${green('sentinel.list')} command'); - break; - - case 'sentinel.revoke': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('sentinel.revoke'); - break; - } - SentinelInfo? entry = await znnClient.embedded.sentinel - .getByOwner(address!) - .catchError((e) { - print("Error: ${e.toString()}"); - if (e.toString().contains('data non existent')) { - return null; - } - }); - - if (entry == null) { - print('No Sentinel found for address ${address.toString()}'); - break; - } - - if (entry.isRevocable == false) { - print( - 'Cannot revoke Sentinel. Revocation window will open in ${formatDuration(entry.revokeCooldown)}'); - break; - } - - await znnClient.send(znnClient.embedded.sentinel.revoke()); - print('Done'); - print( - 'Use ${green('receiveAll')} to collect back the locked amount of ${green('ZNN')} and ${blue('QSR')}'); - break; - - case 'sentinel.collect': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('sentinel.collect'); - break; - } - await znnClient.send(znnClient.embedded.sentinel.collectReward()); - print('Done'); - print( - 'Use ${green('receiveAll')} to collect your Sentinel reward(s) after 1 momentum'); - break; - - case 'sentinel.withdrawQsr': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('sentinel.withdrawQsr'); - break; - } - - BigInt? depositedQsr = - await znnClient.embedded.sentinel.getDepositedQsr(address!); - if (depositedQsr == BigInt.zero) { - print('No deposited ${blue('QSR')} to withdraw'); - break; - } - print( - 'Withdrawing ${AmountUtils.addDecimals(depositedQsr, coinDecimals)} ${blue('QSR')} ...'); - await znnClient.send(znnClient.embedded.sentinel.withdrawQsr()); - print('Done'); - break; - - case 'stake.list': - if (!(args.length == 1 || args.length == 3)) { - print('Incorrect number of arguments. Expected:'); - print(' stake.list [pageIndex pageSize]'); - break; - } - int pageIndex = 0; - int pageSize = 25; - if (args.length == 3) { - pageIndex = int.parse(args[1]); - pageSize = int.parse(args[2]); - } - final currentTime = - (DateTime.now().millisecondsSinceEpoch / 1000).round(); - StakeList stakeList = await znnClient.embedded.stake.getEntriesByAddress( - address!, - pageIndex: pageIndex, - pageSize: pageSize); - - if (stakeList.count > 0) { - print( - 'Showing ${stakeList.list.length} out of a total of ${stakeList.count} staking entries'); - } else { - print('No staking entries found'); - } - - for (StakeEntry entry in stakeList.list) { - print( - 'Stake id ${entry.id.toString()} with amount ${AmountUtils.addDecimals(entry.amount, coinDecimals)} ${green('ZNN')}'); - if (entry.expirationTimestamp > currentTime) { - print( - ' Can be revoked in ${formatDuration(entry.expirationTimestamp - currentTime)}'); - } else { - print(' ${green('Can be revoked now')}'); - } - } - break; - - case 'stake.register': - if (args.length != 3) { - print('Incorrect number of arguments. Expected:'); - print('stake.register amount duration (in months)'); - break; - } - BigInt amount = AmountUtils.extractDecimals( - num.parse(args[1]) * oneZnn, coinDecimals); - final duration = int.parse(args[2]); - if (duration < 1 || duration > 12) { - print( - '${red('Invalid duration')}: ($duration) $stakeUnitDurationName. It must be between 1 and 12'); - break; - } - if (amount < stakeMinZnnAmount) { - print( - '${red('Invalid amount')}: ${AmountUtils.addDecimals(amount, coinDecimals)} ${green('ZNN')}. Minimum staking amount is ${AmountUtils.addDecimals(stakeMinZnnAmount, coinDecimals)}'); - break; - } - AccountInfo balance = - await znnClient.ledger.getAccountInfoByAddress(address!); - if (balance.znn()! < amount) { - print(red('Not enough ZNN to stake')); - break; - } - - print( - 'Staking ${AmountUtils.addDecimals(amount, coinDecimals)} ${green('ZNN')} for $duration $stakeUnitDurationName(s)'); - await znnClient.send( - znnClient.embedded.stake.stake(stakeTimeUnitSec * duration, amount)); - print('Done'); - break; - - case 'stake.revoke': - if (args.length != 2) { - print('Incorrect number of arguments. Expected:'); - print('stake.revoke id'); - break; - } - Hash hash = Hash.parse(args[1]); - - final currentTime = - (DateTime.now().millisecondsSinceEpoch / 1000).round(); - int pageIndex = 0; - bool one = false; - bool gotError = false; - - StakeList entries = await znnClient.embedded.stake - .getEntriesByAddress(address!, pageIndex: pageIndex); - while (entries.list.isNotEmpty) { - for (StakeEntry entry in entries.list) { - if (entry.id.toString() == hash.toString()) { - if (entry.expirationTimestamp > currentTime) { - print( - '${red('Cannot revoke!')} Try again in ${formatDuration(entry.expirationTimestamp - currentTime)}'); - gotError = true; - } - one = true; - } - } - pageIndex++; - entries = await znnClient.embedded.stake - .getEntriesByAddress(address, pageIndex: pageIndex); - } - - if (gotError) { - break; - } else if (!one) { - print( - '${red('Error!')} No stake entry found with id ${hash.toString()}'); - break; - } - - await znnClient.send(znnClient.embedded.stake.cancel(hash)); - print('Done'); - print( - 'Use ${green('receiveAll')} to collect your stake amount and uncollected reward(s) after 2 momentums'); - break; - - case 'stake.collect': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('stake.collect'); - break; - } - await znnClient.send(znnClient.embedded.stake.collectReward()); - print('Done'); - print( - 'Use ${green('receiveAll')} to collect your stake reward(s) after 1 momentum'); - break; - - case 'pillar.list': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('pillar.list'); - break; - } - PillarInfoList pillarList = (await znnClient.embedded.pillar.getAll()); - for (PillarInfo pillar in pillarList.list) { - print( - '#${pillar.rank + 1} Pillar ${green(pillar.name)} has a delegated weight of ${AmountUtils.addDecimals(pillar.weight, coinDecimals)} ${green('ZNN')}'); - print(' Producer address ${pillar.producerAddress}'); - print( - ' Momentums ${pillar.currentStats.producedMomentums} / expected ${pillar.currentStats.expectedMomentums}'); - } - break; - - case 'pillar.register': - if (args.length != 6) { - print('Incorrect number of arguments. Expected:'); - print( - 'pillar.register name producerAddress rewardAddress giveBlockRewardPercentage giveDelegateRewardPercentage'); - break; - } - - int giveBlockRewardPercentage = int.parse(args[4]); - int giveDelegateRewardPercentage = int.parse(args[5]); - - AccountInfo balance = - await znnClient.ledger.getAccountInfoByAddress(address!); - BigInt? qsrAmount = - (await znnClient.embedded.pillar.getQsrRegistrationCost()); - BigInt? depositedQsr = - await znnClient.embedded.pillar.getDepositedQsr(address); - if ((balance.znn()! < pillarRegisterZnnAmount || - balance.qsr()! < qsrAmount) && - qsrAmount > depositedQsr) { - print('Cannot register Pillar with address ${address.toString()}'); - print( - 'Required ${AmountUtils.addDecimals(pillarRegisterZnnAmount, coinDecimals)} ${green('ZNN')} and ${AmountUtils.addDecimals(qsrAmount, coinDecimals)} ${blue('QSR')}'); - print( - 'Available ${AmountUtils.addDecimals(balance.znn()!, coinDecimals)} ${green('ZNN')} and ${AmountUtils.addDecimals(balance.qsr()!, coinDecimals)} ${blue('QSR')}'); - break; - } - - print( - 'Creating a new ${green('Pillar')} will burn the deposited ${blue('QSR')} required for the Pillar slot'); - if (!confirm('Do you want to proceed?', defaultValue: false)) break; - - String newName = args[1]; - bool ok = - (await znnClient.embedded.pillar.checkNameAvailability(newName)); - while (!ok) { - newName = ask( - 'This Pillar name is already reserved. Please choose another name for the Pillar'); - ok = (await znnClient.embedded.pillar.checkNameAvailability(newName)); - } - if (depositedQsr < qsrAmount) { - print( - 'Depositing ${AmountUtils.addDecimals(qsrAmount - depositedQsr, coinDecimals)} ${blue('QSR')} for the Pillar registration'); - await znnClient.send( - znnClient.embedded.pillar.depositQsr(qsrAmount - depositedQsr)); - } - print('Registering Pillar ...'); - await znnClient.send(znnClient.embedded.pillar.register( - newName, - Address.parse(args[2]), - Address.parse(args[3]), - giveBlockRewardPercentage, - giveDelegateRewardPercentage)); - print('Done'); - print( - 'Check after 2 momentums if the Pillar was successfully registered using ${green('pillar.list')} command'); - break; - - case 'pillar.collect': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('pillar.collect'); - break; - } - await znnClient.send(znnClient.embedded.pillar.collectReward()); - print('Done'); - print( - 'Use ${green('receiveAll')} to collect your Pillar reward(s) after 1 momentum'); - break; - - case 'pillar.revoke': - if (args.length != 2) { - print('Incorrect number of arguments. Expected:'); - print('pillar.revoke name'); - break; - } - PillarInfoList pillarList = (await znnClient.embedded.pillar.getAll()); - bool ok = false; - for (PillarInfo pillar in pillarList.list) { - if (args[1].compareTo(pillar.name) == 0) { - ok = true; - if (pillar.isRevocable) { - print('Revoking Pillar ${pillar.name} ...'); - await znnClient.send(znnClient.embedded.pillar.revoke(args[1])); - print( - 'Use ${green('receiveAll')} to collect back the locked amount of ${green('ZNN')}'); - } else { - print( - 'Cannot revoke Pillar ${pillar.name}. Revocation window will open in ${formatDuration(pillar.revokeCooldown)}'); - } - } - } - if (ok) { - print('Done'); - } else { - print('There is no Pillar with this name'); - } - break; - - case 'pillar.delegate': - if (args.length != 2) { - print('Incorrect number of arguments. Expected:'); - print('pillar.delegate name'); - break; - } - print('Delegating to Pillar ${args[1]} ...'); - await znnClient.send(znnClient.embedded.pillar.delegate(args[1])); - print('Done'); - break; - - case 'pillar.undelegate': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('pillar.undelegate'); - break; - } - - print('Undelegating ...'); - await znnClient.send(znnClient.embedded.pillar.undelegate()); - print('Done'); - break; - - case 'pillar.withdrawQsr': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('pillar.withdrawQsr'); - break; - } - BigInt? depositedQsr = - await znnClient.embedded.pillar.getDepositedQsr(address!); - if (depositedQsr == BigInt.zero) { - print('No deposited ${blue('QSR')} to withdraw'); - break; - } - print( - 'Withdrawing ${AmountUtils.addDecimals(depositedQsr, coinDecimals)} ${blue('QSR')} ...'); - await znnClient.send(znnClient.embedded.pillar.withdrawQsr()); - print('Done'); - break; - - case 'token.list': - if (!(args.length == 1 || args.length == 3)) { - print('Incorrect number of arguments. Expected:'); - print('token.list [pageIndex pageSize]'); - break; - } - int pageIndex = 0; - int pageSize = 25; - if (args.length == 3) { - pageIndex = int.parse(args[1]); - pageSize = int.parse(args[2]); - } - TokenList tokenList = await znnClient.embedded.token - .getAll(pageIndex: pageIndex, pageSize: pageSize); - for (Token token in tokenList.list!) { - if (token.tokenStandard == znnZts || token.tokenStandard == qsrZts) { - print( - '${token.tokenStandard == znnZts ? green(token.name) : blue(token.name)} with symbol ${token.tokenStandard == znnZts ? green(token.symbol) : blue(token.symbol)} and standard ${token.tokenStandard == znnZts ? green(token.tokenStandard.toString()) : blue(token.tokenStandard.toString())}'); - print( - ' Created by ${token.tokenStandard == znnZts ? green(token.owner.toString()) : blue(token.owner.toString())}'); - print( - ' ${token.tokenStandard == znnZts ? green(token.name) : blue(token.name)} has ${token.decimals} decimals, ${token.isMintable ? 'is mintable' : 'is not mintable'}, ${token.isBurnable ? 'can be burned' : 'cannot be burned'}, and ${token.isUtility ? 'is a utility coin' : 'is not a utility coin'}'); - print( - ' The total supply is ${AmountUtils.addDecimals(token.totalSupply, token.decimals)} and the maximum supply is ${AmountUtils.addDecimals(token.maxSupply, token.decimals)}'); - } else { - print( - 'Token ${token.name} with symbol ${token.symbol} and standard ${magenta(token.tokenStandard.toString())}'); - print(' Issued by ${token.owner.toString()}'); - print( - ' ${token.name} has ${token.decimals} decimals, ${token.isMintable ? 'can be minted' : 'cannot be minted'}, ${token.isBurnable ? 'can be burned' : 'cannot be burned'}, and ${token.isUtility ? 'is a utility token' : 'is not a utility token'}'); - } - print(' Domain `${token.domain}`'); - } - break; - - case 'token.getByStandard': - if (args.length != 2) { - print('Incorrect number of arguments. Expected:'); - print('token.getByStandard tokenStandard'); - break; - } - TokenStandard tokenStandard = TokenStandard.parse(args[1]); - Token token = (await znnClient.embedded.token.getByZts(tokenStandard))!; - String type = 'Token'; - if (token.tokenStandard.toString() == qsrTokenStandard || - token.tokenStandard.toString() == znnTokenStandard) { - type = 'Coin'; - } - print( - '$type ${token.name} with symbol ${token.symbol} and standard ${token.tokenStandard.toString()}'); - print(' Created by ${green(token.owner.toString())}'); - print( - ' The total supply is ${AmountUtils.addDecimals(token.totalSupply, token.decimals)} and a maximum supply is ${AmountUtils.addDecimals(token.maxSupply, token.decimals)}'); - print( - ' The token has ${token.decimals} decimals ${token.isMintable ? 'can be minted' : 'cannot be minted'} and ${token.isBurnable ? 'can be burned' : 'cannot be burned'}'); - break; - - case 'token.getByOwner': - if (args.length != 2) { - print('Incorrect number of arguments. Expected:'); - print('token.getByOwner ownerAddress'); - break; - } - String type = 'Token'; - Address ownerAddress = Address.parse(args[1]); - TokenList tokens = - await znnClient.embedded.token.getByOwner(ownerAddress); - for (Token token in tokens.list!) { - type = 'Token'; - if (token.tokenStandard.toString() == znnTokenStandard || - token.tokenStandard.toString() == qsrTokenStandard) { - type = 'Coin'; - } - print( - '$type ${token.name} with symbol ${token.symbol} and standard ${token.tokenStandard.toString()}'); - print(' Created by ${green(token.owner.toString())}'); - print( - ' The total supply is ${AmountUtils.addDecimals(token.totalSupply, token.decimals)} and a maximum supply is ${AmountUtils.addDecimals(token.maxSupply, token.decimals)}'); - print( - ' The token ${token.decimals} decimals ${token.isMintable ? 'can be minted' : 'cannot be minted'} and ${token.isBurnable ? 'can be burned' : 'cannot be burned'}'); - } - break; - - case 'token.issue': - if (args.length != 10) { - print('Incorrect number of arguments. Expected:'); - print( - 'token.issue name symbol domain totalSupply maxSupply decimals isMintable isBurnable isUtility'); - break; - } - - RegExp regExpName = RegExp(r'^([a-zA-Z0-9]+[-._]?)*[a-zA-Z0-9]$'); - if (!regExpName.hasMatch(args[1])) { - print('${red("Error!")} The ZTS name contains invalid characters'); - break; - } - - RegExp regExpSymbol = RegExp(r'^[A-Z0-9]+$'); - if (!regExpSymbol.hasMatch(args[2])) { - print('${red("Error!")} The ZTS symbol must be all uppercase'); - break; - } - - RegExp regExpDomain = RegExp( - r'^([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9]\.)+[A-Za-z]{2,}$'); - if (args[3].isEmpty || !regExpDomain.hasMatch(args[3])) { - print('${red("Error!")} Invalid domain'); - print('Examples of ${green('valid')} domain names:'); - print(' zenon.network'); - print(' www.zenon.network'); - print(' quasar.zenon.network'); - print(' zenon.community'); - print('Examples of ${red('invalid')} domain names:'); - print(' zenon.network/index.html'); - print(' www.zenon.network/quasar'); - break; - } - - if (args[1].isEmpty || args[1].length > 40) { - print( - '${red("Error!")} Invalid ZTS name length (min 1, max 40, current ${args[1].length})'); - break; - } - - if (args[2].isEmpty || args[2].length > 10) { - print( - '${red("Error!")} Invalid ZTS symbol length (min 1, max 10, current ${args[2].length})'); - break; - } - - if (args[3].length > 128) { - print( - '${red("Error!")} Invalid ZTS domain length (min 0, max 128, current ${args[3].length})'); - break; - } - - bool mintable; - if (args[7] == '0' || args[7] == 'false') { - mintable = false; - } else if (args[7] == '1' || args[7] == 'true') { - mintable = true; - } else { - print( - '${red("Error!")} Mintable flag variable of type "bool" should be provided as either "true", "false", "1" or "0"'); - break; - } - - bool burnable; - if (args[8] == '0' || args[8] == 'false') { - burnable = false; - } else if (args[8] == '1' || args[8] == 'true') { - burnable = true; - } else { - print( - '${red("Error!")} Burnable flag variable of type "bool" should be provided as either "true", "false", "1" or "0"'); - break; - } - - bool utility; - if (args[9] == '0' || args[9] == 'false') { - utility = false; - } else if (args[9] == '1' || args[9] == 'true') { - utility = true; - } else { - print( - '${red("Error!")} Utility flag variable of type "bool" should be provided as either "true", "false", "1" or "0"'); - break; - } - - BigInt totalSupply = - AmountUtils.extractDecimals(num.parse(args[4]), coinDecimals); - BigInt maxSupply = - AmountUtils.extractDecimals(num.parse(args[5]), coinDecimals); - int decimals = int.parse(args[6]); - - if (mintable == true) { - if (maxSupply < totalSupply) { - print( - '${red("Error!")} Max supply must to be larger than the total supply'); - break; - } - if (maxSupply > kBigP255m1) { - print('${red("Error!")} Max supply must to be less than $kBigP255m1'); - break; - } - } else { - if (maxSupply != totalSupply) { - print( - '${red("Error!")} Max supply must be equal to totalSupply for non-mintable tokens'); - break; - } - if (totalSupply == BigInt.zero) { - print( - '${red("Error!")} Total supply cannot be "0" for non-mintable tokens'); - break; - } - } - - print('Issuing a new ${green('ZTS token')} will burn 1 ZNN'); - if (!confirm('Do you want to proceed?', defaultValue: false)) break; - - print('Issuing ${args[1]} ZTS token ...'); - await znnClient.send(znnClient.embedded.token.issueToken( - args[1], - args[2], - args[3], - totalSupply, - maxSupply, - decimals, - mintable, - burnable, - utility)); - print('Done'); - break; - - case 'token.mint': - if (args.length != 4) { - print('Incorrect number of arguments. Expected:'); - print('token.mint tokenStandard amount receiveAddress'); - break; - } - TokenStandard tokenStandard = TokenStandard.parse(args[1]); - BigInt amount = - AmountUtils.extractDecimals(num.parse(args[2]), coinDecimals); - Address mintAddress = Address.parse(args[3]); - - Token? token = await znnClient.embedded.token.getByZts(tokenStandard); - if (token == null) { - print('${red("Error!")} The token does not exist'); - break; - } else if (token.isMintable == false) { - print('${red("Error!")} The token is not mintable'); - break; - } - - print('Minting ZTS token ...'); - await znnClient.send(znnClient.embedded.token - .mintToken(tokenStandard, amount, mintAddress)); - print('Done'); - break; - - case 'token.burn': - if (args.length != 3) { - print('Incorrect number of arguments. Expected:'); - print('token.burn tokenStandard amount'); - break; - } - TokenStandard tokenStandard = TokenStandard.parse(args[1]); - BigInt amount = - AmountUtils.extractDecimals(num.parse(args[2]), coinDecimals); - AccountInfo info = - await znnClient.ledger.getAccountInfoByAddress(address!); - bool ok = true; - for (BalanceInfoListItem entry in info.balanceInfoList!) { - if (entry.token!.tokenStandard.toString() == tokenStandard.toString() && - entry.balance! < amount) { - print( - '${red("Error!")} You only have ${AmountUtils.addDecimals(entry.balance!, entry.token!.decimals)} ${entry.token!.symbol} tokens'); - ok = false; - break; - } - } - if (!ok) break; - print('Burning ${args[1]} ZTS token ...'); - await znnClient - .send(znnClient.embedded.token.burnToken(tokenStandard, amount)); - print('Done'); - break; - - case 'token.transferOwnership': - if (args.length != 3) { - print('Incorrect number of arguments. Expected:'); - print('token.transferOwnership tokenStandard newOwnerAddress'); - break; - } - print('Transferring ZTS token ownership ...'); - TokenStandard tokenStandard = TokenStandard.parse(args[1]); - Address newOwnerAddress = Address.parse(args[2]); - var token = (await znnClient.embedded.token.getByZts(tokenStandard))!; - if (token.owner.toString() != address!.toString()) { - print('${red('Error!')} Not owner of token ${args[1]}'); - break; - } - await znnClient.send(znnClient.embedded.token.updateToken( - tokenStandard, newOwnerAddress, token.isMintable, token.isBurnable)); - print('Done'); - break; - - case 'token.disableMint': - if (args.length != 2) { - print('Incorrect number of arguments. Expected:'); - print('token.disableMint tokenStandard'); - break; - } - print('Disabling ZTS token mintable flag ...'); - TokenStandard tokenStandard = TokenStandard.parse(args[1]); - var token = (await znnClient.embedded.token.getByZts(tokenStandard))!; - if (token.owner.toString() != address!.toString()) { - print('${red('Error!')} Not owner of token ${args[1]}'); - break; - } - await znnClient.send(znnClient.embedded.token - .updateToken(tokenStandard, token.owner, false, token.isBurnable)); - print('Done'); - break; - - case 'wallet.createNew': - if (!(args.length == 2 || args.length == 3)) { - print('Incorrect number of arguments. Expected:'); - print('wallet.createNew passphrase [keyStoreName]'); - break; - } - - String? name; - if (args.length == 3) name = args[2]; - - File keyStore = await znnClient.keyStoreManager.createNew(args[1], name); - print( - 'keyStore ${green('successfully')} created: ${path.basename(keyStore.path)}'); - break; - - case 'wallet.createFromMnemonic': - if (!(args.length == 3 || args.length == 4)) { - print('Incorrect number of arguments. Expected:'); - print( - 'wallet.createFromMnemonic "${green('mnemonic')}" passphrase [keyStoreName]'); - break; - } - if (!bip39.validateMnemonic(args[1])) { - throw AskValidatorException(red('Invalid mnemonic')); - } - - String? name; - if (args.length == 4) name = args[3]; - File keyStore = await znnClient.keyStoreManager - .createFromMnemonic(args[1], args[2], name); - print( - 'keyStore ${green('successfully')} created from mnemonic: ${path.basename(keyStore.path)}'); - break; - - case 'wallet.dumpMnemonic': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('wallet.dumpMnemonic'); - break; - } - - print('Mnemonic for keyStore ${znnClient.defaultKeyStorePath!}'); - print(znnClient.defaultKeyStore!.mnemonic); - break; - - case 'wallet.export': - if (args.length != 2) { - print('Incorrect number of arguments. Expected:'); - print('wallet.export filePath'); - break; - } - - await znnClient.defaultKeyStorePath!.copy(args[1]); - print('Done! Check the current directory'); - break; - - case 'wallet.list': - if (args.length != 1) { - print('Incorrect number of arguments. Expected:'); - print('wallet.list'); - break; - } - List stores = await znnClient.keyStoreManager.listAllKeyStores(); - if (stores.isNotEmpty) { - print('Available keyStores:'); - for (File store in stores) { - print(path.basename(store.path)); - } - } else { - print('No keyStores found'); - } - break; - - case 'wallet.deriveAddresses': - if (args.length != 3) { - print('Incorrect number of arguments. Expected:'); - print('wallet.deriveAddresses'); - break; - } - - print('Addresses for keyStore ${znnClient.defaultKeyStorePath!}'); - int left = int.parse(args[1]); - int right = int.parse(args[2]); - List addresses = - await znnClient.defaultKeyStore!.deriveAddressesByRange(left, right); - for (int i = 0; i < right - left; i += 1) { - print(' ${i + left}\t${addresses[i].toString()}'); - } - break; - - default: - print('${red('Error!')} Unrecognized command ${red(args[0])}'); - help(); - break; - } - return; -} diff --git a/docs/assets/screenshots/nanos-znn-app.png b/docs/assets/screenshots/nanos-znn-app.png new file mode 100644 index 0000000..3b05e57 Binary files /dev/null and b/docs/assets/screenshots/nanos-znn-app.png differ diff --git a/docs/assets/screenshots/nanos-znn-ready.png b/docs/assets/screenshots/nanos-znn-ready.png new file mode 100644 index 0000000..e03dda1 Binary files /dev/null and b/docs/assets/screenshots/nanos-znn-ready.png differ diff --git a/docs/ledger.md b/docs/ledger.md new file mode 100644 index 0000000..9e15c3e --- /dev/null +++ b/docs/ledger.md @@ -0,0 +1,47 @@ +# Connect Ledger Nano S to your Zenon Wallet + +Install the Zenon Ledger app on your Ledger Nano S to manage ZNN, QSR and ZTS tokens with the Zenon CLI. The Zenon Ledger app is developed and supported by the [{H}yperCore One](https://github.com/hypercore-one) team. + +### 1. Before you start + +- You've [initialized](https://support.ledgerwallet.com/hc/en-us/articles/360000613793) your Ledger Nano S. +- The latest firmware is [installed](https://support.ledgerwallet.com/hc/en-us/articles/360002731113). +- Ledger Live is [ready to use](https://support.ledgerwallet.com/hc/en-us/articles/360006395233). + +### 2. Install the Zenon app + +1. Open the Manager in Ledger Live. +2. Connect and unlock your Ledger Nano S. +3. If asked, allow the manager on your device by pressing the right button. +4. Find **Zenon** in the app catalog. +5. Press the **Install** button of the app. + - An installation window appears. + - Your device will display **Processing** + - The app installation is confirmed. + +![nanos-znn-app](/docs/assets/screenshots/nanos-znn-app.png) + +### **3. Connect device to your Zenon wallet** + +- Open the Zenon application on your Ledger device, the screen will display "Zenon is ready". + +![nanos-znn-app](/docs/assets/screenshots/nanos-znn-ready.png) + +- The Zenon CLI is available Linux/MacOs/Windows as a downloadable binary from the [releases page](../../../../releases). +- Install the Zenon CLI by extracting the archive to a location on your desktop device. +- Open a command prompt and change directory to the location of the Zenon CLI. For example: `cd ~/znn-cli`. + +### **4. Use the Zenon wallet** + +- Your address can be displayed with the following command: `znn-cli wallet.deriveAddresses 0 1 -k "Nano S"`. You can use it to receive ZTS tokens. +- To receive, execute the following command: `znn-cli receiveAll -k "Nano S" -u wss://my.hc1node.com:35998`. +- To send 10 ZNN to z1qqjnwjjpnue8xmmpanz6csze6tcmtzzdtfsww7, execute the following command: `znn-cli send z1qqjnwjjpnue8xmmpanz6csze6tcmtzzdtfsww7 10 ZNN -k "Nano S" -u wss://my.hc1node.com:35998`. +- Verify and confirm all transaction details on the ledger device. +- Press both buttons to sign the transaction. + +### **5. Contact info** + +- Support: Go to the #dev-community channel in our Discord: https://discord.gg/aEW2UZvs +- Name: [{H}yperCore One](https://github.com/hypercore-one) +- Legal Entity: NOM Labz LLC +- URL: [Zenon Network]http://zenon.network \ No newline at end of file diff --git a/docs/setup-devnet-linux-x64.md b/docs/setup-devnet-linux-x64.md new file mode 100644 index 0000000..b72676b --- /dev/null +++ b/docs/setup-devnet-linux-x64.md @@ -0,0 +1,153 @@ +# Setup Zenon devnet node on Ubuntu 22.04+ + +The instructions below are for setting up a Zenon **devnet** node on Ubuntu 22.04+. + +## Required software + +### Git + +We will need git to interact wth the GitHub repositories. Execute the following command in a Terminal. + +``` bash +sudo apt install git +``` + +### Golang + +We will need Golang to compile the go-zenon code. Execute the following command in a Terminal. + +``` bash +sudo add-apt-repository ppa:longsleep/golang-backports +sudo apt update +sudo apt install golang-go +``` + +### Make + +We will need Make to execute makefiles. Execute the following command in a Terminal. + +``` bash +sudo apt install make +``` + +### GCC compiler + +We need a gcc compiler to compile the go-zenon code. Check to make sure gcc is installed. + +``` bash +gcc --version +``` + +You should see something like the following in Ubuntu 22.04 + +``` bash +gcc (Ubuntu 11.4.0-1ubuntu1-22.04) 11.4.0 +Copyright (C) 2021 Free Software Foundation, Inc. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +``` + +If gcc is not installed run the following command + +``` bash +sudo apt install gcc +``` + +## Configuration + +After installing all the above tools make sure you close and open a new Bash Shell. + +To use **git** we need to configure an user. Use you GitHub account if you have one; otherwise you can use anything you like. + +``` bash +git config --global user.email [your e-mail] +git config --global user.name [your name] +``` + +## Compilation + +We will make a **repos** directory under the current userprofile to store all our work. Replace the path if you want it stored on a different location. + +``` bash +cd ~/ +mkdir repos +cd repos +``` + +### Zenon node + +Create a clone of the **master** branch of the [zenon-network/go-zenon repository](https://github.com/zenon-network/go-zenon.git). + +``` bash +git clone https://github.com/zenon-network/go-zenon.git +``` + +Change directory to the **go-zenon** directory. + +``` bash +cd go-zenon +``` + +Compile the **go-zenon** code. + +``` bash +make znnd +``` + +Change directory to the parent directory. + +``` bash +cd .. +``` + +### NoM community controller + +Create a clone of the **master** branch of the [hypercore-one/nomctl repository](https://github.com/hypercore-one/nomctl.git). + +``` bash +git clone https://github.com/hypercore-one/nomctl.git +``` + +Change directory to the **nomctl** directory. + +``` bash +cd nomctl +``` + +Compile the **nomctl** code. + +``` bash +make +``` + +Change directory to the parent directory. + +``` bash +cd .. +``` + +## Running + +Generate **devnet** configuration. + +``` bash +./nomctl/build/nomctl generate-devnet --data ./devnet --genesis-block=z1qqjnwjjpnue8xmmpanz6csze6tcmtzzdtfsww7/40000/400000 +``` + +> Replace the genesis-block address if you want to use another address for you devnet. + +Run the node with **devnet** configuration. + +``` bash +./go-zenon/build/znnd --data ./devnet +``` + +## Explorer + +While keeping the shell with the node running it is now possible to connect the **Zenon Explorer** to the node. + +Open a web browser and go to https://explorer.zenon.network and connect the **Zenon Explorer** to http://127.0.0.1:35997 + +Search for the address **z1qqjnwjjpnue8xmmpanz6csze6tcmtzzdtfsww7** + +> Try the Firefox or Brave browser if the Zenon Explorer does not want to connect. Google Chrome can throw a mixed content error when connecting to an insecure destination. \ No newline at end of file diff --git a/docs/setup-devnet-win10-x64.md b/docs/setup-devnet-win10-x64.md new file mode 100644 index 0000000..33a4f0d --- /dev/null +++ b/docs/setup-devnet-win10-x64.md @@ -0,0 +1,148 @@ +# Setup Zenon devnet node on Windows 10+ + +The instructions below are for setting up a Zenon **devnet** node on Windows 10+. + +## Required software + +### Chocolatey + +We will use **Chocolatey** for installing the necessary dependencies. **Chocolatey** is a Packet Manager for Windows. Check out their website at https://chocolatey.org/ for more information. + +Make sure **Chocolatey** is installed on your system by following the instructions at https://chocolatey.org/install#individual + +After installing **Chocolatey**, ensure that you are using an [PowerShell administrative shell](https://www.howtogeek.com/742916/how-to-open-windows-powershell-as-an-admin-in-windows-10/). + +### Git + +We will need git to interact wth the GitHub repositories. Execute the following command in PowerShell. + +``` powershell +choco install git -y +``` + +### Golang + +We will need Golang to to compile the go-zenon code. Execute the following command in PowerShell. + +``` powershell +choco install go -y +``` + +### GCC compiler + +We will need a GCC compiler to compile the go-zenon code. Execute the following command in PowerShell. + +``` powershell +choco install winlibs-llvm-free +``` + +## Configuration + +After installing all the above tools make sure you close and open a new [PowerShell administrative shell](https://www.howtogeek.com/742916/how-to-open-windows-powershell-as-an-admin-in-windows-10/). + +To use **git** we need to configure an user. Use you GitHub account if you have one; otherwise you can use anything you like. + +``` powershell +git config --global user.email [your e-mail] +git config --global user.name [your name] +``` + +## Compilation + +We will make a **repos** directory under the current userprofile to store all our work. Replace the path if you want it stored on a different location. + +``` powershell +cd $ENV:USERPROFILE +mkdir repos +cd repos +``` + +### Zenon node + +Create a clone of the **devnet** branch of the [zenon-network/go-zenon repository](https://github.com/zenon-network/go-zenon.git). + +``` powershell +git clone https://github.com/zenon-network/go-zenon.git +``` + +Change directory to the **go-zenon** directory. + +``` powershell +cd go-zenon +``` + +Compile the **go-zenon** code. + +``` powershell +go build -o build/libznn.dll -buildmode=c-shared -tags libznn cmd/libznn/main_libznn.go +go build -o build/znnd.exe cmd/znnd/main.go +``` + +Change directory to the parent directory. + +``` powershell +cd .. +``` + +### NoM community controller + +Create a clone of the **master** branch of the [hypercore-one/nomctl repository](https://github.com/hypercore-one/nomctl.git). + +``` powershell +git clone https://github.com/hypercore-one/nomctl.git +``` + +Change directory to the **nomctl** directory. + +``` powershell +cd nomctl +``` + +Compile the **nomctl** code. + +``` powershell +go build -o build/nomctl.exe +``` + +Change directory to the parent directory. + +``` powershell +cd .. +``` + +## Running + +Generate **devnet** configuration. + +``` powershell +./nomctl/build/nomctl generate-devnet --data ./devnet --genesis-block=z1qqjnwjjpnue8xmmpanz6csze6tcmtzzdtfsww7/40000/400000 +``` + +> Replace the genesis-block address if you want to use another address for you devnet. + +Run the node with **devnet** configuration. + +``` powershell +./go-zenon/build/znnd --data ./devnet +``` + +## Explorer + +While keeping the shell with the node running it is now possible to connect the **Zenon Explorer** to the node. + +Open a web browser and go to https://explorer.zenon.network and connect the **Zenon Explorer** to http://127.0.0.1:35997 + +Search for the address **z1qqjnwjjpnue8xmmpanz6csze6tcmtzzdtfsww7** + +> Try the Firefox or Brave browser if the Zenon Explorer does not want to connect. Google Chrome can throw a mixed content error when connecting to an insecure destination. + +## Clean up + +Execute the following commands in order to undo all the installation files of this tutorial. + +``` powershell +rm $ENV:USERPROFILE/repos -r -force +choco uninstall mingw -y +choco uninstall go -y +choco uninstall git -y +``` \ No newline at end of file diff --git a/init_znn.dart b/init_znn.dart deleted file mode 100644 index 864c42a..0000000 --- a/init_znn.dart +++ /dev/null @@ -1,364 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:args/args.dart'; -import 'package:dcli/dcli.dart'; -import 'package:path/path.dart' as path; -import 'package:znn_sdk_dart/znn_sdk_dart.dart'; - -const znnDaemon = 'znnd'; -const znnCli = 'znn-cli'; -const znnCliVersion = '0.0.4'; - -String _argsUsage = ''; - -final znndConfigPath = File(path.join(znnDefaultDirectory.path, 'config.json')); - -void help() { - print('USAGE:'); - print(' $znnCli [OPTIONS] [FLAGS]\n'); - print('FLAGS:'); - print('$_argsUsage\n'); - print('OPTIONS:'); - print(' General'); - print( - ' send toAddress amount [${green('ZNN')}/${blue('QSR')}/${magenta('ZTS')}]'); - print(' receive blockHash'); - print(' receiveAll'); - print(' unreceived'); - print(' unconfirmed'); - print(' balance'); - print(' frontierMomentum'); - print(' version'); - print(' Plasma'); - print(' plasma.list [pageIndex pageCount]'); - print(' plasma.get'); - print(' plasma.fuse toAddress amount (in ${blue('QSR')})'); - print(' plasma.cancel id'); - print(' Sentinel'); - print(' sentinel.list'); - print(' sentinel.register'); - print(' sentinel.revoke'); - print(' sentinel.collect'); - print(' sentinel.withdrawQsr'); - print(' Staking'); - print(' stake.list [pageIndex pageCount]'); - print(' stake.register amount duration (in months)'); - print(' stake.revoke id'); - print(' stake.collect'); - print(' Pillar'); - print(' pillar.list'); - print( - ' pillar.register name producerAddress rewardAddress giveBlockRewardPercentage giveDelegateRewardPercentage'); - print(' pillar.revoke name'); - print(' pillar.delegate name'); - print(' pillar.undelegate'); - print(' pillar.collect'); - print(' pillar.withdrawQsr'); - print(' ZTS Tokens'); - print(' token.list [pageIndex pageCount]'); - print(' token.getByStandard tokenStandard'); - print(' token.getByOwner ownerAddress'); - print( - ' token.issue name symbol domain totalSupply maxSupply decimals isMintable isBurnable isUtility'); - print(' token.mint tokenStandard amount receiveAddress'); - print(' token.burn tokenStandard amount'); - print(' token.transferOwnership tokenStandard newOwnerAddress'); - print(' token.disableMint tokenStandard'); - print(' Wallet'); - print(' wallet.list'); - print(' wallet.createNew passphrase [keyStoreName]'); - print( - ' wallet.createFromMnemonic "${green('mnemonic')}" passphrase [keyStoreName]'); - print(' wallet.dumpMnemonic'); - print(' wallet.deriveAddresses start end'); - print(' wallet.export filePath'); -} - -Future initZnn(List args, Function handler) async { - final ArgParser argParser = ArgParser(); - final Zenon znnClient = Zenon(); - - // Options - argParser.addOption('url', - abbr: 'u', - defaultsTo: 'ws://127.0.0.1:$defaultWsPort', - help: 'Provide a websocket $znnDaemon connection URL with a port'); - argParser.addOption('passphrase', - abbr: 'p', - help: - 'use this passphrase for the keyStore or enter it manually in a secure way'); - argParser.addOption('keyStore', - abbr: 'k', - defaultsTo: 'available keyStore if only one is present', - help: 'Select the local keyStore'); - argParser.addOption('index', - abbr: 'i', defaultsTo: '0', help: 'Address index'); - argParser.addOption('chainid', - abbr: 'c', defaultsTo: '1', help: 'Provide a chain identifier'); - - // Flags - argParser.addFlag('verbose', - abbr: 'v', - negatable: false, - help: 'Prints detailed information about the action that it performs'); - argParser.addFlag('help', - abbr: 'h', negatable: false, help: 'Displays help information'); - - final argResult = argParser.parse(args); - args = argResult.rest; - - _argsUsage = argParser.usage; - - if (argResult['help'] || - args.isEmpty || - (args.isNotEmpty && (args[0] == 'help'))) { - help(); - return 0; - } - - if (argResult.wasParsed('chainid')) { - setChainIdentifier(chainIdentifier: int.parse(argResult['chainid'])); - } - - if (argResult.wasParsed('verbose')) { - logger.level = Level.ALL; - } - - ensureDirectoriesExist(); - - List commandsWithKeyStore = [ - 'send', - 'receive', - 'receiveAll', - 'autoreceive', - 'unreceived', - 'unconfirmed', - 'balance', - 'plasma.list', - 'plasma.fuse', - 'plasma.get', - 'plasma.cancel', - 'sentinel.list', - 'sentinel.register', - 'sentinel.revoke', - 'sentinel.collect', - 'sentinel.withdrawQsr', - 'stake.list', - 'stake.register', - 'stake.revoke', - 'stake.collect', - 'pillar.register', - 'pillar.revoke', - 'pillar.delegate', - 'pillar.undelegate', - 'pillar.collect', - 'pillar.withdrawQsr', - 'token.issue', - 'token.mint', - 'token.burn', - 'token.transferOwnership', - 'token.disableMint', - 'wallet.dumpMnemonic', - 'wallet.deriveAddresses', - 'wallet.export', - ]; - - List commandsWithoutKeyStore = [ - 'frontierMomentum', - 'version', - 'pillar.list', - 'token.list', - 'token.getByStandard', - 'token.getByOwner', - 'wallet.createNew', - 'wallet.createFromMnemonic', - 'wallet.list', - ]; - - List commandsWithoutConnection = [ - 'version', - 'wallet.createNew', - 'wallet.createFromMnemonic', - 'wallet.list', - 'wallet.dumpMnemonic', - 'wallet.deriveAddresses', - ]; - - if (!(commandsWithoutKeyStore.contains(args[0]) || - commandsWithKeyStore.contains(args[0]))) { - print('${red('Error!')} Unrecognized command ${red(args[0])}'); - exit(-1); - } - - if (!commandsWithoutKeyStore.contains(args[0])) { - // Get keyStoreFile - File keyStoreFile; - List allKeyStores = - await znnClient.keyStoreManager.listAllKeyStores(); - - if (allKeyStores.isEmpty) { - // Make sure at least one keyStore exists - print('${red('Error!')} No keyStore in the default directory'); - return 0; - } else if (argResult.wasParsed('keyStore')) { - // Use user provided keyStore: make sure it exists - keyStoreFile = File( - path.join(znnDefaultWalletDirectory.path, argResult['keyStore'])); - if (!keyStoreFile.existsSync()) { - print( - '${red('Error!')} The keyStore ${argResult['keyStore']} does not exist in the default directory'); - return 0; - } - } else if (allKeyStores.length == 1) { - // In case there is just one keyStore, use it by default - print( - 'Using the default keyStore ${green(path.basename(allKeyStores[0].path))}'); - keyStoreFile = allKeyStores[0]; - } else { - // Multiple keyStores present, but none is selected: action required - print( - '${red('Error!')} Please provide a keyStore or an address. Use ${green('wallet.list')} to list all available keyStores'); - return 0; - } - - String? passphrase; - if (argResult.wasParsed('passphrase')) { - passphrase = argResult['passphrase']; - } else { - print('Insert passphrase:'); - stdin.echoMode = false; - passphrase = stdin.readLineSync(); - stdin.echoMode = true; - } - - int index = 0; - if (argResult.wasParsed('index')) { - index = int.parse(argResult['index']); - } - try { - znnClient.defaultKeyStore = await znnClient.keyStoreManager - .readKeyStore(passphrase!, keyStoreFile); - znnClient.defaultKeyStorePath = keyStoreFile; - } on Exception catch (e) { - if (e.runtimeType == IncorrectPasswordException) { - print('${red('Error!')} Invalid passphrase for keyStore $keyStoreFile'); - } else { - rethrow; - } - } - - znnClient.defaultKeyPair = znnClient.defaultKeyStore!.getKeyPair(index); - var address = await znnClient.defaultKeyPair!.address; - logger.info('Using address ${green(address.toString())}'); - } - - String? urlOption; - bool urlOptionSupplied = false; - - if (argResult.wasParsed('url') && validateWsConnectionURL(argResult['url'])) { - urlOption = argResult['url']; - urlOptionSupplied = true; - } else if (!validateWsConnectionURL(argResult['url'])) { - print('${red('Error!')} Malformed URL. Please try again'); - exit(-1); - } else { - urlOption = 'ws://127.0.0.1:$defaultWsPort'; - } - - if (!commandsWithoutConnection.contains(args[0]) || - urlOptionSupplied == true) { - if (!urlOptionSupplied && !isZnndRunning(znnDaemon)) { - print('$znnDaemon is not running. Please try again'); - exit(-1); - } - - await znnClient.wsClient.initialize(urlOption!, retry: false); - } - - await handler(args); - - znnClient.wsClient.stop(); - - return 0; -} - -String formatJSON(Map j) { - var spaces = ' ' * 4; - var encoder = JsonEncoder.withIndent(spaces); - return encoder.convert(j); -} - -String formatDuration(int seconds) { - format(Duration d) => d.toString().split('.').first.padLeft(8, '0'); - final duration = Duration(seconds: seconds); - return format(duration); -} - -Map parseJSON(String fileName) { - File file = File(fileName); - if (file.existsSync()) { - return json.decode(file.readAsStringSync()); - } else { - return {}; - } -} - -Map parseConfig() { - if (znndConfigPath.existsSync()) { - return json.decode(znndConfigPath.readAsStringSync()); - } else { - return {}; - } -} - -bool writeConfig(Map config) { - try { - znndConfigPath.writeAsStringSync(formatJSON(config)); - } on FileSystemException { - return false; - } - return true; -} - -bool isZnndRunning(String executableName) { - switch (Platform.operatingSystem) { - case 'linux': - return Process.runSync('pgrep', [executableName], runInShell: true) - .stdout - .toString() - .isNotEmpty; - case 'windows': - return Process.runSync('tasklist', [], runInShell: true) - .stdout - .toString() - .contains(znnDaemon); - case 'macos': - return Process.runSync('pgrep', [executableName], runInShell: true) - .stdout - .toString() - .isNotEmpty; - default: - return false; - } -} - -String getZnndVersion() { - switch (Platform.operatingSystem) { - case 'linux': - return Process.runSync('./$znnDaemon', ['-v'], runInShell: true) - .stdout - .toString(); - case 'windows': - return Process.runSync('$znnDaemon.exe', ['-v'], runInShell: true) - .stdout - .toString(); - case 'macos': - return Process.runSync('./$znnDaemon', ['-v'], runInShell: true) - .stdout - .toString(); - default: - return 'Unknown znnd version'; - } -} diff --git a/lib/accelerator.dart b/lib/accelerator.dart new file mode 100644 index 0000000..fc7f7a8 --- /dev/null +++ b/lib/accelerator.dart @@ -0,0 +1,55 @@ +import 'package:dcli/dcli.dart' hide verbose; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +void acceleratorMenu() { + print(' ${white('Accelerator-Z')}'); + print(' az.donate amount ${green('ZNN')}/${blue('QSR')}'); +} + +Future acceleratorFunctions() async { + switch (args[0].split('.')[1]) { + case 'donate': + verbose + ? print('Description: Donate ZNN or QSR as fuel for the Mothership') + : null; + await _donate(); + return; + + default: + invalidCommand(); + } +} + +Future _donate() async { + if (args.length != 3) { + print('Incorrect number of arguments. Expected:'); + print('az.donate amount tokenStandard'); + return; + } + + TokenStandard tokenStandard = getTokenStandard(args[2]); + if (tokenStandard != znnZts && tokenStandard != qsrZts) { + print( + '${red('Error!')} You can only send ${green('ZNN')} or ${blue('QSR')}.'); + return; + } + + Token token = await getToken(tokenStandard); + BigInt amount = AmountUtils.extractDecimals(args[1], token.decimals); + if (amount <= BigInt.zero) { + print('${red('Error!')} You cannot send that amount.'); + return; + } + + if (!await hasBalance(address, tokenStandard, amount)) { + return; + } + + print( + 'Donating ${AmountUtils.addDecimals(amount, token.decimals)} ${token.symbol} to Accelerator-Z ...'); + await znnClient + .send(znnClient.embedded.accelerator.donate(amount, tokenStandard)); + + print('Done'); +} diff --git a/lib/bridge.dart b/lib/bridge.dart new file mode 100644 index 0000000..bf4f9f6 --- /dev/null +++ b/lib/bridge.dart @@ -0,0 +1,1275 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; +import 'package:dcli/dcli.dart' hide verbose; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +void bridgeMenu() { + print(' ${white('Bridge')}'); + print(' bridge.info'); + print(' bridge.security'); + print(' bridge.timeChallenges'); + print(' bridge.orchestratorInfo'); + print(' bridge.fees [tokenStandard]'); + print(' bridge.network.list'); + print(' bridge.network.get networkClass chainId'); + print( + ' bridge.wrap.token networkClass chainId toAddress amount tokenStandard'); + print(' bridge.wrap.list'); + print(' bridge.wrap.listByAddress address [networkClass chainId]'); + print(' bridge.wrap.listUnsigned'); + print(' bridge.wrap.get id'); + print(' bridge.unwrap.redeem transactionHash logIndex'); + print(' bridge.unwrap.redeemAll [bool]'); + print(' bridge.unwrap.list'); + print(' bridge.unwrap.listByAddress toAddress'); + print(' bridge.unwrap.listUnredeemed [toAddress]'); + print(' bridge.unwrap.get transactionHash logIndex'); + print(' bridge.guardian.proposeAdmin address'); +} + +void bridgeAdminMenu() { + print(' ${white('Bridge Admin')}'); + print(' bridge.admin.emergency'); + print(' bridge.admin.halt'); + print(' bridge.admin.unhalt'); + print(' bridge.admin.enableKeyGen'); + print(' bridge.admin.disableKeyGen'); + print( + ' bridge.admin.setTokenPair networkClass chainId tokenStandard tokenAddress bridgeable redeemable owned minAmount feePercentage redeemDelay metadata'); + print( + ' bridge.admin.removeTokenPair networkClass chainId tokenStandard tokenAddress'); + print(' bridge.admin.revokeUnwrapRequest transactionHash logIndex'); + print(' bridge.admin.nominateGuardians address1 address2 ... addressN'); + print(' bridge.admin.changeAdmin address'); + print(' bridge.admin.setMetadata metadata'); + print( + ' bridge.admin.setOrchestratorInfo windowSize keyGenThreshold confirmationsToFinality estimatedMomentumTime'); + print( + ' bridge.admin.setNetwork networkClass chainId name contractAddress metadata'); + print(' bridge.admin.removeNetwork networkClass chainId'); + print(' bridge.admin.setNetworkMetadata networkClass chainId metadata'); +} + +Future bridgeFunctions() async { + switch (args[0].split('.')[1]) { + case 'info': + verbose ? print('Description: Get the bridge information') : null; + await _info(); + return; + + case 'security': + verbose + ? print('Description: Get the bridge security information') + : null; + await _security(); + return; + + case 'timeChallenges': + verbose ? print('Description: List all bridge time challenges') : null; + await _timeChallenges(); + return; + + case 'orchestratorInfo': + verbose ? print('Description: Get the orchestrator information') : null; + await _orchestratorInfo(); + return; + + case 'fees': + verbose + ? print( + 'Description: Display the accumulated wrapping fees for a ZTS') + : null; + await _fees(); + return; + + case 'network': + await _networkFunctions(); + return; + + case 'wrap': + await _wrapFunctions(); + return; + + case 'unwrap': + await _unwrapFunctions(); + return; + + case 'guardian': + await _guardianFunctions(); + return; + + case 'admin': + await _adminFunctions(); + return; + + default: + invalidCommand(); + } +} + +Future _info() async { + BridgeInfo info = await znnClient.embedded.bridge.getBridgeInfo(); + var metadata = jsonDecode(info.metadata); + print('Bridge info:'); + print(' Administrator: ${info.administrator}'); + print(' Compressed TSS ECDSA public key: ${info.compressedTssECDSAPubKey}'); + print( + ' Decompressed TSS ECDSA public key: ${info.decompressedTssECDSAPubKey}'); + print(' Allow key generation: ${info.allowKeyGen}'); + print(' Is halted: ${info.halted}'); + print(' Unhalted at: ${info.unhaltedAt}'); + print(' Unhalt duration in momentums: ${info.unhaltDurationInMomentums}'); + print(' TSS nonce: ${info.tssNonce}'); + print(' Metadata:'); + print(' Party Timeout: ${metadata['partyTimeout']}'); + print(' KeyGen Timeout: ${metadata['keyGenTimeout']}'); + print(' KeySign Timeout: ${metadata['keySignTimeout']}'); + print(' PreParam Timeout: ${metadata['preParamTimeout']}'); + print(' KeyGen Version: ${metadata['keyGenVersion']}'); + print(' Leader Block Height: ${metadata['leaderBlockHeight']}'); + print(' Affiliate Program: ${metadata['affiliateProgram']}'); +} + +Future _security() async { + SecurityInfo info = await znnClient.embedded.bridge.getSecurityInfo(); + print('Security info:'); + + if (info.guardians.isEmpty) { + print(' Guardians: none'); + } else { + print(' Guardians: '); + for (Address guardian in info.guardians) { + print(' $guardian'); + } + } + + if (info.guardiansVotes.isEmpty) { + print(' Guardian votes: none'); + } else { + print(' Guardian votes: '); + for (Address guardianVotes in info.guardiansVotes) { + print(' $guardianVotes'); + } + } + + print(' Administrator delay: ${info.administratorDelay}'); + print(' Soft delay: ${info.softDelay}'); +} + +Future _timeChallenges() async { + TimeChallengesList list = + await znnClient.embedded.bridge.getTimeChallengesInfo(); + + if (list.count == 0) { + print('No time challenges found.'); + return; + } + + print('Time challenges:'); + for (var info in list.list) { + print(' Method: ${info.methodName}'); + print(' Start height: ${info.challengeStartHeight}'); + print(' Params hash: ${info.paramsHash}'); + print(''); + } +} + +Future _orchestratorInfo() async { + OrchestratorInfo info = await znnClient.embedded.bridge.getOrchestratorInfo(); + print('Orchestrator info:'); + print(' Window size: ${info.windowSize}'); + print(' Key generation threshold: ${info.keyGenThreshold}'); + print(' Confirmations to finality: ${info.confirmationsToFinality}'); + print(' Estimated momentum time: ${info.estimatedMomentumTime}'); + print(' Allow key generation height: ${info.allowKeyGenHeight}'); +} + +Future _fees() async { + if (args.length > 2) { + print('Incorrect number of arguments. Expected:'); + print('bridge.getFees [tokenStandard]'); + return; + } + if (args.length == 2) { + TokenStandard tokenStandard = getTokenStandard(args[1]); + Token token = await getToken(tokenStandard); + ZtsFeesInfo info = + await znnClient.embedded.bridge.getFeeTokenPair(tokenStandard); + Function color = getColor(tokenStandard); + + print( + 'Fees accumulated for ${color(token.symbol)}: ${AmountUtils.addDecimals(info.accumulatedFee, token.decimals)}'); + } else { + ZtsFeesInfo znnInfo = + await znnClient.embedded.bridge.getFeeTokenPair(znnZts); + ZtsFeesInfo qsrInfo = + await znnClient.embedded.bridge.getFeeTokenPair(qsrZts); + print( + 'Fees accumulated for ${green('ZNN')}: ${AmountUtils.addDecimals(znnInfo.accumulatedFee, coinDecimals)}'); + print( + 'Fees accumulated for ${blue('QSR')}: ${AmountUtils.addDecimals(qsrInfo.accumulatedFee, coinDecimals)}'); + } +} + +Future _networkFunctions() async { + switch (args[0].split('.')[2]) { + case 'list': + verbose ? print('Description: List all available bridge networks') : null; + await _networkList(); + return; + + case 'get': + verbose + ? print( + 'Description: Get the information for a network class and chain id') + : null; + await _networkGet(); + return; + + default: + invalidCommand(); + } +} + +Future _networkList() async { + BridgeNetworkInfoList networkList = + await znnClient.embedded.bridge.getAllNetworks(); + if (networkList.count == 0) { + print('No bridge networks found'); + return; + } + + for (var network in networkList.list) { + print(' Name: ${network.name}'); + print(' Network Class: ${network.networkClass}'); + print(' Chain Id: ${network.chainId}'); + print(' Contract Address: ${network.contractAddress}'); + print(' Metadata: ${network.metadata}'); + if (network.tokenPairs.isNotEmpty) { + print(' Token Pairs:'); + for (var tokenPair in network.tokenPairs) { + print(' ${tokenPair.toJson()}'); + } + } + print(''); + } +} + +Future _networkGet() async { + if (args.length != 3) { + print('Incorrect number of arguments. Expected:'); + print('bridge.network.get networkClass chainId'); + return; + } + + int networkClass = int.parse(args[1]); + int chainId = int.parse(args[2]); + + if (networkClass == 0 || chainId == 0) { + print('The bridge network does not exist'); + return; + } + + BridgeNetworkInfo info = + await znnClient.embedded.bridge.getNetworkInfo(networkClass, chainId); + + if (info.networkClass == 0 || info.chainId == 0) { + print('The bridge network does not exist'); + return; + } + + print(' Name: ${info.name}'); + print(' Network Class: ${info.networkClass}'); + print(' Chain Id: ${info.chainId}'); + print(' Contract Address: ${info.contractAddress}'); + print(' Metadata: ${info.metadata}'); + if (info.tokenPairs.isNotEmpty) { + print(' Token Pairs:'); + for (var tokenPair in info.tokenPairs) { + print(' ${tokenPair.toJson()}'); + } + } +} + +Future _wrapFunctions() async { + switch (args[0].split('.')[2]) { + case 'token': + verbose + ? print('Description: Wrap assets for an EVM-compatible network') + : null; + await _wrapToken(); + return; + + case 'list': + verbose ? print('Description: List all wrap token requests') : null; + await _wrapList(); + return; + + case 'listByAddress': + verbose + ? print( + 'Description: List all wrap token requests for a NoM or EVM address') + : null; + await _wrapListByAddress(); + return; + + case 'listUnsigned': + verbose + ? print('Description: List all unsigned wrap token requests') + : null; + await _wrapListUnsigned(); + return; + + case 'get': + verbose ? print('Description: Get wrap token request by id') : null; + await _wrapGet(); + return; + + default: + invalidCommand(); + } +} + +Future _wrapToken() async { + if (args.length != 6) { + print('Incorrect number of arguments. Expected:'); + print( + 'bridge.wrap.token networkClass chainId toAddress amount tokenStandard'); + return; + } + + int networkClass = int.parse(args[1]); + int chainId = int.parse(args[2]); + String toAddress = args[3]; // must be EVM-compatible + TokenStandard tokenStandard = getTokenStandard(args[5]); + Token token = await getToken(tokenStandard); + BigInt amount = AmountUtils.extractDecimals(args[4], token.decimals); + + if (amount <= BigInt.zero) { + print('${red('Error!')} You cannot send that amount.'); + return; + } + + if (!await hasBalance(address, tokenStandard, amount)) { + return; + } + + BridgeNetworkInfo info = + await znnClient.embedded.bridge.getNetworkInfo(networkClass, chainId); + + if (info.networkClass == 0 || info.chainId == 0) { + print('${red('Error!')} The bridge network does not exist'); + return; + } + + bool found = false; + for (TokenPair tokenPair in info.tokenPairs) { + if (tokenPair.tokenStandard == tokenStandard) { + found = true; + if (amount < tokenPair.minAmount) { + print( + '${red('Error!')} Invalid amount. Must be at least ${AmountUtils.addDecimals(tokenPair.minAmount, token.decimals)} ${token.symbol}'); + return; + } + break; + } + } + + if (!found) { + print('${red('Error!')} That token cannot be wrapped'); + return; + } + + print('Wrapping token ...'); + AccountBlockTemplate block = znnClient.embedded.bridge + .wrapToken(networkClass, chainId, toAddress, amount, tokenStandard); + block = await send(block); + print('Done'); +} + +Future _wrapList() async { + WrapTokenRequestList list = + await znnClient.embedded.bridge.getAllWrapTokenRequests(); + print('All wrap token requests:'); + print('Count: ${list.count}'); + + if (list.count > 0) { + for (WrapTokenRequest request in list.list) { + print(' ${request.toJson()}'); + } + } +} + +Future _wrapListByAddress() async { + if (args.length != 2 && args.length != 4) { + print('Incorrect number of arguments. Expected:'); + print('bridge.wrap.listByAddress address [networkClass] [chainId]'); + return; + } + WrapTokenRequestList list; + String toAddress = args[1]; + var fromAddress; + try { + fromAddress = Address.parse(args[1]); + } catch (e) { + /* assume input is an EVM-compatible address */ + } + + if (fromAddress != null) { + var blocks = await znnClient.ledger.getAccountBlocksByPage(fromAddress); + if (blocks.count! > 0) { + List list = []; + for (var block in blocks.list!) { + if (block.toAddress == bridgeAddress && block.data.isNotEmpty) { + Function eq = const ListEquality().equals; + late AbiFunction f; + for (var entry in Definitions.bridge.entries) { + if (eq(AbiFunction.extractSignature(entry.encodeSignature()), + AbiFunction.extractSignature(block.data))) { + f = AbiFunction(entry.name!, entry.inputs!); + } + } + if (f.name == 'WrapToken') { + var request = await znnClient.embedded.bridge + .getWrapTokenRequestById(block.hash); + if (args.length == 4) { + int networkClass = int.parse(args[2]); + int chainId = int.parse(args[3]); + if (request.chainId != chainId || + request.networkClass != networkClass) { + continue; + } + } + list.add(request); + } + } + } + if (list.isNotEmpty) { + print('Count: ${list.length}'); + for (WrapTokenRequest request in list) { + await _printWrapTokenRequest(request); + } + } else { + print('No wrap requests found for $fromAddress'); + } + } + } else { + if (args.length == 4) { + int networkClass = int.parse(args[2]); + int chainId = int.parse(args[3]); + list = await znnClient.embedded.bridge + .getAllWrapTokenRequestsByToAddressNetworkClassAndChainId( + toAddress, networkClass, chainId); + } else { + list = await znnClient.embedded.bridge + .getAllWrapTokenRequestsByToAddress(toAddress); + } + if (list.count > 0) { + print('Count: ${list.count}'); + for (WrapTokenRequest request in list.list) { + await _printWrapTokenRequest(request); + } + } else { + print('No wrap requests found for $toAddress'); + } + } +} + +Future _wrapListUnsigned() async { + WrapTokenRequestList list = + await znnClient.embedded.bridge.getAllUnsignedWrapTokenRequests(); + + print('All unsigned wrap token requests:'); + print('Count: ${list.count}'); + + if (list.count > 0) { + for (var request in list.list) { + print(' ${request.toJson()}'); + } + } +} + +Future _wrapGet() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('bridge.wrap.get id'); + return; + } + + Hash id = parseHash(args[1]); + WrapTokenRequest request = + await znnClient.embedded.bridge.getWrapTokenRequestById(id); + + await _printWrapTokenRequest(request); +} + +Future _unwrapFunctions() async { + switch (args[0].split('.')[2]) { + case 'redeem': + verbose + ? print( + 'Description: redeem a pending unwrap request for any recipient') + : null; + await _unwrapRedeem(); + return; + + case 'redeemAll': + verbose + ? print( + 'Description: redeem all pending unwrap requests for yourself or all addresses') + : null; + await _unwrapRedeemAll(); + return; + + case 'list': + verbose ? print('Description: List all unwrap token requests') : null; + await _unwrapList(); + return; + + case 'listByAddress': + verbose + ? print('Description: List all unwrap token requests by NoM address') + : null; + await _unwrapListByAddress(); + return; + + case 'listUnredeemed': + verbose + ? print('Description: List all unredeemed unwrap token requests') + : null; + await _unwrapListUnredeemed(); + return; + + case 'get': + verbose + ? print('Description: Get unwrap token request by hash and log index') + : null; + await _unwrapGet(); + return; + + default: + invalidCommand(); + } +} + +Future _unwrapRedeem() async { + if (args.length != 3) { + print('Incorrect number of arguments. Expected:'); + print('bridge.unwrap.redeem transactionHash logIndex'); + return; + } + + Hash transactionHash = parseHash(args[1]); + int logIndex = int.parse(args[2]); + + UnwrapTokenRequest request = await znnClient.embedded.bridge + .getUnwrapTokenRequestByHashAndLog(transactionHash, logIndex); + + if (request.redeemed == 0 && request.revoked == 0) { + _printRedeem(request); + + AccountBlockTemplate block = znnClient.embedded.bridge + .redeem(request.transactionHash, request.logIndex); + await send(block); + + print('Done'); + if (request.toAddress == address) { + print( + 'Use ${green('receiveAll')} to collect your unwrapped tokens after 2 momentums'); + } + } else { + print('The unwrap request cannot be redeemed'); + } +} + +Future _unwrapRedeemAll() async { + if (args.length > 2) { + print('Incorrect number of arguments. Expected:'); + print('bridge.unwrap.redeemAll [bool]'); + print( + 'Note: if the boolean is true, all unredeemed transactions will be redeemed'); + return; + } + + bool redeemAllGlobally = false; + if (args.length == 2 && bool.parse(args[1]) == true) { + redeemAllGlobally = true; + } + + UnwrapTokenRequestList allUnwrapRequests = + await znnClient.embedded.bridge.getAllUnwrapTokenRequests(); + + int redeemedSelf = 0; + int redeemedTotal = 0; + + for (UnwrapTokenRequest request in allUnwrapRequests.list) { + if (request.redeemed == 0 && request.revoked == 0) { + if (redeemAllGlobally || + (args.length == 1 && request.toAddress == address)) { + _printRedeem(request); + AccountBlockTemplate redeem = znnClient.embedded.bridge + .redeem(request.transactionHash, request.logIndex); + redeem = await send(redeem); + if (request.toAddress == address) { + redeemedSelf += 1; + } + redeemedTotal += 1; + } + } + } + if (redeemedTotal > 0) { + print('Done'); + if (redeemedSelf > 0) { + print( + 'Use ${green('receiveAll')} to collect your unwrapped tokens after 2 momentums'); + } + } else { + print('No redeemable unwrap requests were found'); + } +} + +Future _unwrapList() async { + UnwrapTokenRequestList list = + await znnClient.embedded.bridge.getAllUnwrapTokenRequests(); + print('All unwrap token requests:'); + print('Count: ${list.count}'); + + if (list.count > 0) { + for (UnwrapTokenRequest request in list.list) { + print(' ${request.toJson()}'); + } + } +} + +Future _unwrapListByAddress() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('bridge.unwrap.listByAddress toAddress'); + return; + } + + Address toAddress = parseAddress(args[1]); + + UnwrapTokenRequestList list = await znnClient.embedded.bridge + .getAllUnwrapTokenRequestsByToAddress(toAddress.toString()); + + if (list.count > 0) { + print('Count: ${list.count}'); + for (UnwrapTokenRequest request in list.list) { + await _printUnwrapTokenRequest(request); + } + } else { + print('No unwrap requests found for $toAddress'); + } +} + +Future _unwrapListUnredeemed() async { + if (args.length > 2) { + print('Incorrect number of arguments. Expected:'); + print('bridge.unwrap.listUnredeemed [toAddress]'); + return; + } + UnwrapTokenRequestList allUnwrapRequests = + await znnClient.embedded.bridge.getAllUnwrapTokenRequests(); + + List unredeemed = []; + + for (UnwrapTokenRequest request in allUnwrapRequests.list) { + if (request.redeemed == 0 && request.revoked == 0) { + if ((args.length == 2 && request.toAddress == parseAddress(args[1])) || + (args.length == 1)) { + unredeemed.add(request); + } + } + } + print( + 'All unredeemed unwrap token requests${args.length == 2 ? ' for ${args[1]}:' : ':'}'); + print('Count: ${unredeemed.length}'); + + if (unredeemed.isNotEmpty) { + for (UnwrapTokenRequest request in unredeemed) { + await _printUnwrapTokenRequest(request); + } + } +} + +Future _unwrapGet() async { + if (args.length != 3) { + print('Incorrect number of arguments. Expected:'); + print('bridge.unwrap.get transactionHash logIndex'); + return; + } + + Hash transactionHash = parseHash(args[1]); + int logIndex = int.parse(args[2]); + + UnwrapTokenRequest request = await znnClient.embedded.bridge + .getUnwrapTokenRequestByHashAndLog(transactionHash, logIndex); + + await _printUnwrapTokenRequest(request); +} + +Future _guardianFunctions() async { + switch (args[0].split('.')[2]) { + case 'proposeAdmin': + verbose + ? print( + 'Description: Participate in a vote to elect a new bridge administrator when the bridge is in Emergency mode') + : null; + await _proposeAdmin(); + return; + + default: + invalidCommand(); + } +} + +Future _proposeAdmin() async { + if (!await _isGuardian()) return; + + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('bridge.guardian.proposeAdmin address'); + return; + } + + String currentAdmin = (await znnClient.embedded.bridge.getBridgeInfo()) + .administrator + .toString(); + Address newAdmin = parseAddress(args[1]); + if (!assertUserAddress(newAdmin)) { + return; + } + + if (currentAdmin == '' || + currentAdmin.isEmpty || + currentAdmin == emptyAddress.toString()) { + print('Proposing new bridge administrator ...'); + AccountBlockTemplate block = + znnClient.embedded.bridge.proposeAdministrator(newAdmin); + await send(block); + print('Done'); + } else { + print('${red('Permission denied!')} Bridge is not in emergency mode'); + } +} + +Future _adminFunctions() async { + switch (args[0].split('.')[2]) { + case 'emergency': + verbose + ? print('Description: Put the bridge contract in emergency mode') + : null; + await _emergency(); + return; + + case 'halt': + verbose ? print('Description: Halt bridge operations') : null; + await _halt(); + return; + + case 'unhalt': + verbose ? print('Description: Unhalt bridge operations') : null; + await _unhalt(); + return; + + case 'enableKeyGen': + verbose ? print('Description: Enable bridge key generation') : null; + await _enableKeyGen(); + return; + + case 'disableKeyGen': + verbose ? print('Description: Disable bridge key generation') : null; + await _disableKeyGen(); + return; + + case 'setTokenPair': + verbose + ? print('Description: Set a token pair to enable bridging the asset') + : null; + await _setTokenPair(); + return; + + case 'removeTokenPair': + verbose + ? print( + 'Description: Remove a token pair to disable bridging the asset') + : null; + await _removeTokenPair(); + return; + + case 'revokeUnwrapRequest': + verbose + ? print( + 'Description: Revoke an unwrap request to prevent it from being redeemed') + : null; + await _revokeUnwrapRequest(); + return; + + case 'nominateGuardians': + verbose ? print('Description: Nominate bridge guardians') : null; + await _nominateGuardians(); + return; + + case 'changeAdmin': + verbose ? print('Description: Change bridge administrator') : null; + await _changeAdmin(); + return; + + case 'setMetadata': + verbose ? print('Description: Set the bridge metadata') : null; + await _setMetadata(); + return; + + case 'setOrchestratorInfo': + verbose ? print('Description: Get the bridge information') : null; + await _setOrchestratorInfo(); + return; + + case 'setNetwork': + verbose + ? print('Description: Configure network parameters to allow bridging') + : null; + await _setNetwork(); + return; + + case 'removeNetwork': + verbose + ? print('Description: Remove a network to disable bridging') + : null; + await _removeNetwork(); + return; + + case 'setNetworkMetadata': + verbose ? print('Description: Set network metadata') : null; + await _setNetworkMetadata(); + return; + + default: + invalidCommand(); + } +} + +Future _emergency() async { + if (!await _isAdmin()) return; + print('Initializing bridge emergency mode ...'); + AccountBlockTemplate block = znnClient.embedded.bridge.emergency(); + await send(block); + print('Done'); +} + +Future _halt() async { + if (!await _isAdmin()) return; + print('Halting bridge ...'); + AccountBlockTemplate block = znnClient.embedded.bridge.halt('1'); + await send(block); + print('Done'); +} + +Future _unhalt() async { + if (!await _isAdmin()) return; + print('Unhalting the bridge ...'); + AccountBlockTemplate block = znnClient.embedded.bridge.unhalt(); + await send(block); + print('Done'); +} + +Future _enableKeyGen() async { + if (!await _isAdmin()) return; + print('Enabling TSS key generation ...'); + AccountBlockTemplate setAllowKeyGen = + znnClient.embedded.bridge.setAllowKeyGen(true); + setAllowKeyGen = await send(setAllowKeyGen); + print('Done'); +} + +Future _disableKeyGen() async { + if (!await _isAdmin()) return; + print('Disabling TSS key generation ...'); + AccountBlockTemplate block = znnClient.embedded.bridge.setAllowKeyGen(false); + await send(block); + print('Done'); +} + +Future _setTokenPair() async { + if (!await _isAdmin()) return; + + if (args.length != 12) { + print('Incorrect number of arguments. Expected:'); + print( + 'bridge.admin.setTokenPair networkClass chainId tokenStandard tokenAddress bridgeable redeemable owned minAmount feePercentage redeemDelay metadata'); + return; + } + + int networkClass = int.parse(args[1]); + int chainId = int.parse(args[2]); + TokenStandard tokenStandard = getTokenStandard(args[3]); + Token token = await getToken(tokenStandard); + String tokenAddress = args[4]; // must be EVM-compatible + bool bridgeable = bool.parse(args[5]); + bool redeemable = bool.parse(args[6]); + bool owned = bool.parse(args[7]); + BigInt minAmount = AmountUtils.extractDecimals(args[8], token.decimals); + int feePercentage = int.parse(args[9]) * 100; + int redeemDelay = int.parse(args[10]); + String metadata = args[11]; + jsonDecode(metadata); + + if (feePercentage > bridgeMaximumFee) { + print( + '${red('Error!')} Fee percentage may not exceed ${bridgeMaximumFee / 100}'); + return; + } + + if (redeemDelay == 0) { + print('${red('Error!')} Redeem delay cannot be 0'); + return; + } + + print('Setting token pair ...'); + AccountBlockTemplate setTokenPair = znnClient.embedded.bridge.setTokenPair( + networkClass, + chainId, + tokenStandard, + tokenAddress, + bridgeable, + redeemable, + owned, + minAmount, + feePercentage, + redeemDelay, + metadata, + ); + setTokenPair = await send(setTokenPair); + print('Done'); +} + +Future _removeTokenPair() async { + if (!await _isAdmin()) return; + + if (args.length != 5) { + print('Incorrect number of arguments. Expected:'); + print( + 'bridge.admin.removeTokenPair networkClass chainId tokenStandard tokenAddress'); + return; + } + + int networkClass = int.parse(args[1]); + int chainId = int.parse(args[2]); + TokenStandard tokenStandard = getTokenStandard(args[3]); + String tokenAddress = args[4]; // must be EVM-compatible + + print('Removing token pair ...'); + AccountBlockTemplate block = znnClient.embedded.bridge + .removeTokenPair(networkClass, chainId, tokenStandard, tokenAddress); + await send(block); + print('Done'); +} + +Future _revokeUnwrapRequest() async { + if (!await _isAdmin()) return; + + if (args.length != 3) { + print('Incorrect number of arguments. Expected:'); + print('bridge.admin.revokeUnwrapRequest transactionHash logIndex'); + return; + } + + Hash transactionHash = parseHash(args[1]); + int logIndex = int.parse(args[2]); + + print('Removing unwrap request ...'); + AccountBlockTemplate block = + znnClient.embedded.bridge.revokeUnwrapRequest(transactionHash, logIndex); + await send(block); + print('Done'); +} + +Future _nominateGuardians() async { + if (!await _isAdmin()) return; + + if (args.length < bridgeMinGuardians + 1) { + print( + 'Incorrect number of arguments. Expected at least $bridgeMinGuardians addresses:'); + print('bridge.admin.nominateGuardians address1 address2 ... addressN'); + return; + } + + List
guardians = []; + + for (int i = 1; i < args.length; i++) { + Address guardian = parseAddress(args[i]); + if (!assertUserAddress(guardian)) { + return; + } + guardians.add(guardian); + } + + List addresses = guardians.map((e) => e.toString()).toSet().toList(); + addresses.sort(); + + if (addresses.length != guardians.length) { + print('Duplicate address nomination detected'); + return; + } + + guardians = addresses.map((e) => Address.parse(e)).toList(); + + TimeChallengesList list = + await znnClient.embedded.bridge.getTimeChallengesInfo(); + TimeChallengeInfo? tc; + + if (list.count > 0) { + for (var _tc in list.list) { + if (_tc.methodName == 'NominateGuardians') { + tc = _tc; + } + } + } + + if (tc != null && tc.paramsHash != emptyHash) { + Momentum frontierMomentum = await znnClient.ledger.getFrontierMomentum(); + SecurityInfo securityInfo = + await znnClient.embedded.bridge.getSecurityInfo(); + + if (tc.challengeStartHeight + securityInfo.administratorDelay > + frontierMomentum.height) { + print('Cannot nominate guardians; wait for time challenge to expire.'); + return; + } + + ByteData bd = combine(guardians); + Hash paramsHash = Hash.digest(bd.buffer.asUint8List()); + + if (tc.paramsHash == paramsHash) { + print('Committing guardians ...'); + } else { + print('Time challenge hash does not match nominated guardians'); + if (!confirm('Are you sure you want to nominate new guardians?', + defaultValue: false)) return; + print('Nominating guardians ...'); + } + } else { + print('Nominating guardians ...'); + } + + AccountBlockTemplate block = + znnClient.embedded.bridge.nominateGuardians(guardians); + await send(block); + print('Done'); +} + +Future _changeAdmin() async { + if (!await _isAdmin()) return; + + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('bridge.admin.changeAdmin address'); + return; + } + + Address newAdmin = parseAddress(args[1]); + if (!assertUserAddress(newAdmin)) { + return; + } + + print('Changing bridge administrator ...'); + AccountBlockTemplate block = + znnClient.embedded.bridge.changeAdministrator(newAdmin); + await send(block); + print('Done'); +} + +Future _setMetadata() async { + if (!await _isAdmin()) return; + + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('bridge.admin.setMetadata metadata'); + return; + } + + String metadata = args[1]; + jsonDecode(metadata); + + print('Setting bridge metadata ...'); + AccountBlockTemplate block = + znnClient.embedded.bridge.setBridgeMetadata(metadata); + await send(block); + print('Done'); +} + +Future _setOrchestratorInfo() async { + if (!await _isAdmin()) return; + + if (args.length != 5) { + print('Incorrect number of arguments. Expected:'); + print( + 'bridge.admin.setOrchestratorInfo windowSize keyGenThreshold confirmationsToFinality estimatedMomentumTime'); + return; + } + + int windowSize = int.parse(args[1]); + int keyGenThreshold = int.parse(args[2]); + int confirmationsToFinality = int.parse(args[3]); + int estimatedMomentumTime = int.parse(args[4]); + + print('Setting orchestrator info ...'); + AccountBlockTemplate block = znnClient.embedded.bridge.setOrchestratorInfo( + windowSize, + keyGenThreshold, + confirmationsToFinality, + estimatedMomentumTime); + await send(block); + print('Done'); +} + +Future _setNetwork() async { + if (!await _isAdmin()) return; + + if (args.length != 6) { + print('Incorrect number of arguments. Expected:'); + print( + 'bridge.admin.setNetwork networkClass chainId name contractAddress metadata'); + return; + } + + int networkClass = int.parse(args[1]); + int chainId = int.parse(args[2]); + String name = args[3]; + String contractAddress = args[4]; + String metadata = args[5]; + jsonDecode(metadata); + + print('Setting network ...'); + AccountBlockTemplate block = znnClient.embedded.bridge.setNetwork( + networkClass, + chainId, + name, + contractAddress, + metadata, + ); + await send(block); + print('Done'); +} + +Future _removeNetwork() async { + if (!await _isAdmin()) return; + + if (args.length != 3) { + print('Incorrect number of arguments. Expected:'); + print('bridge.admin.removeNetwork networkClass chainId'); + return; + } + + int networkClass = int.parse(args[1]); + int chainId = int.parse(args[2]); + + print('Removing network ...'); + AccountBlockTemplate block = + znnClient.embedded.bridge.removeNetwork(networkClass, chainId); + await send(block); + print('Done'); +} + +Future _setNetworkMetadata() async { + if (!await _isAdmin()) return; + + if (args.length != 4) { + print('Incorrect number of arguments. Expected:'); + print('bridge.admin.setNetworkMetadata networkClass chainId metadata'); + return; + } + + int networkClass = int.parse(args[1]); + int chainId = int.parse(args[2]); + String metadata = args[3]; + jsonDecode(metadata); + + print('Setting network metadata ...'); + AccountBlockTemplate block = znnClient.embedded.bridge.setNetworkMetadata( + networkClass, + chainId, + metadata, + ); + await send(block); + print('Done'); +} + +Future _isGuardian() async { + if (!(await znnClient.embedded.bridge.getSecurityInfo()) + .guardians + .contains(address)) { + print( + '${red('Permission denied!')} This function can only be called by a Guardian'); + return false; + } + return true; +} + +Future _isAdmin() async { + if (!((await znnClient.embedded.bridge.getBridgeInfo()).administrator == + address)) { + print( + '${red('Permission denied!')} $address is not the Bridge administrator'); + return false; + } + return true; +} + +Future _printWrapTokenRequest(WrapTokenRequest request) async { + Token token = await getToken(request.tokenStandard); + int decimals = token.decimals; + Function color = getColor(request.tokenStandard); + + print('Id: ${request.id}'); + print(' Network Class: ${request.networkClass}'); + print(' Chain Id: ${request.chainId}'); + print(' To: ${request.toAddress}'); + print( + ' From: ${(await znnClient.ledger.getAccountBlockByHash(request.id))?.address}'); + print(' Token Standard: ${color(request.tokenStandard.toString())}'); + print( + ' Amount: ${AmountUtils.addDecimals(request.amount, decimals)} ${color(token.symbol)}'); + print(' Fee: ${AmountUtils.addDecimals(request.fee, decimals)}'); + print(' Signature: ${request.signature}'); + print(' Creation Momentum Height: ${request.creationMomentumHeight}'); + print(''); +} + +Future _printUnwrapTokenRequest(UnwrapTokenRequest request) async { + Token token = await getToken(request.tokenStandard); + int decimals = token.decimals; + Function color = getColor(request.tokenStandard); + + print('Id: ${request.transactionHash}'); + print(' Network Class: ${request.networkClass}'); + print(' Chain Id: ${request.chainId}'); + print(' Log Index: ${request.logIndex}'); + print(' To: ${request.toAddress}'); + print(' Token Standard: ${color(request.tokenStandard.toString())}'); + print( + ' Amount: ${AmountUtils.addDecimals(request.amount, decimals)} ${color(token.symbol)}'); + print(' Signature: ${request.signature}'); + print( + ' Registration Momentum Height: ${request.registrationMomentumHeight}'); + print(' Redeemed: ${request.redeemed == 1 ? 'True' : 'False'}'); + print(' Revoked: ${request.revoked == 1 ? 'True' : 'False'}'); + print(''); +} + +Future _printRedeem(UnwrapTokenRequest request) async { + Token token = await getToken(request.tokenStandard); + int decimals = token.decimals; + Function color = getColor(request.tokenStandard); + + print('Redeeming id: ${request.transactionHash}'); + print(' Log Index: ${request.logIndex}'); + print( + ' Amount: ${AmountUtils.addDecimals(request.amount, decimals)} ${color(token.symbol)}'); + print(' To: ${request.toAddress}'); + print(''); +} diff --git a/lib/general.dart b/lib/general.dart new file mode 100644 index 0000000..f89a674 --- /dev/null +++ b/lib/general.dart @@ -0,0 +1,326 @@ +import 'dart:convert'; +import 'package:convert/convert.dart'; +import 'package:dcli/dcli.dart' hide verbose; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +void generalMenu() { + print(' ${white('General')}'); + print( + ' send toAddress amount [${green('ZNN')}/${blue('QSR')}/${magenta('ZTS')} message]'); + print(' receive blockHash'); + print(' receiveAll'); + print(' autoreceive'); + print(' unreceived'); + print(' unconfirmed'); + print(' balance address'); + print(' frontierMomentum'); + print(' createHash [hashType preimageLength]'); + print(' version'); +} + +Future generalFunctions() async { + switch (args[0]) { + case 'send': + verbose ? print('Description: Send tokens to an address') : null; + await _send(); + return; + + case 'receive': + verbose + ? print('Description: Manually receive a transaction by blockHash') + : null; + await _receive(); + return; + + case 'receiveAll': + verbose ? print('Description: Receive all pending transactions') : null; + await _receiveAll(); + return; + + case 'autoreceive': + verbose ? print('Description: Automatically receive transactions') : null; + await _autoreceive(); + return; + + case 'unreceived': + verbose + ? print('Description: List pending/unreceived transactions') + : null; + await _unreceived(); + return; + + case 'unconfirmed': + verbose ? print('Description: List unconfirmed transactions') : null; + await _unconfirmed(); + return; + + case 'balance': + verbose ? print('Description: List account balance') : null; + await _balance(); + return; + + case 'frontierMomentum': + verbose ? print('Description: Display frontier momentum') : null; + await _frontierMomentum(); + return; + + case 'createHash': + verbose + ? print( + 'Description: Create hash digests by using the stated algorithm') + : null; + await _createHash(); + return; + + case 'version': + verbose ? print('Description: Display version information') : null; + _version(); + return; + + default: + invalidCommand(); + } +} + +void _version() { + print('$znnCli v$znnCliVersion using Zenon SDK v$znnSdkVersion'); +} + +Future _send() async { + if (args.length < 3 || args.length > 5) { + print('Incorrect number of arguments. Expected:'); + print( + 'send toAddress amount [${green('ZNN')}/${blue('QSR')}/${magenta('ZTS')} message]'); + return; + } + + Address recipient = parseAddress(args[1]); + TokenStandard tokenStandard = + args.length > 3 ? getTokenStandard(args[3]) : znnZts; + Token token = await getToken(tokenStandard); + BigInt amount = AmountUtils.extractDecimals(args[2], token.decimals); + Function color = getColor(tokenStandard); + + if (!await hasBalance(address, tokenStandard, amount)) { + return; + } + + var block = AccountBlockTemplate.send(recipient, tokenStandard, amount); + + if (args.length == 5) { + block.data = utf8.encode(args[4]); + print( + 'Sending ${AmountUtils.addDecimals(amount, token.decimals)} ${color(token.symbol)} to ${args[1]} with a message "${args[4]}"'); + } else { + print( + 'Sending ${AmountUtils.addDecimals(amount, token.decimals)} ${color(token.symbol)} to ${args[1]}'); + } + + await send(block); + print('Done'); +} + +Future _receive() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('receive blockHash'); + return; + } + Hash sendBlockHash = parseHash(args[1]); + print('Please wait ...'); + await send(AccountBlockTemplate.receive(sendBlockHash)); + print('Done'); +} + +Future _receiveAll() async { + var unreceived = await znnClient.ledger + .getUnreceivedBlocksByAddress(address, pageIndex: 0, pageSize: 5); + if (unreceived.count == 0) { + print('Nothing to receive'); + return; + } else { + if (unreceived.more!) { + print( + 'You have ${red('more')} than ${green(unreceived.count.toString())} transaction(s) to receive'); + } else { + print( + 'You have ${green(unreceived.count.toString())} transaction(s) to receive'); + } + } + + print('Please wait ...'); + while (unreceived.count! > 0) { + for (var block in unreceived.list!) { + await send(AccountBlockTemplate.receive(block.hash)); + } + unreceived = await znnClient.ledger + .getUnreceivedBlocksByAddress(address, pageIndex: 0, pageSize: 5); + } + print('Done'); +} + +Future _autoreceive() async { + znnClient.wsClient.addOnConnectionEstablishedCallback((broadcaster) async { + print('Subscribing for account-block events ...'); + await znnClient.subscribe.toAllAccountBlocks(); + print('Subscribed successfully!'); + + broadcaster.listen((json) async { + if (json!['method'] == 'ledger.subscription') { + for (var i = 0; i < json['params']['result'].length; i += 1) { + var tx = json['params']['result'][i]; + if (tx['toAddress'] != address.toString()) { + continue; + } + var hash = tx['hash']; + print('receiving transaction with hash $hash'); + var template = await send(AccountBlockTemplate.receive(parseHash(hash))); + print( + 'successfully received $hash. Receive-block-hash ${template.hash}'); + await Future.delayed(Duration(seconds: 1)); + } + } + }); + }); + + for (;;) { + await Future.delayed(Duration(seconds: 1)); + } +} + +Future _unreceived() async { + var unreceived = await znnClient.ledger + .getUnreceivedBlocksByAddress(address, pageIndex: 0, pageSize: 5); + + if (unreceived.count == 0) { + print('Nothing to receive'); + } else { + if (unreceived.more!) { + print( + 'You have ${red('more')} than ${green(unreceived.count.toString())} transaction(s) to receive'); + } else { + print( + 'You have ${green(unreceived.count.toString())} transaction(s) to receive'); + } + print('Showing the first ${unreceived.list!.length}'); + } + + for (var block in unreceived.list!) { + print( + 'Unreceived ${AmountUtils.addDecimals(block.amount, block.token!.decimals)} ${block.token!.symbol} from ${block.address.toString()}. Use the hash ${block.hash} to receive'); + } +} + +Future _unconfirmed() async { + var unconfirmed = await znnClient.ledger + .getUnconfirmedBlocksByAddress(address, pageIndex: 0, pageSize: 5); + + if (unconfirmed.count == 0) { + print('No unconfirmed transactions'); + } else { + print( + 'You have ${green(unconfirmed.count.toString())} unconfirmed transaction(s)'); + print('Showing the first ${unconfirmed.list!.length}'); + } + + var encoder = JsonEncoder.withIndent(' '); + for (var block in unconfirmed.list!) { + print(encoder.convert(block.toJson())); + } +} + +Future _balance() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('balance address'); + return; + } + + Address address = parseAddress(args[1]); + + AccountInfo info = await znnClient.ledger.getAccountInfoByAddress(address); + print( + 'Balance for account-chain ${info.address!.toString()} having height ${info.blockCount}'); + if (info.balanceInfoList!.isEmpty) { + print(' No coins or tokens at address ${address.toString()}'); + } + for (BalanceInfoListItem entry in info.balanceInfoList!) { + print( + ' ${AmountUtils.addDecimals(entry.balance!, entry.token!.decimals)} ${entry.token!.symbol} ' + '${entry.token!.domain} ${entry.token!.tokenStandard.toString()}'); + } +} + +Future _frontierMomentum() async { + Momentum currentFrontierMomentum = + await znnClient.ledger.getFrontierMomentum(); + print('Momentum height: ${currentFrontierMomentum.height.toString()}'); + print('Momentum hash: ${currentFrontierMomentum.hash.toString()}'); + print( + 'Momentum previousHash: ${currentFrontierMomentum.previousHash.toString()}'); + print('Momentum timestamp: ${currentFrontierMomentum.timestamp.toString()}'); +} + +Future _createHash() async { + if (args.length > 3) { + print('Incorrect number of arguments. Expected:'); + print('createHash [hashType preimageLength]'); + return; + } + + Hash hash; + int hashType = 0; + final List preimage; + int preimageLength = htlcPreimageDefaultLength; + + if (args.length >= 2) { + try { + hashType = int.parse(args[1]); + if (hashType > 1) { + print( + '${red('Error!')} Invalid hash type. Value $hashType not supported.'); + return; + } + } catch (e) { + print('${red('Error!')} hash type must be an integer.'); + print('Supported hash types:'); + print(' 0: SHA3-256'); + print(' 1: SHA2-256'); + return; + } + } + + if (args.length == 3) { + try { + preimageLength = int.parse(args[2]); + } catch (e) { + print('${red('Error!')} preimageLength must be an integer.'); + return; + } + } + + if (preimageLength > htlcPreimageMaxLength || + preimageLength < htlcPreimageMinLength) { + print( + '${red('Error!')} Invalid preimageLength. Preimage must be $htlcPreimageMaxLength bytes or less.'); + return; + } + if (preimageLength < htlcPreimageDefaultLength) { + print( + '${yellow('Warning!')} preimageLength is less than $htlcPreimageDefaultLength and may be insecure'); + } + preimage = generatePreimage(preimageLength); + print('Preimage: ${hex.encode(preimage)}'); + + switch (hashType) { + case 1: + hash = Hash.fromBytes(await Crypto.sha256Bytes(preimage)); + print('SHA2-256 Hash: $hash'); + return; + default: + hash = Hash.digest(preimage); + print('SHA3-256 Hash: $hash'); + return; + } +} diff --git a/lib/global.dart b/lib/global.dart new file mode 100644 index 0000000..ea6ee0e --- /dev/null +++ b/lib/global.dart @@ -0,0 +1,160 @@ +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; +import 'package:znn_ledger_dart/znn_ledger_dart.dart'; + +const znnDaemon = 'znnd'; +const znnCli = 'znn-cli'; +const znnCliVersion = '0.0.6'; + +final Zenon znnClient = Zenon(); +final KeyStoreManager keyStoreManager = + KeyStoreManager(walletPath: znnDefaultWalletDirectory); +final List walletManagers = [ + keyStoreManager, + LedgerWalletManager() +]; +late final WalletDefinition walletDefinition; +late final WalletOptions? walletOptions; +late final int accountIndex; +late final Address address; +late final List args; +bool verbose = false; + +List commandsWithWallet = [ + 'send', + 'receive', + 'receiveAll', + 'autoreceive', + 'unreceived', + 'unconfirmed', + 'plasma.list', + 'plasma.fuse', + 'plasma.cancel', + 'sentinel.list', + 'sentinel.register', + 'sentinel.revoke', + 'sentinel.collect', + 'sentinel.depositQsr', + 'sentinel.withdrawQsr', + 'stake.list', + 'stake.register', + 'stake.revoke', + 'stake.collect', + 'pillar.register', + 'pillar.revoke', + 'pillar.delegate', + 'pillar.undelegate', + 'pillar.collect', + 'pillar.depositQsr', + 'pillar.withdrawQsr', + 'token.issue', + 'token.mint', + 'token.burn', + 'token.transferOwnership', + 'token.disableMint', + 'wallet.dumpMnemonic', + 'wallet.deriveAddresses', + 'wallet.export', + 'az.donate', + 'spork.create', + 'spork.activate', + 'htlc.create', + 'htlc.reclaim', + 'htlc.unlock', + 'htlc.denyProxy', + 'htlc.allowProxy', + 'htlc.monitor', + 'bridge.wrap.token', + 'bridge.unwrap.redeem', + 'bridge.unwrap.redeemAll', + 'bridge.guardian.proposeAdmin', + 'liquidity.stake', + 'liquidity.cancelStake', + 'liquidity.collectRewards', + 'liquidity.guardian.proposeAdmin', + 'orchestrator.changePubKey', + 'orchestrator.haltBridge', + 'orchestrator.updateWrapRequest', + 'orchestrator.unwrapToken', +]; + +List commandsWithoutWallet = [ + 'frontierMomentum', + 'balance', + 'version', + 'createHash', + 'pillar.list', + 'plasma.get', + 'token.list', + 'token.getByStandard', + 'token.getByOwner', + 'wallet.createNew', + 'wallet.createFromMnemonic', + 'wallet.list', + 'spork.list', + 'htlc.get', + 'htlc.inspect', + 'htlc.getProxyStatus', + 'stats.osInfo', + 'stats.networkInfo', + 'stats.processInfo', + 'stats.syncInfo', + 'bridge.info', + 'bridge.security', + 'bridge.timeChallenges', + 'bridge.orchestratorInfo', + 'bridge.fees', + 'bridge.network.list', + 'bridge.network.get', + 'bridge.wrap.list', + 'bridge.wrap.listByAddress', + 'bridge.wrap.listUnsigned', + 'bridge.wrap.get', + 'bridge.unwrap.list', + 'bridge.unwrap.listByAddress', + 'bridge.unwrap.listUnredeemed', + 'bridge.unwrap.get', + 'liquidity.info', + 'liquidity.security', + 'liquidity.timeChallenges', + 'liquidity.getRewardTotal', + 'liquidity.getStakeEntries', + 'liquidity.getUncollectedReward', +]; + +List commandsWithoutConnection = [ + 'version', + 'wallet.createNew', + 'wallet.createFromMnemonic', + 'wallet.list', + 'wallet.dumpMnemonic', + 'wallet.deriveAddresses', + 'wallet.export', + 'createHash', +]; + +List adminCommands = [ + 'bridge.admin.emergency', + 'bridge.admin.halt', + 'bridge.admin.unhalt', + 'bridge.admin.enableKeyGen', + 'bridge.admin.disableKeyGen', + 'bridge.admin.setTokenPair', + 'bridge.admin.removeTokenPair', + 'bridge.admin.revokeUnwrapRequest', + 'bridge.admin.setMetadata', + 'bridge.admin.nominateGuardians', + 'bridge.admin.setMetadata', + 'bridge.admin.changeAdmin', + 'bridge.admin.setOrchestratorInfo', + 'bridge.admin.setNetwork', + 'bridge.admin.removeNetwork', + 'bridge.admin.setNetworkMetadata', + 'liquidity.admin.emergency', + 'liquidity.admin.halt', + 'liquidity.admin.unhalt', + 'liquidity.admin.changeAdmin', + 'liquidity.admin.nominateGuardians', + 'liquidity.admin.unlockStakeEntries', + 'liquidity.admin.setAdditionalReward', + 'liquidity.admin.setTokenTuple', +]; diff --git a/lib/htlc.dart b/lib/htlc.dart new file mode 100644 index 0000000..22afd92 --- /dev/null +++ b/lib/htlc.dart @@ -0,0 +1,609 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:convert/convert.dart'; +import 'package:dcli/dcli.dart' hide verbose; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +void htlcMenu() { + print(' ${white('HTLC')}'); + print( + ' htlc.create hashLockedAddress tokenStandard amount expirationTime (in hours) [hashType hashLock]'); + print(' htlc.unlock id preimage'); + print(' htlc.reclaim id'); + print(' htlc.get id'); + print(' htlc.inspect blockHash'); + print(' htlc.getProxyStatus address'); + print(' htlc.denyProxy'); + print(' htlc.allowProxy'); + print(' htlc.monitor id'); +} + +Future htlcFunctions() async { + switch (args[0].split('.')[1]) { + case 'create': + verbose ? print('Description: Create an htlc') : null; + await _create(); + return; + + case 'unlock': + verbose ? print('Description: Unlock an active htlc') : null; + await _unlock(); + return; + + case 'reclaim': + verbose ? print('Description: Reclaim an expired htlc') : null; + await _reclaim(); + return; + + case 'get': + verbose ? print('Description: Display htlc details') : null; + await _get(); + return; + + case 'inspect': + verbose ? print('Description: Inspect htlc account-block') : null; + await _inspect(); + return; + + case 'getProxyStatus': + verbose + ? print('Description: Display proxy unlock status for an address') + : null; + await _getProxyStatus(); + return; + + case 'denyProxy': + verbose ? print('Description: Deny htlc proxy unlock') : null; + await _denyProxy(); + return; + + case 'allowProxy': + verbose ? print('Description: Allow htlc proxy unlock') : null; + await _allowProxy(); + return; + + case 'monitor': + verbose + ? print( + 'Description: Monitor htlc by id -- automatically reclaim it or display its preimage') + : null; + await _monitor(); + return; + + default: + invalidCommand(); + } +} + +Future _create() async { + if (args.length < 5 || args.length > 7) { + print('Incorrect number of arguments. Expected:'); + print( + 'htlc.create hashLockedAddress tokenStandard amount expirationTime (in hours) [hashType hashLock]'); + return; + } + + Address hashLockedAddress; + TokenStandard tokenStandard = getTokenStandard(args[2]); + Token token = await getToken(tokenStandard); + BigInt amount = AmountUtils.extractDecimals(args[3], token.decimals); + Function color = getColor(tokenStandard); + int expirationTime; + late Hash hashLock; + int keyMaxSize = htlcPreimageMaxLength; + int hashType = 0; + List preimage = generatePreimage(htlcPreimageDefaultLength); + + int htlcTimelockMinHours = 1; + int htlcTimelockMaxHours = htlcTimelockMinHours * 24; + + hashLockedAddress = parseAddress(args[1]); + if (!assertUserAddress(hashLockedAddress)) { + return; + } + + if (amount <= BigInt.zero) { + print('${red('Error!')} amount must be greater than 0'); + return; + } + + if (!await hasBalance(address, tokenStandard, amount)) { + return; + } + + if (args.length >= 6) { + try { + hashType = int.parse(args[5]); + } catch (e) { + print('${red('Error!')} hash type must be an integer.'); + print('Supported hash types:'); + print(' 0: SHA3-256'); + print(' 1: SHA2-256'); + return; + } + } + + if (args.length == 7) { + hashLock = parseHash(args[6]); + } else { + switch (hashType) { + case 1: + hashLock = Hash.fromBytes(await Crypto.sha256Bytes(preimage)); + break; + default: + hashLock = Hash.digest(preimage); + } + } + + try { + expirationTime = int.parse(args[4]); + } catch (e) { + print('${red('Error!')} expirationTime must be an integer.'); + return; + } + + if (expirationTime < htlcTimelockMinHours || + expirationTime > htlcTimelockMaxHours) { + print( + '${red('Error!')} expirationTime (hours) must be at least $htlcTimelockMinHours and at most $htlcTimelockMaxHours.'); + return; + } + + expirationTime *= 60 * 60; // convert to seconds + final duration = Duration(seconds: expirationTime); + format(Duration d) => d.toString().split('.').first.padLeft(8, '0'); + Momentum currentFrontierMomentum = + await znnClient.ledger.getFrontierMomentum(); + int currentTime = currentFrontierMomentum.timestamp; + expirationTime += currentTime; + + if (args.length == 7) { + print( + 'Creating htlc with amount ${AmountUtils.addDecimals(amount, token.decimals)} ${color(token.symbol)}'); + } else { + print( + 'Creating htlc with amount ${AmountUtils.addDecimals(amount, token.decimals)} ${color(token.symbol)} using preimage ${green(hex.encode(preimage))}'); + } + print(' Can be reclaimed in ${format(duration)} by $address'); + print( + ' Can be unlocked by $hashLockedAddress with hashlock $hashLock hashtype $hashType'); + + AccountBlockTemplate block = await send(znnClient.embedded.htlc + .create(token, amount, hashLockedAddress, expirationTime, hashType, + keyMaxSize, hashLock.getBytes())); + + print('Submitted htlc with id ${green(block.hash.toString())}'); + print('Done'); +} + +Future _unlock() async { + if (args.length < 2 || args.length > 3) { + print('Incorrect number of arguments. Expected:'); + print('htlc.unlock id preimage'); + return; + } + + Hash id = parseHash(args[1]); + String preimage = ''; + late Hash preimageCheck; + int hashType = 0; + int currentTime = ((DateTime.now().millisecondsSinceEpoch) / 1000).floor(); + + HtlcInfo htlc = await _getById(id); + hashType = htlc.hashType; + + if (!await znnClient.embedded.htlc.getProxyUnlockStatus(htlc.hashLocked) && + address != htlc.hashLocked) { + print('${red('Error!')} Cannot unlock htlc. Permission denied'); + return; + } else if (htlc.expirationTime <= currentTime) { + print('${red('Error!')} Cannot unlock htlc. Time lock expired'); + return; + } + + if (args.length == 2) { + print('Insert preimage:'); + stdin.echoMode = false; + preimage = stdin.readLineSync()!; + stdin.echoMode = true; + } else if (args.length == 3) { + preimage = args[2]; + } + + if (preimage.isEmpty) { + print('${red('Error!')} Cannot unlock htlc. Invalid pre-image'); + return; + } + + switch (hashType) { + case 1: + print('HashType 1 detected. Encoding preimage to SHA2-256...'); + preimageCheck = + Hash.fromBytes(await Crypto.sha256Bytes(hex.decode(preimage))); + break; + default: + preimageCheck = (Hash.digest(hex.decode(preimage))); + } + + if (preimageCheck != Hash.fromBytes(htlc.hashLock)) { + print('${red('Error!')} preimage does not match the hashlock'); + return; + } + + Token token = await getToken(htlc.tokenStandard); + Function color = getColor(htlc.tokenStandard); + print( + 'Unlocking htlc id ${htlc.id} with amount ${AmountUtils.addDecimals(htlc.amount, token.decimals)} ${color(token.symbol)}'); + + await znnClient + .send(znnClient.embedded.htlc.unlock(id, hex.decode(preimage))); + print('Done'); + print( + 'Use ${green('receiveAll')} to collect your htlc amount after 2 momentums'); +} + +Future _reclaim() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('htlc.reclaim id'); + return; + } + + Hash id = parseHash(args[1]); + int currentTime = ((DateTime.now().millisecondsSinceEpoch) / 1000).floor(); + HtlcInfo htlc = await _getById(id); + + if (htlc.expirationTime > currentTime) { + format(Duration d) => d.toString().split('.').first.padLeft(8, '0'); + print( + '${red('Error!')} Cannot reclaim htlc. Try again in ${format(Duration(seconds: htlc.expirationTime - currentTime))}.'); + return; + } + + if (htlc.timeLocked != address) { + print('${red('Error!')} Cannot reclaim htlc. Permission denied'); + return; + } + + Token token = await getToken(htlc.tokenStandard); + Function color = getColor(htlc.tokenStandard); + print( + 'Reclaiming htlc id ${htlc.id} with amount ${AmountUtils.addDecimals(htlc.amount, token.decimals)} ${color(token.symbol)}'); + + await send(znnClient.embedded.htlc.reclaim(id)); + print('Done'); + print( + 'Use ${green('receiveAll')} to collect your htlc amount after 2 momentums'); +} + +Future _get() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('htlc.get id'); + return; + } + + Hash id = parseHash(args[1]); + HtlcInfo htlc = await _getById(id); + Token token = await getToken(htlc.tokenStandard); + Function color = getColor(htlc.tokenStandard); + int currentTime = ((DateTime.now().millisecondsSinceEpoch) / 1000).floor(); + format(Duration d) => d.toString().split('.').first.padLeft(8, '0'); + + print( + 'Htlc id ${htlc.id} with amount ${AmountUtils.addDecimals(htlc.amount, token.decimals)} ${color(token.symbol)}'); + if (htlc.expirationTime > currentTime) { + print( + ' Can be unlocked by ${htlc.hashLocked} with hashlock ${Hash.fromBytes(htlc.hashLock)} hashtype ${htlc.hashType}'); + print( + ' Can be reclaimed in ${format(Duration(seconds: htlc.expirationTime - currentTime))} by ${htlc.timeLocked}'); + } else { + print(' Can be reclaimed now by ${htlc.timeLocked}'); + } + + print('Done'); +} + +Future _inspect() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('htlc.inspect blockHash'); + return; + } + + Hash blockHash = parseHash(args[1]); + var block = await znnClient.ledger.getAccountBlockByHash(blockHash); + + if (block == null) { + print('The account block ${blockHash.toString()} does not exist'); + return; + } + + if (block.pairedAccountBlock == null || + block.blockType != BlockTypeEnum.userSend.index) { + print('The account block was not sent by a user'); + return; + } + + Function eq = const ListEquality().equals; + late AbiFunction f; + for (var entry in Definitions.htlc.entries) { + if (eq(AbiFunction.extractSignature(entry.encodeSignature()), + AbiFunction.extractSignature(block.data))) { + f = AbiFunction(entry.name!, entry.inputs!); + } + } + + if (f.name == null) { + print('The account block contains invalid data'); + return; + } + + var txArgs = f.decode(block.data); + if (f.name.toString() == 'Unlock') { + if (txArgs.length != 2) { + print('The account block has an invalid unlock argument length'); + return; + } + String preimage = hex.encode(txArgs[1]); + print( + 'Unlock htlc: id ${cyan(txArgs[0].toString())} unlocked by ${block.address} with pre-image: ${green(preimage)}'); + } else if (f.name.toString() == 'Reclaim') { + if (txArgs.length != 1) { + print('The account block has an invalid reclaim argument length'); + return; + } + print( + 'Reclaim htlc: id ${red(txArgs[0].toString())} reclaimed by ${block.address}'); + } else if (f.name.toString() == 'Create') { + if (txArgs.length != 5) { + print('The account block has an invalid create argument length'); + return; + } + + var hashLocked = txArgs[0]; + var expirationTime = txArgs[1]; + var hashLock = Hash.fromBytes(txArgs[4]); + var amount = block.amount; + var token = block.token; + var hashType = txArgs[2].toString(); + var keyMaxSize = txArgs[3].toString(); + print('Create htlc: ${hashLocked.toString()} ' + '${AmountUtils.addDecimals(amount, token!.decimals)} ' + '${token.symbol} $expirationTime ' + '$hashType ' + '$keyMaxSize ' + '${hashLock.toString()} ' + 'created by ${block.address}'); + } else { + print('The account block contains an unknown function call'); + } +} + +Future _getProxyStatus() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('htlc.getProxyStatus address'); + return; + } + + try { + address = parseAddress(args[1]); + } catch (e) { + print('${red('Error!')} address is not valid'); + return; + } + + await znnClient.embedded.htlc.getProxyUnlockStatus(address).then((value) => print( + 'Htlc proxy unlocking is ${(value) ? green('allowed') : red('denied')} for ${address.toString()}')); + + print('Done'); +} + +Future _denyProxy() async { + await send(znnClient.embedded.htlc.denyProxyUnlock()).then( + (_) => print('Htlc proxy unlocking is denied for ${address.toString()}')); + + print('Done'); +} + +Future _allowProxy() async { + await send(znnClient.embedded.htlc.allowProxyUnlock()).then((_) => + print('Htlc proxy unlocking is allowed for ${address.toString()}')); + + print('Done'); +} + +Future _monitor() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('htlc.monitor id'); + return; + } + + Hash id = parseHash(args[1]); + HtlcInfo htlc = await _getById(id); + List htlcs = []; + htlcs.add(htlc); + + while (await _monitorAsync(znnClient, address, htlcs) != true) { + await Future.delayed(Duration(seconds: 1)); + } +} + +Future _monitorAsync( + Zenon znnClient, Address address, List htlcs) async { + for (var htlc in htlcs) { + print('Monitoring htlc id ${cyan(htlc.id.toString())}'); + } + + // Thread 1: append new htlc contract interactions to queue + List queue = []; + znnClient.wsClient.addOnConnectionEstablishedCallback((broadcaster) async { + print('Subscribing for htlc-contract events...'); + + try { + await znnClient.subscribe.toAllAccountBlocks(); + } catch (e) { + print(e); + } + + // Extract hashes for all new tx that interact with the htlc contract + broadcaster.listen((json) async { + if (json!['method'] == 'ledger.subscription') { + for (var i = 0; i < json['params']['result'].length; i += 1) { + var tx = json['params']['result'][i]; + if (tx['toAddress'] != htlcAddress.toString()) { + continue; + } else { + var hash = tx['hash']; + queue.add(parseHash(hash)); + print('Receiving transaction with hash ${orange(hash)}'); + } + } + } + }); + }); + + List waitingToBeReclaimed = []; + + // Thread 2: if any tx in queue matches monitored htlc, remove it from queue + for (;;) { + if (htlcs.isEmpty && waitingToBeReclaimed.isEmpty) { + break; + } + var currentTime = (DateTime.now().millisecondsSinceEpoch / 1000).round(); + List _htlcs = htlcs.toList(); + + for (var htlc in _htlcs) { + // Reclaim any expired timeLocked htlc that is being monitored + if (htlc.expirationTime <= currentTime) { + print('Htlc id ${red(htlc.id.toString())} expired'); + + if (htlc.timeLocked == address) { + try { + await send(znnClient.embedded.htlc.reclaim(htlc.id)); + print(' Reclaiming htlc id ${red(htlc.id.toString())} now... '); + htlcs.remove(htlc); + } catch (e) { + print(' Error occurred when reclaiming ${htlc.id}'); + } + } else { + print(' Waiting for ${htlc.timeLocked} to reclaim...'); + waitingToBeReclaimed.add(htlc); + htlcs.remove(htlc); + } + } + + List _waitingToBeReclaimed = waitingToBeReclaimed.toList(); + List _queue = queue.toList(); + + if (queue.isNotEmpty) { + for (var hash in _queue) { + // Identify if htlc txs are either 'Unlock' or 'Reclaim' + var block = await znnClient.ledger.getAccountBlockByHash(hash); + + if (block?.blockType != BlockTypeEnum.userSend.index) { + continue; + } + + if (block?.pairedAccountBlock == null || + block?.pairedAccountBlock?.blockType != + BlockTypeEnum.contractReceive.index) { + continue; + } + + if ((block?.pairedAccountBlock?.descendantBlocks)!.isEmpty) { + continue; + } + + Function eq = const ListEquality().equals; + late AbiFunction f; + for (var entry in Definitions.htlc.entries) { + if (eq(AbiFunction.extractSignature(entry.encodeSignature()), + AbiFunction.extractSignature((block?.data)!))) { + f = AbiFunction(entry.name!, entry.inputs!); + } + } + + if (f.name == null) { + continue; + } + + // If 'Unlock', display its preimage + for (var htlc in _htlcs) { + if (f.name.toString() == 'Unlock') { + var args = f.decode((block?.data)!); + + if (args.length != 2) { + continue; + } + + if (args[0].toString() != htlc.id.toString()) { + continue; + } + + if ((block?.pairedAccountBlock?.descendantBlocks)!.any((x) => + x.blockType == BlockTypeEnum.contractSend.index && + x.tokenStandard == htlc.tokenStandard && + x.amount == htlc.amount)) { + final preimage = hex.encode(args[1]); + print( + 'htlc id ${cyan(htlc.id.toString())} unlocked with pre-image: ${green(preimage)}'); + + htlcs.remove(htlc); + } + } + } + + // If 'Reclaim', inform user that a monitored, expired htlc + // has been reclaimed by the timeLocked address + for (var htlc in _waitingToBeReclaimed) { + if (f.name.toString() == 'Reclaim') { + if (block?.address != htlc.timeLocked) { + continue; + } + + var args = f.decode((block?.data)!); + + if (args.length != 1) { + continue; + } + + if (args[0].toString() != htlc.id.toString()) { + continue; + } + + if ((block?.pairedAccountBlock?.descendantBlocks)!.any((x) => + x.blockType == BlockTypeEnum.contractSend.index && + x.toAddress == htlc.timeLocked && + x.tokenStandard == htlc.tokenStandard && + x.amount == htlc.amount)) { + print( + 'htlc id ${red(htlc.id.toString())} reclaimed by ${htlc.timeLocked}'); + waitingToBeReclaimed.remove(htlc); + } else { + print((block?.pairedAccountBlock?.descendantBlocks)!); + } + } + } + queue.remove(hash); + } + } + await Future.delayed(Duration(seconds: 1)); + } + } + print('No longer monitoring the htlc'); + return true; +} + +Future _getById(Hash id) async { + try { + return await znnClient.embedded.htlc.getById(id); + } catch (e) { + throw ('${red('Error!')} The htlc id $id does not exist'); + } +} diff --git a/lib/lib.dart b/lib/lib.dart new file mode 100644 index 0000000..342c110 --- /dev/null +++ b/lib/lib.dart @@ -0,0 +1,16 @@ +export 'accelerator.dart'; +export 'bridge.dart'; +export 'general.dart'; +export 'global.dart'; +export 'htlc.dart'; +export 'liquidity.dart'; +export 'orchestrator.dart'; +export 'pillar.dart'; +export 'plasma.dart'; +export 'sentinel.dart'; +export 'spork.dart'; +export 'staking.dart'; +export 'stats.dart'; +export 'token.dart'; +export 'wallet.dart'; +export 'utils/utils.dart'; diff --git a/lib/liquidity.dart b/lib/liquidity.dart new file mode 100644 index 0000000..6c1d839 --- /dev/null +++ b/lib/liquidity.dart @@ -0,0 +1,800 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:dcli/dcli.dart' hide verbose; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +void liquidityMenu() { + print(' ${white('Liquidity')}'); + print(' liquidity.info'); + print(' liquidity.security'); + print(' liquidity.timeChallenges'); + print(' liquidity.getRewardTotal address'); + print(' liquidity.getStakeEntries address'); + print(' liquidity.getUncollectedReward address'); + print(' liquidity.stake duration (in months) amount tokenStandard'); + print(' liquidity.cancelStake id'); + print(' liquidity.collectRewards'); + print(' liquidity.guardian.proposeAdmin address'); +} + +void liquidityAdminMenu() { + print(' ${white('Liquidity Admin')}'); + print(' liquidity.admin.emergency'); + print(' liquidity.admin.halt'); + print(' liquidity.admin.unhalt'); + print(' liquidity.admin.changeAdmin address'); + print(' liquidity.admin.nominateGuardians address1 address2 ... addressN'); + print(' liquidity.admin.unlockStakeEntries tokenStandard'); + print(' liquidity.admin.setAdditionalReward znnReward qsrReward'); + print(' liquidity.admin.setTokenTuple tokenTuples (json)'); +} + +Future liquidityFunctions() async { + switch (args[0].split('.')[1]) { + case 'info': + verbose ? print('Description: Get the liquidity information') : null; + await _info(); + return; + + case 'security': + verbose ? print('Description: Get the liquidity security info') : null; + await _security(); + return; + + case 'timeChallenges': + verbose ? print('Description: List the liquidity time challenges') : null; + await _timeChallenges(); + return; + + case 'getRewardTotal': + verbose + ? print('Description: Display total rewards an address has earned') + : null; + await _getRewardTotal(); + return; + + case 'getStakeEntries': + verbose + ? print('Description: Display all stake entries for an address') + : null; + await _getStakeEntries(); + return; + + case 'getUncollectedReward': + verbose + ? print('Description: Display uncollected rewards for an address') + : null; + await _getUncollectedReward(); + return; + + case 'stake': + verbose ? print('Description: Stake LP tokens') : null; + await _stake(); + return; + + case 'cancelStake': + verbose + ? print( + 'Description: Cancel an unlocked stake and receive your LP tokens') + : null; + await _cancelStake(); + return; + + case 'collectRewards': + verbose ? print('Description: Collect liquidity rewards') : null; + await _collectRewards(); + return; + + case 'guardian': + await _guardianFunctions(); + return; + + case 'admin': + await _adminFunctions(); + return; + + default: + invalidCommand(); + } +} + +Future _info() async { + LiquidityInfo info = await znnClient.embedded.liquidity.getLiquidityInfo(); + + print('Liquidity info:'); + print(' Administrator: ${info.administrator}'); + print( + ' ${green('ZNN')} reward: ${green(AmountUtils.addDecimals(info.znnReward, coinDecimals))}'); + print( + ' ${blue('QSR')} reward: ${blue(AmountUtils.addDecimals(info.qsrReward, coinDecimals))}'); + print(' Is halted: ${info.isHalted}'); + print(' Tokens:'); + + for (TokenTuple tuple in info.tokenTuples) { + { + Token token = await getToken(tuple.tokenStandard); + Function color = getColor(tuple.tokenStandard); + + var type = 'Token'; + if (token.tokenStandard == qsrZts || token.tokenStandard == znnZts) { + type = 'Coin'; + } + print( + ' $type ${color(token.name)} with symbol ${color(token.symbol)} and standard ${color(token.tokenStandard.toString())}'); + print( + ' ${green('ZNN ${tuple.znnPercentage / 100}%')} ${blue('QSR ${tuple.qsrPercentage / 100}%')} minimum amount ${AmountUtils.addDecimals(tuple.minAmount, token.decimals)}'); + print(''); + } + } +} + +Future _security() async { + SecurityInfo info = await znnClient.embedded.liquidity.getSecurityInfo(); + print('Security info:'); + + if (info.guardians.isEmpty) { + print(' Guardians: none'); + } else { + print(' Guardians: '); + for (Address guardian in info.guardians) { + print(' $guardian'); + } + } + + if (info.guardiansVotes.isEmpty) { + print(' Guardian votes: none'); + } else { + print(' Guardian votes: '); + for (Address guardianVotes in info.guardiansVotes) { + print(' $guardianVotes'); + } + } + + print(' Administrator delay: ${info.administratorDelay}'); + print(' Soft delay: ${info.softDelay}'); +} + +Future _timeChallenges() async { + TimeChallengesList list = + await znnClient.embedded.liquidity.getTimeChallengesInfo(); + + if (list.count == 0) { + print('No time challenges found.'); + return; + } + + print('Time challenges:'); + for (var info in list.list) { + print(' Method: ${info.methodName}'); + print(' Start height: ${info.challengeStartHeight}'); + print(' Params hash: ${info.paramsHash}'); + print(''); + } +} + +Future _getRewardTotal() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('liquidity.getRewardTotal address'); + return; + } + + Address address = parseAddress(args[1]); + RewardHistoryList list = + await znnClient.embedded.liquidity.getFrontierRewardByPage(address); + + BigInt znnRewards = BigInt.zero; + BigInt qsrRewards = BigInt.zero; + + if (list.count > 0) { + for (RewardHistoryEntry entry in list.list) { + if (entry.znnAmount != BigInt.zero || entry.qsrAmount != BigInt.zero) { + znnRewards += entry.znnAmount; + qsrRewards += entry.qsrAmount; + } + } + if (znnRewards == BigInt.zero && qsrRewards == BigInt.zero) { + print('No rewards found.'); + } else { + print('Total Rewards:'); + print( + ' ${green('ZNN')}: ${green(AmountUtils.addDecimals(znnRewards, coinDecimals))}'); + print( + ' ${blue('QSR')}: ${blue(AmountUtils.addDecimals(qsrRewards, coinDecimals))}'); + } + } else { + print('No rewards found.'); + } +} + +Future _getStakeEntries() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('liquidity.getStakeEntries address'); + return; + } + + Address address = parseAddress(args[1]); + LiquidityStakeList list = await znnClient.embedded.liquidity + .getLiquidityStakeEntriesByAddress(address); + + if (list.count == 0) { + print('No stake entries found.'); + return; + } + + print('Stake Entries:'); + print(' Total Amount: ${list.totalAmount}'); + print(' Total Weighted Amount: ${list.totalWeightedAmount}'); + for (LiquidityStakeEntry info in list.list) { + Token token = await getToken(info.tokenStandard); + + int currentTime = ((DateTime.now().millisecondsSinceEpoch) / 1000).floor(); + format(Duration d) => d.toString().split('.').first.padLeft(8, '0'); + double duration = (info.expirationTime - info.startTime) / stakeTimeUnitSec; + int timeRemaining = info.expirationTime - currentTime; + + print(' Id: ${info.id}'); + print( + ' Status: ${info.amount != BigInt.zero && info.revokeTime == 0 ? 'Active' : 'Cancelled'}'); + print(' Token: ${token.name}'); + print( + ' Amount: ${AmountUtils.addDecimals(info.amount, token.decimals)} ${token.symbol}'); + print( + ' Weighted Amount: ${AmountUtils.addDecimals(info.weightedAmount, token.decimals)} ${token.symbol}'); + print( + ' Duration: $duration $stakeUnitDurationName${duration > 1 ? 's' : null}'); + print( + ' Time Remaining: ${format(Duration(seconds: timeRemaining))} day${timeRemaining > (24 * 60 * 60) ? 's' : null}'); + print(' Revoke Time: ${info.revokeTime}'); + print(''); + } +} + +Future _getUncollectedReward() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('liquidity.getUncollectedReward address'); + return; + } + + Address address = parseAddress(args[1]); + RewardDeposit uncollectedRewards = + await znnClient.embedded.liquidity.getUncollectedReward(address); + + if (uncollectedRewards.znnAmount != BigInt.zero || + uncollectedRewards.qsrAmount != BigInt.zero) { + print('Uncollected Rewards:'); + print( + ' ${green('ZNN')}: ${green(AmountUtils.addDecimals(uncollectedRewards.znnAmount, coinDecimals))}'); + print( + ' ${blue('QSR')}: ${blue(AmountUtils.addDecimals(uncollectedRewards.qsrAmount, coinDecimals))}'); + } else { + print('No uncollected rewards'); + } +} + +Future _stake() async { + if (args.length != 4) { + print('Incorrect number of arguments. Expected:'); + print('liquidity.stake duration (in months) amount tokenStandard'); + return; + } + + int months = int.parse(args[1]); + int duration = months * stakeTimeUnitSec; + TokenStandard tokenStandard = getTokenStandard(args[3]); + Token token = await getToken(tokenStandard); + BigInt amount = AmountUtils.extractDecimals(args[2], token.decimals); + + if (duration < stakeTimeMinSec || + duration > stakeTimeMaxSec || + duration % stakeTimeUnitSec != 0) { + print('${red('Error!')} Invalid staking duration'); + return; + } + + if (!await hasBalance(address, tokenStandard, amount)) { + return; + } + + LiquidityInfo info = await znnClient.embedded.liquidity.getLiquidityInfo(); + if (info.isHalted) { + print('${red('Error!')} Liquidity contract is halted'); + return; + } + + bool found = false; + late TokenTuple liquidityToken; + for (TokenTuple token in info.tokenTuples) { + if (token.tokenStandard == tokenStandard) { + found = true; + liquidityToken = token; + break; + } + } + + if (found) { + if (amount < liquidityToken.minAmount) { + print( + '${red('Error!')} Minimum staking requirement: ${AmountUtils.addDecimals(liquidityToken.minAmount, token.decimals)} ${token.symbol}'); + return; + } + } else { + print( + '${red('Error!')} ${token.name} cannot be staked in the Liquidity contract'); + return; + } + + print( + 'Staking ${AmountUtils.addDecimals(amount, token.decimals)} ${token.symbol} for $months month${months > 1 ? 's' : null} ...'); + AccountBlockTemplate block = znnClient.embedded.liquidity + .liquidityStake(duration, amount, tokenStandard); + await send(block); + print('Done'); +} + +Future _cancelStake() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('liquidity.cancelStake id'); + return; + } + + Hash id = parseHash(args[1]); + + LiquidityStakeList list = await znnClient.embedded.liquidity + .getLiquidityStakeEntriesByAddress(address); + + if (list.count == 0) { + print('No stake entries found.'); + return; + } + + LiquidityStakeEntry? entry; + bool found = false; + for (LiquidityStakeEntry info in list.list) { + if (info.id == id) { + entry = info; + found = true; + break; + } + } + + if (found) { + int currentTime = ((DateTime.now().millisecondsSinceEpoch) / 1000).floor(); + + if (currentTime > entry!.expirationTime) { + print('Cancelling liquidity stake ...'); + AccountBlockTemplate block = + znnClient.embedded.liquidity.cancelLiquidityStake(id); + await send(block); + print('Done'); + print( + 'Use ${green('receiveAll')} to collect your staked amount after 2 momentums'); + } else { + format(Duration d) => d.toString().split('.').first.padLeft(8, '0'); + print('That staking entry is not unlocked yet.'); + print( + 'Time Remaining: ${format(Duration(seconds: entry.expirationTime - currentTime))}'); + } + } else { + print('Staking entry not found'); + } +} + +Future _collectRewards() async { + RewardDeposit uncollectedRewards = + await znnClient.embedded.liquidity.getUncollectedReward(address); + + if (uncollectedRewards.znnAmount != BigInt.zero || + uncollectedRewards.qsrAmount != BigInt.zero) { + print('Uncollected Rewards:'); + print( + ' ${green('ZNN')}: ${green(AmountUtils.addDecimals(uncollectedRewards.znnAmount, coinDecimals))}'); + print( + ' ${blue('QSR')}: ${blue(AmountUtils.addDecimals(uncollectedRewards.qsrAmount, coinDecimals))}'); + print(''); + print('Collecting rewards ...'); + AccountBlockTemplate block = znnClient.embedded.liquidity.collectReward(); + await send(block); + print('Done'); + } else { + print('No uncollected rewards'); + } +} + +Future _guardianFunctions() async { + switch (args[0].split('.')[2]) { + case 'proposeAdmin': + verbose + ? print( + 'Description: Participate in a vote to elect a new liquidity administrator when the contract is in Emergency mode') + : null; + await _proposeAdmin(); + return; + + default: + invalidCommand(); + } +} + +Future _proposeAdmin() async { + if (!await _isGuardian()) return; + + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('liquidity.guardian.proposeAdmin address'); + return; + } + + String currentAdmin = (await znnClient.embedded.liquidity.getLiquidityInfo()) + .administrator + .toString(); + Address newAdmin = parseAddress(args[1]); + if (!assertUserAddress(newAdmin)) { + return; + } + + if (currentAdmin == '' || + currentAdmin.isEmpty || + currentAdmin == emptyAddress.toString()) { + print('Proposing new liquidity administrator ...'); + AccountBlockTemplate block = + znnClient.embedded.liquidity.proposeAdministrator(newAdmin); + await send(block); + print('Done'); + } else { + print( + '${red('Permission denied!')} Liquidity contract is not in emergency mode'); + } +} + +Future _adminFunctions() async { + switch (args[0].split('.')[2]) { + case 'emergency': + verbose + ? print('Description: Put the liquidity contract in emergency mode') + : null; + await _emergency(); + return; + + case 'halt': + verbose ? print('Description: Halt liquidity operations') : null; + await _halt(); + return; + + case 'unhalt': + verbose ? print('Description: Unhalt liquidity operations') : null; + await _unhalt(); + return; + + case 'changeAdmin': + verbose ? print('Description: Change liquidity administrator') : null; + await _changeAdmin(); + return; + + case 'nominateGuardians': + verbose ? print('Description: Nominate liquidity guardians') : null; + await _nominateGuardians(); + return; + + case 'unlockStakeEntries': + verbose + ? print( + 'Description: Allows all staked entries to be cancelled immediately') + : null; + await _unlockStakeEntries(); + return; + + case 'setAdditionalReward': + verbose + ? print('Description: Set additional liquidity reward percentages') + : null; + await _setAdditionalReward(); + return; + + case 'setTokenTuple': + verbose + ? print('Description: Configure token tuples that can be staked\n' + 'Example: ' + '${green('\'{\"tokenStandards\": [\"zts1nmfd7dkgqtwh6h9a3wu4xm\", \"zts1q5dh77csuncy6aetwd05gn\"],' + '\"znnPercentages\": [5000,5000],' + '\"qsrPercentages\": [5000,5000],' + '\"minAmounts\": [\"10\",\"1000\"]}\'')}') + : null; + await _setTokenTuple(); + return; + + default: + invalidCommand(); + } +} + +Future _emergency() async { + if (!await _isAdmin()) return; + print('Initializing liquidity emergency mode ...'); + AccountBlockTemplate block = znnClient.embedded.liquidity.emergency(); + await send(block); + print('Done'); +} + +Future _halt() async { + if (!await _isAdmin()) return; + print('Halting the liquidity ...'); + AccountBlockTemplate block = znnClient.embedded.liquidity.setIsHalted(false); + await send(block); + print('Done'); +} + +Future _unhalt() async { + if (!await _isAdmin()) return; + print('Unhalting the liquidity ...'); + AccountBlockTemplate block = znnClient.embedded.liquidity.setIsHalted(false); + await send(block); + print('Done'); +} + +Future _changeAdmin() async { + if (!await _isAdmin()) return; + + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('liquidity.admin.changeAdmin address'); + return; + } + + Address newAdmin = parseAddress(args[1]); + if (!assertUserAddress(newAdmin)) { + return; + } + + print('Changing liquidity administrator ...'); + AccountBlockTemplate block = + znnClient.embedded.liquidity.changeAdministrator(newAdmin); + await send(block); + print('Done'); +} + +Future _nominateGuardians() async { + if (!await _isAdmin()) return; + + if (args.length < bridgeMinGuardians + 1) { + print( + 'Incorrect number of arguments. Expected at least $bridgeMinGuardians addresses:'); + print('liquidity.admin.nominateGuardians address1 address2 ... addressN'); + return; + } + + List
guardians = []; + + for (int i = 1; i < args.length; i++) { + Address guardian = parseAddress(args[i]); + if (!assertUserAddress(guardian)) { + return; + } + guardians.add(guardian); + } + + List addresses = guardians.map((e) => e.toString()).toSet().toList(); + addresses.sort(); + + if (addresses.length != guardians.length) { + print('Duplicate address nomination detected'); + return; + } + + guardians = addresses.map((e) => Address.parse(e)).toList(); + + if (!await _checkTimeChallenge('NominateGuardians', guardians)) { + print('${red('Error!')} Time challenge failure'); + return; + } + + print('Nominating guardians ...'); + AccountBlockTemplate block = + znnClient.embedded.liquidity.nominateGuardians(guardians); + await send(block); + print('Done'); +} + +Future _unlockStakeEntries() async { + if (!await _isAdmin()) return; + + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('liquidity.unlockStakeEntries tokenStandard'); + return; + } + + TokenStandard tokenStandard = getTokenStandard(args[1]); + Token token = await getToken(tokenStandard); + + print('Unlocking ${token.name} stake entries ...'); + AccountBlockTemplate block = + znnClient.embedded.liquidity.unlockLiquidityStakeEntries(tokenStandard); + await send(block); + print('Done'); +} + +Future _setAdditionalReward() async { + if (!await _isAdmin()) return; + + if (args.length != 3) { + print('Incorrect number of arguments. Expected:'); + print('liquidity.admin.setAdditionalReward znnReward qsrReward'); + return; + } + + int znnReward = int.parse(args[1]); + int qsrReward = int.parse(args[2]); + + print('Setting additional liquidity reward ...'); + AccountBlockTemplate block = + znnClient.embedded.liquidity.setAdditionalReward(znnReward, qsrReward); + await send(block); + print('Done'); +} + +Future _setTokenTuple() async { + if (!await _isAdmin()) return; + + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('liquidity.admin.setTokenTuple tokenTuples (json)'); + return; + } + + String tokenTuples = args[1]; + final data = jsonDecode(tokenTuples); + + List tokenStandards = []; + List znnPercentages = []; + List qsrPercentages = []; + List minAmounts = []; + + for (MapEntry entries in data.entries) { + switch (entries.key.toString().toLowerCase()) { + case 'tokenstandards': + entries.value.forEach((zts) { + parseTokenStandard(zts); + + if (zts == emptyZts.toString()) { + throw ('${red('Error!')} Invalid ZTS'); + } + + if (tokenStandards.contains(zts)) { + throw ('${red('Error!')} Cannot set parameters for duplicate ZTS'); + } + + tokenStandards.add(zts); + }); + break; + case 'znnpercentages': + entries.value.forEach((percentage) { + znnPercentages.add(percentage); + }); + break; + case 'qsrpercentages': + entries.value.forEach((percentage) { + qsrPercentages.add(percentage); + }); + break; + case 'minamounts': + entries.value.forEach((amount) { + minAmounts.add(BigInt.parse(amount)); + }); + break; + default: + print('${red('Error!')} Invalid json parameters'); + return; + } + } + + if (tokenStandards.isEmpty) { + print('${red('Error!')} Invalid number of arguments'); + return; + } + + if ((znnPercentages.length != tokenStandards.length) || + (qsrPercentages.length != tokenStandards.length) || + (minAmounts.length != tokenStandards.length)) { + print( + '${red('Error!')} Each argument type must have the same number of values'); + return; + } + + int totalZnn = 0; + int totalQsr = 0; + int znnTotalPercentages = 10000; + int qsrTotalPercentages = 10000; + + for (int i = 0; i < znnPercentages.length; i++) { + totalZnn += znnPercentages.elementAt(i); + totalQsr += qsrPercentages.elementAt(i); + } + if (totalZnn != znnTotalPercentages || totalQsr != qsrTotalPercentages) { + print( + '${red('Error!')} The sum of each set of percentages must be equal to $znnTotalPercentages'); + return; + } + + if (!await _checkTimeChallenge('SetTokenTuple', + (await znnClient.embedded.liquidity.getLiquidityInfo()).tokenTuples)) { + print('${red('Error!')} Time challenge failure'); + return; + } + + print('Setting token parameters ...'); + AccountBlockTemplate block = znnClient.embedded.liquidity.setTokenTuple( + tokenStandards, + znnPercentages, + qsrPercentages, + minAmounts, + ); + await send(block); + print('Done'); +} + +Future _isGuardian() async { + if (!(await znnClient.embedded.liquidity.getSecurityInfo()) + .guardians + .contains(address)) { + print( + '${red('Permission denied!')} This function can only be called by a Guardian'); + return false; + } + return true; +} + +Future _isAdmin() async { + if (!((await znnClient.embedded.liquidity.getLiquidityInfo()).administrator == + address)) { + print( + '${red('Permission denied!')} $address is not the Liquidity administrator'); + return false; + } + return true; +} + +Future _checkTimeChallenge( + String methodName, + List params, +) async { + TimeChallengesList list = + await znnClient.embedded.liquidity.getTimeChallengesInfo(); + TimeChallengeInfo? tc; + + if (list.count > 0) { + for (var _tc in list.list) { + if (_tc.methodName == methodName) { + tc = _tc; + } + } + } + + if (tc != null && tc.paramsHash != emptyHash) { + Momentum frontierMomentum = await znnClient.ledger.getFrontierMomentum(); + SecurityInfo securityInfo = + await znnClient.embedded.liquidity.getSecurityInfo(); + + if (tc.challengeStartHeight + securityInfo.administratorDelay > + frontierMomentum.height) { + print('Cannot proceed; wait for time challenge to expire.'); + return false; + } + + ByteData bd = combine(params); + Hash paramsHash = Hash.digest(bd.buffer.asUint8List()); + + if (tc.paramsHash != paramsHash) { + print('Time challenge mismatch'); + if (!confirm('Are you sure you want to proceed?', defaultValue: false)) { + return false; + } + } + } + return true; +} diff --git a/lib/orchestrator.dart b/lib/orchestrator.dart new file mode 100644 index 0000000..e867401 --- /dev/null +++ b/lib/orchestrator.dart @@ -0,0 +1,14 @@ +import 'package:dcli/dcli.dart' hide verbose; + +void orchestratorMenu() { + print(' ${white('Orchestrator')}'); + print(' orchestrator.changePubKey'); + print(' orchestrator.haltBridge'); + print(' orchestrator.updateWrapRequest'); + print(' orchestrator.unwrapToken'); +} + +Future orchestatorFunctions() async { + print('Orchestrator-only functions are currently unsupported'); + return; +} diff --git a/lib/pillar.dart b/lib/pillar.dart new file mode 100644 index 0000000..9f6a3fa --- /dev/null +++ b/lib/pillar.dart @@ -0,0 +1,231 @@ +import 'package:dcli/dcli.dart' hide verbose; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +void pillarMenu() { + print(' ${white('Pillar')}'); + print(' pillar.list'); + print( + ' pillar.register name producerAddress rewardAddress giveBlockRewardPercentage giveDelegateRewardPercentage'); + print(' pillar.revoke name'); + print(' pillar.delegate name'); + print(' pillar.undelegate'); + print(' pillar.collect'); + print(' pillar.depositQsr'); + print(' pillar.withdrawQsr'); +} + +Future pillarFunctions() async { + switch (args[0].split('.')[1]) { + case 'list': + verbose ? print('Description: List all pillars') : null; + await _list(); + return; + + case 'register': + verbose ? print('Description: Register pillar') : null; + await _register(); + return; + + case 'revoke': + verbose ? print('Description: Revoke pillar') : null; + await _revoke(); + return; + + case 'delegate': + verbose ? print('Description: Delegate to pillar') : null; + await _delegate(); + return; + + case 'undelegate': + verbose ? print('Description: Undelegate pillar') : null; + await _undelegate(); + return; + + case 'collect': + verbose ? print('Description: Collect pillar rewards') : null; + await _collect(); + return; + + case 'depositQsr': + verbose ? print('Description: Deposit QSR to the pillar contract') : null; + await _depositQsr(); + return; + + case 'withdrawQsr': + verbose + ? print( + 'Description: Withdraw deposited QSR from the pillar contract') + : null; + await _withdrawQsr(); + return; + + default: + invalidCommand(); + } +} + +Future _list() async { + PillarInfoList pillarList = await znnClient.embedded.pillar.getAll(); + for (PillarInfo pillar in pillarList.list) { + print( + '#${pillar.rank + 1} Pillar ${green(pillar.name)} has a delegated weight of ${AmountUtils.addDecimals(pillar.weight, coinDecimals)} ${green('ZNN')}'); + print(' Producer address ${pillar.producerAddress}'); + print( + ' Momentums ${pillar.currentStats.producedMomentums} / expected ${pillar.currentStats.expectedMomentums}'); + } +} + +Future _register() async { + if (args.length != 6) { + print('Incorrect number of arguments. Expected:'); + print( + 'pillar.register name producerAddress rewardAddress giveBlockRewardPercentage giveDelegateRewardPercentage'); + return; + } + + Address producerAddress = parseAddress(args[2]); + Address rewardAddress = parseAddress(args[3]); + int giveBlockRewardPercentage = int.parse(args[4]); + int giveDelegateRewardPercentage = int.parse(args[5]); + + if (!assertUserAddress(producerAddress) || + !assertUserAddress(rewardAddress)) { + return; + } + + AccountInfo balance = await znnClient.ledger.getAccountInfoByAddress(address); + BigInt qsrAmount = await znnClient.embedded.pillar.getQsrRegistrationCost(); + BigInt depositedQsr = + await znnClient.embedded.pillar.getDepositedQsr(address); + if ((balance.znn()! < pillarRegisterZnnAmount || + balance.qsr()! < qsrAmount) && + qsrAmount > depositedQsr) { + print('Cannot register Pillar with address ${address.toString()}'); + print( + 'Required ${AmountUtils.addDecimals(pillarRegisterZnnAmount, coinDecimals)} ${green('ZNN')} and ${AmountUtils.addDecimals(qsrAmount, coinDecimals)} ${blue('QSR')}'); + print( + 'Available ${AmountUtils.addDecimals(balance.znn()!, coinDecimals)} ${green('ZNN')} and ${AmountUtils.addDecimals(balance.qsr()!, coinDecimals)} ${blue('QSR')}'); + return; + } + + print( + 'Creating a new ${green('Pillar')} will burn the deposited ${blue('QSR')} required for the Pillar slot'); + if (!confirm('Do you want to proceed?', defaultValue: false)) return; + + String newName = args[1]; + bool ok = await znnClient.embedded.pillar.checkNameAvailability(newName); + while (!ok) { + newName = ask( + 'This Pillar name is already reserved. Please choose another name for the Pillar'); + ok = await znnClient.embedded.pillar.checkNameAvailability(newName); + } + if (depositedQsr < qsrAmount) { + print( + 'Depositing ${AmountUtils.addDecimals(qsrAmount - depositedQsr, coinDecimals)} ${blue('QSR')} for the Pillar registration'); + await znnClient + .send(znnClient.embedded.pillar.depositQsr(qsrAmount - depositedQsr)); + } + print('Registering Pillar ...'); + await send(znnClient.embedded.pillar.register( + newName, + producerAddress, + rewardAddress, + giveBlockRewardPercentage, + giveDelegateRewardPercentage)); + print('Done'); + print( + 'Check after 2 momentums if the Pillar was successfully registered using ${green('pillar.list')} command'); +} + +Future _revoke() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('pillar.revoke name'); + return; + } + PillarInfoList pillarList = await znnClient.embedded.pillar.getAll(); + bool ok = false; + for (PillarInfo pillar in pillarList.list) { + if (args[1].compareTo(pillar.name) == 0) { + ok = true; + if (pillar.isRevocable) { + print('Revoking Pillar ${pillar.name} ...'); + await send(znnClient.embedded.pillar.revoke(args[1])); + print( + 'Use ${green('receiveAll')} to collect back the locked amount of ${green('ZNN')}'); + } else { + print( + 'Cannot revoke Pillar ${pillar.name}. Revocation window will open in ${formatDuration(pillar.revokeCooldown)}'); + } + } + } + if (ok) { + print('Done'); + } else { + print('There is no Pillar with this name'); + } +} + +Future _delegate() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('pillar.delegate name'); + return; + } + print('Delegating to Pillar ${args[1]} ...'); + await send(znnClient.embedded.pillar.delegate(args[1])); + print('Done'); +} + +Future _undelegate() async { + print('Undelegating ...'); + await send(znnClient.embedded.pillar.undelegate()); + print('Done'); +} + +Future _collect() async { + await send(znnClient.embedded.pillar.collectReward()); + print('Done'); + print( + 'Use ${green('receiveAll')} to collect your Pillar reward(s) after 1 momentum'); +} + +Future _depositQsr() async { + AccountInfo balance = await znnClient.ledger.getAccountInfoByAddress(address); + BigInt qsrAmount = await znnClient.embedded.pillar.getQsrRegistrationCost(); + BigInt depositedQsr = + await znnClient.embedded.pillar.getDepositedQsr(address); + + print( + 'You have ${AmountUtils.addDecimals(depositedQsr, coinDecimals)} / ${AmountUtils.addDecimals(qsrAmount, coinDecimals)} ${blue('QSR')} deposited for the Pillar registration'); + + if (balance.qsr()! < qsrAmount) { + print( + 'Required ${AmountUtils.addDecimals(qsrAmount, coinDecimals)} ${blue('QSR')}'); + print( + 'Available ${AmountUtils.addDecimals(balance.qsr()!, coinDecimals)} ${blue('QSR')}'); + return; + } + + if (depositedQsr < qsrAmount) { + print( + 'Depositing ${AmountUtils.addDecimals(qsrAmount - depositedQsr, coinDecimals)} ${blue('QSR')} for the Pillar registration'); + await znnClient + .send(znnClient.embedded.pillar.depositQsr(qsrAmount - depositedQsr)); + } + print('Done'); +} + +Future _withdrawQsr() async { + BigInt depositedQsr = + await znnClient.embedded.pillar.getDepositedQsr(address); + if (depositedQsr == BigInt.zero) { + print('No deposited ${blue('QSR')} to withdraw'); + return; + } + print( + 'Withdrawing ${AmountUtils.addDecimals(depositedQsr, coinDecimals)} ${blue('QSR')} ...'); + await send(znnClient.embedded.pillar.withdrawQsr()); + print('Done'); +} diff --git a/lib/plasma.dart b/lib/plasma.dart new file mode 100644 index 0000000..c9d07cf --- /dev/null +++ b/lib/plasma.dart @@ -0,0 +1,166 @@ +import 'package:dcli/dcli.dart' hide verbose; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +void plasmaMenu() { + print(' ${white('Plasma')}'); + print(' plasma.list [pageIndex pageCount]'); + print(' plasma.get address'); + print(' plasma.fuse toAddress amount (in ${blue('QSR')})'); + print(' plasma.cancel id'); +} + +Future plasmaFunctions() async { + if (args[0].split('.').isEmpty) { + plasmaMenu(); + return; + } + + switch (args[0].split('.')[1]) { + case 'list': + verbose ? print('Description: List plasma fusion entries') : null; + await _list(); + break; + + case 'get': + verbose + ? print( + 'Description: Display the amount of plasma and QSR fused for an address') + : null; + await _get(); + break; + + case 'fuse': + verbose + ? print('Description: Fuse QSR to an address to generate plasma') + : null; + await _fuse(); + break; + + case 'cancel': + verbose + ? print( + 'Description: Cancel a plasma fusion and receive the QSR back') + : null; + await _cancel(); + break; + + default: + invalidCommand(); + } +} + +Future _list() async { + if (!(args.length == 1 || args.length == 3)) { + print('Incorrect number of arguments. Expected:'); + print('plasma.list [pageIndex pageSize]'); + return; + } + int pageIndex = 0; + int pageSize = 25; + if (args.length == 3) { + pageIndex = int.parse(args[1]); + pageSize = int.parse(args[2]); + } + if (!areValidPageVars(pageIndex, pageSize)) { + return; + } + + FusionEntryList fusionEntryList = await znnClient.embedded.plasma + .getEntriesByAddress(address, pageIndex: pageIndex, pageSize: pageSize); + + if (fusionEntryList.count > 0) { + print( + 'Fusing ${AmountUtils.addDecimals(fusionEntryList.qsrAmount, coinDecimals)} ${blue('QSR')} for Plasma in ${fusionEntryList.count} entries'); + } else { + print('No Plasma fusion entries found'); + } + + for (FusionEntry entry in fusionEntryList.list) { + print( + ' ${AmountUtils.addDecimals(entry.qsrAmount, coinDecimals)} ${blue('QSR')} for ${entry.beneficiary.toString()}'); + print( + 'Can be canceled at momentum height: ${entry.expirationHeight}. Use id ${entry.id} to cancel'); + } +} + +Future _get() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('plasma.get address'); + return; + } + Address address = parseAddress(args[1]); + PlasmaInfo plasmaInfo = await znnClient.embedded.plasma.get(address); + print( + '${green(address.toString())} has ${plasmaInfo.currentPlasma} / ${plasmaInfo.maxPlasma}' + ' plasma with ${AmountUtils.addDecimals(plasmaInfo.qsrAmount, coinDecimals)} ${blue('QSR')} fused.'); +} + +Future _fuse() async { + if (args.length != 3) { + print('Incorrect number of arguments. Expected:'); + print('plasma.fuse toAddress amount (in ${blue('QSR')})'); + return; + } + Address beneficiary = parseAddress(args[1]); + BigInt amount = AmountUtils.extractDecimals(args[2], coinDecimals); + + if (!assertUserAddress(beneficiary)) { + return; + } + + if (amount < fuseMinQsrAmount) { + print( + '${red('Invalid amount')}: ${AmountUtils.addDecimals(amount, coinDecimals)} ${blue('QSR')}. Minimum staking amount is ${AmountUtils.addDecimals(fuseMinQsrAmount, coinDecimals)}'); + return; + } else if (amount % BigInt.from(oneQsr) != BigInt.zero) { + print('${red('Error!')} Amount has to be integer'); + return; + } + print( + 'Fusing ${AmountUtils.addDecimals(amount, coinDecimals)} ${blue('QSR')} to ${args[1]}'); + await send(znnClient.embedded.plasma.fuse(beneficiary, amount)); + print('Done'); +} + +Future _cancel() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('plasma.cancel id'); + return; + } + Hash id = parseHash(args[1]); + + int pageIndex = 0; + bool found = false; + bool gotError = false; + + FusionEntryList fusions = + await znnClient.embedded.plasma.getEntriesByAddress(address); + while (fusions.list.isNotEmpty) { + var index = fusions.list.indexWhere((entry) => entry.id == id); + if (index != -1) { + found = true; + if (fusions.list[index].expirationHeight > + (await znnClient.ledger.getFrontierMomentum()).height) { + print('${red('Error!')} Fuse entry can not be cancelled yet'); + gotError = true; + } + } + pageIndex++; + fusions = await znnClient.embedded.plasma + .getEntriesByAddress(address, pageIndex: pageIndex); + } + + if (!found) { + print('${red('Error!')} Fuse entry was not found'); + return; + } + if (gotError) { + return; + } + print('Canceling Plasma fuse entry with id ${args[1]}'); + await send(znnClient.embedded.plasma.cancel(id)); + print('Done'); +} diff --git a/lib/sentinel.dart b/lib/sentinel.dart new file mode 100644 index 0000000..0c03538 --- /dev/null +++ b/lib/sentinel.dart @@ -0,0 +1,192 @@ +import 'package:dcli/dcli.dart' hide verbose; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +void sentinelMenu() { + print(' ${white('Sentinel')}'); + print(' sentinel.list'); + print(' sentinel.register'); + print(' sentinel.revoke'); + print(' sentinel.collect'); + print(' sentinel.depositQsr'); + print(' sentinel.withdrawQsr'); +} + +Future sentinelFunctions() async { + switch (args[0].split('.')[1]) { + case 'list': + verbose ? print('Description: List all sentinels') : null; + await _list(); + return; + + case 'register': + verbose ? print('Description: Register a sentinel') : null; + await _register(); + return; + + case 'revoke': + verbose ? print('Description: Revoke a sentinel') : null; + await _revoke(); + return; + + case 'collect': + verbose ? print('Description: Collect sentinel rewards') : null; + await _collect(); + return; + + case 'depositQsr': + verbose + ? print('Description: Deposit QSR to the sentinel contract') + : null; + await _depositQsr(); + return; + + case 'withdrawQsr': + verbose + ? print( + 'Description: Withdraw deposited QSR from the sentinel contract') + : null; + await _withdrawQsr(); + return; + + default: + invalidCommand(); + } +} + +Future _list() async { + if (args.length != 1) { + print('Incorrect number of arguments. Expected:'); + print('sentinel.list'); + return; + } + SentinelInfoList sentinels = + (await znnClient.embedded.sentinel.getAllActive()); + bool one = false; + for (SentinelInfo entry in sentinels.list) { + if (entry.owner.toString() == address.toString()) { + if (entry.isRevocable) { + print( + 'Revocation window will close in ${formatDuration(entry.revokeCooldown)}'); + } else { + print( + 'Revocation window will open in ${formatDuration(entry.revokeCooldown)}'); + } + one = true; + } + } + if (!one) { + print('No Sentinel registered at address ${address.toString()}'); + } +} + +Future _register() async { + if (args.length != 1) { + print('Incorrect number of arguments. Expected:'); + print('sentinel.register'); + return; + } + AccountInfo accountInfo = + await znnClient.ledger.getAccountInfoByAddress(address); + var depositedQsr = await znnClient.embedded.sentinel.getDepositedQsr(address); + print('You have ${AmountUtils.addDecimals(depositedQsr, coinDecimals)} ${blue('QSR')} deposited for the Sentinel'); + if (accountInfo.znn()! < sentinelRegisterZnnAmount || + accountInfo.qsr()! < sentinelRegisterQsrAmount) { + print('Cannot register Sentinel with address ${address.toString()}'); + print( + 'Required ${AmountUtils.addDecimals(sentinelRegisterZnnAmount, coinDecimals)} ${green('ZNN')} and ${AmountUtils.addDecimals(sentinelRegisterQsrAmount, coinDecimals)} ${blue('QSR')}'); + print( + 'Available ${AmountUtils.addDecimals(accountInfo.znn()!, coinDecimals)} ${green('ZNN')} and ${AmountUtils.addDecimals(accountInfo.qsr()!, coinDecimals)} ${blue('QSR')}'); + return; + } + + if (depositedQsr < sentinelRegisterQsrAmount) { + await send(znnClient.embedded.sentinel + .depositQsr(sentinelRegisterQsrAmount - depositedQsr)); + } + await send(znnClient.embedded.sentinel.register()); + print('Done'); + print( + 'Check after 2 momentums if the Sentinel was successfully registered using ${green('sentinel.list')} command'); +} + +Future _revoke() async { + if (args.length != 1) { + print('Incorrect number of arguments. Expected:'); + print('sentinel.revoke'); + return; + } + SentinelInfo? entry = + await znnClient.embedded.sentinel.getByOwner(address).catchError((e) { + if (e.toString().contains('data non existent')) { + return null; + } else { + print('Error: ${e.toString()}'); + } + }); + + if (entry == null) { + print('No Sentinel found for address ${address.toString()}'); + return; + } + + if (entry.isRevocable == false) { + print( + 'Cannot revoke Sentinel. Revocation window will open in ${formatDuration(entry.revokeCooldown)}'); + return; + } + + await send(znnClient.embedded.sentinel.revoke()); + print('Done'); + print( + 'Use ${green('receiveAll')} to collect back the locked amount of ${green('ZNN')} and ${blue('QSR')}'); +} + +Future _collect() async { + if (args.length != 1) { + print('Incorrect number of arguments. Expected:'); + print('sentinel.collect'); + return; + } + await send(znnClient.embedded.sentinel.collectReward()); + print('Done'); + print( + 'Use ${green('receiveAll')} to collect your Sentinel reward(s) after 1 momentum'); +} + +Future _depositQsr() async { + AccountInfo balance = await znnClient.ledger.getAccountInfoByAddress(address); + BigInt depositedQsr = + await znnClient.embedded.sentinel.getDepositedQsr(address); + print( + 'You have ${AmountUtils.addDecimals(depositedQsr, coinDecimals)} / ${AmountUtils.addDecimals(sentinelRegisterQsrAmount, coinDecimals)} ${blue('QSR')} deposited for the Sentinel'); + + if (balance.qsr()! < sentinelRegisterQsrAmount) { + print( + 'Required ${AmountUtils.addDecimals(sentinelRegisterQsrAmount, coinDecimals)} ${blue('QSR')}'); + print( + 'Available ${AmountUtils.addDecimals(balance.qsr()!, coinDecimals)} ${blue('QSR')}'); + return; + } + + if (depositedQsr < sentinelRegisterQsrAmount) { + print( + 'Depositing ${AmountUtils.addDecimals(sentinelRegisterQsrAmount - depositedQsr, coinDecimals)} ${blue('QSR')} for the Sentinel'); + await send(znnClient.embedded.sentinel + .depositQsr(sentinelRegisterQsrAmount - depositedQsr)); + } + print('Done'); +} + +Future _withdrawQsr() async { + BigInt depositedQsr = + await znnClient.embedded.sentinel.getDepositedQsr(address); + if (depositedQsr == BigInt.zero) { + print('No deposited ${blue('QSR')} to withdraw'); + return; + } + print( + 'Withdrawing ${AmountUtils.addDecimals(depositedQsr, coinDecimals)} ${blue('QSR')} ...'); + await send(znnClient.embedded.sentinel.withdrawQsr()); + print('Done'); +} diff --git a/lib/spork.dart b/lib/spork.dart new file mode 100644 index 0000000..b78f351 --- /dev/null +++ b/lib/spork.dart @@ -0,0 +1,109 @@ +import 'package:dcli/dcli.dart' hide verbose; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +void sporkMenu() { + print(' ${white('Spork')}'); + print(' spork.list'); + print(' spork.create name description'); + print(' spork.activate id'); +} + +Future sporkFunctions() async { + switch (args[0].split('.')[1]) { + case 'list': + verbose ? print('Description: List all sporks') : null; + await _list(); + return; + + case 'create': + verbose ? print('Description: Create a new spork') : null; + await _create(); + return; + + case 'activate': + verbose ? print('Description: Activate a spork') : null; + await _activate(); + return; + + default: + invalidCommand(); + } +} + +Future _list() async { + if (!(args.length == 1 || args.length == 3)) { + print('Incorrect number of arguments. Expected:'); + print('spork.list [pageIndex pageSize]'); + return; + } + int pageIndex = 0; + int pageSize = rpcMaxPageSize; + if (args.length == 3) { + pageIndex = int.parse(args[1]); + pageSize = int.parse(args[2]); + } + if (!areValidPageVars(pageIndex, pageSize)) { + return; + } + + SporkList sporks = await znnClient.embedded.spork + .getAll(pageIndex: pageIndex, pageSize: pageSize); + if (sporks.list.isNotEmpty) { + print('Sporks:'); + for (Spork spork in sporks.list) { + print('Name: ${spork.name}'); + print(' Description: ${spork.description}'); + print(' Activated: ${spork.activated}'); + if (spork.activated) { + print(' EnforcementHeight: ${spork.enforcementHeight}'); + } + print(' Hash: ${spork.id}'); + } + } else { + print('No sporks found'); + } +} + +Future _create() async { + if (args.length != 3) { + print('Incorrect number of arguments. Expected:'); + print('spork.create name description'); + return; + } + + String name = args[1]; + String description = args[2]; + + if (name.length < sporkNameMinLength || name.length > sporkNameMaxLength) { + print( + '${red('Error!')} Spork name must be $sporkNameMinLength to $sporkNameMaxLength characters in length'); + return; + } + if (description.isEmpty) { + print('${red('Error!')} Spork description cannot be empty'); + return; + } + if (description.length > sporkDescriptionMaxLength) { + print( + '${red('Error!')} Spork description cannot exceed $sporkDescriptionMaxLength characters in length'); + return; + } + + print('Creating spork...'); + await send(znnClient.embedded.spork.createSpork(name, description)); + print('Done'); +} + +Future _activate() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('spork.activate id'); + return; + } + + Hash id = parseHash(args[1]); + print('Activating spork...'); + await send(znnClient.embedded.spork.activateSpork(id)); + print('Done'); +} diff --git a/lib/staking.dart b/lib/staking.dart new file mode 100644 index 0000000..fd718ff --- /dev/null +++ b/lib/staking.dart @@ -0,0 +1,159 @@ +import 'package:dcli/dcli.dart' hide verbose; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +void stakingMenu() { + print(' ${white('Staking')}'); + print(' stake.list [pageIndex pageCount]'); + print(' stake.register amount duration (in months)'); + print(' stake.revoke id'); + print(' stake.collect'); +} + +Future stakingFunctions() async { + switch (args[0].split('.')[1]) { + case 'list': + verbose ? print('Description: List all stakes') : null; + await _list(); + return; + + case 'register': + verbose ? print('Description: Register stake') : null; + await _register(); + return; + + case 'revoke': + verbose ? print('Description: Revoke stake') : null; + await _revoke(); + return; + + case 'collect': + verbose ? print('Description: Collect staking rewards') : null; + await _collect(); + return; + + default: + invalidCommand(); + } +} + +Future _list() async { + if (!(args.length == 1 || args.length == 3)) { + print('Incorrect number of arguments. Expected:'); + print(' stake.list [pageIndex pageSize]'); + return; + } + int pageIndex = 0; + int pageSize = 25; + if (args.length == 3) { + pageIndex = int.parse(args[1]); + pageSize = int.parse(args[2]); + } + if (!areValidPageVars(pageIndex, pageSize)) { + return; + } + + final currentTime = (DateTime.now().millisecondsSinceEpoch / 1000).round(); + StakeList stakeList = await znnClient.embedded.stake + .getEntriesByAddress(address, pageIndex: pageIndex, pageSize: pageSize); + + if (stakeList.count > 0) { + print( + 'Showing ${stakeList.list.length} out of a total of ${stakeList.count} staking entries'); + } else { + print('No staking entries found'); + } + + for (StakeEntry entry in stakeList.list) { + print( + 'Stake id ${entry.id.toString()} with amount ${AmountUtils.addDecimals(entry.amount, coinDecimals)} ${green('ZNN')}'); + if (entry.expirationTimestamp > currentTime) { + print( + ' Can be revoked in ${formatDuration(entry.expirationTimestamp - currentTime)}'); + } else { + print(' ${green('Can be revoked now')}'); + } + } +} + +Future _register() async { + if (args.length != 3) { + print('Incorrect number of arguments. Expected:'); + print('stake.register amount duration (in months)'); + return; + } + BigInt amount = AmountUtils.extractDecimals(args[1], coinDecimals); + final duration = int.parse(args[2]); + if (duration < 1 || duration > 12) { + print( + '${red('Invalid duration')}: ($duration) $stakeUnitDurationName. It must be between 1 and 12'); + return; + } + if (amount < stakeMinZnnAmount) { + print( + '${red('Invalid amount')}: ${AmountUtils.addDecimals(amount, coinDecimals)} ${green('ZNN')}. Minimum staking amount is ${AmountUtils.addDecimals(stakeMinZnnAmount, coinDecimals)}'); + return; + } + AccountInfo balance = await znnClient.ledger.getAccountInfoByAddress(address); + if (balance.znn()! < amount) { + print(red('Not enough ZNN to stake')); + return; + } + + print( + 'Staking ${AmountUtils.addDecimals(amount, coinDecimals)} ${green('ZNN')} for $duration $stakeUnitDurationName(s)'); + await send( + znnClient.embedded.stake.stake(stakeTimeUnitSec * duration, amount)); + print('Done'); +} + +Future _revoke() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('stake.revoke id'); + return; + } + Hash hash = parseHash(args[1]); + + final currentTime = (DateTime.now().millisecondsSinceEpoch / 1000).round(); + int pageIndex = 0; + bool one = false; + bool gotError = false; + + StakeList entries = await znnClient.embedded.stake + .getEntriesByAddress(address, pageIndex: pageIndex); + while (entries.list.isNotEmpty) { + for (StakeEntry entry in entries.list) { + if (entry.id.toString() == hash.toString()) { + if (entry.expirationTimestamp > currentTime) { + print( + '${red('Cannot revoke!')} Try again in ${formatDuration(entry.expirationTimestamp - currentTime)}'); + gotError = true; + } + one = true; + } + } + pageIndex++; + entries = await znnClient.embedded.stake + .getEntriesByAddress(address, pageIndex: pageIndex); + } + + if (gotError) { + return; + } else if (!one) { + print('${red('Error!')} No stake entry found with id ${hash.toString()}'); + return; + } + + await send(znnClient.embedded.stake.cancel(hash)); + print('Done'); + print( + 'Use ${green('receiveAll')} to collect your stake amount and uncollected reward(s) after 2 momentums'); +} + +Future _collect() async { + await send(znnClient.embedded.stake.collectReward()); + print('Done'); + print( + 'Use ${green('receiveAll')} to collect your stake reward(s) after 1 momentum'); +} diff --git a/lib/stats.dart b/lib/stats.dart new file mode 100644 index 0000000..08948d3 --- /dev/null +++ b/lib/stats.dart @@ -0,0 +1,85 @@ +import 'dart:math'; + +import 'package:dcli/dcli.dart' hide verbose; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +void statsMenu() { + print(' ${white('Stats')}'); + print(' stats.networkInfo'); + print(' stats.osInfo'); + print(' stats.processInfo'); + print(' stats.syncInfo'); +} + +Future statsFunctions() async { + switch (args[0].split('.')[1]) { + case 'networkInfo': + verbose ? print('Description: Get the network info') : null; + await _networkInfo(); + return; + + case 'osInfo': + verbose ? print('Description: Get the os info') : null; + await _osInfo(); + return; + + case 'processInfo': + verbose ? print('Description: Get the process info') : null; + await _processInfo(); + return; + + case 'syncInfo': + verbose ? print('Description: Get the sync info') : null; + await _syncInfo(); + return; + + default: + invalidCommand(); + } +} + +Future _networkInfo() async { + NetworkInfo networkInfo = await znnClient.stats.networkInfo(); + print('numPeers: ${networkInfo.numPeers}'); + for (var peer in networkInfo.peers) { + print(' publicKey: ${peer.publicKey}'); + print(' ip: ${peer.ip}'); + } + //print('self.ip: ${networkInfo.self.ip}'); + print('self.publicKey: ${networkInfo.self.publicKey}'); +} + +Future _osInfo() async { + OsInfo osInfo = (await znnClient.stats.osInfo()); + print('os: ${osInfo.os}'); + print('platform: ${osInfo.platform}'); + print('platformFamily: ${osInfo.platformFamily}'); + print('platformVersion: ${osInfo.platformVersion}'); + print('kernelVersion: ${osInfo.kernelVersion}'); + print( + 'memoryTotal: ${osInfo.memoryTotal} (${formatMemory(osInfo.memoryTotal)})'); + print( + 'memoryFree: ${osInfo.memoryFree} (${formatMemory(osInfo.memoryFree)})'); + print('numCPU: ${osInfo.numCPU}'); + print('numGoroutine: ${osInfo.numGoroutine}'); +} + +Future _processInfo() async { + ProcessInfo processInfo = await znnClient.stats.processInfo(); + print('version: ${processInfo.version}'); + print('commit: ${processInfo.commit}'); +} + +Future _syncInfo() async { + SyncInfo syncInfo = await znnClient.stats.syncInfo(); + print('state: ${syncInfo.state.name} (${syncInfo.state.index})'); + print('currentHeight: ${syncInfo.currentHeight}'); + print('targetHeight: ${syncInfo.targetHeight}'); +} + +String formatMemory(size) { + var i = size == 0 ? 0 : (log(size) / log(1024)).floor(); + return ((size / pow(1024, i)) * 1).toStringAsFixed(2) + + ' ${['B', 'kB', 'MB', 'GB', 'TB'][i]}'; +} diff --git a/lib/token.dart b/lib/token.dart new file mode 100644 index 0000000..721b9b2 --- /dev/null +++ b/lib/token.dart @@ -0,0 +1,356 @@ +import 'dart:io'; + +import 'package:dcli/dcli.dart' hide verbose; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +void tokenMenu() { + print(' ${white('ZTS Tokens')}'); + print(' token.list [pageIndex pageCount]'); + print(' token.getByStandard tokenStandard'); + print(' token.getByOwner ownerAddress'); + print( + ' token.issue name symbol domain totalSupply maxSupply decimals isMintable isBurnable isUtility'); + print(' token.mint tokenStandard amount receiveAddress'); + print(' token.burn tokenStandard amount'); + print(' token.transferOwnership tokenStandard newOwnerAddress'); + print(' token.disableMint tokenStandard'); +} + +Future tokenFunctions() async { + switch (args[0].split('.')[1]) { + case 'list': + verbose ? print('Description: List all tokens') : null; + await _list(); + return; + + case 'getByStandard': + verbose ? print('Description: List tokens by standard') : null; + await _getByStandard(); + return; + + case 'getByOwner': + verbose ? print('Description: List tokens by owner') : null; + await _getByOwner(); + return; + + case 'issue': + verbose ? print('Description: Issue token') : null; + await _issue(); + return; + + case 'mint': + verbose ? print('Description: Mint token') : null; + await _mint(); + return; + + case 'burn': + verbose ? print('Description: Burn token') : null; + await _burn(); + return; + + case 'transferOwnership': + verbose + ? print('Description: Transfer token ownership to another address') + : null; + await _transferOwnership(); + return; + + case 'disableMint': + verbose + ? print('Description: Disable a token\'s minting capability') + : null; + await _disableMint(); + return; + + default: + invalidCommand(); + } +} + +Future _list() async { + if (!(args.length == 1 || args.length == 3)) { + print('Incorrect number of arguments. Expected:'); + print('token.list [pageIndex pageSize]'); + return; + } + int pageIndex = 0; + int pageSize = 25; + if (args.length == 3) { + pageIndex = int.parse(args[1]); + pageSize = int.parse(args[2]); + } + if (!areValidPageVars(pageIndex, pageSize)) { + return; + } + TokenList tokenList = await znnClient.embedded.token + .getAll(pageIndex: pageIndex, pageSize: pageSize); + for (Token token in tokenList.list!) { + Function color = getColor(token.tokenStandard); + String issuance = 'Created'; + if (token.tokenStandard.toString() != qsrTokenStandard && + token.tokenStandard.toString() != znnTokenStandard) { + stdout.write('Token '); + issuance = 'Issued'; + } + print( + '${color(token.name)} with symbol ${color(token.symbol)} and standard ${color(token.tokenStandard.toString())}'); + print(' $issuance by ${color(token.owner.toString())}'); + print( + ' ${color(token.name)} has ${token.decimals} decimals, ${token.isMintable ? 'is mintable' : 'is not mintable'}, ${token.isBurnable ? 'can be burned' : 'cannot be burned'}, and ${token.isUtility ? 'is a utility coin' : 'is not a utility coin'}'); + print( + ' The total supply is ${AmountUtils.addDecimals(token.totalSupply, token.decimals)} and the maximum supply is ${AmountUtils.addDecimals(token.maxSupply, token.decimals)}'); + print(' Domain `${token.domain}`'); + } +} + +Future _getByStandard() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('token.getByStandard tokenStandard'); + return; + } + TokenStandard tokenStandard = getTokenStandard(args[1]); + Token token = await getToken(tokenStandard); + String type = 'Token'; + if (token.tokenStandard.toString() == qsrTokenStandard || + token.tokenStandard.toString() == znnTokenStandard) { + type = 'Coin'; + } + Function color = getColor(token.tokenStandard); + print( + '${color(type)} ${token.name} with symbol ${color(token.symbol)} and standard ${color(token.tokenStandard.toString())}'); + print(' Created by ${green(token.owner.toString())}'); + print( + ' The total supply is ${AmountUtils.addDecimals(token.totalSupply, token.decimals)} and a maximum supply is ${AmountUtils.addDecimals(token.maxSupply, token.decimals)}'); + print( + ' The token has ${token.decimals} decimals ${token.isMintable ? 'can be minted' : 'cannot be minted'} and ${token.isBurnable ? 'can be burned' : 'cannot be burned'}'); +} + +Future _getByOwner() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('token.getByOwner ownerAddress'); + return; + } + String type = 'Token'; + Address ownerAddress = parseAddress(args[1]); + TokenList tokens = await znnClient.embedded.token.getByOwner(ownerAddress); + for (Token token in tokens.list!) { + type = 'Token'; + if (token.tokenStandard.toString() == znnTokenStandard || + token.tokenStandard.toString() == qsrTokenStandard) { + type = 'Coin'; + } + Function color = getColor(token.tokenStandard); + print( + '${color(type)} ${token.name} with symbol ${color(token.symbol)} and standard ${color(token.tokenStandard.toString())}'); + print(' Created by ${green(token.owner.toString())}'); + print( + ' The total supply is ${AmountUtils.addDecimals(token.totalSupply, token.decimals)} and a maximum supply is ${AmountUtils.addDecimals(token.maxSupply, token.decimals)}'); + print( + ' The token ${token.decimals} decimals ${token.isMintable ? 'can be minted' : 'cannot be minted'} and ${token.isBurnable ? 'can be burned' : 'cannot be burned'}'); + } +} + +Future _issue() async { + if (args.length != 10) { + print('Incorrect number of arguments. Expected:'); + print( + 'token.issue name symbol domain totalSupply maxSupply decimals isMintable isBurnable isUtility'); + return; + } + + RegExp regExpName = RegExp(r'^([a-zA-Z0-9]+[-._]?)*[a-zA-Z0-9]$'); + if (!regExpName.hasMatch(args[1])) { + print('${red('Error!')} The ZTS name contains invalid characters'); + return; + } + + RegExp regExpSymbol = RegExp(r'^[A-Z0-9]+$'); + if (!regExpSymbol.hasMatch(args[2])) { + print('${red('Error!')} The ZTS symbol must be all uppercase'); + return; + } + + RegExp regExpDomain = + RegExp(r'^([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9]\.)+[A-Za-z]{2,}$'); + if (args[3].isEmpty || !regExpDomain.hasMatch(args[3])) { + print('${red('Error!')} Invalid domain'); + print('Examples of ${green('valid')} domain names:'); + print(' zenon.network'); + print(' www.zenon.network'); + print(' quasar.zenon.network'); + print(' zenon.community'); + print('Examples of ${red('invalid')} domain names:'); + print(' zenon.network/index.html'); + print(' www.zenon.network/quasar'); + return; + } + + if (args[1].isEmpty || args[1].length > 40) { + print( + '${red('Error!')} Invalid ZTS name length (min 1, max 40, current ${args[1].length})'); + return; + } + + if (args[2].isEmpty || args[2].length > 10) { + print( + '${red('Error!')} Invalid ZTS symbol length (min 1, max 10, current ${args[2].length})'); + return; + } + + if (args[3].length > 128) { + print( + '${red('Error!')} Invalid ZTS domain length (min 0, max 128, current ${args[3].length})'); + return; + } + + bool mintable; + if (args[7] == '0' || args[7] == 'false') { + mintable = false; + } else if (args[7] == '1' || args[7] == 'true') { + mintable = true; + } else { + print( + '${red('Error!')} Mintable flag variable of type "bool" should be provided as either "true", "false", "1" or "0"'); + return; + } + + bool burnable; + if (args[8] == '0' || args[8] == 'false') { + burnable = false; + } else if (args[8] == '1' || args[8] == 'true') { + burnable = true; + } else { + print( + '${red('Error!')} Burnable flag variable of type "bool" should be provided as either "true", "false", "1" or "0"'); + return; + } + + bool utility; + if (args[9] == '0' || args[9] == 'false') { + utility = false; + } else if (args[9] == '1' || args[9] == 'true') { + utility = true; + } else { + print( + '${red('Error!')} Utility flag variable of type "bool" should be provided as either "true", "false", "1" or "0"'); + return; + } + + int decimals = int.parse(args[6]); + BigInt totalSupply = AmountUtils.extractDecimals(args[4], decimals); + BigInt maxSupply = AmountUtils.extractDecimals(args[5], decimals); + + if (mintable == true) { + if (maxSupply < totalSupply) { + print( + '${red('Error!')} Max supply must to be larger than the total supply'); + return; + } + if (maxSupply >= BigInt.from(1 << 255)) { + print( + '${red('Error!')} Max supply must to be less than ${BigInt.from(1 << 255)}'); + return; + } + } else { + if (maxSupply != totalSupply) { + print( + '${red('Error!')} Max supply must be equal to totalSupply for non-mintable tokens'); + return; + } + if (totalSupply == BigInt.zero) { + print( + '${red('Error!')} Total supply cannot be "0" for non-mintable tokens'); + return; + } + } + + print('Issuing a new ${magenta('ZTS token')} will burn 1 ZNN'); + if (!confirm('Do you want to proceed?', defaultValue: false)) return; + + print('Issuing ${magenta(args[1])} ZTS token ...'); + await send(znnClient.embedded.token.issueToken(args[1], args[2], args[3], + totalSupply, maxSupply, decimals, mintable, burnable, utility)); + print('Done'); +} + +Future _mint() async { + if (args.length != 4) { + print('Incorrect number of arguments. Expected:'); + print('token.mint tokenStandard amount receiveAddress'); + return; + } + TokenStandard tokenStandard = getTokenStandard(args[1]); + Token token = await getToken(tokenStandard); + BigInt amount = AmountUtils.extractDecimals(args[2], token.decimals); + Address mintAddress = parseAddress(args[3]); + + if (token.isMintable == false) { + print('${red('Error!')} The token is not mintable'); + return; + } + + print('Minting ZTS token ...'); + await send( + znnClient.embedded.token.mintToken(tokenStandard, amount, mintAddress)); + print('Done'); +} + +Future _burn() async { + if (args.length != 3) { + print('Incorrect number of arguments. Expected:'); + print('token.burn tokenStandard amount'); + return; + } + TokenStandard tokenStandard = getTokenStandard(args[1]); + Token token = await getToken(tokenStandard); + BigInt amount = AmountUtils.extractDecimals(args[2], token.decimals); + + if (!await hasBalance(address, tokenStandard, amount)) { + return; + } + + print('Burning ${args[1]} ZTS token ...'); + await znnClient + .send(znnClient.embedded.token.burnToken(tokenStandard, amount)); + print('Done'); +} + +Future _transferOwnership() async { + if (args.length != 3) { + print('Incorrect number of arguments. Expected:'); + print('token.transferOwnership tokenStandard newOwnerAddress'); + return; + } + print('Transferring ZTS token ownership ...'); + TokenStandard tokenStandard = getTokenStandard(args[1]); + Address newOwnerAddress = parseAddress(args[2]); + Token token = await getToken(tokenStandard); + if (token.owner.toString() != address.toString()) { + print('${red('Error!')} Not owner of token ${args[1]}'); + return; + } + await send(znnClient.embedded.token.updateToken( + tokenStandard, newOwnerAddress, token.isMintable, token.isBurnable)); + print('Done'); +} + +Future _disableMint() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('token.disableMint tokenStandard'); + return; + } + print('Disabling ZTS token mintable flag ...'); + TokenStandard tokenStandard = getTokenStandard(args[1]); + Token token = await getToken(tokenStandard); + if (token.owner.toString() != address.toString()) { + print('${red('Error!')} Not owner of token ${args[1]}'); + return; + } + await send(znnClient.embedded.token + .updateToken(tokenStandard, token.owner, false, token.isBurnable)); + print('Done'); +} diff --git a/lib/utils/args.dart b/lib/utils/args.dart new file mode 100644 index 0000000..cbf8b52 --- /dev/null +++ b/lib/utils/args.dart @@ -0,0 +1,197 @@ +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:logging/logging.dart' as log; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +String argsUsage = ''; + +ArgParser parseArgs() { + final ArgParser argParser = ArgParser(); + + argParser.addOption('url', + abbr: 'u', + defaultsTo: 'ws://127.0.0.1:$defaultWsPort', + help: 'Provide a websocket $znnDaemon connection URL with a port'); + argParser.addOption('passphrase', + abbr: 'p', + help: + 'use this passphrase for the keyStore or enter it manually in a secure way'); + argParser.addOption('keyStore', + abbr: 'k', + defaultsTo: 'available keyStore if only one is present', + help: 'Select the local keyStore'); + argParser.addOption('index', + abbr: 'i', defaultsTo: '0', help: 'Address index'); + argParser.addOption('chain', + abbr: 'c', + defaultsTo: '1 (mainnet)', + help: 'Chain Identifier for the connected node'); + + // Flags + argParser.addFlag('verbose', + abbr: 'v', + negatable: false, + help: 'Prints detailed information about the action that it performs'); + argParser.addFlag('help', + abbr: 'h', negatable: false, help: 'Displays help information'); + argParser.addFlag('admin', + abbr: 'a', negatable: false, help: 'Displays admin functions'); + + return argParser; +} + +void handleFlags(ArgResults argResult) { + if (argResult['admin'] || (args.isNotEmpty && (args[0] == 'admin'))) { + adminHelp(); + exit(0); + } + + if (args.isEmpty || args[0] == 'help') { + fullMenu(); + exit(0); + } + + if (args.isNotEmpty && argResult['help']) { + handleHelp(); + } + + if (argResult.wasParsed('verbose')) { + log.hierarchicalLoggingEnabled = true; + logger.level = Level.ALL; + verbose = true; + } + + if (!(commandsWithoutWallet.contains(args[0]) || + commandsWithWallet.contains(args[0]) || + adminCommands.contains(args[0]))) { + invalidCommand(); + exit(-1); + } +} + +void handleHelp() { + String command = args[0].split('.')[0]; + + switch (command) { + case 'general': + generalMenu(); + exit(0); + case 'stats': + statsMenu(); + exit(0); + case 'plasma': + plasmaMenu(); + exit(0); + case 'sentinel': + sentinelMenu(); + exit(0); + case 'stake': + stakingMenu(); + exit(0); + case 'pillar': + pillarMenu(); + exit(0); + case 'token': + tokenMenu(); + exit(0); + case 'wallet': + walletMenu(); + exit(0); + case 'az': + acceleratorMenu(); + exit(0); + case 'spork': + sporkMenu(); + exit(0); + case 'htlc': + htlcMenu(); + exit(0); + case 'bridge': + bridgeMenu(); + exit(0); + case 'liquidity': + liquidityMenu(); + exit(0); + case 'orchestrator': + orchestratorMenu(); + exit(0); + case 'admin': + bridgeAdminMenu(); + liquidityAdminMenu(); + exit(0); + default: + break; + } +} + +Future handleCli() async { + List command = args[0].split('.'); + + if (command.length == 1) { + await generalFunctions(); + } else { + if (command[0].contains('plasma')) { + await plasmaFunctions(); + } else if (command[0].contains('stats')) { + await statsFunctions(); + } else if (command[0].contains('sentinel')) { + await sentinelFunctions(); + } else if (command[0].contains('stake')) { + await stakingFunctions(); + } else if (command[0].contains('pillar')) { + await pillarFunctions(); + } else if (command[0].contains('token')) { + await tokenFunctions(); + } else if (command[0].contains('wallet')) { + await walletFunctions(); + } else if (command[0].contains('az')) { + await acceleratorFunctions(); + } else if (command[0].contains('spork')) { + await sporkFunctions(); + } else if (command[0].contains('htlc')) { + await htlcFunctions(); + } else if (command[0].contains('bridge')) { + await bridgeFunctions(); + } else if (command[0].contains('liquidity')) { + await liquidityFunctions(); + } else if (command[0].contains('orchestrator')) { + await orchestatorFunctions(); + } else { + invalidCommand(); + } + } +} + +void header() { + print('USAGE:'); + print(' $znnCli [OPTIONS] [FLAGS]\n'); + print('FLAGS:'); + print('$argsUsage\n'); + print('OPTIONS:'); +} + +void fullMenu() { + header(); + generalMenu(); + statsMenu(); + plasmaMenu(); + sentinelMenu(); + stakingMenu(); + pillarMenu(); + tokenMenu(); + walletMenu(); + acceleratorMenu(); + sporkMenu(); + htlcMenu(); + bridgeMenu(); + liquidityMenu(); + orchestratorMenu(); +} + +void adminHelp() { + header(); + bridgeAdminMenu(); + liquidityAdminMenu(); +} diff --git a/lib/utils/formatUtils.dart b/lib/utils/formatUtils.dart new file mode 100644 index 0000000..75690b1 --- /dev/null +++ b/lib/utils/formatUtils.dart @@ -0,0 +1,114 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:dcli/dcli.dart'; +import 'package:path/path.dart' as path; +import 'package:znn_cli_dart/global.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +final znndConfigPath = File(path.join(znnDefaultDirectory.path, 'config.json')); + +String formatJSON(Map j) { + var spaces = ' ' * 4; + var encoder = JsonEncoder.withIndent(spaces); + return encoder.convert(j); +} + +String formatDuration(int seconds) { + format(Duration d) => d.toString().split('.').first.padLeft(8, '0'); + final duration = Duration(seconds: seconds); + return format(duration); +} + +Map parseJSON(String fileName) { + File file = File(fileName); + if (file.existsSync()) { + return json.decode(file.readAsStringSync()); + } else { + return {}; + } +} + +Map parseConfig() { + if (znndConfigPath.existsSync()) { + return json.decode(znndConfigPath.readAsStringSync()); + } else { + return {}; + } +} + +bool writeConfig(Map config) { + try { + znndConfigPath.writeAsStringSync(formatJSON(config)); + } on FileSystemException { + return false; + } + return true; +} + +TokenStandard getTokenStandard(String zts) { + try { + TokenStandard tokenStandard; + if (zts.toLowerCase() == 'znn') { + tokenStandard = znnZts; + } else if (zts.toLowerCase() == 'qsr') { + tokenStandard = qsrZts; + } else { + tokenStandard = TokenStandard.parse(zts); + } + return tokenStandard; + } catch (e) { + throw ('${red('Error!')} tokenStandard must be a valid token standard\nExamples: ${green('ZNN')}/${blue('QSR')}/${magenta('ZTS')}'); + } +} + +Future getToken(TokenStandard tokenStandard) async { + try { + Token? token = await znnClient.embedded.token.getByZts(tokenStandard); + return token!; + } catch (e) { + throw ('${red('Error!')} $tokenStandard does not exist'); + } +} + +Function getColor(TokenStandard tokenStandard) { + switch (tokenStandard.toString()) { + case znnTokenStandard: + return green; + case qsrTokenStandard: + return blue; + default: + return magenta; + } +} + +List generatePreimage([int length = htlcPreimageDefaultLength]) { + const maxInt = 256; + return List.generate(length, (i) => Random.secure().nextInt(maxInt)); +} + +ByteData combine(List values) { + try { + List> byteArrays = []; + values.map((e) => byteArrays.add(e.getBytes()!)).toList(); + + int offset = 0; + int length = 0; + for (List data in byteArrays) { + length += Uint8List.fromList(data).length; + } + ByteData bd = ByteData(length); + + for (List data in byteArrays) { + for (var byte in Uint8List.fromList(data)) { + bd.setUint8(offset, byte); + offset++; + } + } + return bd; + } catch (e) { + throw ('${red('Error!')} Error while reading address data'); + } +} diff --git a/lib/utils/inputUtils.dart b/lib/utils/inputUtils.dart new file mode 100644 index 0000000..ab266d3 --- /dev/null +++ b/lib/utils/inputUtils.dart @@ -0,0 +1,11 @@ +import 'dart:io'; + +import 'package:dcli/dcli.dart'; + +String? enterPassphrase() { + print('Insert passphrase:'); + stdin.echoMode = false; + String? passphrase = stdin.readLineSync(); + stdin.echoMode = true; + return passphrase; +} diff --git a/lib/utils/node.dart b/lib/utils/node.dart new file mode 100644 index 0000000..1659f93 --- /dev/null +++ b/lib/utils/node.dart @@ -0,0 +1,85 @@ +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:dcli/dcli.dart'; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; +import 'package:znn_ledger_dart/znn_ledger_dart.dart'; + +Future connectToNode(ArgResults argResult) async { + String? urlOption; + bool urlOptionSupplied = false; + + if (argResult.wasParsed('url') && validateWsConnectionURL(argResult['url'])) { + urlOption = argResult['url']; + urlOptionSupplied = true; + } else if (!validateWsConnectionURL(argResult['url'])) { + print('${red('Error!')} Malformed URL. Please try again'); + exit(-1); + } else { + urlOption = 'ws://127.0.0.1:$defaultWsPort'; + } + + if (!commandsWithoutConnection.contains(args[0]) || + urlOptionSupplied == true) { + await znnClient.wsClient.initialize(urlOption!, retry: false); + await selectChainId(argResult); + } +} + +Future selectChainId(ArgResults argResult) async { + if (argResult.wasParsed('chain')) { + String? chainOption = argResult['chain']; + if (chainOption != 'auto') { + chainId = int.parse(chainOption!); + } else { + await znnClient.ledger.getFrontierMomentum().then((value) { + chainId = value.chainIdentifier.toInt(); + }); + } + } +} + +Future send(AccountBlockTemplate blockTemplate, + [bool reconnect = false, int retries = 1]) async { + try { + if (reconnect) { + if (znnClient.defaultKeyStore is LedgerWallet) { + (znnClient.defaultKeyStore as LedgerWallet).disconnect(); + } + // Reconnect the wallet. + znnClient.defaultKeyStore = await znnClient.keyStoreManager + .getWallet(walletDefinition, walletOptions); + znnClient.defaultKeyPair = + await znnClient.defaultKeyStore!.getAccount(accountIndex); + } + return await znnClient.send(blockTemplate); + } catch (e) { + if (e is ResponseError) { + if (e.statusWord == StatusWord.unknownError) { + print( + '${red('Error!')} The Ledger ${walletDefinition.walletName} is not connected or unlocked.'); + } else if (e.statusWord == StatusWord.appIsNotOpen) { + print( + '${red('Error!')} The Zenon app is not open on the Ledger ${walletDefinition.walletName}.'); + } else if (e.statusWord == StatusWord.wrongResponseLength) { + // This happens when the Ledger device was opened by another process. + if (retries > 0) { + return await send(blockTemplate, true, retries - 1); + } + } + } else { + print('${red('Error!')} Failed to send transaction $e.'); + } + + if (retries > 0) { + if (confirm('Do you want to retry?', defaultValue: false)) { + return await send(blockTemplate, true); + } else { + exit(-1); + } + } + + rethrow; + } +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart new file mode 100644 index 0000000..649c077 --- /dev/null +++ b/lib/utils/utils.dart @@ -0,0 +1,6 @@ +export 'args.dart'; +export 'formatUtils.dart'; +export 'inputUtils.dart'; +export 'wallet.dart'; +export 'node.dart'; +export 'validationUtils.dart'; diff --git a/lib/utils/validationUtils.dart b/lib/utils/validationUtils.dart new file mode 100644 index 0000000..12eaaf4 --- /dev/null +++ b/lib/utils/validationUtils.dart @@ -0,0 +1,86 @@ +import 'package:dcli/dcli.dart'; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +Future hasBalance( + Address address, TokenStandard tokenStandard, BigInt amount) async { + AccountInfo info = await znnClient.ledger.getAccountInfoByAddress(address); + bool ok = true; + bool found = false; + + if (amount < BigInt.zero) { + print('${red('Error!')} Negative amount is not supported'); + return found; + } + + for (BalanceInfoListItem entry in info.balanceInfoList!) { + if (entry.token!.tokenStandard.toString() == tokenStandard.toString()) { + if (entry.balance! < amount) { + if (entry.balance == BigInt.zero) { + print('${red('Error!')} You do not have any ${entry.token!.symbol}'); + } else { + print( + '${red('Error!')} You only have ${AmountUtils.addDecimals(entry.balance!, entry.token!.decimals)} ${entry.token!.symbol} tokens'); + } + ok = false; + return false; + } + found = true; + } + } + + if (!found) { + print( + '${red('Error!')} You do not have any ${tokenStandard.toString()} tokens'); + return found; + } + return ok; +} + +bool areValidPageVars(int pageIndex, int pageSize) { + if (pageIndex < 0) { + print('${red('Error!')} The page index must be a positive integer'); + return false; + } + if (pageSize < 1 || pageSize > rpcMaxPageSize) { + print( + '${red('Error!')} The page size must be greater than 0 and less than or equal to $rpcMaxPageSize'); + return false; + } + return true; +} + +Hash parseHash(String hash) { + try { + return Hash.parse(hash); + } catch (e) { + throw ('${red('Error!')} $hash is not a valid hash'); + } +} + +Address parseAddress(String address) { + try { + return Address.parse(address); + } catch (e) { + throw ('${red('Error!')} $address is not a valid address'); + } +} + +TokenStandard parseTokenStandard(String zts) { + try { + return TokenStandard.parse(zts); + } catch (e) { + throw ('${red('Error!')} $zts is not a valid ZTS'); + } +} + +bool assertUserAddress(Address address) { + if (address.isEmbedded() || address == emptyAddress) { + print('${red('Invalid address')}: $address is not a user address'); + return false; + } + return true; +} + +void invalidCommand() => + print('${red('Error!')} Unrecognized command ${red(args[0])}'); diff --git a/lib/utils/wallet.dart b/lib/utils/wallet.dart new file mode 100644 index 0000000..79c7336 --- /dev/null +++ b/lib/utils/wallet.dart @@ -0,0 +1,112 @@ +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:dcli/dcli.dart'; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_ledger_dart/znn_ledger_dart.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +Future> getWalletDefinitions() async { + List>> futures = + walletManagers.map((manager) => manager.getWalletDefinitions()).toList(); + + List> listOfDefinitions = + await Future.wait(futures); + + // Combine all the iterables into a single list using fold or expand + // For example, using fold: + List combinedList = + listOfDefinitions.fold>( + [], + (previousList, element) => previousList..addAll(element), + ); + + return combinedList; +} + +Future unlockWallet(ArgResults argResult) async { + var walletDefinitions = await getWalletDefinitions(); + + if (walletDefinitions.isEmpty) { + // Make sure at least one wallet exists + print('${red('Error!')} No wallets founds'); + exit(-1); + } else if (argResult.wasParsed('keyStore')) { + String? walletName; + + if (argResult['keyStore'] == "nanos" || + argResult['keyStore'] == "nanosp" || + argResult['keyStore'] == "nanox" || + argResult['keyStore'] == "stax") { + walletName = (argResult['keyStore'].substring(0, 4) + + ' ' + + argResult['keyStore'].substring(4)) + .trim(); + } else { + walletName = argResult['keyStore']; + } + + if (!walletDefinitions + .any((x) => x.walletName.toLowerCase() == walletName!.toLowerCase())) { + print( + '${red('Error!')} The wallet ${argResult['keyStore']} does not exist'); + exit(-1); + } + + // Use user provided wallet: make sure it exists + walletDefinition = walletDefinitions.firstWhere( + (x) => x.walletName.toLowerCase() == walletName!.toLowerCase()); + } else if (walletDefinitions.length == 1) { + // In case there is just one wallet, use it by default + print( + 'Using the default wallet ${green(walletDefinitions.first.walletName)}'); + walletDefinition = walletDefinitions.first; + } else { + // Multiple wallets present, but none is selected: action required + print('${red('Error!')} Please provide a wallet name. ' + 'Use ${green('wallet.list')} to list all available wallets'); + exit(-1); + } + + if (walletDefinition is KeyStoreDefinition) { + String? passphrase; + if (argResult.wasParsed('passphrase')) { + passphrase = argResult['passphrase']; + } else { + passphrase = enterPassphrase(); + } + walletOptions = KeyStoreOptions(passphrase!); + } else { + walletOptions = null; + } + + if (argResult.wasParsed('index')) { + accountIndex = int.parse(argResult['index']); + } else { + accountIndex = 0; + } + + try { + for (var walletManager in walletManagers) { + if (await walletManager.supportsWallet(walletDefinition)) { + znnClient.defaultKeyStorePath = walletDefinition; + znnClient.keyStoreManager = walletManager; + znnClient.defaultKeyStore = await znnClient.keyStoreManager + .getWallet(walletDefinition, walletOptions); + znnClient.defaultKeyPair = + await znnClient.defaultKeyStore!.getAccount(accountIndex); + address = (await znnClient.defaultKeyPair!.getAddress()); + logger.info('Using address ${green(address.toString())}'); + break; + } + } + } on IncorrectPasswordException { + print( + '${red('Error!')} Invalid passphrase for wallet ${walletDefinition.walletName}'); + exit(-1); + } on ResponseError catch (e) { + print( + '${red('Error!')} Could not connect to the Ledger ${walletDefinition.walletName}. Reason: ${e.statusWord}'); + exit(-1); + } +} diff --git a/lib/wallet.dart b/lib/wallet.dart new file mode 100644 index 0000000..89bb6b3 --- /dev/null +++ b/lib/wallet.dart @@ -0,0 +1,149 @@ +import 'dart:io'; + +import 'package:bip39/bip39.dart' as bip39; +import 'package:dcli/dcli.dart' hide verbose; +import 'package:znn_cli_dart/lib.dart'; +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +void walletMenu() { + print(' ${white('Wallet')}'); + print(' wallet.list'); + print(' wallet.createNew passphrase [keyStoreName]'); + print( + ' wallet.createFromMnemonic "${green('mnemonic')}" passphrase [keyStoreName]'); + print(' wallet.dumpMnemonic'); + print(' wallet.deriveAddresses start end'); + print(' wallet.export filePath'); +} + +Future walletFunctions() async { + switch (args[0].split('.')[1]) { + case 'list': + verbose ? print('Description: List all wallets') : null; + await _list(); + return; + + case 'createNew': + verbose ? print('Description: Create a new wallet') : null; + await _createNew(); + return; + + case 'createFromMnemonic': + verbose + ? print('Description: Create a new wallet from a mnemonic') + : null; + await _createFromMnemonic(); + return; + + case 'dumpMnemonic': + verbose ? print('Description: Dump the mnemonic of a wallet') : null; + await _dumpMnemonic(); + return; + + case 'deriveAddresses': + verbose + ? print('Description: Derive one or more addresses of a wallet') + : null; + await _deriveAddresses(); + return; + + case 'export': + verbose ? print('Description: Export wallet') : null; + await _export(); + return; + + default: + invalidCommand(); + } +} + +Future _list() async { + var walletDefinitions = await getWalletDefinitions(); + if (walletDefinitions.isNotEmpty) { + print('Available wallets:'); + for (var walletDef in walletDefinitions) { + print(walletDef.walletName); + } + } else { + print('No wallets found'); + } +} + +Future _createNew() async { + if (!(args.length == 2 || args.length == 3)) { + print('Incorrect number of arguments. Expected:'); + print('wallet.createNew passphrase [keyStoreName]'); + return; + } + + String? name; + if (args.length == 3) name = args[2]; + + var walletDef = await keyStoreManager.createNew(args[1], name); + print('keyStore ${green('successfully')} created: ${walletDef.walletName}'); +} + +Future _createFromMnemonic() async { + if (!(args.length == 3 || args.length == 4)) { + print('Incorrect number of arguments. Expected:'); + print( + 'wallet.createFromMnemonic "${green('mnemonic')}" passphrase [keyStoreName]'); + return; + } + if (!bip39.validateMnemonic(args[1])) { + throw AskValidatorException(red('Invalid mnemonic')); + } + + String? name; + if (args.length == 4) name = args[3]; + var walletDef = + await keyStoreManager.createFromMnemonic(args[1], args[2], name); + print( + 'keyStore ${green('successfully')} created from mnemonic: ${walletDef.walletName}'); +} + +Future _dumpMnemonic() async { + if (znnClient.defaultKeyStore is! KeyStore) { + print('${red('Error!')} this command is not supported by this wallet'); + return; + } + var keyStore = znnClient.defaultKeyStore as KeyStore; + print('Mnemonic for keyStore ${znnClient.defaultKeyStorePath!.walletName}'); + print(keyStore.mnemonic); +} + +Future _deriveAddresses() async { + if (args.length != 3) { + print('Incorrect number of arguments. Expected:'); + print('wallet.deriveAddresses'); + return; + } + + print('Addresses for wallet ${znnClient.defaultKeyStorePath!.walletName}'); + + var addresses = []; + var left = int.parse(args[1]); + var right = int.parse(args[2]); + for (var i = left; i < right; i++) { + var walletAccount = await znnClient.defaultKeyStore!.getAccount(i); + addresses.add(await walletAccount.getAddress()); + } + for (int i = 0; i < right - left; i += 1) { + print(' ${i + left}\t${addresses[i].toString()}'); + } +} + +Future _export() async { + if (args.length != 2) { + print('Incorrect number of arguments. Expected:'); + print('wallet.export filePath'); + return; + } + if (znnClient.defaultKeyStorePath is! KeyStoreDefinition) { + print('${red('Error!')} this command is not supported by this wallet'); + return; + } + var walletDef = znnClient.defaultKeyStorePath as KeyStoreDefinition; + await File(walletDef.walletId).copy(args[1]); + print('Done! Check the current directory'); +} diff --git a/pubspec.lock b/pubspec.lock index a80d78d..ed547f6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: "36a321c3d2cbe01cbcb3540a87b8843846e0206df3e691fa7b23e19e78de6d49" url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "65.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: dfe03b90ec022450e22513b5e5ca1f01c0c01de9c3fba2f7fd233cb57a6b9a07 url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.3.0" archive: dependency: transitive description: name: archive - sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" url: "https://pub.dev" source: hosted - version: "3.3.7" + version: "3.4.9" argon2_ffi_base: dependency: transitive description: @@ -49,14 +49,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" - basic_utils: - dependency: transitive - description: - name: basic_utils - sha256: "8815477fcf58499e42326bd858e391442425fa57db9a45e48e15224c62049262" - url: "https://pub.dev" - source: hosted - version: "5.5.4" bech32: dependency: transitive description: @@ -113,14 +105,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 - url: "https://pub.dev" - source: hosted - version: "1.3.1" chunked_stream: dependency: transitive description: @@ -137,14 +121,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.0" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 - url: "https://pub.dev" - source: hosted - version: "0.4.0" clock: dependency: transitive description: @@ -154,15 +130,23 @@ packages: source: hosted version: "1.1.1" collection: - dependency: transitive + dependency: "direct main" description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" - convert: + version: "1.18.0" + completer_ex: dependency: transitive + description: + name: completer_ex + sha256: "7bc3b65fb581c999891fdee0aaa3b5194b50329e0ccda28eb119ebedfec4b852" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + convert: + dependency: "direct main" description: name: convert sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" @@ -173,10 +157,10 @@ packages: dependency: transitive description: name: coverage - sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + sha256: ac86d3abab0f165e4b8f561280ff4e066bceaac83c424dd19f1ae2c2fcd12ca9 url: "https://pub.dev" source: hosted - version: "1.6.3" + version: "1.7.1" crypto: dependency: transitive description: @@ -197,10 +181,10 @@ packages: dependency: transitive description: name: csv - sha256: "016b31a51a913744a0a1655c74ff13c9379e1200e246a03d96c81c5d9ed297b5" + sha256: "63ed2871dd6471193dffc52c0e6c76fb86269c00244d244297abbb355c84a86e" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "5.1.1" dart_console2: dependency: transitive description: @@ -213,18 +197,26 @@ packages: dependency: "direct main" description: name: dcli - sha256: f9d71f1ec4977744f1a7b1e92bf9382588070ae3b1794a071f15c0972e7fb50d + sha256: "64ec1924241fe53d5517a92f874390f9db7a4b2f986b3dbea2e900f60c44cea7" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.2.0" dcli_core: dependency: transitive description: name: dcli_core - sha256: aabfd5645dded76e4813a63fbc1907c7b9810fa45d8a8e84113311aba9c86a3f + sha256: "8bcba1b7edce99e42a74e56b184101ece0fb3994e0fd152cdef94c0f9aa2acdb" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.2.2" + dcli_terminal: + dependency: transitive + description: + name: dcli_terminal + sha256: "4d68792b83da509206028b6c028353fbb5fb41cd75a694affd86e2f2016f6897" + url: "https://pub.dev" + source: hosted + version: "0.2.0" equatable: dependency: transitive description: @@ -237,10 +229,10 @@ packages: dependency: transitive description: name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" file: dependency: transitive description: @@ -249,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" frontend_server_client: dependency: transitive description: @@ -257,6 +257,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + fsm2: + dependency: transitive + description: + name: fsm2 + sha256: "7cfc5503617e88f352f224e6715afb345bd23bd2a23e83d2b1ea977014f0111a" + url: "https://pub.dev" + source: hosted + version: "3.0.0" functional_data: dependency: transitive description: @@ -289,14 +297,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" - http: - dependency: transitive - description: - name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" - url: "https://pub.dev" - source: hosted - version: "0.13.6" http_multi_server: dependency: transitive description: @@ -377,6 +377,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + logger: + dependency: transitive + description: + name: logger + sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac" + url: "https://pub.dev" + source: hosted + version: "2.0.2+1" logging: dependency: transitive description: @@ -397,10 +413,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mime: dependency: transitive description: @@ -426,13 +442,13 @@ packages: source: hosted version: "2.1.0" path: - dependency: "direct main" + dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" pointycastle: dependency: transitive description: @@ -465,14 +481,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - pubspec2: - dependency: transitive - description: - name: pubspec2 - sha256: "7b1fd81927f1da6d88457c83b51134e1bc8cb07638bd8d9e205b2ce1cd9ec091" - url: "https://pub.dev" - source: hosted - version: "2.4.2" pubspec_lock: dependency: transitive description: @@ -481,38 +489,38 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" - quiver: + pubspec_manager: dependency: transitive description: - name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + name: pubspec_manager + sha256: c8ff74f2de8e3732d48934c9121195497e28b66a10883493e1990b0c7a306c14 url: "https://pub.dev" source: hosted - version: "3.2.1" - random_string: + version: "1.0.0-alpha.11" + quiver: dependency: transitive description: - name: random_string - sha256: "03b52435aae8cbdd1056cf91bfc5bf845e9706724dd35ae2e99fa14a1ef79d02" + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "3.2.1" scope: dependency: transitive description: name: scope - sha256: "5e777fa2dcdf31e09ad0b0034b965d75ea2f0b0a77fab8ef340d9478f9df755c" + sha256: "80cf1cb727791fdaaa4131817974a6084815ed59b9ab02ef352c3a1badea488b" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.1.0" settings_yaml: dependency: transitive description: name: settings_yaml - sha256: d1e4df76d0fba7aae772ff59c56ef756eb1ae7a4b836387d3ae87765bf968505 + sha256: b803b97e6f303f43248212e7d2ab329b584b8c38011341c10302519f1865c189 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.1.0" sha3: dependency: transitive description: @@ -553,6 +561,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + simple_logger: + dependency: transitive + description: + name: simple_logger + sha256: bd3f09099a890f5f66cd27a39e5422f4e27b5e7cf4c5a7331569e86d89846898 + url: "https://pub.dev" + source: hosted + version: "1.9.0+3" source_map_stack_trace: dependency: transitive description: @@ -577,14 +593,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" + stacktrace_impl: + dependency: transitive + description: + name: stacktrace_impl + sha256: a42791862f672151d7f5a12911bf607c5b6d600f15f4f2457ef4ec92bfcf561b + url: "https://pub.dev" + source: hosted + version: "2.3.0" stream_channel: dependency: transitive description: @@ -601,6 +633,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + strings: + dependency: transitive + description: + name: strings + sha256: "074ee21f17a17bb907af60e5b9e7494cd53bba0bf9d73599892c787a0c172a42" + url: "https://pub.dev" + source: hosted + version: "2.0.3" sum_types: dependency: transitive description: @@ -609,6 +649,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.5" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.dev" + source: hosted + version: "3.1.0" system_info2: dependency: transitive description: @@ -629,10 +677,10 @@ packages: dependency: "direct dev" description: name: test - sha256: "67ec5684c7a19b2aba91d2831f3d305a6fd8e1504629c5818f8d64478abf4f38" + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f url: "https://pub.dev" source: hosted - version: "1.24.4" + version: "1.24.9" test_api: dependency: transitive description: @@ -645,10 +693,18 @@ packages: dependency: transitive description: name: test_core - sha256: "6b753899253c38ca0523bb0eccff3934ec83d011705dae717c61ecf209e333c9" + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + url: "https://pub.dev" + source: hosted + version: "0.5.9" + tree_iterator: + dependency: transitive + description: + name: tree_iterator + sha256: bf3e797743cbf16366c40fb481a83cd7a2a30f36240a633de27e7ef1549b8b34 url: "https://pub.dev" source: hosted - version: "0.5.4" + version: "3.0.0" typed_data: dependency: transitive description: @@ -657,22 +713,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" - uri: + unicode: dependency: transitive description: - name: uri - sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "0.3.1" uuid: dependency: transitive description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.2.1" validators2: dependency: transitive description: @@ -685,10 +741,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b8c67f5fa3897b122cf60fe9ff314f7b0ef71eab25c5f8b771480bc338f48823 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.7.2" + version: "13.0.0" watcher: dependency: transitive description: @@ -709,10 +765,10 @@ packages: dependency: transitive description: name: webkit_inspection_protocol - sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" win32: dependency: transitive description: @@ -729,14 +785,23 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" - znn_sdk_dart: + znn_ledger_dart: dependency: "direct main" description: path: "." - ref: e0b39b696c23ba3cd964df59d743e070b84a5f81 - resolved-ref: e0b39b696c23ba3cd964df59d743e070b84a5f81 - url: "https://github.com/alienc0der/znn_sdk_dart.git" + ref: "release/v0.0.4" + resolved-ref: c66264f599038e15a3457e382cfaaff202ab231a + url: "https://github.com/hypercore-one/znn_ledger_dart.git" source: git version: "0.0.4" + znn_sdk_dart: + dependency: "direct main" + description: + path: "." + ref: "8511f17a0dfa721e8f0ec2e0b0aae409f509e102" + resolved-ref: "8511f17a0dfa721e8f0ec2e0b0aae409f509e102" + url: "https://github.com/zenon-network/znn_sdk_dart.git" + source: git + version: "0.0.7" sdks: dart: ">=3.0.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3b883c0..4b7b6c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,21 +1,26 @@ name: znn_cli_dart description: Zenon CLI tool -version: 0.0.4 +version: 0.0.6 publish_to: none environment: - sdk: '>=2.14.0 <3.0.0' + sdk: '>=2.17.0 <3.0.0' dependencies: args: ^2.4.2 - bip39: ^1.0.6 dcli: ^3.0.2 - path: ^1.8.2 znn_sdk_dart: git: - url: https://github.com/alienc0der/znn_sdk_dart.git - ref: e0b39b696c23ba3cd964df59d743e070b84a5f81 # bigint-support - + url: https://github.com/zenon-network/znn_sdk_dart.git + ref: 8511f17a0dfa721e8f0ec2e0b0aae409f509e102 + znn_ledger_dart: + git: + url: https://github.com/hypercore-one/znn_ledger_dart.git + ref: release/v0.0.4 + bip39: ^1.0.6 + convert: ^3.1.1 + collection: ^1.18.0 + dev_dependencies: test: ^1.24.4 lints: ^2.1.1 diff --git a/test-runs/v0.0.6-rc.1/rpc/rpc-2023-12-01-linux.csv b/test-runs/v0.0.6-rc.1/rpc/rpc-2023-12-01-linux.csv new file mode 100644 index 0000000..2eb5d8a --- /dev/null +++ b/test-runs/v0.0.6-rc.1/rpc/rpc-2023-12-01-linux.csv @@ -0,0 +1,65 @@ +Column1,Column2,Column3,Column4 +Version: 0.0.6-rc.1,Environment: Ubuntu 22.04.3 LTS,, +Date: 2023-12-01,Tester: CryptoFish,, +,,, +Test case,Description,Priority,Result +General functionality,,, +balance address,List account balance.,PRIO2,PASS +frontierMomentum,Display frontier momentum.,PRIO1,PASS +createHash [hashType preimageLength],Create hash digests by using the stated algorithm.,PRIO2,PASS +version,Display version information.,PRIO2,PASS +,,, +Statistical information,,, +stats.networkInfo,Get the os info.,PRIO3,PASS +stats.osInfo,Get the process info.,PRIO3,PASS +stats.processInfo,Get the network info.,PRIO3,PASS +stats.syncInfo,Get the sync info.,PRIO3,PASS +,,, +Plasma functionality,,, +plasma.get address,Display the amount of plasma and QSR fused for an address.,PRIO1,PASS +,,, +Pillar,,, +pillar.list,List all pillars.,PRIO2, +,,, +ZTS Tokens,,, +token.list [pageIndex pageCount],List all tokens.,PRIO1,PASS +token.getByStandard tokenStandard,List tokens by standard.,PRIO2,PASS +token.getByOwner ownerAddress,List tokens by owner.,PRIO2,PASS +,,, +Wallet,,, +wallet.list,List all wallets.,PRIO1,PASS +wallet.createNew passphrase [keyStoreName],Create a new wallet.,PRIO1,PASS +"wallet.createFromMnemonic ""mnemonic"" passphrase [keyStoreName]",Create a new wallet from a mnemonic.,PRIO1,PASS +,,, +Spork,,, +spork.list,List all sporks.,PRIO2,PASS +,,, +HTLC,,, +htlc.get id,Display htlc details.,PRIO2,PASS +htlc.inspect blockHash,Inspect htlc account-block.,PRIO2,PASS +htlc.getProxyStatus address,Display proxy unlock status for an address.,PRIO2,PASS +,,, +Bridge,,, +bridge.info,Get the bridge information.,PRIO2,PASS +bridge.security,Get the bridge security information.,PRIO2,PASS +bridge.timeChallenges,List all bridge time challenges.,PRIO2,PASS +bridge.orchestratorInfo,Get the orchestrator information.,PRIO2,PASS +bridge.fees [tokenStandard],Display the accumulated wrapping fees for a ZTS.,PRIO2, +bridge.network.list,List all available bridge netwoks.,PRIO2, +bridge.network.get networkClass chainId,Get the information for a network class and chain id.,PRIO2, +bridge.wrap.list,List all wrap token requests.,PRIO2, +bridge.wrap.listByAddress address [networkClass chainId],List all wrap token requests made by EVM address.,PRIO2, +bridge.wrap.listUnsigned,List all unsigned wrap token requests.,PRIO2, +bridge.wrap.get id,Get wrap token request by id.,PRIO2, +bridge.unwrap.list,List all unwrap token requests by NoM address.,PRIO2, +bridge.unwrap.listByAddress toAddress,List all unwrap token requests by NoM address.,PRIO2, +bridge.unwrap.listUnredeemed [toAddress],List all unredeemed unwrap token requests.,PRIO2, +bridge.unwrap.get transactionHash logIndex,Get unwrap token request by hash and log index.,PRIO2, +,,, +Liquidity,,, +liquidity.info,Get the liquidity information.,PRIO2, +liquidity.security,Get the liquidity security info.,PRIO2, +liquidity.timeChallenges,List the liquidity time challenges.,PRIO2, +liquidity.getFrontierReward address,Display total rewards an address has earned.,PRIO2, +liquidity.getStakeEntries address,Display all stake entries for an address.,PRIO2, +liquidity.getUncollectedReward address,Display uncollected rewards for an address.,PRIO2, diff --git a/test-runs/v0.0.6-rc.1/wallet-keystore/wallet-keystore-2023-12-01-linux.csv b/test-runs/v0.0.6-rc.1/wallet-keystore/wallet-keystore-2023-12-01-linux.csv new file mode 100644 index 0000000..b30d20d --- /dev/null +++ b/test-runs/v0.0.6-rc.1/wallet-keystore/wallet-keystore-2023-12-01-linux.csv @@ -0,0 +1,85 @@ +Column1,Column2,Column3,Column4 +Version: 0.0.6-rc.1,Environment: Ubuntu 22.04.3 LTS,, +Date: 2023-12-01,Tester: CryptoFish,, +,,, +Test case,Description,Priority,Result +General functionality,,, +send toAddress amount [ZNN/QSR/ZTS message],Send tokens to an address.,PRIO1,PASS +receive blockHash,Manually receive a transaction by blockHash.,PRIO1,PASS +receiveAll,Receive all pending transactions.,PRIO1,PASS +autoreceive,Automaticly receive transactions.,PRIO2,PASS +unreceived,List pending/unreceived transactions.,PRIO2,PASS +unconfirmed,List unconfirmed transactions.,PRIO2, +,,, +Plasma functionality,,, +plasma.list [pageIndex pageCount],List plasma fusion entries.,PRIO2,PASS +plasma.fuse toAddress amount (in QSR),Fuse QSR to an address to generate plasma.,PRIO1,PASS +plasma.cancel id,Cancel a plasma fusion and receive the QSR back.,PRIO1,PASS +,,, +Sentinel,,, +sentinel.list,List all sentinels.,PRIO2,PASS +sentinel.register,Register a sentinel.,PRIO1,PASS +sentinel.revoke,Revoke a sentinel.,PRIO1, +sentinel.collect,Collect sentinel rewards.,PRIO1,PASS +sentinel.depositQsr,Deposit QSR to the sentinel contract.,PRIO1,PASS +sentinel.withdrawQsr,Withdraw deposited QSR from the sentinel contract.,PRIO2,PASS +,,, +Staking,,, +stake.list [pageIndex pageCount],List all stakes.,PRIO2,FAIL +stake.register amount duration (in months),Register stake.,PRIO1,FAIL +stake.revoke id,Revoke stake.,PRIO1,FAIL +stake.collect,Collect staking rewards.,PRIO1,FAIL +,,, +Pillar,,, +pillar.register name producerAddress rewardAddress giveBlockRewardPercentage giveDelegateRewardPercentage,Register pillar.,PRIO1,PASS +pillar.revoke name,Revoke pillar.,PRIO1, +pillar.delegate name,Delegate to pillar.,PRIO1,PASS +pillar.undelegate,Undelegate pillar.,PRIO1,PASS +pillar.collect,Collect pillar rewards.,PRIO1,PASS +pillar.depositQsr,Deposit QSR to the pillar contract.,PRIO1,PASS +pillar.withdrawQsr,Withdraw deposited QSR from the pillar contract.,PRIO1,PASS +,,, +ZTS Tokens,,, +token.issue name symbol domain totalSupply maxSupply decimals isMintable isBurnable isUtility,Issue token.,PRIO1,PASS +token.mint tokenStandard amount receiveAddress,Mint token.,PRIO1,PASS +token.burn tokenStandard amount,Burn token.,PRIO1,PASS +token.transferOwnership tokenStandard newOwnerAddress,Transfer token ownership to another address.,PRIO1,PASS +token.disableMint tokenStandard,Disable a token's minting capability.,PRIO1,PASS +,,, +Wallet,,, +wallet.dumpMnemonic,Dump the mnemonic of a wallet.,PRIO3,PASS +wallet.deriveAddresses start end,Derive one or more addresses of a wallet.,PRIO1,PASS +wallet.export filePath,Export wallet.,PRIO3,PASS +,,, +Accelerator-Z,,, +az.donate amount ZNN/QSR,Donate ZNN or QSR as fuel for the Mothership.,PRIO3,PASS +,,, +Spork,,, +spork.create name description,Create a new spork.,PRIO3, +spork.activate id,Activate a spork.,PRIO3, +,,, +HTLC,,, +htlc.create hashLockedAddress tokenStandard amount expirationTime (in hours) [hashType hashLock],Create an htlc.,PRIO1,PASS +htlc.unlock id preimage,Unlock an active htlc.,PRIO1,FAIL +htlc.reclaim id,Reclaim an expired htlc.,PRIO1, +htlc.denyProxy,Deny htlc proxy unlock.,PRIO2,PASS +htlc.allowProxy,Allow htlc proxy unlock.,PRIO2,PASS +htlc.monitor id,Monitor htlc by id -- automatically reclaim it or display its preimage.,PRIO2,PASS +,,, +Bridge,,, +bridge.wrap.token networkClass chainId toAddress amount tokenStandard,Wrap assets for an EVM-compatible network.,PRIO2, +bridge.unwrap.redeem transactionHash logIndex,Redeem a pending unwrap request for any recipient.,PRIO2, +bridge.unwrap.redeemAll [bool],Redeem all pending unwrap requests for yourself or all addresses.,PRIO2, +bridge.guardian.proposeAdmin address,Participate in a vote to elect a new bridge administrator when the contract is in emergency mode,PRIO2, +,,, +Liquidity,,, +liquidity.stake duration (in months) amount tokenStandard,Stake LP tokens.,PRIO2, +liquidity.cancelStake id,Cancel an unlocked stake and receive your LP tokens.,PRIO2, +liquidity.collectRewards,Collect liquidity rewards.,PRIO2, +liquidity.guardian.proposeAdmin address,Participate in a vote to elect a new liquidity administrator when the contract is in emergency mode,PRIO2, +,,, +Orchestrator,,, +orchestrator.changePubKey,Change bridge TSS ECDSA public key. Can only be called by the administrator.,PRIO2, +orchestrator.haltBridge,Halt bridge operations.,PRIO2, +orchestrator.updateWrapRequest,Update wrap token request.,PRIO2, +orchestrator.unwrapToken,Unwrap assets.,PRIO2, diff --git a/test-runs/v0.0.6-rc.1/wallet-ledger/wallet-ledger-2023-12-02-linux.csv b/test-runs/v0.0.6-rc.1/wallet-ledger/wallet-ledger-2023-12-02-linux.csv new file mode 100644 index 0000000..95dc904 --- /dev/null +++ b/test-runs/v0.0.6-rc.1/wallet-ledger/wallet-ledger-2023-12-02-linux.csv @@ -0,0 +1,83 @@ +Column1,Column2,Column3,Column4 +Version: 0.0.6-rc.1,Environment: Ubuntu 22.04.3 LTS/Ledger Nano S,, +Date: 2023-12-02,Tester: CryptoFish,, +,,, +Test case,Description,Priority,Result +General functionality,,, +send toAddress amount [ZNN/QSR/ZTS message],Send tokens to an address.,PRIO1,PASS +receive blockHash,Manually receive a transaction by blockHash.,PRIO1,PASS +receiveAll,Receive all pending transactions.,PRIO1,PASS +autoreceive,Automaticly receive transactions.,PRIO2,FAIL +unreceived,List pending/unreceived transactions.,PRIO2,PASS +unconfirmed,List unconfirmed transactions.,PRIO2, +,,, +Plasma functionality,,, +plasma.list [pageIndex pageCount],List plasma fusion entries.,PRIO2,PASS +plasma.fuse toAddress amount (in QSR),Fuse QSR to an address to generate plasma.,PRIO1,PASS +plasma.cancel id,Cancel a plasma fusion and receive the QSR back.,PRIO1, +,,, +Sentinel,,, +sentinel.list,List all sentinels.,PRIO2,PASS +sentinel.register,Register a sentinel.,PRIO1,PASS +sentinel.revoke,Revoke a sentinel.,PRIO1, +sentinel.collect,Collect sentinel rewards.,PRIO1, +sentinel.depositQsr,Deposit QSR to the sentinel contract.,PRIO1,PASS +sentinel.withdrawQsr,Withdraw deposited QSR from the sentinel contract.,PRIO2,PASS +,,, +Staking,,, +stake.list [pageIndex pageCount],List all stakes.,PRIO2,FAIL +stake.register amount duration (in months),Register stake.,PRIO1,FAIL +stake.revoke id,Revoke stake.,PRIO1,FAIL +stake.collect,Collect staking rewards.,PRIO1,FAIL +,,, +Pillar,,, +pillar.register name producerAddress rewardAddress giveBlockRewardPercentage giveDelegateRewardPercentage,Register pillar.,PRIO1, +pillar.revoke name,Revoke pillar.,PRIO1, +pillar.delegate name,Delegate to pillar.,PRIO1,PASS +pillar.undelegate,Undelegate pillar.,PRIO1,PASS +pillar.collect,Collect pillar rewards.,PRIO1,PASS +pillar.depositQsr,Deposit QSR to the pillar contract.,PRIO1, +pillar.withdrawQsr,Withdraw deposited QSR from the pillar contract.,PRIO1, +,,, +ZTS Tokens,,, +token.issue name symbol domain totalSupply maxSupply decimals isMintable isBurnable isUtility,Issue token.,PRIO1,PASS +token.mint tokenStandard amount receiveAddress,Mint token.,PRIO1,PASS +token.burn tokenStandard amount,Burn token.,PRIO1,FAIL +token.transferOwnership tokenStandard newOwnerAddress,Transfer token ownership to another address.,PRIO1,PASS +token.disableMint tokenStandard,Disable a token's minting capability.,PRIO1,PASS +,,, +Wallet,,, +wallet.deriveAddresses start end,Derive one or more addresses of a wallet.,PRIO1,PASS +,,, +Accelerator-Z,,, +az.donate amount ZNN/QSR,Donate ZNN or QSR as fuel for the Mothership.,PRIO3,PASS +,,, +Spork,,, +spork.create name description,Create a new spork.,PRIO3, +spork.activate id,Activate a spork.,PRIO3, +,,, +HTLC,,, +htlc.create hashLockedAddress tokenStandard amount expirationTime (in hours) [hashType hashLock],Create an htlc.,PRIO1,PASS +htlc.unlock id preimage,Unlock an active htlc.,PRIO1,FAIL +htlc.reclaim id,Reclaim an expired htlc.,PRIO1, +htlc.denyProxy,Deny htlc proxy unlock.,PRIO2,PASS +htlc.allowProxy,Allow htlc proxy unlock.,PRIO2,PASS +htlc.monitor id,Monitor htlc by id -- automatically reclaim it or display its preimage.,PRIO2,FAIL +,,, +Bridge,,, +bridge.wrap.token networkClass chainId toAddress amount tokenStandard,Wrap assets for an EVM-compatible network.,PRIO2, +bridge.unwrap.redeem transactionHash logIndex,Redeem a pending unwrap request for any recipient.,PRIO2, +bridge.unwrap.redeemAll [bool],Redeem all pending unwrap requests for yourself or all addresses.,PRIO2, +bridge.guardian.proposeAdmin address,Participate in a vote to elect a new bridge administrator when the contract is in emergency mode,PRIO2, +,,, +Liquidity,,, +liquidity.stake duration (in months) amount tokenStandard,Stake LP tokens.,PRIO2, +liquidity.cancelStake id,Cancel an unlocked stake and receive your LP tokens.,PRIO2, +liquidity.collectRewards,Collect liquidity rewards.,PRIO2, +liquidity.guardian.proposeAdmin address,Participate in a vote to elect a new liquidity administrator when the contract is in emergency mode,PRIO2, +,,, +Orchestrator,,, +orchestrator.changePubKey,Change bridge TSS ECDSA public key. Can only be called by the administrator.,PRIO2, +orchestrator.haltBridge,Halt bridge operations.,PRIO2, +orchestrator.updateWrapRequest,Update wrap token request.,PRIO2, +orchestrator.unwrapToken,Unwrap assets.,PRIO2, \ No newline at end of file diff --git a/test-runs/v0.0.6-rc.2/rpc/rpc-2023-12-04-windows.csv b/test-runs/v0.0.6-rc.2/rpc/rpc-2023-12-04-windows.csv new file mode 100644 index 0000000..717b01a --- /dev/null +++ b/test-runs/v0.0.6-rc.2/rpc/rpc-2023-12-04-windows.csv @@ -0,0 +1,29 @@ +Column1,Column2,Column3,Column4 +Version: 0.0.6-rc.2,Environment: Windows 10,, +Date: 2023-12-04,Tester: CryptoFish,, +,,, +Test case,Description,Priority,Result +,,, +Pillar,,, +pillar.list,List all pillars.,PRIO2,PASS +,,, +Bridge,,, +bridge.fees [tokenStandard],Display the accumulated wrapping fees for a ZTS.,PRIO2,PASS +bridge.network.list,List all available bridge netwoks.,PRIO2,PASS +bridge.network.get networkClass chainId,Get the information for a network class and chain id.,PRIO2,PASS +bridge.wrap.list,List all wrap token requests.,PRIO2,PASS +bridge.wrap.listByAddress address [networkClass chainId],List all wrap token requests made by EVM address.,PRIO2,FAIL +bridge.wrap.listUnsigned,List all unsigned wrap token requests.,PRIO2,PASS +bridge.wrap.get id,Get wrap token request by id.,PRIO2,FAIL +bridge.unwrap.list,List all unwrap token requests by NoM address.,PRIO2,PASS +bridge.unwrap.listByAddress toAddress,List all unwrap token requests by NoM address.,PRIO2,FAIL +bridge.unwrap.listUnredeemed [toAddress],List all unredeemed unwrap token requests.,PRIO2,FAIL +bridge.unwrap.get transactionHash logIndex,Get unwrap token request by hash and log index.,PRIO2,FAIL +,,, +Liquidity,,, +liquidity.info,Get the liquidity information.,PRIO2,PASS +liquidity.security,Get the liquidity security info.,PRIO2,PASS +liquidity.timeChallenges,List the liquidity time challenges.,PRIO2,PASS +liquidity.getFrontierReward address,Display total rewards an address has earned.,PRIO2,FAIL +liquidity.getStakeEntries address,Display all stake entries for an address.,PRIO2,PASS +liquidity.getUncollectedReward address,Display uncollected rewards for an address.,PRIO2,PASS diff --git a/test-runs/v0.0.6-rc.2/wallet-keystore/wallet-keystore-2023-12-04-windows.csv b/test-runs/v0.0.6-rc.2/wallet-keystore/wallet-keystore-2023-12-04-windows.csv new file mode 100644 index 0000000..4ec57c1 --- /dev/null +++ b/test-runs/v0.0.6-rc.2/wallet-keystore/wallet-keystore-2023-12-04-windows.csv @@ -0,0 +1,45 @@ +Column1,Column2,Column3,Column4 +Version: 0.0.6-rc.2,Environment: Windows 10,, +Date: 2023-12-04,Tester: CryptoFish,, +,,, +Test case,Description,Priority,Result +General functionality,,, +unconfirmed,List unconfirmed transactions.,PRIO2, +,,, +Sentinel,,, +sentinel.revoke,Revoke a sentinel.,PRIO1,PASS +,,, +Staking,,, +stake.list [pageIndex pageCount],List all stakes.,PRIO2,PASS +stake.register amount duration (in months),Register stake.,PRIO1,PASS +stake.revoke id,Revoke stake.,PRIO1,PASS +stake.collect,Collect staking rewards.,PRIO1,PASS +,,, +Pillar,,, +pillar.revoke name,Revoke pillar.,PRIO1, +,,, +Spork,,, +spork.create name description,Create a new spork.,PRIO3, +spork.activate id,Activate a spork.,PRIO3, +,,, +HTLC,,, +htlc.unlock id preimage,Unlock an active htlc.,PRIO1,PASS +htlc.reclaim id,Reclaim an expired htlc.,PRIO1,PASS +,,, +Bridge,,, +bridge.wrap.token networkClass chainId toAddress amount tokenStandard,Wrap assets for an EVM-compatible network.,PRIO2,PASS +bridge.unwrap.redeem transactionHash logIndex,Redeem a pending unwrap request for any recipient.,PRIO2, +bridge.unwrap.redeemAll [bool],Redeem all pending unwrap requests for yourself or all addresses.,PRIO2, +bridge.guardian.proposeAdmin address,Participate in a vote to elect a new bridge administrator when the contract is in emergency mode,PRIO2, +,,, +Liquidity,,, +liquidity.stake duration (in months) amount tokenStandard,Stake LP tokens.,PRIO2, +liquidity.cancelStake id,Cancel an unlocked stake and receive your LP tokens.,PRIO2, +liquidity.collectRewards,Collect liquidity rewards.,PRIO2,PASS +liquidity.guardian.proposeAdmin address,Participate in a vote to elect a new liquidity administrator when the contract is in emergency mode,PRIO2, +,,, +Orchestrator,,, +orchestrator.changePubKey,Change bridge TSS ECDSA public key. Can only be called by the administrator.,PRIO2, +orchestrator.haltBridge,Halt bridge operations.,PRIO2, +orchestrator.updateWrapRequest,Update wrap token request.,PRIO2, +orchestrator.unwrapToken,Unwrap assets.,PRIO2, \ No newline at end of file diff --git a/test-runs/v0.0.6-rc.2/wallet-ledger/wallet-ledger-2023-12-06-windows.csv b/test-runs/v0.0.6-rc.2/wallet-ledger/wallet-ledger-2023-12-06-windows.csv new file mode 100644 index 0000000..bd2d13b --- /dev/null +++ b/test-runs/v0.0.6-rc.2/wallet-ledger/wallet-ledger-2023-12-06-windows.csv @@ -0,0 +1,57 @@ +Column1,Column2,Column3,Column4 +Version: 0.0.6-rc.2,Environment: Windows 10/Ledger Nano S,, +Date: 2023-12-06,Tester: CryptoFish,, +,,, +Test case,Description,Priority,Result +General functionality,,, +autoreceive,Automaticly receive transactions.,PRIO2,FAIL +unconfirmed,List unconfirmed transactions.,PRIO2, +,,, +Plasma functionality,,, +plasma.cancel id,Cancel a plasma fusion and receive the QSR back.,PRIO1,PASS +,,, +Sentinel,,, +sentinel.revoke,Revoke a sentinel.,PRIO1,PASS +sentinel.collect,Collect sentinel rewards.,PRIO1,PASS +,,, +Staking,,, +stake.list [pageIndex pageCount],List all stakes.,PRIO2,PASS +stake.register amount duration (in months),Register stake.,PRIO1,PASS +stake.revoke id,Revoke stake.,PRIO1,PASS +stake.collect,Collect staking rewards.,PRIO1,PASS +,,, +Pillar,,, +pillar.register name producerAddress rewardAddress giveBlockRewardPercentage giveDelegateRewardPercentage,Register pillar.,PRIO1, +pillar.revoke name,Revoke pillar.,PRIO1, +pillar.depositQsr,Deposit QSR to the pillar contract.,PRIO1, +pillar.withdrawQsr,Withdraw deposited QSR from the pillar contract.,PRIO1, +,,, +ZTS Tokens,,, +token.burn tokenStandard amount,Burn token.,PRIO1,PASS +,,, +Spork,,, +spork.create name description,Create a new spork.,PRIO3, +spork.activate id,Activate a spork.,PRIO3, +,,, +HTLC,,, +htlc.unlock id preimage,Unlock an active htlc.,PRIO1,PASS +htlc.reclaim id,Reclaim an expired htlc.,PRIO1,FAIL +htlc.monitor id,Monitor htlc by id -- automatically reclaim it or display its preimage.,PRIO2,FAIL +,,, +Bridge,,, +bridge.wrap.token networkClass chainId toAddress amount tokenStandard,Wrap assets for an EVM-compatible network.,PRIO2, +bridge.unwrap.redeem transactionHash logIndex,Redeem a pending unwrap request for any recipient.,PRIO2, +bridge.unwrap.redeemAll [bool],Redeem all pending unwrap requests for yourself or all addresses.,PRIO2, +bridge.guardian.proposeAdmin address,Participate in a vote to elect a new bridge administrator when the contract is in emergency mode,PRIO2, +,,, +Liquidity,,, +liquidity.stake duration (in months) amount tokenStandard,Stake LP tokens.,PRIO2, +liquidity.cancelStake id,Cancel an unlocked stake and receive your LP tokens.,PRIO2, +liquidity.collectRewards,Collect liquidity rewards.,PRIO2,PASS +liquidity.guardian.proposeAdmin address,Participate in a vote to elect a new liquidity administrator when the contract is in emergency mode,PRIO2, +,,, +Orchestrator,,, +orchestrator.changePubKey,Change bridge TSS ECDSA public key. Can only be called by the administrator.,PRIO2, +orchestrator.haltBridge,Halt bridge operations.,PRIO2, +orchestrator.updateWrapRequest,Update wrap token request.,PRIO2, +orchestrator.unwrapToken,Unwrap assets.,PRIO2, \ No newline at end of file diff --git a/test-runs/v0.0.6-rc.3/rpc/rpc-2023-12-06-windows.csv b/test-runs/v0.0.6-rc.3/rpc/rpc-2023-12-06-windows.csv new file mode 100644 index 0000000..9e33070 --- /dev/null +++ b/test-runs/v0.0.6-rc.3/rpc/rpc-2023-12-06-windows.csv @@ -0,0 +1,15 @@ +Column1,Column2,Column3,Column4 +Version: 0.0.6-rc.3,Environment: Windows 10,, +Date: 2023-12-06,Tester: CryptoFish,, +,,, +Test case,Description,Priority,Result +,,, +Bridge,,, +bridge.wrap.listByAddress address [networkClass chainId],List all wrap token requests made by EVM address.,PRIO2,PASS +bridge.wrap.get id,Get wrap token request by id.,PRIO2,PASS +bridge.unwrap.listByAddress toAddress,List all unwrap token requests by NoM address.,PRIO2,PASS +bridge.unwrap.listUnredeemed [toAddress],List all unredeemed unwrap token requests.,PRIO2,PASS +bridge.unwrap.get transactionHash logIndex,Get unwrap token request by hash and log index.,PRIO2,PASS +,,, +Liquidity,,, +liquidity.getRewardTotal address,Display total rewards an address has earned.,PRIO2,PASS diff --git a/test-runs/v0.0.6-rc.3/wallet-keystore/wallet-keystore-2023-12-06-windows.csv b/test-runs/v0.0.6-rc.3/wallet-keystore/wallet-keystore-2023-12-06-windows.csv new file mode 100644 index 0000000..5a009ae --- /dev/null +++ b/test-runs/v0.0.6-rc.3/wallet-keystore/wallet-keystore-2023-12-06-windows.csv @@ -0,0 +1,30 @@ +Column1,Column2,Column3,Column4 +Version: 0.0.6-rc.3,Environment: Windows 10,, +Date: 2023-12-06,Tester: CryptoFish,, +,,, +Test case,Description,Priority,Result +General functionality,,, +unconfirmed,List unconfirmed transactions.,PRIO2, +,,, +Pillar,,, +pillar.revoke name,Revoke pillar.,PRIO1, +,,, +Spork,,, +spork.create name description,Create a new spork.,PRIO3, +spork.activate id,Activate a spork.,PRIO3, +,,, +Bridge,,, +bridge.unwrap.redeem transactionHash logIndex,Redeem a pending unwrap request for any recipient.,PRIO2, +bridge.unwrap.redeemAll [bool],Redeem all pending unwrap requests for yourself or all addresses.,PRIO2, +bridge.guardian.proposeAdmin address,Participate in a vote to elect a new bridge administrator when the contract is in emergency mode,PRIO2, +,,, +Liquidity,,, +liquidity.stake duration (in months) amount tokenStandard,Stake LP tokens.,PRIO2, +liquidity.cancelStake id,Cancel an unlocked stake and receive your LP tokens.,PRIO2, +liquidity.guardian.proposeAdmin address,Participate in a vote to elect a new liquidity administrator when the contract is in emergency mode,PRIO2, +,,, +Orchestrator,,, +orchestrator.changePubKey,Change bridge TSS ECDSA public key. Can only be called by the administrator.,PRIO2, +orchestrator.haltBridge,Halt bridge operations.,PRIO2, +orchestrator.updateWrapRequest,Update wrap token request.,PRIO2, +orchestrator.unwrapToken,Unwrap assets.,PRIO2, \ No newline at end of file diff --git a/test-runs/v0.0.6-rc.3/wallet-ledger/wallet-ledger-2023-12-06-windows.csv b/test-runs/v0.0.6-rc.3/wallet-ledger/wallet-ledger-2023-12-06-windows.csv new file mode 100644 index 0000000..2766c18 --- /dev/null +++ b/test-runs/v0.0.6-rc.3/wallet-ledger/wallet-ledger-2023-12-06-windows.csv @@ -0,0 +1,39 @@ +Column1,Column2,Column3,Column4 +Version: 0.0.6-rc.3,Environment: Windows 10/Ledger Nano S,, +Date: 2023-12-06,Tester: CryptoFish,, +,,, +Test case,Description,Priority,Result +General functionality,,, +autoreceive,Automaticly receive transactions.,PRIO2,PASS +unconfirmed,List unconfirmed transactions.,PRIO2, +,,, +Pillar,,, +pillar.register name producerAddress rewardAddress giveBlockRewardPercentage giveDelegateRewardPercentage,Register pillar.,PRIO1, +pillar.revoke name,Revoke pillar.,PRIO1, +pillar.depositQsr,Deposit QSR to the pillar contract.,PRIO1, +pillar.withdrawQsr,Withdraw deposited QSR from the pillar contract.,PRIO1, +,,, +Spork,,, +spork.create name description,Create a new spork.,PRIO3, +spork.activate id,Activate a spork.,PRIO3, +,,, +HTLC,,, +htlc.reclaim id,Reclaim an expired htlc.,PRIO1,PASS +htlc.monitor id,Monitor htlc by id -- automatically reclaim it or display its preimage.,PRIO2,PASS +,,, +Bridge,,, +bridge.wrap.token networkClass chainId toAddress amount tokenStandard,Wrap assets for an EVM-compatible network.,PRIO2, +bridge.unwrap.redeem transactionHash logIndex,Redeem a pending unwrap request for any recipient.,PRIO2, +bridge.unwrap.redeemAll [bool],Redeem all pending unwrap requests for yourself or all addresses.,PRIO2, +bridge.guardian.proposeAdmin address,Participate in a vote to elect a new bridge administrator when the contract is in emergency mode,PRIO2, +,,, +Liquidity,,, +liquidity.stake duration (in months) amount tokenStandard,Stake LP tokens.,PRIO2, +liquidity.cancelStake id,Cancel an unlocked stake and receive your LP tokens.,PRIO2, +liquidity.guardian.proposeAdmin address,Participate in a vote to elect a new liquidity administrator when the contract is in emergency mode,PRIO2, +,,, +Orchestrator,,, +orchestrator.changePubKey,Change bridge TSS ECDSA public key. Can only be called by the administrator.,PRIO2, +orchestrator.haltBridge,Halt bridge operations.,PRIO2, +orchestrator.updateWrapRequest,Update wrap token request.,PRIO2, +orchestrator.unwrapToken,Unwrap assets.,PRIO2, \ No newline at end of file diff --git a/tests/admin-tests.csv b/tests/admin-tests.csv new file mode 100644 index 0000000..0312f1a --- /dev/null +++ b/tests/admin-tests.csv @@ -0,0 +1,31 @@ +Column1,Column2,Column3,Column4 +Version: ,Environment: ,, +Date: ,Tester: ,, +,,, +Test case,Description,Priority,Result +Bridge Admin,,, +bridge.admin.emergency,Put the bridge contract in emergency mode.,PRIO1, +bridge.admin.halt,Halt bridge operations.,PRIO1, +bridge.admin.unhalt,Unhalt bridge operations.,PRIO1, +bridge.admin.enableKeyGen,Enable bridge key generation.,PRIO1, +bridge.admin.disableKeyGen,Disable bridge key generation.,PRIO1, +bridge.admin.setTokenPair networkClass chainId tokenStandard tokenAddress bridgeable redeemable owned minAmount feePercentage redeemDelay metadata,Set a token pair to enable bridging the asset,PRIO1, +bridge.admin.removeTokenPair networkClass chainId tokenStandard tokenAddress,Remove a token pair to disable bridging the asset,PRIO1, +bridge.admin.revokeUnwrapRequest transactionHash logIndex,Revoke an unwrap request to prevent it from being redeemed.,PRIO1, +bridge.admin.nominateGuardians address1 address2 ... addressN,Nominate bridge guardians.,PRIO1, +bridge.admin.changeAdmin address,Change bridge administrator.,PRIO2, +bridge.admin.setMetadata metadata,Set the bridge metadata.,PRIO2, +bridge.admin.setOrchestratorInfo windowSize keyGenThreshold confirmationsToFinality estimatedMomentumTime,Get the bridge information.,PRIO1, +bridge.admin.setNetwork networkClass chainId name contractAddress metadata,Configure network parameters to allow bridging.,PRIO1, +bridge.admin.removeNetwork networkClass chainId,Remove a network to disable bridging.,PRIO1, +bridge.admin.setNetworkMetadata networkClass chainId metadata,Set network metadata.,PRIO1, +,,, +Liquidity Admin,,, +liquidity.admin.emergency,Put the liquidity contract in emergency mode.,PRIO1, +liquidity.admin.halt,Halt liquidity operations.,PRIO1, +liquidity.admin.unhalt,Unhalt liquidity operations.,PRIO1, +liquidity.admin.changeAdmin address,Change liquidity administrator.,PRIO1, +liquidity.admin.nominateGuardians address1 address2 ... addressN,Nominate liquidity guardians.,PRIO1, +liquidity.admin.unlockStakeEntries tokenStandard,Allows all staked entries to be cancelled immediately.,PRIO1, +liquidity.admin.setAdditionalReward znnReward qsrReward,Set additional liquidity reward percentages.,PRIO1, +liquidity.admin.setTokenTuple tokenTuples (json),Configure token tuples that can be staked.,PRIO1, diff --git a/tests/rpc-tests.csv b/tests/rpc-tests.csv new file mode 100644 index 0000000..82beb5e --- /dev/null +++ b/tests/rpc-tests.csv @@ -0,0 +1,65 @@ +Column1,Column2,Column3,Column4 +Version: ,Environment: ,, +Date: ,Tester: ,, +,,, +Test case,Description,Priority,Result +General functionality,,, +balance address,List account balance.,PRIO2, +frontierMomentum,Display frontier momentum.,PRIO1, +createHash [hashType preimageLength],Create hash digests by using the stated algorithm.,PRIO2, +version,Display version information.,PRIO2, +,,, +Statistical information,,, +stats.networkInfo,Get the os info.,PRIO3, +stats.osInfo,Get the process info.,PRIO3, +stats.processInfo,Get the network info.,PRIO3, +stats.syncInfo,Get the sync info.,PRIO3, +,,, +Plasma functionality,,, +plasma.get address,Display the amount of plasma and QSR fused for an address.,PRIO1, +,,, +Pillar,,, +pillar.list,List all pillars.,PRIO2, +,,, +ZTS Tokens,,, +token.list [pageIndex pageCount],List all tokens.,PRIO1, +token.getByStandard tokenStandard,List tokens by standard.,PRIO2, +token.getByOwner ownerAddress,List tokens by owner.,PRIO2, +,,, +Wallet,,, +wallet.list,List all wallets.,PRIO1, +wallet.createNew passphrase [keyStoreName],Create a new wallet.,PRIO1, +"wallet.createFromMnemonic ""mnemonic"" passphrase [keyStoreName]",Create a new wallet from a mnemonic.,PRIO1, +,,, +Spork,,, +spork.list,List all sporks.,PRIO2, +,,, +HTLC,,, +htlc.get id,Display htlc details.,PRIO2, +htlc.inspect blockHash,Inspect htlc account-block.,PRIO2, +htlc.getProxyStatus address,Display proxy unlock status for an address.,PRIO2, +,,, +Bridge,,, +bridge.info,Get the bridge information.,PRIO2, +bridge.security,Get the bridge security information.,PRIO2, +bridge.timeChallenges,List all bridge time challenges.,PRIO2, +bridge.orchestratorInfo,Get the orchestrator information.,PRIO2, +bridge.fees [tokenStandard],Display the accumulated wrapping fees for a ZTS.,PRIO2, +bridge.network.list,List all available bridge netwoks.,PRIO2, +bridge.network.get networkClass chainId,Get the information for a network class and chain id.,PRIO2, +bridge.wrap.list,List all wrap token requests.,PRIO2, +bridge.wrap.listByAddress address [networkClass chainId],List all wrap token requests made by EVM address.,PRIO2, +bridge.wrap.listUnsigned,List all unsigned wrap token requests.,PRIO2, +bridge.wrap.get id,Get wrap token request by id.,PRIO2, +bridge.unwrap.list,List all unwrap token requests by NoM address.,PRIO2, +bridge.unwrap.listByAddress toAddress,List all unwrap token requests by NoM address.,PRIO2, +bridge.unwrap.listUnredeemed [toAddress],List all unredeemed unwrap token requests.,PRIO2, +bridge.unwrap.get transactionHash logIndex,Get unwrap token request by hash and log index.,PRIO2, +,,, +Liquidity,,, +liquidity.info,Get the liquidity information.,PRIO2, +liquidity.security,Get the liquidity security info.,PRIO2, +liquidity.timeChallenges,List the liquidity time challenges.,PRIO2, +liquidity.getRewardTotal address,Display total rewards an address has earned.,PRIO2, +liquidity.getStakeEntries address,Display all stake entries for an address.,PRIO2, +liquidity.getUncollectedReward address,Display uncollected rewards for an address.,PRIO2, \ No newline at end of file diff --git a/tests/wallet-keystore-tests.csv b/tests/wallet-keystore-tests.csv new file mode 100644 index 0000000..c0c7ca4 --- /dev/null +++ b/tests/wallet-keystore-tests.csv @@ -0,0 +1,85 @@ +Column1,Column2,Column3,Column4 +Version: ,Environment: ,, +Date: ,Tester: ,, +,,, +Test case,Description,Priority,Result +General functionality,,, +send toAddress amount [ZNN/QSR/ZTS message],Send tokens to an address.,PRIO1, +receive blockHash,Manually receive a transaction by blockHash.,PRIO1, +receiveAll,Receive all pending transactions.,PRIO1, +autoreceive,Automaticly receive transactions.,PRIO2, +unreceived,List pending/unreceived transactions.,PRIO2, +unconfirmed,List unconfirmed transactions.,PRIO2, +,,, +Plasma functionality,,, +plasma.list [pageIndex pageCount],List plasma fusion entries.,PRIO2, +plasma.fuse toAddress amount (in QSR),Fuse QSR to an address to generate plasma.,PRIO1, +plasma.cancel id,Cancel a plasma fusion and receive the QSR back.,PRIO1, +,,, +Sentinel,,, +sentinel.list,List all sentinels.,PRIO2, +sentinel.register,Register a sentinel.,PRIO1, +sentinel.revoke,Revoke a sentinel.,PRIO1, +sentinel.collect,Collect sentinel rewards.,PRIO1, +sentinel.depositQsr,Deposit QSR to the sentinel contract.,PRIO1, +sentinel.withdrawQsr,Withdraw deposited QSR from the sentinel contract.,PRIO2, +,,, +Staking,,, +stake.list [pageIndex pageCount],List all stakes.,PRIO2, +stake.register amount duration (in months),Register stake.,PRIO1, +stake.revoke id,Revoke stake.,PRIO1, +stake.collect,Collect staking rewards.,PRIO1, +,,, +Pillar,,, +pillar.register name producerAddress rewardAddress giveBlockRewardPercentage giveDelegateRewardPercentage,Register pillar.,PRIO1, +pillar.revoke name,Revoke pillar.,PRIO1, +pillar.delegate name,Delegate to pillar.,PRIO1, +pillar.undelegate,Undelegate pillar.,PRIO1, +pillar.collect,Collect pillar rewards.,PRIO1, +pillar.depositQsr,Deposit QSR to the pillar contract.,PRIO1, +pillar.withdrawQsr,Withdraw deposited QSR from the pillar contract.,PRIO1, +,,, +ZTS Tokens,,, +token.issue name symbol domain totalSupply maxSupply decimals isMintable isBurnable isUtility,Issue token.,PRIO1, +token.mint tokenStandard amount receiveAddress,Mint token.,PRIO1, +token.burn tokenStandard amount,Burn token.,PRIO1, +token.transferOwnership tokenStandard newOwnerAddress,Transfer token ownership to another address.,PRIO1, +token.disableMint tokenStandard,Disable a token's minting capability.,PRIO1, +,,, +Wallet,,, +wallet.dumpMnemonic,Dump the mnemonic of a wallet.,PRIO3, +wallet.deriveAddresses start end,Derive one or more addresses of a wallet.,PRIO1, +wallet.export filePath,Export wallet.,PRIO3, +,,, +Accelerator-Z,,, +az.donate amount ZNN/QSR,Donate ZNN or QSR as fuel for the Mothership.,PRIO3, +,,, +Spork,,, +spork.create name description,Create a new spork.,PRIO3, +spork.activate id,Activate a spork.,PRIO3, +,,, +HTLC,,, +htlc.create hashLockedAddress tokenStandard amount expirationTime (in hours) [hashType hashLock],Create an htlc.,PRIO1, +htlc.unlock id preimage,Unlock an active htlc.,PRIO1, +htlc.reclaim id,Reclaim an expired htlc.,PRIO1, +htlc.denyProxy,Deny htlc proxy unlock.,PRIO2, +htlc.allowProxy,Allow htlc proxy unlock.,PRIO2, +htlc.monitor id,Monitor htlc by id -- automatically reclaim it or display its preimage.,PRIO2, +,,, +Bridge,,, +bridge.wrap.token networkClass chainId toAddress amount tokenStandard,Wrap assets for an EVM-compatible network.,PRIO2, +bridge.unwrap.redeem transactionHash logIndex,Redeem a pending unwrap request for any recipient.,PRIO2, +bridge.unwrap.redeemAll [bool],Redeem all pending unwrap requests for yourself or all addresses.,PRIO2, +bridge.guardian.proposeAdmin address,Participate in a vote to elect a new bridge administrator when the contract is in emergency mode,PRIO2, +,,, +Liquidity,,, +liquidity.stake duration (in months) amount tokenStandard,Stake LP tokens.,PRIO2, +liquidity.cancelStake id,Cancel an unlocked stake and receive your LP tokens.,PRIO2, +liquidity.collectRewards,Collect liquidity rewards.,PRIO2, +liquidity.guardian.proposeAdmin address,Participate in a vote to elect a new liquidity administrator when the contract is in emergency mode,PRIO2, +,,, +Orchestrator,,, +orchestrator.changePubKey,Change bridge TSS ECDSA public key. Can only be called by the administrator.,PRIO2, +orchestrator.haltBridge,Halt bridge operations.,PRIO2, +orchestrator.updateWrapRequest,Update wrap token request.,PRIO2, +orchestrator.unwrapToken,Unwrap assets.,PRIO2, \ No newline at end of file diff --git a/tests/wallet-ledger-tests.csv b/tests/wallet-ledger-tests.csv new file mode 100644 index 0000000..765173a --- /dev/null +++ b/tests/wallet-ledger-tests.csv @@ -0,0 +1,83 @@ +Column1,Column2,Column3,Column4 +Version: ,Environment: ,, +Date: ,Tester: ,, +,,, +Test case,Description,Priority,Result +General functionality,,, +send toAddress amount [ZNN/QSR/ZTS message],Send tokens to an address.,PRIO1, +receive blockHash,Manually receive a transaction by blockHash.,PRIO1, +receiveAll,Receive all pending transactions.,PRIO1, +autoreceive,Automaticly receive transactions.,PRIO2, +unreceived,List pending/unreceived transactions.,PRIO2, +unconfirmed,List unconfirmed transactions.,PRIO2, +,,, +Plasma functionality,,, +plasma.list [pageIndex pageCount],List plasma fusion entries.,PRIO2, +plasma.fuse toAddress amount (in QSR),Fuse QSR to an address to generate plasma.,PRIO1, +plasma.cancel id,Cancel a plasma fusion and receive the QSR back.,PRIO1, +,,, +Sentinel,,, +sentinel.list,List all sentinels.,PRIO2, +sentinel.register,Register a sentinel.,PRIO1, +sentinel.revoke,Revoke a sentinel.,PRIO1, +sentinel.collect,Collect sentinel rewards.,PRIO1, +sentinel.depositQsr,Deposit QSR to the sentinel contract.,PRIO1, +sentinel.withdrawQsr,Withdraw deposited QSR from the sentinel contract.,PRIO2, +,,, +Staking,,, +stake.list [pageIndex pageCount],List all stakes.,PRIO2, +stake.register amount duration (in months),Register stake.,PRIO1, +stake.revoke id,Revoke stake.,PRIO1, +stake.collect,Collect staking rewards.,PRIO1, +,,, +Pillar,,, +pillar.register name producerAddress rewardAddress giveBlockRewardPercentage giveDelegateRewardPercentage,Register pillar.,PRIO1, +pillar.revoke name,Revoke pillar.,PRIO1, +pillar.delegate name,Delegate to pillar.,PRIO1, +pillar.undelegate,Undelegate pillar.,PRIO1, +pillar.collect,Collect pillar rewards.,PRIO1, +pillar.depositQsr,Deposit QSR to the pillar contract.,PRIO1, +pillar.withdrawQsr,Withdraw deposited QSR from the pillar contract.,PRIO1, +,,, +ZTS Tokens,,, +token.issue name symbol domain totalSupply maxSupply decimals isMintable isBurnable isUtility,Issue token.,PRIO1, +token.mint tokenStandard amount receiveAddress,Mint token.,PRIO1, +token.burn tokenStandard amount,Burn token.,PRIO1, +token.transferOwnership tokenStandard newOwnerAddress,Transfer token ownership to another address.,PRIO1, +token.disableMint tokenStandard,Disable a token's minting capability.,PRIO1, +,,, +Wallet,,, +wallet.deriveAddresses start end,Derive one or more addresses of a wallet.,PRIO1, +,,, +Accelerator-Z,,, +az.donate amount ZNN/QSR,Donate ZNN or QSR as fuel for the Mothership.,PRIO3, +,,, +Spork,,, +spork.create name description,Create a new spork.,PRIO3, +spork.activate id,Activate a spork.,PRIO3, +,,, +HTLC,,, +htlc.create hashLockedAddress tokenStandard amount expirationTime (in hours) [hashType hashLock],Create an htlc.,PRIO1, +htlc.unlock id preimage,Unlock an active htlc.,PRIO1, +htlc.reclaim id,Reclaim an expired htlc.,PRIO1, +htlc.denyProxy,Deny htlc proxy unlock.,PRIO2, +htlc.allowProxy,Allow htlc proxy unlock.,PRIO2, +htlc.monitor id,Monitor htlc by id -- automatically reclaim it or display its preimage.,PRIO2, +,,, +Bridge,,, +bridge.wrap.token networkClass chainId toAddress amount tokenStandard,Wrap assets for an EVM-compatible network.,PRIO2, +bridge.unwrap.redeem transactionHash logIndex,Redeem a pending unwrap request for any recipient.,PRIO2, +bridge.unwrap.redeemAll [bool],Redeem all pending unwrap requests for yourself or all addresses.,PRIO2, +bridge.guardian.proposeAdmin address,Participate in a vote to elect a new bridge administrator when the contract is in emergency mode,PRIO2, +,,, +Liquidity,,, +liquidity.stake duration (in months) amount tokenStandard,Stake LP tokens.,PRIO2, +liquidity.cancelStake id,Cancel an unlocked stake and receive your LP tokens.,PRIO2, +liquidity.collectRewards,Collect liquidity rewards.,PRIO2, +liquidity.guardian.proposeAdmin address,Participate in a vote to elect a new liquidity administrator when the contract is in emergency mode,PRIO2, +,,, +Orchestrator,,, +orchestrator.changePubKey,Change bridge TSS ECDSA public key. Can only be called by the administrator.,PRIO2, +orchestrator.haltBridge,Halt bridge operations.,PRIO2, +orchestrator.updateWrapRequest,Update wrap token request.,PRIO2, +orchestrator.unwrapToken,Unwrap assets.,PRIO2, \ No newline at end of file diff --git a/znn-cli.dart b/znn-cli.dart new file mode 100644 index 0000000..fbf9e60 --- /dev/null +++ b/znn-cli.dart @@ -0,0 +1,23 @@ +import 'dart:async'; +import 'package:args/args.dart'; +import 'package:znn_cli_dart/lib.dart'; + +import 'package:znn_sdk_dart/znn_sdk_dart.dart'; + +Future main(List _args) async { + final ArgParser argParser = parseArgs(); + final argResult = argParser.parse(_args); + args = argResult.rest; + + handleFlags(argResult); + + ensureDirectoriesExist(); + if (!commandsWithoutWallet.contains(args[0])) { + await unlockWallet(argResult); + } + + await connectToNode(argResult); + await handleCli(); + znnClient.wsClient.stop(); + return 0; +}