Compare commits

...

224 commits

Author SHA1 Message Date
Diego Salazar
e2014df4df
Merge pull request from cypherstack/staging
Staging
2025-04-17 10:25:32 -06:00
julian-CStack
2aedb15489
Merge pull request from Tritonn204/xelis-patch
Xelis Patch
2025-04-15 15:30:08 -06:00
Tritonn204
aa53690f06 switched back to native path separators for Xelis 2025-04-15 12:42:26 -07:00
Tritonn204
89f55c7fec remove libtinfo5.deb 2025-04-15 10:40:07 -07:00
Tritonn204
6aa6b8c950 gitignore fix 2025-04-15 10:39:18 -07:00
Tritonn204
7f69eb8eff git commit -m "Stop tracking Microsoft.Windows* files and folders" 2025-04-15 10:39:07 -07:00
Tritonn204
f45f8cfa76 updated gitignore 2025-04-15 10:36:37 -07:00
Tritonn204
7c408a1af6 updated to xelis_flutter v0.1.1 2025-04-14 21:32:20 -07:00
Tritonn204
13d23a42e3 tx fee fully fixed 2025-04-14 20:08:37 -07:00
Tritonn204
a4de7d34cc windows secp script fix 2025-04-14 13:45:37 -07:00
Tritonn204
27dd2a2134 moved to org xelis repo 2025-04-14 12:29:37 -07:00
Tritonn204
422c191e65 windows secp script fix 2025-04-14 12:27:57 -07:00
Tritonn204
56bcc23c74 fixed path separator on windows, decouple fee calculation from transfer count in TX history 2025-04-14 11:58:03 -07:00
julian
b435838dc3 fix bad patch 2025-04-01 17:19:03 -06:00
julian-CStack
6534bf62dd
Merge pull request from cypherstack/spark-win-fix
update spark ref and coinlib with win build fix
2025-04-01 14:13:48 -06:00
julian
29416bdfc6 update spark ref and coinlib with win build fix 2025-04-01 14:12:21 -06:00
julian-CStack
f43a0ba97d
Merge pull request from cypherstack/ios-min-ver-ref
update liblelantus ref
2025-03-29 12:07:16 -06:00
julian
d9acb6728a update liblelantus ref 2025-03-29 12:06:29 -06:00
julian-CStack
906c2c3a97
Merge pull request from cypherstack/cargo_ndk_3
Cargo ndk 3
2025-03-28 11:39:54 -06:00
julian
913a4ac7c5 add explicit cast and type check 2025-03-28 11:38:20 -06:00
julian
d2e77c9ff0 handle nanswaps api response change 2025-03-28 11:32:22 -06:00
julian
93212d7970 update merged refs 2025-03-28 11:08:31 -06:00
julian
cfba818f12 temporary git ref updates for testing 2025-03-28 11:03:51 -06:00
julian
ce639c9955 docs update 2025-03-28 11:03:51 -06:00
julian-CStack
a1b4a35180
Merge pull request from Tritonn204/xelis
Xelis new TX type handling fix
2025-03-28 07:44:05 -06:00
julian-CStack
86e8c38266
Merge branch 'staging' into xelis 2025-03-28 07:43:44 -06:00
Tritonn204
321ae2e7ad New TX typing bug fix for 2025-03-27 23:44:09 -05:00
julian-CStack
fcc2df5e5b
Merge pull request from cypherstack/back2multi_rust
revert to switching rust versions as required
2025-03-27 14:20:57 -06:00
julian
5bb7813234 revert to switching rust versions as required 2025-03-27 14:19:33 -06:00
julian-CStack
02fec3d581
Merge pull request from cypherstack/gradle_ndk
Gradle ndk
2025-03-27 11:01:47 -06:00
julian
3251159814 update docs re rust version 2025-03-27 10:58:28 -06:00
julian
a62b58041a clean up scripts 2025-03-27 10:43:31 -06:00
julian
72fa218a98 update liblelantus 2025-03-27 09:39:03 -06:00
julian
ec2777b9ff update coinlib and sparkmobile 2025-03-26 17:49:38 -06:00
julian-CStack
65e6c50a66
Merge pull request from cypherstack/epic
fix: Epic on Android, use NDK 28
2025-03-26 17:48:17 -06:00
sneurlax
6f8c6003d1 fix: Windows Epic example and build scripts 2025-03-26 23:47:14 -05:00
sneurlax
dabb2aa1b3 fix: update scripts and docs re: rust verion, ie 1.67.1->1.81 2025-03-26 15:11:54 -05:00
sneurlax
d789ae120b feat: use NDK 28, fix epic on android, remove automatic cbindgen use 2025-03-26 15:11:49 -05:00
julian
815c16a736 NDK and gradle updates 2025-03-26 09:32:26 -06:00
julian-CStack
f1014a6c78
Merge pull request from detherminal/staging
feat: detect bitcoin/monero uri's
2025-03-24 15:40:08 -06:00
dethe
3fbc6daba0 feat: use _applyUri for paste 2025-03-22 01:30:36 +03:00
dethe
15a7a34ece
Merge branch 'cypherstack:staging' into staging 2025-03-21 21:18:08 +03:00
julian-CStack
3118968230
Merge pull request from cypherstack/xelis
Xelis
2025-03-20 20:48:55 -06:00
Julian
37f318a902 linting, formatting, small cleanups, and an extra logging call 2025-03-20 20:47:10 -06:00
Tritonn204
42d7275b51 removed print 2025-03-20 21:32:44 -05:00
julian-CStack
7cf966c443
Merge pull request from Tritonn204/xelis
Xelis bug fixes
2025-03-20 20:28:42 -06:00
Tritonn204
03f4b2fdea removed init finally{} block for Xelis 2025-03-20 21:22:51 -05:00
Tritonn204
4431d8c689 re-enabled null checks for Xelis afer open() 2025-03-20 19:55:46 -05:00
Anthony Tritonn
50163b5d19
Merge branch 'cypherstack:xelis' into xelis 2025-03-20 17:50:48 -07:00
Tritonn204
78b4e2d6b7 Added initCompleter for Xelis 2025-03-20 19:50:01 -05:00
julian-CStack
22ff0b1709
Merge pull request from Tritonn204/xelis
Xelis init/open refactor
2025-03-20 14:35:58 -06:00
Tritonn204
e18a254f0c Merge branch 'xelis' of https://github.com/Tritonn204/stack_wallet into xelis 2025-03-20 15:11:41 -05:00
Tritonn204
ae0631adeb reversed investigative changes 2025-03-20 15:11:37 -05:00
Anthony Tritonn
302ceaaf1f
Merge branch 'xelis' into xelis 2025-03-20 13:00:16 -07:00
Tritonn204
176ed0f89f Xelis init/open refactor 2025-03-20 14:51:12 -05:00
Julian
a503861c0f Merge remote-tracking branch 'origin/staging' into xelis 2025-03-20 12:02:00 -06:00
julian-CStack
b32ec57a8d
Merge pull request from cypherstack/epic
flutter_libepiccash update: reorganize Rust wrapper, add tests, fix example app
2025-03-20 12:00:58 -06:00
sneurlax
87101c86c2 Merge remote-tracking branch 'origin/staging' into epic 2025-03-20 12:57:07 -05:00
sneurlax
8c48930feb fix: update flutter_libepiccash to main 2025-03-20 12:56:41 -05:00
Julian
e4ac7d8569 add missing continue; 2025-03-20 11:34:50 -06:00
Julian
65782bb711 Merge remote-tracking branch 'origin/staging' into xelis 2025-03-20 11:34:22 -06:00
julian-CStack
574d0e82ff
Merge pull request from cypherstack/apple_updates
Apple updates
2025-03-20 11:30:15 -06:00
Julian
fe43785546 more logging 2025-03-20 10:04:16 -06:00
Julian
f77950de68 ensure fee amount is formatted correctly to String 2025-03-20 07:53:38 -06:00
julian-CStack
e07d878eae
Merge pull request from Tritonn204/xelis
Xelis Polish
2025-03-19 15:52:25 -06:00
Tritonn204
8f5d17d026 close on restore for all ExternalWallet instances 2025-03-19 16:31:50 -05:00
Tritonn204
a40fdfecda Merge branch 'xelis' of https://github.com/cypherstack/stack_wallet into xelis 2025-03-19 13:07:25 -05:00
Tritonn204
6e04f8e34d Xelis updates. configured Unix line endings repo-wide 2025-03-19 13:07:14 -05:00
Julian
2298a12afb update themes 2025-03-18 14:48:22 -06:00
Julian
6361d9f048 WIP: wallet exists check 2025-03-18 09:05:11 -06:00
Julian
ce5d9d43e1 use Platform.pathSeparator 2025-03-18 09:04:50 -06:00
dethe
d835b14230 fix: controller positioning 2025-03-18 13:31:28 +03:00
dethe
1cddb14bf1 feat: detect bitcoin/monero uri's
fix: use uri instead of manual parsing

reformat: remove braces and use efficient variables

refactor: add forgotten vars
2025-03-18 13:15:55 +03:00
Julian
5adfee831e Use logger 2025-03-17 17:19:07 -06:00
Julian
b3e02b64de use standard app dir 2025-03-17 17:16:05 -06:00
Julian
564c3ba715 update default themes 2025-03-17 17:14:05 -06:00
sneurlax
441bc8c113 feat: update flutter_libepiccash
TODO: merge https://github.com/cypherstack/flutter_libepiccash/pull/56 "Reorganize codebase, add tests, and fix example app" to main
2025-03-17 17:00:45 -05:00
Julian
8e703f128c temp libspark fix 2025-03-17 14:28:38 -06:00
Julian
c24935dabf logging change 2025-03-17 12:01:53 -06:00
Julian
fe2514e97e lint and code formatting 2025-03-17 08:42:32 -06:00
Julian
b190907cae pubspec lock update 2025-03-17 08:25:41 -06:00
Julian
e39c817ec5 Merge remote-tracking branch 'origin_sw/apple_updates' into xelis
# Conflicts:
#	ios/Podfile.lock
2025-03-17 08:14:50 -06:00
julian-CStack
6b33aee103
Merge pull request from Tritonn204/main
Initial Xelis integration
2025-03-17 08:08:40 -06:00
Julian
30dedee63f ios tweaks and updates 2025-03-17 07:59:13 -06:00
Tritonn204
6e725a5bb5 removed comment 2025-03-14 20:51:14 -07:00
Tritonn204
4feb14c7da updated rust logging method, added mnemonic word validation for Xelis 2025-03-14 20:51:00 -07:00
Tritonn204
0f7e44fadd rebase cleanup + xelis bug fixes 2025-03-14 14:38:05 -07:00
Julian
08c5a5fbc7 macos tweaks and updates 2025-03-13 17:50:56 -06:00
Tritonn204
0d20cb6b3b remove logging package from test_node_connection 2025-03-13 16:17:32 -07:00
Tritonn204
dd67d2fdbb reversions and deletions as per request 2025-03-13 16:17:32 -07:00
Tritonn204
f7b73620e2 added port information to xelis connection invocations 2025-03-13 16:17:32 -07:00
Tritonn204
4e26e4c246 updated dep template 2025-03-13 16:17:32 -07:00
Tritonn204
c1cd9869c0 removed xelis submodule in favor of git tag dependency 2025-03-13 16:17:13 -07:00
Tritonn204
448fd0c94d keep version update without frostdart in windows build scripts 2025-03-13 16:17:13 -07:00
Tritonn204
0ed0ef3ed9 removed frostdart from windows build scripts 2025-03-13 16:17:13 -07:00
Tritonn204
f56519ec14 svg change reversal 2025-03-13 16:17:13 -07:00
Tritonn204
8c6f660ec6 libepiccash build script corrections, pubspec template adjusted for staging 2025-03-13 16:17:13 -07:00
Tritonn204
2879e5bc03 updated fee estimation params in xelis_flutter 2025-03-13 16:17:13 -07:00
Tritonn204
a06945b3a2 xelis_flutter correction 2025-03-13 16:17:13 -07:00
Tritonn204
8b41d0b588 xelis_flutter branch switch adopted 2025-03-13 16:17:13 -07:00
Tritonn204
102fab5fae updated price_test, corrected outgoing tx db entries for xelis 2025-03-13 16:17:13 -07:00
Tritonn204
0dad4ad591 updated linux devops for new Xelis flutter setup, fixed Xelis fee estimation bug 2025-03-13 16:17:13 -07:00
Tritonn204
2f02d4dc58 removed Xelis lib from build scripts 2025-03-13 16:17:13 -07:00
Anthony Tritonn
e04efc7247 Apple/iOS builds working (with PRs from plugins) 2025-03-13 16:17:02 -07:00
Anthony Tritonn
1463ea7972 Xelis price service added, wallet storage bugs fixed, updated to apple-compliant xelis lib 2025-03-13 16:16:52 -07:00
Tritonn204
e1964ea68e adjusted for xelis flutter refactor 2025-03-13 16:16:24 -07:00
Tritonn204
c6bff81648 Android works 2025-03-13 16:16:24 -07:00
Tritonn204
7a3558e7e9 Slight polish for Xelis integration 2025-03-13 16:16:24 -07:00
Tritonn204
24a20238bc Xelis base integration 2025-03-13 16:16:24 -07:00
Tritonn204
96478de9cc gitignore 2025-03-13 16:10:37 -07:00
Tritonn204
5f9466ca3c build script + dep updates, xelis library integration progress 2025-03-13 16:10:37 -07:00
Tritonn204
34cee82018 added xelis enum variants 2025-03-13 16:07:30 -07:00
Tritonn204
2061eba2f2 xelis CryptoCurrency definition added, bridged wallet wrapper started 2025-03-13 16:07:30 -07:00
Tritonn204
2edfe0f3cc xelis boiler plate early setup 2025-03-13 16:07:30 -07:00
Julian
709eebc1b7 update frostdart submodule 2025-03-13 16:06:31 -06:00
Julian
dbc805ed21 update liblelantus submodule 2025-03-13 14:55:37 -06:00
julian-CStack
7439dbf9fc
Merge pull request from detherminal/staging
fix: use trocador provider icons
2025-03-13 11:24:14 -06:00
dethe
bf5bfc3d71 fix: use trocador provider icons 2025-03-12 23:51:29 +03:00
julian
60d47f235d update sparkmobile lib 2025-02-26 16:32:59 -06:00
julian
48d2ac5e9b firo desktop wallet header refactor 2025-02-26 16:32:59 -06:00
julian
3146f4dce9 WIP firo balance display desktop 2025-02-26 16:32:59 -06:00
julian
5ddde67555 hide zero lelantus balance on desktop 2025-02-26 16:32:59 -06:00
julian
fd223ddaea android build files ignore 2025-02-25 11:47:07 -06:00
julian
03dcb2babf update barcode_scan2 for flutter 3.29.0 2025-02-25 11:47:07 -06:00
julian
44f0d0d8df flutter_libmonero is no more 2025-02-25 11:47:07 -06:00
julian
ed65ab5648 flutter version update 2025-02-25 11:47:07 -06:00
julian
29802ddda5 update name functionality w/ basic json pretty view 2025-02-25 08:32:53 -06:00
julian
7dd919f795 disable manage for pre reg names for now 2025-02-25 08:32:53 -06:00
julian
6539b75f9f mobile/desktop name ui clean up and transfer name functionality 2025-02-25 08:32:53 -06:00
julian
05767dea13 name tx note tweaks 2025-02-25 08:32:53 -06:00
julian
9fc6368644 WIP: manage domain ui 2025-02-25 08:32:53 -06:00
julian
edaaeda838 clean up domain names management ui somewhat 2025-02-25 08:32:53 -06:00
julian
ae97d3ce24 domain name input length restriction 2025-02-25 08:32:53 -06:00
julian
a576e45199 WIP: namecoin names desktop/mobile specific layout tweaks, and various clean up 2025-02-25 08:32:53 -06:00
julian
ad07d7abb9 fix: pop buy name dialog on success 2025-02-25 08:32:53 -06:00
julian
0d1bfc191f clean up logging 2025-02-25 08:32:53 -06:00
julian
06b0584691 better encoding 2025-02-25 08:32:53 -06:00
julian
7cdbe581ef show error dialog 2025-02-25 08:32:53 -06:00
julian
116b5747cf validate name value length 2025-02-25 08:32:53 -06:00
julian
63474ac6eb better base dialog widget 2025-02-25 08:32:53 -06:00
julian
321cf855a7 clean up record info display 2025-02-25 08:32:53 -06:00
julian
51b0cc1510 add method to check for and ignore name outputs in balance 2025-02-25 08:32:53 -06:00
julian
cd9907db5c WIP: namecoin domain name buy and add record ui 2025-02-25 08:32:53 -06:00
julian
667f2f504e change particl default address type to old 2025-02-25 08:32:53 -06:00
julian
f1e1bd0dc0 WIP names gui 2025-02-25 08:32:53 -06:00
julian
534ee51275 update namecoin dns More option name and icon 2025-02-25 08:32:53 -06:00
julian
cdffec81df refactor auto register NAME NEW process 2025-02-25 08:32:53 -06:00
julian
69b3e9b1ab handle name op output selection based on name op type 2025-02-25 08:32:53 -06:00
julian
6d291408a3 hack in name op utxo confirmation check 2025-02-25 08:32:53 -06:00
julian
873fc63045 consistent log level edit 2025-02-25 08:32:53 -06:00
julian
5a39bb2b64 auto focus cursor in search field on desktop 2025-02-25 08:32:53 -06:00
julian
3ea469bb1f use private key for deterministic salt 2025-02-25 08:32:53 -06:00
julian
c277cd9641 update namecoin_dart version 2025-02-25 08:32:53 -06:00
julian
0623023b3a WIP names 2025-02-25 08:32:53 -06:00
julian
2e737b5911 build runner update mocks 2025-02-25 08:32:53 -06:00
julian-CStack
5169dfd7fe
Merge pull request from cypherstack/sneurlax-patch-1
Replace `vapigen` with `valac` in Ubuntu 20.04 build docs
2025-02-25 07:36:11 -06:00
julian-CStack
1c6b84c823
Merge branch 'staging' into sneurlax-patch-1 2025-02-25 07:35:52 -06:00
sneurlax
56314e7f24
Replace vapigen with valac for Ubuntu 20.04 build docs
If `valac` works, merge this PR.
2025-02-23 19:05:56 -06:00
julian
024086910b log some extra wallet info 2025-02-06 12:14:10 -06:00
julian
8ca4421c2a adjust log level privacy warning text 2025-02-06 12:14:10 -06:00
julian
694910ab18 replace deprecated logd function, clean up some logging and fix some log level logging levels 2025-02-06 12:14:10 -06:00
julian-CStack
2dfacd42c5
Merge pull request from cypherstack/logging
Logging
2025-02-05 17:22:24 -06:00
julian
c4db10c9d3 add deprecated message 2025-02-05 17:12:44 -06:00
julian
4589e42ac8 refactor log function name 2025-02-05 17:07:49 -06:00
julian
68c7ba6910 log level preference 2025-02-05 17:01:40 -06:00
julian
176f0ba331 clean up some prints and use logging instead in some places 2025-02-05 14:57:55 -06:00
julian
b9ed3ae0a0 update file_picker 2025-02-05 14:47:56 -06:00
Julian
452f4a7daa macos file entitlements for updated file picker to allow SWB save/load 2025-02-05 14:08:45 -06:00
Julian
b05f664088 disable logs location selection on macos for now 2025-02-05 13:48:47 -06:00
julian
de047339f8 WIP updated logging 2025-02-05 11:31:57 -06:00
julian
90e421174a option to rescan on lelantus scanning enable 2025-01-31 12:50:40 -06:00
julian
75f6e65fd9 expose firo advanced features in dev options 2025-01-31 12:50:40 -06:00
julian
1d9b2e39fe update trocador api 2025-01-31 12:50:40 -06:00
julian
6c31e4662d desktop single coin wallet delete ui fix 2025-01-31 12:50:40 -06:00
Julian
1a3a09a325 firo balance type button changes 2025-01-31 12:50:40 -06:00
Julian
ab450684b2 clarify checking of new/unmined spark coins 2025-01-31 12:50:40 -06:00
Julian
9bd343c987 obscure seed words while restoring in progress 2025-01-31 12:50:40 -06:00
Julian
128dc14ce7 update min flutter/dart sdk 2025-01-31 12:50:40 -06:00
julian
cb11d58c47 fix: double encoded qr address prefix 2025-01-15 15:17:08 -06:00
julian
1a094d3745 update test 2025-01-14 16:55:25 -06:00
julian
a01dce1c72 Only fetch full monero transactions via ffi if required 2025-01-14 16:55:25 -06:00
julian
38e66bfcb1 chore: update pubspec.lock 2025-01-13 17:17:34 -06:00
julian
0d1bf5895d fix: incorrect number of confirms for monero (and wownero) outputs and transactions 2025-01-13 17:17:34 -06:00
julian-CStack
ad667025ac
Merge pull request from singpolyma/fastlane
Add fastlane metadata
2025-01-13 15:07:37 -06:00
julian-CStack
e7ad498b71
Merge branch 'staging' into fastlane 2025-01-13 15:07:21 -06:00
julian-CStack
975fe733f0
Merge pull request from Cyrix126/fix_1061
fix: Login/Password of Node dialog can't be made empty after setting a value
2025-01-13 14:35:34 -06:00
julian-CStack
22f9d4c653
Merge branch 'staging' into fix_1061 2025-01-13 14:35:20 -06:00
Louis-Marie Baer
130895a449 fix: update logins even if null 2025-01-13 20:35:24 +01:00
julian-CStack
2aa548e3e0
Merge pull request from Cyrix126/node_auth
handle authentication in test of monero nodes
2025-01-13 13:09:55 -06:00
julian-CStack
4c7cb0c309
Merge branch 'staging' into node_auth 2025-01-13 13:09:23 -06:00
Louis-Marie Baer
b44cde334c fix: no 24 words option for restoring namecoin wallet 2025-01-06 09:46:36 -06:00
Louis-Marie Baer
516b503f31 use monero_rpc from pub dev 2024-12-27 20:19:33 +01:00
Louis-Marie Baer
1137b50b8a fix: 2024-12-26 13:56:37 +01:00
Louis-Marie Baer
d64c344fd0 feat: handle test monero node authentication 2024-12-24 17:00:19 +01:00
julian
4fc2a7acfa fix: optional cast error 2024-12-17 19:27:49 -06:00
julian
ba1ab977d6 chore: build runner 2024-12-17 16:37:22 -06:00
julian
0e32e8a408 fix: Issue where spark spend may not have appeared as confirmed 2024-12-17 16:37:22 -06:00
julian
f97ef50978 fix: update hash format and use better sector/page size 2024-12-17 16:37:22 -06:00
julian
4439ad70d2 fix: incremental spark data cache 2024-12-17 16:37:22 -06:00
julian
2028505367 fix: spark coin confirmations issue 2024-12-17 16:37:22 -06:00
julian
1884bfbaf7 fix: tweak sync percent feature and track progress during spark anon set download 2024-12-17 16:37:22 -06:00
julian
c1ef98833a reduced pings 2024-12-17 16:37:22 -06:00
julian
ae10bef0ee fix: updated spark data calls and caching 2024-12-17 16:37:22 -06:00
julian
d6aec00b58 WIP: paginated spark anon set prep 2024-12-17 16:37:22 -06:00
julian
744f273862 WIP: fix electrumx failovers, add some "default" firo nodes, and tweak firo pings 2024-12-17 16:37:22 -06:00
Diego Salazar
34ad1d9022
Merge pull request from cypherstack/julian
Various
2024-12-13 10:52:03 -07:00
julian
e9aa2d6a30 fix: force override of a dep we don't even use so that sqlite doesn't complain 2024-12-12 17:26:55 -06:00
julian
eeb595e0d9 fix: Update device-locale dep version to handle linux crashes with certain locale configurations 2024-12-12 17:05:58 -06:00
julian
2e0ac0b2f5 chore: extra logging 2024-12-12 16:58:01 -06:00
julian
c56038cadf fix: Prevent failed keys fetch from disabling display of mnemonic 2024-12-12 16:54:55 -06:00
julian
9c64ed6316 chore: update deps 2024-12-12 16:53:21 -06:00
julian
120952156f fix: Linux qr scan would previously load the last scanned data if it exists 2024-12-11 18:20:44 -06:00
julian
3adddc2368 fix: possible race condition when processing desktop qr data by returning the result on pop rather than using a callback, clean up some linter stuff, and prevent calling setState in dispose 2024-12-11 17:40:22 -06:00
julian
e749c62ccd chore: satisfy linter 2024-12-11 16:41:00 -06:00
julian
cdf2dd8819 feat: xmrrpc (and wowrpc) node uri scanning ui for adding new nodes 2024-12-11 15:26:12 -06:00
julian
4af7243265 feat: xmrrpc (and wowrpc) uri parsing + tests 2024-12-11 11:54:14 -06:00
julian
2873595e40 chore: use libsecret mirror due to instability of gitlab.gnome.org 2024-12-11 09:24:42 -06:00
julian
71609c34b0 feat: allow setting the restore/refresh height in xmr/wow wallets 2024-12-10 14:51:37 -06:00
julian
937550cb04 fix: xmr/wow node connection was replacing onion addresses with an empty string 2024-12-09 16:29:02 -06:00
julian
fed7ae91d9 fix: frost reshare issue when initiator is a new participant of a 1/2 config 2024-12-09 15:29:25 -06:00
julian
e9252a4d46 fix: ensure xmr/wow transactions updated on balance changed 2024-12-09 13:07:56 -06:00
julian
ec1b5d7d2b fix: propagate restore errors correctly. Should fix https://github.com/cypherstack/stack_wallet/issues/1047 2024-12-09 10:37:39 -06:00
julian-CStack
9f0f94b29b
Merge pull request from cypherstack/staging
Staging
2024-12-03 12:45:48 -06:00
Stephen Paul Weber
0d8df43d97
Add fastlane metadata
For 
2024-11-15 11:23:22 -05:00
340 changed files with 18837 additions and 8106 deletions
.gitignore
android
asset_sources/default_themes
crypto_plugins
docs
fastlane
ios
Podfile.lock
Runner.xcodeproj/xcshareddata/xcschemes
lib
db
electrumx_rpc
main.dart
models
networking
pages

11
.gitignore vendored
View file

@ -20,6 +20,9 @@
# is commented out by default.
#.vscode/
#CppWinRT manual install
Microsoft.Windows*
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
@ -29,6 +32,7 @@
.pub-cache/
.pub/
/build/
android/app/.cxx
# Web related
lib/generated_plugin_registrant.dart
@ -58,8 +62,6 @@ coverage
scripts/**/build
/lib/external_api_keys.dart
libcw_monero.dll
libcw_wownero.dll
libepic_cash_wallet.dll
libmobileliblelantus.dll
libtor_ffi.dll
@ -69,6 +71,10 @@ secp256k1.dll
/lib/app_config.g.dart
/android/app/src/main/app_icon-playstore.png
# Dart generated files (Freezed, Riverpod, GoRouter etc..)
lib/**/*.g.dart
lib/**/*.freezed.dart
## other generated project files
pubspec.yaml
@ -105,3 +111,4 @@ scripts/linux/build/libsecret/subprojects/gi-docgen/.meson-subproject-wrap-hash.
crypto_plugins/cs_monero/built_outputs
crypto_plugins/cs_monero/build
crypto_plugins/*.diff

View file

@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View file

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip

View file

@ -18,7 +18,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.6.0' apply false
id "com.android.application" version '8.7.0' apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}

@ -1 +1 @@
Subproject commit 0bb1b1ced6e0d3c66e383698f89825754c692986
Subproject commit 25e6cb3a3e7bee04e425af6beccb47e8d0708fdb

@ -1 +1 @@
Subproject commit 5b08645a5b5d30955f4bde2a624ff89ef516e452
Subproject commit 7b325030bce46a423aa46497d1a608b7a8a58976

@ -1 +1 @@
Subproject commit 2451deab817b456ad93d5579c0d0687cb681392a
Subproject commit 6f1310eccd336fb3c8dc00b61e39a3f0f3a2b59a

View file

@ -13,12 +13,12 @@ Here you will find instructions on how to install the necessary tools for buildi
The following instructions are for building and running on a Linux host. Alternatively, see the [Mac](#mac-host) and/or [Windows](#windows-host) section. This entire section (except for the Android Studio section) needs to be completed in WSL if building on a Windows host.
### Flutter
Install Flutter 3.24.3 by [following their guide](https://docs.flutter.dev/get-started/install/linux/desktop?tab=download#install-the-flutter-sdk). You can also clone https://github.com/flutter/flutter, check out the `3.24.3` tag, and add its `flutter/bin` folder to your PATH as in
Install Flutter 3.29.2 by [following their guide](https://docs.flutter.dev/get-started/install/linux/desktop?tab=download#install-the-flutter-sdk). You can also clone https://github.com/flutter/flutter, check out the `3.29.2` tag, and add its `flutter/bin` folder to your PATH as in
```sh
FLUTTER_DIR="$HOME/development/flutter"
git clone https://github.com/flutter/flutter.git "$FLUTTER_DIR"
cd "$FLUTTER_DIR"
git checkout 3.24.3
git checkout 3.29.2
echo 'export PATH="$PATH:'"$FLUTTER_DIR"'/bin"' >> "$HOME/.profile"
source "$HOME/.profile"
flutter precache
@ -38,7 +38,7 @@ Use `Tools > SDK Manager` to install:
- `SDK Tools > Android SDK command line tools`
- `SDK Tools > CMake`
and for Android builds,
- `SDK Tools > Android SDK (API 30)`
- `SDK Tools > Android SDK (API 35)`
- `SDK Tools > NDK`
Then in `File > Settings > Plugins`, install the **Flutter** and **Dart** plugins and restart the IDE. In `File > Settings > Languages & Frameworks > Flutter > Editor`, enable auto format on save to match the project's code style. If you have problems with the Dart SDK, make sure to run `flutter` in a terminal to download it (use `source ~/.bashrc` to update your environment variables if you're still using the same terminal from which you ran `setup.sh`). Run `flutter doctor` to install any missing dependencies and review and agree to any license agreements.
@ -58,7 +58,7 @@ sudo apt-get install libssl-dev curl unzip automake build-essential file pkg-con
For Ubuntu 20.04,
```
sudo apt-get install vapigen
sudo apt-get install valac
pip3 install --upgrade meson==0.64.1 markdown==3.4.1 markupsafe==2.1.1 jinja2==3.1.2 pygments==2.13.0 toml==0.10.2 typogrify==2.0.7 tomli==2.0.1
```
@ -68,20 +68,13 @@ sudo apt install pipx libgcrypt20-dev libglib2.0-dev libsecret-1-dev
pipx install meson==0.64.1 markdown==3.4.1 markupsafe==2.1.1 jinja2==3.1.2 pygments==2.13.0 toml==0.10.2 typogrify==2.0.7 tomli==2.0.1
```
Install `libtinfo5` (required by [monero_c](https://github.com/MrCyjaneK/monero_c), should be dropped in the future):
```
wget http://mirrors.kernel.org/ubuntu/pool/universe/n/ncurses/libtinfo5_6.3-2ubuntu0.1_amd64.deb -O libtinfo5.deb \
&& apt install ./libtinfo5.deb \
&& rm libtinfo5.deb
```
Install [Rust](https://www.rust-lang.org/tools/install) via [rustup.rs](https://rustup.rs), the required Rust toolchains, and `cargo-ndk 2.12.7` with command:
Install [Rust](https://www.rust-lang.org/tools/install) via [rustup.rs](https://rustup.rs), the required Rust toolchains, and `cargo-ndk` with command:
```
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.bashrc
rustup install 1.67.1 1.71.0 1.72.0 1.73.0
rustup default 1.67.1
cargo install cargo-ndk --version 2.12.7 --locked
rustup install 1.85.1 1.81.0
rustup default 1.85.1
cargo install cargo-ndk
```
Android specific dependencies:
@ -162,19 +155,6 @@ cd scripts
cd scripts
./build_app.sh -a stack_wallet -p linux
```
<!--
##### Remove system packages (may be needed for building flutter_libmonero)
[`flutter_libmonero`](https://github.com/cypherstack/flutter_libmonero) may have issues building due to conflicts with system packages: if so, follow this section.
Remove pre-installed system libraries for the following packages built by cryptography plugins in the crypto_plugins folder: `boost iconv libjson-dev libsecret openssl sodium unbound zmq`. You can use
```
sudo apt list --installed | grep boost
```
for example to find which pre-installed packages you may need to remove with `sudo apt remove`. Be careful, as some packages (especially boost) are linked to GNOME (GUI) packages: when in doubt, remove `-dev` packages first like with
```
sudo apt-get remove '^libboost.*-dev.*'
```
TODO: configure compiler to prefer built over system libraries. Should already use them? -->
#### Building plugins and configure for Windows
Install dependencies like MXE:
@ -229,13 +209,13 @@ brew install brotli cairo coreutils gdbm gettext glib gmp libevent libidn2 libng
```
<!-- TODO: determine which of the above list are not needed at all. -->
Download and install [Rust](https://www.rust-lang.org/tools/install). [Rustup](https://rustup.rs/) is recommended for Rust setup. Use `rustc` to confirm successful installation. Install toolchains 1.67.1 and 1.72.0 and `cbindgen` and `cargo-lipo` too. You will also have to add the platform target(s) `aarch64-apple-ios` and/or `aarch64-apple-darwin`. You can use the command(s):
Download and install [Rust](https://www.rust-lang.org/tools/install). [Rustup](https://rustup.rs/) is recommended for Rust setup. Use `rustc` to confirm successful installation. Install toolchains 1.81.0 and 1.85.1 and `cbindgen` and `cargo-lipo` too. You will also have to add the platform target(s) `aarch64-apple-ios` and/or `aarch64-apple-darwin`. You can use the command(s):
```
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.bashrc
rustup install 1.67.1 1.71.0 1.72.0 1.73.0
rustup default 1.67.1
cargo install cargo-ndk --version 2.12.7 --locked
rustup install 1.85.1 1.81.0
rustup default 1.85.1
cargo install cargo-ndk
cargo install cbindgen cargo-lipo
rustup target add aarch64-apple-ios aarch64-apple-darwin
```
@ -243,7 +223,7 @@ rustup target add aarch64-apple-ios aarch64-apple-darwin
Optionally download [Android Studio](https://developer.android.com/studio) as an IDE and activate its Dart and Flutter plugins. VS Code may work as an alternative, but this is not recommended.
### Flutter
Install [Flutter](https://docs.flutter.dev/get-started/install) 3.24.3 on your Mac host by following [these instructions](https://docs.flutter.dev/get-started/install/macos). Run `flutter doctor` in a terminal to confirm its installation.
Install [Flutter](https://docs.flutter.dev/get-started/install) 3.29.2 on your Mac host by following [these instructions](https://docs.flutter.dev/get-started/install/macos). Run `flutter doctor` in a terminal to confirm its installation.
### Build plugins and configure
#### Building plugins for iOS
@ -304,22 +284,19 @@ If the DLLs were built on the WSL filesystem instead of on Windows, copy the res
- `stack_wallet/crypto_plugins/flutter_libepiccash/scripts/windows/build/libepic_cash_wallet.dll`
- `stack_wallet/crypto_plugins/flutter_liblelantus/scripts/windows/build/libmobileliblelantus.dll`
<!--
- `stack_wallet/crypto_plugins/flutter_libmonero/scripts/windows/build/libcw_monero.dll`
- `stack_wallet/crypto_plugins/flutter_libmonero/scripts/windows/build/libcw_wownero.dll`
-->
<!-- TODO: script the copying or installation of libraries from WSL2 to the parent Windows host -->
Frostdart will be built by the Windows host later.
### Install Flutter on Windows host
Install Flutter 3.24.3 on your Windows host (not in WSL2) by [following their guide](https://docs.flutter.dev/get-started/install/windows/desktop?tab=download#install-the-flutter-sdk) or by cloning https://github.com/flutter/flutter, checking out the `3.24.3` tag, and adding its `flutter/bin` folder to your PATH as in
Install Flutter 3.29.2 on your Windows host (not in WSL2) by [following their guide](https://docs.flutter.dev/get-started/install/windows/desktop?tab=download#install-the-flutter-sdk) or by cloning https://github.com/flutter/flutter, checking out the `3.29.2` tag, and adding its `flutter/bin` folder to your PATH as in
```bat
@echo off
set "FLUTTER_DIR=%USERPROFILE%\development\flutter"
git clone https://github.com/flutter/flutter.git "%FLUTTER_DIR%"
cd /d "%FLUTTER_DIR%"
git checkout 3.24.3
git checkout 3.29.2
setx PATH "%PATH%;%FLUTTER_DIR%\bin"
echo Flutter setup completed. Please restart your command prompt.
```
@ -329,9 +306,9 @@ Run `flutter doctor` in PowerShell to confirm its installation.
### Rust
Install [Rust](https://www.rust-lang.org/tools/install) on the Windows host (not in WSL2). Download the installer from [rustup.rs](https://rustup.rs), make sure it works on the commandline (you may need to open a new terminal), and install the following versions:
```
rustup install 1.67.1 1.71.0 1.72.0 1.73.0
rustup default 1.67.1
cargo install cargo-ndk --version 2.12.7 --locked
rustup install 1.85.1 1.81.0
rustup default 1.85.1
cargo install cargo-ndk
```
### Windows SDK and Developer Mode

1
fastlane/Appfile Normal file
View file

@ -0,0 +1 @@
package_name("com.cypherstack.stackwallet")

View file

@ -0,0 +1,11 @@
Stack Wallet is a fully open source cryptocurrency wallet. With an easy to use user interface and quick and speedy transactions, this wallet is ideal for anyone no matter how much they know about the cryptocurrency space. The app is actively maintained to provide new user friendly features.
Highlights include:
- 10 Different cryptocurrencies
- All private keys and seeds stay on device and are never shared.
- Easy backup and restore feature to save all the information that's important to you.
- Trading cryptocurrencies through our partners.
- Custom address book
- Favorite wallets with fast syncing
- Custom Nodes.
- Open source software.

Binary file not shown.

After

(image error) Size: 26 KiB

View file

@ -0,0 +1 @@
An open source, non-custodial cryptocurrency wallet.

View file

@ -9,7 +9,7 @@ PODS:
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
- cs_monero_flutter_libs (0.0.1):
- cs_monero_flutter_libs_ios (0.0.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
@ -87,8 +87,6 @@ PODS:
- "sqlite3 (3.46.0+1)":
- "sqlite3/common (= 3.46.0+1)"
- "sqlite3/common (3.46.0+1)"
- "sqlite3/dbstatvtab (3.46.0+1)":
- sqlite3/common
- "sqlite3/fts5 (3.46.0+1)":
- sqlite3/common
- "sqlite3/perf-threadsafe (3.46.0+1)":
@ -97,8 +95,7 @@ PODS:
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- "sqlite3 (~> 3.46.0+1)"
- sqlite3/dbstatvtab
- sqlite3 (~> 3.46.0)
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
@ -112,12 +109,14 @@ PODS:
- Flutter
- wakelock_plus (0.0.1):
- Flutter
- xelis_flutter (0.0.1):
- Flutter
DEPENDENCIES:
- barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`)
- coinlib_flutter (from `.symlinks/plugins/coinlib_flutter/darwin`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- cs_monero_flutter_libs (from `.symlinks/plugins/cs_monero_flutter_libs/ios`)
- cs_monero_flutter_libs_ios (from `.symlinks/plugins/cs_monero_flutter_libs_ios/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- devicelocale (from `.symlinks/plugins/devicelocale/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
@ -141,6 +140,7 @@ DEPENDENCIES:
- tor_ffi_plugin (from `.symlinks/plugins/tor_ffi_plugin/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- xelis_flutter (from `.symlinks/plugins/xelis_flutter/ios`)
SPEC REPOS:
trunk:
@ -160,8 +160,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/coinlib_flutter/darwin"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
cs_monero_flutter_libs:
:path: ".symlinks/plugins/cs_monero_flutter_libs/ios"
cs_monero_flutter_libs_ios:
:path: ".symlinks/plugins/cs_monero_flutter_libs_ios/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
devicelocale:
@ -208,17 +208,19 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/url_launcher_ios/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
xelis_flutter:
:path: ".symlinks/plugins/xelis_flutter/ios"
SPEC CHECKSUMS:
barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0
coinlib_flutter: 9275e8255ef67d3da33beb6e117d09ced4f46eb5
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
cs_monero_flutter_libs: 43cda3474c2bc907f2b2b5bb26fd89cb864fcfc6
cs_monero_flutter_libs_ios: fd353631682247f72a36493ff060d4328d6f720d
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_libepiccash: 36241aa7d3126f6521529985ccb3dc5eaf7bb317
flutter_libsparkmobile: 6373955cc3327a926d17059e7405dde2fb12f99f
@ -231,14 +233,14 @@ SPEC CHECKSUMS:
lelantus: 417f0221260013dfc052cae9cf4b741b6479edba
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: 72f86271a6f3139cc7e4a89220946489d4b9a866
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
sqlite3_flutter_libs: 0d611efdf6d1c9297d5ab03dab21b75aeebdae31
stack_wallet_backup: 5b8563aba5d8ffbf2ce1944331ff7294a0ec7c03
SwiftProtobuf: 6ef3f0e422ef90d6605ca20b21a94f6c1324d6b3
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
@ -248,4 +250,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 57c8aed26fba39d3ec9424816221f294a07c58eb
COCOAPODS: 1.15.2
COCOAPODS: 1.16.2

View file

@ -48,6 +48,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View file

@ -29,6 +29,8 @@ import '../utilities/constants.dart';
import '../utilities/flutter_secure_storage_interface.dart';
import '../utilities/logger.dart';
import '../utilities/prefs.dart';
import '../utilities/stack_file_system.dart';
import '../utilities/util.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import 'hive/db.dart';
import 'isar/main_db.dart';
@ -43,10 +45,7 @@ class DbVersionMigrator with WalletDB {
// safe to skip to v11 for campfire
fromVersion = 11;
}
Logging.instance.log(
"Running migrate fromVersion $fromVersion",
level: LogLevel.Warning,
);
Logging.instance.i("Running migrate fromVersion $fromVersion");
switch (fromVersion) {
case 0:
await DB.instance.hive.openBox<dynamic>(DB.boxNameAllWalletsData);
@ -100,12 +99,13 @@ class DbVersionMigrator with WalletDB {
try {
latestSetId = await client.getLelantusLatestCoinId();
} catch (e) {
} catch (e, s) {
// default to 2 for now
latestSetId = 2;
Logging.instance.log(
Logging.instance.w(
"Failed to fetch latest coin id during firo db migrate: $e \nUsing a default value of 2",
level: LogLevel.Warning,
error: e,
stackTrace: s,
);
}
}
@ -144,7 +144,6 @@ class DbVersionMigrator with WalletDB {
),
});
}
Logger.print("newcoins $coins", normalLength: false);
await DB.instance.put<dynamic>(
boxName: walletInfo.walletId,
key: '_lelantus_coins',
@ -443,6 +442,20 @@ class DbVersionMigrator with WalletDB {
// try to continue migrating
return await migrate(13, secureStore: secureStore);
case 13:
// migrate
await _v13(secureStore);
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 14,
);
// try to continue migrating
return await migrate(14, secureStore: secureStore);
default:
// finally return
return;
@ -734,4 +747,31 @@ class DbVersionMigrator with WalletDB {
);
}
}
Future<void> _v13(SecureStorageInterface secureStore) async {
if (!(Util.isArmLinux || Util.isTestEnv)) {
// open logs db
final isar = await Isar.open(
[isar_models.LogSchema],
directory: (await StackFileSystem.applicationIsarDirectory()).path,
inspector: false,
maxSizeMiB: 512,
);
// fetch all logs
final allLogs = await isar.logs.where().findAll();
// migrate to simple file based logs. Date/time may be out of order
for (final log in allLogs) {
Logging.instance.log(
log.logLevel.getLoggerLevel(),
"MIGRATED LOG::=> ${log.message}",
time: DateTime.fromMillisecondsSinceEpoch(log.timestampInMillisUTC),
);
}
// finally delete logs db
await isar.close(deleteFromDisk: true);
}
}
}

View file

@ -166,9 +166,10 @@ class DB {
AppConfig.getCryptoCurrencyFor(jsonObject["coin"] as String);
return false;
} catch (e, s) {
Logging.instance.log(
Logging.instance.e(
"Error, ${jsonObject["coin"]} does not exist, $name wallet cannot be loaded",
level: LogLevel.Error,
error: e,
stackTrace: s,
);
return true;
}
@ -343,7 +344,7 @@ class DB {
await DB.instance.deleteBoxFromDisk(boxName: "theme");
return true;
} catch (e, s) {
Logging.instance.log("$e $s", level: LogLevel.Error);
Logging.instance.e("$e $s", error: e, stackTrace: s);
return false;
}
}

View file

@ -2,13 +2,13 @@ import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/foundation.dart';
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
import 'package:mutex/mutex.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:uuid/uuid.dart';
import '../../electrumx_rpc/electrumx_client.dart';
import '../../models/electrumx_response/spark_models.dart';
import '../../utilities/extensions/extensions.dart';
import '../../utilities/logger.dart';
import '../../utilities/stack_file_system.dart';
@ -19,18 +19,8 @@ part 'firo_cache_reader.dart';
part 'firo_cache_worker.dart';
part 'firo_cache_writer.dart';
/// Temporary debugging log function for this file
void _debugLog(Object? object) {
if (kDebugMode) {
Logging.instance.log(
object,
level: LogLevel.Debug,
);
}
}
abstract class _FiroCache {
static const int _setCacheVersion = 1;
static const int _setCacheVersion = 2;
static const int _tagsCacheVersion = 2;
static final networks = [
@ -115,7 +105,8 @@ abstract class _FiroCache {
VACUUM;
""",
);
_debugLog(
Logging.instance.d(
"_deleteAllCache() "
"duration = ${DateTime.now().difference(start)}",
);
@ -134,7 +125,7 @@ abstract class _FiroCache {
blockHash TEXT NOT NULL,
setHash TEXT NOT NULL,
groupId INTEGER NOT NULL,
timestampUTC INTEGER NOT NULL,
size INTEGER NOT NULL,
UNIQUE (blockHash, setHash, groupId)
);
@ -143,7 +134,8 @@ abstract class _FiroCache {
serialized TEXT NOT NULL,
txHash TEXT NOT NULL,
context TEXT NOT NULL,
UNIQUE(serialized, txHash, context)
groupId INTEGER NOT NULL,
UNIQUE(serialized, txHash, context, groupId)
);
CREATE TABLE SparkSetCoins (

View file

@ -6,6 +6,8 @@ typedef LTagPair = ({String tag, String txid});
/// background isolate and [FiroCacheCoordinator] should manage that isolate
abstract class FiroCacheCoordinator {
static final Map<CryptoCurrencyNetwork, _FiroCacheWorker> _workers = {};
static final Map<CryptoCurrencyNetwork, Mutex> _tagLocks = {};
static final Map<CryptoCurrencyNetwork, Mutex> _setLocks = {};
static bool _init = false;
static Future<void> init() async {
@ -15,6 +17,8 @@ abstract class FiroCacheCoordinator {
_init = true;
await _FiroCache.init();
for (final network in _FiroCache.networks) {
_tagLocks[network] = Mutex();
_setLocks[network] = Mutex();
_workers[network] = await _FiroCacheWorker.spawn(network);
}
}
@ -31,11 +35,17 @@ abstract class FiroCacheCoordinator {
final usedTagsCacheFile = File(
"${dir.path}/${_FiroCache.sparkUsedTagsCacheFileName(network)}",
);
final int bytes =
((await setCacheFile.exists()) ? await setCacheFile.length() : 0) +
((await usedTagsCacheFile.exists())
? await usedTagsCacheFile.length()
: 0);
final setSize =
(await setCacheFile.exists()) ? await setCacheFile.length() : 0;
final tagsSize = (await usedTagsCacheFile.exists())
? await usedTagsCacheFile.length()
: 0;
Logging.instance.d("Spark cache used tags size: $tagsSize");
Logging.instance.d("Spark cache anon set size: $setSize");
final int bytes = tagsSize + setSize;
if (bytes < 1024) {
return '$bytes B';
@ -55,43 +65,93 @@ abstract class FiroCacheCoordinator {
ElectrumXClient client,
CryptoCurrencyNetwork network,
) async {
final count = await FiroCacheCoordinator.getUsedCoinTagsCount(network);
final unhashedTags = await client.getSparkUnhashedUsedCoinsTagsWithTxHashes(
startNumber: count,
);
if (unhashedTags.isNotEmpty) {
await _workers[network]!.runTask(
FCTask(
func: FCFuncName._updateSparkUsedTagsWith,
data: unhashedTags,
),
await _tagLocks[network]!.protect(() async {
final count = await FiroCacheCoordinator.getUsedCoinTagsCount(network);
final unhashedTags =
await client.getSparkUnhashedUsedCoinsTagsWithTxHashes(
startNumber: count,
);
}
if (unhashedTags.isNotEmpty) {
await _workers[network]!.runTask(
FCTask(
func: FCFuncName._updateSparkUsedTagsWith,
data: unhashedTags,
),
);
}
});
}
static Future<void> runFetchAndUpdateSparkAnonSetCacheForGroupId(
int groupId,
ElectrumXClient client,
CryptoCurrencyNetwork network,
void Function(int countFetched, int totalCount)? progressUpdated,
) async {
final blockhashResult =
await FiroCacheCoordinator.getLatestSetInfoForGroupId(
groupId,
network,
);
final blockHash = blockhashResult?.blockHash ?? "";
await _setLocks[network]!.protect(() async {
const sectorSize =
1500; // chosen as a somewhat decent value. Could be changed in the future if wanted/needed
final prevMeta = await FiroCacheCoordinator.getLatestSetInfoForGroupId(
groupId,
network,
);
final json = await client.getSparkAnonymitySet(
coinGroupId: groupId.toString(),
startBlockHash: blockHash.toHexReversedFromBase64,
);
final prevSize = prevMeta?.size ?? 0;
await _workers[network]!.runTask(
FCTask(
func: FCFuncName._updateSparkAnonSetCoinsWith,
data: (groupId, json),
),
);
final meta = await client.getSparkAnonymitySetMeta(
coinGroupId: groupId,
);
progressUpdated?.call(prevSize, meta.size);
if (prevMeta?.blockHash == meta.blockHash) {
Logging.instance.d("prevMeta?.blockHash == meta.blockHash");
return;
}
final numberOfCoinsToFetch = meta.size - prevSize;
final fullSectorCount = numberOfCoinsToFetch ~/ sectorSize;
final remainder = numberOfCoinsToFetch % sectorSize;
final List<dynamic> coins = [];
for (int i = 0; i < fullSectorCount; i++) {
final start = (i * sectorSize);
final data = await client.getSparkAnonymitySetBySector(
coinGroupId: groupId,
latestBlock: meta.blockHash,
startIndex: start,
endIndex: start + sectorSize,
);
progressUpdated?.call(start + sectorSize, numberOfCoinsToFetch);
coins.addAll(data);
}
if (remainder > 0) {
final data = await client.getSparkAnonymitySetBySector(
coinGroupId: groupId,
latestBlock: meta.blockHash,
startIndex: numberOfCoinsToFetch - remainder,
endIndex: numberOfCoinsToFetch,
);
progressUpdated?.call(numberOfCoinsToFetch, numberOfCoinsToFetch);
coins.addAll(data);
}
final result = coins
.map((e) => RawSparkCoin.fromRPCResponse(e as List, groupId))
.toList();
await _workers[network]!.runTask(
FCTask(
func: FCFuncName._updateSparkAnonSetCoinsWith,
data: (meta, result),
),
);
});
}
// ===========================================================================
@ -165,28 +225,29 @@ abstract class FiroCacheCoordinator {
);
}
static Future<
List<
({
String serialized,
String txHash,
String context,
})>> getSetCoinsForGroupId(
static Future<List<RawSparkCoin>> getSetCoinsForGroupId(
int groupId, {
int? newerThanTimeStamp,
String? afterBlockHash,
required CryptoCurrencyNetwork network,
}) async {
final resultSet = await _Reader._getSetCoinsForGroupId(
groupId,
db: _FiroCache.setCacheDB(network),
newerThanTimeStamp: newerThanTimeStamp,
);
final resultSet = afterBlockHash == null
? await _Reader._getSetCoinsForGroupId(
groupId,
db: _FiroCache.setCacheDB(network),
)
: await _Reader._getSetCoinsForGroupIdAndBlockHash(
groupId,
afterBlockHash,
db: _FiroCache.setCacheDB(network),
);
return resultSet
.map(
(row) => (
(row) => RawSparkCoin(
serialized: row["serialized"] as String,
txHash: row["txHash"] as String,
context: row["context"] as String,
groupId: groupId,
),
)
.toList()
@ -194,12 +255,7 @@ abstract class FiroCacheCoordinator {
.toList();
}
static Future<
({
String blockHash,
String setHash,
int timestampUTC,
})?> getLatestSetInfoForGroupId(
static Future<SparkAnonymitySetMeta?> getLatestSetInfoForGroupId(
int groupId,
CryptoCurrencyNetwork network,
) async {
@ -212,10 +268,11 @@ abstract class FiroCacheCoordinator {
return null;
}
return (
return SparkAnonymitySetMeta(
coinGroupId: groupId,
blockHash: result.first["blockHash"] as String,
setHash: result.first["setHash"] as String,
timestampUTC: result.first["timestampUTC"] as int,
size: result.first["size"] as int,
);
}

View file

@ -8,21 +8,15 @@ abstract class _Reader {
static Future<ResultSet> _getSetCoinsForGroupId(
int groupId, {
required Database db,
int? newerThanTimeStamp,
}) async {
String query = """
SELECT sc.serialized, sc.txHash, sc.context
final query = """
SELECT sc.serialized, sc.txHash, sc.context, sc.groupId
FROM SparkSet AS ss
JOIN SparkSetCoins AS ssc ON ss.id = ssc.setId
JOIN SparkCoin AS sc ON ssc.coinId = sc.id
WHERE ss.groupId = $groupId
WHERE ss.groupId = $groupId;
""";
if (newerThanTimeStamp != null) {
query += " AND ss.timestampUTC"
" > $newerThanTimeStamp";
}
return db.select("$query;");
}
@ -31,16 +25,45 @@ abstract class _Reader {
required Database db,
}) async {
final query = """
SELECT ss.blockHash, ss.setHash, ss.timestampUTC
SELECT ss.blockHash, ss.setHash, ss.size
FROM SparkSet ss
WHERE ss.groupId = $groupId
ORDER BY ss.timestampUTC DESC
ORDER BY ss.size DESC
LIMIT 1;
""";
return db.select("$query;");
}
static Future<ResultSet> _getSetCoinsForGroupIdAndBlockHash(
int groupId,
String blockHash, {
required Database db,
}) async {
const query = """
WITH TargetBlock AS (
SELECT id
FROM SparkSet
WHERE blockHash = ?
),
TargetSets AS (
SELECT id AS setId
FROM SparkSet
WHERE groupId = ? AND id > (SELECT id FROM TargetBlock)
)
SELECT
SparkCoin.serialized,
SparkCoin.txHash,
SparkCoin.context,
SparkCoin.groupId
FROM SparkSetCoins
JOIN SparkCoin ON SparkSetCoins.coinId = SparkCoin.id
WHERE SparkSetCoins.setId IN (SELECT setId FROM TargetSets);
""";
return db.select("$query;", [blockHash, groupId]);
}
static Future<bool> _checkSetInfoForGroupIdExists(
int groupId, {
required Database db,

View file

@ -48,7 +48,11 @@ class _FiroCacheWorker {
try {
await Isolate.spawn(
_startWorkerIsolate,
(initPort.sendPort, setCacheFilePath, usedTagsCacheFilePath),
(
initPort.sendPort,
setCacheFilePath,
usedTagsCacheFilePath,
),
);
} catch (_) {
initPort.close();
@ -90,7 +94,8 @@ class _FiroCacheWorker {
final FCResult result;
switch (task.func) {
case FCFuncName._updateSparkAnonSetCoinsWith:
final data = task.data as (int, Map<String, dynamic>);
final data =
task.data as (SparkAnonymitySetMeta, List<RawSparkCoin>);
result = _updateSparkAnonSetCoinsWith(
setCacheDb,
data.$2,

View file

@ -52,29 +52,13 @@ FCResult _updateSparkUsedTagsWith(
// ================== write to spark anon set cache ==========================
/// update the sqlite cache
/// Expected json format:
/// {
/// "blockHash": "someBlockHash",
/// "setHash": "someSetHash",
/// "coins": [
/// ["serliazed1", "hash1", "context1"],
/// ["serliazed2", "hash2", "context2"],
/// ...
/// ["serliazed3", "hash3", "context3"],
/// ["serliazed4", "hash4", "context4"],
/// ],
/// }
///
/// returns true if successful, otherwise false
FCResult _updateSparkAnonSetCoinsWith(
Database db,
Map<String, dynamic> json,
int groupId,
final List<RawSparkCoin> coinsRaw,
SparkAnonymitySetMeta meta,
) {
final blockHash = json["blockHash"] as String;
final setHash = json["setHash"] as String;
final coinsRaw = json["coins"] as List;
if (coinsRaw.isEmpty) {
// no coins to actually insert
return FCResult(success: true);
@ -87,9 +71,9 @@ FCResult _updateSparkAnonSetCoinsWith(
WHERE blockHash = ? AND setHash = ? AND groupId = ?;
""",
[
blockHash,
setHash,
groupId,
meta.blockHash,
meta.setHash,
meta.coinGroupId,
],
);
@ -98,59 +82,28 @@ FCResult _updateSparkAnonSetCoinsWith(
return FCResult(success: true);
}
final coins = coinsRaw
.map(
(e) => [
e[0] as String,
e[1] as String,
e[2] as String,
],
)
.toList()
.reversed;
final timestamp = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000;
final coins = coinsRaw.reversed;
db.execute("BEGIN;");
try {
db.execute(
"""
INSERT INTO SparkSet (blockHash, setHash, groupId, timestampUTC)
INSERT INTO SparkSet (blockHash, setHash, groupId, size)
VALUES (?, ?, ?, ?);
""",
[blockHash, setHash, groupId, timestamp],
[meta.blockHash, meta.setHash, meta.coinGroupId, meta.size],
);
final setId = db.lastInsertRowId;
for (final coin in coins) {
int coinId;
try {
// try to insert and get row id
db.execute(
"""
INSERT INTO SparkCoin (serialized, txHash, context)
VALUES (?, ?, ?);
db.execute(
"""
INSERT INTO SparkCoin (serialized, txHash, context, groupId)
VALUES (?, ?, ?, ?);
""",
coin,
);
coinId = db.lastInsertRowId;
} on SqliteException catch (e) {
// if there already is a matching coin in the db
// just grab its row id
if (e.extendedResultCode == 2067) {
final result = db.select(
"""
SELECT id
FROM SparkCoin
WHERE serialized = ? AND txHash = ? AND context = ?;
""",
coin,
);
coinId = result.first["id"] as int;
} else {
rethrow;
}
}
[coin.serialized, coin.txHash, coin.context, coin.groupId],
);
final coinId = db.lastInsertRowId;
// finally add the row id to the newly added set
db.execute(

View file

@ -100,17 +100,17 @@ class CachedElectrumXClient {
}
// save set to db
await box.put(groupId, set);
Logging.instance.log(
Logging.instance.d(
"Updated current anonymity set for ${cryptoCurrency.identifier} with group ID $groupId",
level: LogLevel.Info,
);
}
return set;
} catch (e, s) {
Logging.instance.log(
"Failed to process CachedElectrumX.getAnonymitySet(): $e\n$s",
level: LogLevel.Error,
Logging.instance.e(
"Failed to process CachedElectrumX.getAnonymitySet(): ",
error: e,
stackTrace: s,
);
rethrow;
}
@ -155,16 +155,17 @@ class CachedElectrumXClient {
await box.put(txHash, result);
}
// Logging.instance.log("using fetched result", level: LogLevel.Info);
// Logging.instance.log("using fetched result");
return result;
} else {
// Logging.instance.log("using cached result", level: LogLevel.Info);
// Logging.instance.log("using cached result");
return Map<String, dynamic>.from(cachedTx);
}
} catch (e, s) {
Logging.instance.log(
"Failed to process CachedElectrumX.getTransaction(): $e\n$s",
level: LogLevel.Error,
Logging.instance.e(
"Failed to process CachedElectrumX.getTransaction(): ",
error: e,
stackTrace: s,
);
rethrow;
}
@ -212,9 +213,10 @@ class CachedElectrumXClient {
return resultingList;
} catch (e, s) {
Logging.instance.log(
"Failed to process CachedElectrumX.getUsedCoinSerials(): $e\n$s",
level: LogLevel.Error,
Logging.instance.e(
"Failed to process CachedElectrumX.getUsedCoinSerials(): ",
error: e,
stackTrace: s,
);
rethrow;
}

View file

@ -69,9 +69,10 @@ class ClientManager {
_heightCompleters[key]!.complete(event.height);
}
},
onError: (Object err, StackTrace s) => Logging.instance.log(
"ClientManager listen: $err\n$s",
level: LogLevel.Error,
onError: (Object err, StackTrace s) => Logging.instance.e(
"ClientManager listen",
error: err,
stackTrace: s,
),
);
}

View file

@ -21,6 +21,7 @@ import 'package:mutex/mutex.dart';
import 'package:stream_channel/stream_channel.dart';
import '../exceptions/electrumx/no_such_transaction.dart';
import '../models/electrumx_response/spark_models.dart';
import '../services/event_bus/events/global/tor_connection_status_changed_event.dart';
import '../services/event_bus/events/global/tor_status_changed_event.dart';
import '../services/event_bus/global_event_bus.dart';
@ -34,13 +35,6 @@ import '../wallets/crypto_currency/crypto_currency.dart';
import '../wallets/crypto_currency/interfaces/electrumx_currency_interface.dart';
import 'client_manager.dart';
typedef SparkMempoolData = ({
String txid,
List<String> serialContext,
List<String> lTags,
List<String> coins,
});
class WifiOnlyException implements Exception {}
class TorOnlyException implements Exception {}
@ -108,7 +102,7 @@ class ElectrumXClient {
late Prefs _prefs;
late TorService _torService;
List<ElectrumXNode>? failovers;
late final List<ElectrumXNode> _failovers;
int currentFailoverIndex = -1;
final Duration connectionTimeoutForSpecialCaseJsonRPCClients;
@ -145,6 +139,7 @@ class ElectrumXClient {
_host = host;
_port = port;
_useSSL = useSSL;
_failovers = failovers;
final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
@ -238,10 +233,9 @@ class ElectrumXClient {
if (!_prefs.torKillSwitch) {
// Then we'll just proceed and connect to ElectrumX through
// clearnet at the bottom of this function.
Logging.instance.log(
Logging.instance.w(
"Tor preference set but Tor is not enabled, killswitch not set,"
" connecting to Electrum adapter through clearnet",
level: LogLevel.Warning,
);
} else {
// ... But if the killswitch is set, then we throw an exception.
@ -284,9 +278,11 @@ class ElectrumXClient {
usePort = port;
useUseSSL = useSSL;
} else {
useHost = failovers![currentFailoverIndex].address;
usePort = failovers![currentFailoverIndex].port;
useUseSSL = failovers![currentFailoverIndex].useSSL;
_electrumAdapterChannel = null;
await ClientManager.sharedInstance.remove(cryptoCurrency: cryptoCurrency);
useHost = _failovers[currentFailoverIndex].address;
usePort = _failovers[currentFailoverIndex].port;
useUseSSL = _failovers[currentFailoverIndex].useSSL;
}
_electrumAdapterChannel ??= await electrum_adapter.connect(
@ -401,8 +397,17 @@ class ElectrumXClient {
} else {
rethrow;
}
} catch (e) {
if (failovers != null && currentFailoverIndex < failovers!.length - 1) {
} catch (e, s) {
final errorMessage = e.toString();
Logging.instance.w(
"$host $e",
error: e,
stackTrace: s,
);
if (errorMessage.contains("JSON-RPC error")) {
currentFailoverIndex = _failovers.length;
}
if (currentFailoverIndex < _failovers.length - 1) {
currentFailoverIndex++;
return request(
command: command,
@ -495,7 +500,7 @@ class ElectrumXClient {
rethrow;
}
} catch (e) {
if (failovers != null && currentFailoverIndex < failovers!.length - 1) {
if (currentFailoverIndex < _failovers.length - 1) {
currentFailoverIndex++;
return batchRequest(
command: command,
@ -528,14 +533,13 @@ class ElectrumXClient {
return await request(
requestID: requestID,
command: 'server.ping',
requestTimeout: const Duration(seconds: 3),
requestTimeout: const Duration(seconds: 30),
retries: retryCount,
).timeout(
const Duration(seconds: 3),
const Duration(seconds: 30),
onTimeout: () {
Logging.instance.log(
Logging.instance.d(
"ElectrumxClient.ping timed out with retryCount=$retryCount, host=$_host",
level: LogLevel.Debug,
);
},
) as bool;
@ -560,10 +564,7 @@ class ElectrumXClient {
command: 'blockchain.headers.subscribe',
);
if (response == null) {
Logging.instance.log(
"getBlockHeadTip returned null response",
level: LogLevel.Error,
);
Logging.instance.e("getBlockHeadTip returned null response");
throw 'getBlockHeadTip returned null response';
}
return Map<String, dynamic>.from(response as Map);
@ -754,14 +755,15 @@ class ElectrumXClient {
try {
final data = List<Map<String, dynamic>>.from(response[i] as List);
result.add(data);
} catch (e) {
} catch (e, s) {
// to ensure we keep same length of responses as requests/args
// add empty list on error
result.add([]);
Logging.instance.log(
Logging.instance.e(
"getBatchUTXOs failed to parse response=${response[i]}: $e",
level: LogLevel.Error,
error: e,
stackTrace: s,
);
}
}
@ -824,15 +826,13 @@ class ElectrumXClient {
bool verbose = true,
String? requestID,
}) async {
Logging.instance.log(
Logging.instance.d(
"attempting to fetch blockchain.transaction.get...",
level: LogLevel.Info,
);
await checkElectrumAdapter();
final dynamic response = await getElectrumAdapter()!.getTransaction(txHash);
Logging.instance.log(
Logging.instance.d(
"Fetching blockchain.transaction.get finished",
level: LogLevel.Info,
);
if (!verbose) {
@ -861,17 +861,15 @@ class ElectrumXClient {
String blockhash = "",
String? requestID,
}) async {
Logging.instance.log(
Logging.instance.d(
"attempting to fetch lelantus.getanonymityset...",
level: LogLevel.Info,
);
await checkElectrumAdapter();
final Map<String, dynamic> response =
await (getElectrumAdapter() as FiroElectrumClient)
.getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash);
Logging.instance.log(
Logging.instance.d(
"Fetching lelantus.getanonymityset finished",
level: LogLevel.Info,
);
return response;
}
@ -884,16 +882,14 @@ class ElectrumXClient {
dynamic mints,
String? requestID,
}) async {
Logging.instance.log(
Logging.instance.d(
"attempting to fetch lelantus.getmintmetadata...",
level: LogLevel.Info,
);
await checkElectrumAdapter();
final dynamic response = await (getElectrumAdapter() as FiroElectrumClient)
.getLelantusMintData(mints: mints);
Logging.instance.log(
Logging.instance.d(
"Fetching lelantus.getmintmetadata finished",
level: LogLevel.Info,
);
return response;
}
@ -904,9 +900,8 @@ class ElectrumXClient {
String? requestID,
required int startNumber,
}) async {
Logging.instance.log(
Logging.instance.d(
"attempting to fetch lelantus.getusedcoinserials...",
level: LogLevel.Info,
);
await checkElectrumAdapter();
@ -917,9 +912,8 @@ class ElectrumXClient {
response = await (getElectrumAdapter() as FiroElectrumClient)
.getLelantusUsedCoinSerials(startNumber: startNumber);
// TODO add 2 minute timeout.
Logging.instance.log(
Logging.instance.d(
"Fetching lelantus.getusedcoinserials finished",
level: LogLevel.Info,
);
retryCount--;
@ -932,16 +926,14 @@ class ElectrumXClient {
///
/// ex: 1
Future<int> getLelantusLatestCoinId({String? requestID}) async {
Logging.instance.log(
Logging.instance.d(
"attempting to fetch lelantus.getlatestcoinid...",
level: LogLevel.Info,
);
await checkElectrumAdapter();
final int response =
await (getElectrumAdapter() as FiroElectrumClient).getLatestCoinId();
Logging.instance.log(
Logging.instance.d(
"Fetching lelantus.getlatestcoinid finished",
level: LogLevel.Info,
);
return response;
}
@ -975,12 +967,11 @@ class ElectrumXClient {
coinGroupId: coinGroupId,
startBlockHash: startBlockHash,
);
Logging.instance.log(
Logging.instance.d(
"Finished ElectrumXClient.getSparkAnonymitySet(coinGroupId"
"=$coinGroupId, startBlockHash=$startBlockHash). "
"coins.length: ${(response["coins"] as List?)?.length}"
"Duration=${DateTime.now().difference(start)}",
level: LogLevel.Info,
);
return response;
} catch (e) {
@ -1018,7 +1009,7 @@ class ElectrumXClient {
// );
//
// return tags;
// } catch (e) {
// } catch (e, s) {
// Logging.instance.log(e, level: LogLevel.Error);
// rethrow;
// }
@ -1034,29 +1025,30 @@ class ElectrumXClient {
/// "b476ed2b374bb081ea51d111f68f0136252521214e213d119b8dc67b92f5a390",
/// ]
/// }
Future<List<Map<String, dynamic>>> getSparkMintMetaData({
String? requestID,
required List<String> sparkCoinHashes,
}) async {
try {
Logging.instance.log(
"attempting to fetch spark.getsparkmintmetadata...",
level: LogLevel.Info,
);
await checkElectrumAdapter();
final List<dynamic> response =
await (getElectrumAdapter() as FiroElectrumClient)
.getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes);
Logging.instance.log(
"Fetching spark.getsparkmintmetadata finished",
level: LogLevel.Info,
);
return List<Map<String, dynamic>>.from(response);
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
}
}
/// NOT USED?
// Future<List<Map<String, dynamic>>> getSparkMintMetaData({
// String? requestID,
// required List<String> sparkCoinHashes,
// }) async {
// try {
// Logging.instance.log(
// "attempting to fetch spark.getsparkmintmetadata...",
// level: LogLevel.Info,
// );
// await checkElectrumAdapter();
// final List<dynamic> response =
// await (getElectrumAdapter() as FiroElectrumClient)
// .getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes);
// Logging.instance.log(
// "Fetching spark.getsparkmintmetadata finished",
// level: LogLevel.Info,
// );
// return List<Map<String, dynamic>>.from(response);
// } catch (e, s) {
// Logging.instance.log(e, level: LogLevel.Error);
// rethrow;
// }
// }
/// Returns the latest Spark set id
///
@ -1065,20 +1057,22 @@ class ElectrumXClient {
String? requestID,
}) async {
try {
Logging.instance.log(
Logging.instance.d(
"attempting to fetch spark.getsparklatestcoinid...",
level: LogLevel.Info,
);
await checkElectrumAdapter();
final int response = await (getElectrumAdapter() as FiroElectrumClient)
.getSparkLatestCoinId();
Logging.instance.log(
Logging.instance.d(
"Fetching spark.getsparklatestcoinid finished",
level: LogLevel.Info,
);
return response;
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
} catch (e, s) {
Logging.instance.e(
e,
error: e,
stackTrace: s,
);
rethrow;
}
}
@ -1098,15 +1092,18 @@ class ElectrumXClient {
.map((e) => e.toHexReversedFromBase64)
.toSet();
Logging.instance.log(
Logging.instance.d(
"Finished ElectrumXClient.getMempoolTxids(). "
"Duration=${DateTime.now().difference(start)}",
level: LogLevel.Info,
);
return txids;
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
} catch (e, s) {
Logging.instance.e(
e,
error: e,
stackTrace: s,
);
rethrow;
}
}
@ -1132,7 +1129,7 @@ class ElectrumXClient {
final List<SparkMempoolData> result = [];
for (final entry in map.entries) {
result.add(
(
SparkMempoolData(
txid: entry.key,
serialContext:
List<String>.from(entry.value["serial_context"] as List),
@ -1143,15 +1140,14 @@ class ElectrumXClient {
);
}
Logging.instance.log(
Logging.instance.d(
"Finished ElectrumXClient.getMempoolSparkData(txids: $txids). "
"Duration=${DateTime.now().difference(start)}",
level: LogLevel.Info,
);
return result;
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
Logging.instance.e("$e\n$s", error: e, stackTrace: s);
rethrow;
}
}
@ -1175,19 +1171,119 @@ class ElectrumXClient {
final map = Map<String, dynamic>.from(response as Map);
final tags = List<List<dynamic>>.from(map["tagsandtxids"] as List);
Logging.instance.log(
Logging.instance.d(
"Finished ElectrumXClient.getSparkUnhashedUsedCoinsTagsWithTxHashes("
"startNumber=$startNumber). # of tags fetched=${tags.length}, "
"Duration=${DateTime.now().difference(start)}",
level: LogLevel.Info,
);
return tags;
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
} catch (e, s) {
Logging.instance.e(
e,
error: e,
stackTrace: s,
);
rethrow;
}
}
// ======== New Paginated Endpoints ==========================================
Future<SparkAnonymitySetMeta> getSparkAnonymitySetMeta({
String? requestID,
required int coinGroupId,
}) async {
try {
const command = "spark.getsparkanonymitysetmeta";
Logging.instance.d(
"[${getElectrumAdapter()?.host}] => attempting to fetch $command...",
);
final start = DateTime.now();
final response = await request(
requestID: requestID,
command: command,
args: [
"$coinGroupId",
],
);
final map = Map<String, dynamic>.from(response as Map);
final result = SparkAnonymitySetMeta(
coinGroupId: coinGroupId,
blockHash: map["blockHash"] as String,
setHash: map["setHash"] as String,
size: map["size"] as int,
);
Logging.instance.d(
"Finished ElectrumXClient.getSparkAnonymitySetMeta("
"requestID=$requestID, "
"coinGroupId=$coinGroupId"
"). Set meta=$result, "
"Duration=${DateTime.now().difference(start)}",
);
return result;
} catch (e, s) {
Logging.instance.e(
e,
error: e,
stackTrace: s,
);
rethrow;
}
}
Future<List<dynamic>> getSparkAnonymitySetBySector({
String? requestID,
required int coinGroupId,
required String latestBlock,
required int startIndex, // inclusive
required int endIndex, // exclusive
}) async {
try {
const command =
"spark.getsparkanonymitysetsector"; // TODO verify this will be correct
final start = DateTime.now();
final response = await request(
requestID: requestID,
command: command,
args: [
"$coinGroupId",
latestBlock,
"$startIndex",
"$endIndex",
],
);
final map = Map<String, dynamic>.from(response as Map);
final result = map["coins"] as List;
Logging.instance.d(
"Finished ElectrumXClient.getSparkAnonymitySetBySector("
"requestID=$requestID, "
"coinGroupId=$coinGroupId, "
"latestBlock=$latestBlock, "
"startIndex=$startIndex, "
"endIndex=$endIndex"
"). # of coins=${result.length}, "
"Duration=${DateTime.now().difference(start)}",
);
return result;
} catch (e, s) {
Logging.instance.e(
e,
error: e,
stackTrace: s,
);
rethrow;
}
}
// ===========================================================================
Future<bool> isMasterNodeCollateral({
@ -1206,16 +1302,19 @@ class ElectrumXClient {
],
);
Logging.instance.log(
Logging.instance.d(
"Finished ElectrumXClient.isMasterNodeCollateral, "
"response: $response, "
"Duration=${DateTime.now().difference(start)}",
level: LogLevel.Info,
);
return response as bool;
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
} catch (e, s) {
Logging.instance.e(
e,
error: e,
stackTrace: s,
);
rethrow;
}
}
@ -1274,7 +1373,7 @@ class ElectrumXClient {
} catch (e, s) {
final String msg = "Error parsing fee rate. Response: $response"
"\nResult: $response\nError: $e\nStack trace: $s";
Logging.instance.log(msg, level: LogLevel.Fatal);
Logging.instance.e(msg, error: e, stackTrace: s);
throw Exception(msg);
}
} catch (e) {

View file

@ -503,7 +503,7 @@
// _responseHandler(response);
// } catch (e, s) {
// Logging.instance
// .log("JsonRPC jsonDecode: $e\n$s", level: LogLevel.Error);
// .log("JsonRPC jsonDecode", error: e, stackTrace: s,);
// rethrow;
// } finally {
// _responseData = [];

View file

@ -21,10 +21,13 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:isar/isar.dart';
import 'package:keyboard_dismisser/keyboard_dismisser.dart';
import 'package:logger/logger.dart';
import 'package:path_provider/path_provider.dart';
import 'package:window_size/window_size.dart';
import 'package:xelis_flutter/src/api/api.dart' as xelis_api;
import 'package:xelis_flutter/src/api/logger.dart' as xelis_logging;
import 'package:xelis_flutter/src/frb_generated.dart' as xelis_rust;
import 'app_config.dart';
import 'db/db_version_migration.dart';
@ -35,7 +38,6 @@ import 'db/sqlite/firo_cache.dart';
import 'models/exchange/change_now/exchange_transaction.dart';
import 'models/exchange/change_now/exchange_transaction_status.dart';
import 'models/exchange/response_objects/trade.dart';
import 'models/isar/models/isar_models.dart';
import 'models/models.dart';
import 'models/node_model.dart';
import 'models/notification_model.dart';
@ -56,8 +58,6 @@ import 'providers/global/base_currencies_provider.dart';
import 'providers/global/trades_service_provider.dart';
import 'providers/providers.dart';
import 'route_generator.dart';
// import 'package:stackwallet/services/buy/buy_data_loading_service.dart';
import 'services/debug_service.dart';
import 'services/exchange/exchange_data_loading_service.dart';
import 'services/locale_service.dart';
import 'services/node_service.dart';
@ -75,15 +75,47 @@ import 'utilities/prefs.dart';
import 'utilities/stack_file_system.dart';
import 'utilities/util.dart';
import 'wallets/isar/providers/all_wallets_info_provider.dart';
import 'wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import 'widgets/crypto_notifications.dart';
final openedFromSWBFileStringStateProvider =
StateProvider<String?>((ref) => null);
final openedFromSWBFileStringStateProvider = StateProvider<String?>(
(ref) => null,
);
void startListeningToRustLogs() {
xelis_api.createLogStream().listen(
(logEntry) {
final Level level;
switch (logEntry.level) {
case xelis_logging.Level.error:
level = Level.error;
case xelis_logging.Level.warn:
level = Level.warning;
case xelis_logging.Level.info:
level = Level.info;
case xelis_logging.Level.debug:
level = Level.debug;
case xelis_logging.Level.trace:
level = Level.trace;
}
Logging.instance.log(
level,
"[Xelis Rust Log] ${logEntry.tag}: ${logEntry.msg}",
);
},
onError: (dynamic e) {
Logging.instance.e("Error receiving Xelis Rust logs: $e");
},
);
}
// main() is the entry point to the app. It initializes Hive (local database),
// runs the MyApp widget and checks for new users, caching the value in the
// miscellaneous box for later use
void main(List<String> args) async {
// talker.info('initializing Rust lib ...');
await xelis_rust.RustLib.init();
WidgetsFlutterBinding.ensureInitialized();
if (Util.isDesktop && args.length == 2 && args.first == "-d") {
@ -111,26 +143,11 @@ void main(List<String> args) async {
if (screenHeight != null) {
// starting to height be 3/4 screen height or 900, whichever is smaller
final height = min<double>(screenHeight * 0.75, 900);
setWindowFrame(
Rect.fromLTWH(0, 0, 1220, height),
);
setWindowFrame(Rect.fromLTWH(0, 0, 1220, height));
}
}
// FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
if (!(Logging.isArmLinux || Logging.isTestEnv)) {
final isar = await Isar.open(
[LogSchema],
directory: (await StackFileSystem.applicationIsarDirectory()).path,
inspector: false,
maxSizeMiB: 512,
);
await Logging.instance.init(isar);
await DebugService.instance.init(isar);
// clear out all info logs on startup. No need to await and block
unawaited(DebugService.instance.deleteLogsOlderThan());
}
// Registering Transaction Model Adapters
DB.instance.hive.registerAdapter(TransactionDataAdapter());
@ -162,8 +179,9 @@ void main(List<String> args) async {
// node model adapter
DB.instance.hive.registerAdapter(NodeModelAdapter());
if (!DB.instance.hive
.isAdapterRegistered(lib_monero_compat.WalletInfoAdapter().typeId)) {
if (!DB.instance.hive.isAdapterRegistered(
lib_monero_compat.WalletInfoAdapter().typeId,
)) {
DB.instance.hive.registerAdapter(lib_monero_compat.WalletInfoAdapter());
}
@ -179,6 +197,17 @@ void main(List<String> args) async {
await DB.instance.hive.openBox<dynamic>(DB.boxNamePrefs);
await Prefs.instance.init();
await Logging.instance.initialize(
(await StackFileSystem.applicationLogsDirectory(Prefs.instance)).path,
level: Prefs.instance.logLevel,
);
await xelis_api.setUpRustLogger();
startListeningToRustLogs();
// setup lib spark logging
initSparkLogging(Prefs.instance.logLevel);
if (AppConfig.appName == "Campfire" &&
!Util.isDesktop &&
!CampfireMigration.didRun) {
@ -202,10 +231,12 @@ void main(List<String> args) async {
// Desktop migrate handled elsewhere (currently desktop_login_view.dart)
if (!Util.isDesktop) {
final int dbVersion = DB.instance.get<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
) as int? ??
final int dbVersion =
DB.instance.get<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
)
as int? ??
0;
if (dbVersion < Constants.currentDataVersion) {
try {
@ -217,10 +248,10 @@ void main(List<String> args) async {
),
);
} catch (e, s) {
Logging.instance.log(
"Cannot migrate mobile database\n$e $s",
level: LogLevel.Error,
printFullLength: true,
Logging.instance.w(
"Cannot migrate mobile database",
error: e,
stackTrace: s,
);
}
}
@ -240,22 +271,25 @@ void main(List<String> args) async {
// verify current user preference theme and revert to default
// if problems are found to prevent app being unusable
if (!(await ThemeService.instance
.verifyInstalled(themeId: Prefs.instance.themeId))) {
if (!(await ThemeService.instance.verifyInstalled(
themeId: Prefs.instance.themeId,
))) {
Prefs.instance.themeId = "light";
}
// verify current user preference light brightness theme and revert to default
// if problems are found to prevent app being unusable
if (!(await ThemeService.instance
.verifyInstalled(themeId: Prefs.instance.systemBrightnessLightThemeId))) {
if (!(await ThemeService.instance.verifyInstalled(
themeId: Prefs.instance.systemBrightnessLightThemeId,
))) {
Prefs.instance.systemBrightnessLightThemeId = "light";
}
// verify current user preference dark brightness theme and revert to default
// if problems are found to prevent app being unusable
if (!(await ThemeService.instance
.verifyInstalled(themeId: Prefs.instance.systemBrightnessDarkThemeId))) {
if (!(await ThemeService.instance.verifyInstalled(
themeId: Prefs.instance.systemBrightnessDarkThemeId,
))) {
Prefs.instance.systemBrightnessDarkThemeId = "dark";
}
@ -271,18 +305,14 @@ class MyApp extends StatelessWidget {
final localeService = LocaleService();
localeService.loadLocale();
return const KeyboardDismisser(
child: MaterialAppWithTheme(),
);
return const KeyboardDismisser(child: MaterialAppWithTheme());
}
}
// Sidenote: MaterialAppWithTheme and InitView are only separated for clarity. No other reason.
class MaterialAppWithTheme extends ConsumerStatefulWidget {
const MaterialAppWithTheme({
super.key,
});
const MaterialAppWithTheme({super.key});
@override
ConsumerState<MaterialAppWithTheme> createState() =>
@ -356,7 +386,9 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
prefs: ref.read(prefsChangeNotifierProvider),
);
ref.read(priceAnd24hChangeNotifierProvider).start(true);
await ref.read(pWallets).load(
await ref
.read(pWallets)
.load(
ref.read(prefsChangeNotifierProvider),
ref.read(mainDBProvider),
);
@ -386,7 +418,9 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled) {
switch (ref.read(prefsChangeNotifierProvider).backupFrequencyType) {
case BackupFrequencyType.everyTenMinutes:
ref.read(autoSWBServiceProvider).startPeriodicBackupTimer(
ref
.read(autoSWBServiceProvider)
.startPeriodicBackupTimer(
duration: const Duration(minutes: 10),
);
break;
@ -404,7 +438,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
// .userID; // Just reading the ref should set it if it's not already set
// We shouldn't need to do this, instead only generating an ID when (or if) the userID is looked up when creating a quote
} catch (e, s) {
Logger.print("$e $s", normalLength: false);
Logging.instance.e("load failure", error: e, stackTrace: s);
}
}
@ -419,9 +453,10 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
ref.read(prefsChangeNotifierProvider).systemBrightnessDarkThemeId;
break;
case Brightness.light:
themeId = ref
.read(prefsChangeNotifierProvider)
.systemBrightnessLightThemeId;
themeId =
ref
.read(prefsChangeNotifierProvider)
.systemBrightnessLightThemeId;
break;
}
} else {
@ -440,9 +475,8 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
ref.read(applicationThemesDirectoryPathProvider.notifier).state =
StackFileSystem.themesDir!.path;
ref.read(themeProvider.state).state = ref.read(pThemeService).getTheme(
themeId: themeId,
)!;
ref.read(themeProvider.state).state =
ref.read(pThemeService).getTheme(themeId: themeId)!;
if (Platform.isAndroid) {
// fetch open file if it exists
@ -470,18 +504,17 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
ref.read(prefsChangeNotifierProvider).systemBrightnessDarkThemeId;
break;
case Brightness.light:
themeId = ref
.read(prefsChangeNotifierProvider)
.systemBrightnessLightThemeId;
themeId =
ref
.read(prefsChangeNotifierProvider)
.systemBrightnessLightThemeId;
break;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (ref.read(prefsChangeNotifierProvider).enableSystemBrightness) {
ref.read(themeProvider.state).state =
ref.read(pThemeService).getTheme(
themeId: themeId,
)!;
ref.read(pThemeService).getTheme(themeId: themeId)!;
}
});
};
@ -560,15 +593,14 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
/// should only be called on android currently
Future<void> getOpenFile() async {
// update provider with new file content state
ref.read(openedFromSWBFileStringStateProvider.state).state =
await platform.invokeMethod("getOpenFile");
ref.read(openedFromSWBFileStringStateProvider.state).state = await platform
.invokeMethod("getOpenFile");
// call reset to clear cached value
await resetOpenPath();
Logging.instance.log(
Logging.instance.d(
"This is the .swb content from intent: ${ref.read(openedFromSWBFileStringStateProvider.state).state}",
level: LogLevel.Info,
);
}
@ -579,9 +611,9 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
Future<void> goToRestoreSWB(String encrypted) async {
if (!ref.read(prefsChangeNotifierProvider).hasPin) {
await Navigator.of(navigatorKey.currentContext!)
.pushNamed(CreatePinView.routeName, arguments: true)
.then((value) {
await Navigator.of(
navigatorKey.currentContext!,
).pushNamed(CreatePinView.routeName, arguments: true).then((value) {
if (value is! bool || value == false) {
Navigator.of(navigatorKey.currentContext!).pushNamed(
RestoreFromEncryptedStringView.routeName,
@ -595,16 +627,17 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
navigatorKey.currentContext!,
RouteGenerator.getRoute(
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
builder: (_) => LockscreenView(
showBackButton: true,
routeOnSuccess: RestoreFromEncryptedStringView.routeName,
routeOnSuccessArguments: encrypted,
biometricsCancelButtonString: "CANCEL",
biometricsLocalizedReason:
"Authenticate to restore ${AppConfig.appName} backup",
biometricsAuthenticationTitle:
"Restore ${AppConfig.prefix} backup",
),
builder:
(_) => LockscreenView(
showBackButton: true,
routeOnSuccess: RestoreFromEncryptedStringView.routeName,
routeOnSuccessArguments: encrypted,
biometricsCancelButtonString: "CANCEL",
biometricsLocalizedReason:
"Authenticate to restore ${AppConfig.appName} backup",
biometricsAuthenticationTitle:
"Restore ${AppConfig.prefix} backup",
),
settings: const RouteSettings(name: "/swbrestorelockscreen"),
),
),
@ -614,10 +647,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
InputBorder _buildOutlineInputBorder(Color color) {
return OutlineInputBorder(
borderSide: BorderSide(
width: 1,
color: color,
),
borderSide: BorderSide(width: 1, color: color),
borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius),
);
}
@ -655,9 +685,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
),
// splashFactory: NoSplash.splashFactory,
splashColor: Colors.transparent,
buttonTheme: ButtonThemeData(
splashColor: colorScheme.splash,
),
buttonTheme: ButtonThemeData(splashColor: colorScheme.splash),
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
// splashFactory: NoSplash.splashFactory,
@ -665,8 +693,9 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
minimumSize: MaterialStateProperty.all<Size>(const Size(46, 46)),
// textStyle: MaterialStateProperty.all<TextStyle>(
// STextStyles.button(context)),
foregroundColor:
MaterialStateProperty.all(colorScheme.buttonTextSecondary),
foregroundColor: MaterialStateProperty.all(
colorScheme.buttonTextSecondary,
),
backgroundColor: MaterialStateProperty.all<Color>(
colorScheme.buttonBackSecondary,
),
@ -683,25 +712,22 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
checkboxTheme: CheckboxThemeData(
splashRadius: 0,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(Constants.size.checkboxBorderRadius),
borderRadius: BorderRadius.circular(
Constants.size.checkboxBorderRadius,
),
),
checkColor: MaterialStateColor.resolveWith(
(state) {
if (state.contains(MaterialState.selected)) {
return colorScheme.checkboxIconChecked;
}
checkColor: MaterialStateColor.resolveWith((state) {
if (state.contains(MaterialState.selected)) {
return colorScheme.checkboxIconChecked;
}
return colorScheme.checkboxBGChecked;
}),
fillColor: MaterialStateColor.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
return colorScheme.checkboxBGChecked;
},
),
fillColor: MaterialStateColor.resolveWith(
(states) {
if (states.contains(MaterialState.selected)) {
return colorScheme.checkboxBGChecked;
}
return colorScheme.checkboxBorderEmpty;
},
),
}
return colorScheme.checkboxBorderEmpty;
}),
),
appBarTheme: AppBarTheme(
centerTitle: false,
@ -719,91 +745,101 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
),
// labelStyle: STextStyles.fieldLabel(context),
// hintStyle: STextStyles.fieldLabel(context),
enabledBorder:
_buildOutlineInputBorder(colorScheme.textFieldDefaultBG),
focusedBorder:
_buildOutlineInputBorder(colorScheme.textFieldDefaultBG),
enabledBorder: _buildOutlineInputBorder(
colorScheme.textFieldDefaultBG,
),
focusedBorder: _buildOutlineInputBorder(
colorScheme.textFieldDefaultBG,
),
errorBorder: _buildOutlineInputBorder(colorScheme.textFieldDefaultBG),
disabledBorder:
_buildOutlineInputBorder(colorScheme.textFieldDefaultBG),
focusedErrorBorder:
_buildOutlineInputBorder(colorScheme.textFieldDefaultBG),
disabledBorder: _buildOutlineInputBorder(
colorScheme.textFieldDefaultBG,
),
focusedErrorBorder: _buildOutlineInputBorder(
colorScheme.textFieldDefaultBG,
),
),
),
home: CryptoNotifications(
child: Util.isDesktop
? FutureBuilder(
future: loadShared(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (_desktopHasPassword) {
String? startupWalletId;
if (ref
.read(prefsChangeNotifierProvider)
.gotoWalletOnStartup) {
startupWalletId = ref
child:
Util.isDesktop
? FutureBuilder(
future: loadShared(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (_desktopHasPassword) {
String? startupWalletId;
if (ref
.read(prefsChangeNotifierProvider)
.startupWalletId;
}
.gotoWalletOnStartup) {
startupWalletId =
ref
.read(prefsChangeNotifierProvider)
.startupWalletId;
}
return DesktopLoginView(
startupWalletId: startupWalletId,
load: load,
);
} else {
return const IntroView();
}
} else {
return const LoadingView();
}
},
)
: FutureBuilder(
future: load(),
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// FlutterNativeSplash.remove();
if (ref.read(pAllWalletsInfo).isNotEmpty ||
ref.read(prefsChangeNotifierProvider).hasPin) {
// return HomeView();
String? startupWalletId;
if (ref
.read(prefsChangeNotifierProvider)
.gotoWalletOnStartup) {
startupWalletId = ref
.read(prefsChangeNotifierProvider)
.startupWalletId;
}
return LockscreenView(
isInitialAppLogin: true,
routeOnSuccess: HomeView.routeName,
routeOnSuccessArguments: startupWalletId,
biometricsAuthenticationTitle:
"Unlock ${AppConfig.prefix}",
biometricsLocalizedReason:
"Unlock your ${AppConfig.appName} using biometrics",
biometricsCancelButtonString: "Cancel",
);
} else {
if (AppConfig.appName == "Campfire" &&
!CampfireMigration.didRun &&
CampfireMigration.hasOldWallets) {
return const CampfireMigrateView();
return DesktopLoginView(
startupWalletId: startupWalletId,
load: load,
);
} else {
return const IntroView();
}
} else {
return const LoadingView();
}
} else {
// CURRENTLY DISABLED as cannot be animated
// technically not needed as FlutterNativeSplash will overlay
// anything returned here until the future completes but
// FutureBuilder requires you to return something
return const LoadingView();
}
},
),
},
)
: FutureBuilder(
future: load(),
builder: (
BuildContext context,
AsyncSnapshot<void> snapshot,
) {
if (snapshot.connectionState == ConnectionState.done) {
// FlutterNativeSplash.remove();
if (ref.read(pAllWalletsInfo).isNotEmpty ||
ref.read(prefsChangeNotifierProvider).hasPin) {
// return HomeView();
String? startupWalletId;
if (ref
.read(prefsChangeNotifierProvider)
.gotoWalletOnStartup) {
startupWalletId =
ref
.read(prefsChangeNotifierProvider)
.startupWalletId;
}
return LockscreenView(
isInitialAppLogin: true,
routeOnSuccess: HomeView.routeName,
routeOnSuccessArguments: startupWalletId,
biometricsAuthenticationTitle:
"Unlock ${AppConfig.prefix}",
biometricsLocalizedReason:
"Unlock your ${AppConfig.appName} using biometrics",
biometricsCancelButtonString: "Cancel",
);
} else {
if (AppConfig.appName == "Campfire" &&
!CampfireMigration.didRun &&
CampfireMigration.hasOldWallets) {
return const CampfireMigrateView();
} else {
return const IntroView();
}
}
} else {
// CURRENTLY DISABLED as cannot be animated
// technically not needed as FlutterNativeSplash will overlay
// anything returned here until the future completes but
// FutureBuilder requires you to return something
return const LoadingView();
}
},
),
),
);
}

View file

@ -0,0 +1,98 @@
class SparkMempoolData {
final String txid;
final List<String> serialContext;
final List<String> lTags;
final List<String> coins;
SparkMempoolData({
required this.txid,
required this.serialContext,
required this.lTags,
required this.coins,
});
@override
String toString() {
return "SparkMempoolData{"
"txid: $txid, "
"serialContext: $serialContext, "
"lTags: $lTags, "
"coins: $coins"
"}";
}
}
class SparkAnonymitySetMeta {
final int coinGroupId;
final String blockHash;
final String setHash;
final int size;
SparkAnonymitySetMeta({
required this.coinGroupId,
required this.blockHash,
required this.setHash,
required this.size,
});
@override
String toString() {
return "SparkAnonymitySetMeta{"
"coinGroupId: $coinGroupId, "
"blockHash: $blockHash, "
"setHash: $setHash, "
"size: $size"
"}";
}
}
class RawSparkCoin {
final String serialized;
final String txHash;
final String context;
final int groupId;
RawSparkCoin({
required this.serialized,
required this.txHash,
required this.context,
required this.groupId,
});
static RawSparkCoin fromRPCResponse(List<dynamic> data, int groupId) {
try {
if (data.length != 3) throw Exception();
return RawSparkCoin(
serialized: data[0] as String,
txHash: data[1] as String,
context: data[2] as String,
groupId: groupId,
);
} catch (_) {
throw Exception("Invalid coin data: $data");
}
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! RawSparkCoin) return false;
return serialized == other.serialized &&
txHash == other.txHash &&
groupId == other.groupId &&
context == other.context;
}
@override
int get hashCode => Object.hash(serialized, txHash, context);
@override
String toString() {
return "SparkAnonymitySetMeta{"
"serialized: $serialized, "
"txHash: $txHash, "
"context: $context, "
"groupId: $groupId"
"}";
}
}

View file

@ -9,6 +9,7 @@
*/
import 'package:decimal/decimal.dart';
import '../../../utilities/logger.dart';
enum CNEstimateType { direct, reverse }
@ -112,8 +113,11 @@ class CNExchangeEstimate {
toAmount: Decimal.parse(json["toAmount"].toString()),
);
} catch (e, s) {
Logging.instance
.log("Failed to parse: $json \n$e\n$s", level: LogLevel.Fatal);
Logging.instance.e(
"Failed to parse: $json",
error: e,
stackTrace: s,
);
rethrow;
}
}

View file

@ -57,8 +57,11 @@ class EstimatedExchangeAmount {
networkFee: Decimal.tryParse(json["networkFee"].toString()),
);
} catch (e, s) {
Logging.instance
.log("Failed to parse: $json \n$e\n$s", level: LogLevel.Fatal);
Logging.instance.e(
"Failed to parse: $json",
error: e,
stackTrace: s,
);
rethrow;
}
}

View file

@ -187,7 +187,7 @@ class ExchangeTransactionStatus {
});
factory ExchangeTransactionStatus.fromJson(Map<String, dynamic> json) {
Logging.instance.log(json, printFullLength: true, level: LogLevel.Info);
Logging.instance.d(json, stackTrace: StackTrace.current);
try {
return ExchangeTransactionStatus(
status: changeNowTransactionStatusFromStringIgnoreCase(
@ -228,7 +228,7 @@ class ExchangeTransactionStatus {
payload: json["payload"] as Object?,
);
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Fatal);
Logging.instance.f("", error: e, stackTrace: s);
rethrow;
}
}

View file

@ -9,6 +9,7 @@
*/
import 'package:decimal/decimal.dart';
import '../../../utilities/logger.dart';
class Estimate {
@ -18,6 +19,7 @@ class Estimate {
final String? warningMessage;
final String? rateId;
final String exchangeProvider;
final String? exchangeProviderLogo;
final String? kycRating;
Estimate({
@ -27,6 +29,7 @@ class Estimate {
this.warningMessage,
this.rateId,
required this.exchangeProvider,
this.exchangeProviderLogo,
this.kycRating,
});
@ -46,7 +49,11 @@ class Estimate {
kycRating: kycRating,
);
} catch (e, s) {
Logging.instance.log("Estimate.fromMap(): $e\n$s", level: LogLevel.Error);
Logging.instance.e(
"Estimate.fromMap()",
error: e,
stackTrace: s,
);
rethrow;
}
}

View file

@ -9,6 +9,7 @@
*/
import 'package:decimal/decimal.dart';
import '../../../utilities/logger.dart';
class FixedRateMarket {
@ -53,10 +54,7 @@ class FixedRateMarket {
minerFee: Decimal.tryParse(json["minerFee"].toString()),
);
} catch (e, s) {
Logging.instance.log(
"FixedRateMarket.fromMap(): $e\n$s",
level: LogLevel.Error,
);
Logging.instance.e("FixedRateMarket.fromMap(): ", error: e, stackTrace: s);
rethrow;
}
}

View file

@ -59,10 +59,7 @@ class SPCurrency {
warningsTo: json["warnings_to"] as List<dynamic>,
);
} catch (e, s) {
Logging.instance.log(
"SPCurrency.fromJson failed to parse: $e\n$s",
level: LogLevel.Error,
);
Logging.instance.e("SPCurrency.fromJson failed to parse: ", error: e, stackTrace: s);
rethrow;
}
}

View file

@ -175,7 +175,8 @@ enum AddressType {
frostMS,
p2tr,
solana,
cardanoShelley;
cardanoShelley,
xelis;
String get readableName {
switch (this) {
@ -213,6 +214,8 @@ enum AddressType {
return "P2TR (taproot)";
case AddressType.cardanoShelley:
return "Cardano Shelley";
case AddressType.xelis:
return "Xelis";
}
}
}

View file

@ -278,6 +278,8 @@ const _AddresstypeEnumValueMap = {
'frostMS': 13,
'p2tr': 14,
'solana': 15,
'cardanoShelley': 16,
'xelis': 17,
};
const _AddresstypeValueEnumMap = {
0: AddressType.p2pkh,
@ -296,6 +298,8 @@ const _AddresstypeValueEnumMap = {
13: AddressType.frostMS,
14: AddressType.p2tr,
15: AddressType.solana,
16: AddressType.cardanoShelley,
17: AddressType.xelis,
};
Id _addressGetId(Address object) {

View file

@ -77,19 +77,31 @@ class UTXO {
int getConfirmations(int currentChainHeight) {
if (blockTime == null || blockHash == null) return 0;
if (blockHeight == null || blockHeight! <= 0) return 0;
return max(0, currentChainHeight - (blockHeight! - 1));
return _isMonero()
? max(0, currentChainHeight - (blockHeight!))
: max(0, currentChainHeight - (blockHeight! - 1));
}
bool isConfirmed(
int currentChainHeight,
int minimumConfirms,
int minimumCoinbaseConfirms,
) {
int minimumCoinbaseConfirms, {
int? overrideMinConfirms, // added to handle namecoin name op outputs
}) {
final confirmations = getConfirmations(currentChainHeight);
if (overrideMinConfirms != null) {
return confirmations >= overrideMinConfirms;
}
return confirmations >=
(isCoinbase ? minimumCoinbaseConfirms : minimumConfirms);
}
// fuzzy
bool _isMonero() {
return keyImage != null;
}
@ignore
String? get keyImage {
if (otherData == null) {
@ -98,7 +110,7 @@ class UTXO {
try {
final map = jsonDecode(otherData!) as Map;
return map["keyImage"] as String;
return map[UTXOOtherDataKeys.keyImage] as String;
} catch (_) {
return null;
}
@ -169,3 +181,9 @@ class UTXO {
@ignore
int get hashCode => Object.hashAll([walletId, txid, vout]);
}
abstract final class UTXOOtherDataKeys {
static const keyImage = "keyImage";
static const spent = "spent";
static const nameOpData = "nameOpData";
}

View file

@ -109,7 +109,9 @@ class TransactionV2 {
int getConfirmations(int currentChainHeight) {
if (height == null || height! <= 0) return 0;
return max(0, currentChainHeight - (height! - 1));
return _isMonero()
? max(0, currentChainHeight - (height!))
: max(0, currentChainHeight - (height! - 1));
}
bool isConfirmed(

View file

@ -13,6 +13,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:isar/isar.dart';
import '../../app_config.dart';
import '../../utilities/extensions/impl/box_shadow.dart';
import '../../utilities/extensions/impl/gradient.dart';
@ -1884,10 +1885,7 @@ class StackTheme {
(map[mainNetId] as String).toBigIntFromHex.toInt(),
);
} else {
Logging.instance.log(
"Color not found in theme for $mainNetId",
level: LogLevel.Error,
);
Logging.instance.w("Color not found in theme for $mainNetId");
}
}

View file

@ -0,0 +1,25 @@
enum DNSAddressType {
IPv4,
IPv6,
Tor,
Freenet,
I2P,
ZeroNet;
String get key {
switch (this) {
case DNSAddressType.IPv4:
return "ip";
case DNSAddressType.IPv6:
return "ip6";
case DNSAddressType.Tor:
return "_tor";
case DNSAddressType.Freenet:
return "freenet";
case DNSAddressType.I2P:
return "i2p";
case DNSAddressType.ZeroNet:
return "zeronet";
}
}
}

View file

@ -0,0 +1,129 @@
import 'dart:convert';
import 'package:meta/meta.dart';
import 'package:namecoin/namecoin.dart';
import '../../utilities/extensions/extensions.dart';
import 'dns_record_type.dart';
@Immutable()
abstract class DNSRecordBase {
final String name;
DNSRecordBase({required this.name});
String getValueString();
}
@Immutable()
final class RawDNSRecord extends DNSRecordBase {
final String value;
RawDNSRecord({required super.name, required this.value});
@override
String getValueString() => value;
@override
String toString() {
return "RawDNSRecord(name: $name, value: $value)";
}
}
@Immutable()
final class DNSRecord extends DNSRecordBase {
final DNSRecordType type;
final Map<String, dynamic> data;
DNSRecord({
required super.name,
required this.type,
required this.data,
});
@override
String getValueString() {
// TODO error handling
dynamic value = data;
while (value is Map) {
value = value[value.keys.first];
}
return value.toString();
}
DNSRecord copyWith({
DNSRecordType? type,
Map<String, dynamic>? data,
}) {
return DNSRecord(
type: type ?? this.type,
data: data ?? this.data,
name: name,
);
}
@override
String toString() {
return "DNSRecord(name: $name, type: $type, data: $data)";
}
static String merge(List<DNSRecord> records) {
final Map<String, dynamic> result = {};
for (final record in records) {
switch (record.type) {
case DNSRecordType.CNAME:
if (result[record.data.keys.first] != null) {
throw Exception("CNAME record already exists");
}
_deepMerge(result, record.data);
break;
case DNSRecordType.TLS:
case DNSRecordType.NS:
case DNSRecordType.DS:
case DNSRecordType.SRV:
case DNSRecordType.SSH:
case DNSRecordType.TXT:
case DNSRecordType.IMPORT:
case DNSRecordType.A:
_deepMerge(result, record.data);
break;
}
}
final string = jsonEncode(result);
if (string.toUint8ListFromUtf8.length > valueMaxLength) {
throw Exception(
"Value length (${string.toUint8ListFromUtf8.length}) exceeds maximum"
" allowed ($valueMaxLength)",
);
}
return string;
}
}
void _deepMerge(Map<String, dynamic> base, Map<String, dynamic> updates) {
updates.forEach((key, value) {
if (value is Map<String, dynamic> && base[key] is Map<String, dynamic>) {
_deepMerge(base[key] as Map<String, dynamic>, value);
} else if (value is List && base[key] is List) {
(base[key] as List).addAll(value);
} else {
if (base[key] != null) {
throw Exception(
"Attempted to overwrite value: ${base[key]} where key=$key",
);
}
if (value is Map) {
base[key] = Map<String, dynamic>.from(value);
} else if (value is List) {
base[key] = List<dynamic>.from(value);
} else {
base[key] = value;
}
}
});
}

View file

@ -0,0 +1,68 @@
enum DNSRecordType {
A,
CNAME,
NS,
DS,
TLS,
SRV,
TXT,
IMPORT,
SSH;
String get info {
switch (this) {
case DNSRecordType.A:
return "An A record maps your domain to an address (IPv4, IPv6, Tor,"
" Freenet, I2P, or ZeroNet).";
case DNSRecordType.CNAME:
return "A CNAME record redirects your domain to another domain,"
" essentially acting as an alias.";
case DNSRecordType.NS:
return "An NS record specifies the nameservers that are authoritative"
" for your domain.";
case DNSRecordType.DS:
return "A DS record holds information about DNSSEC (DNS Security "
"Extensions) for your domain, helping with verification and "
"integrity.";
case DNSRecordType.TLS:
return "A TLS record is used for specifying details about how to "
"establish secure connections (like TLS certificates) for your"
" domain.";
case DNSRecordType.SRV:
return "An SRV record specifies the location of servers for specific"
" services, such as SIP, XMPP, or Minecraft servers.";
case DNSRecordType.TXT:
return "A TXT record allows you to add arbitrary text to your domain's"
" DNS record, often used for verification (e.g., SPF, DKIM).";
case DNSRecordType.IMPORT:
return "An IMPORT record is used to bring in DNS records from an"
" external source into your domain's configuration.";
case DNSRecordType.SSH:
return "An SSH record provides information related to SSH public keys"
" for securely connecting to your domain's services.";
}
}
String? get key {
switch (this) {
case DNSRecordType.A:
return null;
case DNSRecordType.CNAME:
return "alias";
case DNSRecordType.NS:
return "ns";
case DNSRecordType.DS:
return "ds";
case DNSRecordType.TLS:
return "tls";
case DNSRecordType.SRV:
return "srv";
case DNSRecordType.TXT:
return "txt";
case DNSRecordType.IMPORT:
return "import";
case DNSRecordType.SSH:
return "sshfp";
}
}
}

View file

@ -65,12 +65,12 @@ class NodeModel {
int? port,
String? name,
bool? useSSL,
String? loginName,
required String? loginName,
bool? enabled,
String? coinName,
bool? isFailover,
bool? isDown,
bool? trusted,
required bool? trusted,
bool? torEnabled,
bool? clearnetEnabled,
}) {
@ -80,12 +80,12 @@ class NodeModel {
name: name ?? this.name,
id: id,
useSSL: useSSL ?? this.useSSL,
loginName: loginName ?? this.loginName,
loginName: loginName,
enabled: enabled ?? this.enabled,
coinName: coinName ?? this.coinName,
isFailover: isFailover ?? this.isFailover,
isDown: isDown ?? this.isDown,
trusted: trusted ?? this.trusted,
trusted: trusted,
torEnabled: torEnabled ?? this.torEnabled,
clearnetEnabled: clearnetEnabled ?? this.clearnetEnabled,
);

View file

@ -53,10 +53,7 @@ class HTTP {
response.statusCode,
);
} catch (e, s) {
Logging.instance.log(
"HTTP.get() rethrew: $e\n$s",
level: LogLevel.Info,
);
Logging.instance.w("HTTP.get() rethrew: ", error: e, stackTrace: s);
rethrow;
} finally {
httpClient.close(force: true);
@ -99,10 +96,7 @@ class HTTP {
response.statusCode,
);
} catch (e, s) {
Logging.instance.log(
"HTTP.post() rethrew: $e\n$s",
level: LogLevel.Info,
);
Logging.instance.w("HTTP.post() rethrew: ", error: e, stackTrace: s);
rethrow;
} finally {
httpClient.close(force: true);
@ -119,9 +113,10 @@ class HTTP {
onDone: () => completer.complete(
Uint8List.fromList(bytes),
),
onError: (Object err, StackTrace s) => Logging.instance.log(
"Http wrapper layer listen: $err\n$s",
level: LogLevel.Error,
onError: (Object err, StackTrace s) => Logging.instance.e(
"Http wrapper layer listen",
error: err,
stackTrace: s,
),
);
return completer.future;

View file

@ -153,7 +153,12 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
}
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.refresh(addWalletSelectedEntityStateProvider);
if (mounted) {
ref.refresh(addWalletSelectedEntityStateProvider);
if (isDesktop) {
_searchFocusNode.requestFocus();
}
}
});
super.initState();

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../../frost_route_generator.dart';
import '../../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart';
import '../../../../../providers/frost_wallet/frost_wallet_providers.dart';
import '../../../../../services/frost.dart';
import '../../../../../utilities/logger.dart';
@ -15,6 +15,7 @@ import '../../../../../widgets/dialogs/frost/frost_error_dialog.dart';
import '../../../../../widgets/frost_step_user_steps.dart';
import '../../../../../widgets/stack_dialog.dart';
import '../../../../../widgets/textfields/frost_step_field.dart';
import '../../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart';
class FrostCreateStep2 extends ConsumerStatefulWidget {
const FrostCreateStep2({
@ -177,10 +178,7 @@ class _FrostCreateStep2State extends ConsumerState<FrostCreateStep2> {
.routeName,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
Logging.instance.f("$e\n$s", error: e, stackTrace: s,);
if (context.mounted) {
return await showDialog<void>(
context: context,

View file

@ -178,10 +178,7 @@ class _FrostCreateStep3State extends ConsumerState<FrostCreateStep3> {
.routeName,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
Logging.instance.f("$e\n$s", error: e, stackTrace: s,);
if (context.mounted) {
return await showDialog<void>(

View file

@ -219,10 +219,7 @@ class _FrostCreateStep5State extends ConsumerState<FrostCreateStep5> {
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
Logging.instance.f("$e\n$s", error: e, stackTrace: s,);
// pop progress dialog
if (context.mounted && !progressPopped) {

View file

@ -81,10 +81,7 @@ class _FrostReshareStep1aState extends ConsumerState<FrostReshareStep1a> {
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
Logging.instance.f("$e\n$s", error: e, stackTrace: s,);
if (mounted) {
await showDialog<void>(

View file

@ -117,10 +117,7 @@ class _FrostReshareStep1bState extends ConsumerState<FrostReshareStep1b> {
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
Logging.instance.f("$e\n$s", error: e, stackTrace: s,);
if (mounted) {
await showDialog<void>(

View file

@ -204,10 +204,7 @@ class _FrostReshareStep1cState extends ConsumerState<FrostReshareStep1c> {
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
Logging.instance.f("$e\n$s", error: e, stackTrace: s,);
if (context.mounted) {
await showDialog<void>(

View file

@ -79,10 +79,7 @@ class _FrostReshareStep2abdState extends ConsumerState<FrostReshareStep2abd> {
.routeName,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
Logging.instance.f("$e\n$s", error: e, stackTrace: s,);
if (mounted) {
await showDialog<void>(
@ -208,7 +205,7 @@ class _FrostReshareStep2abdState extends ConsumerState<FrostReshareStep2abd> {
label: "Continue",
enabled: _userVerifyContinue &&
(amOutgoingParticipant ||
!fieldIsEmptyFlags.reduce((v, e) => v |= e)),
!fieldIsEmptyFlags.fold(false, (v, e) => v || e)),
onPressed: _onPressed,
),
],

View file

@ -59,10 +59,7 @@ class _FrostReshareStep2cState extends ConsumerState<FrostReshareStep2c> {
.routeName,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
Logging.instance.f("$e\n$s", error: e, stackTrace: s,);
if (mounted) {
await showDialog<void>(

View file

@ -70,10 +70,7 @@ class _FrostReshareStep3abdState extends ConsumerState<FrostReshareStep3abd> {
.routeName,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
Logging.instance.f("$e\n$s", error: e, stackTrace: s,);
if (mounted) {
await showDialog<void>(
context: context,

View file

@ -88,10 +88,7 @@ class _FrostReshareStep4State extends ConsumerState<FrostReshareStep4> {
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
Logging.instance.f("$e\n$s", error: e, stackTrace: s,);
if (mounted) {
await showDialog<void>(
context: context,
@ -238,7 +235,7 @@ class _FrostReshareStep4State extends ConsumerState<FrostReshareStep4> {
label: amOutgoingParticipant ? "Done" : "Complete",
enabled: (amNewParticipant || _userVerifyContinue) &&
(amOutgoingParticipant ||
!fieldIsEmptyFlags.reduce((v, e) => v |= e)),
!fieldIsEmptyFlags.fold(false, (v, e) => v || e)),
onPressed: _onPressed,
),
],

View file

@ -104,10 +104,7 @@ class _FrostReshareStep5State extends ConsumerState<FrostReshareStep5> {
}
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
Logging.instance.f("$e\n$s", error: e, stackTrace: s,);
if (mounted) {
await showDialog<void>(
context: context,

View file

@ -171,9 +171,10 @@ class _RestoreFrostMsWalletViewState
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
Logging.instance.e(
"",
error: e,
stackTrace: s,
);
if (mounted) {
@ -228,31 +229,29 @@ class _RestoreFrostMsWalletViewState
});
} else {
// Platform.isLinux, Platform.isWindows, or Platform.isMacOS.
await showDialog(
final qrResult = await showDialog<String>(
context: context,
builder: (context) {
return QrCodeScannerDialog(
onQrCodeDetected: (qrCodeData) {
try {
// TODO [prio=low]: Validate QR code data.
configFieldController.text = qrCodeData;
setState(() {
_configEmpty = configFieldController.text.isEmpty;
});
} catch (e, s) {
Logging.instance.log("Error processing QR code data: $e\n$s",
level: LogLevel.Error);
}
},
);
},
builder: (context) => const QrCodeScannerDialog(),
);
if (qrResult == null) {
Logging.instance.d(
"Qr scanning cancelled",
);
} else {
// TODO [prio=low]: Validate QR code data.
configFieldController.text = qrResult;
setState(() {
_configEmpty = configFieldController.text.isEmpty;
});
}
}
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
level: LogLevel.Warning,
Logging.instance.w(
"Failed to get camera permissions while trying to scan qr code: ",
error: e,
stackTrace: s,
);
}
}

View file

@ -191,7 +191,7 @@ class _NewWalletRecoveryPhraseWarningViewState
// TODO: Refactor these to generate each coin in their respective classes
// This code should not be in a random view page file
if (coin is Monero || coin is Wownero) {
if (coin is Monero || coin is Wownero || coin is Xelis) {
// currently a special case due to the
// xmr/wow libraries handling their
// own mnemonic generation
@ -274,10 +274,7 @@ class _NewWalletRecoveryPhraseWarningViewState
return (wallet, fetchedMnemonic);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
Logging.instance.f("$e\n$s", error: e, stackTrace: s,);
rethrow;
}
}

View file

@ -31,6 +31,7 @@ import '../../../wallets/isar/models/wallet_info.dart';
import '../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../wallets/wallet/impl/monero_wallet.dart';
import '../../../wallets/wallet/impl/wownero_wallet.dart';
import '../../../wallets/wallet/impl/xelis_wallet.dart';
import '../../../wallets/wallet/wallet.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
@ -264,6 +265,10 @@ class _RestoreViewOnlyWalletViewState
await (wallet as WowneroWallet).init(isRestore: true);
break;
case const (XelisWallet):
await (wallet as XelisWallet).init(isRestore: true);
break;
default:
await wallet.init();
}

View file

@ -25,6 +25,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:xelis_flutter/src/api/seed_search_engine.dart' as x_seed;
import '../../../notifications/show_flush_bar.dart';
import '../../../pages_desktop_specific/desktop_home_view.dart';
import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
@ -48,7 +50,8 @@ import '../../../wallets/isar/models/wallet_info.dart';
import '../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../wallets/wallet/impl/monero_wallet.dart';
import '../../../wallets/wallet/impl/wownero_wallet.dart';
import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../../wallets/wallet/intermediate/external_wallet.dart';
import '../../../wallets/wallet/impl/xelis_wallet.dart';
import '../../../wallets/wallet/supporting/epiccash_wallet_info_extension.dart';
import '../../../wallets/wallet/wallet.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
@ -103,6 +106,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
late final int _seedWordCount;
late final bool isDesktop;
x_seed.SearchEngine? _xelisSeedSearch;
final HashSet<String> _wordListHashSet = HashSet.from(bip39wordlist.WORDLIST);
final ScrollController controller = ScrollController();
@ -114,6 +118,8 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
late final TextSelectionControls textSelectionControls;
bool _hideSeedWords = false;
Future<void> onControlsPaste(TextSelectionDelegate delegate) async {
final data = await widget.clipboard.getData(Clipboard.kTextPlain);
if (data?.text == null) {
@ -165,6 +171,10 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
// _focusNodes.add(FocusNode());
}
if (widget.coin is Xelis) {
_xelisSeedSearch = x_seed.SearchEngine.init(languageIndex: BigInt.from(0));
}
super.initState();
}
@ -197,6 +207,9 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
);
return wowneroWordList.contains(word);
}
if (widget.coin is Xelis) {
return _xelisSeedSearch!.search(query: word).length > 0;
}
return _wordListHashSet.contains(word);
}
@ -212,6 +225,8 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
Future<void> attemptRestore() async {
if (_formKey.currentState!.validate()) {
if (mounted) setState(() => _hideSeedWords = true);
String mnemonic = "";
for (final element in _controllers) {
mnemonic += " ${element.text.trim().toLowerCase()}";
@ -279,9 +294,9 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
);
}
// TODO: do actual check to make sure it is a valid mnemonic for monero
// TODO: do actual check to make sure it is a valid mnemonic for monero + xelis
if (bip39.validateMnemonic(mnemonic) == false &&
!(widget.coin is Monero || widget.coin is Wownero)) {
!(widget.coin is Monero || widget.coin is Wownero || widget.coin is Xelis)) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
@ -313,6 +328,8 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
onCancel: () async {
isRestoring = false;
if (mounted) setState(() => _hideSeedWords = false);
await ref.read(pWallets).deleteWallet(
info,
ref.read(secureStoreProvider),
@ -364,13 +381,17 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
await (wallet as WowneroWallet).init(isRestore: true);
break;
case const (XelisWallet):
await (wallet as XelisWallet).init(isRestore: true);
break;
default:
await wallet.init();
}
await wallet.recover(isRescan: false);
if (wallet is LibMoneroWallet) {
if (wallet is ExternalWallet) {
await wallet.exit();
}
@ -471,6 +492,8 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
);
},
);
if (mounted) setState(() => _hideSeedWords = false);
}
}
@ -619,24 +642,24 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
final results = AddressUtils.decodeQRSeedData(qrResult.rawContent);
Logging.instance.log("scan parsed: $results", level: LogLevel.Info);
if (results["mnemonic"] != null) {
final list = (results["mnemonic"] as List)
.map((value) => value as String)
.toList(growable: false);
if (list.isNotEmpty) {
_clearAndPopulateMnemonic(list);
Logging.instance.log("mnemonic populated", level: LogLevel.Info);
Logging.instance.i("mnemonic populated");
} else {
Logging.instance
.log("mnemonic failed to populate", level: LogLevel.Info);
Logging.instance.i("mnemonic failed to populate");
}
}
} on PlatformException catch (e) {
} on PlatformException catch (e, s) {
// likely failed to get camera permissions
Logging.instance
.log("Restore wallet qr scan failed: $e", level: LogLevel.Warning);
Logging.instance.e(
"Restore wallet qr scan failed: $e",
error: e,
stackTrace: s,
);
}
}
@ -868,6 +891,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
child: Column(
children: [
TextFormField(
obscureText: _hideSeedWords,
autocorrect: !isDesktop,
enableSuggestions: !isDesktop,
textCapitalization:
@ -1001,6 +1025,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
child: Column(
children: [
TextFormField(
obscureText: _hideSeedWords,
autocorrect: !isDesktop,
enableSuggestions: !isDesktop,
textCapitalization:
@ -1135,6 +1160,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
padding:
const EdgeInsets.symmetric(vertical: 4),
child: TextFormField(
obscureText: _hideSeedWords,
autocorrect: !isDesktop,
enableSuggestions: !isDesktop,
textCapitalization: TextCapitalization.none,

View file

@ -10,6 +10,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../providers/global/secure_store_provider.dart';
import '../../../../providers/providers.dart';
import '../../../../themes/stack_colors.dart';
@ -72,11 +73,8 @@ class _RestoreFailedDialogState extends ConsumerState<RestoreFailedDialog> {
ref.read(secureStoreProvider),
);
} catch (e, s) {
Logging.instance.log(
"Error while getting wallet info in restore failed dialog\n"
"Error: $e\nStack trace: $s",
level: LogLevel.Error,
);
Logging.instance.e("Error while getting wallet info in restore failed dialog\n"
"Error: $e\nStack trace: $s");
} finally {
if (mounted) {
Navigator.of(context).pop();

View file

@ -40,6 +40,7 @@ import '../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../wallets/wallet/impl/monero_wallet.dart';
import '../../../wallets/wallet/impl/wownero_wallet.dart';
import '../../../wallets/wallet/impl/xelis_wallet.dart';
import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../../wallets/wallet/wallet.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
@ -225,6 +226,10 @@ class _VerifyRecoveryPhraseViewState
await (voWallet as WowneroWallet).init(isRestore: true);
break;
case const (XelisWallet):
await (voWallet as XelisWallet).init(isRestore: true);
break;
default:
await voWallet.init();
}
@ -306,10 +311,7 @@ class _VerifyRecoveryPhraseViewState
throw ex!;
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
Logging.instance.f("$e\n$s", error: e, stackTrace: s,);
if (mounted) {
await showDialog<void>(

View file

@ -115,10 +115,7 @@ class _NewContactAddressEntryFormState
// .read(shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true;
Logging.instance.log(
"Failed to get camera permissions to scan address qr code: $e\n$s",
level: LogLevel.Warning,
);
Logging.instance.w("Failed to get camera permissions to scan address qr code: ", error: e, stackTrace: s);
}
}

View file

@ -311,10 +311,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
.read(simplexProvider)
.updateSupportedCryptos(response.value!); // TODO validate
} else {
Logging.instance.log(
"_loadSimplexCurrencies: $response",
level: LogLevel.Warning,
);
Logging.instance.d("_loadSimplexCurrencies: $response");
}
}
@ -326,10 +323,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
.read(simplexProvider)
.updateSupportedFiats(response.value!); // TODO validate
} else {
Logging.instance.log(
"_loadSimplexCurrencies: $response",
level: LogLevel.Warning,
);
Logging.instance.d("_loadSimplexCurrencies: $response");
}
}
@ -626,10 +620,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
ref.read(simplexProvider).updateQuote(response.value!);
return BuyResponse(value: response.value!);
} else {
Logging.instance.log(
"_loadQuote: $response",
level: LogLevel.Warning,
);
Logging.instance.d("_loadQuote: $response");
return BuyResponse(
exception: response.exception ??
BuyException(
@ -724,20 +715,14 @@ class _BuyFormState extends ConsumerState<BuyForm> {
final qrResult = await scanner.scan();
Logging.instance.log(
"qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info,
);
Logging.instance.d("qrResult content: ${qrResult.rawContent}");
final paymentData = AddressUtils.parsePaymentUri(
qrResult.rawContent,
logging: Logging.instance,
);
Logging.instance.log(
"qrResult parsed: $paymentData",
level: LogLevel.Info,
);
Logging.instance.d("qrResult parsed: $paymentData");
if (paymentData != null) {
// auto fill address
@ -760,9 +745,10 @@ class _BuyFormState extends ConsumerState<BuyForm> {
} on PlatformException catch (e, s) {
// here we ignore the exception caused by not giving permission
// to use the camera to scan a qr code
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
Logging.instance.e(
"Failed to get camera permissions while trying to scan qr code in SendView: ",
error: e,
stackTrace: s,
);
}
}
@ -1241,7 +1227,11 @@ class _BuyFormState extends ConsumerState<BuyForm> {
}
});
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Info);
Logging.instance.w(
"",
error: e,
stackTrace: s,
);
}
},
),

View file

@ -304,7 +304,7 @@ class CoinIconForTicker extends ConsumerWidget {
height: size,
);
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Fatal);
Logging.instance.f("", error: e, stackTrace: s);
rethrow;
}
}

View file

@ -26,6 +26,8 @@ import '../../utilities/assets.dart';
import '../../utilities/constants.dart';
import '../../utilities/text_styles.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/wallet/impl/namecoin_wallet.dart';
import '../../wallets/wallet/wallet.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
import '../../widgets/animated_widgets/rotate_icon.dart';
import '../../widgets/app_bar_field.dart';
@ -88,6 +90,18 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
await coinControlInterface.updateBalance();
}
bool _isConfirmed(UTXO utxo, int currentChainHeight, Wallet wallet) {
if (wallet is NamecoinWallet) {
return wallet.checkUtxoConfirmed(utxo, currentChainHeight);
} else {
return utxo.isConfirmed(
currentChainHeight,
wallet.cryptoCurrency.minConfirms,
wallet.cryptoCurrency.minCoinbaseConfirms,
);
}
}
@override
void initState() {
if (widget.selectedUTXOs != null) {
@ -347,10 +361,15 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
CoinControlViewType.manage ||
(widget.type == CoinControlViewType.use &&
!utxo.isBlocked &&
utxo.isConfirmed(
_isConfirmed(
utxo,
currentHeight,
minConfirms,
coin.minCoinbaseConfirms,
ref.watch(
pWallets.select(
(s) => s
.getWallet(widget.walletId),
),
),
)),
initialSelectedState: isSelected,
onSelectedChanged: (value) {
@ -412,10 +431,16 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
(widget.type ==
CoinControlViewType.use &&
!_showBlocked &&
utxo.isConfirmed(
_isConfirmed(
utxo,
currentHeight,
minConfirms,
coin.minCoinbaseConfirms,
ref.watch(
pWallets.select(
(s) => s.getWallet(
widget.walletId,
),
),
),
)),
initialSelectedState: isSelected,
onSelectedChanged: (value) {
@ -557,10 +582,16 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
CoinControlViewType
.use &&
!utxo.isBlocked &&
utxo.isConfirmed(
_isConfirmed(
utxo,
currentHeight,
minConfirms,
coin.minCoinbaseConfirms,
ref.watch(
pWallets.select(
(s) => s.getWallet(
widget.walletId,
),
),
),
)),
initialSelectedState: isSelected,
onSelectedChanged: (value) {

View file

@ -20,6 +20,8 @@ import '../../utilities/amount/amount_formatter.dart';
import '../../utilities/constants.dart';
import '../../utilities/text_styles.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/wallet/impl/namecoin_wallet.dart';
import '../../wallets/wallet/wallet.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/icon_widgets/utxo_status_icon.dart';
import '../../widgets/rounded_container.dart';
@ -52,6 +54,18 @@ class _UtxoCardState extends ConsumerState<UtxoCard> {
late bool _selected;
bool _isConfirmed(UTXO utxo, int currentChainHeight, Wallet wallet) {
if (wallet is NamecoinWallet) {
return wallet.checkUtxoConfirmed(utxo, currentChainHeight);
} else {
return utxo.isConfirmed(
currentChainHeight,
wallet.cryptoCurrency.minConfirms,
wallet.cryptoCurrency.minCoinbaseConfirms,
);
}
}
@override
void initState() {
_selected = widget.initialSelectedState;
@ -110,18 +124,16 @@ class _UtxoCardState extends ConsumerState<UtxoCard> {
),
child: UTXOStatusIcon(
blocked: utxo.isBlocked,
status: utxo.isConfirmed(
status: _isConfirmed(
utxo,
currentHeight,
ref
.watch(pWallets)
.getWallet(widget.walletId)
.cryptoCurrency
.minConfirms,
ref
.watch(pWallets)
.getWallet(widget.walletId)
.cryptoCurrency
.minCoinbaseConfirms,
ref.watch(
pWallets.select(
(s) => s.getWallet(
widget.walletId,
),
),
),
)
? UTXOStatusIconStatus.confirmed
: UTXOStatusIconStatus.unconfirmed,

View file

@ -23,6 +23,8 @@ import '../../utilities/amount/amount_formatter.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/wallet/impl/namecoin_wallet.dart';
import '../../wallets/wallet/wallet.dart';
import '../../widgets/background.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
@ -67,6 +69,18 @@ class _UtxoDetailsViewState extends ConsumerState<UtxoDetailsView> {
await MainDB.instance.putUTXO(utxo!.copyWith(isBlocked: !utxo!.isBlocked));
}
bool _isConfirmed(UTXO utxo, int currentChainHeight, Wallet wallet) {
if (wallet is NamecoinWallet) {
return wallet.checkUtxoConfirmed(utxo, currentChainHeight);
} else {
return utxo.isConfirmed(
currentChainHeight,
wallet.cryptoCurrency.minConfirms,
wallet.cryptoCurrency.minCoinbaseConfirms,
);
}
}
@override
void initState() {
utxo = MainDB.instance.isar.utxos
@ -95,14 +109,14 @@ class _UtxoDetailsViewState extends ConsumerState<UtxoDetailsView> {
final coin = ref.watch(pWalletCoin(widget.walletId));
final currentHeight = ref.watch(pWalletChainHeight(widget.walletId));
final confirmed = utxo!.isConfirmed(
final confirmed = _isConfirmed(
utxo!,
currentHeight,
ref.watch(pWallets).getWallet(widget.walletId).cryptoCurrency.minConfirms,
ref
.watch(pWallets)
.getWallet(widget.walletId)
.cryptoCurrency
.minCoinbaseConfirms,
ref.watch(
pWallets.select(
(s) => s.getWallet(widget.walletId),
),
),
);
return ConditionalParent(

View file

@ -162,10 +162,7 @@ class _ConfirmChangeNowSendViewState
Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName));
}
} catch (e, s) {
Logging.instance.log(
"Broadcast transaction failed: $e\n$s",
level: LogLevel.Error,
);
Logging.instance.e("Broadcast transaction failed: ", error: e, stackTrace: s);
// pop sending dialog
Navigator.of(context).pop();

View file

@ -98,9 +98,10 @@ class _Step2ViewState extends ConsumerState<Step2View> {
});
}
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
Logging.instance.w(
"Failed to get camera permissions while trying to scan qr code in SendView: ",
error: e,
stackTrace: s,
);
}
}
@ -135,9 +136,10 @@ class _Step2ViewState extends ConsumerState<Step2View> {
});
}
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
Logging.instance.w(
"Failed to get camera permissions while trying to scan qr code in SendView: ",
error: e,
stackTrace: s,
);
}
}
@ -303,8 +305,11 @@ class _Step2ViewState extends ConsumerState<Step2View> {
}
});
} catch (e, s) {
Logging.instance
.log("$e\n$s", level: LogLevel.Info);
Logging.instance.e(
"",
error: e,
stackTrace: s,
);
}
},
),
@ -543,9 +548,10 @@ class _Step2ViewState extends ConsumerState<Step2View> {
});
});
} catch (e, s) {
Logging.instance.log(
Logging.instance.i(
"$e\n$s",
level: LogLevel.Info,
error: e,
stackTrace: s,
);
}
},

View file

@ -317,7 +317,7 @@ class _Step4ViewState extends ConsumerState<Step4View> {
}
}
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
Logging.instance.e("$e\n$s", error: e, stackTrace: s);
if (mounted && !wasCancelled) {
// pop building dialog
Navigator.of(context).pop();

View file

@ -35,7 +35,7 @@ import '../../wallets/crypto_currency/crypto_currency.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/models/tx_data.dart';
import '../../wallets/wallet/impl/firo_wallet.dart';
import '../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../wallets/wallet/intermediate/external_wallet.dart';
import '../../widgets/background.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
@ -277,7 +277,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
// access to this screen but this is needed to get past an error that
// would occur only to lead to another error which is why xmr/wow wallets
// don't have access to this screen currently
if (wallet is LibMoneroWallet) {
if (wallet is ExternalWallet) {
await wallet.init();
await wallet.open();
}
@ -387,7 +387,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
}
}
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
Logging.instance.e("$e\n$s", error: e, stackTrace: s);
if (mounted) {
// pop building dialog
Navigator.of(context).pop();

View file

@ -173,9 +173,8 @@ class _ExchangeOptionState extends ConsumerState<ExchangeOption> {
],
);
} else {
Logging.instance.log(
Logging.instance.w(
"$runtimeType rate unavailable for ${widget.exchange.name}: $data",
level: LogLevel.Warning,
);
return Consumer(
@ -315,7 +314,43 @@ class _ProviderOptionState extends ConsumerState<_ProviderOption> {
child: SizedBox(
width: isDesktop ? 32 : 24,
height: isDesktop ? 32 : 24,
child: SvgPicture.asset(
child: widget.estimate?.exchangeProviderLogo != null &&
widget
.estimate!
.exchangeProviderLogo!
.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Image.network(
widget.estimate!.exchangeProviderLogo!,
loadingBuilder: (
context,
child,
loadingProgress,
) {
if (loadingProgress == null) {
return child;
} else {
return const Center(
child:
CircularProgressIndicator(),
);
}
},
errorBuilder: (context, error, stackTrace) {
return SvgPicture.asset(
Assets.exchange.getIconFor(
exchangeName: widget.exchange.name,
),
width: isDesktop ? 32 : 24,
height: isDesktop ? 32 : 24,
);
},
width: isDesktop ? 32 : 24,
height: isDesktop ? 32 : 24,
),
)
: SvgPicture.asset(
Assets.exchange.getIconFor(
exchangeName: widget.exchange.name,
),

View file

@ -0,0 +1,257 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import '../../../models/namecoin_dns/dns_record_type.dart';
import '../../../route_generator.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/assets.dart';
import '../../../utilities/constants.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../widgets/desktop/desktop_dialog.dart';
import '../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/desktop/secondary_button.dart';
import '../../../widgets/stack_dialog.dart';
import 'add_dns_step_2.dart';
class AddDnsStep1 extends StatefulWidget {
const AddDnsStep1({super.key, required this.name});
final String name;
@override
State<AddDnsStep1> createState() => _AddDnsStep1State();
}
class _AddDnsStep1State extends State<AddDnsStep1> {
DNSRecordType? _recordType;
bool _nextLock = false;
void _next() {
if (_nextLock) return;
_nextLock = true;
try {
if (mounted) {
Navigator.of(context).push(
RouteGenerator.getRoute(
builder: (context) {
return Util.isDesktop
? DesktopDialog(
maxHeight: double.infinity,
maxWidth: 580,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(
left: 32,
),
child: Text(
"Add DNS record",
style: STextStyles.desktopH3(
context,
),
),
),
const DesktopDialogCloseButton(),
],
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: AddDnsStep2(
recordType: _recordType!,
name: widget.name,
),
),
],
),
)
: StackDialogBase(
keyboardPaddingAmount:
MediaQuery.of(context).viewInsets.bottom,
child: AddDnsStep2(
recordType: _recordType!,
name: widget.name,
),
);
},
),
);
}
} finally {
_nextLock = false;
}
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!Util.isDesktop)
Text(
"Add DNS record",
style: STextStyles.pageTitleH2(context),
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
Text(
"Choose a record type",
style: Util.isDesktop
? STextStyles.w500_12(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
)
: STextStyles.w500_14(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
),
SizedBox(
height: Util.isDesktop ? 12 : 8,
),
DropdownButtonHideUnderline(
child: DropdownButton2<DNSRecordType>(
hint: Text(
"Choose a record type",
style: STextStyles.fieldLabel(context),
),
buttonStyleData: ButtonStyleData(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
dropdownStyleData: DropdownStyleData(
offset: const Offset(0, -10),
elevation: 0,
maxHeight: Util.isDesktop ? null : 200,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
),
isExpanded: true,
value: _recordType,
onChanged: (value) {
if (value is DNSRecordType && _recordType != value) {
setState(() {
_recordType = value;
});
}
},
iconStyleData: IconStyleData(
icon: Padding(
padding: const EdgeInsets.only(right: 10),
child: SvgPicture.asset(
Assets.svg.chevronDown,
width: 10,
height: 5,
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
),
),
items: [
...DNSRecordType.values.map(
(e) => DropdownMenuItem<DNSRecordType>(
value: e,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
e.name,
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
),
),
),
],
),
),
if (_recordType != null)
SizedBox(
height: Util.isDesktop ? 10 : 6,
),
if (_recordType != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
Expanded(
child: Text(
_recordType!.info,
style: Util.isDesktop
? STextStyles.w500_10(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.infoItemLabel,
)
: STextStyles.w500_8(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.infoItemLabel,
),
),
),
],
),
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
buttonHeight: Util.isDesktop ? ButtonHeight.l : null,
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
},
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
label: "Next",
enabled: _recordType != null,
onPressed: _next,
buttonHeight: Util.isDesktop ? ButtonHeight.l : null,
),
),
],
),
if (Util.isDesktop)
const SizedBox(
height: 32,
),
],
);
}
}

View file

@ -0,0 +1,163 @@
import 'package:flutter/material.dart';
import '../../../models/namecoin_dns/dns_record_type.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/desktop/secondary_button.dart';
import '../../../widgets/stack_dialog.dart';
import 'name_form_interface.dart';
import 'sub_widgets/a_form.dart';
import 'sub_widgets/cname_form.dart';
import 'sub_widgets/ds_form.dart';
import 'sub_widgets/import_form.dart';
import 'sub_widgets/ns_form.dart';
import 'sub_widgets/srv_form.dart';
import 'sub_widgets/ssh_form.dart';
import 'sub_widgets/tls_form.dart';
import 'sub_widgets/txt_form.dart';
class AddDnsStep2 extends StatefulWidget {
const AddDnsStep2({
super.key,
required this.recordType,
required this.name,
});
final String name;
final DNSRecordType recordType;
@override
State<AddDnsStep2> createState() => _AddDnsStep2State();
}
class _AddDnsStep2State extends State<AddDnsStep2> {
final GlobalKey<NameFormState> _formStateKey = GlobalKey();
bool _nextLock = false;
void _nextPressed() {
if (_nextLock) return;
_nextLock = true;
try {
final record = _formStateKey.currentState!.buildRecord();
Navigator.of(context, rootNavigator: true).pop(
record,
);
} catch (e, s) {
Logging.instance.e(
runtimeType,
error: e,
stackTrace: s,
);
final String err;
switch (e.runtimeType) {
case const (ArgumentError):
err = e.toString().replaceFirst(
"Invalid Arguments(s): ",
"",
);
case const (Exception):
err = e.toString().replaceFirst(
"Exception: ",
"",
);
default:
err = e.toString();
}
showDialog<void>(
context: context,
useRootNavigator: true,
builder: (context) {
return StackOkDialog(
desktopPopRootNavigator: true, // mobile as well due to sub nav flow
title: "Error",
maxWidth: 500,
message: err,
);
},
);
} finally {
_nextLock = false;
}
}
NameFormStatefulWidget? _form;
NameFormStatefulWidget get form => _form ??= _buildForm();
NameFormStatefulWidget _buildForm() {
switch (widget.recordType) {
case DNSRecordType.A:
return AForm(key: _formStateKey, name: widget.name);
case DNSRecordType.CNAME:
return CNAMEForm(key: _formStateKey, name: widget.name);
case DNSRecordType.NS:
return NSForm(key: _formStateKey, name: widget.name);
case DNSRecordType.DS:
return DSForm(key: _formStateKey, name: widget.name);
case DNSRecordType.TLS:
return TLSForm(key: _formStateKey, name: widget.name);
case DNSRecordType.SRV:
return SRVForm(key: _formStateKey, name: widget.name);
case DNSRecordType.TXT:
return TXTForm(key: _formStateKey, name: widget.name);
case DNSRecordType.IMPORT:
return IMPORTForm(key: _formStateKey, name: widget.name);
case DNSRecordType.SSH:
return SSHForm(key: _formStateKey, name: widget.name);
}
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!Util.isDesktop)
Text(
"Add DNS record",
style: STextStyles.pageTitleH2(context),
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
form,
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
buttonHeight: Util.isDesktop ? ButtonHeight.l : null,
onPressed: () {
Navigator.of(context).pop();
},
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
label: "Next",
onPressed: _nextPressed,
buttonHeight: Util.isDesktop ? ButtonHeight.l : null,
),
),
],
),
if (Util.isDesktop)
const SizedBox(
height: 32,
),
],
);
}
}

View file

@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import '../../../models/namecoin_dns/dns_record.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/constants.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
abstract class NameFormStatefulWidget extends StatefulWidget {
const NameFormStatefulWidget({super.key, required this.name});
final String name;
}
abstract class NameFormState<T extends NameFormStatefulWidget>
extends State<T> {
DNSRecord buildRecord();
}
class DNSFieldText extends StatelessWidget {
const DNSFieldText(this.text, {super.key});
final String text;
@override
Widget build(BuildContext context) {
return Text(
text,
style: Util.isDesktop
? STextStyles.w500_12(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
)
: STextStyles.w500_14(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
);
}
}
class DNSFormField extends StatelessWidget {
const DNSFormField({super.key, required this.controller, this.keyboardType});
final TextEditingController controller;
final TextInputType? keyboardType;
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
controller: controller,
textAlignVertical: TextAlignVertical.center,
keyboardType: keyboardType,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.all(16),
hintStyle: STextStyles.fieldLabel(context),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
),
),
);
}
}

View file

@ -0,0 +1,239 @@
import 'dart:io';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import '../../../../models/namecoin_dns/dns_a_record_address_type.dart';
import '../../../../models/namecoin_dns/dns_record.dart';
import '../../../../models/namecoin_dns/dns_record_type.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/assets.dart';
import '../../../../utilities/constants.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart';
import '../name_form_interface.dart';
class AForm extends NameFormStatefulWidget {
const AForm({super.key, required super.name});
@override
NameFormState<AForm> createState() => _AFormState();
}
class _AFormState extends NameFormState<AForm> {
final _addressDataController = TextEditingController();
final _addressDataFieldFocus = FocusNode();
DNSAddressType _addressType = DNSAddressType.IPv4;
@override
DNSRecord buildRecord() {
final parts = _addressDataController.text.split(",").map((e) => e.trim());
final List<String> addresses = [];
for (final part in parts) {
switch (_addressType) {
case DNSAddressType.IPv4:
final address =
InternetAddress(part.trim(), type: InternetAddressType.IPv4);
addresses.add(address.address);
break;
case DNSAddressType.IPv6:
final address = InternetAddress(part, type: InternetAddressType.IPv6);
addresses.add(address.address);
break;
case DNSAddressType.Tor:
final regex = RegExp(r'^[a-z2-7]{56}\.onion$');
if (regex.hasMatch(part)) {
addresses.add(part);
} else {
throw Exception("Invalid tor address: $part");
}
case DNSAddressType.Freenet:
// TODO: verify
final regex = RegExp(r'(CHK|SSK|USK)@[a-zA-Z0-9~-]{43,}/?');
final kskRegex = RegExp(r'KSK@[\w\-.~]+');
if (regex.hasMatch(part) || kskRegex.hasMatch(part)) {
addresses.add(part);
} else {
throw Exception("Invalid freenet address: $part");
}
case DNSAddressType.I2P:
// TODO: verify
final b32Regex = RegExp(r'^[a-z2-7]{52}\.b32\.i2p$');
final b64Regex = RegExp(r'^[A-Za-z0-9+/=]{516,}$');
if (b32Regex.hasMatch(part) || b64Regex.hasMatch(part)) {
addresses.add(part);
} else {
throw Exception("Invalid i2p address: $part");
}
case DNSAddressType.ZeroNet:
// TODO: verify
final regex = RegExp(r'^[13][a-km-zA-HJ-NP-Z1-9]{32,33}$');
if (regex.hasMatch(part)) {
addresses.add(part);
} else {
throw Exception("Invalid zeronet address: $part");
}
}
}
final Map<String, dynamic> map;
if (_addressType == DNSAddressType.Tor) {
map = {
"map": {
"_tor": {
"txt": addresses,
},
},
};
} else {
map = {
_addressType!.key: addresses,
};
}
return DNSRecord(
name: widget.name,
type: DNSRecordType.A,
data: map,
);
}
@override
void dispose() {
_addressDataController.dispose();
_addressDataFieldFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const DNSFieldText(
"Address type",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DropdownButtonHideUnderline(
child: DropdownButton2<DNSAddressType>(
hint: Text(
"Choose address type",
style: STextStyles.fieldLabel(context),
),
dropdownStyleData: DropdownStyleData(
offset: const Offset(0, -10),
elevation: 0,
maxHeight: Util.isDesktop ? null : 200,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
),
isExpanded: true,
value: _addressType,
onChanged: (value) {
if (value is DNSAddressType && _addressType != value) {
setState(() {
_addressType = value;
});
}
},
buttonStyleData: ButtonStyleData(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
iconStyleData: IconStyleData(
icon: Padding(
padding: const EdgeInsets.only(right: 10),
child: SvgPicture.asset(
Assets.svg.chevronDown,
width: 10,
height: 5,
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
),
),
items: [
...DNSAddressType.values.map(
(e) => DropdownMenuItem<DNSAddressType>(
value: e,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
e.name,
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
),
),
),
],
),
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
const DNSFieldText(
"Value",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
focusNode: _addressDataFieldFocus,
controller: _addressDataController,
textAlignVertical: TextAlignVertical.center,
maxLines: 3,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.all(16),
hintText: "e.g. 255.255.255.255, "
"76f4a520a262c269dcba66bc1f560452e30a44e14ce6b37ce20b8.onion",
hintStyle: STextStyles.fieldLabel(context),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
),
),
),
],
);
}
}

View file

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import '../../../../models/namecoin_dns/dns_record.dart';
import '../../../../models/namecoin_dns/dns_record_type.dart';
import '../../../../utilities/util.dart';
import '../name_form_interface.dart';
class CNAMEForm extends NameFormStatefulWidget {
const CNAMEForm({super.key, required super.name});
@override
NameFormState<CNAMEForm> createState() => _CNAMEFormState();
}
class _CNAMEFormState extends NameFormState<CNAMEForm> {
final _aliasController = TextEditingController();
@override
DNSRecord buildRecord() {
final address = _aliasController.text.trim();
return DNSRecord(
name: widget.name,
type: DNSRecordType.CNAME,
data: {"alias": address},
);
}
@override
void dispose() {
_aliasController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const DNSFieldText(
"Alias of",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _aliasController,
),
],
);
}
}

View file

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import '../../../../models/namecoin_dns/dns_record.dart';
import '../../../../models/namecoin_dns/dns_record_type.dart';
import '../../../../utilities/util.dart';
import '../name_form_interface.dart';
class DSForm extends NameFormStatefulWidget {
const DSForm({super.key, required super.name});
@override
NameFormState<DSForm> createState() => _DSFormState();
}
class _DSFormState extends NameFormState<DSForm> {
final _keytagController = TextEditingController();
final _algoController = TextEditingController();
final _typeController = TextEditingController();
final _hashController = TextEditingController();
@override
DNSRecord buildRecord() {
return DNSRecord(
name: widget.name,
type: DNSRecordType.DS,
data: {
"ds": [
[
int.parse(_keytagController.text.trim()),
int.parse(_algoController.text.trim()),
int.parse(_typeController.text.trim()),
_hashController.text.trim(),
],
],
},
);
}
@override
void dispose() {
_keytagController.dispose();
_algoController.dispose();
_typeController.dispose();
_hashController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const DNSFieldText(
"Keytag",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _keytagController,
keyboardType: TextInputType.number,
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
const DNSFieldText(
"Algorithm",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _algoController,
keyboardType: TextInputType.number,
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
const DNSFieldText(
"Hash type",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _typeController,
keyboardType: TextInputType.number,
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
const DNSFieldText(
"Hash (base64)",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _hashController,
),
],
);
}
}

View file

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import '../../../../models/namecoin_dns/dns_record.dart';
import '../../../../models/namecoin_dns/dns_record_type.dart';
import '../../../../utilities/util.dart';
import '../name_form_interface.dart';
class IMPORTForm extends NameFormStatefulWidget {
const IMPORTForm({super.key, required super.name});
@override
NameFormState<IMPORTForm> createState() => _IMPORTFormState();
}
class _IMPORTFormState extends NameFormState<IMPORTForm> {
final _nameController = TextEditingController();
final _subdomainController = TextEditingController();
@override
DNSRecord buildRecord() {
return DNSRecord(
name: widget.name,
type: DNSRecordType.IMPORT,
data: {
"import": [
[
_nameController.text.trim(),
if (_subdomainController.text.trim().isNotEmpty)
_subdomainController.text.trim(),
],
],
},
);
}
@override
void dispose() {
_nameController.dispose();
_subdomainController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const DNSFieldText(
"Namecoin name",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _nameController,
),
const DNSFieldText(
"Subdomain (optional)",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _subdomainController,
),
],
);
}
}

View file

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import '../../../../models/namecoin_dns/dns_record.dart';
import '../../../../models/namecoin_dns/dns_record_type.dart';
import '../../../../utilities/util.dart';
import '../name_form_interface.dart';
class NSForm extends NameFormStatefulWidget {
const NSForm({super.key, required super.name});
@override
NameFormState<NSForm> createState() => _NSFormState();
}
class _NSFormState extends NameFormState<NSForm> {
final _serverController = TextEditingController();
@override
DNSRecord buildRecord() {
final address = _serverController.text.trim();
return DNSRecord(
name: widget.name,
type: DNSRecordType.NS,
data: {
"ns": [address],
},
);
}
@override
void dispose() {
_serverController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const DNSFieldText(
"Nameserver",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _serverController,
),
],
);
}
}

View file

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import '../../../../models/namecoin_dns/dns_record.dart';
import '../../../../models/namecoin_dns/dns_record_type.dart';
import '../../../../utilities/util.dart';
import '../name_form_interface.dart';
class SRVForm extends NameFormStatefulWidget {
const SRVForm({super.key, required super.name});
@override
NameFormState<SRVForm> createState() => _SRVFormState();
}
class _SRVFormState extends NameFormState<SRVForm> {
final _priorityController = TextEditingController();
final _weightController = TextEditingController();
final _portController = TextEditingController();
final _hostController = TextEditingController();
@override
DNSRecord buildRecord() {
return DNSRecord(
name: widget.name,
type: DNSRecordType.SRV,
data: {
"srv": [
[
int.parse(_priorityController.text.trim()),
int.parse(_weightController.text.trim()),
int.parse(_portController.text.trim()),
_hostController.text.trim(),
],
],
},
);
}
@override
void dispose() {
_priorityController.dispose();
_weightController.dispose();
_portController.dispose();
_hostController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const DNSFieldText(
"Priority",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _priorityController,
keyboardType: TextInputType.number,
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
const DNSFieldText(
"Weight",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _weightController,
keyboardType: TextInputType.number,
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
const DNSFieldText(
"Port",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _portController,
keyboardType: TextInputType.number,
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
const DNSFieldText(
"Host",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _hostController,
),
],
);
}
}

View file

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import '../../../../models/namecoin_dns/dns_record.dart';
import '../../../../models/namecoin_dns/dns_record_type.dart';
import '../../../../utilities/util.dart';
import '../name_form_interface.dart';
class SSHForm extends NameFormStatefulWidget {
const SSHForm({super.key, required super.name});
@override
NameFormState<SSHForm> createState() => _SSHFormState();
}
class _SSHFormState extends NameFormState<SSHForm> {
final _algoController = TextEditingController();
final _fingerprintTypeController = TextEditingController();
final _fingerprintController = TextEditingController();
@override
DNSRecord buildRecord() {
return DNSRecord(
name: widget.name,
type: DNSRecordType.SSH,
data: {
"sshfp": [
[
int.parse(_algoController.text.trim()),
int.parse(_fingerprintTypeController.text.trim()),
_fingerprintController.text.trim(),
],
],
},
);
}
@override
void dispose() {
_algoController.dispose();
_fingerprintTypeController.dispose();
_fingerprintController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const DNSFieldText(
"Algorithm",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _algoController,
keyboardType: TextInputType.number,
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
const DNSFieldText(
"Fingerprint type",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _fingerprintTypeController,
keyboardType: TextInputType.number,
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
const DNSFieldText(
"Fingerprint (base64)",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _fingerprintController,
),
],
);
}
}

View file

@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import '../../../../models/namecoin_dns/dns_record.dart';
import '../../../../models/namecoin_dns/dns_record_type.dart';
import '../../../../utilities/util.dart';
import '../name_form_interface.dart';
class TLSForm extends NameFormStatefulWidget {
const TLSForm({super.key, required super.name});
@override
NameFormState<TLSForm> createState() => _TLSFormState();
}
class _TLSFormState extends NameFormState<TLSForm> {
final _pubkeyController = TextEditingController();
@override
DNSRecord buildRecord() {
return DNSRecord(
name: widget.name,
type: DNSRecordType.TLS,
data: {
"map": {
"*": {
"tls": [
[
2,
1,
0,
_pubkeyController.text.trim(),
],
],
},
},
},
);
}
@override
void dispose() {
_pubkeyController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const DNSFieldText(
"DANE-TA public key (base64)",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _pubkeyController,
),
],
);
}
}

View file

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import '../../../../models/namecoin_dns/dns_record.dart';
import '../../../../models/namecoin_dns/dns_record_type.dart';
import '../../../../utilities/util.dart';
import '../name_form_interface.dart';
class TXTForm extends NameFormStatefulWidget {
const TXTForm({super.key, required super.name});
@override
NameFormState<TXTForm> createState() => _TXTFormState();
}
class _TXTFormState extends NameFormState<TXTForm> {
final _valueController = TextEditingController();
@override
DNSRecord buildRecord() {
return DNSRecord(
name: widget.name,
type: DNSRecordType.TXT,
data: {
"txt": [_valueController.text.trim()],
},
);
}
@override
void dispose() {
_valueController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const DNSFieldText(
"Value",
),
SizedBox(
height: Util.isDesktop ? 10 : 8,
),
DNSFormField(
controller: _valueController,
),
],
);
}
}

View file

@ -0,0 +1,541 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:namecoin/namecoin.dart';
import '../../../providers/providers.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/util.dart';
import '../../../wallets/models/name_op_state.dart';
import '../../../wallets/models/tx_data.dart';
import '../../../wallets/wallet/impl/namecoin_wallet.dart';
import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/desktop/secondary_button.dart';
import '../../../widgets/stack_dialog.dart';
import '../../models/namecoin_dns/dns_a_record_address_type.dart';
import '../../models/namecoin_dns/dns_record.dart';
import '../../models/namecoin_dns/dns_record_type.dart';
import '../../route_generator.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/amount/amount_formatter.dart';
import '../../utilities/show_loading.dart';
import '../../utilities/text_styles.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../widgets/background.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/custom_buttons/blue_text_button.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/dialogs/s_dialog.dart';
import '../../widgets/rounded_white_container.dart';
import 'add_dns_record/add_dns_step_1.dart';
import 'confirm_name_transaction_view.dart';
class BuyDomainView extends ConsumerStatefulWidget {
const BuyDomainView({
super.key,
required this.walletId,
required this.domainName,
});
final String walletId;
final String domainName;
static const routeName = "/buyDomainView";
@override
ConsumerState<BuyDomainView> createState() => _BuyDomainWidgetState();
}
class _BuyDomainWidgetState extends ConsumerState<BuyDomainView> {
bool _settingsHidden = true;
final List<DNSRecord> _dnsRecords = [];
String _getFormattedDNSRecords() {
if (_dnsRecords.isEmpty) return "";
return DNSRecord.merge(_dnsRecords);
}
String _getNameFormattedForInternal() {
String formattedName = widget.domainName;
if (!formattedName.startsWith("d/")) {
formattedName = "d/$formattedName";
}
if (formattedName.endsWith(".bit")) {
formattedName.split(".bit").first;
}
return formattedName;
}
Future<TxData> _preRegFuture() async {
final wallet =
ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet;
final myAddress = await wallet.getCurrentReceivingAddress();
if (myAddress == null) {
throw Exception("No receiving address found");
}
final value = _getFormattedDNSRecords();
Logging.instance.t("Formatted namecoin name value: $value");
// get address private key for deterministic salt
final pk = await wallet.getPrivateKey(myAddress);
final formattedName = _getNameFormattedForInternal();
final data = await compute(_computeScriptNameNew, (formattedName, pk.data));
TxData txData = TxData(
opNameState: NameOpState(
name: formattedName,
saltHex: data.$2,
commitment: data.$3,
value: value,
nameScriptHex: data.$1,
type: OpName.nameNew,
outputPosition: -1, //currently unknown, updated later
),
note: "Reserve ${widget.domainName.substring(2)}.bit",
feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable?
recipients: [
(
address: myAddress.value,
isChange: false,
amount: Amount(
rawValue: BigInt.from(kNameNewAmountSats),
fractionDigits: wallet.cryptoCurrency.fractionDigits,
),
),
],
);
txData = await wallet.prepareNameSend(txData: txData);
return txData;
}
bool _preRegLock = false;
Future<void> _preRegister() async {
if (_preRegLock) return;
_preRegLock = true;
try {
final txData = (await showLoading(
whileFuture: _preRegFuture(),
context: context,
message: "Preparing transaction...",
onException: (e) {
throw e;
},
))!;
if (mounted) {
if (Util.isDesktop) {
await showDialog<void>(
context: context,
builder: (context) => SDialog(
child: SizedBox(
width: 580,
child: ConfirmNameTransactionView(
txData: txData,
walletId: widget.walletId,
),
),
),
);
} else {
await Navigator.of(context).pushNamed(
ConfirmNameTransactionView.routeName,
arguments: (txData, widget.walletId),
);
}
}
} catch (e, s) {
Logging.instance.e("_preRegister failed", error: e, stackTrace: s);
if (mounted) {
String err = e.toString();
if (err.startsWith("Exception: ")) {
err = err.replaceFirst("Exception: ", "");
}
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Error",
message: err,
desktopPopRootNavigator: Util.isDesktop,
maxWidth: Util.isDesktop ? 600 : null,
),
);
}
} finally {
_preRegLock = false;
}
}
bool _addLock = false;
Future<void> _addRecord() async {
if (_addLock) return;
_addLock = true;
try {
final value = await showDialog<DNSRecord?>(
context: context,
barrierDismissible: false,
builder: (context) {
return Navigator(
onGenerateRoute: (settings) {
return RouteGenerator.getRoute(
builder: (context) {
return Util.isDesktop
? SDialog(
child: SizedBox(
width: 580,
child: Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(
left: 32,
),
child: Text(
"Add DNS record",
style: STextStyles.desktopH3(context),
),
),
DesktopDialogCloseButton(
onPressedOverride: () {
Navigator.of(
context,
rootNavigator: true,
).pop();
},
),
],
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: AddDnsStep1(
name: _getNameFormattedForInternal(),
),
),
],
),
),
)
: StackDialogBase(
child: AddDnsStep1(
name: _getNameFormattedForInternal(),
),
);
},
);
},
);
},
);
if (mounted && value != null) {
setState(() {
_dnsRecords.add(value);
});
}
} catch (e, s) {
Logging.instance.e("Add DNS record failed", error: e, stackTrace: s);
if (mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Add DNS record failed",
desktopPopRootNavigator: Util.isDesktop,
maxWidth: Util.isDesktop ? 600 : null,
),
);
}
} finally {
_addLock = false;
}
}
@override
Widget build(BuildContext context) {
final coin = ref.watch(pWalletCoin(widget.walletId));
return ConditionalParent(
condition: !Util.isDesktop,
builder: (child) {
return Background(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
leading: const AppBarBackButton(),
titleSpacing: 0,
title: Text(
"Buy domain",
style: STextStyles.navBarTitle(context),
overflow: TextOverflow.ellipsis,
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (ctx, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints:
BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: child,
),
),
),
);
},
),
),
),
);
},
child: Column(
crossAxisAlignment: Util.isDesktop
? CrossAxisAlignment.start
: CrossAxisAlignment.stretch,
children: [
if (!Util.isDesktop)
Text(
"Buy domain",
style: Util.isDesktop
? STextStyles.desktopH3(context)
: STextStyles.pageTitleH2(context),
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
Row(
mainAxisAlignment: Util.isDesktop
? MainAxisAlignment.center
: MainAxisAlignment.start,
children: [
Text(
"Name registration will take approximately 2 to 4 hours.",
style: Util.isDesktop
? STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
)
: STextStyles.w500_12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
),
),
],
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Domain name",
style: Util.isDesktop
? STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.infoItemLabel,
)
: STextStyles.w500_12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.infoItemLabel,
),
),
Text(
"${widget.domainName.substring(2)}.bit",
style: Util.isDesktop
? STextStyles.w500_14(context)
: STextStyles.w500_12(context),
),
],
),
),
SizedBox(
height: Util.isDesktop ? 16 : 8,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Amount",
style: Util.isDesktop
? STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.infoItemLabel,
)
: STextStyles.w500_12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.infoItemLabel,
),
),
Text(
ref.watch(pAmountFormatter(coin)).format(
Amount(
rawValue: BigInt.from(kNameNewAmountSats),
fractionDigits: coin.fractionDigits,
),
),
style: Util.isDesktop
? STextStyles.w500_14(context)
: STextStyles.w500_12(context),
),
],
),
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Row(
children: [child],
),
child: CustomTextButton(
text: _settingsHidden ? "More settings" : "Hide settings",
onTap: () {
setState(() {
_settingsHidden = !_settingsHidden;
});
},
),
),
if (!_settingsHidden)
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
if (!_settingsHidden)
if (_dnsRecords.isEmpty)
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Add DNS records to your domain name",
style: STextStyles.w500_12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
),
),
if (!_settingsHidden)
ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Expanded(child: child),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
..._dnsRecords.map(
(e) => DNSRecordCard(
key: ValueKey(e),
record: e,
onRemoveTapped: () => setState(() {
_dnsRecords.remove(e);
}),
),
),
SizedBox(
height: Util.isDesktop ? 16 : 8,
),
SecondaryButton(
label: _dnsRecords.isEmpty
? "Add DNS record"
: "Add another DNS record",
buttonHeight: Util.isDesktop ? ButtonHeight.l : null,
onPressed: _addRecord,
),
],
),
),
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
if (!Util.isDesktop && _settingsHidden) const Spacer(),
PrimaryButton(
label: "Buy",
// width: Util.isDesktop ? 160 : double.infinity,
buttonHeight: Util.isDesktop ? ButtonHeight.l : null,
onPressed: _preRegister,
),
SizedBox(
height: Util.isDesktop ? 32 : 16,
),
],
),
);
}
}
(String, String, String) _computeScriptNameNew((String, Uint8List) args) {
return scriptNameNew(args.$1, args.$2);
}
class DNSRecordCard extends StatelessWidget {
const DNSRecordCard({
super.key,
required this.record,
required this.onRemoveTapped,
});
final DNSRecord record;
final VoidCallback onRemoveTapped;
String get _extraInfo {
if (record.type == DNSRecordType.A) {
// TODO error handling
return " - ${DNSAddressType.values.firstWhere((e) => e.key == record.data.keys.first).name}";
}
return "";
}
@override
Widget build(BuildContext context) {
return RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${record.type.name}$_extraInfo",
),
CustomTextButton(
text: "Remove",
onTap: onRemoveTapped,
),
],
),
Text(record.getValueString()),
],
),
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import '../../models/isar/models/blockchain_data/utxo.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/constants.dart';
import '../../utilities/text_styles.dart';
import '../../widgets/background.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/toggle.dart';
import 'sub_widgets/transfer_option_widget.dart';
import 'sub_widgets/update_option_widget.dart';
class ManageDomainView extends StatefulWidget {
const ManageDomainView({
super.key,
required this.walletId,
required this.utxo,
});
final String walletId;
final UTXO utxo;
static const routeName = "/manageDomainView";
@override
State<ManageDomainView> createState() => _ManageDomainViewState();
}
class _ManageDomainViewState extends State<ManageDomainView> {
bool _onTransfer = true;
@override
Widget build(BuildContext context) {
return Background(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
leading: const AppBarBackButton(),
titleSpacing: 0,
title: Text(
"Manage domain",
style: STextStyles.navBarTitle(context),
overflow: TextOverflow.ellipsis,
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
SizedBox(
height: 48,
child: Toggle(
key: UniqueKey(),
onColor:
Theme.of(context).extension<StackColors>()!.popupBG,
offColor: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
onText: "Transfer",
offText: "Update",
isOn: !_onTransfer,
onValueChanged: (value) {
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
_onTransfer = !value;
});
},
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
),
const SizedBox(
height: 16,
),
Expanded(
child: IndexedStack(
index: _onTransfer ? 0 : 1,
children: [
TransferOptionWidget(
walletId: widget.walletId,
utxo: widget.utxo,
),
UpdateOptionWidget(
walletId: widget.walletId,
utxo: widget.utxo,
),
],
),
),
],
),
),
),
),
);
}
}

View file

@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/assets.dart';
import '../../utilities/constants.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_app_bar.dart';
import '../../widgets/desktop/desktop_scaffold.dart';
import '../../widgets/toggle.dart';
import 'sub_widgets/buy_domain_option_widget.dart';
import 'sub_widgets/manage_domains_option_widget.dart';
class NamecoinNamesHomeView extends ConsumerStatefulWidget {
const NamecoinNamesHomeView({
super.key,
required this.walletId,
});
final String walletId;
static const String routeName = "/namecoinNamesHomeView";
@override
ConsumerState<NamecoinNamesHomeView> createState() =>
_NamecoinNamesHomeViewState();
}
class _NamecoinNamesHomeViewState extends ConsumerState<NamecoinNamesHomeView> {
bool _onManage = true;
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final isDesktop = Util.isDesktop;
return MasterScaffold(
isDesktop: isDesktop,
appBar: isDesktop
? DesktopAppBar(
isCompactHeight: true,
background: Theme.of(context).extension<StackColors>()!.popupBG,
leading: Row(
children: [
Padding(
padding: const EdgeInsets.only(
left: 24,
right: 20,
),
child: AppBarIconButton(
size: 32,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
shadows: const [],
icon: SvgPicture.asset(
Assets.svg.arrowLeft,
width: 18,
height: 18,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: Navigator.of(context).pop,
),
),
SvgPicture.asset(
Assets.svg.robotHead,
width: 32,
height: 32,
color: Theme.of(context).extension<StackColors>()!.textDark,
),
const SizedBox(
width: 10,
),
Text(
"Domains",
style: STextStyles.desktopH3(context),
),
],
),
)
: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
titleSpacing: 0,
title: Text(
"Domains",
style: STextStyles.navBarTitle(context),
overflow: TextOverflow.ellipsis,
),
),
body: ConditionalParent(
condition: !isDesktop,
builder: (child) => SafeArea(
child: Padding(
padding: const EdgeInsets.only(
top: 16,
left: 16,
right: 16,
),
child: child,
),
),
child: Util.isDesktop
? Padding(
padding: const EdgeInsets.only(
top: 24,
left: 24,
right: 24,
),
child: Row(
children: [
SizedBox(
width: 460,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Text(
"Buy domain",
style:
STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconLeft,
),
),
],
),
const SizedBox(
height: 14,
),
Flexible(
child: BuyDomainOptionWidget(
walletId: widget.walletId,
),
),
],
),
),
const SizedBox(
width: 24,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Text(
"Manage domains",
style:
STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconLeft,
),
),
],
),
const SizedBox(
height: 14,
),
Flexible(
child: SingleChildScrollView(
child: ManageDomainsOptionWidget(
walletId: widget.walletId,
),
),
),
],
),
),
],
),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
height: 48,
child: Toggle(
key: UniqueKey(),
onColor:
Theme.of(context).extension<StackColors>()!.popupBG,
offColor: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
onText: "Buy domain",
offText: "Manage domains",
isOn: !_onManage,
onValueChanged: (value) {
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
_onManage = !value;
});
},
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
),
const SizedBox(
height: 16,
),
Expanded(
child: IndexedStack(
index: _onManage ? 0 : 1,
children: [
BuyDomainOptionWidget(
walletId: widget.walletId,
),
LayoutBuilder(
builder: (context, constraints) {
return ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: SingleChildScrollView(
child: IntrinsicHeight(
child: ManageDomainsOptionWidget(
walletId: widget.walletId,
),
),
),
);
},
),
],
),
),
],
),
),
);
}
}

Some files were not shown because too many files have changed in this diff Show more