Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cw_core/lib/wallet_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class WalletInfoAddressInfo {
required this.accountIndex,
required this.address,
required this.label,
this.txCount,
this.balance,
});

int id;
Expand All @@ -69,6 +71,8 @@ class WalletInfoAddressInfo {
int accountIndex;
String address;
String label;
int? txCount;
int? balance;

static String get tableName => 'walletInfoAddressInfo';

Expand Down
156 changes: 145 additions & 11 deletions cw_zcash/lib/src/zcash_taddress_rotation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,62 @@ class ZcashTaddressRotation {
static Map<int, List<Account>> rotationAccounts = {};
static Map<int, List<Account>> rotationAccountsUsable = {};
static Map<int, List<ShieldedTx>> shieldedAccountsTx = {};
static final Map<String, int> addressBalances = {};
static final Map<String, int> addressTxCounts = {};
static const int minimumUsableAddressPool = 5;
static const int recoveryGapLimit = 20;
static const int maximumManualUnusedGap = recoveryGapLimit - 1;

static void refreshAddressMetadata(final Account account) {
try {
final address = WarpApi.getTAddr(coin, account.id);
addressBalances[address] =
WarpApi.getPoolBalances(coin, account.id, 0, true).transparent;
addressTxCounts[address] = WarpApi.getTxsSync(coin, account.id).length;
} catch (e) {
printV("Failed to refresh metadata for Zcash account ${account.id}: $e");
}
}

static int? balanceForAddress(final String address) => addressBalances[address];

static int? txCountForAddress(final String address) => addressTxCounts[address];

static List<Account> accountsForAccount(final int accountId) =>
rotationAccounts[accountId]?.toList() ?? [];

static Future<String> generateAddressForAccount(final int accountId) async {
final mainSeed = WarpApi.getBackup(coin, accountId).seed;
if (mainSeed == null) {
throw StateError("Transparent address generation requires a wallet seed");
}

rotationAccounts[accountId] ??= WarpApi.getAccountList(coin).where((final account) {
return isSeedForWallet(mainSeed, WarpApi.getBackup(coin, account.id).seed);
}).toList();

if (_trailingUnusedCount(accountId) >= maximumManualUnusedGap) {
throw StateError(
"Use one of the existing transparent addresses before generating another",
);
}

final id = await _createRotationAccount(accountId, mainSeed);
final account = WarpApi.getAccountList(coin).firstWhere((final item) => item.id == id);
final accounts = rotationAccounts[accountId]!;
if (!accounts.any((final item) => item.id == account.id)) {
accounts.add(account);
}
rotationAccountsUsable[accountId] ??= [];
if (!rotationAccountsUsable[accountId]!.any((final item) => item.id == account.id)) {
rotationAccountsUsable[accountId]!.add(account);
}

refreshAddressMetadata(account);
await serializeToFile();
return WarpApi.getTAddr(coin, account.id);
}

static Future<void> init() async {
printV("Deserializing previous state");
if (_isStarted) {
Expand Down Expand Up @@ -172,6 +228,68 @@ class ZcashTaddressRotation {
static Uint8List atob(final String value) =>
Uint8List.fromList(List<int>.from(base64.decode(value)));

static List<Account> _sortedAccountsFor(final int accountId) {
final accounts = rotationAccounts[accountId]?.toList() ?? [];
accounts.sort((final a, final b) {
final aIndex = WarpApi.getBackup(coin, a.id).index;
final bIndex = WarpApi.getBackup(coin, b.id).index;
return aIndex.compareTo(bIndex);
});
return accounts;
}

static bool _isUnused(final Account account) {
refreshAddressMetadata(account);
final address = WarpApi.getTAddr(coin, account.id);
return (addressTxCounts[address] ?? 0) == 0 && (addressBalances[address] ?? 0) == 0;
}

static int _trailingUnusedCount(final int accountId) {
final accounts = _sortedAccountsFor(accountId);
int count = 0;
for (final account in accounts.reversed) {
if (!_isUnused(account)) break;
count++;
}
return count;
}

static int _nextAccountIndex(final int accountId) {
int nextIndex = 0;
for (final account in rotationAccounts[accountId] ?? <Account>[]) {
final index = WarpApi.getBackup(coin, account.id).index;
if (index >= nextIndex) nextIndex = index + 1;
}
return nextIndex;
}

static Future<int> _createRotationAccount(final int accountId, final String mainSeed) {
final seed = seedForOffset(mainSeed);
final name = CRC32.compute(accountId.toString()).toString();
final index = _nextAccountIndex(accountId);
return ZcashWalletService.runInDbMutex(
() => WarpApi.newAccount(coin, name, seed, index),
);
}

static Future<bool> _isRecoveryAccount(final int accountId) async {
final accounts = WarpApi.getAccountList(coin);
String? accountName;
for (final account in accounts) {
if (account.id == accountId) {
accountName = account.name;
break;
}
}
if (accountName == null) return false;

final walletInfos = await WalletInfo.selectList('type = ?', [WalletType.zcash.index]);
for (final walletInfo in walletInfos) {
if (walletInfo.name == accountName) return walletInfo.isRecovery;
}
return false;
}

static Future<void> createAndSweepTAddresses() async {
int chainHeight = 0;
try {
Expand All @@ -196,11 +314,13 @@ class ZcashTaddressRotation {
final acc = accounts[i];
final backup = WarpApi.getBackup(coin, acc.id);
await WarpApi.transparentSync(coin, acc.id, syncHeight);
refreshAddressMetadata(acc);
if (backup.seed == null) continue;
seeds[acc.id] = backup.seed!;
}
for (int i = 0; i < accounts.length; i++) {
final seed = seeds[accounts[i].id]!;
final seed = seeds[accounts[i].id];
if (seed == null) continue;
if ([12, 13, 24, 25].contains(seed.split(" ").length)) {
if (seed.split(" ").last.contains(":tgen:")) continue;
rotationAccounts[accounts[i].id] = [];
Expand Down Expand Up @@ -255,13 +375,26 @@ class ZcashTaddressRotation {

bool didAddNewAccount = false;
for (int i = 0; i < raKeys.length; i++) {
if (rotationAccountsUsable[raKeys[i]]!.length < 5) {
final seed = seedForOffset(seeds[raKeys[i]]!);
final name = CRC32.compute(raKeys[i].toString()).toString();
final id = await ZcashWalletService.runInDbMutex(
() => WarpApi.newAccount(coin, name, seed, rotationAccounts[raKeys[i]]!.length),
);
printV("new id: $id / $seed");
final accountId = raKeys[i];
final mainSeed = seeds[accountId];
if (mainSeed == null || !await _isRecoveryAccount(accountId)) {
continue;
}

if (_trailingUnusedCount(accountId) < recoveryGapLimit) {
await _createRotationAccount(accountId, mainSeed);
didAddNewAccount = true;
}
}
if (didAddNewAccount) {
return createAndSweepTAddresses();
}

for (int i = 0; i < raKeys.length; i++) {
if (rotationAccountsUsable[raKeys[i]]!.length < minimumUsableAddressPool &&
_trailingUnusedCount(raKeys[i]) < maximumManualUnusedGap) {
final id = await _createRotationAccount(raKeys[i], seeds[raKeys[i]]!);
printV("new rotation account id: $id");
printV("${rotationAccounts[raKeys[i]]}");
printV(raKeys[i]);
rotationAccounts[raKeys[i]]!.forEach((final a) {
Expand Down Expand Up @@ -416,21 +549,22 @@ class ZcashTaddressRotation {
for (int i = 0; i < acc.length; i++) {
final b = WarpApi.getBackup(coin, acc[i].id);
printV("$i. ${b.seed?.split(" ").last}, ${b.index}, ${WarpApi.getTAddr(coin, acc[i].id)}");
refreshAddressMetadata(acc[i]);
}
return acc.map((final a) => WarpApi.getTAddr(coin, a.id)).toList();
}

static List<String>? allUsedAddressesForAccount(final int accountId) {
final seed = WarpApi.getBackup(coin, accountId).seed;
if (seed == null) return [];
final acc = rotationAccounts[seed]?.toList();
final acc = rotationAccounts[accountId]?.toList();
if (acc == null) {
printV("Nothing found");
return null;
}
acc.removeWhere((final a1) {
for (int i = 0; i < (rotationAccountsUsable[seed]?.length ?? 0); i++) {
if (rotationAccountsUsable[seed]?[i].id == a1.id) {
for (int i = 0; i < (rotationAccountsUsable[accountId]?.length ?? 0); i++) {
if (rotationAccountsUsable[accountId]?[i].id == a1.id) {
return true;
}
}
Expand Down
14 changes: 10 additions & 4 deletions cw_zcash/lib/src/zcash_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -784,21 +784,27 @@ abstract class ZcashWalletBase

final confirmedPoolBalances = WarpApi.getPoolBalances(coin, accountId, 3, true);
final confirmedBalances = confirmedPoolBalances.unpack();
final confirmedTotal =
confirmedBalances.orchard + confirmedBalances.sapling + confirmedBalances.transparent;

int rotatedTransparentBalance = 0;
for (final account in ZcashTaddressRotation.accountsForAccount(accountId)) {
final accountBalances = WarpApi.getPoolBalances(coin, account.id, 0, true);
rotatedTransparentBalance += accountBalances.transparent;
ZcashTaddressRotation.refreshAddressMetadata(account);
}

int knownOutPending = 0;
ZcashWalletBase.temporarySentTx[accountId]?.forEach((final sTx) {
knownOutPending += sTx.value; // it's negative
});
final confirmedSpendable = confirmedTotal - balances.transparent + knownOutPending;
final confirmedSpendable =
confirmedBalances.orchard + confirmedBalances.sapling + knownOutPending;

unawaited(_autoShield());

balance[CryptoCurrency.zec] = ZcashBalance(
Money.fromInt(confirmedSpendable, currency),
Money.fromInt(spendable - confirmedSpendable, currency),
frozen: Money.zero(currency),
frozen: Money.fromInt(balances.transparent + rotatedTransparentBalance, currency),
);
} catch (e, stackTrace) {
printV("Balance update error: $e");
Expand Down
76 changes: 61 additions & 15 deletions cw_zcash/lib/src/zcash_wallet_addresses.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,11 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {

@override
bool containsAddress(final String address) {
return this.address == address || addressesMap.values.contains(address);
return this.address == address ||
addressesMap.values.contains(address) ||
addressInfos.values.any((final infos) {
return infos.any((final info) => info.address == address);
});
}

static int get coin => ZcashWalletBase.coin;
Expand All @@ -149,25 +153,42 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
address = latestAddress;
}

Map<String, String> _labelsByAddress() {
final labels = <String, String>{};
for (final infos in addressInfos.values) {
for (final info in infos) {
labels[info.address] = info.label;
}
}
return labels;
}

Map<int, List<WalletInfoAddressInfo>> _buildTransparentAddressInfos(
final Map<String, String> labels,
) {
int mapKey = 0;
final infos = ZcashTaddressRotation.accountsForAccount(accountId).map((final account) {
final address = WarpApi.getTAddr(coin, account.id);
ZcashTaddressRotation.refreshAddressMetadata(account);
return WalletInfoAddressInfo(
walletInfoId: walletInfo.internalId,
mapKey: ++mapKey,
accountIndex: account.id,
address: address,
label: labels[address] ?? "",
txCount: ZcashTaddressRotation.txCountForAddress(address),
balance: ZcashTaddressRotation.balanceForAddress(address),
);
}).toList();
return {0: infos};
}

@override
Future<void> init() async {
await _initAddresses();

await ZcashTaddressRotation.init();
int accountIndex = 0;
addressInfos = {
0:
ZcashTaddressRotation.allAddressesForAccount(accountId)?.map((final v) {
return WalletInfoAddressInfo(
walletInfoId: walletInfo.internalId,
mapKey: ++accountIndex,
accountIndex: 0,
address: v,
label: "",
);
}).toList() ??
[],
};
addressInfos = _buildTransparentAddressInfos(_labelsByAddress());
hiddenAddresses.addAll(
ZcashTaddressRotation.allUsedAddressesForAccount(accountId)?.toSet() ?? {},
);
Expand Down Expand Up @@ -227,13 +248,38 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
if (addressPageType != ZcashAddressType.transparentRotated) {
return [];
}
final knownAddresses = addressInfos.values
.expand((final infos) => infos)
.map((final info) => info.address)
.toSet();
final currentAddresses = ZcashTaddressRotation.accountsForAccount(accountId)
.map((final account) => WarpApi.getTAddr(coin, account.id))
.toSet();
if (knownAddresses.length != currentAddresses.length ||
!knownAddresses.containsAll(currentAddresses)) {
addressInfos = _buildTransparentAddressInfos(_labelsByAddress());
}

final List<WalletInfoAddressInfo> allInfos = [];
for (final entry in addressInfos.entries) {
for (final info in entry.value) {
info.txCount = ZcashTaddressRotation.txCountForAddress(info.address);
info.balance = ZcashTaddressRotation.balanceForAddress(info.address);
}
allInfos.addAll(entry.value);
}
return allInfos;
}

Future<String> generateNewTransparentAddress() async {
final labels = _labelsByAddress();
final newAddress = await ZcashTaddressRotation.generateAddressForAccount(accountId);
addressInfos = _buildTransparentAddressInfos(labels);
await saveAddressesInBox();
address = newAddress;
return newAddress;
}

@override
List<ReceivePageOption> get receivePageOptions {
return [
Expand Down
24 changes: 17 additions & 7 deletions lib/new-ui/pages/receive_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -213,15 +213,25 @@ class _NewReceivePageState extends State<NewReceivePage> {
icon: _largeQrMode ? Icon(Icons.share) : widget.addressListViewModel.isRotatingAddress
? CupertinoActivityIndicator()
: Icon(Icons.refresh),
onPressed: () {
onPressed: () async {
if(_largeQrMode) {
ShareUtil.share(
text: widget.addressListViewModel.uri.toString(),
context: context,
ShareUtil.share(
text: widget.addressListViewModel.uri.toString(),
context: context,
);
} else if(widget.addressListViewModel.hasAddressRotation) {
try {
await widget.addressListViewModel.rotateAddress();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
e.toString().replaceFirst("Bad state: ", ""),
),
),
);
} else {
if(widget.addressListViewModel.hasAddressRotation) {
widget.addressListViewModel.rotateAddress();
}
}
}
}):SizedBox.shrink(),
Expand Down
Loading