| 1 | import 'package:cw_core/payment_uris.dart'; |
| 2 | import 'package:cw_core/receive_page_option.dart'; |
| 3 | import 'package:cw_core/utils/print_verbose.dart'; |
| 4 | import 'package:cw_core/wallet_addresses.dart'; |
| 5 | import 'package:cw_core/wallet_info.dart'; |
| 6 | import 'package:cw_zcash/cw_zcash.dart'; |
| 7 | import 'package:cw_zcash/src/zcash_taddress_rotation.dart'; |
| 8 | import 'package:mobx/mobx.dart'; |
| 9 | import 'package:zkool/src/rust/api/account.dart' as zkool_account; |
| 10 | |
| 11 | part 'zcash_wallet_addresses.g.dart'; |
| 12 | |
| 13 | class ZcashWalletAddresses = ZcashWalletAddressesBase with _$ZcashWalletAddresses; |
| 14 | |
| 15 | abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { |
| 16 | ZcashWalletAddressesBase(this.accountId, final WalletInfo walletInfo) : super(walletInfo) { |
| 17 | addressPageType = ZcashReceivePageOption.typeFromString(walletInfo.addressPageType ?? ""); |
| 18 | } |
| 19 | |
| 20 | int accountId; |
| 21 | |
| 22 | static const int transparentType = 0; |
| 23 | static const int shieldedSaplingType = 1; |
| 24 | static const int shieldedOrchardType = 2; |
| 25 | static const int unifiedType = 3; |
| 26 | |
| 27 | @observable |
| 28 | String? transparentAddress; |
| 29 | |
| 30 | @observable |
| 31 | String? _transparentObservableAddress; |
| 32 | |
| 33 | @observable |
| 34 | String? saplingAddress; |
| 35 | |
| 36 | @observable |
| 37 | String? orchardAddress; |
| 38 | |
| 39 | @observable |
| 40 | String? unifiedAddress; |
| 41 | |
| 42 | @observable |
| 43 | bool ironwoodActive = false; |
| 44 | |
| 45 | @action |
| 46 | void setIronwoodActive(final bool active) { |
| 47 | ironwoodActive = active; |
| 48 | } |
| 49 | |
| 50 | @override |
| 51 | @computed |
| 52 | List<ReceivePageOption> get receivePageOptions { |
| 53 | return [ |
| 54 | ...ZcashReceivePageOption.allOptionsFor(ironwood: ironwoodActive), |
| 55 | ...ReceivePageOptions.where((final element) => element != ReceivePageOption.mainnet), |
| 56 | ]; |
| 57 | } |
| 58 | |
| 59 | @override |
| 60 | String get latestAddress { |
| 61 | switch (addressPageType) { |
| 62 | case ZcashAddressType.transparent: |
| 63 | return transparentAddress ?? "unknown transparentAddress"; |
| 64 | case ZcashAddressType.transparentRotated: |
| 65 | // just to display something, after the wallet is synced the address |
| 66 | // will start rotating, and since we can't rotate address until the wallet syncs |
| 67 | // we can't know if some T address already got used, so we would still default to |
| 68 | // the same address. |
| 69 | // So.. we can |
| 70 | // 1) display placeholder text (looks ugly) |
| 71 | // 2) display static address |
| 72 | // 3) display first address from rotated pool which is also static until synced |
| 73 | // 2 seems like an obvious winner simply because it offers little to no benefit over 3 |
| 74 | // and is noticably less complex in implementation |
| 75 | return _transparentObservableAddress ?? |
| 76 | transparentAddress ?? |
| 77 | "unknown transparentAddressRotated"; |
| 78 | case ZcashAddressType.shieldedSapling: |
| 79 | return saplingAddress ?? "unknown saplingAddress"; |
| 80 | case ZcashAddressType.shieldedOrchard: |
| 81 | return orchardAddress ?? "unknown orchardAddress"; |
| 82 | case ZcashAddressType.unifiedType: |
| 83 | return unifiedAddress ?? "unknown unifiedAddress"; |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | @observable |
| 88 | ZcashAddressType? _addressPageType; |
| 89 | |
| 90 | @computed |
| 91 | ZcashAddressType get addressPageType => _addressPageType ?? ZcashAddressType.shieldedOrchard; |
| 92 | |
| 93 | @computed |
| 94 | set addressPageType(final ZcashAddressType newZat) { |
| 95 | _addressPageType = newZat; |
| 96 | init(); |
| 97 | address = latestAddress; |
| 98 | } |
| 99 | |
| 100 | @action |
| 101 | Future<void> setAddressType(final ZcashAddressType type) async { |
| 102 | addressPageType = ZcashReceivePageOption.typeFromString(type.toString()); |
| 103 | walletInfo.addressPageType = type.toString(); |
| 104 | await walletInfo.save(); |
| 105 | } |
| 106 | |
| 107 | @override |
| 108 | @computed |
| 109 | String get addressForExchange => |
| 110 | _transparentObservableAddress ?? transparentAddress ?? "unknown addressForExchange"; |
| 111 | |
| 112 | @override |
| 113 | @computed |
| 114 | String get addressForBuy => |
| 115 | _transparentObservableAddress ?? transparentAddress ?? "unknown addressForBuy"; |
| 116 | |
| 117 | @override |
| 118 | bool containsAddress(final String address) { |
| 119 | return this.address == address || addressesMap.values.contains(address); |
| 120 | } |
| 121 | |
| 122 | @override |
| 123 | @observable |
| 124 | Set<String> hiddenAddresses = {}; |
| 125 | |
| 126 | Future<void> _initAddresses() async { |
| 127 | addressesMap = {}; |
| 128 | addressInfos = await walletInfo.getAddressInfos(); |
| 129 | usedAddresses = await walletInfo.getUsedAddresses(); |
| 130 | manualAddresses = await walletInfo.getManualAddresses(); |
| 131 | hiddenAddresses = await walletInfo.getHiddenAddresses(); |
| 132 | address = latestAddress; |
| 133 | } |
| 134 | |
| 135 | Future<void> _syncRotationHiddenAddresses() async { |
| 136 | final all = (await ZcashTaddressRotation.allAddressesForAccount(accountId)).toSet(); |
| 137 | final used = (await ZcashTaddressRotation.usedAddressesForAccount(accountId)).toSet(); |
| 138 | hiddenAddresses.removeWhere(all.contains); |
| 139 | hiddenAddresses.addAll(used); |
| 140 | } |
| 141 | |
| 142 | @override |
| 143 | Future<void> init() async { |
| 144 | try { |
| 145 | await _init(); |
| 146 | } catch (e) { |
| 147 | printV("init failed, retrying in 2 seconds"); |
| 148 | printV(e); |
| 149 | await Future.delayed(Duration(seconds: 2)); |
| 150 | await init(); |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | static bool isPlaceholderAddress(final String? address) => |
| 155 | address == null || address.isEmpty || address.startsWith('unknown '); |
| 156 | |
| 157 | String _resolvedWalletInfoAddress() { |
| 158 | for (final candidate in [unifiedAddress, orchardAddress, saplingAddress, transparentAddress]) { |
| 159 | if (!isPlaceholderAddress(candidate)) { |
| 160 | return candidate!; |
| 161 | } |
| 162 | } |
| 163 | return latestAddress; |
| 164 | } |
| 165 | |
| 166 | Future<void> _init() async { |
| 167 | await _initAddresses(); |
| 168 | |
| 169 | final addr = await ZcashWalletBase.runWithCoin( |
| 170 | accountId: accountId, |
| 171 | func: (final coin) => zkool_account.getAddresses(c: coin, uaPools: 7), |
| 172 | ); |
| 173 | |
| 174 | transparentAddress = addr.taddr ?? 'unknown addr.taddr'; |
| 175 | saplingAddress = addr.saddr ?? 'unknown addr.saddr'; |
| 176 | orchardAddress = addr.oaddr ?? 'unknown addr.oaddr'; |
| 177 | unifiedAddress = addr.ua ?? 'unknown addr.ua'; |
| 178 | _transparentObservableAddress = await ZcashTaddressRotation.addressForAccount(accountId); |
| 179 | final rotationAddrs = await ZcashTaddressRotation.allAddressesForAccount(accountId); |
| 180 | addressInfos = { |
| 181 | 0: [ |
| 182 | for (int i = 0; i < rotationAddrs.length; i++) |
| 183 | WalletInfoAddressInfo( |
| 184 | walletInfoId: walletInfo.internalId, |
| 185 | mapKey: i + 1, |
| 186 | accountIndex: 0, |
| 187 | address: rotationAddrs[i], |
| 188 | label: "", |
| 189 | ), |
| 190 | ], |
| 191 | }; |
| 192 | await _syncRotationHiddenAddresses(); |
| 193 | await _applyAddressForCurrentType(); |
| 194 | |
| 195 | if (isPlaceholderAddress(walletInfo.address)) { |
| 196 | final resolved = _resolvedWalletInfoAddress(); |
| 197 | if (!isPlaceholderAddress(resolved)) { |
| 198 | walletInfo.address = resolved; |
| 199 | await walletInfo.save(); |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | await saveAddressesInBox(); |
| 204 | } |
| 205 | |
| 206 | Future<void> refreshRotationAddresses() async { |
| 207 | _transparentObservableAddress = await ZcashTaddressRotation.addressForAccount(accountId); |
| 208 | final rotationAddrs = await ZcashTaddressRotation.allAddressesForAccount(accountId); |
| 209 | addressInfos = { |
| 210 | 0: [ |
| 211 | for (int i = 0; i < rotationAddrs.length; i++) |
| 212 | WalletInfoAddressInfo( |
| 213 | walletInfoId: walletInfo.internalId, |
| 214 | mapKey: i + 1, |
| 215 | accountIndex: 0, |
| 216 | address: rotationAddrs[i], |
| 217 | label: "", |
| 218 | ), |
| 219 | ], |
| 220 | }; |
| 221 | await _syncRotationHiddenAddresses(); |
| 222 | await _applyAddressForCurrentType(); |
| 223 | await _persistAddressForExchange(); |
| 224 | } |
| 225 | |
| 226 | Future<void> _persistAddressForExchange() async { |
| 227 | final addr = addressForExchange; |
| 228 | if (isPlaceholderAddress(addr)) return; |
| 229 | await walletInfo.setAddresses({addr: 'transparent'}); |
| 230 | } |
| 231 | |
| 232 | Future<void> _applyAddressForCurrentType() async { |
| 233 | if (_addressPageType == ZcashAddressType.transparentRotated) { |
| 234 | final addr = await ZcashTaddressRotation.addressForAccount(accountId); |
| 235 | if (addr != null) { |
| 236 | address = addr; |
| 237 | } |
| 238 | return; |
| 239 | } |
| 240 | address = latestAddress; |
| 241 | } |
| 242 | |
| 243 | @observable |
| 244 | late String _address = latestAddress; |
| 245 | |
| 246 | @computed |
| 247 | String get address { |
| 248 | return _address; |
| 249 | } |
| 250 | |
| 251 | void set address(final String _$address) => _address = _$address; |
| 252 | |
| 253 | @override |
| 254 | String get primaryAddress => address; |
| 255 | |
| 256 | @override |
| 257 | Future<void> saveAddressesInBox() async { |
| 258 | try { |
| 259 | if (!isPlaceholderAddress(address)) { |
| 260 | walletInfo.address = address; |
| 261 | } |
| 262 | await walletInfo.setAddresses({}); |
| 263 | await _persistAddressForExchange(); |
| 264 | await walletInfo.setAddressInfos(addressInfos); |
| 265 | await walletInfo.setUsedAddresses(usedAddresses.toList()); |
| 266 | await walletInfo.setHiddenAddresses(hiddenAddresses.toList()); |
| 267 | await walletInfo.setManualAddresses(manualAddresses.toList()); |
| 268 | await walletInfo.save(); |
| 269 | await _initAddresses(); |
| 270 | await _syncRotationHiddenAddresses(); |
| 271 | } catch (e) { |
| 272 | printV("Error saving addresses: $e"); |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | @override |
| 277 | Future<void> updateAddressesInBox() async { |
| 278 | await _initAddresses(); |
| 279 | } |
| 280 | |
| 281 | @override |
| 282 | Map<String, String> get usableAddressesMap => addressesMap; |
| 283 | |
| 284 | @override |
| 285 | Map<String, String> get usableAllAddressesMap => allAddressesMap; |
| 286 | |
| 287 | List<WalletInfoAddressInfo> getAddressInfos() { |
| 288 | if (addressPageType != ZcashAddressType.transparentRotated) { |
| 289 | return []; |
| 290 | } |
| 291 | return addressInfos[0] ?? []; |
| 292 | } |
| 293 | |
| 294 | @override |
| 295 | PaymentURI getPaymentUri(final String amount) => ZcashURI(amount: amount, address: address); |
| 296 | } |