mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-11-01 09:57:46 +00:00
f4e71c72ef
* add preference keys for default nano/banano rep * updates to change rep page, add success message * forgot to save this file * nano_client cleanup * first pass * refactor to use sharedprefs in nano_client * review fixes
524 lines
15 KiB
Dart
524 lines
15 KiB
Dart
part of 'nano.dart';
|
|
|
|
class CWNanoAccountList extends NanoAccountList {
|
|
CWNanoAccountList(this._wallet);
|
|
final Object _wallet;
|
|
|
|
@override
|
|
@computed
|
|
ObservableList<NanoAccount> get accounts {
|
|
final nanoWallet = _wallet as NanoWallet;
|
|
final accounts = nanoWallet.walletAddresses.accountList.accounts
|
|
.map((acc) => NanoAccount(id: acc.id, label: acc.label, balance: acc.balance))
|
|
.toList();
|
|
return ObservableList<NanoAccount>.of(accounts);
|
|
}
|
|
|
|
@override
|
|
void update(Object wallet) {
|
|
final nanoWallet = wallet as NanoWallet;
|
|
nanoWallet.walletAddresses.accountList.update(null);
|
|
}
|
|
|
|
@override
|
|
void refresh(Object wallet) {
|
|
final nanoWallet = wallet as NanoWallet;
|
|
nanoWallet.walletAddresses.accountList.refresh();
|
|
}
|
|
|
|
@override
|
|
Future<List<NanoAccount>> getAll(Object wallet) async {
|
|
final nanoWallet = wallet as NanoWallet;
|
|
return (await nanoWallet.walletAddresses.accountList.getAll())
|
|
.map((acc) => NanoAccount(id: acc.id, label: acc.label, balance: acc.balance))
|
|
.toList();
|
|
}
|
|
|
|
@override
|
|
Future<void> addAccount(Object wallet, {required String label}) async {
|
|
final nanoWallet = wallet as NanoWallet;
|
|
await nanoWallet.walletAddresses.accountList.addAccount(label: label);
|
|
}
|
|
|
|
@override
|
|
Future<void> setLabelAccount(Object wallet,
|
|
{required int accountIndex, required String label}) async {
|
|
final nanoWallet = wallet as NanoWallet;
|
|
await nanoWallet.walletAddresses.accountList
|
|
.setLabelAccount(accountIndex: accountIndex, label: label);
|
|
}
|
|
}
|
|
|
|
class CWNano extends Nano {
|
|
@override
|
|
NanoAccountList getAccountList(Object wallet) {
|
|
return CWNanoAccountList(wallet);
|
|
}
|
|
|
|
@override
|
|
Account getCurrentAccount(Object wallet) {
|
|
final nanoWallet = wallet as NanoWallet;
|
|
final acc = nanoWallet.walletAddresses.account;
|
|
return Account(id: acc!.id, label: acc.label, balance: acc.balance);
|
|
}
|
|
|
|
@override
|
|
void setCurrentAccount(Object wallet, int id, String label, String? balance) {
|
|
final nanoWallet = wallet as NanoWallet;
|
|
nanoWallet.walletAddresses.account = NanoAccount(id: id, label: label, balance: balance);
|
|
nanoWallet.regenerateAddress();
|
|
}
|
|
|
|
@override
|
|
List<String> getNanoWordList(String language) {
|
|
return NanoMnemomics.WORDLIST;
|
|
}
|
|
|
|
@override
|
|
WalletService createNanoWalletService(Box<WalletInfo> walletInfoSource) {
|
|
return NanoWalletService(walletInfoSource);
|
|
}
|
|
|
|
@override
|
|
Map<String, String> getKeys(Object wallet) {
|
|
final nanoWallet = wallet as NanoWallet;
|
|
final keys = nanoWallet.keys;
|
|
return <String, String>{
|
|
"seedKey": keys.seedKey,
|
|
};
|
|
}
|
|
|
|
@override
|
|
WalletCredentials createNanoNewWalletCredentials({
|
|
required String name,
|
|
String? password,
|
|
}) =>
|
|
NanoNewWalletCredentials(
|
|
name: name,
|
|
password: password,
|
|
);
|
|
|
|
@override
|
|
WalletCredentials createNanoRestoreWalletFromSeedCredentials({
|
|
required String name,
|
|
required String password,
|
|
required String mnemonic,
|
|
DerivationType? derivationType,
|
|
}) {
|
|
if (derivationType == null) {
|
|
// figure out the derivation type as best we can, otherwise set it to "unknown"
|
|
if (mnemonic.split(" ").length == 12) {
|
|
derivationType = DerivationType.bip39;
|
|
} else {
|
|
derivationType = DerivationType.unknown;
|
|
}
|
|
}
|
|
|
|
return NanoRestoreWalletFromSeedCredentials(
|
|
name: name,
|
|
password: password,
|
|
mnemonic: mnemonic,
|
|
derivationType: derivationType,
|
|
);
|
|
}
|
|
|
|
@override
|
|
WalletCredentials createNanoRestoreWalletFromKeysCredentials({
|
|
required String name,
|
|
required String password,
|
|
required String seedKey,
|
|
DerivationType? derivationType,
|
|
}) {
|
|
if (derivationType == null) {
|
|
// figure out the derivation type as best we can, otherwise set it to "unknown"
|
|
if (seedKey.length == 64) {
|
|
derivationType = DerivationType.nano;
|
|
} else {
|
|
derivationType = DerivationType.unknown;
|
|
}
|
|
}
|
|
|
|
return NanoRestoreWalletFromKeysCredentials(
|
|
name: name,
|
|
password: password,
|
|
seedKey: seedKey,
|
|
derivationType: derivationType,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Object createNanoTransactionCredentials(List<Output> outputs) {
|
|
return NanoTransactionCredentials(
|
|
outputs
|
|
.map((out) => OutputInfo(
|
|
fiatAmount: out.fiatAmount,
|
|
cryptoAmount: out.cryptoAmount,
|
|
address: out.address,
|
|
note: out.note,
|
|
sendAll: out.sendAll,
|
|
extractedAddress: out.extractedAddress,
|
|
isParsedAddress: out.isParsedAddress,
|
|
formattedCryptoAmount: out.formattedCryptoAmount,
|
|
))
|
|
.toList(),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> changeRep(Object wallet, String address) async {
|
|
if ((wallet as NanoWallet).transactionHistory.transactions.isEmpty) {
|
|
throw Exception("Can't change representative without an existing transaction history");
|
|
}
|
|
return wallet.changeRep(address);
|
|
}
|
|
|
|
@override
|
|
Future<void> updateTransactions(Object wallet) async {
|
|
return (wallet as NanoWallet).updateTransactions();
|
|
}
|
|
|
|
@override
|
|
BigInt getTransactionAmountRaw(TransactionInfo transactionInfo) {
|
|
return (transactionInfo as NanoTransactionInfo).amountRaw;
|
|
}
|
|
|
|
@override
|
|
String getRepresentative(Object wallet) {
|
|
return (wallet as NanoWallet).representative;
|
|
}
|
|
}
|
|
|
|
class CWNanoUtil extends NanoUtil {
|
|
// standard:
|
|
@override
|
|
String seedToPrivate(String seed, int index) {
|
|
return ND.NanoKeys.seedToPrivate(seed, index);
|
|
}
|
|
|
|
@override
|
|
String seedToAddress(String seed, int index) {
|
|
return ND.NanoAccounts.createAccount(
|
|
ND.NanoAccountType.NANO, privateKeyToPublic(seedToPrivate(seed, index)));
|
|
}
|
|
|
|
@override
|
|
String seedToMnemonic(String seed) {
|
|
return NanoMnemomics.seedToMnemonic(seed).join(" ");
|
|
}
|
|
|
|
@override
|
|
Future<String> mnemonicToSeed(String mnemonic) async {
|
|
return NanoMnemomics.mnemonicListToSeed(mnemonic.split(' '));
|
|
}
|
|
|
|
@override
|
|
String privateKeyToPublic(String privateKey) {
|
|
// return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!);
|
|
return ND.NanoKeys.createPublicKey(privateKey);
|
|
}
|
|
|
|
@override
|
|
String addressToPublicKey(String publicAddress) {
|
|
return ND.NanoAccounts.extractPublicKey(publicAddress);
|
|
}
|
|
|
|
// universal:
|
|
@override
|
|
String privateKeyToAddress(String privateKey) {
|
|
return ND.NanoAccounts.createAccount(ND.NanoAccountType.NANO, privateKeyToPublic(privateKey));
|
|
}
|
|
|
|
@override
|
|
String publicKeyToAddress(String publicKey) {
|
|
return ND.NanoAccounts.createAccount(ND.NanoAccountType.NANO, publicKey);
|
|
}
|
|
|
|
// standard + hd:
|
|
@override
|
|
bool isValidSeed(String seed) {
|
|
// Ensure seed is 64 or 128 characters long
|
|
if (seed.length != 64 && seed.length != 128) {
|
|
return false;
|
|
}
|
|
// Ensure seed only contains hex characters, 0-9;A-F
|
|
return ND.NanoHelpers.isHexString(seed);
|
|
}
|
|
|
|
// hd:
|
|
@override
|
|
Future<String> hdMnemonicListToSeed(List<String> words) async {
|
|
// if (words.length != 24) {
|
|
// throw Exception('Expected a 24-word list, got a ${words.length} list');
|
|
// }
|
|
final Uint8List salt = Uint8List.fromList(utf8.encode('mnemonic'));
|
|
final Pbkdf2 hasher = Pbkdf2(iterations: 2048);
|
|
final String seed = await hasher.sha512(words.join(' '), salt);
|
|
return seed;
|
|
}
|
|
|
|
@override
|
|
Future<String> hdSeedToPrivate(String seed, int index) async {
|
|
List<int> seedBytes = hex.decode(seed);
|
|
KeyData data = await ED25519_HD_KEY.derivePath("m/44'/165'/$index'", seedBytes);
|
|
return hex.encode(data.key);
|
|
}
|
|
|
|
@override
|
|
Future<String> hdSeedToAddress(String seed, int index) async {
|
|
return ND.NanoAccounts.createAccount(
|
|
ND.NanoAccountType.NANO, privateKeyToPublic(await hdSeedToPrivate(seed, index)));
|
|
}
|
|
|
|
@override
|
|
Future<String> uniSeedToAddress(String seed, int index, String type) {
|
|
if (type == "standard") {
|
|
return Future<String>.value(seedToAddress(seed, index));
|
|
} else if (type == "hd") {
|
|
return hdSeedToAddress(seed, index);
|
|
} else {
|
|
throw Exception('Unknown seed type');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<String> uniSeedToPrivate(String seed, int index, String type) {
|
|
if (type == "standard") {
|
|
return Future<String>.value(seedToPrivate(seed, index));
|
|
} else if (type == "hd") {
|
|
return hdSeedToPrivate(seed, index);
|
|
} else {
|
|
throw Exception('Unknown seed type');
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool isValidBip39Seed(String seed) {
|
|
// Ensure seed is 128 characters long
|
|
if (seed.length != 128) {
|
|
return false;
|
|
}
|
|
// Ensure seed only contains hex characters, 0-9;A-F
|
|
return ND.NanoHelpers.isHexString(seed);
|
|
}
|
|
|
|
// number util:
|
|
|
|
static const int maxDecimalDigits = 6; // Max digits after decimal
|
|
BigInt rawPerNano = BigInt.parse("1000000000000000000000000000000");
|
|
BigInt rawPerNyano = BigInt.parse("1000000000000000000000000");
|
|
BigInt rawPerBanano = BigInt.parse("100000000000000000000000000000");
|
|
BigInt rawPerXMR = BigInt.parse("1000000000000");
|
|
BigInt convertXMRtoNano = BigInt.parse("1000000000000000000");
|
|
// static BigInt convertXMRtoNano = BigInt.parse("1000000000000000000000000000");
|
|
|
|
/// Convert raw to ban and return as BigDecimal
|
|
///
|
|
/// @param raw 100000000000000000000000000000
|
|
/// @return Decimal value 1.000000000000000000000000000000
|
|
///
|
|
Decimal _getRawAsDecimal(String? raw, BigInt? rawPerCur) {
|
|
rawPerCur ??= rawPerNano;
|
|
final Decimal amount = Decimal.parse(raw.toString());
|
|
final Decimal result = (amount / Decimal.parse(rawPerCur.toString())).toDecimal();
|
|
return result;
|
|
}
|
|
|
|
@override
|
|
String getRawAsDecimalString(String? raw, BigInt? rawPerCur) {
|
|
final Decimal result = _getRawAsDecimal(raw, rawPerCur);
|
|
return result.toString();
|
|
}
|
|
|
|
@override
|
|
String truncateDecimal(Decimal input, {int digits = maxDecimalDigits}) {
|
|
Decimal bigger = input.shift(digits);
|
|
bigger = bigger.floor(); // chop off the decimal: 1.059 -> 1.05
|
|
bigger = bigger.shift(-digits);
|
|
return bigger.toString();
|
|
}
|
|
|
|
/// Return raw as a NANO amount.
|
|
///
|
|
/// @param raw 100000000000000000000000000000
|
|
/// @returns 1
|
|
///
|
|
@override
|
|
String getRawAsUsableString(String? raw, BigInt rawPerCur) {
|
|
final String res =
|
|
truncateDecimal(_getRawAsDecimal(raw, rawPerCur), digits: maxDecimalDigits + 9);
|
|
|
|
if (raw == null || raw == "0" || raw == "00000000000000000000000000000000") {
|
|
return "0";
|
|
}
|
|
|
|
if (!res.contains(".")) {
|
|
return res;
|
|
}
|
|
|
|
final String numAmount = res.split(".")[0];
|
|
String decAmount = res.split(".")[1];
|
|
|
|
// truncate:
|
|
if (decAmount.length > maxDecimalDigits) {
|
|
decAmount = decAmount.substring(0, maxDecimalDigits);
|
|
// remove trailing zeros:
|
|
decAmount = decAmount.replaceAllMapped(RegExp(r'0+$'), (Match match) => '');
|
|
if (decAmount.isEmpty) {
|
|
return numAmount;
|
|
}
|
|
}
|
|
|
|
return "$numAmount.$decAmount";
|
|
}
|
|
|
|
@override
|
|
String getRawAccuracy(String? raw, BigInt rawPerCur) {
|
|
final String rawString = getRawAsUsableString(raw, rawPerCur);
|
|
final String rawDecimalString = _getRawAsDecimal(raw, rawPerCur).toString();
|
|
|
|
if (raw == null || raw.isEmpty || raw == "0") {
|
|
return "";
|
|
}
|
|
|
|
if (rawString != rawDecimalString) {
|
|
return "~";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
/// Return readable string amount as raw string
|
|
/// @param amount 1.01
|
|
/// @returns 101000000000000000000000000000
|
|
///
|
|
@override
|
|
String getAmountAsRaw(String amount, BigInt rawPerCur) {
|
|
final Decimal asDecimal = Decimal.parse(amount);
|
|
final Decimal rawDecimal = Decimal.parse(rawPerCur.toString());
|
|
return (asDecimal * rawDecimal).toString();
|
|
}
|
|
|
|
@override
|
|
Future<AccountInfoResponse?> getInfoFromSeedOrMnemonic(
|
|
DerivationType derivationType, {
|
|
String? seedKey,
|
|
String? mnemonic,
|
|
required Node node,
|
|
}) async {
|
|
NanoClient nanoClient = NanoClient();
|
|
nanoClient.connect(node);
|
|
late String publicAddress;
|
|
|
|
if (seedKey != null) {
|
|
if (seedKey.length == 64) {
|
|
try {
|
|
mnemonic = nanoUtil!.seedToMnemonic(seedKey);
|
|
} catch (e) {
|
|
print("not a valid 'nano' seed key");
|
|
}
|
|
}
|
|
if (derivationType == DerivationType.bip39) {
|
|
publicAddress = await hdSeedToAddress(seedKey, 0);
|
|
} else if (derivationType == DerivationType.nano) {
|
|
publicAddress = await seedToAddress(seedKey, 0);
|
|
}
|
|
}
|
|
|
|
if (derivationType == DerivationType.bip39) {
|
|
if (mnemonic != null) {
|
|
seedKey = await hdMnemonicListToSeed(mnemonic.split(' '));
|
|
publicAddress = await hdSeedToAddress(seedKey, 0);
|
|
}
|
|
}
|
|
|
|
if (derivationType == DerivationType.nano) {
|
|
if (mnemonic != null) {
|
|
seedKey = await mnemonicToSeed(mnemonic);
|
|
publicAddress = await seedToAddress(seedKey, 0);
|
|
}
|
|
}
|
|
|
|
AccountInfoResponse? accountInfo = await nanoClient.getAccountInfo(publicAddress);
|
|
if (accountInfo == null) {
|
|
accountInfo = AccountInfoResponse(
|
|
frontier: "", balance: "0", representative: "", confirmationHeight: 0);
|
|
}
|
|
accountInfo.address = publicAddress;
|
|
return accountInfo;
|
|
}
|
|
|
|
@override
|
|
Future<List<DerivationType>> compareDerivationMethods({
|
|
String? mnemonic,
|
|
String? privateKey,
|
|
required Node node,
|
|
}) async {
|
|
String? seedKey = privateKey;
|
|
|
|
if (mnemonic?.split(' ').length == 12) {
|
|
return [DerivationType.bip39];
|
|
}
|
|
if (seedKey?.length == 128) {
|
|
return [DerivationType.bip39];
|
|
} else if (seedKey?.length == 64) {
|
|
try {
|
|
mnemonic = nanoUtil!.seedToMnemonic(seedKey!);
|
|
} catch (e) {
|
|
print("not a valid 'nano' seed key");
|
|
}
|
|
}
|
|
|
|
late String publicAddressStandard;
|
|
late String publicAddressBip39;
|
|
|
|
try {
|
|
NanoClient nanoClient = NanoClient();
|
|
nanoClient.connect(node);
|
|
|
|
if (mnemonic != null) {
|
|
seedKey = await hdMnemonicListToSeed(mnemonic.split(' '));
|
|
publicAddressBip39 = await hdSeedToAddress(seedKey, 0);
|
|
|
|
seedKey = await mnemonicToSeed(mnemonic);
|
|
publicAddressStandard = await seedToAddress(seedKey, 0);
|
|
} else if (seedKey != null) {
|
|
try {
|
|
publicAddressBip39 = await hdSeedToAddress(seedKey, 0);
|
|
} catch (e) {
|
|
return [DerivationType.nano];
|
|
}
|
|
try {
|
|
publicAddressStandard = await seedToAddress(seedKey, 0);
|
|
} catch (e) {
|
|
return [DerivationType.bip39];
|
|
}
|
|
}
|
|
|
|
// check if account has a history:
|
|
AccountInfoResponse? bip39Info;
|
|
AccountInfoResponse? standardInfo;
|
|
|
|
try {
|
|
bip39Info = await nanoClient.getAccountInfo(publicAddressBip39);
|
|
} catch (e) {
|
|
bip39Info = null;
|
|
}
|
|
try {
|
|
standardInfo = await nanoClient.getAccountInfo(publicAddressStandard);
|
|
} catch (e) {
|
|
standardInfo = null;
|
|
}
|
|
|
|
// one of these is *probably* null:
|
|
if (bip39Info == null && standardInfo != null) {
|
|
return [DerivationType.nano];
|
|
} else if (standardInfo == null && bip39Info != null) {
|
|
return [DerivationType.bip39];
|
|
}
|
|
|
|
// we don't know for sure:
|
|
return [DerivationType.nano, DerivationType.bip39];
|
|
} catch (e) {
|
|
return [DerivationType.nano, DerivationType.bip39];
|
|
}
|
|
}
|
|
}
|