diff --git a/README.md b/README.md index 9d0f6ff6d..b1fd68363 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ Highlights include: - The only OS supported for building is Ubuntu 20.04 - A machine with at least 100 GB of Storage -The following prerequisities can be installed with the setup script `scripts/setup.sh` or manually as described below: +The following prerequisites can be installed with the setup script `scripts/setup.sh` or manually as described below: -- Flutter 3.3.4 [(install manually or with git, do not install with snap)](https://docs.flutter.dev/get-started/install) -- Dart SDK Requirement (>=2.17.0, up until <3.0.0) +- Flutter 3.7.6 [(install manually or with git, do not install with snap)](https://docs.flutter.dev/get-started/install) +- Dart SDK Requirement (>=2.19.0, up until <3.0.0) (normally included with a flutter install) - Android setup ([Android Studio](https://developer.android.com/studio) and subsequent dependencies) ### Scripted setup diff --git a/assets/gif/coins/bitcoin/kiss.gif b/assets/gif/coins/bitcoin/kiss.gif new file mode 100644 index 000000000..8649f857d Binary files /dev/null and b/assets/gif/coins/bitcoin/kiss.gif differ diff --git a/assets/gif/coins/bitcoin/plain.gif b/assets/gif/coins/bitcoin/plain.gif new file mode 100644 index 000000000..5f3680219 Binary files /dev/null and b/assets/gif/coins/bitcoin/plain.gif differ diff --git a/assets/gif/coins/bitcoincash/kiss.gif b/assets/gif/coins/bitcoincash/kiss.gif new file mode 100644 index 000000000..bbd5924b1 Binary files /dev/null and b/assets/gif/coins/bitcoincash/kiss.gif differ diff --git a/assets/gif/coins/bitcoincash/plain.gif b/assets/gif/coins/bitcoincash/plain.gif new file mode 100644 index 000000000..4c8d29c77 Binary files /dev/null and b/assets/gif/coins/bitcoincash/plain.gif differ diff --git a/assets/gif/coins/dogecoin/kiss.gif b/assets/gif/coins/dogecoin/kiss.gif new file mode 100644 index 000000000..d0f1ffba4 Binary files /dev/null and b/assets/gif/coins/dogecoin/kiss.gif differ diff --git a/assets/gif/coins/dogecoin/plain.gif b/assets/gif/coins/dogecoin/plain.gif new file mode 100644 index 000000000..9ce1fbb7a Binary files /dev/null and b/assets/gif/coins/dogecoin/plain.gif differ diff --git a/assets/gif/coins/epicCash/kiss.gif b/assets/gif/coins/epicCash/kiss.gif new file mode 100644 index 000000000..ef8f27fab Binary files /dev/null and b/assets/gif/coins/epicCash/kiss.gif differ diff --git a/assets/gif/coins/epicCash/plain.gif b/assets/gif/coins/epicCash/plain.gif new file mode 100644 index 000000000..4a05889af Binary files /dev/null and b/assets/gif/coins/epicCash/plain.gif differ diff --git a/assets/gif/coins/ethereum/kiss.gif b/assets/gif/coins/ethereum/kiss.gif new file mode 100644 index 000000000..712834662 Binary files /dev/null and b/assets/gif/coins/ethereum/kiss.gif differ diff --git a/assets/gif/coins/ethereum/plain.gif b/assets/gif/coins/ethereum/plain.gif new file mode 100644 index 000000000..0da0eeb64 Binary files /dev/null and b/assets/gif/coins/ethereum/plain.gif differ diff --git a/assets/gif/coins/firo/kiss.gif b/assets/gif/coins/firo/kiss.gif new file mode 100644 index 000000000..0225077db Binary files /dev/null and b/assets/gif/coins/firo/kiss.gif differ diff --git a/assets/gif/coins/firo/plain.gif b/assets/gif/coins/firo/plain.gif new file mode 100644 index 000000000..3d2f9b620 Binary files /dev/null and b/assets/gif/coins/firo/plain.gif differ diff --git a/assets/gif/coins/litecoin/kiss.gif b/assets/gif/coins/litecoin/kiss.gif new file mode 100644 index 000000000..68272d06d Binary files /dev/null and b/assets/gif/coins/litecoin/kiss.gif differ diff --git a/assets/gif/coins/litecoin/plain.gif b/assets/gif/coins/litecoin/plain.gif new file mode 100644 index 000000000..5c12d94bf Binary files /dev/null and b/assets/gif/coins/litecoin/plain.gif differ diff --git a/assets/gif/coins/monero/kiss.gif b/assets/gif/coins/monero/kiss.gif new file mode 100644 index 000000000..b54fb2096 Binary files /dev/null and b/assets/gif/coins/monero/kiss.gif differ diff --git a/assets/gif/coins/monero/plain.gif b/assets/gif/coins/monero/plain.gif new file mode 100644 index 000000000..5090eefc2 Binary files /dev/null and b/assets/gif/coins/monero/plain.gif differ diff --git a/assets/gif/coins/namecoin/kiss.gif b/assets/gif/coins/namecoin/kiss.gif new file mode 100644 index 000000000..10b74b356 Binary files /dev/null and b/assets/gif/coins/namecoin/kiss.gif differ diff --git a/assets/gif/coins/namecoin/plain.gif b/assets/gif/coins/namecoin/plain.gif new file mode 100644 index 000000000..954fa0046 Binary files /dev/null and b/assets/gif/coins/namecoin/plain.gif differ diff --git a/assets/gif/coins/particl/kiss.gif b/assets/gif/coins/particl/kiss.gif new file mode 100644 index 000000000..0cc80c1c8 Binary files /dev/null and b/assets/gif/coins/particl/kiss.gif differ diff --git a/assets/gif/coins/particl/plain.gif b/assets/gif/coins/particl/plain.gif new file mode 100644 index 000000000..99d36ef9c Binary files /dev/null and b/assets/gif/coins/particl/plain.gif differ diff --git a/assets/gif/coins/wownero/kiss.gif b/assets/gif/coins/wownero/kiss.gif new file mode 100644 index 000000000..16c1c1fdc Binary files /dev/null and b/assets/gif/coins/wownero/kiss.gif differ diff --git a/assets/gif/coins/wownero/plain.gif b/assets/gif/coins/wownero/plain.gif new file mode 100644 index 000000000..0c30a857f Binary files /dev/null and b/assets/gif/coins/wownero/plain.gif differ diff --git a/assets/images/dark/bitcoin.png b/assets/images/dark/bitcoin.png deleted file mode 100644 index 63408e07e..000000000 Binary files a/assets/images/dark/bitcoin.png and /dev/null differ diff --git a/assets/images/dark/bitcoin.svg b/assets/images/dark/bitcoin.svg deleted file mode 100644 index d3abd0954..000000000 --- a/assets/images/dark/bitcoin.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/dark/bitcoincash.png b/assets/images/dark/bitcoincash.png deleted file mode 100644 index 18552e02e..000000000 Binary files a/assets/images/dark/bitcoincash.png and /dev/null differ diff --git a/assets/images/dark/bitcoincash.svg b/assets/images/dark/bitcoincash.svg deleted file mode 100644 index 6a10cce09..000000000 --- a/assets/images/dark/bitcoincash.svg +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/dark/doge.png b/assets/images/dark/doge.png deleted file mode 100644 index b08e82a5b..000000000 Binary files a/assets/images/dark/doge.png and /dev/null differ diff --git a/assets/images/dark/epic-cash.png b/assets/images/dark/epic-cash.png deleted file mode 100644 index ad26be290..000000000 Binary files a/assets/images/dark/epic-cash.png and /dev/null differ diff --git a/assets/images/dark/epic-cash.svg b/assets/images/dark/epic-cash.svg deleted file mode 100644 index 0edcb4e96..000000000 --- a/assets/images/dark/epic-cash.svg +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/dark/ethereum.png b/assets/images/dark/ethereum.png deleted file mode 100644 index 2827ad56e..000000000 Binary files a/assets/images/dark/ethereum.png and /dev/null differ diff --git a/assets/images/dark/ethereum.svg b/assets/images/dark/ethereum.svg deleted file mode 100644 index df9a44d1e..000000000 --- a/assets/images/dark/ethereum.svg +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/dark/firo.png b/assets/images/dark/firo.png deleted file mode 100644 index 4a679586d..000000000 Binary files a/assets/images/dark/firo.png and /dev/null differ diff --git a/assets/images/dark/firo.svg b/assets/images/dark/firo.svg deleted file mode 100644 index 6f502205c..000000000 --- a/assets/images/dark/firo.svg +++ /dev/null @@ -1,198 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/dark/litecoin.png b/assets/images/dark/litecoin.png deleted file mode 100644 index 17994bd47..000000000 Binary files a/assets/images/dark/litecoin.png and /dev/null differ diff --git a/assets/images/dark/litecoin.svg b/assets/images/dark/litecoin.svg deleted file mode 100644 index f224c429f..000000000 --- a/assets/images/dark/litecoin.svg +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/dark/monero.png b/assets/images/dark/monero.png deleted file mode 100644 index 679e647ea..000000000 Binary files a/assets/images/dark/monero.png and /dev/null differ diff --git a/assets/images/dark/monero.svg b/assets/images/dark/monero.svg deleted file mode 100644 index 86e59e020..000000000 --- a/assets/images/dark/monero.svg +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/dark/namecoin.png b/assets/images/dark/namecoin.png deleted file mode 100644 index 45cf8abb7..000000000 Binary files a/assets/images/dark/namecoin.png and /dev/null differ diff --git a/assets/images/dark/namecoin.svg b/assets/images/dark/namecoin.svg deleted file mode 100644 index ca61d80b4..000000000 --- a/assets/images/dark/namecoin.svg +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/dark/particl.png b/assets/images/dark/particl.png deleted file mode 100644 index ef5939f47..000000000 Binary files a/assets/images/dark/particl.png and /dev/null differ diff --git a/assets/images/dark/particl.svg b/assets/images/dark/particl.svg deleted file mode 100644 index d15918985..000000000 --- a/assets/images/dark/particl.svg +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/dark/stack.png b/assets/images/dark/stack.png deleted file mode 100644 index b59af1608..000000000 Binary files a/assets/images/dark/stack.png and /dev/null differ diff --git a/assets/images/dark/stack.svg b/assets/images/dark/stack.svg deleted file mode 100644 index 43f29e515..000000000 --- a/assets/images/dark/stack.svg +++ /dev/null @@ -1,440 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/dark/wownero.png b/assets/images/dark/wownero.png deleted file mode 100644 index 857ab2b4c..000000000 Binary files a/assets/images/dark/wownero.png and /dev/null differ diff --git a/assets/images/dark/wownero.svg b/assets/images/dark/wownero.svg deleted file mode 100644 index 812123542..000000000 --- a/assets/images/dark/wownero.svg +++ /dev/null @@ -1,190 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/forest/bitcoin.png b/assets/images/forest/bitcoin.png deleted file mode 100644 index 695b1d0ed..000000000 Binary files a/assets/images/forest/bitcoin.png and /dev/null differ diff --git a/assets/images/forest/bitcoincash.png b/assets/images/forest/bitcoincash.png deleted file mode 100644 index 103ae537a..000000000 Binary files a/assets/images/forest/bitcoincash.png and /dev/null differ diff --git a/assets/images/forest/doge.png b/assets/images/forest/doge.png deleted file mode 100644 index 714d2ad85..000000000 Binary files a/assets/images/forest/doge.png and /dev/null differ diff --git a/assets/images/forest/doge.svg b/assets/images/forest/doge.svg deleted file mode 100644 index 2c1d693b4..000000000 --- a/assets/images/forest/doge.svg +++ /dev/null @@ -1,255 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/forest/epic-cash.png b/assets/images/forest/epic-cash.png deleted file mode 100644 index 75b75c267..000000000 Binary files a/assets/images/forest/epic-cash.png and /dev/null differ diff --git a/assets/images/forest/firo.png b/assets/images/forest/firo.png deleted file mode 100644 index 508afc282..000000000 Binary files a/assets/images/forest/firo.png and /dev/null differ diff --git a/assets/images/forest/litecoin.png b/assets/images/forest/litecoin.png deleted file mode 100644 index 17994bd47..000000000 Binary files a/assets/images/forest/litecoin.png and /dev/null differ diff --git a/assets/images/forest/monero.png b/assets/images/forest/monero.png deleted file mode 100644 index 0fb273fba..000000000 Binary files a/assets/images/forest/monero.png and /dev/null differ diff --git a/assets/images/forest/namecoin.png b/assets/images/forest/namecoin.png deleted file mode 100644 index 72706dc9c..000000000 Binary files a/assets/images/forest/namecoin.png and /dev/null differ diff --git a/assets/images/forest/particl.png b/assets/images/forest/particl.png deleted file mode 100644 index 164cf389b..000000000 Binary files a/assets/images/forest/particl.png and /dev/null differ diff --git a/assets/images/forest/stack.png b/assets/images/forest/stack.png deleted file mode 100644 index 17b4256cb..000000000 Binary files a/assets/images/forest/stack.png and /dev/null differ diff --git a/assets/images/forest/wownero.png b/assets/images/forest/wownero.png deleted file mode 100644 index 7fd6a111d..000000000 Binary files a/assets/images/forest/wownero.png and /dev/null differ diff --git a/assets/images/fruitSorbet/bitcoin.png b/assets/images/fruitSorbet/bitcoin.png deleted file mode 100644 index 9424682fe..000000000 Binary files a/assets/images/fruitSorbet/bitcoin.png and /dev/null differ diff --git a/assets/images/fruitSorbet/bitcoincash.png b/assets/images/fruitSorbet/bitcoincash.png deleted file mode 100644 index a10e138bc..000000000 Binary files a/assets/images/fruitSorbet/bitcoincash.png and /dev/null differ diff --git a/assets/images/fruitSorbet/doge.png b/assets/images/fruitSorbet/doge.png deleted file mode 100644 index b08e82a5b..000000000 Binary files a/assets/images/fruitSorbet/doge.png and /dev/null differ diff --git a/assets/images/fruitSorbet/epic-cash.png b/assets/images/fruitSorbet/epic-cash.png deleted file mode 100644 index ad26be290..000000000 Binary files a/assets/images/fruitSorbet/epic-cash.png and /dev/null differ diff --git a/assets/images/fruitSorbet/epic-cash.svg b/assets/images/fruitSorbet/epic-cash.svg deleted file mode 100644 index 0edcb4e96..000000000 --- a/assets/images/fruitSorbet/epic-cash.svg +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/fruitSorbet/firo.png b/assets/images/fruitSorbet/firo.png deleted file mode 100644 index 543ef4abb..000000000 Binary files a/assets/images/fruitSorbet/firo.png and /dev/null differ diff --git a/assets/images/fruitSorbet/litecoin.png b/assets/images/fruitSorbet/litecoin.png deleted file mode 100644 index 17994bd47..000000000 Binary files a/assets/images/fruitSorbet/litecoin.png and /dev/null differ diff --git a/assets/images/fruitSorbet/monero.png b/assets/images/fruitSorbet/monero.png deleted file mode 100644 index 679e647ea..000000000 Binary files a/assets/images/fruitSorbet/monero.png and /dev/null differ diff --git a/assets/images/fruitSorbet/monero.svg b/assets/images/fruitSorbet/monero.svg deleted file mode 100644 index 86e59e020..000000000 --- a/assets/images/fruitSorbet/monero.svg +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/fruitSorbet/namecoin.png b/assets/images/fruitSorbet/namecoin.png deleted file mode 100644 index 45cf8abb7..000000000 Binary files a/assets/images/fruitSorbet/namecoin.png and /dev/null differ diff --git a/assets/images/fruitSorbet/namecoin.svg b/assets/images/fruitSorbet/namecoin.svg deleted file mode 100644 index ca61d80b4..000000000 --- a/assets/images/fruitSorbet/namecoin.svg +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/fruitSorbet/particl.png b/assets/images/fruitSorbet/particl.png deleted file mode 100644 index ef5939f47..000000000 Binary files a/assets/images/fruitSorbet/particl.png and /dev/null differ diff --git a/assets/images/fruitSorbet/stack.png b/assets/images/fruitSorbet/stack.png deleted file mode 100644 index 303c482af..000000000 Binary files a/assets/images/fruitSorbet/stack.png and /dev/null differ diff --git a/assets/images/fruitSorbet/wownero.png b/assets/images/fruitSorbet/wownero.png deleted file mode 100644 index 857ab2b4c..000000000 Binary files a/assets/images/fruitSorbet/wownero.png and /dev/null differ diff --git a/assets/images/light/bitcoin.png b/assets/images/light/bitcoin.png deleted file mode 100644 index 63408e07e..000000000 Binary files a/assets/images/light/bitcoin.png and /dev/null differ diff --git a/assets/images/light/bitcoin.svg b/assets/images/light/bitcoin.svg deleted file mode 100644 index d3abd0954..000000000 --- a/assets/images/light/bitcoin.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/light/bitcoincash.png b/assets/images/light/bitcoincash.png deleted file mode 100644 index 18552e02e..000000000 Binary files a/assets/images/light/bitcoincash.png and /dev/null differ diff --git a/assets/images/light/bitcoincash.svg b/assets/images/light/bitcoincash.svg deleted file mode 100644 index 6a10cce09..000000000 --- a/assets/images/light/bitcoincash.svg +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/light/doge.png b/assets/images/light/doge.png deleted file mode 100644 index b08e82a5b..000000000 Binary files a/assets/images/light/doge.png and /dev/null differ diff --git a/assets/images/light/doge.svg b/assets/images/light/doge.svg deleted file mode 100644 index 2c1d693b4..000000000 --- a/assets/images/light/doge.svg +++ /dev/null @@ -1,255 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/light/epic-cash.png b/assets/images/light/epic-cash.png deleted file mode 100644 index ad26be290..000000000 Binary files a/assets/images/light/epic-cash.png and /dev/null differ diff --git a/assets/images/light/epic-cash.svg b/assets/images/light/epic-cash.svg deleted file mode 100644 index 0edcb4e96..000000000 --- a/assets/images/light/epic-cash.svg +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/light/firo.png b/assets/images/light/firo.png deleted file mode 100644 index 4a679586d..000000000 Binary files a/assets/images/light/firo.png and /dev/null differ diff --git a/assets/images/light/firo.svg b/assets/images/light/firo.svg deleted file mode 100644 index 6f502205c..000000000 --- a/assets/images/light/firo.svg +++ /dev/null @@ -1,198 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/light/litecoin.png b/assets/images/light/litecoin.png deleted file mode 100644 index 17994bd47..000000000 Binary files a/assets/images/light/litecoin.png and /dev/null differ diff --git a/assets/images/light/litecoin.svg b/assets/images/light/litecoin.svg deleted file mode 100644 index f224c429f..000000000 --- a/assets/images/light/litecoin.svg +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/light/monero.png b/assets/images/light/monero.png deleted file mode 100644 index 679e647ea..000000000 Binary files a/assets/images/light/monero.png and /dev/null differ diff --git a/assets/images/light/monero.svg b/assets/images/light/monero.svg deleted file mode 100644 index 86e59e020..000000000 --- a/assets/images/light/monero.svg +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/light/namecoin.png b/assets/images/light/namecoin.png deleted file mode 100644 index 45cf8abb7..000000000 Binary files a/assets/images/light/namecoin.png and /dev/null differ diff --git a/assets/images/light/namecoin.svg b/assets/images/light/namecoin.svg deleted file mode 100644 index ca61d80b4..000000000 --- a/assets/images/light/namecoin.svg +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/light/particl.png b/assets/images/light/particl.png deleted file mode 100644 index ef5939f47..000000000 Binary files a/assets/images/light/particl.png and /dev/null differ diff --git a/assets/images/light/particl.svg b/assets/images/light/particl.svg deleted file mode 100644 index d15918985..000000000 --- a/assets/images/light/particl.svg +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/light/stack.png b/assets/images/light/stack.png deleted file mode 100644 index b59af1608..000000000 Binary files a/assets/images/light/stack.png and /dev/null differ diff --git a/assets/images/light/stack.svg b/assets/images/light/stack.svg deleted file mode 100644 index 43f29e515..000000000 --- a/assets/images/light/stack.svg +++ /dev/null @@ -1,440 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/light/wownero.png b/assets/images/light/wownero.png deleted file mode 100644 index 857ab2b4c..000000000 Binary files a/assets/images/light/wownero.png and /dev/null differ diff --git a/assets/images/light/wownero.svg b/assets/images/light/wownero.svg deleted file mode 100644 index 812123542..000000000 --- a/assets/images/light/wownero.svg +++ /dev/null @@ -1,190 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oceanBreeze/bitcoin.png b/assets/images/oceanBreeze/bitcoin.png deleted file mode 100644 index 63408e07e..000000000 Binary files a/assets/images/oceanBreeze/bitcoin.png and /dev/null differ diff --git a/assets/images/oceanBreeze/bitcoin.svg b/assets/images/oceanBreeze/bitcoin.svg deleted file mode 100644 index d3abd0954..000000000 --- a/assets/images/oceanBreeze/bitcoin.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oceanBreeze/bitcoincash.png b/assets/images/oceanBreeze/bitcoincash.png deleted file mode 100644 index 18552e02e..000000000 Binary files a/assets/images/oceanBreeze/bitcoincash.png and /dev/null differ diff --git a/assets/images/oceanBreeze/bitcoincash.svg b/assets/images/oceanBreeze/bitcoincash.svg deleted file mode 100644 index 6a10cce09..000000000 --- a/assets/images/oceanBreeze/bitcoincash.svg +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oceanBreeze/doge.png b/assets/images/oceanBreeze/doge.png deleted file mode 100644 index b08e82a5b..000000000 Binary files a/assets/images/oceanBreeze/doge.png and /dev/null differ diff --git a/assets/images/oceanBreeze/doge.svg b/assets/images/oceanBreeze/doge.svg deleted file mode 100644 index 2c1d693b4..000000000 --- a/assets/images/oceanBreeze/doge.svg +++ /dev/null @@ -1,255 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oceanBreeze/epic-cash.png b/assets/images/oceanBreeze/epic-cash.png deleted file mode 100644 index ad26be290..000000000 Binary files a/assets/images/oceanBreeze/epic-cash.png and /dev/null differ diff --git a/assets/images/oceanBreeze/epic-cash.svg b/assets/images/oceanBreeze/epic-cash.svg deleted file mode 100644 index 0edcb4e96..000000000 --- a/assets/images/oceanBreeze/epic-cash.svg +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oceanBreeze/firo.png b/assets/images/oceanBreeze/firo.png deleted file mode 100644 index 4a679586d..000000000 Binary files a/assets/images/oceanBreeze/firo.png and /dev/null differ diff --git a/assets/images/oceanBreeze/firo.svg b/assets/images/oceanBreeze/firo.svg deleted file mode 100644 index 6f502205c..000000000 --- a/assets/images/oceanBreeze/firo.svg +++ /dev/null @@ -1,198 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oceanBreeze/litecoin.png b/assets/images/oceanBreeze/litecoin.png deleted file mode 100644 index 17994bd47..000000000 Binary files a/assets/images/oceanBreeze/litecoin.png and /dev/null differ diff --git a/assets/images/oceanBreeze/litecoin.svg b/assets/images/oceanBreeze/litecoin.svg deleted file mode 100644 index f224c429f..000000000 --- a/assets/images/oceanBreeze/litecoin.svg +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oceanBreeze/monero.png b/assets/images/oceanBreeze/monero.png deleted file mode 100644 index 679e647ea..000000000 Binary files a/assets/images/oceanBreeze/monero.png and /dev/null differ diff --git a/assets/images/oceanBreeze/monero.svg b/assets/images/oceanBreeze/monero.svg deleted file mode 100644 index 86e59e020..000000000 --- a/assets/images/oceanBreeze/monero.svg +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oceanBreeze/namecoin.png b/assets/images/oceanBreeze/namecoin.png deleted file mode 100644 index 45cf8abb7..000000000 Binary files a/assets/images/oceanBreeze/namecoin.png and /dev/null differ diff --git a/assets/images/oceanBreeze/namecoin.svg b/assets/images/oceanBreeze/namecoin.svg deleted file mode 100644 index ca61d80b4..000000000 --- a/assets/images/oceanBreeze/namecoin.svg +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oceanBreeze/particl.png b/assets/images/oceanBreeze/particl.png deleted file mode 100644 index ef5939f47..000000000 Binary files a/assets/images/oceanBreeze/particl.png and /dev/null differ diff --git a/assets/images/oceanBreeze/particl.svg b/assets/images/oceanBreeze/particl.svg deleted file mode 100644 index d15918985..000000000 --- a/assets/images/oceanBreeze/particl.svg +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oceanBreeze/stack.png b/assets/images/oceanBreeze/stack.png deleted file mode 100644 index b59af1608..000000000 Binary files a/assets/images/oceanBreeze/stack.png and /dev/null differ diff --git a/assets/images/oceanBreeze/wownero.png b/assets/images/oceanBreeze/wownero.png deleted file mode 100644 index 857ab2b4c..000000000 Binary files a/assets/images/oceanBreeze/wownero.png and /dev/null differ diff --git a/assets/images/oceanBreeze/wownero.svg b/assets/images/oceanBreeze/wownero.svg deleted file mode 100644 index 812123542..000000000 --- a/assets/images/oceanBreeze/wownero.svg +++ /dev/null @@ -1,190 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oledBlack/bitcoin.png b/assets/images/oledBlack/bitcoin.png deleted file mode 100644 index 63408e07e..000000000 Binary files a/assets/images/oledBlack/bitcoin.png and /dev/null differ diff --git a/assets/images/oledBlack/bitcoin.svg b/assets/images/oledBlack/bitcoin.svg deleted file mode 100644 index d3abd0954..000000000 --- a/assets/images/oledBlack/bitcoin.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oledBlack/bitcoincash.png b/assets/images/oledBlack/bitcoincash.png deleted file mode 100644 index 18552e02e..000000000 Binary files a/assets/images/oledBlack/bitcoincash.png and /dev/null differ diff --git a/assets/images/oledBlack/bitcoincash.svg b/assets/images/oledBlack/bitcoincash.svg deleted file mode 100644 index 6a10cce09..000000000 --- a/assets/images/oledBlack/bitcoincash.svg +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oledBlack/doge.png b/assets/images/oledBlack/doge.png deleted file mode 100644 index b08e82a5b..000000000 Binary files a/assets/images/oledBlack/doge.png and /dev/null differ diff --git a/assets/images/oledBlack/doge.svg b/assets/images/oledBlack/doge.svg deleted file mode 100644 index 2c1d693b4..000000000 --- a/assets/images/oledBlack/doge.svg +++ /dev/null @@ -1,255 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oledBlack/epic-cash.png b/assets/images/oledBlack/epic-cash.png deleted file mode 100644 index ad26be290..000000000 Binary files a/assets/images/oledBlack/epic-cash.png and /dev/null differ diff --git a/assets/images/oledBlack/epic-cash.svg b/assets/images/oledBlack/epic-cash.svg deleted file mode 100644 index 0edcb4e96..000000000 --- a/assets/images/oledBlack/epic-cash.svg +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oledBlack/firo.png b/assets/images/oledBlack/firo.png deleted file mode 100644 index 4a679586d..000000000 Binary files a/assets/images/oledBlack/firo.png and /dev/null differ diff --git a/assets/images/oledBlack/firo.svg b/assets/images/oledBlack/firo.svg deleted file mode 100644 index 6f502205c..000000000 --- a/assets/images/oledBlack/firo.svg +++ /dev/null @@ -1,198 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oledBlack/litecoin.png b/assets/images/oledBlack/litecoin.png deleted file mode 100644 index 17994bd47..000000000 Binary files a/assets/images/oledBlack/litecoin.png and /dev/null differ diff --git a/assets/images/oledBlack/litecoin.svg b/assets/images/oledBlack/litecoin.svg deleted file mode 100644 index f224c429f..000000000 --- a/assets/images/oledBlack/litecoin.svg +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oledBlack/monero.png b/assets/images/oledBlack/monero.png deleted file mode 100644 index 679e647ea..000000000 Binary files a/assets/images/oledBlack/monero.png and /dev/null differ diff --git a/assets/images/oledBlack/monero.svg b/assets/images/oledBlack/monero.svg deleted file mode 100644 index 86e59e020..000000000 --- a/assets/images/oledBlack/monero.svg +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oledBlack/namecoin.png b/assets/images/oledBlack/namecoin.png deleted file mode 100644 index 45cf8abb7..000000000 Binary files a/assets/images/oledBlack/namecoin.png and /dev/null differ diff --git a/assets/images/oledBlack/namecoin.svg b/assets/images/oledBlack/namecoin.svg deleted file mode 100644 index ca61d80b4..000000000 --- a/assets/images/oledBlack/namecoin.svg +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oledBlack/particl.png b/assets/images/oledBlack/particl.png deleted file mode 100644 index ef5939f47..000000000 Binary files a/assets/images/oledBlack/particl.png and /dev/null differ diff --git a/assets/images/oledBlack/particl.svg b/assets/images/oledBlack/particl.svg deleted file mode 100644 index d15918985..000000000 --- a/assets/images/oledBlack/particl.svg +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oledBlack/stack.png b/assets/images/oledBlack/stack.png deleted file mode 100644 index b59af1608..000000000 Binary files a/assets/images/oledBlack/stack.png and /dev/null differ diff --git a/assets/images/oledBlack/stack.svg b/assets/images/oledBlack/stack.svg deleted file mode 100644 index 43f29e515..000000000 --- a/assets/images/oledBlack/stack.svg +++ /dev/null @@ -1,440 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/oledBlack/wownero.png b/assets/images/oledBlack/wownero.png deleted file mode 100644 index 857ab2b4c..000000000 Binary files a/assets/images/oledBlack/wownero.png and /dev/null differ diff --git a/assets/images/oledBlack/wownero.svg b/assets/images/oledBlack/wownero.svg deleted file mode 100644 index 812123542..000000000 --- a/assets/images/oledBlack/wownero.svg +++ /dev/null @@ -1,190 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/unclaimed.png b/assets/images/unclaimed.png deleted file mode 100644 index f99d4ab2a..000000000 Binary files a/assets/images/unclaimed.png and /dev/null differ diff --git a/assets/lottie/test.json b/assets/lottie/test.json deleted file mode 100644 index a8130f1da..000000000 --- a/assets/lottie/test.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"4.8.0","meta":{"g":"LottieFiles AE ","a":"","k":"","d":"","tc":""},"fr":25,"ip":0,"op":75,"w":600,"h":600,"nm":"stack-test-1","ddd":0,"assets":[{"id":"image_0","w":171,"h":171,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKsAAACrCAYAAAAZ6GwZAAAOSklEQVR4Xu2d309TWxbHd+ttEQaQqxgqcvlZDTRQwAASFCmQKgnWufwFlxcJmZdx/AeGm0xmHubF+zZvM/4F4w1PxgfBGKPEGIYXeZmouZFIUvUSGVFa25tF2juF29Jzzl7rnL3PWU2MD9177X2++9PDOmutvY9P8IcV0EQBnybz5GmyAoJhZQi0UYBh1WapeKIMKzOgjQIMqzZLxRNlWJkBbRRgWLVZKp4ow8oMaKMAw6rNUvFEGVZmQBsFGFZtloonyrAyA9oowLBqs1Q8UYaVGdBGAYZVm6XiiTKszIA2CjCs2iwVT5RhZQa0UYBh1WapeKIMKzOgjQIMqzZLxRNlWJkBbRRgWLVZKp4ow8oMaKMAw6rNUvFEGVZmQBsFGFZtloonyrAyA9oowLBqs1Q8UYaVkIHFxcWYEOJbIUSfEGKsYKhXPp9vVQixlEql7szMzLwknIZrTDOsBEu5uLgIgN4SQrQYNH87nU4vMLSHq8WwGqTJSLObN29eDYVCfwuHw92BQMBIl1/bZLPZLZ/PdyudTt+amZn52VRnjzRmWBEWen5+vlUI8deGhoYrAwMDxyVNvspmswvXrl37l6Qd13VnWCWWdHZ2ti4YDN4QQvzp1KlTaQRQC2ez7Pf7b0xPT4Nvyx8h+DBhqxRcv359Fv5sCyGO1dbWJsfGxuqt2irTD/zZG+waMKym+Zqfn49lMhmAtDffeXJyUlRVVZm2ZbRD3p9NJBILRvu4sR27AQZXFfzSTCYDfmRhCEqEQqHNwcHBBoNmZJu9EkLcSCQSd2QN6difYS2zauCXVlRULGSz2T8Wazo2NiZqa2vtXntP+rMM6yGYzc3NwZ9deIA6VqyZ3+/fmZ6errSb1ILxfsjFZz0R6mJYi5A2NzdnKKhfXV39enx8/LSDsArwZ/1+/8LVq1fBj3b1h2EtWN7cwxPcTff5paUIaGtre9Xd3W00S0UNEvizs4lEYol6IKfsM6xCiNzDE0D6nZmFUAzW/NSX0+n0rBtTt56GtSCo/2czkObbKgprfnqu82c9C2thUN8KqNCnqanpdX9/v6M+62FzB38WHhDdkrr1HKw5vxTipdK+pgoPWAZ/aP/JxWe19mc9A2upoL7BxS7a7OjRo5vxeNyuhIDMVPf6+ny+H1OpFKRutayfdT2sOb8UwjqmHp6MkpFIJIw2Vand9zqWIroa1nJBfQx6HMpgSU9dR3/WlbDmHp4gFCXtl5ajorOz8/2ZM2e+LtdO4e+XhRALOsRnXQWr2aA+BkB1dXU/jY6OfoNhy2Ebym+tcQWsVoP6WHBMTU0Js9tYsMbGtKP61hqtYZUN6mMtdCQS+dTR0XEUy54CdpTcWqMtrBhBfSwoAoHAh6mpqRosewrZUaoUUTtYMYP6mFAMDg5CITamSZVsKbG1RhtYKYL6mDQEg8HtK1euVGPaVMmWCltrlIeVOqiPCURvb+9Oc3Ozk8XYmJdTypZjW2uUhbXg4alkpb4dK2NmDL/f//ny5csVbogMGLhu2/1ZJWG1M6hvYFFMNTl58uTW8PBw0W0wpgzp09i2UkSlYHUiqE/BhAtDWYfKZNfWGiVghYenL1++3PL5fL+ngMcJmy6PDhzmz5JtrXEcVjuKTZyAFXa+jo6OVjqwTduJyz04JsnWGsdgVTVeirnSHgcWpET1Z22HVadQFBa4HglpFZULsxTRVljn5uYgDAWle156Wt5bxObm5mRvby/V4W1YvytKO9Jba2yBtdhhZpSqqGq7srIy2d/fX3/ixAlVp0g9r9uJRGLW6iCksLrxKd+q0IX96uvr9+6ylCcPYswT0QZKAoEEVvBLnz9//o/3799ffPPmTXpnZ6c6lUqdCAQCbysrK7dDodBXra2tp1tb4cBo737C4fBWOBw+5uKMF2pqFh3WqqqqP6TT6b/s7u6W3epRUVGxdf78+WBnZ6fb8+klf5GBQGArEokE3VRTQFXEjQkr3CZ/c36pkfsmQBuPx481NjYaae7KNjU1NZs9PT0NLvBnybbHYMEK73mCAxSknvJDodCb8fHxUE2NG+uYjf3G4HDivr6+Bg1dA/KNhxiwwh0VXtIgBWrhUkaj0c1z5841BINBYyvsslaQTGhvb9/t6upC05RKIsw4ark5YsAKoP56vn65AY1+Dwt28eJF4XV/tq+v75jCOxBsPSxDFlaImf3TKIBW2lVVVW1OTEw0eNmfhbfBDA4OKhPqcuoYIllY4cwk8oMkAPKmpqb/jo6OdnjZn21pafnQ1dVV46A/6+iBxTKwwlHm/7Zyt7Tax+/3/y8SiSQHBgZaPOzPfurp6cnaGeqyq161HBcysMJhZ0XfYFJuUNnvIbkwPDwsurq6PJu3tDF1i1o5JbP2MrBCqMrQ2fsyEzysb21t7at4PN7igtikZYkIU7ckNamWL1TydZjwOhslQivNzc0/TUxMfONh1+Bje3t7Cil1i5oilYHzYF+ZO2sWcyKytsCf7e7u3h4eHtbmcF/Zaz7YXyZ1q8K5AOX0cA2s+QsFfzYWi1W2tbXRvUy1nKoOfw+pW0iqmNhSo8SJK+Vkk4HVtrBVuYso9n1dXd3rycnJ0172Zw2kbslTpFbWrlQfGVgdf8AyIsTZs2dfj4yMnPawP7vT0dHh7+zsrCjQS8lTAsutp+thBQHAnx0YGNiNRCJfexVa8GfD4XAgHA7/Xcf3CcA6ysAKe6ksveys3C+I6nuv1s++fftW3L9/f/Pdu3fw8Al/+iFNrt0bW2RgtT2DhQXx8ePHN0dGRlxfb7C7uyuWlpY2X758WSxC8kNu86Y2b9SWgRVKA19gAeSEndbW1s1YLObKUsTV1dX3T58+PZrJZA7bhQFvIIS/kFq8UVsGVuCLpDzQTnD9fv/HaDSaGhoaUiLBIXvtGxsb4t69e1ufP382cz1726RzBfSyUyDrLwurY/UB2IqAP3vp0qWArvHZDx8+AKTJZDIpczbBjzlolfRnZWHV3hU4CD34s+Pj49rshQK/9NmzZ5tra2uYmbvvc66BUv6sLKyw1lrEW83eiXWIz66vr+88fPhQlPFLzV56vv3eG7Vzm0Ct2kDthwGrtlGBckrC1pqhoSFfNBpV6rVB4JcuLS0lt7e3Zf7kl7v8/PfK+LMYsLriQeuwlVNlqzj4pcvLy8mNjQ07ID0oieP+LBasMSHEfaM/VV3bQe1oPB6vt3trDfilq6urW2tra19lMpnfOagfuAbwUA3/bPdnsWAF/e4IIVxzcvVhQNi5VRz80idPnuyaDEVR8ww1rxCfhUNNbPtgwlqXS+GZie/ZdqHYAx05cuTThQsXslRbxcEvffToUT5Fij19LHt7VVt2xWcxYQUBXPuwVWp1q6urk7FYrB5rq3iZFCkWZNh2buegJY3PYsMKImhX4IKxco2NjcmxsTEpf3ZlZQX80kAmk9GxcJzcn6WAFdYefJnvMCDQyQaUIkaj0TScomKmFPHFixcfHzx4kFLML7UqPZk/SwWrZ4GFCzdaigihqLt376rul1qFFvxZSCpA/QjKhxJWmKBragesqF2qFBH80sePHyfX19ediJdauRSZPuDPArTSoS5qWOEiodAXoPVElKDYqhaWIq6trX1aWVnJEqVIZaCi7Jv3Z+F5xvLHDlhhcpYPGrZ8ZYp1hFLEQCDgFr/UqrqQuoWzfC197II1Pzm4y8Kvy5bD3Cwpwp2oFJDemWA3rCAEJA/Ah9Hm1exUq+cRu2h7vpyANb9G4BqAL+uJFK1HwCy8zL3jMTGzW07Cmr8wKIIBaNFPz/YgICpcMsrDVLELUQHWQn/W01EDFUiTnANamEp1WAv9Wa3OI5BcYDd0R08A6ABroT9r6Z1ablh5ja5h73jMXHko+bRVcgOKXSz4swAth7rIUTA1AHnRik531oNz9eyr300hZE9jW8oBdYY1789CQsGR9xjYw4HSozi+cVB1N6DY6kG6DqIGjr7PQGmscCenzJZsHWHNLwXsSgBo2Z/FhbPQmlKHXegMa15UcA04dYsLrOPbrnX3WQ9bDkjdArSe252Ay6hAT5Fizs8Nd9ZCPSDUBdCyP2uOEi2OvnQbrPkl4lJE47BKl+4ZH0qupVthzYe6wJfl1G1xRmxJkcrhub+3m2HNXymXIu5fc1tTpAyrNQW8XopIVrpnbTnM9/LCnfWgKl5M3ZKW7pnHzloPL8Ka92e9kLq19Swqawga7+VVWAv9WTeWIpKdimIcLfyWXoc1r6ibShGVSpFiIsuw7ldT59StkilShhVTgd/agq3iUCCjS+rW8dI92uX4v3W+s5ZWWvVSRGVK9xhWuxQoP46KqVttUqTl5TXegu+sxrRS5RQZtNNNjF22Wq0YVnPr4VQpotKle+YktN6aYbWmnV2pW+1TpNbkLd6LYZVTkzJ164oUqZy8+3szrPJqgj+LmbrVrnRPXkJjFhhWYzoZaSV7YLK2pXtGxMFow7BiqLjfhtnUrSOnm+BfNr1FhpVOYyOpW8dON6G7bDrLDCudtmC5VOrWMylSTHkZVkw1S9vKp27hf4gg2PqCXnsukX4UhpVeYx4BSQGGFUlINkOvAMNKrzGPgKQAw4okJJuhV4BhpdeYR0BSgGFFEpLN0CvAsNJrzCMgKcCwIgnJZugVYFjpNeYRkBRgWJGEZDP0CjCs9BrzCEgKMKxIQrIZegUYVnqNeQQkBRhWJCHZDL0CDCu9xjwCkgIMK5KQbIZeAYaVXmMeAUkBhhVJSDZDrwDDSq8xj4CkAMOKJCSboVeAYaXXmEdAUoBhRRKSzdArwLDSa8wjICnAsCIJyWboFWBY6TXmEZAUYFiRhGQz9AowrPQa8whICjCsSEKyGXoFGFZ6jXkEJAUYViQh2Qy9AgwrvcY8ApICvwBH7cTZ9uA+jwAAAABJRU5ErkJggg==","e":1}],"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"stack-icon.ai","cl":"ai","refId":"image_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[306,304,0],"to":[0,4,0],"ti":[0,4.167,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":26,"s":[306,328,0],"to":[0,-4.167,0],"ti":[0,4,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[306,279,0],"to":[0,-4,0],"ti":[0,-4.167,0]},{"t":75,"s":[306,304,0]}],"ix":2,"x":"var $bm_rt;\n$bm_rt = transform.position;"},"a":{"a":0,"k":[85.5,85.5,0],"ix":1},"s":{"a":0,"k":[269.591,269.591,100],"ix":6}},"ao":0,"ip":0,"op":750,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/svg/chanstheme.svg b/assets/svg/chanstheme.svg new file mode 100644 index 000000000..0a94d5f57 --- /dev/null +++ b/assets/svg/chanstheme.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/svg/coin_control/frozen.svg b/assets/svg/coin_control/frozen.svg new file mode 100644 index 000000000..3fe52d57e --- /dev/null +++ b/assets/svg/coin_control/frozen.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/coin_control/gamepad.svg b/assets/svg/coin_control/gamepad.svg new file mode 100644 index 000000000..6fe0b6c30 --- /dev/null +++ b/assets/svg/coin_control/gamepad.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/coin_control/selected.svg b/assets/svg/coin_control/selected.svg new file mode 100644 index 000000000..454ad8af6 --- /dev/null +++ b/assets/svg/coin_control/selected.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/coin_control/unfrozen.svg b/assets/svg/coin_control/unfrozen.svg new file mode 100644 index 000000000..d3d4da221 --- /dev/null +++ b/assets/svg/coin_control/unfrozen.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/forest-theme.svg b/assets/svg/forest-theme.svg index 1472bcbf5..fd73b92a6 100644 --- a/assets/svg/forest-theme.svg +++ b/assets/svg/forest-theme.svg @@ -1,4 +1,15 @@ + + + + + + + + + + + @@ -49,15 +60,4 @@ - - - - - - - - - - - diff --git a/assets/svg/fruit-sorbet-theme.svg b/assets/svg/fruit-sorbet-theme.svg index 9ae18fa40..ca090c713 100644 --- a/assets/svg/fruit-sorbet-theme.svg +++ b/assets/svg/fruit-sorbet-theme.svg @@ -1,33 +1,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -63,4 +34,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/fruitSorbet/persona-easy-1.png b/assets/svg/fruitSorbet/persona-easy-1.png deleted file mode 100644 index f2dd39e2f..000000000 Binary files a/assets/svg/fruitSorbet/persona-easy-1.png and /dev/null differ diff --git a/assets/svg/fruitSorbet/persona-incognito-1.png b/assets/svg/fruitSorbet/persona-incognito-1.png deleted file mode 100644 index 16465fcff..000000000 Binary files a/assets/svg/fruitSorbet/persona-incognito-1.png and /dev/null differ diff --git a/assets/svg/list-ul.svg b/assets/svg/list-ul.svg new file mode 100644 index 000000000..f5cef4eae --- /dev/null +++ b/assets/svg/list-ul.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/ocean-breeze-theme.svg b/assets/svg/ocean-breeze-theme.svg index 0deb96ec8..298a0a372 100644 --- a/assets/svg/ocean-breeze-theme.svg +++ b/assets/svg/ocean-breeze-theme.svg @@ -1,4 +1,13 @@ + + + + + + + + + @@ -16,13 +25,4 @@ - - - - - - - - - diff --git a/assets/svg/oceanBreeze/persona-easy-1.png b/assets/svg/oceanBreeze/persona-easy-1.png deleted file mode 100644 index 20cafb519..000000000 Binary files a/assets/svg/oceanBreeze/persona-easy-1.png and /dev/null differ diff --git a/assets/svg/oceanBreeze/persona-easy-1.svg b/assets/svg/oceanBreeze/persona-easy-1.svg deleted file mode 100644 index f21e19345..000000000 --- a/assets/svg/oceanBreeze/persona-easy-1.svg +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/svg/oceanBreeze/persona-incognito-1.png b/assets/svg/oceanBreeze/persona-incognito-1.png deleted file mode 100644 index 8453cfa76..000000000 Binary files a/assets/svg/oceanBreeze/persona-incognito-1.png and /dev/null differ diff --git a/assets/svg/oceanBreeze/persona-incognito-1.svg b/assets/svg/oceanBreeze/persona-incognito-1.svg deleted file mode 100644 index dcadaf616..000000000 --- a/assets/svg/oceanBreeze/persona-incognito-1.svg +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/svg/oledBlack/persona-easy-1.svg b/assets/svg/oledBlack/persona-easy-1.svg deleted file mode 100644 index 1e15b8dab..000000000 --- a/assets/svg/oledBlack/persona-easy-1.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/svg/oledBlack/persona-incognito-1.svg b/assets/svg/oledBlack/persona-incognito-1.svg deleted file mode 100644 index c31c285b1..000000000 --- a/assets/svg/oledBlack/persona-incognito-1.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/svg/dark/bell-new.svg b/assets/svg/themed/dark/bell-new.svg similarity index 100% rename from assets/svg/dark/bell-new.svg rename to assets/svg/themed/dark/bell-new.svg diff --git a/assets/svg/themed/dark/bitcoin.svg b/assets/svg/themed/dark/bitcoin.svg new file mode 100644 index 000000000..c03761586 --- /dev/null +++ b/assets/svg/themed/dark/bitcoin.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/dark/bitcoincash.svg b/assets/svg/themed/dark/bitcoincash.svg new file mode 100644 index 000000000..27ea7a8fa --- /dev/null +++ b/assets/svg/themed/dark/bitcoincash.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/dark/buy-coins-icon.svg b/assets/svg/themed/dark/buy-coins-icon.svg similarity index 100% rename from assets/svg/dark/buy-coins-icon.svg rename to assets/svg/themed/dark/buy-coins-icon.svg diff --git a/assets/svg/themed/dark/doge.svg b/assets/svg/themed/dark/doge.svg new file mode 100644 index 000000000..f70d0e3c4 --- /dev/null +++ b/assets/svg/themed/dark/doge.svg @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/dark/epic-cash.svg b/assets/svg/themed/dark/epic-cash.svg new file mode 100644 index 000000000..0a5e9cbb4 --- /dev/null +++ b/assets/svg/themed/dark/epic-cash.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/dark/exchange-2.svg b/assets/svg/themed/dark/exchange-2.svg similarity index 100% rename from assets/svg/dark/exchange-2.svg rename to assets/svg/themed/dark/exchange-2.svg diff --git a/assets/svg/themed/dark/firo.svg b/assets/svg/themed/dark/firo.svg new file mode 100644 index 000000000..27c86b5aa --- /dev/null +++ b/assets/svg/themed/dark/firo.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/dark/litecoin.svg b/assets/svg/themed/dark/litecoin.svg new file mode 100644 index 000000000..4c68c9a12 --- /dev/null +++ b/assets/svg/themed/dark/litecoin.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/dark/monero.svg b/assets/svg/themed/dark/monero.svg new file mode 100644 index 000000000..82ecbf3bb --- /dev/null +++ b/assets/svg/themed/dark/monero.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/dark/namecoin.svg b/assets/svg/themed/dark/namecoin.svg new file mode 100644 index 000000000..a842d947f --- /dev/null +++ b/assets/svg/themed/dark/namecoin.svg @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/dark/particl.svg b/assets/svg/themed/dark/particl.svg new file mode 100644 index 000000000..5a4d9b291 --- /dev/null +++ b/assets/svg/themed/dark/particl.svg @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/dark/persona-easy-1.svg b/assets/svg/themed/dark/persona-easy-1.svg similarity index 100% rename from assets/svg/dark/persona-easy-1.svg rename to assets/svg/themed/dark/persona-easy-1.svg diff --git a/assets/svg/dark/persona-incognito-1.svg b/assets/svg/themed/dark/persona-incognito-1.svg similarity index 100% rename from assets/svg/dark/persona-incognito-1.svg rename to assets/svg/themed/dark/persona-incognito-1.svg diff --git a/assets/svg/dark/stack-icon1.svg b/assets/svg/themed/dark/stack-icon1.svg similarity index 100% rename from assets/svg/dark/stack-icon1.svg rename to assets/svg/themed/dark/stack-icon1.svg diff --git a/assets/svg/themed/dark/stack.svg b/assets/svg/themed/dark/stack.svg new file mode 100644 index 000000000..94929475d --- /dev/null +++ b/assets/svg/themed/dark/stack.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/dark/tx-exchange-icon-failed.svg b/assets/svg/themed/dark/tx-exchange-icon-failed.svg similarity index 100% rename from assets/svg/dark/tx-exchange-icon-failed.svg rename to assets/svg/themed/dark/tx-exchange-icon-failed.svg diff --git a/assets/svg/dark/tx-exchange-icon-pending.svg b/assets/svg/themed/dark/tx-exchange-icon-pending.svg similarity index 100% rename from assets/svg/dark/tx-exchange-icon-pending.svg rename to assets/svg/themed/dark/tx-exchange-icon-pending.svg diff --git a/assets/svg/dark/tx-exchange-icon.svg b/assets/svg/themed/dark/tx-exchange-icon.svg similarity index 100% rename from assets/svg/dark/tx-exchange-icon.svg rename to assets/svg/themed/dark/tx-exchange-icon.svg diff --git a/assets/svg/dark/tx-icon-receive-failed.svg b/assets/svg/themed/dark/tx-icon-receive-failed.svg similarity index 100% rename from assets/svg/dark/tx-icon-receive-failed.svg rename to assets/svg/themed/dark/tx-icon-receive-failed.svg diff --git a/assets/svg/dark/tx-icon-receive-pending.svg b/assets/svg/themed/dark/tx-icon-receive-pending.svg similarity index 100% rename from assets/svg/dark/tx-icon-receive-pending.svg rename to assets/svg/themed/dark/tx-icon-receive-pending.svg diff --git a/assets/svg/dark/tx-icon-receive.svg b/assets/svg/themed/dark/tx-icon-receive.svg similarity index 100% rename from assets/svg/dark/tx-icon-receive.svg rename to assets/svg/themed/dark/tx-icon-receive.svg diff --git a/assets/svg/dark/tx-icon-send-failed.svg b/assets/svg/themed/dark/tx-icon-send-failed.svg similarity index 100% rename from assets/svg/dark/tx-icon-send-failed.svg rename to assets/svg/themed/dark/tx-icon-send-failed.svg diff --git a/assets/svg/dark/tx-icon-send-pending.svg b/assets/svg/themed/dark/tx-icon-send-pending.svg similarity index 100% rename from assets/svg/dark/tx-icon-send-pending.svg rename to assets/svg/themed/dark/tx-icon-send-pending.svg diff --git a/assets/svg/dark/tx-icon-send.svg b/assets/svg/themed/dark/tx-icon-send.svg similarity index 100% rename from assets/svg/dark/tx-icon-send.svg rename to assets/svg/themed/dark/tx-icon-send.svg diff --git a/assets/svg/themed/dark/wownero.svg b/assets/svg/themed/dark/wownero.svg new file mode 100644 index 000000000..15f3a317d --- /dev/null +++ b/assets/svg/themed/dark/wownero.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/forest/bell-new.svg b/assets/svg/themed/forest/bell-new.svg similarity index 100% rename from assets/svg/forest/bell-new.svg rename to assets/svg/themed/forest/bell-new.svg diff --git a/assets/svg/forest/bg.svg b/assets/svg/themed/forest/bg.svg similarity index 100% rename from assets/svg/forest/bg.svg rename to assets/svg/themed/forest/bg.svg diff --git a/assets/images/forest/bitcoin.svg b/assets/svg/themed/forest/bitcoin.svg similarity index 100% rename from assets/images/forest/bitcoin.svg rename to assets/svg/themed/forest/bitcoin.svg diff --git a/assets/images/forest/bitcoincash.svg b/assets/svg/themed/forest/bitcoincash.svg similarity index 100% rename from assets/images/forest/bitcoincash.svg rename to assets/svg/themed/forest/bitcoincash.svg diff --git a/assets/svg/forest/buy-coins-icon.svg b/assets/svg/themed/forest/buy-coins-icon.svg similarity index 100% rename from assets/svg/forest/buy-coins-icon.svg rename to assets/svg/themed/forest/buy-coins-icon.svg diff --git a/assets/images/dark/doge.svg b/assets/svg/themed/forest/doge.svg similarity index 65% rename from assets/images/dark/doge.svg rename to assets/svg/themed/forest/doge.svg index 2c1d693b4..1bf72f38f 100644 --- a/assets/images/dark/doge.svg +++ b/assets/svg/themed/forest/doge.svg @@ -1,12 +1,55 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + @@ -14,40 +57,40 @@ - - + + - + - - + + - - + + - + - - + + - + - + @@ -68,39 +111,39 @@ - + - + - + - + - + - + - - - + + + - - + + - + @@ -111,12 +154,12 @@ - + - + @@ -127,129 +170,112 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/forest/epic-cash.svg b/assets/svg/themed/forest/epic-cash.svg similarity index 100% rename from assets/images/forest/epic-cash.svg rename to assets/svg/themed/forest/epic-cash.svg diff --git a/assets/images/forest/ethereum.svg b/assets/svg/themed/forest/ethereum.svg similarity index 100% rename from assets/images/forest/ethereum.svg rename to assets/svg/themed/forest/ethereum.svg diff --git a/assets/svg/forest/exchange-2.svg b/assets/svg/themed/forest/exchange-2.svg similarity index 100% rename from assets/svg/forest/exchange-2.svg rename to assets/svg/themed/forest/exchange-2.svg diff --git a/assets/images/forest/firo.svg b/assets/svg/themed/forest/firo.svg similarity index 100% rename from assets/images/forest/firo.svg rename to assets/svg/themed/forest/firo.svg diff --git a/assets/images/forest/litecoin.svg b/assets/svg/themed/forest/litecoin.svg similarity index 100% rename from assets/images/forest/litecoin.svg rename to assets/svg/themed/forest/litecoin.svg diff --git a/assets/images/forest/monero.svg b/assets/svg/themed/forest/monero.svg similarity index 100% rename from assets/images/forest/monero.svg rename to assets/svg/themed/forest/monero.svg diff --git a/assets/images/forest/namecoin.svg b/assets/svg/themed/forest/namecoin.svg similarity index 100% rename from assets/images/forest/namecoin.svg rename to assets/svg/themed/forest/namecoin.svg diff --git a/assets/images/forest/particl.svg b/assets/svg/themed/forest/particl.svg similarity index 100% rename from assets/images/forest/particl.svg rename to assets/svg/themed/forest/particl.svg diff --git a/assets/svg/forest/persona-easy-1.svg b/assets/svg/themed/forest/persona-easy-1.svg similarity index 100% rename from assets/svg/forest/persona-easy-1.svg rename to assets/svg/themed/forest/persona-easy-1.svg diff --git a/assets/svg/forest/persona-incognito-1.svg b/assets/svg/themed/forest/persona-incognito-1.svg similarity index 100% rename from assets/svg/forest/persona-incognito-1.svg rename to assets/svg/themed/forest/persona-incognito-1.svg diff --git a/assets/svg/forest/stack-icon1.svg b/assets/svg/themed/forest/stack-icon1.svg similarity index 100% rename from assets/svg/forest/stack-icon1.svg rename to assets/svg/themed/forest/stack-icon1.svg diff --git a/assets/images/forest/stack.svg b/assets/svg/themed/forest/stack.svg similarity index 100% rename from assets/images/forest/stack.svg rename to assets/svg/themed/forest/stack.svg diff --git a/assets/svg/forest/tx-exchange-icon-failed.svg b/assets/svg/themed/forest/tx-exchange-icon-failed.svg similarity index 100% rename from assets/svg/forest/tx-exchange-icon-failed.svg rename to assets/svg/themed/forest/tx-exchange-icon-failed.svg diff --git a/assets/svg/forest/tx-exchange-icon-pending.svg b/assets/svg/themed/forest/tx-exchange-icon-pending.svg similarity index 100% rename from assets/svg/forest/tx-exchange-icon-pending.svg rename to assets/svg/themed/forest/tx-exchange-icon-pending.svg diff --git a/assets/svg/forest/tx-exchange-icon.svg b/assets/svg/themed/forest/tx-exchange-icon.svg similarity index 100% rename from assets/svg/forest/tx-exchange-icon.svg rename to assets/svg/themed/forest/tx-exchange-icon.svg diff --git a/assets/svg/forest/tx-icon-receive-failed.svg b/assets/svg/themed/forest/tx-icon-receive-failed.svg similarity index 100% rename from assets/svg/forest/tx-icon-receive-failed.svg rename to assets/svg/themed/forest/tx-icon-receive-failed.svg diff --git a/assets/svg/forest/tx-icon-receive-pending.svg b/assets/svg/themed/forest/tx-icon-receive-pending.svg similarity index 100% rename from assets/svg/forest/tx-icon-receive-pending.svg rename to assets/svg/themed/forest/tx-icon-receive-pending.svg diff --git a/assets/svg/forest/tx-icon-receive.svg b/assets/svg/themed/forest/tx-icon-receive.svg similarity index 100% rename from assets/svg/forest/tx-icon-receive.svg rename to assets/svg/themed/forest/tx-icon-receive.svg diff --git a/assets/svg/forest/tx-icon-send-failed.svg b/assets/svg/themed/forest/tx-icon-send-failed.svg similarity index 100% rename from assets/svg/forest/tx-icon-send-failed.svg rename to assets/svg/themed/forest/tx-icon-send-failed.svg diff --git a/assets/svg/forest/tx-icon-send-pending.svg b/assets/svg/themed/forest/tx-icon-send-pending.svg similarity index 100% rename from assets/svg/forest/tx-icon-send-pending.svg rename to assets/svg/themed/forest/tx-icon-send-pending.svg diff --git a/assets/svg/forest/tx-icon-send.svg b/assets/svg/themed/forest/tx-icon-send.svg similarity index 100% rename from assets/svg/forest/tx-icon-send.svg rename to assets/svg/themed/forest/tx-icon-send.svg diff --git a/assets/images/forest/wownero.svg b/assets/svg/themed/forest/wownero.svg similarity index 100% rename from assets/images/forest/wownero.svg rename to assets/svg/themed/forest/wownero.svg diff --git a/assets/svg/fruitSorbet/bell-new.svg b/assets/svg/themed/fruitSorbet/bell-new.svg similarity index 100% rename from assets/svg/fruitSorbet/bell-new.svg rename to assets/svg/themed/fruitSorbet/bell-new.svg diff --git a/assets/svg/fruitSorbet/bg.svg b/assets/svg/themed/fruitSorbet/bg.svg similarity index 100% rename from assets/svg/fruitSorbet/bg.svg rename to assets/svg/themed/fruitSorbet/bg.svg diff --git a/assets/images/fruitSorbet/bitcoin.svg b/assets/svg/themed/fruitSorbet/bitcoin.svg similarity index 100% rename from assets/images/fruitSorbet/bitcoin.svg rename to assets/svg/themed/fruitSorbet/bitcoin.svg diff --git a/assets/images/fruitSorbet/bitcoincash.svg b/assets/svg/themed/fruitSorbet/bitcoincash.svg similarity index 100% rename from assets/images/fruitSorbet/bitcoincash.svg rename to assets/svg/themed/fruitSorbet/bitcoincash.svg diff --git a/assets/svg/fruitSorbet/buy-coins-icon.svg b/assets/svg/themed/fruitSorbet/buy-coins-icon.svg similarity index 100% rename from assets/svg/fruitSorbet/buy-coins-icon.svg rename to assets/svg/themed/fruitSorbet/buy-coins-icon.svg diff --git a/assets/images/fruitSorbet/doge.svg b/assets/svg/themed/fruitSorbet/doge.svg similarity index 100% rename from assets/images/fruitSorbet/doge.svg rename to assets/svg/themed/fruitSorbet/doge.svg diff --git a/assets/svg/themed/fruitSorbet/epic-cash.svg b/assets/svg/themed/fruitSorbet/epic-cash.svg new file mode 100644 index 000000000..17f7fe0b2 --- /dev/null +++ b/assets/svg/themed/fruitSorbet/epic-cash.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/fruitSorbet/exchange-2.svg b/assets/svg/themed/fruitSorbet/exchange-2.svg similarity index 100% rename from assets/svg/fruitSorbet/exchange-2.svg rename to assets/svg/themed/fruitSorbet/exchange-2.svg diff --git a/assets/images/fruitSorbet/firo.svg b/assets/svg/themed/fruitSorbet/firo.svg similarity index 100% rename from assets/images/fruitSorbet/firo.svg rename to assets/svg/themed/fruitSorbet/firo.svg diff --git a/assets/images/fruitSorbet/litecoin.svg b/assets/svg/themed/fruitSorbet/litecoin.svg similarity index 100% rename from assets/images/fruitSorbet/litecoin.svg rename to assets/svg/themed/fruitSorbet/litecoin.svg diff --git a/assets/svg/themed/fruitSorbet/monero.svg b/assets/svg/themed/fruitSorbet/monero.svg new file mode 100644 index 000000000..5193d0c9d --- /dev/null +++ b/assets/svg/themed/fruitSorbet/monero.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/fruitSorbet/namecoin.svg b/assets/svg/themed/fruitSorbet/namecoin.svg new file mode 100644 index 000000000..1a1a5fc46 --- /dev/null +++ b/assets/svg/themed/fruitSorbet/namecoin.svg @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/fruitSorbet/particl.svg b/assets/svg/themed/fruitSorbet/particl.svg similarity index 100% rename from assets/images/fruitSorbet/particl.svg rename to assets/svg/themed/fruitSorbet/particl.svg diff --git a/assets/svg/themed/fruitSorbet/persona-easy-1.svg b/assets/svg/themed/fruitSorbet/persona-easy-1.svg new file mode 100644 index 000000000..041c775fb --- /dev/null +++ b/assets/svg/themed/fruitSorbet/persona-easy-1.svg @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/fruitSorbet/persona-incognito-1.svg b/assets/svg/themed/fruitSorbet/persona-incognito-1.svg new file mode 100644 index 000000000..6602556f9 --- /dev/null +++ b/assets/svg/themed/fruitSorbet/persona-incognito-1.svg @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/fruitSorbet/stack-icon1.svg b/assets/svg/themed/fruitSorbet/stack-icon1.svg similarity index 100% rename from assets/svg/fruitSorbet/stack-icon1.svg rename to assets/svg/themed/fruitSorbet/stack-icon1.svg diff --git a/assets/images/fruitSorbet/stack.svg b/assets/svg/themed/fruitSorbet/stack.svg similarity index 100% rename from assets/images/fruitSorbet/stack.svg rename to assets/svg/themed/fruitSorbet/stack.svg diff --git a/assets/svg/fruitSorbet/tx-exchange-icon-failed.svg b/assets/svg/themed/fruitSorbet/tx-exchange-icon-failed.svg similarity index 100% rename from assets/svg/fruitSorbet/tx-exchange-icon-failed.svg rename to assets/svg/themed/fruitSorbet/tx-exchange-icon-failed.svg diff --git a/assets/svg/fruitSorbet/tx-exchange-icon-pending.svg b/assets/svg/themed/fruitSorbet/tx-exchange-icon-pending.svg similarity index 100% rename from assets/svg/fruitSorbet/tx-exchange-icon-pending.svg rename to assets/svg/themed/fruitSorbet/tx-exchange-icon-pending.svg diff --git a/assets/svg/fruitSorbet/tx-exchange-icon.svg b/assets/svg/themed/fruitSorbet/tx-exchange-icon.svg similarity index 100% rename from assets/svg/fruitSorbet/tx-exchange-icon.svg rename to assets/svg/themed/fruitSorbet/tx-exchange-icon.svg diff --git a/assets/svg/fruitSorbet/tx-icon-receive-failed.svg b/assets/svg/themed/fruitSorbet/tx-icon-receive-failed.svg similarity index 100% rename from assets/svg/fruitSorbet/tx-icon-receive-failed.svg rename to assets/svg/themed/fruitSorbet/tx-icon-receive-failed.svg diff --git a/assets/svg/fruitSorbet/tx-icon-receive-pending.svg b/assets/svg/themed/fruitSorbet/tx-icon-receive-pending.svg similarity index 100% rename from assets/svg/fruitSorbet/tx-icon-receive-pending.svg rename to assets/svg/themed/fruitSorbet/tx-icon-receive-pending.svg diff --git a/assets/svg/fruitSorbet/tx-icon-receive.svg b/assets/svg/themed/fruitSorbet/tx-icon-receive.svg similarity index 100% rename from assets/svg/fruitSorbet/tx-icon-receive.svg rename to assets/svg/themed/fruitSorbet/tx-icon-receive.svg diff --git a/assets/svg/fruitSorbet/tx-icon-send-failed.svg b/assets/svg/themed/fruitSorbet/tx-icon-send-failed.svg similarity index 100% rename from assets/svg/fruitSorbet/tx-icon-send-failed.svg rename to assets/svg/themed/fruitSorbet/tx-icon-send-failed.svg diff --git a/assets/svg/fruitSorbet/tx-icon-send-pending.svg b/assets/svg/themed/fruitSorbet/tx-icon-send-pending.svg similarity index 100% rename from assets/svg/fruitSorbet/tx-icon-send-pending.svg rename to assets/svg/themed/fruitSorbet/tx-icon-send-pending.svg diff --git a/assets/svg/fruitSorbet/tx-icon-send.svg b/assets/svg/themed/fruitSorbet/tx-icon-send.svg similarity index 100% rename from assets/svg/fruitSorbet/tx-icon-send.svg rename to assets/svg/themed/fruitSorbet/tx-icon-send.svg diff --git a/assets/images/fruitSorbet/wownero.svg b/assets/svg/themed/fruitSorbet/wownero.svg similarity index 100% rename from assets/images/fruitSorbet/wownero.svg rename to assets/svg/themed/fruitSorbet/wownero.svg diff --git a/assets/svg/light/bell-new.svg b/assets/svg/themed/light/bell-new.svg similarity index 100% rename from assets/svg/light/bell-new.svg rename to assets/svg/themed/light/bell-new.svg diff --git a/assets/svg/themed/light/bitcoin.svg b/assets/svg/themed/light/bitcoin.svg new file mode 100644 index 000000000..c03761586 --- /dev/null +++ b/assets/svg/themed/light/bitcoin.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/light/bitcoincash.svg b/assets/svg/themed/light/bitcoincash.svg new file mode 100644 index 000000000..27ea7a8fa --- /dev/null +++ b/assets/svg/themed/light/bitcoincash.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/light/buy-coins-icon.svg b/assets/svg/themed/light/buy-coins-icon.svg similarity index 100% rename from assets/svg/light/buy-coins-icon.svg rename to assets/svg/themed/light/buy-coins-icon.svg diff --git a/assets/svg/themed/light/doge.svg b/assets/svg/themed/light/doge.svg new file mode 100644 index 000000000..f70d0e3c4 --- /dev/null +++ b/assets/svg/themed/light/doge.svg @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/light/epic-cash.svg b/assets/svg/themed/light/epic-cash.svg new file mode 100644 index 000000000..0a5e9cbb4 --- /dev/null +++ b/assets/svg/themed/light/epic-cash.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/light/exchange-2.svg b/assets/svg/themed/light/exchange-2.svg similarity index 100% rename from assets/svg/light/exchange-2.svg rename to assets/svg/themed/light/exchange-2.svg diff --git a/assets/svg/themed/light/firo.svg b/assets/svg/themed/light/firo.svg new file mode 100644 index 000000000..27c86b5aa --- /dev/null +++ b/assets/svg/themed/light/firo.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/light/litecoin.svg b/assets/svg/themed/light/litecoin.svg new file mode 100644 index 000000000..4c68c9a12 --- /dev/null +++ b/assets/svg/themed/light/litecoin.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/light/monero.svg b/assets/svg/themed/light/monero.svg new file mode 100644 index 000000000..82ecbf3bb --- /dev/null +++ b/assets/svg/themed/light/monero.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/light/namecoin.svg b/assets/svg/themed/light/namecoin.svg new file mode 100644 index 000000000..a842d947f --- /dev/null +++ b/assets/svg/themed/light/namecoin.svg @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/light/particl.svg b/assets/svg/themed/light/particl.svg new file mode 100644 index 000000000..5a4d9b291 --- /dev/null +++ b/assets/svg/themed/light/particl.svg @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/fruitSorbet/persona-easy-1.svg b/assets/svg/themed/light/persona-easy-1.svg similarity index 100% rename from assets/svg/fruitSorbet/persona-easy-1.svg rename to assets/svg/themed/light/persona-easy-1.svg diff --git a/assets/svg/fruitSorbet/persona-incognito-1.svg b/assets/svg/themed/light/persona-incognito-1.svg similarity index 100% rename from assets/svg/fruitSorbet/persona-incognito-1.svg rename to assets/svg/themed/light/persona-incognito-1.svg diff --git a/assets/svg/light/stack-icon1.svg b/assets/svg/themed/light/stack-icon1.svg similarity index 100% rename from assets/svg/light/stack-icon1.svg rename to assets/svg/themed/light/stack-icon1.svg diff --git a/assets/svg/themed/light/stack.svg b/assets/svg/themed/light/stack.svg new file mode 100644 index 000000000..94929475d --- /dev/null +++ b/assets/svg/themed/light/stack.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/light/tx-exchange-icon-failed.svg b/assets/svg/themed/light/tx-exchange-icon-failed.svg similarity index 100% rename from assets/svg/light/tx-exchange-icon-failed.svg rename to assets/svg/themed/light/tx-exchange-icon-failed.svg diff --git a/assets/svg/light/tx-exchange-icon-pending.svg b/assets/svg/themed/light/tx-exchange-icon-pending.svg similarity index 100% rename from assets/svg/light/tx-exchange-icon-pending.svg rename to assets/svg/themed/light/tx-exchange-icon-pending.svg diff --git a/assets/svg/light/tx-exchange-icon.svg b/assets/svg/themed/light/tx-exchange-icon.svg similarity index 100% rename from assets/svg/light/tx-exchange-icon.svg rename to assets/svg/themed/light/tx-exchange-icon.svg diff --git a/assets/svg/light/tx-icon-receive-failed.svg b/assets/svg/themed/light/tx-icon-receive-failed.svg similarity index 100% rename from assets/svg/light/tx-icon-receive-failed.svg rename to assets/svg/themed/light/tx-icon-receive-failed.svg diff --git a/assets/svg/light/tx-icon-receive-pending.svg b/assets/svg/themed/light/tx-icon-receive-pending.svg similarity index 100% rename from assets/svg/light/tx-icon-receive-pending.svg rename to assets/svg/themed/light/tx-icon-receive-pending.svg diff --git a/assets/svg/light/tx-icon-receive.svg b/assets/svg/themed/light/tx-icon-receive.svg similarity index 100% rename from assets/svg/light/tx-icon-receive.svg rename to assets/svg/themed/light/tx-icon-receive.svg diff --git a/assets/svg/light/tx-icon-send-failed.svg b/assets/svg/themed/light/tx-icon-send-failed.svg similarity index 100% rename from assets/svg/light/tx-icon-send-failed.svg rename to assets/svg/themed/light/tx-icon-send-failed.svg diff --git a/assets/svg/light/tx-icon-send-pending.svg b/assets/svg/themed/light/tx-icon-send-pending.svg similarity index 100% rename from assets/svg/light/tx-icon-send-pending.svg rename to assets/svg/themed/light/tx-icon-send-pending.svg diff --git a/assets/svg/light/tx-icon-send.svg b/assets/svg/themed/light/tx-icon-send.svg similarity index 100% rename from assets/svg/light/tx-icon-send.svg rename to assets/svg/themed/light/tx-icon-send.svg diff --git a/assets/svg/themed/light/wownero.svg b/assets/svg/themed/light/wownero.svg new file mode 100644 index 000000000..15f3a317d --- /dev/null +++ b/assets/svg/themed/light/wownero.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/oceanBreeze/bell-new.svg b/assets/svg/themed/oceanBreeze/bell-new.svg similarity index 100% rename from assets/svg/oceanBreeze/bell-new.svg rename to assets/svg/themed/oceanBreeze/bell-new.svg diff --git a/assets/svg/oceanBreeze/bg.svg b/assets/svg/themed/oceanBreeze/bg.svg similarity index 100% rename from assets/svg/oceanBreeze/bg.svg rename to assets/svg/themed/oceanBreeze/bg.svg diff --git a/assets/svg/themed/oceanBreeze/bitcoin.svg b/assets/svg/themed/oceanBreeze/bitcoin.svg new file mode 100644 index 000000000..c03761586 --- /dev/null +++ b/assets/svg/themed/oceanBreeze/bitcoin.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oceanBreeze/bitcoincash.svg b/assets/svg/themed/oceanBreeze/bitcoincash.svg new file mode 100644 index 000000000..27ea7a8fa --- /dev/null +++ b/assets/svg/themed/oceanBreeze/bitcoincash.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/oceanBreeze/buy-coins-icon.svg b/assets/svg/themed/oceanBreeze/buy-coins-icon.svg similarity index 100% rename from assets/svg/oceanBreeze/buy-coins-icon.svg rename to assets/svg/themed/oceanBreeze/buy-coins-icon.svg diff --git a/assets/svg/themed/oceanBreeze/doge.svg b/assets/svg/themed/oceanBreeze/doge.svg new file mode 100644 index 000000000..f70d0e3c4 --- /dev/null +++ b/assets/svg/themed/oceanBreeze/doge.svg @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oceanBreeze/epic-cash.svg b/assets/svg/themed/oceanBreeze/epic-cash.svg new file mode 100644 index 000000000..0a5e9cbb4 --- /dev/null +++ b/assets/svg/themed/oceanBreeze/epic-cash.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/oceanBreeze/exchange-2.svg b/assets/svg/themed/oceanBreeze/exchange-2.svg similarity index 100% rename from assets/svg/oceanBreeze/exchange-2.svg rename to assets/svg/themed/oceanBreeze/exchange-2.svg diff --git a/assets/svg/themed/oceanBreeze/firo.svg b/assets/svg/themed/oceanBreeze/firo.svg new file mode 100644 index 000000000..27c86b5aa --- /dev/null +++ b/assets/svg/themed/oceanBreeze/firo.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oceanBreeze/litecoin.svg b/assets/svg/themed/oceanBreeze/litecoin.svg new file mode 100644 index 000000000..4c68c9a12 --- /dev/null +++ b/assets/svg/themed/oceanBreeze/litecoin.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oceanBreeze/monero.svg b/assets/svg/themed/oceanBreeze/monero.svg new file mode 100644 index 000000000..82ecbf3bb --- /dev/null +++ b/assets/svg/themed/oceanBreeze/monero.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oceanBreeze/namecoin.svg b/assets/svg/themed/oceanBreeze/namecoin.svg new file mode 100644 index 000000000..a842d947f --- /dev/null +++ b/assets/svg/themed/oceanBreeze/namecoin.svg @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oceanBreeze/particl.svg b/assets/svg/themed/oceanBreeze/particl.svg new file mode 100644 index 000000000..5a4d9b291 --- /dev/null +++ b/assets/svg/themed/oceanBreeze/particl.svg @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oceanBreeze/persona-easy-1.svg b/assets/svg/themed/oceanBreeze/persona-easy-1.svg new file mode 100644 index 000000000..760b6a852 --- /dev/null +++ b/assets/svg/themed/oceanBreeze/persona-easy-1.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oceanBreeze/persona-incognito-1.svg b/assets/svg/themed/oceanBreeze/persona-incognito-1.svg new file mode 100644 index 000000000..d1160b221 --- /dev/null +++ b/assets/svg/themed/oceanBreeze/persona-incognito-1.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/oceanBreeze/stack-icon1.svg b/assets/svg/themed/oceanBreeze/stack-icon1.svg similarity index 100% rename from assets/svg/oceanBreeze/stack-icon1.svg rename to assets/svg/themed/oceanBreeze/stack-icon1.svg diff --git a/assets/images/oceanBreeze/stack.svg b/assets/svg/themed/oceanBreeze/stack.svg similarity index 100% rename from assets/images/oceanBreeze/stack.svg rename to assets/svg/themed/oceanBreeze/stack.svg diff --git a/assets/svg/oceanBreeze/tx-exchange-icon-failed.svg b/assets/svg/themed/oceanBreeze/tx-exchange-icon-failed.svg similarity index 100% rename from assets/svg/oceanBreeze/tx-exchange-icon-failed.svg rename to assets/svg/themed/oceanBreeze/tx-exchange-icon-failed.svg diff --git a/assets/svg/oceanBreeze/tx-exchange-icon-pending.svg b/assets/svg/themed/oceanBreeze/tx-exchange-icon-pending.svg similarity index 100% rename from assets/svg/oceanBreeze/tx-exchange-icon-pending.svg rename to assets/svg/themed/oceanBreeze/tx-exchange-icon-pending.svg diff --git a/assets/svg/oceanBreeze/tx-exchange-icon.svg b/assets/svg/themed/oceanBreeze/tx-exchange-icon.svg similarity index 100% rename from assets/svg/oceanBreeze/tx-exchange-icon.svg rename to assets/svg/themed/oceanBreeze/tx-exchange-icon.svg diff --git a/assets/svg/oceanBreeze/tx-icon-receive-failed.svg b/assets/svg/themed/oceanBreeze/tx-icon-receive-failed.svg similarity index 100% rename from assets/svg/oceanBreeze/tx-icon-receive-failed.svg rename to assets/svg/themed/oceanBreeze/tx-icon-receive-failed.svg diff --git a/assets/svg/oceanBreeze/tx-icon-receive-pending.svg b/assets/svg/themed/oceanBreeze/tx-icon-receive-pending.svg similarity index 100% rename from assets/svg/oceanBreeze/tx-icon-receive-pending.svg rename to assets/svg/themed/oceanBreeze/tx-icon-receive-pending.svg diff --git a/assets/svg/oceanBreeze/tx-icon-receive.svg b/assets/svg/themed/oceanBreeze/tx-icon-receive.svg similarity index 100% rename from assets/svg/oceanBreeze/tx-icon-receive.svg rename to assets/svg/themed/oceanBreeze/tx-icon-receive.svg diff --git a/assets/svg/oceanBreeze/tx-icon-send-failed.svg b/assets/svg/themed/oceanBreeze/tx-icon-send-failed.svg similarity index 100% rename from assets/svg/oceanBreeze/tx-icon-send-failed.svg rename to assets/svg/themed/oceanBreeze/tx-icon-send-failed.svg diff --git a/assets/svg/oceanBreeze/tx-icon-send-pending.svg b/assets/svg/themed/oceanBreeze/tx-icon-send-pending.svg similarity index 100% rename from assets/svg/oceanBreeze/tx-icon-send-pending.svg rename to assets/svg/themed/oceanBreeze/tx-icon-send-pending.svg diff --git a/assets/svg/oceanBreeze/tx-icon-send.svg b/assets/svg/themed/oceanBreeze/tx-icon-send.svg similarity index 100% rename from assets/svg/oceanBreeze/tx-icon-send.svg rename to assets/svg/themed/oceanBreeze/tx-icon-send.svg diff --git a/assets/svg/themed/oceanBreeze/wownero.svg b/assets/svg/themed/oceanBreeze/wownero.svg new file mode 100644 index 000000000..15f3a317d --- /dev/null +++ b/assets/svg/themed/oceanBreeze/wownero.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/oledBlack/bell-new.svg b/assets/svg/themed/oledBlack/bell-new.svg similarity index 100% rename from assets/svg/oledBlack/bell-new.svg rename to assets/svg/themed/oledBlack/bell-new.svg diff --git a/assets/svg/themed/oledBlack/bitcoin.svg b/assets/svg/themed/oledBlack/bitcoin.svg new file mode 100644 index 000000000..c03761586 --- /dev/null +++ b/assets/svg/themed/oledBlack/bitcoin.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oledBlack/bitcoincash.svg b/assets/svg/themed/oledBlack/bitcoincash.svg new file mode 100644 index 000000000..27ea7a8fa --- /dev/null +++ b/assets/svg/themed/oledBlack/bitcoincash.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/oledBlack/buy-coins-icon.svg b/assets/svg/themed/oledBlack/buy-coins-icon.svg similarity index 100% rename from assets/svg/oledBlack/buy-coins-icon.svg rename to assets/svg/themed/oledBlack/buy-coins-icon.svg diff --git a/assets/svg/themed/oledBlack/doge.svg b/assets/svg/themed/oledBlack/doge.svg new file mode 100644 index 000000000..f70d0e3c4 --- /dev/null +++ b/assets/svg/themed/oledBlack/doge.svg @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oledBlack/epic-cash.svg b/assets/svg/themed/oledBlack/epic-cash.svg new file mode 100644 index 000000000..0a5e9cbb4 --- /dev/null +++ b/assets/svg/themed/oledBlack/epic-cash.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/oledBlack/exchange-2.svg b/assets/svg/themed/oledBlack/exchange-2.svg similarity index 100% rename from assets/svg/oledBlack/exchange-2.svg rename to assets/svg/themed/oledBlack/exchange-2.svg diff --git a/assets/svg/themed/oledBlack/firo.svg b/assets/svg/themed/oledBlack/firo.svg new file mode 100644 index 000000000..27c86b5aa --- /dev/null +++ b/assets/svg/themed/oledBlack/firo.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oledBlack/litecoin.svg b/assets/svg/themed/oledBlack/litecoin.svg new file mode 100644 index 000000000..4c68c9a12 --- /dev/null +++ b/assets/svg/themed/oledBlack/litecoin.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oledBlack/monero.svg b/assets/svg/themed/oledBlack/monero.svg new file mode 100644 index 000000000..82ecbf3bb --- /dev/null +++ b/assets/svg/themed/oledBlack/monero.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oledBlack/namecoin.svg b/assets/svg/themed/oledBlack/namecoin.svg new file mode 100644 index 000000000..a842d947f --- /dev/null +++ b/assets/svg/themed/oledBlack/namecoin.svg @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oledBlack/particl.svg b/assets/svg/themed/oledBlack/particl.svg new file mode 100644 index 000000000..5a4d9b291 --- /dev/null +++ b/assets/svg/themed/oledBlack/particl.svg @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/light/persona-easy-1.svg b/assets/svg/themed/oledBlack/persona-easy-1.svg similarity index 100% rename from assets/svg/light/persona-easy-1.svg rename to assets/svg/themed/oledBlack/persona-easy-1.svg diff --git a/assets/svg/light/persona-incognito-1.svg b/assets/svg/themed/oledBlack/persona-incognito-1.svg similarity index 100% rename from assets/svg/light/persona-incognito-1.svg rename to assets/svg/themed/oledBlack/persona-incognito-1.svg diff --git a/assets/svg/oledBlack/stack-icon1.svg b/assets/svg/themed/oledBlack/stack-icon1.svg similarity index 100% rename from assets/svg/oledBlack/stack-icon1.svg rename to assets/svg/themed/oledBlack/stack-icon1.svg diff --git a/assets/svg/themed/oledBlack/stack.svg b/assets/svg/themed/oledBlack/stack.svg new file mode 100644 index 000000000..94929475d --- /dev/null +++ b/assets/svg/themed/oledBlack/stack.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/oledBlack/tx-exchange-icon-failed.svg b/assets/svg/themed/oledBlack/tx-exchange-icon-failed.svg similarity index 100% rename from assets/svg/oledBlack/tx-exchange-icon-failed.svg rename to assets/svg/themed/oledBlack/tx-exchange-icon-failed.svg diff --git a/assets/svg/oledBlack/tx-exchange-icon-pending.svg b/assets/svg/themed/oledBlack/tx-exchange-icon-pending.svg similarity index 100% rename from assets/svg/oledBlack/tx-exchange-icon-pending.svg rename to assets/svg/themed/oledBlack/tx-exchange-icon-pending.svg diff --git a/assets/svg/oledBlack/tx-exchange-icon.svg b/assets/svg/themed/oledBlack/tx-exchange-icon.svg similarity index 100% rename from assets/svg/oledBlack/tx-exchange-icon.svg rename to assets/svg/themed/oledBlack/tx-exchange-icon.svg diff --git a/assets/svg/oledBlack/tx-icon-receive-failed.svg b/assets/svg/themed/oledBlack/tx-icon-receive-failed.svg similarity index 100% rename from assets/svg/oledBlack/tx-icon-receive-failed.svg rename to assets/svg/themed/oledBlack/tx-icon-receive-failed.svg diff --git a/assets/svg/oledBlack/tx-icon-receive-pending.svg b/assets/svg/themed/oledBlack/tx-icon-receive-pending.svg similarity index 100% rename from assets/svg/oledBlack/tx-icon-receive-pending.svg rename to assets/svg/themed/oledBlack/tx-icon-receive-pending.svg diff --git a/assets/svg/oledBlack/tx-icon-receive.svg b/assets/svg/themed/oledBlack/tx-icon-receive.svg similarity index 100% rename from assets/svg/oledBlack/tx-icon-receive.svg rename to assets/svg/themed/oledBlack/tx-icon-receive.svg diff --git a/assets/svg/oledBlack/tx-icon-send-failed.svg b/assets/svg/themed/oledBlack/tx-icon-send-failed.svg similarity index 100% rename from assets/svg/oledBlack/tx-icon-send-failed.svg rename to assets/svg/themed/oledBlack/tx-icon-send-failed.svg diff --git a/assets/svg/oledBlack/tx-icon-send-pending.svg b/assets/svg/themed/oledBlack/tx-icon-send-pending.svg similarity index 100% rename from assets/svg/oledBlack/tx-icon-send-pending.svg rename to assets/svg/themed/oledBlack/tx-icon-send-pending.svg diff --git a/assets/svg/oledBlack/tx-icon-send.svg b/assets/svg/themed/oledBlack/tx-icon-send.svg similarity index 100% rename from assets/svg/oledBlack/tx-icon-send.svg rename to assets/svg/themed/oledBlack/tx-icon-send.svg diff --git a/assets/svg/themed/oledBlack/wownero.svg b/assets/svg/themed/oledBlack/wownero.svg new file mode 100644 index 000000000..15f3a317d --- /dev/null +++ b/assets/svg/themed/oledBlack/wownero.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/unclaimed.svg b/assets/svg/unclaimed.svg new file mode 100644 index 000000000..a6ff2213d --- /dev/null +++ b/assets/svg/unclaimed.svg @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crypto_plugins/flutter_libepiccash b/crypto_plugins/flutter_libepiccash index f017e01b5..ee7e8c6c2 160000 --- a/crypto_plugins/flutter_libepiccash +++ b/crypto_plugins/flutter_libepiccash @@ -1 +1 @@ -Subproject commit f017e01b574a77ad76b8b9c1837b333b777d7e92 +Subproject commit ee7e8c6c2e02a6b11af0a3976b992a34cc4082d9 diff --git a/crypto_plugins/flutter_liblelantus b/crypto_plugins/flutter_liblelantus index 6864d7c0d..b3fac32f5 160000 --- a/crypto_plugins/flutter_liblelantus +++ b/crypto_plugins/flutter_liblelantus @@ -1 +1 @@ -Subproject commit 6864d7c0d4fa68c371e3f0c067afd50b0d59cc9b +Subproject commit b3fac32f57d7ef97da8641463d1a852f41660f9b diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 4ef19051a..dd711a0b1 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + ITSAppUsesNonExemptEncryption + CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion @@ -29,7 +31,7 @@ LSSupportsOpeningDocumentsInPlace NSCameraUsageDescription - App requires access to the Camera to scan QR codes from other people's installed app in order to coordinate sending Firo. + App requires access to the Camera to scan QR codes from other people's installed app in order to coordinate sending. NSFaceIDUsageDescription This app requires Face ID permissions so that the user can securely lock their wallet if their device uses Face ID. It will be useful feature for all users, and especially those prone to forget their login pin on iPhones where Touch ID is not available. NSPhotoLibraryUsageDescription diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index c9577f3e3..57efc4c87 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -1,10 +1,15 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter_native_splash/cli_commands.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/exceptions/main_db/main_db_exception.dart'; -import 'package:stackwallet/exceptions/sw_exception.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:tuple/tuple.dart'; +part 'queries/queries.dart'; + class MainDB { MainDB._(); static MainDB? _instance; @@ -162,6 +167,43 @@ class MainDB { await isar.utxos.putAll(utxos); }); + Future updateUTXOs(String walletId, List utxos) async { + await isar.writeTxn(() async { + final set = utxos.toSet(); + for (final utxo in utxos) { + // check if utxo exists in db and update accordingly + final storedUtxo = await isar.utxos + .where() + .txidWalletIdVoutEqualTo(utxo.txid, utxo.walletId, utxo.vout) + .findFirst(); + + if (storedUtxo != null) { + // update + set.remove(utxo); + set.add( + storedUtxo.copyWith( + value: utxo.value, + address: utxo.address, + blockTime: utxo.blockTime, + blockHeight: utxo.blockHeight, + blockHash: utxo.blockHash, + ), + ); + } + } + + await isar.utxos.where().walletIdEqualTo(walletId).deleteAll(); + await isar.utxos.putAll(set.toList()); + }); + } + + Stream watchUTXO({ + required Id id, + bool fireImmediately = false, + }) { + return isar.utxos.watchObject(id, fireImmediately: fireImmediately); + } + // transaction notes QueryBuilder getTransactionNotes(String walletId) => @@ -237,10 +279,6 @@ class MainDB { Future updateAddressLabel(AddressLabel addressLabel) async { try { return await isar.writeTxn(() async { - final deleted = await isar.addresses.delete(addressLabel.id); - if (!deleted) { - throw SWException("Failed to delete $addressLabel before updating"); - } return await isar.addressLabels.put(addressLabel); }); } catch (e) { diff --git a/lib/db/queries/queries.dart b/lib/db/queries/queries.dart new file mode 100644 index 000000000..2995de06d --- /dev/null +++ b/lib/db/queries/queries.dart @@ -0,0 +1,182 @@ +part of 'package:stackwallet/db/main_db.dart'; + +enum CCFilter { + all, + available, + frozen; + + @override + String toString() { + if (this == all) { + return "Show $name outputs"; + } + + return "${name.capitalize()} outputs"; + } +} + +enum CCSortDescriptor { + age, + address, + value; + + @override + String toString() { + return name.capitalize(); + } +} + +extension MainDBQueries on MainDB { + List queryUTXOsSync({ + required String walletId, + required CCFilter filter, + required CCSortDescriptor sort, + required String searchTerm, + required Coin coin, + }) { + var preSort = getUTXOs(walletId).filter().group((q) { + final qq = q.group( + (q) => q.usedIsNull().or().usedEqualTo(false), + ); + switch (filter) { + case CCFilter.frozen: + return qq.and().isBlockedEqualTo(true); + case CCFilter.available: + return qq.and().isBlockedEqualTo(false); + case CCFilter.all: + return qq; + } + }); + + if (searchTerm.isNotEmpty) { + preSort = preSort.and().group( + (q) { + var qq = q.addressContains(searchTerm, caseSensitive: false); + + qq = qq.or().nameContains(searchTerm, caseSensitive: false); + qq = qq.or().group( + (q) => q + .isBlockedEqualTo(true) + .and() + .blockedReasonContains(searchTerm, caseSensitive: false), + ); + + qq = qq.or().txidContains(searchTerm, caseSensitive: false); + qq = qq.or().blockHashContains(searchTerm, caseSensitive: false); + + final maybeDecimal = Decimal.tryParse(searchTerm); + if (maybeDecimal != null) { + qq = qq.or().valueEqualTo( + Format.decimalAmountToSatoshis( + maybeDecimal, + coin, + ), + ); + } + + final maybeInt = int.tryParse(searchTerm); + if (maybeInt != null) { + qq = qq.or().valueEqualTo(maybeInt); + } + + return qq; + }, + ); + } + + final List ids; + switch (sort) { + case CCSortDescriptor.age: + ids = preSort.sortByBlockHeight().idProperty().findAllSync(); + break; + case CCSortDescriptor.address: + ids = preSort.sortByAddress().idProperty().findAllSync(); + break; + case CCSortDescriptor.value: + ids = preSort.sortByValueDesc().idProperty().findAllSync(); + break; + } + return ids; + } + + Map> queryUTXOsGroupedByAddressSync({ + required String walletId, + required CCFilter filter, + required CCSortDescriptor sort, + required String searchTerm, + required Coin coin, + }) { + var preSort = getUTXOs(walletId).filter().group((q) { + final qq = q.group( + (q) => q.usedIsNull().or().usedEqualTo(false), + ); + switch (filter) { + case CCFilter.frozen: + return qq.and().isBlockedEqualTo(true); + case CCFilter.available: + return qq.and().isBlockedEqualTo(false); + case CCFilter.all: + return qq; + } + }); + + if (searchTerm.isNotEmpty) { + preSort = preSort.and().group( + (q) { + var qq = q.addressContains(searchTerm, caseSensitive: false); + + qq = qq.or().nameContains(searchTerm, caseSensitive: false); + qq = qq.or().group( + (q) => q + .isBlockedEqualTo(true) + .and() + .blockedReasonContains(searchTerm, caseSensitive: false), + ); + + qq = qq.or().txidContains(searchTerm, caseSensitive: false); + qq = qq.or().blockHashContains(searchTerm, caseSensitive: false); + + final maybeDecimal = Decimal.tryParse(searchTerm); + if (maybeDecimal != null) { + qq = qq.or().valueEqualTo( + Format.decimalAmountToSatoshis( + maybeDecimal, + coin, + ), + ); + } + + final maybeInt = int.tryParse(searchTerm); + if (maybeInt != null) { + qq = qq.or().valueEqualTo(maybeInt); + } + + return qq; + }, + ); + } + + final List utxos; + switch (sort) { + case CCSortDescriptor.age: + utxos = preSort.sortByBlockHeight().findAllSync(); + break; + case CCSortDescriptor.address: + utxos = preSort.sortByAddress().findAllSync(); + break; + case CCSortDescriptor.value: + utxos = preSort.sortByValueDesc().findAllSync(); + break; + } + + final Map> results = {}; + for (final utxo in utxos) { + if (results[utxo.address!] == null) { + results[utxo.address!] = []; + } + results[utxo.address!]!.add(utxo.id); + } + + return results; + } +} diff --git a/lib/exceptions/exchange/exchange_exception.dart b/lib/exceptions/exchange/exchange_exception.dart index af7aa8f65..c68599321 100644 --- a/lib/exceptions/exchange/exchange_exception.dart +++ b/lib/exceptions/exchange/exchange_exception.dart @@ -1,6 +1,6 @@ import 'package:stackwallet/exceptions/sw_exception.dart'; -enum ExchangeExceptionType { generic, serializeResponseError } +enum ExchangeExceptionType { generic, serializeResponseError, orderNotFound } class ExchangeException extends SWException { ExchangeExceptionType type; diff --git a/lib/main.dart b/lib/main.dart index f7548a850..24184cf4b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:cw_core/node.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_libmonero/monero/monero.dart'; @@ -56,6 +57,7 @@ import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; +import 'package:stackwallet/utilities/theme/chan_colors.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/dark_colors.dart'; import 'package:stackwallet/utilities/theme/forest_colors.dart'; @@ -91,7 +93,7 @@ void main() async { setWindowMaxSize(Size.infinite); final screenHeight = screen?.frame.height; - if (screenHeight != null) { + if (screenHeight != null && !kDebugMode) { // starting to height be 3/4 screen height or 900, whichever is smaller final height = min(screenHeight * 0.75, 900); setWindowFrame( @@ -352,6 +354,9 @@ class _MaterialAppWithThemeState extends ConsumerState case "forest": colorTheme = ForestColors(); break; + case "chan": + colorTheme = ChanColors(); + break; case "light": default: colorTheme = LightColors(); @@ -524,7 +529,7 @@ class _MaterialAppWithThemeState extends ConsumerState theme: ThemeData( extensions: [colorScheme], highlightColor: colorScheme.highlight, - brightness: Brightness.light, + brightness: colorScheme.brightness, fontFamily: GoogleFonts.inter().fontFamily, unselectedWidgetColor: colorScheme.radioButtonBorderDisabled, // textTheme: GoogleFonts.interTextTheme().copyWith( diff --git a/lib/models/balance.dart b/lib/models/balance.dart index 90ef24570..0589ac90d 100644 --- a/lib/models/balance.dart +++ b/lib/models/balance.dart @@ -19,7 +19,7 @@ class Balance { required this.pendingSpendable, }); - Decimal getTotal({bool includeBlocked = false}) => Format.satoshisToAmount( + Decimal getTotal({bool includeBlocked = true}) => Format.satoshisToAmount( includeBlocked ? total : total - blockedTotal, coin: coin, ); @@ -39,12 +39,7 @@ class Balance { coin: coin, ); - String toJsonIgnoreCoin() => jsonEncode({ - "total": total, - "spendable": spendable, - "blockedTotal": blockedTotal, - "pendingSpendable": pendingSpendable, - }); + String toJsonIgnoreCoin() => jsonEncode(toMap()..remove("coin")); factory Balance.fromJson(String json, Coin coin) { final decoded = jsonDecode(json); @@ -56,4 +51,17 @@ class Balance { pendingSpendable: decoded["pendingSpendable"] as int, ); } + + Map toMap() => { + "coin": coin, + "total": total, + "spendable": spendable, + "blockedTotal": blockedTotal, + "pendingSpendable": pendingSpendable, + }; + + @override + String toString() { + return toMap().toString(); + } } diff --git a/lib/models/epicbox_config_model.dart b/lib/models/epicbox_config_model.dart new file mode 100644 index 000000000..7927fd165 --- /dev/null +++ b/lib/models/epicbox_config_model.dart @@ -0,0 +1,121 @@ +import 'dart:convert'; + +import 'package:hive/hive.dart'; +import 'package:stackwallet/models/epicbox_server_model.dart'; + +part 'type_adaptors/epicbox_config_model.g.dart'; + +// @HiveType(typeId: 72) +class EpicBoxConfigModel { + // @HiveField(1) + final String host; + // @HiveField(2) + final int? port; + // @HiveField(3) + final bool? protocolInsecure; + // @HiveField(4) + final int? addressIndex; + // // @HiveField(5) + // final String? id; + // // @HiveField(6) + // final String? name; + + EpicBoxConfigModel({ + required this.host, + this.port, + this.protocolInsecure, + this.addressIndex, + // this.id, + // this.name, + }); + + EpicBoxConfigModel copyWith({ + int? port, + bool? protocolInsecure, + int? addressIndex, + // String? id, + // String? name, + }) { + return EpicBoxConfigModel( + host: host, + port: this.port ?? 443, + protocolInsecure: this.protocolInsecure ?? false, + addressIndex: this.addressIndex ?? 0, + // id: id ?? this.id, + // name: name ?? this.name, + ); + } + + Map toMap() { + Map map = {}; + map['epicbox_domain'] = host; + map['epicbox_port'] = port; + map['epicbox_protocol_insecure'] = protocolInsecure; + map['epicbox_address_index'] = addressIndex; + // map['id'] = id; + // map['name'] = name; + return map; + } + + Map toJson() { + return { + 'epicbox_domain': host, + 'epicbox_port': port, + 'epicbox_protocol_insecure': protocolInsecure, + 'epicbox_address_index': addressIndex, + // 'id': id, + // 'name': name, + }; + } + + @override + String toString() { + return json.encode(toJson()); + } + + static EpicBoxConfigModel fromString(String epicBoxConfigString) { + dynamic _epicBox = json.decode(epicBoxConfigString); + + // handle old epicbox config formats + final oldDomain = _epicBox["domain"] ?? "empty"; + if (oldDomain != "empty") { + _epicBox['epicbox_domain'] = _epicBox['domain']; + } + final oldPort = _epicBox["port"] ?? "empty"; + if (oldPort != "empty") { + _epicBox['epicbox_port'] = _epicBox['port']; + } + final oldProtocolInsecure = _epicBox["protocol_insecure"] ?? "empty"; + if (oldProtocolInsecure != "empty") { + _epicBox['epicbox_protocol_insecure'] = _epicBox['protocol_insecure']; + } + final oldAddressIndex = _epicBox["address_index"] ?? "empty"; + if (oldAddressIndex != "empty") { + _epicBox['epicbox_address_index'] = _epicBox['address_index']; + } + + _epicBox['epicbox_protocol_insecure'] ??= false; + _epicBox['epicbox_address_index'] ??= 0; + + return EpicBoxConfigModel( + host: _epicBox['epicbox_domain'] as String, + port: _epicBox['epicbox_port'] as int, + protocolInsecure: _epicBox['epicbox_protocol_insecure'] as bool, + addressIndex: _epicBox['epicbox_address_index'] as int, + // name: fields[5] as String, + // id: fields[6] as String, + ); + } + + static EpicBoxConfigModel fromServer(EpicBoxServerModel server, + {bool? protocolInsecure, int? addressIndex}) { + return EpicBoxConfigModel( + host: server.host, + port: server.port ?? 443, + protocolInsecure: protocolInsecure ?? false, + addressIndex: addressIndex ?? 0, + // name: fields[5] as String, + // id: fields[6] as String, + ); + } +} diff --git a/lib/models/epicbox_server_model.dart b/lib/models/epicbox_server_model.dart new file mode 100644 index 000000000..8bb431348 --- /dev/null +++ b/lib/models/epicbox_server_model.dart @@ -0,0 +1,83 @@ +import 'package:hive/hive.dart'; + +part 'type_adaptors/epicbox_server_model.g.dart'; + +// @HiveType(typeId: 71) +class EpicBoxServerModel { + // @HiveField(0) + final String id; + // @HiveField(1) + final String host; + // @HiveField(2) + final int? port; + // @HiveField(3) + final String name; + // @HiveField(4) + final bool? useSSL; + // @HiveField(5) + final bool? enabled; + // @HiveField(6) + final bool? isFailover; + // @HiveField(7) + final bool? isDown; + + EpicBoxServerModel({ + required this.id, + required this.host, + this.port, + required this.name, + this.useSSL, + this.enabled, + this.isFailover, + this.isDown, + }); + + EpicBoxServerModel copyWith({ + String? host, + int? port, + String? name, + bool? useSSL, + bool? enabled, + bool? isFailover, + bool? isDown, + }) { + return EpicBoxServerModel( + id: id, + host: host ?? this.host, + port: port ?? this.port, + name: name ?? this.name, + useSSL: useSSL ?? this.useSSL, + enabled: enabled ?? this.enabled, + isFailover: isFailover ?? this.isFailover, + isDown: isDown ?? this.isDown, + ); + } + + Map toMap() { + Map map = {}; + map['id'] = id; + map['host'] = host; + map['port'] = port; + map['name'] = name; + map['useSSL'] = useSSL; + map['enabled'] = enabled; + map['isFailover'] = isFailover; + map['isDown'] = isDown; + return map; + } + + bool get isDefault => id.startsWith("default_"); + + Map toJson() { + return { + 'id': id, + 'host': host, + 'port': port, + 'name': name, + 'useSSL': useSSL, + 'enabled': enabled, + 'isFailover': isFailover, + 'isDown': isDown, + }; + } +} diff --git a/lib/models/exchange/majestic_bank/mb_order_status.dart b/lib/models/exchange/majestic_bank/mb_order_status.dart index 5496aa5e2..030fe1ddf 100644 --- a/lib/models/exchange/majestic_bank/mb_order_status.dart +++ b/lib/models/exchange/majestic_bank/mb_order_status.dart @@ -24,9 +24,18 @@ class MBOrderStatus extends MBObject { final Decimal received; final Decimal confirmed; + Map toMap() => { + "orderId": orderId, + "status": status, + "fromCurrency": fromCurrency, + "fromAmount": fromAmount, + "receiveCurrency": receiveCurrency, + "receiveAmount": receiveAmount, + "address": address, + "received": received, + "confirmed": confirmed, + }; + @override - String toString() { - // todo: full toString - return status; - } + String toString() => toMap().toString(); } diff --git a/lib/models/isar/models/blockchain_data/utxo.dart b/lib/models/isar/models/blockchain_data/utxo.dart index 2bea3eead..ba618b0c1 100644 --- a/lib/models/isar/models/blockchain_data/utxo.dart +++ b/lib/models/isar/models/blockchain_data/utxo.dart @@ -4,7 +4,7 @@ import 'package:isar/isar.dart'; part 'utxo.g.dart'; -@Collection(accessor: "utxos") +@Collection(accessor: "utxos", inheritance: false) class UTXO { UTXO({ required this.walletId, @@ -18,6 +18,8 @@ class UTXO { required this.blockHash, required this.blockHeight, required this.blockTime, + this.address, + this.used, this.otherData, }); @@ -26,7 +28,10 @@ class UTXO { @Index() late final String walletId; - @Index(unique: true, replace: true, composite: [CompositeIndex("walletId")]) + @Index(unique: true, replace: true, composite: [ + CompositeIndex("walletId"), + CompositeIndex("vout"), + ]) late final String txid; late final int vout; @@ -48,6 +53,10 @@ class UTXO { late final int? blockTime; + late final String? address; + + late final bool? used; + late final String? otherData; int getConfirmations(int currentChainHeight) { @@ -61,6 +70,40 @@ class UTXO { return confirmations >= minimumConfirms; } + UTXO copyWith({ + Id? id, + String? walletId, + String? txid, + int? vout, + int? value, + String? name, + bool? isBlocked, + String? blockedReason, + bool? isCoinbase, + String? blockHash, + int? blockHeight, + int? blockTime, + String? address, + bool? used, + String? otherData, + }) => + UTXO( + walletId: walletId ?? this.walletId, + txid: txid ?? this.txid, + vout: vout ?? this.vout, + value: value ?? this.value, + name: name ?? this.name, + isBlocked: isBlocked ?? this.isBlocked, + blockedReason: blockedReason ?? this.blockedReason, + isCoinbase: isCoinbase ?? this.isCoinbase, + blockHash: blockHash ?? this.blockHash, + blockHeight: blockHeight ?? this.blockHeight, + blockTime: blockTime ?? this.blockTime, + address: address ?? this.address, + used: used ?? this.used, + otherData: otherData ?? this.otherData, + )..id = id ?? this.id; + @override String toString() => "{ " "id: $id, " @@ -75,5 +118,20 @@ class UTXO { "blockHash: $blockHash, " "blockHeight: $blockHeight, " "blockTime: $blockTime, " + "address: $address, " + "used: $used, " + "otherData: $otherData, " "}"; + + @override + bool operator ==(Object other) { + return other is UTXO && + other.walletId == walletId && + other.txid == txid && + other.vout == vout; + } + + @override + @ignore + int get hashCode => Object.hashAll([walletId, txid, vout]); } diff --git a/lib/models/isar/models/blockchain_data/utxo.g.dart b/lib/models/isar/models/blockchain_data/utxo.g.dart index 435746474..b12e9b470 100644 --- a/lib/models/isar/models/blockchain_data/utxo.g.dart +++ b/lib/models/isar/models/blockchain_data/utxo.g.dart @@ -17,63 +17,73 @@ const UTXOSchema = CollectionSchema( name: r'UTXO', id: 5934032492047519621, properties: { - r'blockHash': PropertySchema( + r'address': PropertySchema( id: 0, + name: r'address', + type: IsarType.string, + ), + r'blockHash': PropertySchema( + id: 1, name: r'blockHash', type: IsarType.string, ), r'blockHeight': PropertySchema( - id: 1, + id: 2, name: r'blockHeight', type: IsarType.long, ), r'blockTime': PropertySchema( - id: 2, + id: 3, name: r'blockTime', type: IsarType.long, ), r'blockedReason': PropertySchema( - id: 3, + id: 4, name: r'blockedReason', type: IsarType.string, ), r'isBlocked': PropertySchema( - id: 4, + id: 5, name: r'isBlocked', type: IsarType.bool, ), r'isCoinbase': PropertySchema( - id: 5, + id: 6, name: r'isCoinbase', type: IsarType.bool, ), r'name': PropertySchema( - id: 6, + id: 7, name: r'name', type: IsarType.string, ), r'otherData': PropertySchema( - id: 7, + id: 8, name: r'otherData', type: IsarType.string, ), r'txid': PropertySchema( - id: 8, + id: 9, name: r'txid', type: IsarType.string, ), + r'used': PropertySchema( + id: 10, + name: r'used', + type: IsarType.bool, + ), r'value': PropertySchema( - id: 9, + id: 11, name: r'value', type: IsarType.long, ), r'vout': PropertySchema( - id: 10, + id: 12, name: r'vout', type: IsarType.long, ), r'walletId': PropertySchema( - id: 11, + id: 13, name: r'walletId', type: IsarType.string, ) @@ -97,9 +107,9 @@ const UTXOSchema = CollectionSchema( ) ], ), - r'txid_walletId': IndexSchema( - id: -2771771174176035985, - name: r'txid_walletId', + r'txid_walletId_vout': IndexSchema( + id: -2984264099359759359, + name: r'txid_walletId_vout', unique: true, replace: true, properties: [ @@ -112,6 +122,11 @@ const UTXOSchema = CollectionSchema( name: r'walletId', type: IndexType.hash, caseSensitive: true, + ), + IndexPropertySchema( + name: r'vout', + type: IndexType.value, + caseSensitive: false, ) ], ), @@ -143,6 +158,12 @@ int _uTXOEstimateSize( Map> allOffsets, ) { var bytesCount = offsets.last; + { + final value = object.address; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } { final value = object.blockHash; if (value != null) { @@ -173,18 +194,20 @@ void _uTXOSerialize( List offsets, Map> allOffsets, ) { - writer.writeString(offsets[0], object.blockHash); - writer.writeLong(offsets[1], object.blockHeight); - writer.writeLong(offsets[2], object.blockTime); - writer.writeString(offsets[3], object.blockedReason); - writer.writeBool(offsets[4], object.isBlocked); - writer.writeBool(offsets[5], object.isCoinbase); - writer.writeString(offsets[6], object.name); - writer.writeString(offsets[7], object.otherData); - writer.writeString(offsets[8], object.txid); - writer.writeLong(offsets[9], object.value); - writer.writeLong(offsets[10], object.vout); - writer.writeString(offsets[11], object.walletId); + writer.writeString(offsets[0], object.address); + writer.writeString(offsets[1], object.blockHash); + writer.writeLong(offsets[2], object.blockHeight); + writer.writeLong(offsets[3], object.blockTime); + writer.writeString(offsets[4], object.blockedReason); + writer.writeBool(offsets[5], object.isBlocked); + writer.writeBool(offsets[6], object.isCoinbase); + writer.writeString(offsets[7], object.name); + writer.writeString(offsets[8], object.otherData); + writer.writeString(offsets[9], object.txid); + writer.writeBool(offsets[10], object.used); + writer.writeLong(offsets[11], object.value); + writer.writeLong(offsets[12], object.vout); + writer.writeString(offsets[13], object.walletId); } UTXO _uTXODeserialize( @@ -194,18 +217,20 @@ UTXO _uTXODeserialize( Map> allOffsets, ) { final object = UTXO( - blockHash: reader.readStringOrNull(offsets[0]), - blockHeight: reader.readLongOrNull(offsets[1]), - blockTime: reader.readLongOrNull(offsets[2]), - blockedReason: reader.readStringOrNull(offsets[3]), - isBlocked: reader.readBool(offsets[4]), - isCoinbase: reader.readBool(offsets[5]), - name: reader.readString(offsets[6]), - otherData: reader.readStringOrNull(offsets[7]), - txid: reader.readString(offsets[8]), - value: reader.readLong(offsets[9]), - vout: reader.readLong(offsets[10]), - walletId: reader.readString(offsets[11]), + address: reader.readStringOrNull(offsets[0]), + blockHash: reader.readStringOrNull(offsets[1]), + blockHeight: reader.readLongOrNull(offsets[2]), + blockTime: reader.readLongOrNull(offsets[3]), + blockedReason: reader.readStringOrNull(offsets[4]), + isBlocked: reader.readBool(offsets[5]), + isCoinbase: reader.readBool(offsets[6]), + name: reader.readString(offsets[7]), + otherData: reader.readStringOrNull(offsets[8]), + txid: reader.readString(offsets[9]), + used: reader.readBoolOrNull(offsets[10]), + value: reader.readLong(offsets[11]), + vout: reader.readLong(offsets[12]), + walletId: reader.readString(offsets[13]), ); object.id = id; return object; @@ -221,26 +246,30 @@ P _uTXODeserializeProp

( case 0: return (reader.readStringOrNull(offset)) as P; case 1: - return (reader.readLongOrNull(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 2: return (reader.readLongOrNull(offset)) as P; case 3: - return (reader.readStringOrNull(offset)) as P; + return (reader.readLongOrNull(offset)) as P; case 4: - return (reader.readBool(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 5: return (reader.readBool(offset)) as P; case 6: - return (reader.readString(offset)) as P; + return (reader.readBool(offset)) as P; case 7: - return (reader.readStringOrNull(offset)) as P; - case 8: return (reader.readString(offset)) as P; + case 8: + return (reader.readStringOrNull(offset)) as P; case 9: - return (reader.readLong(offset)) as P; + return (reader.readString(offset)) as P; case 10: - return (reader.readLong(offset)) as P; + return (reader.readBoolOrNull(offset)) as P; case 11: + return (reader.readLong(offset)) as P; + case 12: + return (reader.readLong(offset)) as P; + case 13: return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -260,89 +289,91 @@ void _uTXOAttach(IsarCollection col, Id id, UTXO object) { } extension UTXOByIndex on IsarCollection { - Future getByTxidWalletId(String txid, String walletId) { - return getByIndex(r'txid_walletId', [txid, walletId]); + Future getByTxidWalletIdVout(String txid, String walletId, int vout) { + return getByIndex(r'txid_walletId_vout', [txid, walletId, vout]); } - UTXO? getByTxidWalletIdSync(String txid, String walletId) { - return getByIndexSync(r'txid_walletId', [txid, walletId]); + UTXO? getByTxidWalletIdVoutSync(String txid, String walletId, int vout) { + return getByIndexSync(r'txid_walletId_vout', [txid, walletId, vout]); } - Future deleteByTxidWalletId(String txid, String walletId) { - return deleteByIndex(r'txid_walletId', [txid, walletId]); + Future deleteByTxidWalletIdVout( + String txid, String walletId, int vout) { + return deleteByIndex(r'txid_walletId_vout', [txid, walletId, vout]); } - bool deleteByTxidWalletIdSync(String txid, String walletId) { - return deleteByIndexSync(r'txid_walletId', [txid, walletId]); + bool deleteByTxidWalletIdVoutSync(String txid, String walletId, int vout) { + return deleteByIndexSync(r'txid_walletId_vout', [txid, walletId, vout]); } - Future> getAllByTxidWalletId( - List txidValues, List walletIdValues) { + Future> getAllByTxidWalletIdVout(List txidValues, + List walletIdValues, List voutValues) { final len = txidValues.length; - assert(walletIdValues.length == len, + assert(walletIdValues.length == len && voutValues.length == len, 'All index values must have the same length'); final values = >[]; for (var i = 0; i < len; i++) { - values.add([txidValues[i], walletIdValues[i]]); + values.add([txidValues[i], walletIdValues[i], voutValues[i]]); } - return getAllByIndex(r'txid_walletId', values); + return getAllByIndex(r'txid_walletId_vout', values); } - List getAllByTxidWalletIdSync( - List txidValues, List walletIdValues) { + List getAllByTxidWalletIdVoutSync(List txidValues, + List walletIdValues, List voutValues) { final len = txidValues.length; - assert(walletIdValues.length == len, + assert(walletIdValues.length == len && voutValues.length == len, 'All index values must have the same length'); final values = >[]; for (var i = 0; i < len; i++) { - values.add([txidValues[i], walletIdValues[i]]); + values.add([txidValues[i], walletIdValues[i], voutValues[i]]); } - return getAllByIndexSync(r'txid_walletId', values); + return getAllByIndexSync(r'txid_walletId_vout', values); } - Future deleteAllByTxidWalletId( - List txidValues, List walletIdValues) { + Future deleteAllByTxidWalletIdVout(List txidValues, + List walletIdValues, List voutValues) { final len = txidValues.length; - assert(walletIdValues.length == len, + assert(walletIdValues.length == len && voutValues.length == len, 'All index values must have the same length'); final values = >[]; for (var i = 0; i < len; i++) { - values.add([txidValues[i], walletIdValues[i]]); + values.add([txidValues[i], walletIdValues[i], voutValues[i]]); } - return deleteAllByIndex(r'txid_walletId', values); + return deleteAllByIndex(r'txid_walletId_vout', values); } - int deleteAllByTxidWalletIdSync( - List txidValues, List walletIdValues) { + int deleteAllByTxidWalletIdVoutSync(List txidValues, + List walletIdValues, List voutValues) { final len = txidValues.length; - assert(walletIdValues.length == len, + assert(walletIdValues.length == len && voutValues.length == len, 'All index values must have the same length'); final values = >[]; for (var i = 0; i < len; i++) { - values.add([txidValues[i], walletIdValues[i]]); + values.add([txidValues[i], walletIdValues[i], voutValues[i]]); } - return deleteAllByIndexSync(r'txid_walletId', values); + return deleteAllByIndexSync(r'txid_walletId_vout', values); } - Future putByTxidWalletId(UTXO object) { - return putByIndex(r'txid_walletId', object); + Future putByTxidWalletIdVout(UTXO object) { + return putByIndex(r'txid_walletId_vout', object); } - Id putByTxidWalletIdSync(UTXO object, {bool saveLinks = true}) { - return putByIndexSync(r'txid_walletId', object, saveLinks: saveLinks); + Id putByTxidWalletIdVoutSync(UTXO object, {bool saveLinks = true}) { + return putByIndexSync(r'txid_walletId_vout', object, saveLinks: saveLinks); } - Future> putAllByTxidWalletId(List objects) { - return putAllByIndex(r'txid_walletId', objects); + Future> putAllByTxidWalletIdVout(List objects) { + return putAllByIndex(r'txid_walletId_vout', objects); } - List putAllByTxidWalletIdSync(List objects, + List putAllByTxidWalletIdVoutSync(List objects, {bool saveLinks = true}) { - return putAllByIndexSync(r'txid_walletId', objects, saveLinks: saveLinks); + return putAllByIndexSync(r'txid_walletId_vout', objects, + saveLinks: saveLinks); } } @@ -472,29 +503,29 @@ extension UTXOQueryWhere on QueryBuilder { }); } - QueryBuilder txidEqualToAnyWalletId( + QueryBuilder txidEqualToAnyWalletIdVout( String txid) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IndexWhereClause.equalTo( - indexName: r'txid_walletId', + indexName: r'txid_walletId_vout', value: [txid], )); }); } - QueryBuilder txidNotEqualToAnyWalletId( + QueryBuilder txidNotEqualToAnyWalletIdVout( String txid) { return QueryBuilder.apply(this, (query) { if (query.whereSort == Sort.asc) { return query .addWhereClause(IndexWhereClause.between( - indexName: r'txid_walletId', + indexName: r'txid_walletId_vout', lower: [], upper: [txid], includeUpper: false, )) .addWhereClause(IndexWhereClause.between( - indexName: r'txid_walletId', + indexName: r'txid_walletId_vout', lower: [txid], includeLower: false, upper: [], @@ -502,13 +533,13 @@ extension UTXOQueryWhere on QueryBuilder { } else { return query .addWhereClause(IndexWhereClause.between( - indexName: r'txid_walletId', + indexName: r'txid_walletId_vout', lower: [txid], includeLower: false, upper: [], )) .addWhereClause(IndexWhereClause.between( - indexName: r'txid_walletId', + indexName: r'txid_walletId_vout', lower: [], upper: [txid], includeUpper: false, @@ -517,29 +548,29 @@ extension UTXOQueryWhere on QueryBuilder { }); } - QueryBuilder txidWalletIdEqualTo( + QueryBuilder txidWalletIdEqualToAnyVout( String txid, String walletId) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IndexWhereClause.equalTo( - indexName: r'txid_walletId', + indexName: r'txid_walletId_vout', value: [txid, walletId], )); }); } - QueryBuilder txidEqualToWalletIdNotEqualTo( - String txid, String walletId) { + QueryBuilder + txidEqualToWalletIdNotEqualToAnyVout(String txid, String walletId) { return QueryBuilder.apply(this, (query) { if (query.whereSort == Sort.asc) { return query .addWhereClause(IndexWhereClause.between( - indexName: r'txid_walletId', + indexName: r'txid_walletId_vout', lower: [txid], upper: [txid, walletId], includeUpper: false, )) .addWhereClause(IndexWhereClause.between( - indexName: r'txid_walletId', + indexName: r'txid_walletId_vout', lower: [txid, walletId], includeLower: false, upper: [txid], @@ -547,13 +578,13 @@ extension UTXOQueryWhere on QueryBuilder { } else { return query .addWhereClause(IndexWhereClause.between( - indexName: r'txid_walletId', + indexName: r'txid_walletId_vout', lower: [txid, walletId], includeLower: false, upper: [txid], )) .addWhereClause(IndexWhereClause.between( - indexName: r'txid_walletId', + indexName: r'txid_walletId_vout', lower: [txid], upper: [txid, walletId], includeUpper: false, @@ -562,6 +593,103 @@ extension UTXOQueryWhere on QueryBuilder { }); } + QueryBuilder txidWalletIdVoutEqualTo( + String txid, String walletId, int vout) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'txid_walletId_vout', + value: [txid, walletId, vout], + )); + }); + } + + QueryBuilder txidWalletIdEqualToVoutNotEqualTo( + String txid, String walletId, int vout) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId_vout', + lower: [txid, walletId], + upper: [txid, walletId, vout], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId_vout', + lower: [txid, walletId, vout], + includeLower: false, + upper: [txid, walletId], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId_vout', + lower: [txid, walletId, vout], + includeLower: false, + upper: [txid, walletId], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId_vout', + lower: [txid, walletId], + upper: [txid, walletId, vout], + includeUpper: false, + )); + } + }); + } + + QueryBuilder + txidWalletIdEqualToVoutGreaterThan( + String txid, + String walletId, + int vout, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId_vout', + lower: [txid, walletId, vout], + includeLower: include, + upper: [txid, walletId], + )); + }); + } + + QueryBuilder txidWalletIdEqualToVoutLessThan( + String txid, + String walletId, + int vout, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId_vout', + lower: [txid, walletId], + upper: [txid, walletId, vout], + includeUpper: include, + )); + }); + } + + QueryBuilder txidWalletIdEqualToVoutBetween( + String txid, + String walletId, + int lowerVout, + int upperVout, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId_vout', + lower: [txid, walletId, lowerVout], + includeLower: includeLower, + upper: [txid, walletId, upperVout], + includeUpper: includeUpper, + )); + }); + } + QueryBuilder isBlockedEqualTo(bool isBlocked) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IndexWhereClause.equalTo( @@ -608,6 +736,150 @@ extension UTXOQueryWhere on QueryBuilder { } extension UTXOQueryFilter on QueryBuilder { + QueryBuilder addressIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'address', + )); + }); + } + + QueryBuilder addressIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'address', + )); + }); + } + + QueryBuilder addressEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'address', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressContains(String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressMatches(String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'address', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'address', + value: '', + )); + }); + } + + QueryBuilder addressIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'address', + value: '', + )); + }); + } + QueryBuilder blockHashIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( @@ -1510,6 +1782,31 @@ extension UTXOQueryFilter on QueryBuilder { }); } + QueryBuilder usedIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'used', + )); + }); + } + + QueryBuilder usedIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'used', + )); + }); + } + + QueryBuilder usedEqualTo(bool? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'used', + value: value, + )); + }); + } + QueryBuilder valueEqualTo(int value) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( @@ -1749,6 +2046,18 @@ extension UTXOQueryObject on QueryBuilder {} extension UTXOQueryLinks on QueryBuilder {} extension UTXOQuerySortBy on QueryBuilder { + QueryBuilder sortByAddress() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.asc); + }); + } + + QueryBuilder sortByAddressDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.desc); + }); + } + QueryBuilder sortByBlockHash() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'blockHash', Sort.asc); @@ -1857,6 +2166,18 @@ extension UTXOQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByUsed() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'used', Sort.asc); + }); + } + + QueryBuilder sortByUsedDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'used', Sort.desc); + }); + } + QueryBuilder sortByValue() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'value', Sort.asc); @@ -1895,6 +2216,18 @@ extension UTXOQuerySortBy on QueryBuilder { } extension UTXOQuerySortThenBy on QueryBuilder { + QueryBuilder thenByAddress() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.asc); + }); + } + + QueryBuilder thenByAddressDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.desc); + }); + } + QueryBuilder thenByBlockHash() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'blockHash', Sort.asc); @@ -2015,6 +2348,18 @@ extension UTXOQuerySortThenBy on QueryBuilder { }); } + QueryBuilder thenByUsed() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'used', Sort.asc); + }); + } + + QueryBuilder thenByUsedDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'used', Sort.desc); + }); + } + QueryBuilder thenByValue() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'value', Sort.asc); @@ -2053,6 +2398,13 @@ extension UTXOQuerySortThenBy on QueryBuilder { } extension UTXOQueryWhereDistinct on QueryBuilder { + QueryBuilder distinctByAddress( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'address', caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByBlockHash( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -2113,6 +2465,12 @@ extension UTXOQueryWhereDistinct on QueryBuilder { }); } + QueryBuilder distinctByUsed() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'used'); + }); + } + QueryBuilder distinctByValue() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'value'); @@ -2140,6 +2498,12 @@ extension UTXOQueryProperty on QueryBuilder { }); } + QueryBuilder addressProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'address'); + }); + } + QueryBuilder blockHashProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'blockHash'); @@ -2194,6 +2558,12 @@ extension UTXOQueryProperty on QueryBuilder { }); } + QueryBuilder usedProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'used'); + }); + } + QueryBuilder valueProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'value'); diff --git a/lib/models/signing_data.dart b/lib/models/signing_data.dart new file mode 100644 index 000000000..bb933976c --- /dev/null +++ b/lib/models/signing_data.dart @@ -0,0 +1,21 @@ +import 'dart:typed_data'; + +import 'package:bitcoindart/bitcoindart.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; + +class SigningData { + SigningData({ + required this.derivePathType, + required this.utxo, + this.output, + this.keyPair, + this.redeemScript, + }); + + final DerivePathType derivePathType; + final UTXO utxo; + Uint8List? output; + ECPair? keyPair; + Uint8List? redeemScript; +} diff --git a/lib/models/type_adaptors/epicbox_config_model.g.dart b/lib/models/type_adaptors/epicbox_config_model.g.dart new file mode 100644 index 000000000..70d066370 --- /dev/null +++ b/lib/models/type_adaptors/epicbox_config_model.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../epicbox_config_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class EpicBoxConfigModelAdapter extends TypeAdapter { + @override + final int typeId = 72; + + @override + EpicBoxConfigModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return EpicBoxConfigModel( + host: fields[1] as String, + port: fields[2] as int, + protocolInsecure: fields[3] as bool, + addressIndex: fields[4] as int, + // name: fields[5] as String, + // id: fields[6] as String, + ); + } + + @override + void write(BinaryWriter writer, EpicBoxConfigModel obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.host) + ..writeByte(1) + ..write(obj.port) + ..writeByte(2) + ..write(obj.protocolInsecure) + ..writeByte(3) + ..write(obj.addressIndex); + // ..writeByte(4) + // ..write(obj.id) + // ..writeByte(5) + // ..write(obj.name) + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is EpicBoxConfigModelAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/models/type_adaptors/epicbox_server_model.g.dart b/lib/models/type_adaptors/epicbox_server_model.g.dart new file mode 100644 index 000000000..cc741bf83 --- /dev/null +++ b/lib/models/type_adaptors/epicbox_server_model.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../epicbox_server_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class EpicBoxServerModelAdapter extends TypeAdapter { + @override + final int typeId = 71; + + @override + EpicBoxServerModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return EpicBoxServerModel( + host: fields[1] as String, + port: fields[2] as int, + name: fields[3] as String, + id: fields[0] as String, + useSSL: fields[4] as bool, + enabled: fields[5] as bool, + isFailover: fields[6] as bool, + isDown: fields[7] as bool, + ); + } + + @override + void write(BinaryWriter writer, EpicBoxServerModel obj) { + writer + ..writeByte(8) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.host) + ..writeByte(2) + ..write(obj.port) + ..writeByte(3) + ..write(obj.name) + ..writeByte(4) + ..write(obj.useSSL) + ..writeByte(5) + ..write(obj.enabled) + ..writeByte(6) + ..write(obj.isFailover) + ..writeByte(7) + ..write(obj.isDown); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is EpicBoxServerModelAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart index 32c4b239e..565636a92 100644 --- a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart @@ -61,7 +61,10 @@ class CreateOrRestoreWalletView extends StatelessWidget { ), CoinImage( coin: entity.coin, - isDesktop: isDesktop, + width: + isDesktop ? 324 : MediaQuery.of(context).size.width / 1.6, + height: + isDesktop ? null : MediaQuery.of(context).size.width / 1.6, ), const SizedBox( height: 32, @@ -89,41 +92,45 @@ class CreateOrRestoreWalletView extends StatelessWidget { }, ), ), - body: Container( - color: Theme.of(context).extension()!.background, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.all(31), - child: CoinImage( + body: SafeArea( + child: Container( + color: Theme.of(context).extension()!.background, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + CoinImage( + coin: entity.coin, + width: isDesktop + ? 324 + : MediaQuery.of(context).size.width / 1.6, + height: isDesktop + ? null + : MediaQuery.of(context).size.width / 1.6, + ), + const Spacer( + flex: 2, + ), + CreateRestoreWalletTitle( coin: entity.coin, isDesktop: isDesktop, ), - ), - const Spacer( - flex: 2, - ), - CreateRestoreWalletTitle( - coin: entity.coin, - isDesktop: isDesktop, - ), - const SizedBox( - height: 8, - ), - CreateRestoreWalletSubTitle( - isDesktop: isDesktop, - ), - const Spacer( - flex: 5, - ), - CreateWalletButtonGroup( - coin: entity.coin, - isDesktop: isDesktop, - ), - ], + const SizedBox( + height: 8, + ), + CreateRestoreWalletSubTitle( + isDesktop: isDesktop, + ), + const Spacer( + flex: 5, + ), + CreateWalletButtonGroup( + coin: entity.coin, + isDesktop: isDesktop, + ), + ], + ), ), ), ), diff --git a/lib/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart b/lib/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart index d991fc669..fb282d9ca 100644 --- a/lib/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart +++ b/lib/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart @@ -1,46 +1,42 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; class CoinImage extends ConsumerWidget { const CoinImage({ Key? key, required this.coin, - required this.isDesktop, + this.width, + this.height, }) : super(key: key); final Coin coin; - final bool isDesktop; + final double? width; + final double? height; @override Widget build(BuildContext context, WidgetRef ref) { - final bool isSorbet = ref.read(colorThemeProvider.state).state.themeType == - ThemeType.fruitSorbet; - final bool isForest = - ref.read(colorThemeProvider.state).state.themeType == ThemeType.forest; - - return ((isSorbet && - coin != Coin.epicCash && - coin != Coin.monero && - coin != Coin.namecoin) || - (isForest && coin != Coin.dogecoin)) - ? SvgPicture.asset( - Assets.svg.imageFor(coin: coin, context: context), - width: isDesktop ? 324 : MediaQuery.of(context).size.width, - ) - // : Image( - // image: AssetImage( - // Assets.png.imageFor(coin: coin, context: context), - // ))) - : Image( - image: AssetImage( - Assets.png.imageFor(coin: coin, context: context), - ), - width: isDesktop ? 324 : MediaQuery.of(context).size.width / 3, - ); + if (Theme.of(context).extension()!.themeType == + ThemeType.chan) { + return SizedBox( + width: width, + height: height, + child: Image( + image: AssetImage( + Assets.gif.plain(coin), + ), + ), + ); + } else { + return SvgPicture.asset( + Assets.svg.imageFor(coin: coin, context: context), + width: width, + height: height, + ); + } } } diff --git a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart index 31af6b3bd..6a3f28318 100644 --- a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart +++ b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; @@ -165,11 +166,10 @@ class _NameYourWalletViewState extends ConsumerState { flex: 1, ), if (!isDesktop) - Image( - image: AssetImage( - Assets.png.imageFor(coin: coin, context: context), - ), + CoinImage( + coin: coin, height: 100, + width: 100, ), SizedBox( height: isDesktop ? 0 : 16, diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index 2afd1c9b1..a83fa5bd2 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; import 'package:flutter_svg/svg.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/restore_from_date_picker.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/restore_options_next_button.dart'; @@ -282,11 +283,10 @@ class _RestoreOptionsViewState extends ConsumerState { flex: isDesktop ? 10 : 1, ), if (!isDesktop) - Image( - image: AssetImage( - Assets.png.imageFor(coin: coin, context: context), - ), + CoinImage( + coin: coin, height: 100, + width: 100, ), SizedBox( height: isDesktop ? 0 : 16, diff --git a/lib/pages/buy_view/sub_widgets/fiat_selection_view.dart b/lib/pages/buy_view/sub_widgets/fiat_selection_view.dart index dfaec0dec..09ee82a79 100644 --- a/lib/pages/buy_view/sub_widgets/fiat_selection_view.dart +++ b/lib/pages/buy_view/sub_widgets/fiat_selection_view.dart @@ -76,7 +76,7 @@ class _FiatSelectionViewState extends State { @override Widget build(BuildContext context) { Locale locale = Localizations.localeOf(context); - var format = NumberFormat.simpleCurrency(locale: locale.toString()); + final format = NumberFormat.simpleCurrency(locale: locale.toString()); // See https://stackoverflow.com/a/67055685 final isDesktop = Util.isDesktop; @@ -186,80 +186,178 @@ class _FiatSelectionViewState extends State { height: 12, ), Flexible( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: ListView.builder( - shrinkWrap: true, - primary: isDesktop ? false : null, - itemCount: _fiats.length, - itemBuilder: (builderContext, index) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: GestureDetector( - onTap: () { - Navigator.of(context).pop(_fiats[index]); - }, - child: RoundedWhiteContainer( - child: Row( + child: SingleChildScrollView( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Table( + columnWidths: const { + 0: IntrinsicColumnWidth(), + 1: FlexColumnWidth(), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + ..._fiats.map( + (e) { + return TableRow( children: [ - Container( - padding: const EdgeInsets.all(7.5), - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .currencyListItemBG, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - format.simpleCurrencySymbol( - _fiats[index].ticker.toUpperCase()), - style: STextStyles.subtitle(context).apply( - fontSizeFactor: (1 / - format - .simpleCurrencySymbol(_fiats[index] - .ticker - .toUpperCase()) - .length * // Couldn't get pow() working here - format - .simpleCurrencySymbol(_fiats[index] - .ticker - .toUpperCase()) - .length)), - textAlign: TextAlign.center, + TableCell( + verticalAlignment: + TableCellVerticalAlignment.fill, + child: GestureDetector( + onTap: () => Navigator.of(context).pop(e), + child: Container( + color: Colors.transparent, + padding: const EdgeInsets.only(left: 12), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.all(7.5), + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .currencyListItemBG, + borderRadius: + BorderRadius.circular(4), + ), + child: Text( + format.simpleCurrencySymbol( + e.ticker.toUpperCase()), + style: STextStyles.subtitle(context) + .apply( + fontSizeFactor: (1 / + format + .simpleCurrencySymbol( + e.ticker.toUpperCase()) + .length * // Couldn't get pow() working here + format + .simpleCurrencySymbol( + e.ticker.toUpperCase()) + .length), + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), ), ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _fiats[index].name, - style: STextStyles.largeMedium14(context), + GestureDetector( + onTap: () => Navigator.of(context).pop(e), + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + e.name, + style: + STextStyles.largeMedium14(context), + ), + const SizedBox( + height: 2, + ), + Text( + e.ticker.toUpperCase(), + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ], ), - const SizedBox( - height: 2, - ), - Text( - _fiats[index].ticker.toUpperCase(), - style: STextStyles.smallMed12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - ], + ), ), ), ], - ), - ), + ); + }, ), - ); - }, + ], + ), + + // child: ListView.builder( + // shrinkWrap: true, + // primary: isDesktop ? false : null, + // itemCount: _fiats.length, + // itemBuilder: (builderContext, index) { + // return Padding( + // padding: const EdgeInsets.symmetric(vertical: 4), + // child: GestureDetector( + // onTap: () { + // Navigator.of(context).pop(_fiats[index]); + // }, + // child: RoundedWhiteContainer( + // child: Row( + // children: [ + // Container( + // padding: const EdgeInsets.all(7.5), + // decoration: BoxDecoration( + // color: Theme.of(context) + // .extension()! + // .currencyListItemBG, + // borderRadius: BorderRadius.circular(4), + // ), + // child: Text( + // format.simpleCurrencySymbol( + // _fiats[index].ticker.toUpperCase()), + // style: STextStyles.subtitle(context).apply( + // fontSizeFactor: (1 / + // format + // .simpleCurrencySymbol(_fiats[index] + // .ticker + // .toUpperCase()) + // .length * // Couldn't get pow() working here + // format + // .simpleCurrencySymbol(_fiats[index] + // .ticker + // .toUpperCase()) + // .length)), + // textAlign: TextAlign.center, + // ), + // ), + // const SizedBox( + // width: 10, + // ), + // Expanded( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text( + // _fiats[index].name, + // style: STextStyles.largeMedium14(context), + // ), + // const SizedBox( + // height: 2, + // ), + // Text( + // _fiats[index].ticker.toUpperCase(), + // style: STextStyles.smallMed12(context) + // .copyWith( + // color: Theme.of(context) + // .extension()! + // .textSubtitle1, + // ), + // ), + // ], + // ), + // ), + // ], + // ), + // ), + // ), + // ); + // }, + // ), ), ), ), diff --git a/lib/pages/coin_control/coin_control_view.dart b/lib/pages/coin_control/coin_control_view.dart new file mode 100644 index 000000000..0eeff1089 --- /dev/null +++ b/lib/pages/coin_control/coin_control_view.dart @@ -0,0 +1,772 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/pages/coin_control/utxo_card.dart'; +import 'package:stackwallet/pages/coin_control/utxo_details_view.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/mixins/coin_control_interface.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/app_bar_field.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/dropdown_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/expandable2.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/toggle.dart'; +import 'package:tuple/tuple.dart'; + +import '../../widgets/animated_widgets/rotate_icon.dart'; +import '../../widgets/rounded_container.dart'; + +enum CoinControlViewType { + manage, + use; +} + +class CoinControlView extends ConsumerStatefulWidget { + const CoinControlView({ + Key? key, + required this.walletId, + required this.type, + this.requestedTotal, + this.selectedUTXOs, + }) : super(key: key); + + static const routeName = "/coinControl"; + + final String walletId; + final CoinControlViewType type; + final int? requestedTotal; + final Set? selectedUTXOs; + + @override + ConsumerState createState() => _CoinControlViewState(); +} + +class _CoinControlViewState extends ConsumerState { + final searchController = TextEditingController(); + final searchFocus = FocusNode(); + + bool _isSearching = false; + bool _showBlocked = false; + + CCSortDescriptor _sort = CCSortDescriptor.age; + + Map>? _map; + List? _list; + + final Set _selectedAvailable = {}; + final Set _selectedBlocked = {}; + + Future _refreshBalance() async { + final coinControlInterface = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as CoinControlInterface; + await coinControlInterface.refreshBalance(notify: true); + } + + @override + void initState() { + if (widget.selectedUTXOs != null) { + _selectedAvailable.addAll(widget.selectedUTXOs!); + } + searchController.addListener(() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + setState(() {}); + }); + }); + super.initState(); + } + + @override + void dispose() { + searchController.dispose(); + searchFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + final coin = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value + .getManager( + widget.walletId, + ) + .coin, + ), + ); + + final currentChainHeight = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value + .getManager( + widget.walletId, + ) + .currentHeight, + ), + ); + + if (_sort == CCSortDescriptor.address && !_isSearching) { + _list = null; + _map = MainDB.instance.queryUTXOsGroupedByAddressSync( + walletId: widget.walletId, + filter: CCFilter.all, + sort: _sort, + searchTerm: "", + coin: coin, + ); + } else { + _map = null; + _list = MainDB.instance.queryUTXOsSync( + walletId: widget.walletId, + filter: _isSearching + ? CCFilter.all + : _showBlocked + ? CCFilter.frozen + : CCFilter.available, + sort: _sort, + searchTerm: _isSearching ? searchController.text : "", + coin: coin, + ); + } + + return WillPopScope( + onWillPop: () async { + unawaited(_refreshBalance()); + Navigator.of(context).pop( + widget.type == CoinControlViewType.use ? _selectedAvailable : null); + return false; + }, + child: Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + automaticallyImplyLeading: false, + leading: _isSearching + ? null + : widget.type == CoinControlViewType.use && + _selectedAvailable.isNotEmpty + ? AppBarIconButton( + icon: XIcon( + width: 24, + height: 24, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: () { + setState(() { + _selectedAvailable.clear(); + }); + }, + ) + : AppBarBackButton( + onPressed: () { + unawaited(_refreshBalance()); + Navigator.of(context).pop( + widget.type == CoinControlViewType.use + ? _selectedAvailable + : null); + }, + ), + title: _isSearching + ? AppBarSearchField( + controller: searchController, + focusNode: searchFocus, + ) + : Text( + "Coin control", + style: STextStyles.navBarTitle(context), + ), + titleSpacing: 0, + actions: _isSearching + ? [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + size: 36, + icon: SvgPicture.asset( + Assets.svg.x, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: () { + // show search + setState(() { + _isSearching = false; + }); + }, + ), + ), + ] + : [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + size: 36, + icon: SvgPicture.asset( + Assets.svg.search, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: () { + // show search + setState(() { + _isSearching = true; + }); + }, + ), + ), + AspectRatio( + aspectRatio: 1, + child: JDropdownIconButton( + mobileAppBar: true, + groupValue: _sort, + items: CCSortDescriptor.values.toSet(), + onSelectionChanged: (CCSortDescriptor? newValue) { + if (newValue != null && newValue != _sort) { + setState(() { + _sort = newValue; + }); + } + }, + displayPrefix: "Sort by", + ), + ), + ], + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Column( + children: [ + const SizedBox( + height: 10, + ), + if (!_isSearching) + RoundedWhiteContainer( + child: Text( + "This option allows you to control, freeze, and utilize " + "outputs at your discretion. Tap the output circle to " + "select.", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ), + if (!_isSearching) + const SizedBox( + height: 10, + ), + if (!(_isSearching || _map != null)) + SizedBox( + height: 48, + child: Toggle( + key: UniqueKey(), + onColor: Theme.of(context) + .extension()! + .popupBG, + onText: "Available outputs", + offColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + offText: "Frozen outputs", + isOn: _showBlocked, + onValueChanged: (value) { + setState(() { + _showBlocked = value; + }); + }, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ), + if (!_isSearching) + const SizedBox( + height: 10, + ), + if (_isSearching) + Expanded( + child: ListView.separated( + itemCount: _list!.length, + separatorBuilder: (context, _) => const SizedBox( + height: 10, + ), + itemBuilder: (context, index) { + final utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(_list![index]) + .findFirstSync()!; + + final isSelected = + _selectedBlocked.contains(utxo) || + _selectedAvailable.contains(utxo); + + return UtxoCard( + key: Key( + "${utxo.walletId}_${utxo.id}_$isSelected"), + walletId: widget.walletId, + utxo: utxo, + canSelect: widget.type == + CoinControlViewType.manage || + (widget.type == CoinControlViewType.use && + !utxo.isBlocked && + utxo.isConfirmed( + currentChainHeight, + coin.requiredConfirmations, + )), + initialSelectedState: isSelected, + onSelectedChanged: (value) { + if (value) { + utxo.isBlocked + ? _selectedBlocked.add(utxo) + : _selectedAvailable.add(utxo); + } else { + utxo.isBlocked + ? _selectedBlocked.remove(utxo) + : _selectedAvailable.remove(utxo); + } + setState(() {}); + }, + onPressed: () async { + final result = + await Navigator.of(context).pushNamed( + UtxoDetailsView.routeName, + arguments: Tuple2( + utxo.id, + widget.walletId, + ), + ); + if (mounted && result == "refresh") { + setState(() {}); + } + }, + ); + }, + ), + ), + if (!_isSearching) + _list != null + ? Expanded( + child: ListView.separated( + itemCount: _list!.length, + separatorBuilder: (context, _) => + const SizedBox( + height: 10, + ), + itemBuilder: (context, index) { + final utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(_list![index]) + .findFirstSync()!; + + final isSelected = _showBlocked + ? _selectedBlocked.contains(utxo) + : _selectedAvailable.contains(utxo); + + return UtxoCard( + key: Key( + "${utxo.walletId}_${utxo.id}_$isSelected"), + walletId: widget.walletId, + utxo: utxo, + canSelect: widget.type == + CoinControlViewType.manage || + (widget.type == + CoinControlViewType.use && + !_showBlocked && + utxo.isConfirmed( + currentChainHeight, + coin.requiredConfirmations, + )), + initialSelectedState: isSelected, + onSelectedChanged: (value) { + if (value) { + _showBlocked + ? _selectedBlocked.add(utxo) + : _selectedAvailable.add(utxo); + } else { + _showBlocked + ? _selectedBlocked.remove(utxo) + : _selectedAvailable + .remove(utxo); + } + setState(() {}); + }, + onPressed: () async { + final result = + await Navigator.of(context) + .pushNamed( + UtxoDetailsView.routeName, + arguments: Tuple2( + utxo.id, + widget.walletId, + ), + ); + if (mounted && result == "refresh") { + setState(() {}); + } + }, + ); + }, + ), + ) + : Expanded( + child: ListView.separated( + itemCount: _map!.entries.length, + separatorBuilder: (context, _) => + const SizedBox( + height: 10, + ), + itemBuilder: (context, index) { + final entry = + _map!.entries.elementAt(index); + final _controller = + RotateIconController(); + + return Expandable2( + border: Theme.of(context) + .extension()! + .backgroundAppBar, + background: Theme.of(context) + .extension()! + .popupBG, + animationDurationMultiplier: + 0.2 * entry.value.length, + onExpandWillChange: (state) { + if (state == + Expandable2State.expanded) { + _controller.forward?.call(); + } else { + _controller.reverse?.call(); + } + }, + header: RoundedContainer( + padding: const EdgeInsets.all(14), + color: Colors.transparent, + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + entry.key, + style: + STextStyles.w600_14( + context), + ), + const SizedBox( + height: 2, + ), + Text( + "${entry.value.length} " + "output${entry.value.length > 1 ? "s" : ""}", + style: + STextStyles.w500_12( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textSubtitle1, + ), + ), + ], + ), + ), + RotateIcon( + animationDurationMultiplier: + 0.2 * entry.value.length, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 14, + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + curve: Curves.easeInOut, + controller: _controller, + ), + ], + ), + ), + children: entry.value.map( + (id) { + final utxo = MainDB + .instance.isar.utxos + .where() + .idEqualTo(id) + .findFirstSync()!; + + final isSelected = _selectedBlocked + .contains(utxo) || + _selectedAvailable + .contains(utxo); + + return UtxoCard( + key: Key( + "${utxo.walletId}_${utxo.id}_$isSelected"), + walletId: widget.walletId, + utxo: utxo, + canSelect: widget.type == + CoinControlViewType + .manage || + (widget.type == + CoinControlViewType + .use && + !utxo.isBlocked && + utxo.isConfirmed( + currentChainHeight, + coin.requiredConfirmations, + )), + initialSelectedState: isSelected, + onSelectedChanged: (value) { + if (value) { + utxo.isBlocked + ? _selectedBlocked + .add(utxo) + : _selectedAvailable + .add(utxo); + } else { + utxo.isBlocked + ? _selectedBlocked + .remove(utxo) + : _selectedAvailable + .remove(utxo); + } + setState(() {}); + }, + onPressed: () async { + final result = + await Navigator.of(context) + .pushNamed( + UtxoDetailsView.routeName, + arguments: Tuple2( + utxo.id, + widget.walletId, + ), + ); + if (mounted && + result == "refresh") { + setState(() {}); + } + }, + ); + }, + ).toList(), + ); + }, + ), + ), + ], + ), + ), + ), + if (((_showBlocked && _selectedBlocked.isNotEmpty) || + (!_showBlocked && _selectedAvailable.isNotEmpty)) && + widget.type == CoinControlViewType.manage) + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .backgroundAppBar, + boxShadow: [ + Theme.of(context) + .extension()! + .standardBoxShadow, + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: SecondaryButton( + label: _showBlocked ? "Unfreeze" : "Freeze", + onPressed: () async { + if (_showBlocked) { + await MainDB.instance.putUTXOs(_selectedBlocked + .map( + (e) => e.copyWith( + isBlocked: false, + ), + ) + .toList()); + _selectedBlocked.clear(); + } else { + await MainDB.instance.putUTXOs(_selectedAvailable + .map( + (e) => e.copyWith( + isBlocked: true, + ), + ) + .toList()); + _selectedAvailable.clear(); + } + setState(() {}); + }, + ), + ), + ), + if (!_showBlocked && widget.type == CoinControlViewType.use) + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .backgroundAppBar, + boxShadow: [ + Theme.of(context) + .extension()! + .standardBoxShadow, + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Selected amount", + style: STextStyles.w600_14(context), + ), + Builder( + builder: (context) { + int selectedSum = + _selectedAvailable.isEmpty + ? 0 + : _selectedAvailable + .map((e) => e.value) + .reduce( + (value, element) => + value += element, + ); + return Text( + "${Format.satoshisToAmount( + selectedSum, + coin: coin, + ).toStringAsFixed( + coin.decimals, + )} ${coin.ticker}", + style: widget.requestedTotal == null + ? STextStyles.w600_14(context) + : STextStyles.w600_14(context).copyWith( + color: selectedSum >= + widget + .requestedTotal! + ? Theme.of(context) + .extension< + StackColors>()! + .accentColorGreen + : Theme.of(context) + .extension< + StackColors>()! + .accentColorRed), + ); + }, + ), + ], + ), + ), + if (widget.requestedTotal != null) + Container( + width: double.infinity, + height: 1.5, + color: Theme.of(context) + .extension()! + .backgroundAppBar, + ), + if (widget.requestedTotal != null) + Padding( + padding: const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount to send", + style: STextStyles.w600_14(context), + ), + Text( + "${Format.satoshisToAmount( + widget.requestedTotal!, + coin: coin, + ).toStringAsFixed( + coin.decimals, + )} ${coin.ticker}", + style: STextStyles.w600_14(context), + ), + ], + ), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Use coins", + enabled: _selectedAvailable.isNotEmpty, + onPressed: () async { + if (searchFocus.hasFocus) { + searchFocus.unfocus(); + } + Navigator.of(context).pop( + _selectedAvailable, + ); + }, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/coin_control/utxo_card.dart b/lib/pages/coin_control/utxo_card.dart new file mode 100644 index 000000000..e2900a48b --- /dev/null +++ b/lib/pages/coin_control/utxo_card.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/icon_widgets/utxo_status_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; + +class UtxoCard extends ConsumerStatefulWidget { + const UtxoCard({ + Key? key, + required this.utxo, + required this.walletId, + required this.onSelectedChanged, + required this.initialSelectedState, + required this.canSelect, + this.onPressed, + }) : super(key: key); + + final String walletId; + final UTXO utxo; + final void Function(bool) onSelectedChanged; + final bool initialSelectedState; + final VoidCallback? onPressed; + final bool canSelect; + + @override + ConsumerState createState() => _UtxoCardState(); +} + +class _UtxoCardState extends ConsumerState { + late Stream stream; + late UTXO utxo; + + late bool _selected; + + @override + void initState() { + _selected = widget.initialSelectedState; + utxo = widget.utxo; + + stream = MainDB.instance.watchUTXO(id: utxo.id); + super.initState(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + final coin = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId).coin)); + + final currentChainHeight = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId).currentHeight)); + + return ConditionalParent( + condition: widget.onPressed != null, + builder: (child) => MaterialButton( + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + color: Theme.of(context).extension()!.popupBG, + elevation: 0, + disabledElevation: 0, + hoverElevation: 0, + focusElevation: 0, + highlightElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(Constants.size.circularBorderRadius), + ), + onPressed: widget.onPressed, + child: child, + ), + child: RoundedContainer( + color: widget.onPressed == null + ? Theme.of(context).extension()!.popupBG + : Colors.transparent, + child: StreamBuilder( + stream: stream, + builder: (context, snapshot) { + if (snapshot.hasData) { + utxo = snapshot.data!; + } + return Row( + children: [ + ConditionalParent( + condition: widget.canSelect, + builder: (child) => GestureDetector( + onTap: () { + _selected = !_selected; + widget.onSelectedChanged(_selected); + setState(() {}); + }, + child: child, + ), + child: UTXOStatusIcon( + blocked: utxo.isBlocked, + status: utxo.isConfirmed( + currentChainHeight, + coin.requiredConfirmations, + ) + ? UTXOStatusIconStatus.confirmed + : UTXOStatusIconStatus.unconfirmed, + background: + Theme.of(context).extension()!.popupBG, + selected: _selected, + width: 32, + height: 32, + ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${Format.satoshisToAmount( + utxo.value, + coin: coin, + ).toStringAsFixed(coin.decimals)} ${coin.ticker}", + style: STextStyles.w600_14(context), + ), + const SizedBox( + height: 2, + ), + Row( + children: [ + Flexible( + child: Text( + utxo.name.isNotEmpty + ? utxo.name + : utxo.address ?? utxo.txid, + style: STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ); + }), + ), + ); + } +} diff --git a/lib/pages/coin_control/utxo_details_view.dart b/lib/pages/coin_control/utxo_details_view.dart new file mode 100644 index 000000000..e4b31c63d --- /dev/null +++ b/lib/pages/coin_control/utxo_details_view.dart @@ -0,0 +1,565 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_edit_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/utxo_status_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; + +class UtxoDetailsView extends ConsumerStatefulWidget { + const UtxoDetailsView({ + Key? key, + required this.utxoId, + required this.walletId, + }) : super(key: key); + + static const routeName = "/utxoDetails"; + + final Id utxoId; + final String walletId; + + @override + ConsumerState createState() => _UtxoDetailsViewState(); +} + +class _UtxoDetailsViewState extends ConsumerState { + final isDesktop = Util.isDesktop; + + late Stream streamUTXO; + UTXO? utxo; + + Stream? streamLabel; + AddressLabel? label; + + bool _popWithRefresh = false; + + Future _toggleFreeze() async { + _popWithRefresh = true; + await MainDB.instance.putUTXO(utxo!.copyWith(isBlocked: !utxo!.isBlocked)); + } + + @override + void initState() { + utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(widget.utxoId) + .findFirstSync()!; + + streamUTXO = MainDB.instance.watchUTXO(id: widget.utxoId); + + if (utxo?.address != null) { + label = MainDB.instance.getAddressLabelSync( + widget.walletId, + utxo!.address!, + ); + + if (label != null) { + streamLabel = MainDB.instance.watchAddressLabel(id: label!.id); + } + } + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final coin = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(widget.walletId).coin, + ), + ); + + final currentHeight = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(widget.walletId).currentHeight, + ), + ); + + final confirmed = utxo!.isConfirmed( + currentHeight, + coin.requiredConfirmations, + ); + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(_popWithRefresh ? "refresh" : null); + }, + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: StreamBuilder( + stream: streamUTXO, + builder: (context, snapshot) { + if (snapshot.hasData) { + utxo = snapshot.data!; + } + return ConditionalParent( + condition: isDesktop, + builder: (child) { + return DesktopDialog( + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Output details", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of(context) + .pop(_popWithRefresh ? "refresh" : null); + }, + ), + ], + ), + IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + top: 10, + ), + child: Column( + children: [ + IntrinsicHeight( + child: RoundedContainer( + padding: EdgeInsets.zero, + color: Colors.transparent, + borderColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: child, + ), + ), + const SizedBox( + height: 20, + ), + SecondaryButton( + buttonHeight: ButtonHeight.l, + label: utxo!.isBlocked ? "Unfreeze" : "Freeze", + onPressed: _toggleFreeze, + ), + ], + ), + ), + ), + ], + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!isDesktop) + const SizedBox( + height: 10, + ), + RoundedContainer( + padding: const EdgeInsets.all(12), + color: isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (isDesktop) + UTXOStatusIcon( + blocked: utxo!.isBlocked, + status: confirmed + ? UTXOStatusIconStatus.confirmed + : UTXOStatusIconStatus.unconfirmed, + background: Theme.of(context) + .extension()! + .popupBG, + selected: false, + width: 32, + height: 32, + ), + if (isDesktop) + const SizedBox( + width: 16, + ), + Text( + "${Format.satoshisToAmount( + utxo!.value, + coin: coin, + ).toStringAsFixed( + coin.decimals, + )} ${coin.ticker}", + style: STextStyles.pageTitleH2(context), + ), + ], + ), + Text( + utxo!.isBlocked + ? "Frozen" + : confirmed + ? "Available" + : "Unconfirmed", + style: STextStyles.w500_14(context).copyWith( + color: utxo!.isBlocked + ? const Color(0xFF7FA2D4) // todo theme + : confirmed + ? Theme.of(context) + .extension()! + .accentColorGreen + : Theme.of(context) + .extension()! + .accentColorYellow, + ), + ), + ], + ), + ), + const _Div(), + RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Label", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + SimpleEditButton( + editValue: utxo!.name, + editLabel: "label", + onValueChanged: (newName) { + MainDB.instance.putUTXO( + utxo!.copyWith( + name: newName, + ), + ); + }, + ), + ], + ), + const SizedBox( + height: 4, + ), + Text( + utxo!.name, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + const _Div(), + RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Address", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + isDesktop + ? IconCopyButton( + data: utxo!.address!, + ) + : SimpleCopyButton( + data: utxo!.address!, + ), + ], + ), + const SizedBox( + height: 4, + ), + Text( + utxo!.address!, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + if (label != null && label!.value.isNotEmpty) const _Div(), + if (label != null && label!.value.isNotEmpty) + RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Address label", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + isDesktop + ? IconCopyButton( + data: utxo!.address!, + ) + : SimpleCopyButton( + data: label!.value, + ), + ], + ), + const SizedBox( + height: 4, + ), + Text( + label!.value, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + const _Div(), + RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction ID", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + isDesktop + ? IconCopyButton( + data: utxo!.address!, + ) + : SimpleCopyButton( + data: utxo!.txid, + ), + ], + ), + const SizedBox( + height: 4, + ), + Text( + utxo!.txid, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + const _Div(), + RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Confirmations", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + const SizedBox( + height: 4, + ), + Text( + "${utxo!.getConfirmations(currentHeight)}", + style: STextStyles.w500_14(context), + ), + ], + ), + ), + if (utxo!.isBlocked) const _Div(), + if (utxo!.isBlocked) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: isDesktop + ? Colors.transparent + : Theme.of(context) + .extension()! + .popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Freeze reason", + style: + STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + SimpleEditButton( + editValue: utxo!.blockedReason ?? "", + editLabel: "freeze reason", + onValueChanged: (newReason) { + MainDB.instance.putUTXO( + utxo!.copyWith( + blockedReason: newReason, + ), + ); + }, + ), + ], + ), + const SizedBox( + height: 4, + ), + Text( + utxo!.blockedReason ?? "", + style: STextStyles.w500_14(context), + ), + ], + ), + ), + if (!isDesktop) const _Div(), + ], + ), + if (!isDesktop) const Spacer(), + if (!isDesktop) + SecondaryButton( + label: utxo!.isBlocked ? "Unfreeze" : "Freeze", + onPressed: _toggleFreeze, + ), + if (!isDesktop) + const SizedBox( + height: 16, + ), + ], + ), + ); + }), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (Util.isDesktop) { + return Container( + width: double.infinity, + height: 1.0, + color: Theme.of(context).extension()!.textFieldDefaultBG, + ); + } else { + return const SizedBox( + height: 12, + ); + } + } +} diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index 808a0417f..23712f0fd 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -64,33 +64,49 @@ class _ConfirmChangeNowSendViewState final isDesktop = Util.isDesktop; Future _attemptSend(BuildContext context) async { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); unawaited( showDialog( context: context, useSafeArea: false, barrierDismissible: false, builder: (context) { - return const SendingTransactionDialog(); + return SendingTransactionDialog( + coin: manager.coin, + ); }, ), ); + final time = Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); + + late String txid; + Future txidFuture; + final String note = transactionInfo["note"] as String? ?? ""; - final manager = - ref.read(walletsChangeNotifierProvider).getManager(walletId); try { - late final String txid; - if (widget.shouldSendPublicFiroFunds == true) { - txid = await (manager.wallet as FiroWallet) + txidFuture = (manager.wallet as FiroWallet) .confirmSendPublic(txData: transactionInfo); } else { - txid = await manager.confirmSend(txData: transactionInfo); + txidFuture = manager.confirmSend(txData: transactionInfo); } unawaited(manager.refresh()); + final results = await Future.wait([ + txidFuture, + time, + ]); + + txid = results.first as String; + // save note await ref .read(notesServiceChangeNotifierProvider(walletId)) diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index c04360bf6..ab7cab56e 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -618,6 +618,16 @@ class _ExchangeFormState extends ConsumerState { if (walletInitiated) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { ref.read(exchangeFormStateProvider).reset(shouldNotifyListeners: true); + ExchangeDataLoadingService.instance + .getAggregateCurrency( + coin!.ticker, + ExchangeRateType.estimated, + ) + .then((value) { + if (value != null) { + ref.read(exchangeFormStateProvider).updateSendCurrency(value, true); + } + }); }); } else { _sendController.text = @@ -848,7 +858,7 @@ class _ExchangeFormState extends ConsumerState { enabled: ref.watch( exchangeFormStateProvider.select((value) => value.canExchange)), onPressed: onExchangePressed, - label: "Exchange", + label: "Swap", ) ], ); diff --git a/lib/pages/exchange_view/exchange_step_views/step_1_view.dart b/lib/pages/exchange_view/exchange_step_views/step_1_view.dart index c57e0acad..b866de448 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_1_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_1_view.dart @@ -56,7 +56,7 @@ class _Step1ViewState extends State { }, ), title: Text( - "Exchange", + "Swap", style: STextStyles.navBarTitle(context), ), ), diff --git a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart index 61f3517ce..a32df6e74 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart @@ -144,7 +144,7 @@ class _Step2ViewState extends ConsumerState { }, ), title: Text( - "Exchange", + "Swap", style: STextStyles.navBarTitle(context), ), ), diff --git a/lib/pages/exchange_view/exchange_step_views/step_3_view.dart b/lib/pages/exchange_view/exchange_step_views/step_3_view.dart index 0c5eaa8c9..22c356b5d 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_3_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_3_view.dart @@ -72,7 +72,7 @@ class _Step3ViewState extends ConsumerState { }, ), title: Text( - "Exchange", + "Swap", style: STextStyles.navBarTitle(context), ), ), diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index 2507ac2d4..30d40f008 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -152,7 +152,7 @@ class _Step4ViewState extends ConsumerState { ), ), title: Text( - "Exchange", + "Swap", style: STextStyles.navBarTitle(context), ), ), @@ -536,24 +536,34 @@ class _Step4ViewState extends ConsumerState { try { bool wasCancelled = false; - unawaited(showDialog( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + coin: manager.coin, + onCancel: () { + wasCancelled = true; - Navigator.of(context) - .pop(); - }, - ); - }, - )); + Navigator.of(context) + .pop(); + }, + ); + }, + ), + ); - final txData = - await manager.prepareSend( + final time = + Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); + + final txDataFuture = + manager.prepareSend( address: address, satoshiAmount: amount, args: { @@ -563,6 +573,15 @@ class _Step4ViewState extends ConsumerState { }, ); + final results = + await Future.wait([ + txDataFuture, + time, + ]); + + final txData = results.last + as Map; + if (!wasCancelled) { // pop building dialog diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 41df723da..780a88d92 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -240,6 +240,7 @@ class _SendFromCardState extends ConsumerState { ), ), child: BuildingTransactionDialog( + coin: manager.coin, onCancel: () { wasCancelled = true; @@ -251,11 +252,18 @@ class _SendFromCardState extends ConsumerState { ), ); - late Map txData; + final time = Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); + + Map txData; + Future> txDataFuture; // if not firo then do normal send if (shouldSendPublicFiroFunds == null) { - txData = await manager.prepareSend( + txDataFuture = manager.prepareSend( address: address, satoshiAmount: _amount, args: { @@ -267,7 +275,7 @@ class _SendFromCardState extends ConsumerState { final firoWallet = manager.wallet as FiroWallet; // otherwise do firo send based on balance selected if (shouldSendPublicFiroFunds) { - txData = await firoWallet.prepareSendPublic( + txDataFuture = firoWallet.prepareSendPublic( address: address, satoshiAmount: _amount, args: { @@ -276,7 +284,7 @@ class _SendFromCardState extends ConsumerState { }, ); } else { - txData = await firoWallet.prepareSend( + txDataFuture = firoWallet.prepareSend( address: address, satoshiAmount: _amount, args: { @@ -287,6 +295,13 @@ class _SendFromCardState extends ConsumerState { } } + final results = await Future.wait([ + txDataFuture, + time, + ]); + + txData = results.first as Map; + if (!wasCancelled) { // pop building dialog diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 662ed6922..76653bd18 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -116,9 +116,16 @@ class _TradeDetailsViewState extends ConsumerState { } status = changeNowTransactionStatusFromStringIgnoreCase(statusString); } on ArgumentError catch (_) { - status = ChangeNowTransactionStatus.Failed; - if (statusString == "Processing payment") { - status = ChangeNowTransactionStatus.Sending; + switch (statusString.toLowerCase()) { + case "funds confirming": + case "processing payment": + return Assets.svg.txExchangePending(context); + + case "completed": + return Assets.svg.txExchange(context); + + default: + status = ChangeNowTransactionStatus.Failed; } } @@ -315,7 +322,7 @@ class _TradeDetailsViewState extends ConsumerState { width: 16, ), SelectableText( - "Exchange", + "Swap service", style: STextStyles.desktopTextMedium(context), ), ], @@ -370,12 +377,46 @@ class _TradeDetailsViewState extends ConsumerState { padding: isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Status", - style: STextStyles.itemSubtitle(context), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: STextStyles.itemSubtitle(context), + ), + if (trade.exchangeName == + MajesticBankExchange.exchangeName && + trade.status == "Completed") + Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (context) => const StackOkDialog( + title: "Trade Info", + message: + "Majestic Bank does not store order data indefinitely", + ), + ); + }, + child: SvgPicture.asset( + Assets.svg.circleInfo, + height: 20, + width: 20, + color: Theme.of(context) + .extension()! + .infoItemIcons, + ), + ), + ], + ) + ], ), const SizedBox( height: 4, @@ -388,8 +429,6 @@ class _TradeDetailsViewState extends ConsumerState { .colorForStatus(trade.status), ), ), - // ), - // ), ], ), ), @@ -629,11 +668,15 @@ class _TradeDetailsViewState extends ConsumerState { text: address, ), ); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } }, child: Row( children: [ @@ -1009,7 +1052,7 @@ class _TradeDetailsViewState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Exchange", + "Swap service", style: STextStyles.itemSubtitle(context), ), if (isDesktop) @@ -1084,11 +1127,15 @@ class _TradeDetailsViewState extends ConsumerState { onTap: () async { final data = ClipboardData(text: trade.tradeId); await clipboard.setData(data); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } }, child: SvgPicture.asset( Assets.svg.copy, diff --git a/lib/pages/exchange_view/wallet_initiated_exchange_view.dart b/lib/pages/exchange_view/wallet_initiated_exchange_view.dart index bfdb5e9cd..99ba04979 100644 --- a/lib/pages/exchange_view/wallet_initiated_exchange_view.dart +++ b/lib/pages/exchange_view/wallet_initiated_exchange_view.dart @@ -127,7 +127,7 @@ class _WalletInitiatedExchangeViewState }, ), title: Text( - "Exchange", + "Swap", style: STextStyles.navBarTitle(context), ), ), diff --git a/lib/pages/generic/single_field_edit_view.dart b/lib/pages/generic/single_field_edit_view.dart new file mode 100644 index 000000000..c1049b006 --- /dev/null +++ b/lib/pages/generic/single_field_edit_view.dart @@ -0,0 +1,204 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_native_splash/cli_commands.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class SingleFieldEditView extends StatefulWidget { + const SingleFieldEditView({ + Key? key, + required this.initialValue, + required this.label, + }) : super(key: key); + + static const String routeName = "/singleFieldEdit"; + + final String initialValue; + final String label; + + @override + State createState() => _SingleFieldEditViewState(); +} + +class _SingleFieldEditViewState extends State { + late final TextEditingController _textController; + final _textFocusNode = FocusNode(); + + late final bool isDesktop; + + @override + void initState() { + isDesktop = Util.isDesktop; + _textController = TextEditingController()..text = widget.initialValue; + super.initState(); + } + + @override + void dispose() { + _textController.dispose(); + _textFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Edit ${widget.label}", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ); + }, + ), + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!isDesktop) + const SizedBox( + height: 10, + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Edit ${widget.label}", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + ), + Padding( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 32, + ) + : const EdgeInsets.all(0), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _textController, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + focusNode: _textFocusNode, + decoration: standardInputDecoration( + widget.label.capitalize(), + _textFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + suffixIcon: _textController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _textController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + // if (!isDesktop) + const Spacer(), + + ConditionalParent( + condition: isDesktop, + builder: (child) => Padding( + padding: const EdgeInsets.all(32), + child: child, + ), + child: PrimaryButton( + label: "Save", + onPressed: () { + if (mounted) { + Navigator.of(context).pop(_textController.text); + } + }, + ), + ) + ], + ), + ); + } +} diff --git a/lib/pages/home_view/home_view.dart b/lib/pages/home_view/home_view.dart index 71acb919b..ea9b29558 100644 --- a/lib/pages/home_view/home_view.dart +++ b/lib/pages/home_view/home_view.dart @@ -17,6 +17,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -34,6 +35,7 @@ class _HomeViewState extends ConsumerState { final GlobalKey _key = GlobalKey(); late final PageController _pageController; + late final RotateIconController _rotateIconController; late final List _children; @@ -102,25 +104,11 @@ class _HomeViewState extends ConsumerState { @override void initState() { _pageController = PageController(); + _rotateIconController = RotateIconController(); _children = [ const WalletsView(), - if (Constants.enableExchange) - Stack( - children: [ - const ExchangeView(), - // ExchangeLoadingOverlayView( - // unawaitedLoad: _loadCNData, - // ), - ], - ), - if (Constants.enableBuy) - // Stack( - // children: [ - const BuyView(), - // BuyLoadingOverlayView( - // unawaitedLoad: _loadSimplexData, - // ), - // ], + if (Constants.enableExchange) const ExchangeView(), + if (Constants.enableExchange) const BuyView(), ]; ref.read(notificationsProvider).startCheckingWatchedNotifications(); @@ -131,6 +119,9 @@ class _HomeViewState extends ConsumerState { @override dispose() { _pageController.dispose(); + _rotateIconController.forward = null; + _rotateIconController.reverse = null; + _rotateIconController.reset = null; super.dispose(); } @@ -138,6 +129,8 @@ class _HomeViewState extends ConsumerState { int _hiddenCount = 0; void _hiddenOptions() { + _rotateIconController.reset?.call(); + _rotateIconController.forward?.call(); if (_hiddenCount == 5) { Navigator.of(context).pushNamed(HiddenSettings.routeName); } @@ -168,10 +161,15 @@ class _HomeViewState extends ConsumerState { children: [ GestureDetector( onTap: _hiddenOptions, - child: SvgPicture.asset( - Assets.svg.stackIcon(context), - width: 24, - height: 24, + child: RotateIcon( + icon: SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 24, + height: 24, + ), + curve: Curves.easeInOutCubic, + rotationPercent: 1.0, + controller: _rotateIconController, ), ), const SizedBox( diff --git a/lib/pages/home_view/sub_widgets/home_view_button_bar.dart b/lib/pages/home_view/sub_widgets/home_view_button_bar.dart index bf1caab77..f532cf3a2 100644 --- a/lib/pages/home_view/sub_widgets/home_view_button_bar.dart +++ b/lib/pages/home_view/sub_widgets/home_view_button_bar.dart @@ -111,7 +111,7 @@ class _HomeViewButtonBarState extends ConsumerState { // } }, child: Text( - "Exchange", + "Swap", style: STextStyles.button(context).copyWith( fontSize: 14, color: selectedIndex == 1 diff --git a/lib/pages/intro_view.dart b/lib/pages/intro_view.dart index 65990d64d..0883a5405 100644 --- a/lib/pages/intro_view.dart +++ b/lib/pages/intro_view.dart @@ -52,10 +52,10 @@ class _IntroViewState extends State { constraints: const BoxConstraints( maxWidth: 300, ), - child: Image( - image: AssetImage( - Assets.png.stack(context), - ), + child: SvgPicture.asset( + Assets.svg.stack(context), + width: isDesktop ? 324 : 266, + height: isDesktop ? 324 : 266, ), ), ), diff --git a/lib/pages/paynym/paynym_claim_view.dart b/lib/pages/paynym/paynym_claim_view.dart index 326983a78..8a66aa26d 100644 --- a/lib/pages/paynym/paynym_claim_view.dart +++ b/lib/pages/paynym/paynym_claim_view.dart @@ -117,7 +117,7 @@ class _PaynymClaimViewState extends ConsumerState { ), Image( image: AssetImage( - Assets.png.unclaimedPaynym, + Assets.svg.unclaimedPaynym, ), width: MediaQuery.of(context).size.width / 2, ), diff --git a/lib/pages/pinpad_views/lock_screen_view.dart b/lib/pages/pinpad_views/lock_screen_view.dart index 25ba910ff..2b9b12b79 100644 --- a/lib/pages/pinpad_views/lock_screen_view.dart +++ b/lib/pages/pinpad_views/lock_screen_view.dart @@ -14,7 +14,6 @@ import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/biometrics.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -130,6 +129,14 @@ class _LockscreenViewState extends ConsumerState { } } + @override + void didChangeDependencies() { + if (widget.isInitialAppLogin) { + unawaited(Assets.precache(context)); + } + super.didChangeDependencies(); + } + @override void initState() { _shakeController = ShakeController(); diff --git a/lib/pages/receive_view/addresses/address_card.dart b/lib/pages/receive_view/addresses/address_card.dart index 72aa5d03d..bd9e74902 100644 --- a/lib/pages/receive_view/addresses/address_card.dart +++ b/lib/pages/receive_view/addresses/address_card.dart @@ -21,13 +21,13 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; class AddressCard extends StatefulWidget { const AddressCard({ Key? key, - required this.address, + required this.addressId, required this.walletId, required this.coin, this.clipboard = const ClipboardWrapper(), }) : super(key: key); - final Address address; + final int addressId; final String walletId; final Coin coin; final ClipboardInterface clipboard; @@ -38,18 +38,23 @@ class AddressCard extends StatefulWidget { class _AddressCardState extends State { late Stream stream; + late final Address address; AddressLabel? label; @override void initState() { - label = MainDB.instance - .getAddressLabelSync(widget.walletId, widget.address.value); + address = MainDB.instance.isar.addresses + .where() + .idEqualTo(widget.addressId) + .findFirstSync()!; + + label = MainDB.instance.getAddressLabelSync(widget.walletId, address.value); Id? id = label?.id; if (id == null) { label = AddressLabel( walletId: widget.walletId, - addressString: widget.address.value, + addressString: address.value, value: "", ); id = MainDB.instance.putAddressLabelSync(label!); @@ -84,7 +89,7 @@ class _AddressCardState extends State { onTap: () { Navigator.of(context).pushNamed( EditAddressLabelView.routeName, - arguments: label!, + arguments: label!.id, ); }, ), @@ -97,7 +102,7 @@ class _AddressCardState extends State { children: [ Expanded( child: SelectableText( - widget.address.value, + address.value, style: STextStyles.itemSubtitle12(context), ), ) @@ -119,16 +124,18 @@ class _AddressCardState extends State { onPressed: () async { await widget.clipboard.setData( ClipboardData( - text: widget.address.value, - ), - ); - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, + text: address.value, ), ); + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } }, ), ), @@ -147,7 +154,7 @@ class _AddressCardState extends State { showDialog( context: context, builder: (context) => AddressQrPopup( - addressString: widget.address.value, + addressString: address.value, coin: widget.coin, clipboard: widget.clipboard, ), diff --git a/lib/pages/receive_view/addresses/edit_address_label_view.dart b/lib/pages/receive_view/addresses/edit_address_label_view.dart index ed6a610fe..35102e7f9 100644 --- a/lib/pages/receive_view/addresses/edit_address_label_view.dart +++ b/lib/pages/receive_view/addresses/edit_address_label_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/address_label.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -18,12 +19,12 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; class EditAddressLabelView extends ConsumerStatefulWidget { const EditAddressLabelView({ Key? key, - required this.addressLabel, + required this.addressLabelId, }) : super(key: key); static const String routeName = "/editAddressLabel"; - final AddressLabel addressLabel; + final int addressLabelId; @override ConsumerState createState() => @@ -36,11 +37,17 @@ class _EditAddressLabelViewState extends ConsumerState { late final bool isDesktop; + late AddressLabel addressLabel; + @override void initState() { isDesktop = Util.isDesktop; _labelFieldController = TextEditingController(); - _labelFieldController.text = widget.addressLabel.value; + addressLabel = MainDB.instance.isar.addressLabels + .where() + .idEqualTo(widget.addressLabelId) + .findFirstSync()!; + _labelFieldController.text = addressLabel.value; super.initState(); } @@ -195,7 +202,7 @@ class _EditAddressLabelViewState extends ConsumerState { label: "Save", onPressed: () async { await MainDB.instance.updateAddressLabel( - widget.addressLabel.copyWith( + addressLabel.copyWith( label: _labelFieldController.text, ), ); @@ -209,7 +216,7 @@ class _EditAddressLabelViewState extends ConsumerState { TextButton( onPressed: () async { await MainDB.instance.updateAddressLabel( - widget.addressLabel.copyWith( + addressLabel.copyWith( label: _labelFieldController.text, ), ); diff --git a/lib/pages/receive_view/addresses/wallet_addresses_view.dart b/lib/pages/receive_view/addresses/wallet_addresses_view.dart new file mode 100644 index 000000000..aec8f24c3 --- /dev/null +++ b/lib/pages/receive_view/addresses/wallet_addresses_view.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/pages/receive_view/addresses/address_card.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/widgets/toggle.dart'; + +class WalletAddressesView extends ConsumerStatefulWidget { + const WalletAddressesView({ + Key? key, + required this.walletId, + }) : super(key: key); + + static const String routeName = "/walletAddressesView"; + + final String walletId; + + @override + ConsumerState createState() => + _WalletAddressesViewState(); +} + +class _WalletAddressesViewState extends ConsumerState { + final bool isDesktop = Util.isDesktop; + + bool _showChange = false; + + @override + Widget build(BuildContext context) { + final coin = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId).coin)); + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.backgroundAppBar, + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "Wallet addresses", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ), + child: Column( + children: [ + SizedBox( + height: isDesktop ? 56 : 48, + width: isDesktop ? 490 : null, + child: Toggle( + key: UniqueKey(), + onColor: Theme.of(context).extension()!.popupBG, + onText: "Receiving", + offColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + offText: "Change", + isOn: _showChange, + onValueChanged: (value) { + setState(() { + _showChange = value; + }); + }, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ), + SizedBox( + height: isDesktop ? 20 : 16, + ), + Expanded( + child: FutureBuilder( + future: MainDB.instance + .getAddresses(widget.walletId) + .filter() + .group( + (q) => _showChange + ? q.subTypeEqualTo(AddressSubType.change) + : q + .subTypeEqualTo(AddressSubType.receiving) + .or() + .subTypeEqualTo(AddressSubType.paynymReceive), + ) + .and() + .not() + .typeEqualTo(AddressType.nonWallet) + .sortByDerivationIndex() + .idProperty() + .findAll(), + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.data != null) { + // listview + return ListView.separated( + itemCount: snapshot.data!.length, + separatorBuilder: (_, __) => Container( + height: 10, + ), + itemBuilder: (_, index) => AddressCard( + walletId: widget.walletId, + addressId: snapshot.data![index], + coin: coin, + ), + ); + } else { + return const Center( + child: LoadingIndicator( + height: 200, + width: 200, + ), + ); + } + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 073b2dfb8..94cec7c14 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -6,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/receive_view/addresses/receiving_addresses_view.dart'; +import 'package:stackwallet/pages/receive_view/addresses/wallet_addresses_view.dart'; import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; @@ -21,7 +21,6 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import 'package:tuple/tuple.dart'; class ReceiveView extends ConsumerStatefulWidget { const ReceiveView({ @@ -182,8 +181,8 @@ class _ReceiveViewState extends ConsumerState { onTap: () { Navigator.of(context).pop(); Navigator.of(context).pushNamed( - ReceivingAddressesView.routeName, - arguments: Tuple2(walletId, false), + WalletAddressesView.routeName, + arguments: walletId, ); }, child: RoundedWhiteContainer( diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 8ad9f90fc..ac7e41592 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -9,6 +9,7 @@ import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/sending_transaction_dialog.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; @@ -74,40 +75,58 @@ class _ConfirmTransactionViewState late final TextEditingController noteController; Future _attemptSend(BuildContext context) async { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); unawaited( showDialog( context: context, useSafeArea: false, barrierDismissible: false, builder: (context) { - return const SendingTransactionDialog(); + return SendingTransactionDialog( + coin: manager.coin, + ); }, ), ); + final time = Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); + + late String txid; + Future txidFuture; + final note = noteController.text; - final manager = - ref.read(walletsChangeNotifierProvider).getManager(walletId); try { - String txid; if (widget.isPaynymNotificationTransaction) { - txid = await (manager.wallet as PaynymWalletInterface) + txidFuture = (manager.wallet as PaynymWalletInterface) .broadcastNotificationTx(preparedTx: transactionInfo); } else if (widget.isPaynymTransaction) { - txid = await manager.confirmSend(txData: transactionInfo); + txidFuture = manager.confirmSend(txData: transactionInfo); } else { final coin = manager.coin; if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { - txid = await (manager.wallet as FiroWallet) + txidFuture = (manager.wallet as FiroWallet) .confirmSendPublic(txData: transactionInfo); } else { - txid = await manager.confirmSend(txData: transactionInfo); + txidFuture = manager.confirmSend(txData: transactionInfo); } } + final results = await Future.wait([ + txidFuture, + time, + ]); + + txid = results.first as String; + ref.refresh(desktopUseUTXOs); + // save note await ref .read(notesServiceChangeNotifierProvider(walletId)) diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index fb05f3a38..c7c63bac7 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -7,9 +7,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/models/send_view_auto_fill_data.dart'; import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; +import 'package:stackwallet/pages/coin_control/coin_control_view.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart'; @@ -43,9 +45,11 @@ import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:tuple/tuple.dart'; class SendView extends ConsumerStatefulWidget { const SendView({ @@ -104,6 +108,8 @@ class _SendViewState extends ConsumerState { Decimal? _cachedBalance; + Set selectedUTXOs = {}; + void _cryptoAmountChanged() async { if (!_cryptoAmountChangeLock) { final String cryptoAmount = cryptoAmountController.text; @@ -140,16 +146,58 @@ class _SendViewState extends ConsumerState { _updatePreviewButtonState(_address, _amountToSend); - // if (_amountToSend == null) { - // setState(() { - // _calculateFeesFuture = calculateFees(0); - // }); - // } else { - // setState(() { - // _calculateFeesFuture = - // calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); - // }); - // } + _cryptoAmountChangedFeeUpdateTimer?.cancel(); + _cryptoAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { + if (coin != Coin.epicCash && !_baseFocus.hasFocus) { + setState(() { + _calculateFeesFuture = calculateFees( + _amountToSend == null + ? 0 + : Format.decimalAmountToSatoshis( + _amountToSend!, + coin, + ), + ); + }); + } + }); + } + } + + final updateFeesTimerDuration = const Duration(milliseconds: 500); + + Timer? _cryptoAmountChangedFeeUpdateTimer; + Timer? _baseAmountChangedFeeUpdateTimer; + + void _baseAmountChanged() { + _baseAmountChangedFeeUpdateTimer?.cancel(); + _baseAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { + if (coin != Coin.epicCash && !_cryptoFocus.hasFocus) { + setState(() { + _calculateFeesFuture = calculateFees( + _amountToSend == null + ? 0 + : Format.decimalAmountToSatoshis( + _amountToSend!, + coin, + ), + ); + }); + } + }); + } + + int _currentFee = 0; + + void _setCurrentFee(String fee, bool shouldSetState) { + final value = Format.decimalAmountToSatoshis( + Decimal.parse(fee), + coin, + ); + if (shouldSetState) { + setState(() => _currentFee = value); + } else { + _currentFee = value; } } @@ -313,75 +361,96 @@ class _SendViewState extends ConsumerState { Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin); } - // confirm send all - if (amount == availableBalance) { - final bool? shouldSendAll = await showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Confirm send all", - message: - "You are about to send your entire balance. Would you like to continue?", - leftButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - "Yes", - style: STextStyles.button(context), - ), - onPressed: () { - Navigator.of(context).pop(true); - }, - ), - ); - }, - ); + final coinControlEnabled = + ref.read(prefsChangeNotifierProvider).enableCoinControl; - if (shouldSendAll == null || shouldSendAll == false) { - // cancel preview - return; + if (!(manager.hasCoinControlSupport && coinControlEnabled) || + (manager.hasCoinControlSupport && + coinControlEnabled && + selectedUTXOs.isEmpty)) { + // confirm send all + if (amount == availableBalance) { + bool? shouldSendAll; + if (mounted) { + shouldSendAll = await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Confirm send all", + message: + "You are about to send your entire balance. Would you like to continue?", + leftButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + child: Text( + "Yes", + style: STextStyles.button(context), + ), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ); + }, + ); + } + + if (shouldSendAll == null || shouldSendAll == false) { + // cancel preview + return; + } } } try { bool wasCancelled = false; - unawaited( - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; + if (mounted) { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + coin: manager.coin, + onCancel: () { + wasCancelled = true; - Navigator.of(context).pop(); - }, - ); - }, + Navigator.of(context).pop(); + }, + ); + }, + ), + ); + } + + final time = Future.delayed( + const Duration( + milliseconds: 2500, ), ); Map txData; + Future> txDataFuture; if (isPaynymSend) { final wallet = manager.wallet as PaynymWalletInterface; @@ -390,27 +459,48 @@ class _SendViewState extends ConsumerState { wallet.networkType, ); final feeRate = ref.read(feeRateTypeStateProvider); - txData = await wallet.preparePaymentCodeSend( + txDataFuture = wallet.preparePaymentCodeSend( paymentCode: paymentCode, satoshiAmount: amount, - args: {"feeRate": feeRate}, + args: { + "feeRate": feeRate, + "UTXOs": (manager.hasCoinControlSupport && + coinControlEnabled && + selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, + }, ); } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { - txData = await (manager.wallet as FiroWallet).prepareSendPublic( + txDataFuture = (manager.wallet as FiroWallet).prepareSendPublic( address: _address!, satoshiAmount: amount, args: {"feeRate": ref.read(feeRateTypeStateProvider)}, ); } else { - txData = await manager.prepareSend( + txDataFuture = manager.prepareSend( address: _address!, satoshiAmount: amount, - args: {"feeRate": ref.read(feeRateTypeStateProvider)}, + args: { + "feeRate": ref.read(feeRateTypeStateProvider), + "UTXOs": (manager.hasCoinControlSupport && + coinControlEnabled && + selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, + }, ); } + final results = await Future.wait([ + txDataFuture, + time, + ]); + + txData = results.first as Map; + if (!wasCancelled && mounted) { // pop building dialog Navigator.of(context).pop(); @@ -491,6 +581,7 @@ class _SendViewState extends ConsumerState { onCryptoAmountChanged = _cryptoAmountChanged; cryptoAmountController.addListener(onCryptoAmountChanged); + baseAmountController.addListener(_baseAmountChanged); if (_data != null) { if (_data!.amount != null) { @@ -506,43 +597,47 @@ class _SendViewState extends ConsumerState { noteController.text = "PayNym send"; } - if (coin != Coin.epicCash) { - _cryptoFocus.addListener(() { - if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { - if (_amountToSend == null) { - setState(() { - _calculateFeesFuture = calculateFees(0); - }); - } else { - setState(() { - _calculateFeesFuture = calculateFees( - Format.decimalAmountToSatoshis(_amountToSend!, coin)); - }); - } - } - }); + // if (coin != Coin.epicCash) { + // _cryptoFocus.addListener(() { + // if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { + // if (_amountToSend == null) { + // setState(() { + // _calculateFeesFuture = calculateFees(0); + // }); + // } else { + // setState(() { + // _calculateFeesFuture = calculateFees( + // Format.decimalAmountToSatoshis(_amountToSend!, coin)); + // }); + // } + // } + // }); - _baseFocus.addListener(() { - if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { - if (_amountToSend == null) { - setState(() { - _calculateFeesFuture = calculateFees(0); - }); - } else { - setState(() { - _calculateFeesFuture = calculateFees( - Format.decimalAmountToSatoshis(_amountToSend!, coin)); - }); - } - } - }); - } + // _baseFocus.addListener(() { + // if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { + // if (_amountToSend == null) { + // setState(() { + // _calculateFeesFuture = calculateFees(0); + // }); + // } else { + // setState(() { + // _calculateFeesFuture = calculateFees( + // Format.decimalAmountToSatoshis(_amountToSend!, coin)); + // }); + // } + // } + // }); + // } super.initState(); } @override void dispose() { + _cryptoAmountChangedFeeUpdateTimer?.cancel(); + _baseAmountChangedFeeUpdateTimer?.cancel(); + cryptoAmountController.removeListener(onCryptoAmountChanged); + baseAmountController.removeListener(_baseAmountChanged); sendToController.dispose(); cryptoAmountController.dispose(); @@ -565,6 +660,17 @@ class _SendViewState extends ConsumerState { final String locale = ref.watch( localeServiceChangeNotifierProvider.select((value) => value.locale)); + final showCoinControl = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(walletId).hasCoinControlSupport, + ), + ) && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.enableCoinControl, + ), + ); + if (coin == Coin.firo || coin == Coin.firoTestNet) { ref.listen(publicPrivateBalanceStateProvider, (previous, next) { if (_amountToSend == null) { @@ -580,6 +686,22 @@ class _SendViewState extends ConsumerState { }); } + // add listener for epic cash to strip http:// and https:// prefixes if the address also ocntains an @ symbol (indicating an epicbox address) + if (coin == Coin.epicCash) { + sendToController.addListener(() { + _address = sendToController.text; + + if (_address != null && _address!.isNotEmpty) { + _address = _address!.trim(); + if (_address!.contains("\n")) { + _address = _address!.substring(0, _address!.indexOf("\n")); + } + + sendToController.text = formatAddress(_address!); + } + }); + } + return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, @@ -739,9 +861,8 @@ class _SendViewState extends ConsumerState { locale: locale, decimalPlaces: 2, )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", - style: STextStyles - .titleBold12_400( - context) + style: STextStyles.subtitle( + context) .copyWith( fontSize: 8, ), @@ -904,6 +1025,12 @@ class _SendViewState extends ConsumerState { "\n")); } + if (coin == + Coin.epicCash) { + // strip http:// and https:// if content contains @ + content = formatAddress( + content); + } sendToController.text = content; _address = content; @@ -1484,6 +1611,82 @@ class _SendViewState extends ConsumerState { ), ), ), + if (showCoinControl) + const SizedBox( + height: 8, + ), + if (showCoinControl) + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Coin control", + style: + STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + CustomTextButton( + text: selectedUTXOs.isEmpty + ? "Select coins" + : "Selected coins (${selectedUTXOs.length})", + onTap: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 100), + ); + } + + if (mounted) { + final spendable = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .balance + .spendable; + + int? amount; + if (_amountToSend != null) { + amount = + Format.decimalAmountToSatoshis( + _amountToSend!, + coin, + ); + + if (spendable == amount) { + // this is now a send all + } else { + amount += _currentFee; + } + } + + final result = + await Navigator.of(context) + .pushNamed( + CoinControlView.routeName, + arguments: Tuple4( + walletId, + CoinControlViewType.use, + amount, + selectedUTXOs, + ), + ); + + if (result is Set) { + setState(() { + selectedUTXOs = result; + }); + } + } + }, + ), + ], + ), + ), const SizedBox( height: 12, ), @@ -1600,6 +1803,10 @@ class _SendViewState extends ConsumerState { .text) ?? Decimal.zero, updateChosen: (String fee) { + _setCurrentFee( + fee, + true, + ); setState(() { _calculateFeesFuture = Future(() => fee); @@ -1625,6 +1832,10 @@ class _SendViewState extends ConsumerState { ConnectionState .done && snapshot.hasData) { + _setCurrentFee( + snapshot.data! as String, + false, + ); return Text( "~${snapshot.data! as String} ${coin.ticker}", style: STextStyles @@ -1677,6 +1888,11 @@ class _SendViewState extends ConsumerState { ConnectionState .done && snapshot.hasData) { + _setCurrentFee( + snapshot.data! + as String, + false, + ); return Text( "~${snapshot.data! as String} ${coin.ticker}", style: STextStyles @@ -1754,3 +1970,22 @@ class _SendViewState extends ConsumerState { ); } } + +String formatAddress(String epicAddress) { + // strip http:// or https:// prefixes if the address contains an @ symbol (and is thus an epicbox address) + if ((epicAddress.startsWith("http://") || + epicAddress.startsWith("https://")) && + epicAddress.contains("@")) { + epicAddress = epicAddress.replaceAll("http://", ""); + epicAddress = epicAddress.replaceAll("https://", ""); + } + // strip mailto: prefix + if (epicAddress.startsWith("mailto:")) { + epicAddress = epicAddress.replaceAll("mailto:", ""); + } + // strip / suffix if the address contains an @ symbol (and is thus an epicbox address) + if (epicAddress.endsWith("/") && epicAddress.contains("@")) { + epicAddress = epicAddress.substring(0, epicAddress.length - 1); + } + return epicAddress; +} diff --git a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart index 6750e1b4c..6915493d5 100644 --- a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; @@ -11,9 +13,11 @@ class BuildingTransactionDialog extends StatefulWidget { const BuildingTransactionDialog({ Key? key, required this.onCancel, + required this.coin, }) : super(key: key); final VoidCallback onCancel; + final Coin coin; @override State createState() => _RestoringDialogState(); @@ -25,6 +29,7 @@ class _RestoringDialogState extends State late Animation _spinAnimation; late final VoidCallback onCancel; + @override void initState() { onCancel = widget.onCancel; @@ -63,16 +68,25 @@ class _RestoringDialogState extends State const SizedBox( height: 40, ), - RotationTransition( - turns: _spinAnimation, - child: SvgPicture.asset( - Assets.svg.arrowRotate, - color: - Theme.of(context).extension()!.accentColorDark, - width: 24, - height: 24, + if (Theme.of(context).extension()!.themeType == + ThemeType.chan) + Image( + image: AssetImage( + Assets.gif.kiss(widget.coin), + ), + ), + if (Theme.of(context).extension()!.themeType != + ThemeType.chan) + RotationTransition( + turns: _spinAnimation, + child: SvgPicture.asset( + Assets.svg.arrowRotate, + color: + Theme.of(context).extension()!.accentColorDark, + width: 24, + height: 24, + ), ), - ), const SizedBox( height: 40, ), @@ -90,34 +104,76 @@ class _RestoringDialogState extends State onWillPop: () async { return false; }, - child: StackDialog( - title: "Generating transaction", - // // TODO get message from design team - // message: "", - icon: RotationTransition( - turns: _spinAnimation, - child: SvgPicture.asset( - Assets.svg.arrowRotate, - color: - Theme.of(context).extension()!.accentColorDark, - width: 24, - height: 24, - ), - ), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), - ), - onPressed: () { - Navigator.of(context).pop(); - onCancel.call(); - }, - ), - ), + child: Theme.of(context).extension()!.themeType == + ThemeType.chan + ? StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Image( + image: AssetImage( + Assets.gif.kiss(widget.coin), + ), + ), + Text( + "Generating transaction", + textAlign: TextAlign.center, + style: STextStyles.pageTitleH2(context), + ), + const SizedBox( + height: 32, + ), + Row( + children: [ + const Spacer(), + Expanded( + child: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context).pop(); + onCancel.call(); + }, + ), + ) + ], + ), + ], + ), + ) + : StackDialog( + title: "Generating transaction", + icon: RotationTransition( + turns: _spinAnimation, + child: SvgPicture.asset( + Assets.svg.arrowRotate, + color: Theme.of(context) + .extension()! + .accentColorDark, + width: 24, + height: 24, + ), + ), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context).pop(); + onCancel.call(); + }, + ), + ), ); } } diff --git a/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart b/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart index e5c86fe2e..3d9928c34 100644 --- a/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -10,8 +12,11 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; class SendingTransactionDialog extends StatefulWidget { const SendingTransactionDialog({ Key? key, + required this.coin, }) : super(key: key); + final Coin coin; + @override State createState() => _RestoringDialogState(); } @@ -60,17 +65,24 @@ class _RestoringDialogState extends State const SizedBox( height: 40, ), - RotationTransition( - turns: _spinAnimation, - child: SvgPicture.asset( - Assets.svg.arrowRotate, - color: Theme.of(context) - .extension()! - .accentColorDark, - width: 24, - height: 24, - ), - ), + Theme.of(context).extension()!.themeType == + ThemeType.chan + ? Image( + image: AssetImage( + Assets.gif.kiss(widget.coin), + ), + ) + : RotationTransition( + turns: _spinAnimation, + child: SvgPicture.asset( + Assets.svg.arrowRotate, + color: Theme.of(context) + .extension()! + .accentColorDark, + width: 24, + height: 24, + ), + ), ], ), ), @@ -80,21 +92,43 @@ class _RestoringDialogState extends State onWillPop: () async { return false; }, - child: StackDialog( - title: "Sending transaction", - // // TODO get message from design team - // message: "", - icon: RotationTransition( - turns: _spinAnimation, - child: SvgPicture.asset( - Assets.svg.arrowRotate, - color: - Theme.of(context).extension()!.accentColorDark, - width: 24, - height: 24, - ), - ), - ), + child: Theme.of(context).extension()!.themeType == + ThemeType.chan + ? StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Image( + image: AssetImage( + Assets.gif.kiss(widget.coin), + ), + ), + Text( + "Sending transaction", + textAlign: TextAlign.center, + style: STextStyles.pageTitleH2(context), + ), + const SizedBox( + height: 32, + ), + ], + ), + ) + : StackDialog( + title: "Sending transaction", + icon: RotationTransition( + turns: _spinAnimation, + child: SvgPicture.asset( + Assets.svg.arrowRotate, + color: Theme.of(context) + .extension()! + .accentColorDark, + width: 24, + height: 24, + ), + ), + ), ); } } diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart index 06b798c36..a19ed8668 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart @@ -121,6 +121,53 @@ class AdvancedSettingsView extends StatelessWidget { const SizedBox( height: 8, ), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Enable coin control", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.enableCoinControl), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .enableCoinControl = newValue; + }, + ), + ), + ], + ), + ), + ); + }, + ), + ), + const SizedBox( + height: 8, + ), RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: Consumer( diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart index f855cf46f..8b4d5b1aa 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart @@ -3,15 +3,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/color_theme_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; -import 'package:stackwallet/utilities/theme/dark_colors.dart'; -import 'package:stackwallet/utilities/theme/forest_colors.dart'; -import 'package:stackwallet/utilities/theme/fruit_sorbet_colors.dart'; -import 'package:stackwallet/utilities/theme/light_colors.dart'; -import 'package:stackwallet/utilities/theme/ocean_breeze_colors.dart'; -import 'package:stackwallet/utilities/theme/oled_black_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -23,23 +18,6 @@ class AppearanceSettingsView extends ConsumerWidget { static const String routeName = "/appearanceSettings"; - String chooseThemeType(ThemeType type) { - switch (type) { - case ThemeType.light: - return "Light theme"; - case ThemeType.dark: - return "Dark theme"; - case ThemeType.oceanBreeze: - return "Ocean theme"; - case ThemeType.oledBlack: - return "Oled Black theme"; - case ThemeType.fruitSorbet: - return "Fruit Sorbet theme"; - case ThemeType.forest: - return "Forest theme"; - } - } - @override Widget build(BuildContext context, WidgetRef ref) { return Background( @@ -173,516 +151,115 @@ class AppearanceSettingsView extends ConsumerWidget { } } -class ThemeOptionsView extends ConsumerStatefulWidget { - const ThemeOptionsView({ - Key? key, - }) : super(key: key); +class ThemeOptionsView extends ConsumerWidget { + const ThemeOptionsView({Key? key}) : super(key: key); @override - ConsumerState createState() => _ThemeOptionsView(); -} - -class _ThemeOptionsView extends ConsumerState { - late String _selectedTheme; - - @override - void initState() { - _selectedTheme = - DB.instance.get(boxName: DB.boxNameTheme, key: "colorScheme") - as String? ?? - "light"; - - super.initState(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Column( children: [ - MaterialButton( - splashColor: Colors.transparent, - hoverColor: Colors.transparent, - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - DB.instance.put( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: ThemeType.light.name, - ); - ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - LightColors(), - ); - - setState(() { - _selectedTheme = "light"; - }); - }, - child: SizedBox( - width: 200, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - SizedBox( - width: 10, - height: 10, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: "light", - groupValue: _selectedTheme, - onChanged: (newValue) { - if (newValue is String && newValue == "light") { - DB.instance.put( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: ThemeType.light.name, - ); - ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - LightColors(), - ); - - setState(() { - _selectedTheme = "light"; - }); - } - }, - ), - ), - const SizedBox( - width: 14, - ), - Text( - "Light", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark2, - ), - ), - ], + for (int i = 0; i < (2 * ThemeType.values.length) - 1; i++) + (i % 2 == 1) + ? const SizedBox( + height: 10, + ) + : ThemeOption( + onPressed: () { + DB.instance.put( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.values[i ~/ 2].name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + ThemeType.values[i ~/ 2].colorTheme, + ); + Assets.precache(context); + }, + onChanged: (newValue) { + if (newValue == ThemeType.values[i ~/ 2]) { + DB.instance.put( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.values[i ~/ 2].name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + ThemeType.values[i ~/ 2].colorTheme, + ); + Assets.precache(context); + } + }, + value: ThemeType.values[i ~/ 2], + groupValue: + Theme.of(context).extension()!.themeType, ), - ], - ), - ), - ), - const SizedBox( - height: 10, - ), - MaterialButton( - splashColor: Colors.transparent, - hoverColor: Colors.transparent, - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - DB.instance.put( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: ThemeType.dark.name, - ); - ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - DarkColors(), - ); - - setState(() { - _selectedTheme = "dark"; - }); - }, - child: SizedBox( - width: 200, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - SizedBox( - width: 10, - height: 10, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: "dark", - groupValue: _selectedTheme, - onChanged: (newValue) { - if (newValue is String && newValue == "dark") { - DB.instance.put( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: ThemeType.dark.name, - ); - ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - DarkColors(), - ); - - setState(() { - _selectedTheme = "dark"; - }); - } - }, - ), - ), - const SizedBox( - width: 14, - ), - Text( - "Dark", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark2, - ), - ), - ], - ), - ], - ), - ), - ), - const SizedBox( - height: 10, - ), - MaterialButton( - splashColor: Colors.transparent, - hoverColor: Colors.transparent, - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - DB.instance.put( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: ThemeType.oceanBreeze.name, - ); - ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - OceanBreezeColors(), - ); - - setState(() { - _selectedTheme = "oceanBreeze"; - }); - }, - child: SizedBox( - width: 200, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - SizedBox( - width: 10, - height: 10, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: "oceanBreeze", - groupValue: _selectedTheme, - onChanged: (newValue) { - if (newValue is String && newValue == "oceanBreeze") { - DB.instance.put( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: ThemeType.oceanBreeze.name, - ); - ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - OceanBreezeColors(), - ); - - setState(() { - _selectedTheme = "oceanBreeze"; - }); - } - }, - ), - ), - const SizedBox( - width: 14, - ), - Text( - "Ocean Breeze", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark2, - ), - ), - ], - ), - ], - ), - ), - ), - const SizedBox( - height: 10, - ), - MaterialButton( - splashColor: Colors.transparent, - hoverColor: Colors.transparent, - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - DB.instance.put( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: ThemeType.oledBlack.name, - ); - ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - OledBlackColors(), - ); - - setState(() { - _selectedTheme = "oledBlack"; - }); - }, - child: SizedBox( - width: 200, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - SizedBox( - width: 10, - height: 10, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: "oledBlack", - groupValue: _selectedTheme, - onChanged: (newValue) { - if (newValue is String && newValue == "oledBlack") { - DB.instance.put( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: ThemeType.oledBlack.name, - ); - ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - OledBlackColors(), - ); - - setState(() { - _selectedTheme = "oledBlack"; - }); - } - }, - ), - ), - const SizedBox( - width: 14, - ), - Text( - "OLED Black", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark2, - ), - ), - ], - ), - ], - ), - ), - ), - const SizedBox( - height: 10, - ), - MaterialButton( - splashColor: Colors.transparent, - hoverColor: Colors.transparent, - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - DB.instance.put( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: ThemeType.fruitSorbet.name, - ); - ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - FruitSorbetColors(), - ); - - setState(() { - _selectedTheme = "fruitSorbet"; - }); - }, - child: SizedBox( - width: 200, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - SizedBox( - width: 10, - height: 10, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: "fruitSorbet", - groupValue: _selectedTheme, - onChanged: (newValue) { - if (newValue is String && newValue == "fruitSorbet") { - DB.instance.put( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: ThemeType.fruitSorbet.name, - ); - ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - FruitSorbetColors(), - ); - - setState(() { - _selectedTheme = "fruitSorbet"; - }); - } - }, - ), - ), - const SizedBox( - width: 14, - ), - Text( - "Fruit Sorbet", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark2, - ), - ), - ], - ), - ], - ), - ), - ), - const SizedBox( - height: 10, - ), - MaterialButton( - splashColor: Colors.transparent, - hoverColor: Colors.transparent, - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - DB.instance.put( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: ThemeType.forest.name, - ); - ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - ForestColors(), - ); - - setState(() { - _selectedTheme = "forest"; - }); - }, - child: SizedBox( - width: 200, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - SizedBox( - width: 10, - height: 10, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: "forest", - groupValue: _selectedTheme, - onChanged: (newValue) { - if (newValue is String && newValue == "forest") { - DB.instance.put( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: ThemeType.forest.name, - ); - ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - ForestColors(), - ); - - setState(() { - _selectedTheme = "forest"; - }); - } - }, - ), - ), - const SizedBox( - width: 14, - ), - Text( - "Forest", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark2, - ), - ), - ], - ), - ], - ), - ), - ), ], ); } } + +class ThemeOption extends StatelessWidget { + const ThemeOption( + {Key? key, + required this.onPressed, + required this.onChanged, + required this.value, + required this.groupValue}) + : super(key: key); + + final VoidCallback onPressed; + final void Function(Object?) onChanged; + final ThemeType value; + final ThemeType groupValue; + + @override + Widget build(BuildContext context) { + return MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: onPressed, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + SizedBox( + width: 10, + height: 10, + child: Radio( + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, + value: value, + groupValue: groupValue, + onChanged: onChanged, + ), + ), + const SizedBox( + width: 14, + ), + Text( + value.prettyName, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension()!.textDark2, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 3bc3e9a17..bd7ba53c1 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -57,11 +57,15 @@ class HiddenSettings extends StatelessWidget { .read(notificationsProvider) .delete(notifs[0], true); - unawaited(showFloatingFlushBar( - type: FlushBarType.success, - message: "Notification history deleted", - context: context, - )); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Notification history deleted", + context: context, + ), + ); + } }, child: RoundedWhiteContainer( child: Text( @@ -112,11 +116,15 @@ class HiddenSettings extends StatelessWidget { .read(debugServiceProvider) .deleteAllLogs(); - unawaited(showFloatingFlushBar( - type: FlushBarType.success, - message: "Debug Logs deleted", - context: context, - )); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Debug Logs deleted", + context: context, + ), + ); + } }, child: RoundedWhiteContainer( child: Text( @@ -261,9 +269,9 @@ class HiddenSettings extends StatelessWidget { // builder: (_) { // return StackDialogBase( // child: SizedBox( - // width: 200, + // width: 300, // child: Lottie.asset( - // Assets.lottie.test2, + // Assets.lottie.plain(Coin.bitcoincash), // ), // ), // ); @@ -274,8 +282,9 @@ class HiddenSettings extends StatelessWidget { // child: Text( // "Lottie test", // style: STextStyles.button(context).copyWith( - // color: Theme.of(context).extension()!.accentColorDark - // ), + // color: Theme.of(context) + // .extension()! + // .accentColorDark), // ), // ), // ), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 1816e9886..70824986d 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -126,6 +126,7 @@ class _NodeDetailsViewState extends ConsumerState { case Coin.litecoin: case Coin.dogecoin: case Coin.firo: + case Coin.particl: case Coin.bitcoinTestNet: case Coin.firoTestNet: case Coin.dogecoinTestNet: diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index e96258f3f..5d78e5de6 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'dart:convert'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/epicbox_config_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; @@ -356,10 +356,9 @@ class _EpiBoxInfoFormState extends ConsumerState { .getManager(widget.walletId) .wallet as EpicCashWallet; - wallet.getEpicBoxConfig().then((value) { - final config = jsonDecode(value); - hostController.text = config["domain"] as String; - portController.text = (config["port"] as int).toString(); + wallet.getEpicBoxConfig().then((EpicBoxConfigModel epicBoxConfig) { + hostController.text = epicBoxConfig.host; + portController.text = "${epicBoxConfig.port ?? 443}"; }); super.initState(); } diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index 05b885eff..f394e7ad1 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -12,7 +12,6 @@ import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; @@ -22,8 +21,6 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import '../providers/ui/color_theme_provider.dart'; - class StackPrivacyCalls extends ConsumerStatefulWidget { const StackPrivacyCalls({ Key? key, @@ -42,13 +39,11 @@ class _StackPrivacyCalls extends ConsumerState { late final bool isDesktop; late bool isEasy; late bool infoToggle; - late final bool usePNG; @override void initState() { isDesktop = Util.isDesktop; isEasy = ref.read(prefsChangeNotifierProvider).externalCalls; - usePNG = ref.read(colorThemeProvider.state).state == "fruitSorbet"; infoToggle = isEasy; super.initState(); } @@ -300,16 +295,10 @@ class _PrivacyToggleState extends ConsumerState { late bool externalCallsEnabled; late final bool isDesktop; - late final bool isSorbet; - late final bool isOcean; @override void initState() { isDesktop = Util.isDesktop; - isSorbet = ref.read(colorThemeProvider.state).state.themeType == - ThemeType.fruitSorbet; - isOcean = ref.read(colorThemeProvider.state).state.themeType == - ThemeType.oceanBreeze; // initial toggle state externalCallsEnabled = widget.externalCallsEnabled; super.initState(); @@ -357,17 +346,11 @@ class _PrivacyToggleState extends ConsumerState { // const SizedBox( // height: 10, // ), - (isSorbet) - ? Image.asset( - Assets.png.personaEasy(context), - width: 140, - height: 140, - ) - : SvgPicture.asset( - Assets.svg.personaEasy(context), - width: 140, - height: 140, - ), + SvgPicture.asset( + Assets.svg.personaEasy(context), + width: 140, + height: 140, + ), // if (isDesktop) // const SizedBox( // height: 12, @@ -468,17 +451,11 @@ class _PrivacyToggleState extends ConsumerState { const SizedBox( height: 10, ), - (isSorbet) - ? Image.asset( - Assets.png.personaIncognito(context), - width: 140, - height: 140, - ) - : SvgPicture.asset( - Assets.svg.personaIncognito(context), - width: 140, - height: 140, - ), + SvgPicture.asset( + Assets.svg.personaIncognito(context), + width: 140, + height: 140, + ), if (isDesktop) const SizedBox( height: 12, diff --git a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart index 7686bc3ab..de9d95f7e 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart @@ -1,7 +1,9 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -10,6 +12,13 @@ import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +enum _BalanceType { + available, + full, + privateAvailable, + privateFull; +} + class WalletBalanceToggleSheet extends ConsumerWidget { const WalletBalanceToggleSheet({ Key? key, @@ -25,21 +34,31 @@ class WalletBalanceToggleSheet extends ConsumerWidget { final coin = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManager(walletId).coin)); - Future? totalBalanceFuture; - Future? availableBalanceFuture; + final balance = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).balance)); + + _BalanceType _bal = + ref.watch(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available + ? _BalanceType.available + : _BalanceType.full; + + Balance? balanceSecondary; if (coin == Coin.firo || coin == Coin.firoTestNet) { - final firoWallet = ref - .watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletId))) - .wallet as FiroWallet; - totalBalanceFuture = Future(() => firoWallet.balance.getSpendable()); - availableBalanceFuture = - Future(() => firoWallet.balancePrivate.getSpendable()); - } else { - final manager = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletId))); - totalBalanceFuture = Future(() => manager.balance.getTotal()); - availableBalanceFuture = Future(() => manager.balance.getSpendable()); + balanceSecondary = ref + .watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(walletId).wallet as FiroWallet?, + ), + ) + ?.balancePrivate; + + if (ref.watch(publicPrivateBalanceStateProvider.state).state == + "Private") { + _bal = _bal == _BalanceType.available + ? _BalanceType.privateAvailable + : _BalanceType.privateFull; + } } return Container( @@ -90,271 +109,103 @@ class WalletBalanceToggleSheet extends ConsumerWidget { const SizedBox( height: 24, ), - RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), + BalanceSelector( + title: "Available balance", + coin: coin, + balance: balance.getSpendable(), onPressed: () { - final state = - ref.read(walletBalanceToggleStateProvider.state).state; - if (state != WalletBalanceToggleState.available) { - ref.read(walletBalanceToggleStateProvider.state).state = - WalletBalanceToggleState.available; - } + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.available; + ref.read(publicPrivateBalanceStateProvider.state).state = + "Public"; Navigator.of(context).pop(); }, - child: Container( - color: Colors.transparent, - padding: const EdgeInsets.all(8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: WalletBalanceToggleState.available, - groupValue: ref - .watch(walletBalanceToggleStateProvider.state) - .state, - onChanged: (_) { - ref - .read(walletBalanceToggleStateProvider.state) - .state = WalletBalanceToggleState.available; - Navigator.of(context).pop(); - }, - ), - ), - const SizedBox( - width: 12, - ), - if (coin != Coin.firo && coin != Coin.firoTestNet) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Available balance", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 2, - ), - FutureBuilder( - future: availableBalanceFuture, - builder: (fbContext, - AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData && - snapshot.data != null) { - return Text( - "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } else { - return Text( - "", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } - }), - ], - ), - if (coin == Coin.firo || coin == Coin.firoTestNet) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Private balance", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 2, - ), - FutureBuilder( - future: availableBalanceFuture, - builder: (fbContext, - AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData && - snapshot.data != null) { - return Text( - "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } else { - return Text( - "", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } - }), - ], - ), - ], - ), - ), + onChanged: (_) { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.available; + ref.read(publicPrivateBalanceStateProvider.state).state = + "Public"; + Navigator.of(context).pop(); + }, + value: _BalanceType.available, + groupValue: _bal, ), + if (balanceSecondary != null) + const SizedBox( + height: 12, + ), + if (balanceSecondary != null) + BalanceSelector( + title: "Available private balance", + coin: coin, + balance: balanceSecondary.getSpendable(), + onPressed: () { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.available; + ref.read(publicPrivateBalanceStateProvider.state).state = + "Private"; + Navigator.of(context).pop(); + }, + onChanged: (_) { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.available; + ref.read(publicPrivateBalanceStateProvider.state).state = + "Private"; + Navigator.of(context).pop(); + }, + value: _BalanceType.privateAvailable, + groupValue: _bal, + ), const SizedBox( height: 12, ), - RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), + BalanceSelector( + title: "Full balance", + coin: coin, + balance: balance.getTotal(), onPressed: () { - final state = - ref.read(walletBalanceToggleStateProvider.state).state; - if (state != WalletBalanceToggleState.full) { - ref.read(walletBalanceToggleStateProvider.state).state = - WalletBalanceToggleState.full; - } + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.full; + ref.read(publicPrivateBalanceStateProvider.state).state = + "Public"; Navigator.of(context).pop(); }, - child: Container( - color: Colors.transparent, - padding: const EdgeInsets.all(8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: WalletBalanceToggleState.full, - groupValue: ref - .watch(walletBalanceToggleStateProvider.state) - .state, - onChanged: (_) { - ref - .read(walletBalanceToggleStateProvider.state) - .state = WalletBalanceToggleState.full; - Navigator.of(context).pop(); - }, - ), - ), - const SizedBox( - width: 12, - ), - if (coin != Coin.firo && coin != Coin.firoTestNet) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Full balance", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 2, - ), - FutureBuilder( - future: totalBalanceFuture, - builder: (fbContext, - AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData && - snapshot.data != null) { - return Text( - "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } else { - return Text( - "", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } - }), - ], - ), - if (coin == Coin.firo || coin == Coin.firoTestNet) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Public balance", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 2, - ), - FutureBuilder( - future: totalBalanceFuture, - builder: (fbContext, - AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData && - snapshot.data != null) { - return Text( - "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } else { - return Text( - "", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } - }), - ], - ), - ], - ), - ), + onChanged: (_) { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.full; + ref.read(publicPrivateBalanceStateProvider.state).state = + "Public"; + Navigator.of(context).pop(); + }, + value: _BalanceType.full, + groupValue: _bal, ), + if (balanceSecondary != null) + const SizedBox( + height: 12, + ), + if (balanceSecondary != null) + BalanceSelector( + title: "Full private balance", + coin: coin, + balance: balanceSecondary.getTotal(), + onPressed: () { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.full; + ref.read(publicPrivateBalanceStateProvider.state).state = + "Private"; + Navigator.of(context).pop(); + }, + onChanged: (_) { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.full; + ref.read(publicPrivateBalanceStateProvider.state).state = + "Private"; + Navigator.of(context).pop(); + }, + value: _BalanceType.privateFull, + groupValue: _bal, + ), const SizedBox( height: 40, ), @@ -365,3 +216,80 @@ class WalletBalanceToggleSheet extends ConsumerWidget { ); } } + +class BalanceSelector extends StatelessWidget { + const BalanceSelector({ + Key? key, + required this.title, + required this.coin, + required this.balance, + required this.onPressed, + required this.onChanged, + required this.value, + required this.groupValue, + }) : super(key: key); + + final String title; + final Coin coin; + final Decimal balance; + final VoidCallback onPressed; + final void Function(T?) onChanged; + final T value; + final T? groupValue; + + @override + Widget build(BuildContext context) { + return RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: onPressed, + child: Container( + color: Colors.transparent, + padding: const EdgeInsets.all(8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, + value: value, + groupValue: groupValue, + onChanged: onChanged, + ), + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 2, + ), + Text( + "${balance.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", + style: STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ) + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart index 173bee009..e69de29bb 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart @@ -1,523 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; -import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; -import 'package:stackwallet/providers/global/paynym_api_provider.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart'; -import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; -import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/loading_indicator.dart'; - -class WalletNavigationBar extends ConsumerStatefulWidget { - const WalletNavigationBar({ - Key? key, - required this.onReceivePressed, - required this.onSendPressed, - required this.onExchangePressed, - required this.onBuyPressed, - required this.onTokensPressed, - required this.height, - required this.enableExchange, - required this.coin, - required this.walletId, - }) : super(key: key); - - final VoidCallback onReceivePressed; - final VoidCallback onSendPressed; - final VoidCallback onExchangePressed; - final VoidCallback onBuyPressed; - final VoidCallback onTokensPressed; - final double height; - final bool enableExchange; - final Coin coin; - final String walletId; - - @override - ConsumerState createState() => - _WalletNavigationBarState(); -} - -class _WalletNavigationBarState extends ConsumerState { - double scale = 0; - final duration = const Duration(milliseconds: 200); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - // const Spacer(), - - AnimatedScale( - scale: scale, - duration: duration, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // AnimatedOpacity( - // opacity: scale, - // duration: duration, - // child: GestureDetector( - // onTap: () {}, - // child: Container( - // padding: const EdgeInsets.all(16), - // width: 146, - // decoration: BoxDecoration( - // color: - // Theme.of(context).extension()!.popupBG, - // boxShadow: [ - // Theme.of(context) - // .extension()! - // .standardBoxShadow - // ], - // borderRadius: BorderRadius.circular( - // widget.height / 2.0, - // ), - // ), - // child: Row( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // Text( - // "Whirlpool", - // style: STextStyles.w600_12(context), - // ), - // ], - // ), - // ), - // ), - // ), - // const SizedBox( - // height: 8, - // ), - AnimatedOpacity( - opacity: scale, - duration: duration, - child: Consumer(builder: (context, ref, __) { - return GestureDetector( - onTap: () async { - setState(() { - scale = 0; - }); - unawaited( - showDialog( - context: context, - builder: (context) => const LoadingIndicator( - width: 100, - ), - ), - ); - - final manager = ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId); - - final paynymInterface = - manager.wallet as PaynymWalletInterface; - - final code = await paynymInterface.getPaymentCode( - DerivePathTypeExt.primaryFor(manager.coin)); - - final account = await ref - .read(paynymAPIProvider) - .nym(code.toString()); - - Logging.instance.log( - "my nym account: $account", - level: LogLevel.Info, - ); - - if (mounted) { - Navigator.of(context).pop(); - - // check if account exists and for matching code to see if claimed - if (account.value != null && - account.value!.codes.first.claimed) { - ref.read(myPaynymAccountStateProvider.state).state = - account.value!; - - await Navigator.of(context).pushNamed( - PaynymHomeView.routeName, - arguments: widget.walletId, - ); - } else { - await Navigator.of(context).pushNamed( - PaynymClaimView.routeName, - arguments: widget.walletId, - ); - } - } - }, - child: Container( - padding: const EdgeInsets.all(16), - width: 146, - decoration: BoxDecoration( - color: - Theme.of(context).extension()!.popupBG, - boxShadow: [ - Theme.of(context) - .extension()! - .standardBoxShadow - ], - borderRadius: BorderRadius.circular( - widget.height / 2.0, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Paynym", - style: STextStyles.buttonSmall(context), - ), - const SizedBox( - width: 16, - ), - SvgPicture.asset( - Assets.svg.robotHead, - height: 20, - width: 20, - color: Theme.of(context) - .extension()! - .bottomNavIconIcon, - ), - ], - ), - ), - ); - }), - ), - const SizedBox( - height: 8, - ), - ], - ), - ), - Container( - height: widget.height, - decoration: BoxDecoration( - color: Theme.of(context).extension()!.bottomNavBack, - boxShadow: [ - Theme.of(context).extension()!.standardBoxShadow - ], - borderRadius: BorderRadius.circular( - widget.height / 2.0, - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 4, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const SizedBox( - width: 12, - ), - RawMaterialButton( - constraints: const BoxConstraints( - minWidth: 66, - ), - onPressed: widget.onReceivePressed, - splashColor: - Theme.of(context).extension()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - widget.height / 2.0, - ), - ), - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer(), - Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .accentColorDark - .withOpacity(0.4), - borderRadius: BorderRadius.circular( - 24, - ), - ), - child: Padding( - padding: const EdgeInsets.all(6.0), - child: SvgPicture.asset( - Assets.svg.arrowDownLeft, - width: 12, - height: 12, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), - ), - const SizedBox( - height: 4, - ), - Text( - "Receive", - style: STextStyles.buttonSmall(context), - ), - const Spacer(), - ], - ), - ), - ), - ), - RawMaterialButton( - constraints: const BoxConstraints( - minWidth: 66, - ), - onPressed: widget.onSendPressed, - splashColor: - Theme.of(context).extension()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - widget.height / 2.0, - ), - ), - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer(), - Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .accentColorDark - .withOpacity(0.4), - borderRadius: BorderRadius.circular( - 24, - ), - ), - child: Padding( - padding: const EdgeInsets.all(6.0), - child: SvgPicture.asset( - Assets.svg.arrowUpRight, - width: 12, - height: 12, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), - ), - const SizedBox( - height: 4, - ), - Text( - "Send", - style: STextStyles.buttonSmall(context), - ), - const Spacer(), - ], - ), - ), - ), - ), - if (widget.coin == Coin.ethereum) - RawMaterialButton( - constraints: const BoxConstraints( - minWidth: 66, - ), - onPressed: widget.onTokensPressed, - splashColor: - Theme.of(context).extension()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - widget.height / 2.0, - ), - ), - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer(), - SvgPicture.asset( - Assets.svg.tokens, - color: Theme.of(context) - .extension()! - .accentColorDark, - width: 24, - height: 24, - ), - const SizedBox( - height: 4, - ), - Text( - "Tokens", - style: STextStyles.buttonSmall(context), - ), - const Spacer(), - ], - ), - ), - ), - ), - if (widget.enableExchange) - RawMaterialButton( - constraints: const BoxConstraints( - minWidth: 66, - ), - onPressed: widget.onExchangePressed, - splashColor: - Theme.of(context).extension()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - widget.height / 2.0, - ), - ), - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer(), - SvgPicture.asset( - Assets.svg.exchange(context), - width: 24, - height: 24, - ), - const SizedBox( - height: 4, - ), - Text( - "Exchange", - style: STextStyles.buttonSmall(context), - ), - const Spacer(), - ], - ), - ), - ), - ), - if (widget.coin.hasBuySupport) - RawMaterialButton( - constraints: const BoxConstraints( - minWidth: 66, - ), - onPressed: widget.onBuyPressed, - splashColor: - Theme.of(context).extension()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - widget.height / 2.0, - ), - ), - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer(), - SvgPicture.asset( - Assets.svg.buy(context), - width: 24, - height: 24, - ), - const SizedBox( - height: 4, - ), - Text( - "Buy", - style: STextStyles.buttonSmall(context), - ), - const Spacer(), - ], - ), - ), - ), - ), - if (ref.watch(walletsChangeNotifierProvider.select((value) => - value.getManager(widget.walletId).hasPaynymSupport))) - RawMaterialButton( - constraints: const BoxConstraints( - minWidth: 66, - ), - onPressed: () { - if (scale == 0) { - setState(() { - scale = 1; - }); - } else if (scale == 1) { - setState(() { - scale = 0; - }); - } - }, - splashColor: - Theme.of(context).extension()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - widget.height / 2.0, - ), - ), - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer(), - const SizedBox( - height: 2, - ), - SvgPicture.asset( - Assets.svg.bars, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .bottomNavIconIcon, - ), - const SizedBox( - height: 6, - ), - Text( - "More", - style: STextStyles.buttonSmall(context), - ), - const Spacer(), - ], - ), - ), - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - ], - ); - } -} diff --git a/lib/pages/wallet_view/sub_widgets/wallet_summary.dart b/lib/pages/wallet_view/sub_widgets/wallet_summary.dart index 2a5beb705..a8928b5d9 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary.dart @@ -48,8 +48,10 @@ class WalletSummary extends StatelessWidget { builder: (_, ref, __) { return Container( decoration: BoxDecoration( - color: Theme.of(context).extension()!.colorForCoin(ref - .watch(managerProvider.select((value) => value.coin))), + color: Theme.of(context) + .extension()! + .colorForCoin(ref.watch( + managerProvider.select((value) => value.coin))), borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -112,7 +114,6 @@ class WalletSummary extends StatelessWidget { padding: const EdgeInsets.all(16.0), child: WalletSummaryInfo( walletId: walletId, - managerProvider: managerProvider, initialSyncStatus: initialSyncStatus, ), ), diff --git a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart index 66e47440b..6339692f5 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -7,35 +9,34 @@ import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button. import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; -import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/services/event_bus/events/global/balance_refreshed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/animated_text.dart'; -class WalletSummaryInfo extends StatefulWidget { +import '../../../providers/wallet/public_private_balance_state_provider.dart'; + +class WalletSummaryInfo extends ConsumerStatefulWidget { const WalletSummaryInfo({ Key? key, required this.walletId, - required this.managerProvider, required this.initialSyncStatus, }) : super(key: key); final String walletId; - final ChangeNotifierProvider managerProvider; final WalletSyncStatus initialSyncStatus; @override - State createState() => _WalletSummaryInfoState(); + ConsumerState createState() => _WalletSummaryInfoState(); } -class _WalletSummaryInfoState extends State { - late final String walletId; - late final ChangeNotifierProvider managerProvider; +class _WalletSummaryInfoState extends ConsumerState { + late StreamSubscription _balanceUpdated; void showSheet() { showModalBottomSheet( @@ -46,251 +47,151 @@ class _WalletSummaryInfoState extends State { top: Radius.circular(20), ), ), - builder: (_) => WalletBalanceToggleSheet(walletId: walletId), + builder: (_) => WalletBalanceToggleSheet(walletId: widget.walletId), ); } - Decimal? _balanceTotalCached; - Decimal? _balanceCached; - @override void initState() { - walletId = widget.walletId; - managerProvider = widget.managerProvider; + _balanceUpdated = + GlobalEventBus.instance.on().listen( + (event) async { + if (event.walletId == widget.walletId) { + setState(() {}); + } + }, + ); super.initState(); } + @override + void dispose() { + _balanceUpdated.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + + final externalCalls = ref.watch( + prefsChangeNotifierProvider.select((value) => value.externalCalls)); + final coin = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId).coin)); + final balance = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId).balance)); + + final locale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale)); + + final baseCurrency = ref + .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + + final priceTuple = ref.watch(priceAnd24hChangeNotifierProvider + .select((value) => value.getPrice(coin))); + + final _showAvailable = + ref.watch(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available; + + final Decimal balanceToShow; + String title; + + if (coin == Coin.firo || coin == Coin.firoTestNet) { + final _showPrivate = + ref.watch(publicPrivateBalanceStateProvider.state).state == "Private"; + + final firoWallet = ref.watch(walletsChangeNotifierProvider.select( + (value) => value.getManager(widget.walletId).wallet)) as FiroWallet; + + final bal = _showPrivate ? firoWallet.balancePrivate : firoWallet.balance; + + balanceToShow = _showAvailable ? bal.getSpendable() : bal.getTotal(); + title = _showAvailable ? "Available" : "Full"; + title += _showPrivate ? " private balance" : " public balance"; + } else { + balanceToShow = + _showAvailable ? balance.getSpendable() : balance.getTotal(); + title = _showAvailable ? "Available balance" : "Full balance"; + } + return Row( children: [ Expanded( - child: Consumer( - builder: (_, ref, __) { - final Coin coin = - ref.watch(managerProvider.select((value) => value.coin)); - final externalCalls = ref.watch(prefsChangeNotifierProvider - .select((value) => value.externalCalls)); - - Future? totalBalanceFuture; - Future? availableBalanceFuture; - if (coin == Coin.firo || coin == Coin.firoTestNet) { - final firoWallet = - ref.watch(managerProvider.select((value) => value.wallet)) - as FiroWallet; - totalBalanceFuture = - Future(() => firoWallet.balance.getSpendable()); - availableBalanceFuture = - Future(() => firoWallet.balancePrivate.getSpendable()); - } else { - final manager = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletId))); - totalBalanceFuture = Future(() => manager.balance.getTotal()); - availableBalanceFuture = - Future(() => manager.balance.getSpendable()); - } - - final locale = ref.watch(localeServiceChangeNotifierProvider - .select((value) => value.locale)); - - final baseCurrency = ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)); - - final priceTuple = ref.watch(priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin))); - - final _showAvailable = - ref.watch(walletBalanceToggleStateProvider.state).state == - WalletBalanceToggleState.available; - - return FutureBuilder( - future: _showAvailable - ? availableBalanceFuture - : totalBalanceFuture, - builder: (fbContext, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData && - snapshot.data != null) { - if (_showAvailable) { - _balanceCached = snapshot.data!; - } else { - _balanceTotalCached = snapshot.data!; - } - } - Decimal? balanceToShow = - _showAvailable ? _balanceCached : _balanceTotalCached; - - if (balanceToShow != null) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: showSheet, - child: Row( - children: [ - if (coin == Coin.firo || coin == Coin.firoTestNet) - Text( - "${_showAvailable ? "Private" : "Public"} Balance", - style: - STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, - ), - ), - if (coin != Coin.firo && coin != Coin.firoTestNet) - Text( - "${_showAvailable ? "Available" : "Full"} Balance", - style: - STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, - ), - ), - const SizedBox( - width: 4, - ), - SvgPicture.asset( - Assets.svg.chevronDown, - color: Theme.of(context) - .extension()! - .textFavoriteCard, - width: 8, - height: 4, - ), - ], - ), - ), - const Spacer(), - FittedBox( - fit: BoxFit.scaleDown, - child: SelectableText( - "${Format.localizedStringAsFixed( - value: balanceToShow, - locale: locale, - decimalPlaces: 8, - )} ${coin.ticker}", - style: STextStyles.pageTitleH1(context).copyWith( - fontSize: 24, - color: Theme.of(context) - .extension()! - .textFavoriteCard, - ), - ), - ), - if (externalCalls) - Text( - "${Format.localizedStringAsFixed( - value: priceTuple.item1 * balanceToShow, - locale: locale, - decimalPlaces: 2, - )} $baseCurrency", - style: STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, - ), - ), - ], - ); - } else { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: showSheet, - child: Row( - children: [ - if (coin == Coin.firo || coin == Coin.firoTestNet) - Text( - "${_showAvailable ? "Private" : "Public"} Balance", - style: - STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, - ), - ), - if (coin != Coin.firo && coin != Coin.firoTestNet) - Text( - "${_showAvailable ? "Available" : "Full"} Balance", - style: - STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, - ), - ), - const SizedBox( - width: 4, - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension()! - .textFavoriteCard, - ), - ], - ), - ), - const Spacer(), - AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance..." - ], - style: STextStyles.pageTitleH1(context).copyWith( - fontSize: 24, - color: Theme.of(context) - .extension()! - .textFavoriteCard, - ), - ), - AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance..." - ], - style: STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, - ), - ), - ], - ); - } - }, - ); - }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: showSheet, + child: Row( + children: [ + Text( + title, + style: STextStyles.subtitle500(context).copyWith( + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + const SizedBox( + width: 4, + ), + SvgPicture.asset( + Assets.svg.chevronDown, + color: Theme.of(context) + .extension()! + .textFavoriteCard, + width: 8, + height: 4, + ), + ], + ), + ), + const Spacer(), + FittedBox( + fit: BoxFit.scaleDown, + child: SelectableText( + "${Format.localizedStringAsFixed( + value: balanceToShow, + locale: locale, + decimalPlaces: 8, + )} ${coin.ticker}", + style: STextStyles.pageTitleH1(context).copyWith( + fontSize: 24, + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + ), + if (externalCalls) + Text( + "${Format.localizedStringAsFixed( + value: priceTuple.item1 * balanceToShow, + locale: locale, + decimalPlaces: 2, + )} $baseCurrency", + style: STextStyles.subtitle500(context).copyWith( + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + ], ), ), Column( children: [ - Consumer( - builder: (_, ref, __) { - return SvgPicture.asset( - Assets.svg.iconFor( - coin: ref.watch( - managerProvider.select((value) => value.coin), - ), - ), - width: 24, - height: 24, - ); - }, + SvgPicture.asset( + Assets.svg.iconFor( + coin: coin, + ), + width: 24, + height: 24, ), const Spacer(), WalletRefreshButton( - walletId: walletId, + walletId: widget.walletId, initialSyncStatus: widget.initialSyncStatus, ), ], diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index c9ba6181b..5b72331a3 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -814,6 +814,8 @@ class _DesktopTransactionCardRowState } else { return "Sending"; } + } else if (type == TransactionType.sentToSelf) { + return "Sent to self"; } else { return type.name; } diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 5b737a939..77decdfcb 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -127,6 +127,8 @@ class _TransactionDetailsViewState } else { return "Sending"; } + } else if (type == TransactionType.sentToSelf) { + return "Sent to self"; } else { return type.name; } diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index f222550ff..083b79990 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -5,24 +5,31 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; +import 'package:stackwallet/pages/coin_control/coin_control_view.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/notification_views/notifications_view.dart'; +import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; +import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart'; -import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; -import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; -import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart'; -import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_summary.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settingew/transaction_views/all_transactions_view.dart'; +import 'package:stackwallet/pages/coin_control/coin_control_view.dart'; +import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; +import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; +import 'package:stackwallet/providers/global/paynym_api_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; import 'package:stackwallet/providers/ui/unread_notifications_provider.dart'; +import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; @@ -30,11 +37,15 @@ import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; +import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; +import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; @@ -42,7 +53,16 @@ import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart'; +import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/coin_control_nav_icon.dart'; +import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/exchange_nav_icon.dart'; +import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/paynym_nav_icon.dart'; +import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/receive_nav_icon.dart'; +import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/send_nav_icon.dart'; +import 'package:stackwallet/widgets/wallet_navigation_bar/components/wallet_navigation_bar_item.dart'; +import 'package:stackwallet/widgets/wallet_navigation_bar/wallet_navigation_bar.dart'; import 'package:tuple/tuple.dart'; /// [eventBus] should only be set during testing @@ -240,19 +260,14 @@ class _WalletViewState extends ConsumerState { } void _onExchangePressed(BuildContext context) async { - // too expensive - // unawaited(ExchangeDataLoadingService.instance.loadAll(ref)); - final coin = ref.read(managerProvider).coin; - if (coin == Coin.epicCash) { - await showDialog( - context: context, - builder: (_) => const StackOkDialog( - title: "Exchange not available for Epic Cash", - ), - ); - } else if (coin.name.endsWith("TestNet")) { + final currency = ExchangeDataLoadingService.instance.isar.currencies + .where() + .tickerEqualToAnyExchangeNameName(coin.ticker) + .findFirstSync(); + + if (coin.isTestNet) { await showDialog( context: context, builder: (_) => const StackOkDialog( @@ -266,7 +281,7 @@ class _WalletViewState extends ConsumerState { WalletInitiatedExchangeView.routeName, arguments: Tuple2( walletId, - coin, + currency == null ? Coin.bitcoin : coin, ), ), ); @@ -274,6 +289,28 @@ class _WalletViewState extends ConsumerState { } } + void _onBuyPressed(BuildContext context) async { + final coin = ref.read(managerProvider).coin; + + if (coin.isTestNet) { + await showDialog( + context: context, + builder: (_) => const StackOkDialog( + title: "Buy not available for test net coins", + ), + ); + } else { + if (mounted) { + unawaited( + Navigator.of(context).pushNamed( + BuyInWalletView.routeName, + arguments: coin.hasBuySupport ? coin : Coin.bitcoin, + ), + ); + } + } + } + Future attemptAnonymize() async { bool shouldPop = false; unawaited( @@ -371,297 +408,303 @@ class _WalletViewState extends ConsumerState { child: WillPopScope( onWillPop: _onWillPop, child: Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - _logout(); - Navigator.of(context).pop(); - }, - ), - titleSpacing: 0, - title: Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - // color: Theme.of(context).extension()!.accentColorDark - width: 24, - height: 24, + child: Stack( + children: [ + Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + _logout(); + Navigator.of(context).pop(); + }, ), - const SizedBox( - width: 16, - ), - Expanded( - child: Text( - ref.watch( - managerProvider.select((value) => value.walletName)), - style: STextStyles.navBarTitle(context), - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("walletViewRadioButton"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension()! - .background, - icon: _buildNetworkIcon(_currentSyncStatus), - onPressed: () { - Navigator.of(context).pushNamed( - WalletNetworkSettingsView.routeName, - arguments: Tuple3( - walletId, - _currentSyncStatus, - _currentNodeStatus, - ), - ); - }, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("walletViewAlertsButton"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension()! - .background, - icon: SvgPicture.asset( - ref.watch(notificationsProvider.select((value) => - value.hasUnreadNotificationsFor(walletId))) - ? Assets.svg.bellNew(context) - : Assets.svg.bell, - width: 20, - height: 20, - color: ref.watch(notificationsProvider.select((value) => - value.hasUnreadNotificationsFor(walletId))) - ? null - : Theme.of(context) - .extension()! - .topNavIconPrimary, + titleSpacing: 0, + title: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + // color: Theme.of(context).extension()!.accentColorDark + width: 24, + height: 24, ), - onPressed: () { - // reset unread state - ref.refresh(unreadNotificationsStateProvider); - - Navigator.of(context) - .pushNamed( - NotificationsView.routeName, - arguments: walletId, - ) - .then((_) { - final Set unreadNotificationIds = ref - .read(unreadNotificationsStateProvider.state) - .state; - if (unreadNotificationIds.isEmpty) return; - - List> futures = []; - for (int i = 0; - i < unreadNotificationIds.length - 1; - i++) { - futures.add(ref - .read(notificationsProvider) - .markAsRead( - unreadNotificationIds.elementAt(i), false)); - } - - // wait for multiple to update if any - Future.wait(futures).then((_) { - // only notify listeners once - ref - .read(notificationsProvider) - .markAsRead(unreadNotificationIds.last, true); - }); - }); - }, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("walletViewSettingsButton"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension()! - .background, - icon: SvgPicture.asset( - Assets.svg.bars, - color: Theme.of(context) - .extension()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () { - //todo: check if print needed - // debugPrint("wallet view settings tapped"); - Navigator.of(context).pushNamed( - WalletSettingsView.routeName, - arguments: Tuple4( - walletId, - ref.read(managerProvider).coin, - _currentSyncStatus, - _currentNodeStatus, - ), - ); - }, - ), - ), - ), - ], - ), - body: SafeArea( - child: Container( - color: Theme.of(context).extension()!.background, - child: Column( - children: [ - const SizedBox( - height: 10, - ), - Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: WalletSummary( - walletId: walletId, - managerProvider: managerProvider, - initialSyncStatus: ref.watch(managerProvider - .select((value) => value.isRefreshing)) - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, - ), - ), - ), - if (coin == Coin.firo) const SizedBox( - height: 10, + width: 16, ), - if (coin == Coin.firo) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - onPressed: () async { - await showDialog( - context: context, - builder: (context) => StackDialog( - title: "Attention!", - message: - "You're about to anonymize all of your public funds.", - leftButton: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - "Cancel", - style: STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), - ), - rightButton: TextButton( - onPressed: () async { - Navigator.of(context).pop(); - - unawaited(attemptAnonymize()); - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle( - context), - child: Text( - "Continue", - style: STextStyles.button(context), - ), - ), - ), - ); - }, - child: Text( - "Anonymize funds", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, - ), - ), + Expanded( + child: Text( + ref.watch(managerProvider + .select((value) => value.walletName)), + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("walletViewRadioButton"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension()! + .background, + icon: _buildNetworkIcon(_currentSyncStatus), + onPressed: () { + Navigator.of(context).pushNamed( + WalletNetworkSettingsView.routeName, + arguments: Tuple3( + walletId, + _currentSyncStatus, + _currentNodeStatus, ), - ), - ], + ); + }, ), ), - const SizedBox( - height: 20, ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transactions", - style: STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("walletViewAlertsButton"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension()! + .background, + icon: SvgPicture.asset( + ref.watch(notificationsProvider.select((value) => + value.hasUnreadNotificationsFor(walletId))) + ? Assets.svg.bellNew(context) + : Assets.svg.bell, + width: 20, + height: 20, + color: ref.watch(notificationsProvider.select( + (value) => value + .hasUnreadNotificationsFor(walletId))) + ? null + : Theme.of(context) + .extension()! + .topNavIconPrimary, ), - CustomTextButton( - text: "See all", - onTap: () { - Navigator.of(context).pushNamed( - AllTransactionsView.routeName, - arguments: walletId, - ); - }, - ), - ], + onPressed: () { + // reset unread state + ref.refresh(unreadNotificationsStateProvider); + + Navigator.of(context) + .pushNamed( + NotificationsView.routeName, + arguments: walletId, + ) + .then((_) { + final Set unreadNotificationIds = ref + .read(unreadNotificationsStateProvider.state) + .state; + if (unreadNotificationIds.isEmpty) return; + + List> futures = []; + for (int i = 0; + i < unreadNotificationIds.length - 1; + i++) { + futures.add(ref + .read(notificationsProvider) + .markAsRead( + unreadNotificationIds.elementAt(i), + false)); + } + + // wait for multiple to update if any + Future.wait(futures).then((_) { + // only notify listeners once + ref.read(notificationsProvider).markAsRead( + unreadNotificationIds.last, true); + }); + }); + }, + ), ), ), - const SizedBox( - height: 12, + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("walletViewSettingsButton"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension()! + .background, + icon: SvgPicture.asset( + Assets.svg.bars, + color: Theme.of(context) + .extension()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + //todo: check if print needed + // debugPrint("wallet view settings tapped"); + Navigator.of(context).pushNamed( + WalletSettingsView.routeName, + arguments: Tuple4( + walletId, + ref.read(managerProvider).coin, + _currentSyncStatus, + _currentNodeStatus, + ), + ); + }, + ), + ), ), - Expanded( - child: Stack( - children: [ + ], + ), + body: SafeArea( + child: Container( + color: + Theme.of(context).extension()!.background, + child: Column( + children: [ + const SizedBox( + height: 10, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: WalletSummary( + walletId: walletId, + managerProvider: managerProvider, + initialSyncStatus: ref.watch(managerProvider + .select((value) => value.isRefreshing)) + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ), + ), + ), + if (coin == Coin.firo) + const SizedBox( + height: 10, + ), + if (coin == Coin.firo) Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle( + context), + onPressed: () async { + await showDialog( + context: context, + builder: (context) => StackDialog( + title: "Attention!", + message: + "You're about to anonymize all of your public funds.", + leftButton: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + "Cancel", + style: STextStyles.button(context) + .copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + ), + rightButton: TextButton( + onPressed: () async { + Navigator.of(context).pop(); + + unawaited(attemptAnonymize()); + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle( + context), + child: Text( + "Continue", + style: + STextStyles.button(context), + ), + ), + ), + ); + }, + child: Text( + "Anonymize funds", + style: + STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + ), + ), + ), + ], + ), + ), + const SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transactions", + style: + STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + ), + CustomTextButton( + text: "See all", + onTap: () { + Navigator.of(context).pushNamed( + AllTransactionsView.routeName, + arguments: walletId, + ); + }, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + Expanded( + child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: ClipRRect( borderRadius: BorderRadius.vertical( @@ -714,104 +757,159 @@ class _WalletViewState extends ConsumerState { ), ), ), - Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only( - bottom: 14, - left: 16, - right: 16, - ), - child: WalletNavigationBar( - walletId: widget.walletId, - coin: ref.watch(managerProvider - .select((value) => value.coin)), - enableExchange: - Constants.enableExchange && - ref.watch(managerProvider.select( - (value) => value.coin)) != - Coin.epicCash, - height: WalletView.navBarHeight, - onExchangePressed: () => - _onExchangePressed(context), - onReceivePressed: () async { - final coin = - ref.read(managerProvider).coin; - if (mounted) { - unawaited( - Navigator.of(context).pushNamed( - ReceiveView.routeName, - arguments: Tuple2( - walletId, - coin, - ), - )); - } - }, - onSendPressed: () { - final walletId = - ref.read(managerProvider).walletId; - final coin = - ref.read(managerProvider).coin; - switch (ref - .read( - walletBalanceToggleStateProvider - .state) - .state) { - case WalletBalanceToggleState.full: - ref - .read( - publicPrivateBalanceStateProvider - .state) - .state = "Public"; - break; - case WalletBalanceToggleState - .available: - ref - .read( - publicPrivateBalanceStateProvider - .state) - .state = "Private"; - break; - } - Navigator.of(context).pushNamed( - SendView.routeName, - arguments: Tuple2( - walletId, - coin, - ), - ); - }, - onBuyPressed: () { - unawaited( - Navigator.of(context).pushNamed( - BuyInWalletView.routeName, - arguments: coin, - )); - }, - onTokensPressed: () { - Navigator.of(context).pushNamed( - MyTokensView.routeName, - arguments: walletId, - ); - }, - ), - ), - ], - ), - ], - ) - ], - ), + ), + ], ), - ], + ), ), ), - ), + WalletNavigationBar( + items: [ + WalletNavigationBarItemData( + label: "Receive", + icon: const ReceiveNavIcon(), + onTap: () { + final coin = ref.read(managerProvider).coin; + if (mounted) { + unawaited( + Navigator.of(context).pushNamed( + ReceiveView.routeName, + arguments: Tuple2( + walletId, + coin, + ), + ), + ); + } + }, + ), + WalletNavigationBarItemData( + label: "Send", + icon: const SendNavIcon(), + onTap: () { + final walletId = ref.read(managerProvider).walletId; + final coin = ref.read(managerProvider).coin; + switch (ref + .read(walletBalanceToggleStateProvider.state) + .state) { + case WalletBalanceToggleState.full: + ref + .read(publicPrivateBalanceStateProvider.state) + .state = "Public"; + break; + case WalletBalanceToggleState.available: + ref + .read(publicPrivateBalanceStateProvider.state) + .state = "Private"; + break; + } + Navigator.of(context).pushNamed( + SendView.routeName, + arguments: Tuple2( + walletId, + coin, + ), + ); + }, + ), + if (Constants.enableExchange) + WalletNavigationBarItemData( + label: "Swap", + icon: const ExchangeNavIcon(), + onTap: () => _onExchangePressed(context), + ), + if (Constants.enableExchange) + WalletNavigationBarItemData( + label: "Buy", + icon: const BuyNavIcon(), + onTap: () => _onBuyPressed(context), + ), + ], + moreItems: [ + if (ref.watch( + walletsChangeNotifierProvider.select( + (value) => value + .getManager(widget.walletId) + .hasCoinControlSupport, + ), + ) && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.enableCoinControl, + ), + )) + WalletNavigationBarItemData( + label: "Coin control", + icon: const CoinControlNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + CoinControlView.routeName, + arguments: Tuple2( + widget.walletId, + CoinControlViewType.manage, + ), + ); + }, + ), + if (ref.watch(walletsChangeNotifierProvider.select((value) => + value.getManager(widget.walletId).hasPaynymSupport))) + WalletNavigationBarItemData( + label: "PayNym", + icon: const PaynymNavIcon(), + onTap: () async { + unawaited( + showDialog( + context: context, + builder: (context) => const LoadingIndicator( + width: 100, + ), + ), + ); + + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId); + + final paynymInterface = + manager.wallet as PaynymWalletInterface; + + final code = await paynymInterface.getPaymentCode( + DerivePathTypeExt.primaryFor(manager.coin)); + + final account = await ref + .read(paynymAPIProvider) + .nym(code.toString()); + + Logging.instance.log( + "my nym account: $account", + level: LogLevel.Info, + ); + + if (mounted) { + Navigator.of(context).pop(); + + // check if account exists and for matching code to see if claimed + if (account.value != null && + account.value!.codes.first.claimed) { + ref.read(myPaynymAccountStateProvider.state).state = + account.value!; + + await Navigator.of(context).pushNamed( + PaynymHomeView.routeName, + arguments: widget.walletId, + ); + } else { + await Navigator.of(context).pushNamed( + PaynymClaimView.routeName, + arguments: widget.walletId, + ); + } + } + }, + ), + ], + ), + ], ), ), ), diff --git a/lib/pages/wallets_view/sub_widgets/empty_wallets.dart b/lib/pages/wallets_view/sub_widgets/empty_wallets.dart index 76770ccf7..4db5ce741 100644 --- a/lib/pages/wallets_view/sub_widgets/empty_wallets.dart +++ b/lib/pages/wallets_view/sub_widgets/empty_wallets.dart @@ -38,21 +38,10 @@ class EmptyWallets extends ConsumerWidget { const Spacer( flex: 2, ), - (isSorbet || isForest || isOcean) - ? SvgPicture.asset( - Assets.svg.stack(context), - width: isDesktop - ? 324 - : MediaQuery.of(context).size.width / 3, - ) - : Image( - image: AssetImage( - Assets.png.stack(context), - ), - width: isDesktop - ? 324 - : MediaQuery.of(context).size.width / 3, - ), + SvgPicture.asset( + Assets.svg.stack(context), + width: isDesktop ? 324 : MediaQuery.of(context).size.width / 3, + ), SizedBox( height: isDesktop ? 30 : 16, ), diff --git a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart new file mode 100644 index 000000000..d0ba9ecbd --- /dev/null +++ b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart @@ -0,0 +1,581 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; +import 'package:stackwallet/pages_desktop_specific/coin_control/utxo_row.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/dropdown_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/expandable2.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/widgets/toggle.dart'; + +final desktopUseUTXOs = StateProvider((ref) => {}); + +class DesktopCoinControlUseDialog extends ConsumerStatefulWidget { + const DesktopCoinControlUseDialog({ + Key? key, + required this.walletId, + this.amountToSend, + }) : super(key: key); + + final String walletId; + final Decimal? amountToSend; + + @override + ConsumerState createState() => + _DesktopCoinControlUseDialogState(); +} + +class _DesktopCoinControlUseDialogState + extends ConsumerState { + late final TextEditingController _searchController; + late final Coin coin; + final searchFieldFocusNode = FocusNode(); + + final Set _selectedUTXOsData = {}; + final Set _selectedUTXOs = {}; + + Map>? _map; + List? _list; + + String _searchString = ""; + + CCFilter _filter = CCFilter.available; + CCSortDescriptor _sort = CCSortDescriptor.age; + + bool selectedChanged(Set newSelected) { + if (ref.read(desktopUseUTXOs).length != newSelected.length) return true; + return !ref.read(desktopUseUTXOs).containsAll(newSelected); + } + + @override + void initState() { + _searchController = TextEditingController(); + coin = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .coin; + + for (final utxo in ref.read(desktopUseUTXOs)) { + final data = UtxoRowData(utxo.id, true); + _selectedUTXOs.add(utxo); + _selectedUTXOsData.add(data); + } + + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + searchFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + if (_sort == CCSortDescriptor.address) { + _list = null; + _map = MainDB.instance.queryUTXOsGroupedByAddressSync( + walletId: widget.walletId, + filter: _filter, + sort: _sort, + searchTerm: _searchString, + coin: coin, + ); + } else { + _map = null; + _list = MainDB.instance.queryUTXOsSync( + walletId: widget.walletId, + filter: _filter, + sort: _sort, + searchTerm: _searchString, + coin: coin, + ); + } + + final selectedSum = Format.satoshisToAmount( + _selectedUTXOs + .map((e) => e.value) + .fold(0, (value, element) => value += element), + coin: coin, + ); + + final enableApply = widget.amountToSend == null + ? selectedChanged(_selectedUTXOs) + : selectedChanged(_selectedUTXOs) && + widget.amountToSend! <= selectedSum; + + return DesktopDialog( + maxWidth: 700, + maxHeight: MediaQuery.of(context).size.height - 128, + child: Column( + children: [ + Row( + children: [ + const AppBarBackButton( + size: 40, + iconSize: 24, + ), + Text( + "Coin control", + style: STextStyles.desktopH3(context), + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + RoundedContainer( + color: Colors.transparent, + borderColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "This option allows you to control, freeze, and utilize " + "outputs at your discretion.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchString = value; + }); + }, + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ), + decoration: standardInputDecoration( + "Search...", + searchFieldFocusNode, + context, + desktopMed: true, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 18, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 20, + height: 20, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + const SizedBox( + width: 16, + ), + SizedBox( + height: 56, + width: 240, + child: Toggle( + isOn: _filter == CCFilter.frozen, + onColor: Theme.of(context) + .extension()! + .rateTypeToggleDesktopColorOn, + offColor: Theme.of(context) + .extension()! + .rateTypeToggleDesktopColorOff, + onIcon: Assets.svg.coinControl.unBlocked, + onText: "Available", + offIcon: Assets.svg.coinControl.blocked, + offText: "Frozen", + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onValueChanged: (value) { + setState(() { + if (value) { + _filter = CCFilter.frozen; + } else { + _filter = CCFilter.available; + } + }); + }, + ), + ), + const SizedBox( + width: 16, + ), + JDropdownIconButton( + redrawOnScreenSizeChanged: true, + groupValue: _sort, + items: CCSortDescriptor.values.toSet(), + onSelectionChanged: (CCSortDescriptor? newValue) { + if (newValue != null && newValue != _sort) { + setState(() { + _sort = newValue; + }); + } + }, + displayPrefix: "Sort by", + ) + ], + ), + const SizedBox( + height: 16, + ), + Expanded( + child: _list != null + ? ListView.separated( + shrinkWrap: true, + primary: false, + itemCount: _list!.length, + separatorBuilder: (context, _) => const SizedBox( + height: 10, + ), + itemBuilder: (context, index) { + final utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(_list![index]) + .findFirstSync()!; + final data = UtxoRowData(utxo.id, false); + data.selected = _selectedUTXOsData.contains(data); + + return UtxoRow( + key: Key( + "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}"), + data: data, + compact: true, + walletId: widget.walletId, + onSelectionChanged: (value) { + setState(() { + if (data.selected) { + _selectedUTXOsData.add(value); + _selectedUTXOs.add(utxo); + } else { + _selectedUTXOsData.remove(value); + _selectedUTXOs.remove(utxo); + } + }); + }, + ); + }, + ) + : ListView.separated( + itemCount: _map!.entries.length, + separatorBuilder: (context, _) => const SizedBox( + height: 10, + ), + itemBuilder: (context, index) { + final entry = _map!.entries.elementAt(index); + final _controller = RotateIconController(); + + return Expandable2( + border: Theme.of(context) + .extension()! + .backgroundAppBar, + background: Theme.of(context) + .extension()! + .popupBG, + animationDurationMultiplier: + 0.2 * entry.value.length, + onExpandWillChange: (state) { + if (state == Expandable2State.expanded) { + _controller.forward?.call(); + } else { + _controller.reverse?.call(); + } + }, + header: RoundedContainer( + padding: const EdgeInsets.all(20), + color: Colors.transparent, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + const SizedBox( + width: 12, + ), + Expanded( + flex: 3, + child: Text( + entry.key, + style: STextStyles.w600_14(context), + ), + ), + Expanded( + child: Text( + "${entry.value.length} " + "output${entry.value.length > 1 ? "s" : ""}", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ), + RotateIcon( + animationDurationMultiplier: + 0.2 * entry.value.length, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 14, + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + curve: Curves.easeInOut, + controller: _controller, + ), + ], + ), + ), + children: entry.value.map( + (id) { + final utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(id) + .findFirstSync()!; + final data = UtxoRowData(utxo.id, false); + data.selected = + _selectedUTXOsData.contains(data); + + return UtxoRow( + key: Key( + "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}"), + data: data, + compact: true, + compactWithBorder: false, + raiseOnSelected: false, + walletId: widget.walletId, + onSelectionChanged: (value) { + setState(() { + if (data.selected) { + _selectedUTXOsData.add(value); + _selectedUTXOs.add(utxo); + } else { + _selectedUTXOsData.remove(value); + _selectedUTXOs.remove(utxo); + } + }); + }, + ); + }, + ).toList(), + ); + }, + ), + ), + const SizedBox( + height: 16, + ), + RoundedContainer( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + padding: EdgeInsets.zero, + child: ConditionalParent( + condition: widget.amountToSend != null, + builder: (child) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + child, + Container( + height: 1.2, + color: Theme.of(context) + .extension()! + .popupBG, + ), + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount to send", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + Text( + "${widget.amountToSend!.toStringAsFixed( + coin.decimals, + )}" + " ${coin.ticker}", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ], + ), + ), + ], + ); + }, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Selected amount", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + Text( + "${selectedSum.toStringAsFixed( + coin.decimals, + )} ${coin.ticker}", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: widget.amountToSend == null + ? Theme.of(context) + .extension()! + .textDark + : selectedSum < widget.amountToSend! + ? Theme.of(context) + .extension()! + .accentColorRed + : Theme.of(context) + .extension()! + .accentColorGreen, + ), + ), + ], + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + enabled: _selectedUTXOsData.isNotEmpty, + buttonHeight: ButtonHeight.l, + label: _selectedUTXOsData.isEmpty + ? "Clear selection" + : "Clear selection (${_selectedUTXOsData.length})", + onPressed: () { + setState(() { + _selectedUTXOsData.clear(); + _selectedUTXOs.clear(); + }); + }, + ), + ), + const SizedBox( + width: 20, + ), + Expanded( + child: PrimaryButton( + enabled: enableApply, + buttonHeight: ButtonHeight.l, + label: "Apply", + onPressed: () { + ref.read(desktopUseUTXOs.state).state = + _selectedUTXOs; + + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + ), + ], + ), + const SizedBox( + height: 16, + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/coin_control/desktop_coin_control_view.dart b/lib/pages_desktop_specific/coin_control/desktop_coin_control_view.dart new file mode 100644 index 000000000..cf27cc80e --- /dev/null +++ b/lib/pages_desktop_specific/coin_control/desktop_coin_control_view.dart @@ -0,0 +1,420 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; +import 'package:stackwallet/pages_desktop_specific/coin_control/freeze_button.dart'; +import 'package:stackwallet/pages_desktop_specific/coin_control/utxo_row.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/dropdown_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/expandable2.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class DesktopCoinControlView extends ConsumerStatefulWidget { + const DesktopCoinControlView({ + Key? key, + required this.walletId, + }) : super(key: key); + + static const String routeName = "/desktopCoinControl"; + + final String walletId; + + @override + ConsumerState createState() => + _DesktopCoinControlViewState(); +} + +class _DesktopCoinControlViewState + extends ConsumerState { + late final TextEditingController _searchController; + late final Coin coin; + final searchFieldFocusNode = FocusNode(); + + final Set _selectedUTXOs = {}; + + Map>? _map; + List? _list; + + String _searchString = ""; + + CCFilter _filter = CCFilter.all; + CCSortDescriptor _sort = CCSortDescriptor.age; + + @override + void initState() { + _searchController = TextEditingController(); + coin = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .coin; + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + searchFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + if (_sort == CCSortDescriptor.address) { + _list = null; + _map = MainDB.instance.queryUTXOsGroupedByAddressSync( + walletId: widget.walletId, + filter: _filter, + sort: _sort, + searchTerm: _searchString, + coin: coin, + ); + } else { + _map = null; + _list = MainDB.instance.queryUTXOsSync( + walletId: widget.walletId, + filter: _filter, + sort: _sort, + searchTerm: _searchString, + coin: coin, + ); + } + + return DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + leading: Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + width: 32, + ), + AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, + ), + const SizedBox( + width: 18, + ), + SvgPicture.asset( + Assets.svg.coinControl.gamePad, + width: 32, + height: 32, + color: + Theme.of(context).extension()!.textSubtitle1, + ), + const SizedBox( + width: 12, + ), + Text( + "Coin control", + style: STextStyles.desktopH3(context), + ), + ], + ), + ), + useSpacers: false, + isCompactHeight: true, + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchString = value; + }); + }, + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ), + decoration: standardInputDecoration( + "Search...", + searchFieldFocusNode, + context, + desktopMed: true, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 18, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 20, + height: 20, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + const SizedBox( + width: 24, + ), + AnimatedCrossFade( + firstChild: JDropdownButton( + redrawOnScreenSizeChanged: true, + showIcon: true, + width: 200, + items: CCFilter.values.toSet(), + groupValue: _filter, + onSelectionChanged: (CCFilter? newValue) { + if (newValue != null && newValue != _filter) { + setState(() { + _filter = newValue; + }); + } + }, + ), + secondChild: FreezeButton( + key: Key("${_selectedUTXOs.length}"), + selectedUTXOs: _selectedUTXOs, + ), + crossFadeState: _selectedUTXOs.isEmpty + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + duration: const Duration( + milliseconds: 200, + ), + ), + const SizedBox( + width: 24, + ), + AnimatedCrossFade( + firstChild: JDropdownButton( + redrawOnScreenSizeChanged: true, + label: "Sort by...", + width: 200, + groupValue: _sort, + items: CCSortDescriptor.values.toSet(), + onSelectionChanged: (CCSortDescriptor? newValue) { + if (newValue != null && newValue != _sort) { + setState(() { + _sort = newValue; + }); + } + }, + ), + secondChild: SecondaryButton( + buttonHeight: ButtonHeight.l, + width: 200, + label: "Clear selection (${_selectedUTXOs.length})", + onPressed: () => setState(() => _selectedUTXOs.clear()), + ), + crossFadeState: _selectedUTXOs.isEmpty + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + duration: const Duration( + milliseconds: 200, + ), + ), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + ), + child: _list != null + ? ListView.separated( + itemCount: _list!.length, + separatorBuilder: (context, _) => const SizedBox( + height: 10, + ), + itemBuilder: (context, index) { + final utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(_list![index]) + .findFirstSync()!; + final data = UtxoRowData(utxo.id, false); + data.selected = _selectedUTXOs.contains(data); + + return UtxoRow( + key: Key( + "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}"), + data: data, + walletId: widget.walletId, + onSelectionChanged: (value) { + setState(() { + if (data.selected) { + _selectedUTXOs.add(value); + } else { + _selectedUTXOs.remove(value); + } + }); + }, + ); + }, + ) + : ListView.separated( + itemCount: _map!.entries.length, + separatorBuilder: (context, _) => const SizedBox( + height: 10, + ), + itemBuilder: (context, index) { + final entry = _map!.entries.elementAt(index); + final _controller = RotateIconController(); + + return Expandable2( + border: Theme.of(context) + .extension()! + .backgroundAppBar, + background: Theme.of(context) + .extension()! + .popupBG, + animationDurationMultiplier: 0.2 * entry.value.length, + onExpandWillChange: (state) { + if (state == Expandable2State.expanded) { + _controller.forward?.call(); + } else { + _controller.reverse?.call(); + } + }, + header: RoundedContainer( + padding: const EdgeInsets.all(20), + color: Colors.transparent, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + entry.key, + style: STextStyles.w600_14(context), + ), + ), + Expanded( + child: Text( + "${entry.value.length} " + "output${entry.value.length > 1 ? "s" : ""}", + style: + STextStyles.desktopTextExtraExtraSmall( + context), + ), + ), + RotateIcon( + animationDurationMultiplier: + 0.2 * entry.value.length, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 14, + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + curve: Curves.easeInOut, + controller: _controller, + ), + ], + ), + ), + children: entry.value.map( + (id) { + final utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(id) + .findFirstSync()!; + final data = UtxoRowData(utxo.id, false); + data.selected = _selectedUTXOs.contains(data); + + return UtxoRow( + key: Key( + "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}"), + data: data, + walletId: widget.walletId, + raiseOnSelected: false, + onSelectionChanged: (value) { + setState(() { + if (data.selected) { + _selectedUTXOs.add(value); + } else { + _selectedUTXOs.remove(value); + } + }); + }, + ); + }, + ).toList(), + ); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/coin_control/freeze_button.dart b/lib/pages_desktop_specific/coin_control/freeze_button.dart new file mode 100644 index 000000000..f50e4661e --- /dev/null +++ b/lib/pages_desktop_specific/coin_control/freeze_button.dart @@ -0,0 +1,117 @@ +import 'package:async/async.dart'; +import 'package:flutter/material.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; +import 'package:stackwallet/pages_desktop_specific/coin_control/utxo_row.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; + +class FreezeButton extends StatefulWidget { + const FreezeButton({ + Key? key, + required this.selectedUTXOs, + }) : super(key: key); + + final Set selectedUTXOs; + + @override + State createState() => _FreezeButtonState(); +} + +class _FreezeButtonState extends State { + String _freezeLabelCache = "Freeze"; + + String _freezeLabel(Set dataSet) { + if (dataSet.isEmpty) return _freezeLabelCache; + + bool hasUnblocked = false; + for (final data in dataSet) { + if (!MainDB.instance.isar.utxos + .where() + .idEqualTo(data.utxoId) + .findFirstSync()! + .isBlocked) { + hasUnblocked = true; + break; + } + } + _freezeLabelCache = hasUnblocked ? "Freeze" : "Unfreeze"; + return _freezeLabelCache; + } + + Future _onFreezeStateButtonPressed() async { + List utxosToUpdate = []; + switch (_freezeLabelCache) { + case "Freeze": + for (final e in widget.selectedUTXOs) { + final utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(e.utxoId) + .findFirstSync()!; + if (!utxo.isBlocked) { + utxosToUpdate.add(utxo.copyWith(isBlocked: true)); + } + } + break; + + case "Unfreeze": + for (final e in widget.selectedUTXOs) { + final utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(e.utxoId) + .findFirstSync()!; + if (utxo.isBlocked) { + utxosToUpdate.add(utxo.copyWith(isBlocked: false)); + } + } + break; + + default: + Logging.instance.log( + "Unknown utxo method name found in $runtimeType", + level: LogLevel.Fatal, + ); + return; + } + + // final update utxo set in db + if (utxosToUpdate.isNotEmpty) { + await MainDB.instance.putUTXOs(utxosToUpdate); + } + } + + late Stream bigStream; + + @override + void initState() { + List> streams = []; + for (final data in widget.selectedUTXOs) { + final stream = MainDB.instance.watchUTXO(id: data.utxoId); + + streams.add(stream); + } + + bigStream = StreamGroup.merge(streams); + bigStream.listen((event) { + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + setState(() {}); + }); + } + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return PrimaryButton( + buttonHeight: ButtonHeight.l, + width: 200, + label: _freezeLabel(widget.selectedUTXOs), + onPressed: _onFreezeStateButtonPressed, + ); + } +} diff --git a/lib/pages_desktop_specific/coin_control/utxo_row.dart b/lib/pages_desktop_specific/coin_control/utxo_row.dart new file mode 100644 index 000000000..e5ddf2f06 --- /dev/null +++ b/lib/pages_desktop_specific/coin_control/utxo_row.dart @@ -0,0 +1,214 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/pages/coin_control/utxo_details_view.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/utxo_status_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; + +class UtxoRowData { + UtxoRowData(this.utxoId, this.selected); + + Id utxoId; + bool selected; + + @override + String toString() { + return "selected=$selected: $utxoId"; + } + + @override + bool operator ==(Object other) { + return other is UtxoRowData && other.utxoId == utxoId; + } + + @override + int get hashCode => Object.hashAll([utxoId.hashCode]); +} + +class UtxoRow extends ConsumerStatefulWidget { + const UtxoRow({ + Key? key, + required this.data, + required this.walletId, + this.onSelectionChanged, + this.compact = false, + this.compactWithBorder = true, + this.raiseOnSelected = true, + }) : super(key: key); + + final String walletId; + final UtxoRowData data; + final void Function(UtxoRowData)? onSelectionChanged; + final bool compact; + final bool compactWithBorder; + final bool raiseOnSelected; + + @override + ConsumerState createState() => _UtxoRowState(); +} + +class _UtxoRowState extends ConsumerState { + late Stream stream; + late UTXO utxo; + + void _details() async { + await showDialog( + context: context, + builder: (context) => UtxoDetailsView( + utxoId: utxo.id, + walletId: widget.walletId, + ), + ); + } + + @override + void initState() { + utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(widget.data.utxoId) + .findFirstSync()!; + + stream = MainDB.instance.watchUTXO(id: utxo.id); + super.initState(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + final coin = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId).coin)); + + final currentChainHeight = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId).currentHeight)); + + return StreamBuilder( + stream: stream, + builder: (context, snapshot) { + if (snapshot.hasData) { + utxo = snapshot.data!; + } + + return RoundedContainer( + borderColor: widget.compact && widget.compactWithBorder + ? Theme.of(context).extension()!.textFieldDefaultBG + : null, + color: Theme.of(context).extension()!.popupBG, + boxShadow: widget.data.selected && widget.raiseOnSelected + ? [ + Theme.of(context).extension()!.standardBoxShadow, + ] + : null, + child: Row( + children: [ + if (!(widget.compact && utxo.isBlocked)) + Checkbox( + value: widget.data.selected, + onChanged: (value) { + setState(() { + widget.data.selected = value!; + }); + widget.onSelectionChanged?.call(widget.data); + }, + ), + if (!(widget.compact && utxo.isBlocked)) + const SizedBox( + width: 10, + ), + UTXOStatusIcon( + blocked: utxo.isBlocked, + status: utxo.isConfirmed( + currentChainHeight, + coin.requiredConfirmations, + ) + ? UTXOStatusIconStatus.confirmed + : UTXOStatusIconStatus.unconfirmed, + background: Theme.of(context).extension()!.popupBG, + selected: false, + width: 32, + height: 32, + ), + const SizedBox( + width: 10, + ), + if (!widget.compact) + Text( + "${Format.satoshisToAmount( + utxo.value, + coin: coin, + ).toStringAsFixed(coin.decimals)} ${coin.ticker}", + textAlign: TextAlign.right, + style: STextStyles.w600_14(context), + ), + if (!widget.compact) + const SizedBox( + width: 10, + ), + Expanded( + child: ConditionalParent( + condition: widget.compact, + builder: (child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${Format.satoshisToAmount( + utxo.value, + coin: coin, + ).toStringAsFixed(coin.decimals)} ${coin.ticker}", + textAlign: TextAlign.right, + style: STextStyles.w600_14(context), + ), + const SizedBox( + height: 2, + ), + child, + ], + ); + }, + child: Text( + utxo.name.isNotEmpty + ? utxo.name + : utxo.address ?? utxo.txid, + textAlign: + widget.compact ? TextAlign.left : TextAlign.center, + style: STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ), + ), + const SizedBox( + width: 10, + ), + widget.compact + ? CustomTextButton( + text: "Details", + onTap: _details, + ) + : SecondaryButton( + width: 120, + buttonHeight: ButtonHeight.xs, + label: "Details", + onPressed: _details, + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart b/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart index aaeb7e854..34d688b21 100644 --- a/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart +++ b/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart @@ -290,11 +290,21 @@ class _DesktopTradeRowCardState extends ConsumerState { ChangeNowTransactionStatus? status; try { if (statusString.toLowerCase().startsWith("waiting")) { - statusString = "waiting"; + statusString = "Waiting"; } status = changeNowTransactionStatusFromStringIgnoreCase(statusString); } on ArgumentError catch (_) { - status = ChangeNowTransactionStatus.Failed; + switch (statusString.toLowerCase()) { + case "funds confirming": + case "processing payment": + return Assets.svg.txExchangePending(context); + + case "completed": + return Assets.svg.txExchange(context); + + default: + status = ChangeNowTransactionStatus.Failed; + } } switch (status) { diff --git a/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart b/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart index 39680fab0..e45f2d234 100644 --- a/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart +++ b/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart @@ -95,7 +95,7 @@ class _DesktopExchangeViewState extends ConsumerState { left: 24, ), child: Text( - "Exchange", + "Swap", style: STextStyles.desktopH3(context), ), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart index b57fb1899..16c3b8116 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart @@ -37,7 +37,7 @@ class DesktopStep1 extends ConsumerWidget { child: Column( children: [ DesktopStepItem( - label: "Exchange", + label: "Swap", value: ref.watch(exchangeFormStateProvider .select((value) => value.exchange.name)), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart index aeb8ece54..ef72553fa 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart @@ -35,7 +35,7 @@ class _DesktopStep3State extends ConsumerState { child: Column( children: [ DesktopStepItem( - label: "Exchange", + label: "Swap", value: ref.watch(exchangeFormStateProvider .select((value) => value.exchange.name)), ), diff --git a/lib/pages_desktop_specific/desktop_home_view.dart b/lib/pages_desktop_specific/desktop_home_view.dart index e9092fe2d..fa7076572 100644 --- a/lib/pages_desktop_specific/desktop_home_view.dart +++ b/lib/pages_desktop_specific/desktop_home_view.dart @@ -89,10 +89,11 @@ class _DesktopHomeViewState extends ConsumerState { ), }; - DesktopMenuItemId prev = DesktopMenuItemId.myStack; - void onMenuSelectionWillChange(DesktopMenuItemId newKey) { - if (prev == DesktopMenuItemId.myStack && prev == newKey) { + // handle logging out of active wallet + if (ref.read(prevDesktopMenuItemProvider.state).state == + DesktopMenuItemId.myStack && + ref.read(prevDesktopMenuItemProvider.state).state == newKey) { Navigator.of(myStackViewNavKey.currentContext!) .popUntil(ModalRoute.withName(MyStackView.routeName)); if (ref.read(currentWalletIdProvider.state).state != null) { @@ -111,7 +112,7 @@ class _DesktopHomeViewState extends ConsumerState { ref.read(managerProvider.notifier).isActiveWallet = false; } } - prev = newKey; + ref.read(prevDesktopMenuItemProvider.state).state = newKey; // check for unread notifications and refresh provider before // showing notifications view diff --git a/lib/pages_desktop_specific/desktop_menu.dart b/lib/pages_desktop_specific/desktop_menu.dart index d8ad67720..53a4cff7c 100644 --- a/lib/pages_desktop_specific/desktop_menu.dart +++ b/lib/pages_desktop_specific/desktop_menu.dart @@ -155,7 +155,7 @@ class _DesktopMenuState extends ConsumerState { DesktopMenuItem( duration: duration, icon: const DesktopExchangeIcon(), - label: "Exchange", + label: "Swap", value: DesktopMenuItemId.exchange, onChanged: updateSelectedMenuItem, controller: controllers[1], diff --git a/lib/pages_desktop_specific/desktop_menu_item.dart b/lib/pages_desktop_specific/desktop_menu_item.dart index 3e2fa015c..5a00bbe95 100644 --- a/lib/pages_desktop_specific/desktop_menu_item.dart +++ b/lib/pages_desktop_specific/desktop_menu_item.dart @@ -61,7 +61,7 @@ class DesktopBuyIcon extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return SvgPicture.asset( - Assets.svg.buyDesktop, + Assets.svg.buy(context), width: 20, height: 20, color: DesktopMenuItemId.buy == diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index 2e5087c4a..f5b120871 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -1,47 +1,32 @@ import 'dart:async'; -import 'package:decimal/decimal.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; -import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart'; import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; -import 'package:stackwallet/providers/global/paynym_api_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; -import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart'; -import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; -import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; -import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; -import 'package:stackwallet/widgets/desktop/primary_button.dart'; -import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/hover_text_field.dart'; -import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; /// [eventBus] should only be set during testing @@ -92,162 +77,6 @@ class _DesktopWalletViewState extends ConsumerState { ref.read(managerProvider.notifier).isActiveWallet = false; } - Future attemptAnonymize() async { - final managerProvider = ref - .read(walletsChangeNotifierProvider) - .getManagerProvider(widget.walletId); - - bool shouldPop = false; - unawaited( - showDialog( - context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( - message: "Anonymizing balance", - eventBus: null, - ), - onWillPop: () async => shouldPop, - ), - ), - ); - final firoWallet = ref.read(managerProvider).wallet as FiroWallet; - - final publicBalance = await firoWallet.availablePublicBalance(); - if (publicBalance <= Decimal.zero) { - shouldPop = true; - if (mounted) { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of(context).popUntil( - ModalRoute.withName(DesktopWalletView.routeName), - ); - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "No funds available to anonymize!", - context: context, - ), - ); - } - return; - } - - try { - await firoWallet.anonymizeAllPublicFunds(); - shouldPop = true; - if (mounted) { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of(context).popUntil( - ModalRoute.withName(DesktopWalletView.routeName), - ); - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: "Anonymize transaction submitted", - context: context, - ), - ); - } - } catch (e) { - shouldPop = true; - if (mounted) { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of(context).popUntil( - ModalRoute.withName(DesktopWalletView.routeName), - ); - await showDialog( - context: context, - builder: (_) => DesktopDialog( - maxWidth: 400, - maxHeight: 300, - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Anonymize all failed", - style: STextStyles.desktopH3(context), - ), - const Spacer( - flex: 1, - ), - Text( - "Reason: $e", - style: STextStyles.desktopTextSmall(context), - ), - const Spacer( - flex: 2, - ), - Row( - children: [ - const Spacer(), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Ok", - buttonHeight: ButtonHeight.l, - onPressed: - Navigator.of(context, rootNavigator: true).pop, - ), - ), - ], - ) - ], - ), - ), - ), - ); - } - } - } - - Future onPaynymButtonPressed() async { - unawaited( - showDialog( - context: context, - builder: (context) => const LoadingIndicator( - width: 100, - ), - ), - ); - - final manager = - ref.read(walletsChangeNotifierProvider).getManager(widget.walletId); - - final wallet = manager.wallet as PaynymWalletInterface; - - final code = - await wallet.getPaymentCode(DerivePathTypeExt.primaryFor(manager.coin)); - - final account = await ref.read(paynymAPIProvider).nym(code.toString()); - - Logging.instance.log( - "my nym account: $account", - level: LogLevel.Info, - ); - - if (mounted) { - Navigator.of(context, rootNavigator: true).pop(); - - // check if account exists and for matching code to see if claimed - if (account.value != null && account.value!.codes.first.claimed) { - ref.read(myPaynymAccountStateProvider.state).state = account.value!; - - await Navigator.of(context).pushNamed( - PaynymHomeView.routeName, - arguments: widget.walletId, - ); - } else { - await Navigator.of(context).pushNamed( - PaynymClaimView.routeName, - arguments: widget.walletId, - ); - } - } - } - @override void initState() { controller = TextEditingController(); @@ -411,122 +240,15 @@ class _DesktopWalletViewState extends ConsumerState { ), DesktopWalletSummary( walletId: widget.walletId, - managerProvider: managerProvider, initialSyncStatus: ref.watch(managerProvider .select((value) => value.isRefreshing)) ? WalletSyncStatus.syncing : WalletSyncStatus.synced, ), const Spacer(), - if (coin == Coin.firo) const SizedBox(width: 10), - if (coin == Coin.firo) - SecondaryButton( - width: 180, - buttonHeight: ButtonHeight.l, - label: "Anonymize funds", - onPressed: () async { - await showDialog( - context: context, - barrierDismissible: false, - builder: (context) => DesktopDialog( - maxWidth: 500, - maxHeight: 210, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, vertical: 20), - child: Column( - children: [ - Text( - "Attention!", - style: STextStyles.desktopH2(context), - ), - const SizedBox(height: 16), - Text( - "You're about to anonymize all of your public funds.", - style: - STextStyles.desktopTextSmall(context), - ), - const SizedBox(height: 32), - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - SecondaryButton( - width: 200, - buttonHeight: ButtonHeight.l, - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - const SizedBox(width: 20), - PrimaryButton( - width: 200, - buttonHeight: ButtonHeight.l, - label: "Continue", - onPressed: () { - Navigator.of(context).pop(); - - unawaited(attemptAnonymize()); - }, - ) - ], - ), - ], - ), - ), - ), - ); - }, - ), - if (ref.watch(walletsChangeNotifierProvider.select( - (value) => value - .getManager(widget.walletId) - .hasPaynymSupport))) - SecondaryButton( - label: "PayNym", - width: 160, - buttonHeight: ButtonHeight.l, - icon: SvgPicture.asset( - Assets.svg.user, - height: 20, - width: 20, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, - ), - onPressed: onPaynymButtonPressed, - ), - // if (coin == Coin.firo) const SizedBox(width: 16), - // SecondaryButton( - // width: 180, - // buttonHeight: ButtonHeight.l, - // onPressed: () { - // _onExchangePressed(context); - // }, - // label: "Exchange", - // icon: Container( - // width: 24, - // height: 24, - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(24), - // color: Theme.of(context) - // .extension()! - // .buttonBackPrimary - // .withOpacity(0.2), - // ), - // child: Center( - // child: SvgPicture.asset( - // Assets.svg.arrowRotate2, - // width: 14, - // height: 14, - // color: Theme.of(context) - // .extension()! - // .buttonTextSecondary, - // ), - // ), - // ), - // ), + DesktopWalletFeatures( + walletId: widget.walletId, + ), ], ), ), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart index 9c890b223..474c1a077 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart @@ -4,6 +4,7 @@ import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provide import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; class DesktopBalanceToggleButton extends ConsumerWidget { @@ -18,7 +19,7 @@ class DesktopBalanceToggleButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return SizedBox( height: 22, - width: 22, + width: 80, child: MaterialButton( color: Theme.of(context).extension()!.buttonBackSecondary, splashColor: Theme.of(context).extension()!.highlight, @@ -43,10 +44,63 @@ class DesktopBalanceToggleButton extends ConsumerWidget { Constants.size.circularBorderRadius, ), ), + child: Center( + child: FittedBox( + child: Text( + ref.watch(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available + ? "AVAILABLE" + : "FULL", + style: STextStyles.w500_10(context), + ), + ), + ), + ), + ); + } +} + +class DesktopPrivateBalanceToggleButton extends ConsumerWidget { + const DesktopPrivateBalanceToggleButton({ + Key? key, + this.onPressed, + }) : super(key: key); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + height: 22, + width: 22, + child: MaterialButton( + color: Theme.of(context).extension()!.buttonBackSecondary, + splashColor: Theme.of(context).extension()!.highlight, + onPressed: () { + if (ref.read(walletPrivateBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available) { + ref.read(walletPrivateBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.full; + } else { + ref.read(walletPrivateBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.available; + } + onPressed?.call(); + }, + elevation: 0, + highlightElevation: 0, + hoverElevation: 0, + padding: EdgeInsets.zero, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), child: Center( child: Image( image: AssetImage( - ref.watch(walletBalanceToggleStateProvider.state).state == + ref.watch(walletPrivateBalanceToggleStateProvider.state).state == WalletBalanceToggleState.available ? Assets.png.glassesHidden : Assets.png.glasses, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 09ed16610..97375f2e7 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/models/send_view_auto_fill_data.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import 'package:stackwallet/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart'; @@ -44,6 +45,7 @@ import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; @@ -120,128 +122,142 @@ class _DesktopSendState extends ConsumerState { Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin); } - // confirm send all - if (amount == availableBalance) { - final bool? shouldSendAll = await showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return DesktopDialog( - maxWidth: 450, - maxHeight: double.infinity, - child: Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 32, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Confirm send all", - style: STextStyles.desktopH3(context), - ), - const DesktopDialogCloseButton(), - ], - ), - const SizedBox( - height: 12, - ), - Padding( - padding: const EdgeInsets.only( - right: 32, - ), - child: Text( - "You are about to send your entire balance. Would you like to continue?", - textAlign: TextAlign.left, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - fontSize: 18, - ), - ), - ), - const SizedBox( - height: 40, - ), - Padding( - padding: const EdgeInsets.only( - right: 32, - ), - child: Row( - children: [ - Expanded( - child: SecondaryButton( - buttonHeight: ButtonHeight.l, - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - buttonHeight: ButtonHeight.l, - label: "Yes", - onPressed: () { - Navigator.of(context).pop(true); + final coinControlEnabled = + ref.read(prefsChangeNotifierProvider).enableCoinControl; - setState(() { - sendToController.text = ""; - cryptoAmountController.text = ""; - baseAmountController.text = ""; - }); - }, - ), + if (!(manager.hasCoinControlSupport && coinControlEnabled) || + (manager.hasCoinControlSupport && + coinControlEnabled && + ref.read(desktopUseUTXOs).isEmpty)) { + // confirm send all + if (amount == availableBalance) { + final bool? shouldSendAll = await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxWidth: 450, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Confirm send all", + style: STextStyles.desktopH3(context), ), + const DesktopDialogCloseButton(), ], ), - ), - ], + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Text( + "You are about to send your entire balance. Would you like to continue?", + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + fontSize: 18, + ), + ), + ), + const SizedBox( + height: 40, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Yes", + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ), + ], + ), + ), + ], + ), ), - ), - ); - }, - ); + ); + }, + ); - if (shouldSendAll == null || shouldSendAll == false) { - // cancel preview - return; + if (shouldSendAll == null || shouldSendAll == false) { + // cancel preview + return; + } } } try { bool wasCancelled = false; - unawaited(showDialog( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return DesktopDialog( - maxWidth: 400, - maxHeight: double.infinity, - child: Padding( - padding: const EdgeInsets.all(32), - child: BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; + if (mounted) { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: BuildingTransactionDialog( + coin: manager.coin, + onCancel: () { + wasCancelled = true; - Navigator.of(context).pop(); - }, - ), - ), - ); - }, - )); + Navigator.of(context).pop(); + }, + ), + ), + ); + }, + ), + ); + } + + final time = Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); Map txData; + Future> txDataFuture; if (isPaynymSend) { final wallet = manager.wallet as PaynymWalletInterface; @@ -250,27 +266,55 @@ class _DesktopSendState extends ConsumerState { wallet.networkType, ); final feeRate = ref.read(feeRateTypeStateProvider); - txData = await wallet.preparePaymentCodeSend( + txDataFuture = wallet.preparePaymentCodeSend( paymentCode: paymentCode, satoshiAmount: amount, - args: {"feeRate": feeRate}, + args: { + "feeRate": feeRate, + "UTXOs": (manager.hasCoinControlSupport && + coinControlEnabled && + ref.read(desktopUseUTXOs).isNotEmpty) + ? ref.read(desktopUseUTXOs) + : null, + }, ); } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { - txData = await (manager.wallet as FiroWallet).prepareSendPublic( + txDataFuture = (manager.wallet as FiroWallet).prepareSendPublic( address: _address!, satoshiAmount: amount, - args: {"feeRate": ref.read(feeRateTypeStateProvider)}, + args: { + "feeRate": ref.read(feeRateTypeStateProvider), + "UTXOs": (manager.hasCoinControlSupport && + coinControlEnabled && + ref.read(desktopUseUTXOs).isNotEmpty) + ? ref.read(desktopUseUTXOs) + : null, + }, ); } else { - txData = await manager.prepareSend( + txDataFuture = manager.prepareSend( address: _address!, satoshiAmount: amount, - args: {"feeRate": ref.read(feeRateTypeStateProvider)}, + args: { + "feeRate": ref.read(feeRateTypeStateProvider), + "UTXOs": (manager.hasCoinControlSupport && + coinControlEnabled && + ref.read(desktopUseUTXOs).isNotEmpty) + ? ref.read(desktopUseUTXOs) + : null, + }, ); } + final results = await Future.wait([ + txDataFuture, + time, + ]); + + txData = results.first as Map; + if (!wasCancelled && mounted) { if (isPaynymSend) { txData["paynymAccountLite"] = widget.accountLite!; @@ -578,6 +622,11 @@ class _DesktopSendState extends ConsumerState { content = content.substring(0, content.indexOf("\n")); } + if (coin == Coin.epicCash) { + // strip http:// and https:// if content contains @ + content = formatAddress(content); + } + sendToController.text = content; _address = content; @@ -661,6 +710,16 @@ class _DesktopSendState extends ConsumerState { } } + void _showDesktopCoinControl() async { + await showDialog( + context: context, + builder: (context) => DesktopCoinControlUseDialog( + walletId: widget.walletId, + amountToSend: _amountToSend, + ), + ); + } + @override void initState() { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -742,6 +801,33 @@ class _DesktopSendState extends ConsumerState { final String locale = ref.watch( localeServiceChangeNotifierProvider.select((value) => value.locale)); + // add listener for epic cash to strip http:// and https:// prefixes if the address also ocntains an @ symbol (indicating an epicbox address) + if (coin == Coin.epicCash) { + sendToController.addListener(() { + _address = sendToController.text; + + if (_address != null && _address!.isNotEmpty) { + _address = _address!.trim(); + if (_address!.contains("\n")) { + _address = _address!.substring(0, _address!.indexOf("\n")); + } + + sendToController.text = formatAddress(_address!); + } + }); + } + + final showCoinControl = ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.enableCoinControl, + ), + ) && + ref.watch( + provider.select( + (value) => value.hasCoinControlSupport, + ), + ); + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1018,6 +1104,31 @@ class _DesktopSendState extends ConsumerState { ), ), ), + if (showCoinControl) + const SizedBox( + height: 10, + ), + if (showCoinControl) + RoundedContainer( + color: Colors.transparent, + borderColor: + Theme.of(context).extension()!.textFieldDefaultBG, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Coin control", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + CustomTextButton( + text: ref.watch(desktopUseUTXOs.state).state.isEmpty + ? "Select coins" + : "Selected coins (${ref.watch(desktopUseUTXOs.state).state.length})", + onTap: _showDesktopCoinControl, + ), + ], + ), + ), const SizedBox( height: 20, ), @@ -1323,3 +1434,22 @@ class _DesktopSendState extends ConsumerState { ); } } + +String formatAddress(String epicAddress) { + // strip http:// or https:// prefixes if the address contains an @ symbol (and is thus an epicbox address) + if ((epicAddress.startsWith("http://") || + epicAddress.startsWith("https://")) && + epicAddress.contains("@")) { + epicAddress = epicAddress.replaceAll("http://", ""); + epicAddress = epicAddress.replaceAll("https://", ""); + } + // strip mailto: prefix + if (epicAddress.startsWith("mailto:")) { + epicAddress = epicAddress.replaceAll("mailto:", ""); + } + // strip / suffix if the address contains an @ symbol (and is thus an epicbox address) + if (epicAddress.endsWith("/") && epicAddress.contains("@")) { + epicAddress = epicAddress.substring(0, epicAddress.length - 1); + } + return epicAddress; +} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart new file mode 100644 index 000000000..5b1c3edce --- /dev/null +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -0,0 +1,374 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; +import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/coin_control/desktop_coin_control_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_menu.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart'; +import 'package:stackwallet/providers/desktop/current_desktop_menu_item.dart'; +import 'package:stackwallet/providers/global/paynym_api_provider.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_loading_overlay.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; + +class DesktopWalletFeatures extends ConsumerStatefulWidget { + const DesktopWalletFeatures({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState createState() => + _DesktopWalletFeaturesState(); +} + +class _DesktopWalletFeaturesState extends ConsumerState { + static const double buttonWidth = 120; + + Future _onSwapPressed() async { + ref.read(currentDesktopMenuItemProvider.state).state = + DesktopMenuItemId.exchange; + ref.read(prevDesktopMenuItemProvider.state).state = + DesktopMenuItemId.exchange; + } + + Future _onBuyPressed() async { + ref.read(currentDesktopMenuItemProvider.state).state = + DesktopMenuItemId.buy; + ref.read(prevDesktopMenuItemProvider.state).state = DesktopMenuItemId.buy; + } + + Future _onMorePressed() async { + await showDialog( + context: context, + builder: (_) => MoreFeaturesDialog( + walletId: widget.walletId, + onPaynymPressed: _onPaynymPressed, + onCoinControlPressed: _onCoinControlPressed, + onAnonymizeAllPressed: _onAnonymizeAllPressed, + onWhirlpoolPressed: _onWhirlpoolPressed, + ), + ); + } + + void _onWhirlpoolPressed() { + Navigator.of(context, rootNavigator: true).pop(); + } + + void _onCoinControlPressed() { + Navigator.of(context, rootNavigator: true).pop(); + + Navigator.of(context).pushNamed( + DesktopCoinControlView.routeName, + arguments: widget.walletId, + ); + } + + Future _onAnonymizeAllPressed() async { + Navigator.of(context, rootNavigator: true).pop(); + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => DesktopDialog( + maxWidth: 500, + maxHeight: 210, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 20), + child: Column( + children: [ + Text( + "Attention!", + style: STextStyles.desktopH2(context), + ), + const SizedBox(height: 16), + Text( + "You're about to anonymize all of your public funds.", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + const SizedBox(width: 20), + PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Continue", + onPressed: () { + Navigator.of(context).pop(); + + unawaited( + _attemptAnonymize(), + ); + }, + ) + ], + ), + ], + ), + ), + ), + ); + } + + Future _attemptAnonymize() async { + final managerProvider = ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(widget.walletId); + + bool shouldPop = false; + unawaited( + showDialog( + context: context, + builder: (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Anonymizing balance", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), + ), + ); + final firoWallet = ref.read(managerProvider).wallet as FiroWallet; + + final publicBalance = firoWallet.availablePublicBalance(); + if (publicBalance <= Decimal.zero) { + shouldPop = true; + if (context.mounted) { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopWalletView.routeName), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "No funds available to anonymize!", + context: context, + ), + ); + } + return; + } + + try { + await firoWallet.anonymizeAllPublicFunds(); + shouldPop = true; + if (context.mounted) { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopWalletView.routeName), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Anonymize transaction submitted", + context: context, + ), + ); + } + } catch (e) { + shouldPop = true; + if (context.mounted) { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopWalletView.routeName), + ); + await showDialog( + context: context, + builder: (_) => DesktopDialog( + maxWidth: 400, + maxHeight: 300, + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Anonymize all failed", + style: STextStyles.desktopH3(context), + ), + const Spacer( + flex: 1, + ), + Text( + "Reason: $e", + style: STextStyles.desktopTextSmall(context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + const Spacer(), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Ok", + buttonHeight: ButtonHeight.l, + onPressed: + Navigator.of(context, rootNavigator: true).pop, + ), + ), + ], + ) + ], + ), + ), + ), + ); + } + } + } + + Future _onPaynymPressed() async { + Navigator.of(context, rootNavigator: true).pop(); + + unawaited( + showDialog( + context: context, + builder: (context) { + return const LoadingIndicator( + width: 100, + ); + }, + ), + ); + + final manager = + ref.read(walletsChangeNotifierProvider).getManager(widget.walletId); + + final wallet = manager.wallet as PaynymWalletInterface; + + final code = + await wallet.getPaymentCode(DerivePathTypeExt.primaryFor(manager.coin)); + + final account = await ref.read(paynymAPIProvider).nym(code.toString()); + + Logging.instance.log( + "my nym account: $account", + level: LogLevel.Info, + ); + + if (context.mounted) { + Navigator.of(context, rootNavigator: true).pop(); + + // check if account exists and for matching code to see if claimed + if (account.value != null && account.value!.codes.first.claimed) { + ref.read(myPaynymAccountStateProvider.state).state = account.value!; + + await Navigator.of(context).pushNamed( + PaynymHomeView.routeName, + arguments: widget.walletId, + ); + } else { + await Navigator.of(context).pushNamed( + PaynymClaimView.routeName, + arguments: widget.walletId, + ); + } + } + } + + @override + Widget build(BuildContext context) { + final manager = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(widget.walletId), + ), + ); + + final showMore = manager.hasPaynymSupport || + manager.hasCoinControlSupport || + manager.coin == Coin.firo || + manager.coin == Coin.firoTestNet || + manager.hasWhirlpoolSupport; + + return Row( + children: [ + if (Constants.enableExchange) + SecondaryButton( + label: "Swap", + width: buttonWidth, + buttonHeight: ButtonHeight.l, + icon: SvgPicture.asset( + Assets.svg.arrowRotate, + height: 20, + width: 20, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + onPressed: () => _onSwapPressed(), + ), + if (Constants.enableExchange) + const SizedBox( + width: 16, + ), + if (Constants.enableExchange) + SecondaryButton( + label: "Buy", + width: buttonWidth, + buttonHeight: ButtonHeight.l, + icon: SvgPicture.asset( + Assets.svg.buy(context), + height: 20, + width: 20, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + onPressed: () => _onBuyPressed(), + ), + if (showMore) + const SizedBox( + width: 16, + ), + SecondaryButton( + label: "More", + width: buttonWidth, + buttonHeight: ButtonHeight.l, + icon: SvgPicture.asset( + Assets.svg.bars, + height: 20, + width: 20, + color: + Theme.of(context).extension()!.buttonTextSecondary, + ), + onPressed: () => _onMorePressed(), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart index f56e2160a..4d30ebc60 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart @@ -1,207 +1,154 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; -import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/animated_text.dart'; -class DesktopWalletSummary extends StatefulWidget { +class DesktopWalletSummary extends ConsumerStatefulWidget { const DesktopWalletSummary({ Key? key, required this.walletId, - required this.managerProvider, required this.initialSyncStatus, }) : super(key: key); final String walletId; - final ChangeNotifierProvider managerProvider; final WalletSyncStatus initialSyncStatus; @override - State createState() => _WDesktopWalletSummaryState(); + ConsumerState createState() => + _WDesktopWalletSummaryState(); } -class _WDesktopWalletSummaryState extends State { +class _WDesktopWalletSummaryState extends ConsumerState { late final String walletId; - late final ChangeNotifierProvider managerProvider; - - Decimal? _balanceTotalCached; - Decimal? _balanceCached; @override void initState() { walletId = widget.walletId; - managerProvider = widget.managerProvider; super.initState(); } @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + + final externalCalls = ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.externalCalls, + ), + ); + final coin = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(widget.walletId).coin, + ), + ); + final locale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale)); + + final baseCurrency = ref + .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + + final priceTuple = ref.watch(priceAnd24hChangeNotifierProvider + .select((value) => value.getPrice(coin))); + + final _showAvailable = + ref.watch(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available; + + Balance balance = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).balance)); + + Decimal balanceToShow; + if (coin == Coin.firo || coin == Coin.firoTestNet) { + Balance? balanceSecondary = ref + .watch( + walletsChangeNotifierProvider.select( + (value) => + value.getManager(widget.walletId).wallet as FiroWallet?, + ), + ) + ?.balancePrivate; + final showPrivate = + ref.watch(walletPrivateBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available; + + if (_showAvailable) { + balanceToShow = showPrivate + ? balanceSecondary!.getSpendable() + : balance.getSpendable(); + } else { + balanceToShow = + showPrivate ? balanceSecondary!.getTotal() : balance.getTotal(); + } + } else { + if (_showAvailable) { + balanceToShow = balance.getSpendable(); + } else { + balanceToShow = balance.getTotal(); + } + } + return Consumer( builder: (context, ref, __) { - final Coin coin = - ref.watch(managerProvider.select((value) => value.coin)); return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Consumer( - builder: (_, ref, __) { - final externalCalls = ref.watch(prefsChangeNotifierProvider - .select((value) => value.externalCalls)); - - Future? totalBalanceFuture; - Future? availableBalanceFuture; - if (coin == Coin.firo || coin == Coin.firoTestNet) { - final firoWallet = ref.watch( - managerProvider.select((value) => value.wallet)) - as FiroWallet; - totalBalanceFuture = - Future(() => firoWallet.balance.getSpendable()); - availableBalanceFuture = Future( - () => firoWallet.balancePrivate.getSpendable()); - } else { - final manager = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletId))); - totalBalanceFuture = - Future(() => manager.balance.getTotal()); - availableBalanceFuture = - Future(() => manager.balance.getSpendable()); - } - - final locale = ref.watch(localeServiceChangeNotifierProvider - .select((value) => value.locale)); - - final baseCurrency = ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)); - - final priceTuple = ref.watch( - priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin))); - - final _showAvailable = ref - .watch(walletBalanceToggleStateProvider.state) - .state == - WalletBalanceToggleState.available; - - return FutureBuilder( - future: _showAvailable - ? availableBalanceFuture - : totalBalanceFuture, - builder: (fbContext, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData && - snapshot.data != null) { - if (_showAvailable) { - _balanceCached = snapshot.data!; - } else { - _balanceTotalCached = snapshot.data!; - } - } - Decimal? balanceToShow = _showAvailable - ? _balanceCached - : _balanceTotalCached; - - if (balanceToShow != null) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "${Format.localizedStringAsFixed( - value: balanceToShow, - locale: locale, - decimalPlaces: 8, - )} ${coin.ticker}", - style: STextStyles.desktopH3(context), - ), - ), - if (externalCalls) - Text( - "${Format.localizedStringAsFixed( - value: priceTuple.item1 * balanceToShow, - locale: locale, - decimalPlaces: 2, - )} $baseCurrency", - style: - STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - ], - ); - } else { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AnimatedText( - stringsToLoopThrough: const [ - "Loading balance ", - "Loading balance. ", - "Loading balance.. ", - "Loading balance..." - ], - style: STextStyles.desktopH3(context).copyWith( - fontSize: 24, - color: Theme.of(context) - .extension()! - .textDark, - ), - ), - if (externalCalls) - AnimatedText( - stringsToLoopThrough: const [ - "Loading balance ", - "Loading balance. ", - "Loading balance.. ", - "Loading balance..." - ], - style: - STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - ], - ); - } - }, - ); - }, + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "${Format.localizedStringAsFixed( + value: balanceToShow, + locale: locale, + decimalPlaces: 8, + )} ${coin.ticker}", + style: STextStyles.desktopH3(context), + ), ), + if (externalCalls) + Text( + "${Format.localizedStringAsFixed( + value: priceTuple.item1 * balanceToShow, + locale: locale, + decimalPlaces: 2, + )} $baseCurrency", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), ], ), - if (coin == Coin.firo || coin == Coin.firoTestNet) - const SizedBox( - width: 8, - ), - if (coin == Coin.firo || coin == Coin.firoTestNet) - const DesktopBalanceToggleButton(), const SizedBox( width: 8, ), WalletRefreshButton( walletId: walletId, initialSyncStatus: widget.initialSyncStatus, - ) + ), + if (coin == Coin.firo || coin == Coin.firoTestNet) + const SizedBox( + width: 8, + ), + if (coin == Coin.firo || coin == Coin.firoTestNet) + const DesktopPrivateBalanceToggleButton(), + const SizedBox( + width: 8, + ), + const DesktopBalanceToggleButton(), ], ); }, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart new file mode 100644 index 000000000..b94eccb54 --- /dev/null +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; + +class MoreFeaturesDialog extends ConsumerStatefulWidget { + const MoreFeaturesDialog({ + Key? key, + required this.walletId, + required this.onPaynymPressed, + required this.onCoinControlPressed, + required this.onAnonymizeAllPressed, + required this.onWhirlpoolPressed, + }) : super(key: key); + + final String walletId; + final VoidCallback? onPaynymPressed; + final VoidCallback? onCoinControlPressed; + final VoidCallback? onAnonymizeAllPressed; + final VoidCallback? onWhirlpoolPressed; + + @override + ConsumerState createState() => _MoreFeaturesDialogState(); +} + +class _MoreFeaturesDialogState extends ConsumerState { + @override + Widget build(BuildContext context) { + final manager = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(widget.walletId), + ), + ); + + final coinControlPrefEnabled = ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.enableCoinControl, + ), + ); + + return DesktopDialog( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "More features", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + if (manager.coin == Coin.firo || manager.coin == Coin.firoTestNet) + _MoreFeaturesItem( + label: "Anonymize funds", + detail: "Anonymize funds", + iconAsset: Assets.svg.anonymize, + onPressed: () => widget.onAnonymizeAllPressed?.call(), + ), + if (manager.hasWhirlpoolSupport) + _MoreFeaturesItem( + label: "Whirlpool", + detail: "Powerful Bitcoin privacy enhancer", + iconAsset: Assets.svg.whirlPool, + onPressed: () => widget.onWhirlpoolPressed?.call(), + ), + if (manager.hasCoinControlSupport && coinControlPrefEnabled) + _MoreFeaturesItem( + label: "Coin control", + detail: "Control, freeze, and utilize outputs at your discretion", + iconAsset: Assets.svg.coinControl.gamePad, + onPressed: () => widget.onCoinControlPressed?.call(), + ), + if (manager.hasPaynymSupport) + _MoreFeaturesItem( + label: "PayNym", + detail: "Increased address privacy using BIP47", + iconAsset: Assets.svg.robotHead, + onPressed: () => widget.onPaynymPressed?.call(), + ), + const SizedBox( + height: 28, + ), + ], + ), + ); + } +} + +class _MoreFeaturesItem extends StatelessWidget { + const _MoreFeaturesItem({ + Key? key, + required this.label, + required this.detail, + required this.iconAsset, + this.onPressed, + }) : super(key: key); + + static const double iconSizeBG = 46; + static const double iconSize = 24; + + final String label; + final String detail; + final String iconAsset; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 6, + horizontal: 32, + ), + child: RoundedContainer( + color: Colors.transparent, + borderColor: + Theme.of(context).extension()!.textFieldDefaultBG, + onPressed: onPressed, + child: Row( + children: [ + RoundedContainer( + padding: const EdgeInsets.all(0), + color: + Theme.of(context).extension()!.settingsIconBack, + width: iconSizeBG, + height: iconSizeBG, + radiusMultiplier: iconSizeBG, + child: Center( + child: SvgPicture.asset( + iconAsset, + width: iconSize, + height: iconSize, + color: Theme.of(context) + .extension()! + .settingsIconIcon, + ), + ), + ), + const SizedBox( + width: 16, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: STextStyles.w600_20(context), + ), + Text( + detail, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ) + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/password/desktop_login_view.dart b/lib/pages_desktop_specific/password/desktop_login_view.dart index 641d70fa5..bb2390bfe 100644 --- a/lib/pages_desktop_specific/password/desktop_login_view.dart +++ b/lib/pages_desktop_specific/password/desktop_login_view.dart @@ -119,14 +119,23 @@ class _DesktopLoginViewState extends ConsumerState { await Future.delayed(const Duration(seconds: 1)); - await showFloatingFlushBar( - type: FlushBarType.warning, - message: e.toString(), - context: context, - ); + if (mounted) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: e.toString(), + context: context, + ); + } } } + @override + void didChangeDependencies() { + unawaited(Assets.precache(context)); + + super.didChangeDependencies(); + } + @override void initState() { passwordController = TextEditingController(); diff --git a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart index e041bcb21..796c139f4 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart @@ -110,6 +110,44 @@ class _AdvancedSettings extends ConsumerState { thickness: 0.5, ), ), + Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Enable coin control", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.enableCoinControl), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .enableCoinControl = newValue; + }, + ), + ), + ], + ), + ), + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider( + thickness: 0.5, + ), + ), /// TODO: Make a dialog popup Consumer(builder: (_, ref, __) { diff --git a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart index 8b119b369..2915b0509 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart @@ -10,7 +10,6 @@ import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -19,8 +18,6 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import '../../../../providers/ui/color_theme_provider.dart'; - class StackPrivacyDialog extends ConsumerStatefulWidget { const StackPrivacyDialog({Key? key}) : super(key: key); @@ -215,16 +212,10 @@ class _PrivacyToggleState extends ConsumerState { late bool externalCallsEnabled; late final bool isDesktop; - late final bool isSorbet; - late final bool isOcean; @override void initState() { isDesktop = Util.isDesktop; - isSorbet = ref.read(colorThemeProvider.state).state.themeType == - ThemeType.fruitSorbet; - isOcean = ref.read(colorThemeProvider.state).state.themeType == - ThemeType.oceanBreeze; // initial toggle state externalCallsEnabled = widget.externalCallsEnabled; super.initState(); @@ -273,17 +264,11 @@ class _PrivacyToggleState extends ConsumerState { const SizedBox( height: 10, ), - (isSorbet) - ? Image.asset( - Assets.png.personaEasy(context), - width: 120, - height: 120, - ) - : SvgPicture.asset( - Assets.svg.personaEasy(context), - width: 120, - height: 120, - ), + SvgPicture.asset( + Assets.svg.personaEasy(context), + width: 120, + height: 120, + ), if (isDesktop) const SizedBox( height: 12, @@ -385,17 +370,11 @@ class _PrivacyToggleState extends ConsumerState { const SizedBox( height: 10, ), - (isSorbet) - ? Image.asset( - Assets.png.personaIncognito(context), - width: 120, - height: 120, - ) - : SvgPicture.asset( - Assets.svg.personaIncognito(context), - width: 120, - height: 120, - ), + SvgPicture.asset( + Assets.svg.personaIncognito(context), + width: 120, + height: 120, + ), if (isDesktop) const SizedBox( height: 12, diff --git a/lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart index 15ae4b7b1..014093f56 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart @@ -135,8 +135,8 @@ class _AppearanceOptionSettings ], ), ), - Padding( - padding: EdgeInsets.all(10), + const Padding( + padding: EdgeInsets.all(2), child: ThemeToggle(), ), ], @@ -174,6 +174,8 @@ class _ThemeToggle extends ConsumerState { return Assets.svg.themeFruit; case ThemeType.forest: return Assets.svg.themeForest; + case ThemeType.chan: + return Assets.svg.themeChan; } } @@ -184,100 +186,95 @@ class _ThemeToggle extends ConsumerState { runSpacing: 16, children: [ for (int i = 0; i < ThemeType.values.length; i++) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (i > 0) - const SizedBox( - width: 10, - ), - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - if (ref.read(colorThemeProvider.state).state.themeType != - ThemeType.values[i]) { - DB.instance.put( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: ThemeType.values[i].name, - ); - ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - ThemeType.values[i].colorTheme); - } - }, - child: Container( - width: 200, - color: Colors.transparent, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration( - border: Border.all( - width: 2.5, - color: ref - .read(colorThemeProvider.state) - .state - .themeType == - ThemeType.values[i] - ? Theme.of(context) - .extension()! - .infoItemIcons - : Theme.of(context) - .extension()! - .popupBG, - ), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + Padding( + padding: const EdgeInsets.all(8.0), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + if (ref.read(colorThemeProvider.state).state.themeType != + ThemeType.values[i]) { + DB.instance.put( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.values[i].name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + ThemeType.values[i].colorTheme); + } + }, + child: Container( + width: 200, + color: Colors.transparent, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.5, + color: ref + .read(colorThemeProvider.state) + .state + .themeType == + ThemeType.values[i] + ? Theme.of(context) + .extension()! + .infoItemIcons + : Theme.of(context) + .extension()! + .popupBG, ), - child: SvgPicture.asset( - assetNameFor(ThemeType.values[i]), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), ), - const SizedBox( - height: 12, + child: SvgPicture.asset( + assetNameFor(ThemeType.values[i]), + height: 160, ), - Row( - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: ThemeType.values[i], - groupValue: ref - .read(colorThemeProvider.state) - .state - .themeType, - onChanged: (_) {}, - ), + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, + value: ThemeType.values[i], + groupValue: ref + .read(colorThemeProvider.state) + .state + .themeType, + onChanged: (_) {}, ), - const SizedBox( - width: 14, + ), + const SizedBox( + width: 14, + ), + Text( + ThemeType.values[i].prettyName, + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, ), - Text( - ThemeType.values[i].prettyName, - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), - ), - ], - ), - ], - ), + ), + ], + ), + ], ), ), - ) - ], - ), + ), + ), + ) ], ); } diff --git a/lib/providers/desktop/current_desktop_menu_item.dart b/lib/providers/desktop/current_desktop_menu_item.dart index 85ac7c46b..a9d3b17e1 100644 --- a/lib/providers/desktop/current_desktop_menu_item.dart +++ b/lib/providers/desktop/current_desktop_menu_item.dart @@ -3,3 +3,6 @@ import 'package:stackwallet/pages_desktop_specific/desktop_menu.dart'; final currentDesktopMenuItemProvider = StateProvider((ref) => DesktopMenuItemId.myStack); + +final prevDesktopMenuItemProvider = + StateProvider((ref) => DesktopMenuItemId.myStack); diff --git a/lib/providers/wallet/wallet_balance_toggle_state_provider.dart b/lib/providers/wallet/wallet_balance_toggle_state_provider.dart index 35d85a6cf..477f4fb03 100644 --- a/lib/providers/wallet/wallet_balance_toggle_state_provider.dart +++ b/lib/providers/wallet/wallet_balance_toggle_state_provider.dart @@ -4,3 +4,7 @@ import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; final walletBalanceToggleStateProvider = StateProvider.autoDispose( (ref) => WalletBalanceToggleState.full); + +final walletPrivateBalanceToggleStateProvider = + StateProvider.autoDispose( + (ref) => WalletBalanceToggleState.full); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 75a5dc429..2cc28d52d 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:isar/isar.dart'; import 'package:stackwallet/models/buy/response_objects/quote.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; @@ -30,6 +31,8 @@ import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_name_ import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; import 'package:stackwallet/pages/buy_view/buy_quote_preview.dart'; import 'package:stackwallet/pages/buy_view/buy_view.dart'; +import 'package:stackwallet/pages/coin_control/coin_control_view.dart'; +import 'package:stackwallet/pages/coin_control/utxo_details_view.dart'; import 'package:stackwallet/pages/exchange_view/choose_from_stack_view.dart'; import 'package:stackwallet/pages/exchange_view/edit_trade_note_view.dart'; import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_1_view.dart'; @@ -39,6 +42,7 @@ import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_4_view. import 'package:stackwallet/pages/exchange_view/send_from_view.dart'; import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; +import 'package:stackwallet/pages/generic/single_field_edit_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/intro_view.dart'; import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart'; @@ -48,7 +52,7 @@ import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; import 'package:stackwallet/pages/receive_view/addresses/edit_address_label_view.dart'; -import 'package:stackwallet/pages/receive_view/addresses/receiving_addresses_view.dart'; +import 'package:stackwallet/pages/receive_view/addresses/wallet_addresses_view.dart'; import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; @@ -99,6 +103,7 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_sear import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallets_view/wallets_view.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/desktop_address_book.dart'; +import 'package:stackwallet/pages_desktop_specific/coin_control/desktop_coin_control_view.dart'; // import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_buys_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_buy/desktop_buy_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart'; @@ -231,6 +236,65 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case SingleFieldEditView.routeName: + if (args is Tuple2) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => SingleFieldEditView( + initialValue: args.item1, + label: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case CoinControlView.routeName: + if (args is Tuple2) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => CoinControlView( + walletId: args.item1, + type: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } else if (args + is Tuple4?>) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => CoinControlView( + walletId: args.item1, + type: args.item2, + requestedTotal: args.item3, + selectedUTXOs: args.item4, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case UtxoDetailsView.routeName: + if (args is Tuple2) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => UtxoDetailsView( + walletId: args.item2, + utxoId: args.item1, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case PaynymClaimView.routeName: if (args is String) { return getRoute( @@ -507,11 +571,11 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case EditAddressLabelView.routeName: - if (args is AddressLabel) { + if (args is int) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => EditAddressLabelView( - addressLabel: args, + addressLabelId: args, ), settings: RouteSettings( name: settings.name, @@ -867,13 +931,12 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case ReceivingAddressesView.routeName: - if (args is Tuple2) { + case WalletAddressesView.routeName: + if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ReceivingAddressesView( - walletId: args.item1, - isDesktop: args.item2, + builder: (_) => WalletAddressesView( + walletId: args, ), settings: RouteSettings( name: settings.name, @@ -1258,6 +1321,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case DesktopCoinControlView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => DesktopCoinControlView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case BackupRestoreSettings.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index 53adc9892..d3a1e8495 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -18,12 +18,14 @@ import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/mixins/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/electrum_x_parsing.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; @@ -94,7 +96,12 @@ String constructDerivePath({ } class BitcoinWallet extends CoinServiceAPI - with WalletCache, WalletDB, ElectrumXParsing, PaynymWalletInterface { + with + WalletCache, + WalletDB, + ElectrumXParsing, + PaynymWalletInterface, + CoinControlInterface { BitcoinWallet({ required String walletId, required String walletName, @@ -114,6 +121,17 @@ class BitcoinWallet extends CoinServiceAPI _secureStore = secureStore; initCache(walletId, coin); initWalletDB(mockableOverride: mockableOverride); + initCoinControlInterface( + walletId: walletId, + walletName: walletName, + coin: coin, + db: db, + getChainHeight: () => chainHeight, + refreshedBalanceCallback: (balance) async { + _balance = balance; + await updateCachedBalance(_balance!); + }, + ); initPaynymWalletInterface( walletId: walletId, walletName: walletName, @@ -1106,6 +1124,7 @@ class BitcoinWallet extends CoinServiceAPI try { final feeRateType = args?["feeRate"]; final feeRateAmount = args?["feeRateAmount"]; + final utxos = args?["UTXOs"] as Set?; if (feeRateType is FeeRateType || feeRateAmount is int) { late final int rate; if (feeRateType is FeeRateType) { @@ -1133,8 +1152,16 @@ class BitcoinWallet extends CoinServiceAPI isSendAll = true; } - final txData = - await coinSelection(satoshiAmount, rate, address, isSendAll); + final bool coinControl = utxos != null; + + final txData = await coinSelection( + satoshiAmountToSend: satoshiAmount, + selectedTxFeeRate: rate, + recipientAddress: address, + isSendAll: isSendAll, + utxos: utxos?.toList(), + coinControl: coinControl, + ); Logging.instance.log("prepare send: $txData", level: LogLevel.Info); try { @@ -1197,6 +1224,11 @@ class BitcoinWallet extends CoinServiceAPI final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex); Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + final utxos = txData["usedUTXOs"] as List; + + // mark utxos as used + await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList()); + return txHash; } catch (e, s) { Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", @@ -1790,18 +1822,14 @@ class BitcoinWallet extends CoinServiceAPI } } - final currentChainHeight = await chainHeight; - final List outputArray = []; - int satoshiBalanceTotal = 0; - int satoshiBalancePending = 0; - int satoshiBalanceSpendable = 0; - int satoshiBalanceBlocked = 0; for (int i = 0; i < fetchedUtxoList.length; i++) { for (int j = 0; j < fetchedUtxoList[i].length; j++) { + final jsonUTXO = fetchedUtxoList[i][j]; + final txn = await cachedElectrumXClient.getTransaction( - txHash: fetchedUtxoList[i][j]["tx_hash"] as String, + txHash: jsonUTXO["tx_hash"] as String, verbose: true, coin: coin, ); @@ -1809,49 +1837,54 @@ class BitcoinWallet extends CoinServiceAPI // fetch stored tx to see if paynym notification tx and block utxo final storedTx = await db.getTransaction( walletId, - fetchedUtxoList[i][j]["tx_hash"] as String, + jsonUTXO["tx_hash"] as String, ); bool shouldBlock = false; String? blockReason; if (storedTx?.subType == - isar_models.TransactionSubType.bip47Notification && - storedTx?.type == isar_models.TransactionType.incoming) { - // probably safe to assume this is an incoming tx as it is a utxo - // belonging to this wallet. The extra check may be redundant but - // just in case... + isar_models.TransactionSubType.bip47Notification) { + if (storedTx?.type == isar_models.TransactionType.incoming) { + shouldBlock = true; + blockReason = "Incoming paynym notification transaction."; + } else if (storedTx?.type == isar_models.TransactionType.outgoing) { + shouldBlock = true; + blockReason = "Paynym notification change output. Incautious " + "handling of change outputs from notification transactions " + "may cause unintended loss of privacy."; + } + } - shouldBlock = true; - blockReason = "Incoming paynym notification transaction."; + final vout = jsonUTXO["tx_pos"] as int; + + final outputs = txn["vout"] as List; + + String? utxoOwnerAddress; + // get UTXO owner address + for (final output in outputs) { + if (output["n"] == vout) { + utxoOwnerAddress = + output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]?["address"] as String?; + } } final utxo = isar_models.UTXO( walletId: walletId, txid: txn["txid"] as String, - vout: fetchedUtxoList[i][j]["tx_pos"] as int, - value: fetchedUtxoList[i][j]["value"] as int, + vout: vout, + value: jsonUTXO["value"] as int, name: "", isBlocked: shouldBlock, blockedReason: blockReason, isCoinbase: txn["is_coinbase"] as bool? ?? false, blockHash: txn["blockhash"] as String?, - blockHeight: fetchedUtxoList[i][j]["height"] as int?, + blockHeight: jsonUTXO["height"] as int?, blockTime: txn["blocktime"] as int?, + address: utxoOwnerAddress, ); - satoshiBalanceTotal += utxo.value; - - if (utxo.isBlocked) { - satoshiBalanceBlocked += utxo.value; - } else { - if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - satoshiBalanceSpendable += utxo.value; - } else { - satoshiBalancePending += utxo.value; - } - } - outputArray.add(utxo); } } @@ -1859,27 +1892,20 @@ class BitcoinWallet extends CoinServiceAPI Logging.instance .log('Outputs fetched: $outputArray', level: LogLevel.Info); - // TODO move this out of here and into IDB - await db.isar.writeTxn(() async { - await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll(); - await db.isar.utxos.putAll(outputArray); - }); + await db.updateUTXOs(walletId, outputArray); // finally update balance - _balance = Balance( - coin: coin, - total: satoshiBalanceTotal, - spendable: satoshiBalanceSpendable, - blockedTotal: satoshiBalanceBlocked, - pendingSpendable: satoshiBalancePending, - ); - await updateCachedBalance(_balance!); + await _updateBalance(); } catch (e, s) { Logging.instance .log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error); } } + Future _updateBalance() async { + await refreshBalance(); + } + @override Balance get balance => _balance ??= getCachedBalance(); Balance? _balance; @@ -2252,11 +2278,12 @@ class BitcoinWallet extends CoinServiceAPI /// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return /// a map containing the tx hex along with other important information. If not, then it will return /// an integer (1 or 2) - dynamic coinSelection( - int satoshiAmountToSend, - int selectedTxFeeRate, - String _recipientAddress, - bool isSendAll, { + dynamic coinSelection({ + required int satoshiAmountToSend, + required int selectedTxFeeRate, + required String recipientAddress, + required bool coinControl, + required bool isSendAll, int additionalOutputs = 0, List? utxos, }) async { @@ -2268,18 +2295,26 @@ class BitcoinWallet extends CoinServiceAPI int spendableSatoshiValue = 0; // Build list of spendable outputs and totaling their satoshi amount - for (var i = 0; i < availableOutputs.length; i++) { - if (availableOutputs[i].isBlocked == false && - availableOutputs[i] - .isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) == - true) { - spendableOutputs.add(availableOutputs[i]); - spendableSatoshiValue += availableOutputs[i].value; + for (final utxo in availableOutputs) { + if (utxo.isBlocked == false && + utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) && + utxo.used != true) { + spendableOutputs.add(utxo); + spendableSatoshiValue += utxo.value; } } - // sort spendable by age (oldest first) - spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); + if (coinControl) { + if (spendableOutputs.length < availableOutputs.length) { + throw ArgumentError("Attempted to use an unavailable utxo"); + } + } + + // don't care about sorting if using all utxos + if (!coinControl) { + // sort spendable by age (oldest first) + spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); + } Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}", level: LogLevel.Info); @@ -2310,19 +2345,27 @@ class BitcoinWallet extends CoinServiceAPI int inputsBeingConsumed = 0; List utxoObjectsToUse = []; - for (var i = 0; - satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; - i++) { - utxoObjectsToUse.add(spendableOutputs[i]); - satoshisBeingUsed += spendableOutputs[i].value; - inputsBeingConsumed += 1; - } - for (int i = 0; - i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length; - i++) { - utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); - satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; - inputsBeingConsumed += 1; + if (!coinControl) { + for (var i = 0; + satoshisBeingUsed < satoshiAmountToSend && + i < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[i]); + satoshisBeingUsed += spendableOutputs[i].value; + inputsBeingConsumed += 1; + } + for (int i = 0; + i < additionalOutputs && + inputsBeingConsumed < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); + satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; + inputsBeingConsumed += 1; + } + } else { + satoshisBeingUsed = spendableSatoshiValue; + utxoObjectsToUse = spendableOutputs; + inputsBeingConsumed = spendableOutputs.length; } Logging.instance @@ -2333,7 +2376,7 @@ class BitcoinWallet extends CoinServiceAPI .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info); // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray - List recipientsArray = [_recipientAddress]; + List recipientsArray = [recipientAddress]; List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data @@ -2344,9 +2387,8 @@ class BitcoinWallet extends CoinServiceAPI .log("Attempting to send all $coin", level: LogLevel.Info); final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, - recipients: [_recipientAddress], + recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; int feeForOneOutput = estimateTxFee( @@ -2362,7 +2404,6 @@ class BitcoinWallet extends CoinServiceAPI final int amount = satoshiAmountToSend - feeForOneOutput; dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: [amount], @@ -2373,6 +2414,7 @@ class BitcoinWallet extends CoinServiceAPI "recipientAmt": amount, "fee": feeForOneOutput, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2380,9 +2422,8 @@ class BitcoinWallet extends CoinServiceAPI final int vSizeForOneOutput; try { vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, - recipients: [_recipientAddress], + recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; } catch (e) { @@ -2393,10 +2434,9 @@ class BitcoinWallet extends CoinServiceAPI final int vSizeForTwoOutPuts; try { vSizeForTwoOutPuts = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [ - _recipientAddress, + recipientAddress, await _getCurrentAddressForChain( 1, DerivePathTypeExt.primaryFor(coin)), ], @@ -2464,7 +2504,6 @@ class BitcoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2492,7 +2531,6 @@ class BitcoinWallet extends CoinServiceAPI Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2505,6 +2543,7 @@ class BitcoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeBeingPaid, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2521,7 +2560,6 @@ class BitcoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2532,6 +2570,7 @@ class BitcoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2550,7 +2589,6 @@ class BitcoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2561,6 +2599,7 @@ class BitcoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2579,7 +2618,6 @@ class BitcoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2590,6 +2628,7 @@ class BitcoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeForOneOutput, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2601,249 +2640,158 @@ class BitcoinWallet extends CoinServiceAPI level: LogLevel.Warning); // try adding more outputs if (spendableOutputs.length > inputsBeingConsumed) { - return coinSelection(satoshiAmountToSend, selectedTxFeeRate, - _recipientAddress, isSendAll, - additionalOutputs: additionalOutputs + 1, utxos: utxos); + return coinSelection( + satoshiAmountToSend: satoshiAmountToSend, + selectedTxFeeRate: selectedTxFeeRate, + recipientAddress: recipientAddress, + isSendAll: isSendAll, + additionalOutputs: additionalOutputs + 1, + utxos: utxos, + coinControl: coinControl, + ); } return 2; } } - Future> fetchBuildTxData( + Future> fetchBuildTxData( List utxosToUse, ) async { // return data - Map results = {}; - Map> addressTxid = {}; - - // addresses to check - List addressesP2PKH = []; - List addressesP2SH = []; - List addressesP2WPKH = []; + List signingData = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - final tx = await _cachedElectrumXClient.getTransaction( - txHash: txid, - coin: coin, - ); - - for (final output in tx["vout"] as List) { - final n = output["n"]; - if (n != null && n == utxosToUse[i].vout) { - final address = output["scriptPubKey"]["address"] as String; - if (!addressTxid.containsKey(address)) { - addressTxid[address] = []; - } - (addressTxid[address] as List).add(txid); - switch (addressType(address: address)) { - case DerivePathType.bip44: - addressesP2PKH.add(address); - break; - case DerivePathType.bip49: - addressesP2SH.add(address); - break; - case DerivePathType.bip84: - addressesP2WPKH.add(address); - break; - default: - throw Exception("DerivePathType unsupported"); + if (utxosToUse[i].address == null) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + utxosToUse[i] = utxosToUse[i].copyWith( + address: output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]["address"] as String, + ); } } } + + final derivePathType = addressType(address: utxosToUse[i].address!); + + signingData.add( + SigningData( + derivePathType: derivePathType, + utxo: utxosToUse[i], + ), + ); } - // p2pkh / bip44 - final p2pkhLength = addressesP2PKH.length; - if (p2pkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip44, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip44, - ); - for (int i = 0; i < p2pkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2PKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - ).data; + Map> receiveDerivations = {}; + Map> changeDerivations = {}; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2PKH( + for (final sd in signingData) { + String? pubKey; + String? wif; + + // fetch receiving derivations if null + receiveDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 0, + derivePathType: sd.derivePathType, + ); + final receiveDerivation = + receiveDerivations[sd.derivePathType]![sd.utxo.address!]; + + if (receiveDerivation != null) { + pubKey = receiveDerivation["pubKey"] as String; + wif = receiveDerivation["wif"] as String; + } else { + // fetch change derivations if null + changeDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 1, + derivePathType: sd.derivePathType, + ); + final changeDerivation = + changeDerivations[sd.derivePathType]![sd.utxo.address!]; + if (changeDerivation != null) { + pubKey = changeDerivation["pubKey"] as String; + wif = changeDerivation["wif"] as String; + } + } + + if (wif == null || pubKey == null) { + final address = await db.getAddress(walletId, sd.utxo.address!); + if (address?.derivationPath != null) { + final node = await Bip32Utils.getBip32Node( + (await mnemonicString)!, + (await mnemonicPassphrase)!, + _network, + address!.derivationPath!.value, + ); + + wif = node.toWIF(); + pubKey = Format.uint8listToString(node.publicKey); + } + } + + if (wif != null && pubKey != null) { + final PaymentData data; + final Uint8List? redeemScript; + + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = P2PKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: _network, ).data; + redeemScript = null; + break; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } - } - } - } - - // p2sh / bip49 - final p2shLength = addressesP2SH.length; - if (p2shLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip49, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip49, - ); - for (int i = 0; i < p2shLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2SH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final p2wpkh = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network) - .data; - - final redeemScript = p2wpkh.output; - - final data = - P2SH(data: PaymentData(redeem: p2wpkh), network: _network).data; - - for (String tx in addressTxid[addressesP2SH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - "redeemScript": redeemScript, - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2SH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { + case DerivePathType.bip49: final p2wpkh = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), - network: _network) - .data; - - final redeemScript = p2wpkh.output; - - final data = - P2SH(data: PaymentData(redeem: p2wpkh), network: _network) - .data; - - for (String tx in addressTxid[addressesP2SH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - "redeemScript": redeemScript, - }; - } - } - } - } - } - - // p2wpkh / bip84 - final p2wpkhLength = addressesP2WPKH.length; - if (p2wpkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip84, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip84, - ); - - for (int i = 0; i < p2wpkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - ).data; - - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2WPKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: _network, ).data; + redeemScript = p2wpkh.output; + data = P2SH( + data: PaymentData(redeem: p2wpkh), + network: _network, + ).data; + break; - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } + case DerivePathType.bip84: + data = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List(pubKey), + ), + network: _network, + ).data; + redeemScript = null; + break; + + default: + throw Exception("DerivePathType unsupported"); } + + final keyPair = ECPair.fromWIF( + wif, + network: _network, + ); + + sd.redeemScript = redeemScript; + sd.output = data.output; + sd.keyPair = keyPair; } } - return results; + return signingData; } catch (e, s) { Logging.instance .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); @@ -2853,8 +2801,7 @@ class BitcoinWallet extends CoinServiceAPI /// Builds and signs a transaction Future> buildTransaction({ - required List utxosToUse, - required Map utxoSigningData, + required List utxoSigningData, required List recipients, required List satoshiAmounts, }) async { @@ -2865,10 +2812,14 @@ class BitcoinWallet extends CoinServiceAPI txb.setVersion(1); // Add transaction inputs - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - txb.addInput(txid, utxosToUse[i].vout, null, - utxoSigningData[txid]["output"] as Uint8List); + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + txb.addInput( + txid, + utxoSigningData[i].utxo.vout, + null, + utxoSigningData[i].output!, + ); } // Add transaction output @@ -2878,13 +2829,12 @@ class BitcoinWallet extends CoinServiceAPI try { // Sign the transaction accordingly - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; + for (var i = 0; i < utxoSigningData.length; i++) { txb.sign( vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, + keyPair: utxoSigningData[i].keyPair!, + witnessValue: utxoSigningData[i].utxo.value, + redeemScript: utxoSigningData[i].redeemScript, ); } } catch (e, s) { diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index b20f34309..bd934552f 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -24,6 +24,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/mixins/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; @@ -98,7 +99,8 @@ String constructDerivePath({ return "m/$purpose'/$coinType'/$account'/$chain/$index"; } -class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { +class BitcoinCashWallet extends CoinServiceAPI + with WalletCache, WalletDB, CoinControlInterface { BitcoinCashWallet({ required String walletId, required String walletName, @@ -118,6 +120,17 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { _secureStore = secureStore; initCache(walletId, coin); initWalletDB(mockableOverride: mockableOverride); + initCoinControlInterface( + walletId: walletId, + walletName: walletName, + coin: coin, + db: db, + getChainHeight: () => chainHeight, + refreshedBalanceCallback: (balance) async { + _balance = balance; + await updateCachedBalance(_balance!); + }, + ); } static const integrationTestFlag = @@ -1041,6 +1054,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { try { final feeRateType = args?["feeRate"]; final feeRateAmount = args?["feeRateAmount"]; + final utxos = args?["UTXOs"] as Set?; if (feeRateType is FeeRateType || feeRateAmount is int) { late final int rate; if (feeRateType is FeeRateType) { @@ -1067,9 +1081,19 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { isSendAll = true; } - final result = - await coinSelection(satoshiAmount, rate, address, isSendAll); - Logging.instance.log("SEND RESULT: $result", level: LogLevel.Info); + final bool coinControl = utxos != null; + + final result = await coinSelection( + satoshiAmountToSend: satoshiAmount, + selectedTxFeeRate: rate, + recipientAddress: address, + isSendAll: isSendAll, + utxos: utxos?.toList(), + coinControl: coinControl, + ); + + Logging.instance + .log("PREPARE SEND RESULT: $result", level: LogLevel.Info); if (result is int) { switch (result) { case 1: @@ -1115,6 +1139,12 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { final txHash = await _electrumXClient.broadcastTransaction( rawTx: txData["hex"] as String); Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + + final utxos = txData["usedUTXOs"] as List; + + // mark utxos as used + await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList()); + return txHash; } catch (e, s) { Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", @@ -1720,49 +1750,47 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { } } - final currentChainHeight = await chainHeight; - final List outputArray = []; - int satoshiBalanceTotal = 0; - int satoshiBalancePending = 0; - int satoshiBalanceSpendable = 0; - int satoshiBalanceBlocked = 0; for (int i = 0; i < fetchedUtxoList.length; i++) { for (int j = 0; j < fetchedUtxoList[i].length; j++) { + final jsonUTXO = fetchedUtxoList[i][j]; + final txn = await cachedElectrumXClient.getTransaction( - txHash: fetchedUtxoList[i][j]["tx_hash"] as String, + txHash: jsonUTXO["tx_hash"] as String, verbose: true, coin: coin, ); - // todo check here if we should mark as blocked + final vout = jsonUTXO["tx_pos"] as int; + + final outputs = txn["vout"] as List; + + String? utxoOwnerAddress; + // get UTXO owner address + for (final output in outputs) { + if (output["n"] == vout) { + utxoOwnerAddress = + output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]?["address"] as String?; + } + } + final utxo = isar_models.UTXO( walletId: walletId, txid: txn["txid"] as String, - vout: fetchedUtxoList[i][j]["tx_pos"] as int, - value: fetchedUtxoList[i][j]["value"] as int, + vout: vout, + value: jsonUTXO["value"] as int, name: "", isBlocked: false, blockedReason: null, isCoinbase: txn["is_coinbase"] as bool? ?? false, blockHash: txn["blockhash"] as String?, - blockHeight: fetchedUtxoList[i][j]["height"] as int?, + blockHeight: jsonUTXO["height"] as int?, blockTime: txn["blocktime"] as int?, + address: utxoOwnerAddress, ); - satoshiBalanceTotal += utxo.value; - - if (utxo.isBlocked) { - satoshiBalanceBlocked += utxo.value; - } else { - if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - satoshiBalanceSpendable += utxo.value; - } else { - satoshiBalancePending += utxo.value; - } - } - outputArray.add(utxo); } } @@ -1770,27 +1798,20 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { Logging.instance .log('Outputs fetched: $outputArray', level: LogLevel.Info); - // TODO move this out of here and into IDB - await db.isar.writeTxn(() async { - await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll(); - await db.isar.utxos.putAll(outputArray); - }); + await db.updateUTXOs(walletId, outputArray); // finally update balance - _balance = Balance( - coin: coin, - total: satoshiBalanceTotal, - spendable: satoshiBalanceSpendable, - blockedTotal: satoshiBalanceBlocked, - pendingSpendable: satoshiBalancePending, - ); - await updateCachedBalance(_balance!); + await _updateBalance(); } catch (e, s) { Logging.instance .log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error); } } + Future _updateBalance() async { + await refreshBalance(); + } + @override Balance get balance => _balance ??= getCachedBalance(); Balance? _balance; @@ -2323,11 +2344,12 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { /// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return /// a map containing the tx hex along with other important information. If not, then it will return /// an integer (1 or 2) - dynamic coinSelection( - int satoshiAmountToSend, - int selectedTxFeeRate, - String _recipientAddress, - bool isSendAll, { + dynamic coinSelection({ + required int satoshiAmountToSend, + required int selectedTxFeeRate, + required String recipientAddress, + required bool coinControl, + required bool isSendAll, int additionalOutputs = 0, List? utxos, }) async { @@ -2339,18 +2361,26 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { int spendableSatoshiValue = 0; // Build list of spendable outputs and totaling their satoshi amount - for (var i = 0; i < availableOutputs.length; i++) { - if (availableOutputs[i].isBlocked == false && - availableOutputs[i] - .isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) == - true) { - spendableOutputs.add(availableOutputs[i]); - spendableSatoshiValue += availableOutputs[i].value; + for (final utxo in availableOutputs) { + if (utxo.isBlocked == false && + utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) && + utxo.used != true) { + spendableOutputs.add(utxo); + spendableSatoshiValue += utxo.value; } } - // sort spendable by age (oldest first) - spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); + if (coinControl) { + if (spendableOutputs.length < availableOutputs.length) { + throw ArgumentError("Attempted to use an unavailable utxo"); + } + } + + // don't care about sorting if using all utxos + if (!coinControl) { + // sort spendable by age (oldest first) + spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); + } Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}", level: LogLevel.Info); @@ -2379,19 +2409,27 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { int inputsBeingConsumed = 0; List utxoObjectsToUse = []; - for (var i = 0; - satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; - i++) { - utxoObjectsToUse.add(spendableOutputs[i]); - satoshisBeingUsed += spendableOutputs[i].value; - inputsBeingConsumed += 1; - } - for (int i = 0; - i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length; - i++) { - utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); - satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; - inputsBeingConsumed += 1; + if (!coinControl) { + for (var i = 0; + satoshisBeingUsed < satoshiAmountToSend && + i < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[i]); + satoshisBeingUsed += spendableOutputs[i].value; + inputsBeingConsumed += 1; + } + for (int i = 0; + i < additionalOutputs && + inputsBeingConsumed < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); + satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; + inputsBeingConsumed += 1; + } + } else { + satoshisBeingUsed = spendableSatoshiValue; + utxoObjectsToUse = spendableOutputs; + inputsBeingConsumed = spendableOutputs.length; } Logging.instance @@ -2404,7 +2442,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { .log('satoshiAmountToSend $satoshiAmountToSend', level: LogLevel.Info); // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray - List recipientsArray = [_recipientAddress]; + List recipientsArray = [recipientAddress]; List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data @@ -2417,7 +2455,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { final int vSizeForOneOutput = (await buildTransaction( utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, - recipients: [_recipientAddress], + recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; int feeForOneOutput = estimateTxFee( @@ -2441,6 +2479,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { "recipientAmt": amount, "fee": feeForOneOutput, "vSize": txn["vSize"], + "usedUTXOs": utxoObjectsToUse, }; return transactionObject; } @@ -2448,14 +2487,14 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { final int vSizeForOneOutput = (await buildTransaction( utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, - recipients: [_recipientAddress], + recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; final int vSizeForTwoOutPuts = (await buildTransaction( utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [ - _recipientAddress, + recipientAddress, await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)), ], satoshiAmounts: [ @@ -2573,6 +2612,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { "recipientAmt": recipientsAmtArray[0], "fee": feeBeingPaid, "vSize": txn["vSize"], + "usedUTXOs": utxoObjectsToUse, }; return transactionObject; } else { @@ -2600,6 +2640,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], + "usedUTXOs": utxoObjectsToUse, }; return transactionObject; } @@ -2629,6 +2670,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], + "usedUTXOs": utxoObjectsToUse, }; return transactionObject; } @@ -2658,6 +2700,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { "recipientAmt": recipientsAmtArray[0], "fee": feeForOneOutput, "vSize": txn["vSize"], + "usedUTXOs": utxoObjectsToUse, }; return transactionObject; } else { @@ -2669,9 +2712,15 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { level: LogLevel.Warning); // try adding more outputs if (spendableOutputs.length > inputsBeingConsumed) { - return coinSelection(satoshiAmountToSend, selectedTxFeeRate, - _recipientAddress, isSendAll, - additionalOutputs: additionalOutputs + 1, utxos: utxos); + return coinSelection( + satoshiAmountToSend: satoshiAmountToSend, + selectedTxFeeRate: selectedTxFeeRate, + recipientAddress: recipientAddress, + isSendAll: isSendAll, + additionalOutputs: additionalOutputs + 1, + utxos: utxos, + coinControl: coinControl, + ); } return 2; } diff --git a/lib/services/coins/coin_paynym_extension.dart b/lib/services/coins/coin_paynym_extension.dart deleted file mode 100644 index 082976f67..000000000 --- a/lib/services/coins/coin_paynym_extension.dart +++ /dev/null @@ -1,815 +0,0 @@ -// import 'dart:convert'; -// import 'dart:typed_data'; -// -// import 'package:bip32/bip32.dart' as bip32; -// import 'package:bip47/bip47.dart'; -// import 'package:bip47/src/util.dart'; -// import 'package:bitcoindart/bitcoindart.dart' as btc_dart; -// import 'package:bitcoindart/src/utils/constants/op.dart' as op; -// import 'package:bitcoindart/src/utils/script.dart' as bscript; -// import 'package:isar/isar.dart'; -// import 'package:pointycastle/digests/sha256.dart'; -// import 'package:stackwallet/exceptions/wallet/insufficient_balance_exception.dart'; -// import 'package:stackwallet/exceptions/wallet/paynym_send_exception.dart'; -// import 'package:stackwallet/models/isar/models/isar_models.dart'; -// import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; -// import 'package:stackwallet/utilities/bip32_utils.dart'; -// import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; -// import 'package:stackwallet/utilities/format.dart'; -// import 'package:stackwallet/utilities/logger.dart'; -// import 'package:tuple/tuple.dart'; -// -// const kPaynymDerivePath = "m/47'/0'/0'"; -// -// extension PayNym on DogecoinWallet { -// // generate bip32 payment code root -// Future getRootNode({ -// required List mnemonic, -// }) async { -// final root = await Bip32Utils.getBip32Root(mnemonic.join(" "), network); -// return root; -// } -// -// Future deriveNotificationPrivateKey({ -// required List mnemonic, -// }) async { -// final root = await getRootNode(mnemonic: mnemonic); -// final node = root.derivePath(kPaynymDerivePath).derive(0); -// return node.privateKey!; -// } -// -// /// fetch or generate this wallet's bip47 payment code -// Future getPaymentCode( -// DerivePathType derivePathType, -// ) async { -// final address = await getMyNotificationAddress(derivePathType); -// final paymentCode = PaymentCode.fromPaymentCode( -// address.otherData!, -// network, -// ); -// return paymentCode; -// } -// -// Future signWithNotificationKey(Uint8List data) async { -// final privateKey = -// await deriveNotificationPrivateKey(mnemonic: await mnemonic); -// final pair = btc_dart.ECPair.fromPrivateKey(privateKey, network: network); -// final signed = pair.sign(SHA256Digest().process(data)); -// return signed; -// } -// -// Future signStringWithNotificationKey(String data) async { -// final bytes = -// await signWithNotificationKey(Uint8List.fromList(utf8.encode(data))); -// return Format.uint8listToString(bytes); -// } -// -// Future>> preparePaymentCodeSend( -// {required PaymentCode paymentCode, -// required int satoshiAmount, -// Map? args}) async { -// if (!(await hasConnected(paymentCode.notificationAddressP2PKH()))) { -// throw PaynymSendException( -// "No notification transaction sent to $paymentCode"); -// } else { -// final myPrivateKey = -// await deriveNotificationPrivateKey(mnemonic: await mnemonic); -// final sendToAddress = await nextUnusedSendAddressFrom( -// pCode: paymentCode, -// privateKey: myPrivateKey, -// ); -// -// return prepareSend( -// address: sendToAddress.value, satoshiAmount: satoshiAmount); -// } -// } -// -// /// get the next unused address to send to given the receiver's payment code -// /// and your own private key -// Future

nextUnusedSendAddressFrom({ -// required PaymentCode pCode, -// required Uint8List privateKey, -// int startIndex = 0, -// }) async { -// // https://en.bitcoin.it/wiki/BIP_0047#Path_levels -// const maxCount = 2147483647; -// -// for (int i = startIndex; i < maxCount; i++) { -// final address = await db -// .getAddresses(walletId) -// .filter() -// .subTypeEqualTo(AddressSubType.paynymSend) -// .and() -// .otherDataEqualTo(pCode.toString()) -// .and() -// .derivationIndexEqualTo(i) -// .findFirst(); -// -// if (address != null) { -// final count = await getTxCount(address: address.value); -// // return address if unused, otherwise continue to next index -// if (count == 0) { -// return address; -// } -// } else { -// final pair = PaymentAddress.initWithPrivateKey( -// privateKey, -// pCode, -// i, // index to use -// ).getSendAddressKeyPair(); -// -// // add address to local db -// final address = generatePaynymSendAddressFromKeyPair( -// pair: pair, -// derivationIndex: i, -// derivePathType: DerivePathType.bip44, -// toPaymentCode: pCode, -// ); -// await db.putAddress(address); -// -// final count = await getTxCount(address: address.value); -// // return address if unused, otherwise continue to next index -// if (count == 0) { -// return address; -// } -// } -// } -// -// throw PaynymSendException("Exhausted unused send addresses!"); -// } -// -// Future> prepareNotificationTx({ -// required int selectedTxFeeRate, -// required String targetPaymentCodeString, -// int additionalOutputs = 0, -// List? utxos, -// }) async { -// const amountToSend = DUST_LIMIT; -// final List availableOutputs = utxos ?? await this.utxos; -// final List spendableOutputs = []; -// int spendableSatoshiValue = 0; -// -// // Build list of spendable outputs and totaling their satoshi amount -// for (var i = 0; i < availableOutputs.length; i++) { -// if (availableOutputs[i].isBlocked == false && -// availableOutputs[i] -// .isConfirmed(await chainHeight, MINIMUM_CONFIRMATIONS) == -// true) { -// spendableOutputs.add(availableOutputs[i]); -// spendableSatoshiValue += availableOutputs[i].value; -// } -// } -// -// if (spendableSatoshiValue < amountToSend) { -// // insufficient balance -// throw InsufficientBalanceException( -// "Spendable balance is less than the minimum required for a notification transaction."); -// } else if (spendableSatoshiValue == amountToSend) { -// // insufficient balance due to missing amount to cover fee -// throw InsufficientBalanceException( -// "Remaining balance does not cover the network fee."); -// } -// -// // sort spendable by age (oldest first) -// spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); -// -// int satoshisBeingUsed = 0; -// int outputsBeingUsed = 0; -// List utxoObjectsToUse = []; -// -// for (int i = 0; -// satoshisBeingUsed < amountToSend && i < spendableOutputs.length; -// i++) { -// utxoObjectsToUse.add(spendableOutputs[i]); -// satoshisBeingUsed += spendableOutputs[i].value; -// outputsBeingUsed += 1; -// } -// -// // add additional outputs if required -// for (int i = 0; -// i < additionalOutputs && outputsBeingUsed < spendableOutputs.length; -// i++) { -// utxoObjectsToUse.add(spendableOutputs[outputsBeingUsed]); -// satoshisBeingUsed += spendableOutputs[outputsBeingUsed].value; -// outputsBeingUsed += 1; -// } -// -// // gather required signing data -// final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); -// -// final int vSizeForNoChange = (await _createNotificationTx( -// targetPaymentCodeString: targetPaymentCodeString, -// utxosToUse: utxoObjectsToUse, -// utxoSigningData: utxoSigningData, -// change: 0)) -// .item2; -// -// final int vSizeForWithChange = (await _createNotificationTx( -// targetPaymentCodeString: targetPaymentCodeString, -// utxosToUse: utxoObjectsToUse, -// utxoSigningData: utxoSigningData, -// change: satoshisBeingUsed - amountToSend)) -// .item2; -// -// // Assume 2 outputs, for recipient and payment code script -// int feeForNoChange = estimateTxFee( -// vSize: vSizeForNoChange, -// feeRatePerKB: selectedTxFeeRate, -// ); -// -// // Assume 3 outputs, for recipient, payment code script, and change -// int feeForWithChange = estimateTxFee( -// vSize: vSizeForWithChange, -// feeRatePerKB: selectedTxFeeRate, -// ); -// -// if (feeForNoChange < vSizeForNoChange * 1000) { -// feeForNoChange = vSizeForNoChange * 1000; -// } -// if (feeForWithChange < vSizeForWithChange * 1000) { -// feeForWithChange = vSizeForWithChange * 1000; -// } -// -// if (satoshisBeingUsed - amountToSend > feeForNoChange + DUST_LIMIT) { -// // try to add change output due to "left over" amount being greater than -// // the estimated fee + the dust limit -// int changeAmount = satoshisBeingUsed - amountToSend - feeForWithChange; -// -// // check estimates are correct and build notification tx -// if (changeAmount >= DUST_LIMIT && -// satoshisBeingUsed - amountToSend - changeAmount == feeForWithChange) { -// final txn = await _createNotificationTx( -// targetPaymentCodeString: targetPaymentCodeString, -// utxosToUse: utxoObjectsToUse, -// utxoSigningData: utxoSigningData, -// change: changeAmount, -// ); -// -// int feeBeingPaid = satoshisBeingUsed - amountToSend - changeAmount; -// -// Map transactionObject = { -// "hex": txn.item1, -// "recipientPaynym": targetPaymentCodeString, -// "amount": amountToSend, -// "fee": feeBeingPaid, -// "vSize": txn.item2, -// }; -// return transactionObject; -// } else { -// // something broke during fee estimation or the change amount is smaller -// // than the dust limit. Try without change -// final txn = await _createNotificationTx( -// targetPaymentCodeString: targetPaymentCodeString, -// utxosToUse: utxoObjectsToUse, -// utxoSigningData: utxoSigningData, -// change: 0, -// ); -// -// int feeBeingPaid = satoshisBeingUsed - amountToSend; -// -// Map transactionObject = { -// "hex": txn.item1, -// "recipientPaynym": targetPaymentCodeString, -// "amount": amountToSend, -// "fee": feeBeingPaid, -// "vSize": txn.item2, -// }; -// return transactionObject; -// } -// } else if (satoshisBeingUsed - amountToSend >= feeForNoChange) { -// // since we already checked if we need to add a change output we can just -// // build without change here -// final txn = await _createNotificationTx( -// targetPaymentCodeString: targetPaymentCodeString, -// utxosToUse: utxoObjectsToUse, -// utxoSigningData: utxoSigningData, -// change: 0, -// ); -// -// int feeBeingPaid = satoshisBeingUsed - amountToSend; -// -// Map transactionObject = { -// "hex": txn.item1, -// "recipientPaynym": targetPaymentCodeString, -// "amount": amountToSend, -// "fee": feeBeingPaid, -// "vSize": txn.item2, -// }; -// return transactionObject; -// } else { -// // if we get here we do not have enough funds to cover the tx total so we -// // check if we have any more available outputs and try again -// if (spendableOutputs.length > outputsBeingUsed) { -// return prepareNotificationTx( -// selectedTxFeeRate: selectedTxFeeRate, -// targetPaymentCodeString: targetPaymentCodeString, -// additionalOutputs: additionalOutputs + 1, -// ); -// } else { -// throw InsufficientBalanceException( -// "Remaining balance does not cover the network fee."); -// } -// } -// } -// -// // return tuple with string value equal to the raw tx hex and the int value -// // equal to its vSize -// Future> _createNotificationTx({ -// required String targetPaymentCodeString, -// required List utxosToUse, -// required Map utxoSigningData, -// required int change, -// }) async { -// final targetPaymentCode = -// PaymentCode.fromPaymentCode(targetPaymentCodeString, network); -// final myCode = await getPaymentCode(DerivePathType.bip44); -// -// final utxo = utxosToUse.first; -// final txPoint = utxo.txid.fromHex.toList(); -// final txPointIndex = utxo.vout; -// -// final rev = Uint8List(txPoint.length + 4); -// Util.copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length); -// final buffer = rev.buffer.asByteData(); -// buffer.setUint32(txPoint.length, txPointIndex, Endian.little); -// -// final myKeyPair = utxoSigningData[utxo.txid]["keyPair"] as btc_dart.ECPair; -// -// final S = SecretPoint( -// myKeyPair.privateKey!, -// targetPaymentCode.notificationPublicKey(), -// ); -// -// final blindingMask = PaymentCode.getMask(S.ecdhSecret(), rev); -// -// final blindedPaymentCode = PaymentCode.blind( -// myCode.getPayload(), -// blindingMask, -// ); -// -// final opReturnScript = bscript.compile([ -// (op.OPS["OP_RETURN"] as int), -// blindedPaymentCode, -// ]); -// -// // build a notification tx -// final txb = btc_dart.TransactionBuilder(network: network); -// txb.setVersion(1); -// -// txb.addInput( -// utxo.txid, -// txPointIndex, -// ); -// -// // todo: modify address once segwit support is in our bip47 -// txb.addOutput(targetPaymentCode.notificationAddressP2PKH(), DUST_LIMIT); -// txb.addOutput(opReturnScript, 0); -// -// // TODO: add possible change output and mark output as dangerous -// if (change > 0) { -// // generate new change address if current change address has been used -// await checkChangeAddressForTransactions(); -// final String changeAddress = await currentChangeAddress; -// txb.addOutput(changeAddress, change); -// } -// -// txb.sign( -// vin: 0, -// keyPair: myKeyPair, -// ); -// -// // sign rest of possible inputs -// for (var i = 1; i < utxosToUse.length - 1; i++) { -// final txid = utxosToUse[i].txid; -// txb.sign( -// vin: i, -// keyPair: utxoSigningData[txid]["keyPair"] as btc_dart.ECPair, -// // witnessValue: utxosToUse[i].value, -// ); -// } -// -// final builtTx = txb.build(); -// -// return Tuple2(builtTx.toHex(), builtTx.virtualSize()); -// } -// -// Future broadcastNotificationTx( -// {required Map preparedTx}) async { -// try { -// Logging.instance.log("confirmNotificationTx txData: $preparedTx", -// level: LogLevel.Info); -// final txHash = await electrumXClient.broadcastTransaction( -// rawTx: preparedTx["hex"] as String); -// Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); -// -// // TODO: only refresh transaction data -// try { -// await refresh(); -// } catch (e) { -// Logging.instance.log( -// "refresh() failed in confirmNotificationTx ($walletName::$walletId): $e", -// level: LogLevel.Error, -// ); -// } -// -// return txHash; -// } catch (e, s) { -// Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", -// level: LogLevel.Error); -// rethrow; -// } -// } -// -// // TODO optimize -// Future hasConnected(String paymentCodeString) async { -// final myNotificationAddress = -// await getMyNotificationAddress(DerivePathTypeExt.primaryFor(coin)); -// -// final txns = await db -// .getTransactions(walletId) -// .filter() -// .subTypeEqualTo(TransactionSubType.bip47Notification) -// .findAll(); -// -// for (final tx in txns) { -// // quick check that may cause problems? -// if (tx.address.value?.value == myNotificationAddress.value) { -// return true; -// } -// -// final unBlindedPaymentCode = await unBlindedPaymentCodeFromTransaction( -// transaction: tx, -// myNotificationAddress: myNotificationAddress, -// ); -// -// if (paymentCodeString == unBlindedPaymentCode.toString()) { -// return true; -// } -// } -// -// // otherwise return no -// return false; -// } -// -// Future unBlindedPaymentCodeFromTransaction({ -// required Transaction transaction, -// required Address myNotificationAddress, -// }) async { -// if (transaction.address.value != null && -// transaction.address.value!.value != myNotificationAddress.value) { -// return null; -// } -// -// try { -// final blindedCode = -// transaction.outputs.elementAt(1).scriptPubKeyAsm!.split(" ")[1]; -// -// final designatedInput = transaction.inputs.first; -// -// final txPoint = designatedInput.txid.fromHex.toList(); -// final txPointIndex = designatedInput.vout; -// -// final rev = Uint8List(txPoint.length + 4); -// Util.copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length); -// final buffer = rev.buffer.asByteData(); -// buffer.setUint32(txPoint.length, txPointIndex, Endian.little); -// -// final pubKey = designatedInput.scriptSigAsm!.split(" ")[1].fromHex; -// -// final myPrivateKey = -// await deriveNotificationPrivateKey(mnemonic: await mnemonic); -// -// final S = SecretPoint(myPrivateKey, pubKey); -// -// final mask = PaymentCode.getMask(S.ecdhSecret(), rev); -// -// final unBlindedPayload = PaymentCode.blind(blindedCode.fromHex, mask); -// -// final unBlindedPaymentCode = -// PaymentCode.initFromPayload(unBlindedPayload); -// -// return unBlindedPaymentCode; -// } catch (e) { -// Logging.instance.log( -// "unBlindedPaymentCodeFromTransaction() failed: $e", -// level: LogLevel.Warning, -// ); -// return null; -// } -// } -// -// Future> -// getAllPaymentCodesFromNotificationTransactions() async { -// final myAddress = -// await getMyNotificationAddress(DerivePathTypeExt.primaryFor(coin)); -// final txns = await db -// .getTransactions(walletId) -// .filter() -// .subTypeEqualTo(TransactionSubType.bip47Notification) -// .findAll(); -// -// List unBlindedList = []; -// -// for (final tx in txns) { -// final unBlinded = await unBlindedPaymentCodeFromTransaction( -// transaction: tx, -// myNotificationAddress: myAddress, -// ); -// if (unBlinded != null) { -// unBlindedList.add(unBlinded); -// } -// } -// -// return unBlindedList; -// } -// -// Future restoreHistoryWith( -// PaymentCode other, -// int maxUnusedAddressGap, -// int maxNumberOfIndexesToCheck, -// ) async { -// // https://en.bitcoin.it/wiki/BIP_0047#Path_levels -// const maxCount = 2147483647; -// assert(maxNumberOfIndexesToCheck < maxCount); -// -// final myPrivateKey = -// await deriveNotificationPrivateKey(mnemonic: await mnemonic); -// -// List
addresses = []; -// int receivingGapCounter = 0; -// int outgoingGapCounter = 0; -// -// for (int i = 0; -// i < maxNumberOfIndexesToCheck && -// (receivingGapCounter < maxUnusedAddressGap || -// outgoingGapCounter < maxUnusedAddressGap); -// i++) { -// final paymentAddress = PaymentAddress.initWithPrivateKey( -// myPrivateKey, -// other, -// i, // index to use -// ); -// -// if (receivingGapCounter < maxUnusedAddressGap) { -// final pair = paymentAddress.getSendAddressKeyPair(); -// final address = generatePaynymSendAddressFromKeyPair( -// pair: pair, -// derivationIndex: i, -// derivePathType: DerivePathType.bip44, -// toPaymentCode: other, -// ); -// addresses.add(address); -// -// final count = await getTxCount(address: address.value); -// -// if (count > 0) { -// receivingGapCounter++; -// } else { -// receivingGapCounter = 0; -// } -// } -// -// if (outgoingGapCounter < maxUnusedAddressGap) { -// final pair = paymentAddress.getReceiveAddressKeyPair(); -// final address = generatePaynymReceivingAddressFromKeyPair( -// pair: pair, -// derivationIndex: i, -// derivePathType: DerivePathType.bip44, -// fromPaymentCode: other, -// ); -// addresses.add(address); -// -// final count = await getTxCount(address: address.value); -// -// if (count > 0) { -// outgoingGapCounter++; -// } else { -// outgoingGapCounter = 0; -// } -// } -// } -// await db.putAddresses(addresses); -// } -// -// Address generatePaynymSendAddressFromKeyPair({ -// required btc_dart.ECPair pair, -// required int derivationIndex, -// required DerivePathType derivePathType, -// required PaymentCode toPaymentCode, -// }) { -// final data = btc_dart.PaymentData(pubkey: pair.publicKey); -// -// String addressString; -// switch (derivePathType) { -// case DerivePathType.bip44: -// addressString = -// btc_dart.P2PKH(data: data, network: network).data.address!; -// break; -// -// // The following doesn't apply currently -// // case DerivePathType.bip49: -// // addressString = btc_dart -// // .P2SH( -// // data: btc_dart.PaymentData( -// // redeem: btc_dart -// // .P2WPKH( -// // data: data, -// // network: network, -// // ) -// // .data), -// // network: network, -// // ) -// // .data -// // .address!; -// // break; -// // -// // case DerivePathType.bip84: -// // addressString = btc_dart -// // .P2WPKH( -// // network: network, -// // data: data, -// // ) -// // .data -// // .address!; -// // break; -// default: -// throw UnimplementedError("segwit paynyms not implemented yet"); -// } -// -// final address = Address( -// walletId: walletId, -// value: addressString, -// publicKey: pair.publicKey, -// derivationIndex: derivationIndex, -// type: AddressType.nonWallet, -// subType: AddressSubType.paynymSend, -// otherData: toPaymentCode.toString(), -// ); -// -// return address; -// } -// -// Address generatePaynymReceivingAddressFromKeyPair({ -// required btc_dart.ECPair pair, -// required int derivationIndex, -// required DerivePathType derivePathType, -// required PaymentCode fromPaymentCode, -// }) { -// final data = btc_dart.PaymentData(pubkey: pair.publicKey); -// -// String addressString; -// AddressType addrType; -// switch (derivePathType) { -// case DerivePathType.bip44: -// addressString = btc_dart -// .P2PKH( -// data: data, -// network: network, -// ) -// .data -// .address!; -// addrType = AddressType.p2pkh; -// break; -// -// // The following doesn't apply currently -// // case DerivePathType.bip49: -// // addressString = btc_dart -// // .P2SH( -// // data: btc_dart.PaymentData( -// // redeem: btc_dart -// // .P2WPKH( -// // data: data, -// // network: network, -// // ) -// // .data), -// // network: network, -// // ) -// // .data -// // .address!; -// // addrType = AddressType.p2sh; -// // break; -// // -// // case DerivePathType.bip84: -// // addressString = btc_dart -// // .P2WPKH( -// // network: network, -// // data: data, -// // ) -// // .data -// // .address!; -// // addrType = AddressType.p2wpkh; -// // break; -// default: -// throw UnimplementedError("segwit paynyms not implemented yet"); -// } -// -// final address = Address( -// walletId: walletId, -// value: addressString, -// publicKey: pair.publicKey, -// derivationIndex: derivationIndex, -// type: addrType, -// subType: AddressSubType.paynymReceive, -// otherData: fromPaymentCode.toString(), -// ); -// -// return address; -// } -// -// Future
getMyNotificationAddress( -// DerivePathType derivePathType, -// ) async { -// // TODO: fix when segwit is here -// derivePathType = DerivePathType.bip44; -// -// AddressType type; -// switch (derivePathType) { -// case DerivePathType.bip44: -// type = AddressType.p2pkh; -// break; -// case DerivePathType.bip49: -// type = AddressType.p2sh; -// break; -// case DerivePathType.bip84: -// type = AddressType.p2wpkh; -// break; -// } -// -// final storedAddress = await db -// .getAddresses(walletId) -// .filter() -// .subTypeEqualTo(AddressSubType.paynymNotification) -// .and() -// .typeEqualTo(type) -// .and() -// .not() -// .typeEqualTo(AddressType.nonWallet) -// .findFirst(); -// -// if (storedAddress != null) { -// return storedAddress; -// } else { -// final root = await getRootNode(mnemonic: await mnemonic); -// final node = root.derivePath(kPaynymDerivePath); -// final paymentCode = PaymentCode.initFromPubKey( -// node.publicKey, -// node.chainCode, -// network, -// ); -// -// String addressString; -// final data = -// btc_dart.PaymentData(pubkey: paymentCode.notificationPublicKey()); -// switch (derivePathType) { -// case DerivePathType.bip44: -// addressString = btc_dart -// .P2PKH( -// data: data, -// network: network, -// ) -// .data -// .address!; -// break; -// // case DerivePathType.bip49: -// // addressString = btc_dart -// // .P2SH( -// // data: btc_dart.PaymentData( -// // redeem: btc_dart -// // .P2WPKH( -// // data: data, -// // network: network, -// // ) -// // .data), -// // network: network, -// // ) -// // .data -// // .address!; -// // break; -// // case DerivePathType.bip84: -// // addressString = btc_dart -// // .P2WPKH( -// // network: network, -// // data: data, -// // ) -// // .data -// // .address!; -// // break; -// default: -// throw UnimplementedError("segwit paynyms not implemented yet"); -// } -// -// final address = Address( -// walletId: walletId, -// value: addressString, -// publicKey: paymentCode.getPubKey(), -// derivationIndex: 0, -// type: type, -// subType: AddressSubType.paynymNotification, -// otherData: paymentCode.toString(), -// ); -// -// await db.putAddress(address); -// return address; -// } -// } -// } diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index 86e3e8be1..a86d6fcd6 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -18,12 +18,14 @@ import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/mixins/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/electrum_x_parsing.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; @@ -85,7 +87,7 @@ String constructDerivePath({ } class DogecoinWallet extends CoinServiceAPI - with WalletCache, WalletDB, ElectrumXParsing { + with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface { DogecoinWallet({ required String walletId, required String walletName, @@ -105,6 +107,17 @@ class DogecoinWallet extends CoinServiceAPI _secureStore = secureStore; initCache(walletId, coin); initWalletDB(mockableOverride: mockableOverride); + initCoinControlInterface( + walletId: walletId, + walletName: walletName, + coin: coin, + db: db, + getChainHeight: () => chainHeight, + refreshedBalanceCallback: (balance) async { + _balance = balance; + await updateCachedBalance(_balance!); + }, + ); // paynym stuff // initPaynymWalletInterface( @@ -907,6 +920,7 @@ class DogecoinWallet extends CoinServiceAPI try { final feeRateType = args?["feeRate"]; final feeRateAmount = args?["feeRateAmount"]; + final utxos = args?["UTXOs"] as Set?; if (feeRateType is FeeRateType || feeRateAmount is int) { late final int rate; if (feeRateType is FeeRateType) { @@ -933,9 +947,19 @@ class DogecoinWallet extends CoinServiceAPI isSendAll = true; } - final result = - await coinSelection(satoshiAmount, rate, address, isSendAll); - Logging.instance.log("SEND RESULT: $result", level: LogLevel.Info); + final bool coinControl = utxos != null; + + final result = await coinSelection( + satoshiAmountToSend: satoshiAmount, + selectedTxFeeRate: rate, + recipientAddress: address, + isSendAll: isSendAll, + utxos: utxos?.toList(), + coinControl: coinControl, + ); + + Logging.instance + .log("PREPARE SEND RESULT: $result", level: LogLevel.Info); if (result is int) { switch (result) { case 1: @@ -981,6 +1005,12 @@ class DogecoinWallet extends CoinServiceAPI final txHash = await _electrumXClient.broadcastTransaction( rawTx: txData["hex"] as String); Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + + final utxos = txData["usedUTXOs"] as List; + + // mark utxos as used + await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList()); + return txHash; } catch (e, s) { Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", @@ -1537,18 +1567,14 @@ class DogecoinWallet extends CoinServiceAPI } } - final currentChainHeight = await chainHeight; - final List outputArray = []; - int satoshiBalanceTotal = 0; - int satoshiBalancePending = 0; - int satoshiBalanceSpendable = 0; - int satoshiBalanceBlocked = 0; for (int i = 0; i < fetchedUtxoList.length; i++) { for (int j = 0; j < fetchedUtxoList[i].length; j++) { + final jsonUTXO = fetchedUtxoList[i][j]; + final txn = await cachedElectrumXClient.getTransaction( - txHash: fetchedUtxoList[i][j]["tx_hash"] as String, + txHash: jsonUTXO["tx_hash"] as String, verbose: true, coin: coin, ); @@ -1556,49 +1582,54 @@ class DogecoinWallet extends CoinServiceAPI // fetch stored tx to see if paynym notification tx and block utxo final storedTx = await db.getTransaction( walletId, - fetchedUtxoList[i][j]["tx_hash"] as String, + jsonUTXO["tx_hash"] as String, ); bool shouldBlock = false; String? blockReason; if (storedTx?.subType == - isar_models.TransactionSubType.bip47Notification && - storedTx?.type == isar_models.TransactionType.incoming) { - // probably safe to assume this is an incoming tx as it is a utxo - // belonging to this wallet. The extra check may be redundant but - // just in case... + isar_models.TransactionSubType.bip47Notification) { + if (storedTx?.type == isar_models.TransactionType.incoming) { + shouldBlock = true; + blockReason = "Incoming paynym notification transaction."; + } else if (storedTx?.type == isar_models.TransactionType.outgoing) { + shouldBlock = true; + blockReason = "Paynym notification change output. Incautious " + "handling of change outputs from notification transactions " + "may cause unintended loss of privacy."; + } + } - shouldBlock = true; - blockReason = "Incoming paynym notification transaction."; + final vout = jsonUTXO["tx_pos"] as int; + + final outputs = txn["vout"] as List; + + String? utxoOwnerAddress; + // get UTXO owner address + for (final output in outputs) { + if (output["n"] == vout) { + utxoOwnerAddress = + output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]?["address"] as String?; + } } final utxo = isar_models.UTXO( walletId: walletId, txid: txn["txid"] as String, - vout: fetchedUtxoList[i][j]["tx_pos"] as int, - value: fetchedUtxoList[i][j]["value"] as int, + vout: vout, + value: jsonUTXO["value"] as int, name: "", isBlocked: shouldBlock, blockedReason: blockReason, isCoinbase: txn["is_coinbase"] as bool? ?? false, blockHash: txn["blockhash"] as String?, - blockHeight: fetchedUtxoList[i][j]["height"] as int?, + blockHeight: jsonUTXO["height"] as int?, blockTime: txn["blocktime"] as int?, + address: utxoOwnerAddress, ); - satoshiBalanceTotal += utxo.value; - - if (utxo.isBlocked) { - satoshiBalanceBlocked += utxo.value; - } else { - if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - satoshiBalanceSpendable += utxo.value; - } else { - satoshiBalancePending += utxo.value; - } - } - outputArray.add(utxo); } } @@ -1606,27 +1637,20 @@ class DogecoinWallet extends CoinServiceAPI Logging.instance .log('Outputs fetched: $outputArray', level: LogLevel.Info); - // TODO move this out of here and into IDB - await db.isar.writeTxn(() async { - await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll(); - await db.isar.utxos.putAll(outputArray); - }); + await db.updateUTXOs(walletId, outputArray); // finally update balance - _balance = Balance( - coin: coin, - total: satoshiBalanceTotal, - spendable: satoshiBalanceSpendable, - blockedTotal: satoshiBalanceBlocked, - pendingSpendable: satoshiBalancePending, - ); - await updateCachedBalance(_balance!); + await _updateBalance(); } catch (e, s) { Logging.instance .log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error); } } + Future _updateBalance() async { + await refreshBalance(); + } + @override Balance get balance => _balance ??= getCachedBalance(); Balance? _balance; @@ -2022,11 +2046,12 @@ class DogecoinWallet extends CoinServiceAPI /// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return /// a map containing the tx hex along with other important information. If not, then it will return /// an integer (1 or 2) - dynamic coinSelection( - int satoshiAmountToSend, - int selectedTxFeeRate, - String _recipientAddress, - bool isSendAll, { + dynamic coinSelection({ + required int satoshiAmountToSend, + required int selectedTxFeeRate, + required String recipientAddress, + required bool coinControl, + required bool isSendAll, int additionalOutputs = 0, List? utxos, }) async { @@ -2038,18 +2063,26 @@ class DogecoinWallet extends CoinServiceAPI int spendableSatoshiValue = 0; // Build list of spendable outputs and totaling their satoshi amount - for (var i = 0; i < availableOutputs.length; i++) { - if (availableOutputs[i].isBlocked == false && - availableOutputs[i] - .isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) == - true) { - spendableOutputs.add(availableOutputs[i]); - spendableSatoshiValue += availableOutputs[i].value; + for (final utxo in availableOutputs) { + if (utxo.isBlocked == false && + utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) && + utxo.used != true) { + spendableOutputs.add(utxo); + spendableSatoshiValue += utxo.value; } } - // sort spendable by age (oldest first) - spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); + if (coinControl) { + if (spendableOutputs.length < availableOutputs.length) { + throw ArgumentError("Attempted to use an unavailable utxo"); + } + } + + // don't care about sorting if using all utxos + if (!coinControl) { + // sort spendable by age (oldest first) + spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); + } Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}", level: LogLevel.Info); @@ -2078,19 +2111,27 @@ class DogecoinWallet extends CoinServiceAPI int inputsBeingConsumed = 0; List utxoObjectsToUse = []; - for (var i = 0; - satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; - i++) { - utxoObjectsToUse.add(spendableOutputs[i]); - satoshisBeingUsed += spendableOutputs[i].value; - inputsBeingConsumed += 1; - } - for (int i = 0; - i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length; - i++) { - utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); - satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; - inputsBeingConsumed += 1; + if (!coinControl) { + for (var i = 0; + satoshisBeingUsed < satoshiAmountToSend && + i < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[i]); + satoshisBeingUsed += spendableOutputs[i].value; + inputsBeingConsumed += 1; + } + for (int i = 0; + i < additionalOutputs && + inputsBeingConsumed < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); + satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; + inputsBeingConsumed += 1; + } + } else { + satoshisBeingUsed = spendableSatoshiValue; + utxoObjectsToUse = spendableOutputs; + inputsBeingConsumed = spendableOutputs.length; } Logging.instance @@ -2103,7 +2144,7 @@ class DogecoinWallet extends CoinServiceAPI .log('satoshiAmountToSend $satoshiAmountToSend', level: LogLevel.Info); // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray - List recipientsArray = [_recipientAddress]; + List recipientsArray = [recipientAddress]; List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data @@ -2114,9 +2155,8 @@ class DogecoinWallet extends CoinServiceAPI .log("Attempting to send all $coin", level: LogLevel.Info); final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, - recipients: [_recipientAddress], + recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; int feeForOneOutput = estimateTxFee( @@ -2129,7 +2169,6 @@ class DogecoinWallet extends CoinServiceAPI final int amount = satoshiAmountToSend - feeForOneOutput; dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: [amount], @@ -2140,21 +2179,20 @@ class DogecoinWallet extends CoinServiceAPI "recipientAmt": amount, "fee": feeForOneOutput, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, - recipients: [_recipientAddress], + recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; final int vSizeForTwoOutPuts = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [ - _recipientAddress, + recipientAddress, await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)), ], satoshiAmounts: [ @@ -2231,7 +2269,6 @@ class DogecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2259,7 +2296,6 @@ class DogecoinWallet extends CoinServiceAPI Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2272,6 +2308,7 @@ class DogecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeBeingPaid, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2288,7 +2325,6 @@ class DogecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2299,6 +2335,7 @@ class DogecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2317,7 +2354,6 @@ class DogecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2328,6 +2364,7 @@ class DogecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2346,7 +2383,6 @@ class DogecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2357,6 +2393,7 @@ class DogecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeForOneOutput, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2368,111 +2405,134 @@ class DogecoinWallet extends CoinServiceAPI level: LogLevel.Warning); // try adding more outputs if (spendableOutputs.length > inputsBeingConsumed) { - return coinSelection(satoshiAmountToSend, selectedTxFeeRate, - _recipientAddress, isSendAll, - additionalOutputs: additionalOutputs + 1, utxos: utxos); + return coinSelection( + satoshiAmountToSend: satoshiAmountToSend, + selectedTxFeeRate: selectedTxFeeRate, + recipientAddress: recipientAddress, + isSendAll: isSendAll, + additionalOutputs: additionalOutputs + 1, + utxos: utxos, + coinControl: coinControl, + ); } return 2; } } - Future> fetchBuildTxData( + Future> fetchBuildTxData( List utxosToUse, ) async { // return data - Map results = {}; - Map> addressTxid = {}; - - // addresses to check - List addressesP2PKH = []; + List signingData = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - final tx = await _cachedElectrumXClient.getTransaction( - txHash: txid, - coin: coin, - ); - - for (final output in tx["vout"] as List) { - final n = output["n"]; - if (n != null && n == utxosToUse[i].vout) { - final address = output["scriptPubKey"]["addresses"][0] as String; - if (!addressTxid.containsKey(address)) { - addressTxid[address] = []; - } - (addressTxid[address] as List).add(txid); - switch (addressType(address: address)) { - case DerivePathType.bip44: - addressesP2PKH.add(address); - break; - default: - throw Exception("Unsupported DerivePathType"); + if (utxosToUse[i].address == null) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + utxosToUse[i] = utxosToUse[i].copyWith( + address: output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]["address"] as String, + ); } } } + + final derivePathType = addressType(address: utxosToUse[i].address!); + + signingData.add( + SigningData( + derivePathType: derivePathType, + utxo: utxosToUse[i], + ), + ); } - // p2pkh / bip44 - final p2pkhLength = addressesP2PKH.length; - if (p2pkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip44, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip44, - ); - for (int i = 0; i < p2pkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2PKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: network, - ).data; + Map> receiveDerivations = {}; + Map> changeDerivations = {}; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: network, - ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2PKH( + for (final sd in signingData) { + String? pubKey; + String? wif; + + // fetch receiving derivations if null + receiveDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 0, + derivePathType: sd.derivePathType, + ); + final receiveDerivation = + receiveDerivations[sd.derivePathType]![sd.utxo.address!]; + + if (receiveDerivation != null) { + pubKey = receiveDerivation["pubKey"] as String; + wif = receiveDerivation["wif"] as String; + } else { + // fetch change derivations if null + changeDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 1, + derivePathType: sd.derivePathType, + ); + final changeDerivation = + changeDerivations[sd.derivePathType]![sd.utxo.address!]; + if (changeDerivation != null) { + pubKey = changeDerivation["pubKey"] as String; + wif = changeDerivation["wif"] as String; + } + } + + if (wif == null || pubKey == null) { + final address = await db.getAddress(walletId, sd.utxo.address!); + if (address?.derivationPath != null) { + final node = await Bip32Utils.getBip32Node( + (await mnemonicString)!, + (await mnemonicPassphrase)!, + network, + address!.derivationPath!.value, + ); + + wif = node.toWIF(); + pubKey = Format.uint8listToString(node.publicKey); + } + } + + if (wif != null && pubKey != null) { + final PaymentData data; + final Uint8List? redeemScript; + + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = P2PKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: network, ).data; + redeemScript = null; + break; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: network, - ), - }; - } - } + default: + throw Exception("DerivePathType unsupported"); } + + final keyPair = ECPair.fromWIF( + wif, + network: network, + ); + + sd.redeemScript = redeemScript; + sd.output = data.output; + sd.keyPair = keyPair; } } - return results; + return signingData; } catch (e, s) { Logging.instance .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); @@ -2482,8 +2542,7 @@ class DogecoinWallet extends CoinServiceAPI /// Builds and signs a transaction Future> buildTransaction({ - required List utxosToUse, - required Map utxoSigningData, + required List utxoSigningData, required List recipients, required List satoshiAmounts, }) async { @@ -2494,10 +2553,14 @@ class DogecoinWallet extends CoinServiceAPI txb.setVersion(1); // Add transaction inputs - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - txb.addInput(txid, utxosToUse[i].vout, null, - utxoSigningData[txid]["output"] as Uint8List); + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + txb.addInput( + txid, + utxoSigningData[i].utxo.vout, + null, + utxoSigningData[i].output!, + ); } // Add transaction output @@ -2507,13 +2570,12 @@ class DogecoinWallet extends CoinServiceAPI try { // Sign the transaction accordingly - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; + for (var i = 0; i < utxoSigningData.length; i++) { txb.sign( vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, + keyPair: utxoSigningData[i].keyPair!, + witnessValue: utxoSigningData[i].utxo.value, + redeemScript: utxoSigningData[i].redeemScript, ); } } catch (e, s) { diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index 980784ecf..d23c349d4 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -11,6 +11,7 @@ import 'package:mutex/mutex.dart'; import 'package:stack_wallet_backup/generate_password.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/models/epicbox_config_model.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; @@ -27,6 +28,7 @@ import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/default_epicboxes.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; @@ -219,43 +221,34 @@ Future _cancelTransactionWrapper(Tuple2 data) async { return cancelTransaction(data.item1, data.item2); } -Future _deleteWalletWrapper(String wallet) async { - return deleteWallet(wallet); +Future _deleteWalletWrapper(Tuple2 data) async { + return deleteWallet(data.item1, data.item2); } Future deleteEpicWallet({ required String walletId, required SecureStorageInterface secureStore, }) async { - // is this even needed for anything? - // String? config = await secureStore.read(key: '${walletId}_config'); - // // TODO: why double check for iOS? - // if (Platform.isIOS) { - // Directory appDir = await StackFileSystem.applicationRootDirectory(); - // // todo why double check for ios? - // // if (Platform.isIOS) { - // // appDir = (await getLibraryDirectory()); - // // } - // // if (Platform.isLinux) { - // // appDir = Directory("${appDir.path}/.stackwallet"); - // // } - // final path = "${appDir.path}/epiccash"; - // final String name = walletId; - // - // final walletDir = '$path/$name'; - // var editConfig = jsonDecode(config as String); - // - // editConfig["wallet_dir"] = walletDir; - // config = jsonEncode(editConfig); - // } - final wallet = await secureStore.read(key: '${walletId}_wallet'); + String? config = await secureStore.read(key: '${walletId}_config'); + if (Platform.isIOS) { + Directory appDir = await StackFileSystem.applicationRootDirectory(); + + final path = "${appDir.path}/epiccash"; + final String name = walletId.trim(); + final walletDir = '$path/$name'; + + var editConfig = jsonDecode(config as String); + + editConfig["wallet_dir"] = walletDir; + config = jsonEncode(editConfig); + } if (wallet == null) { return "Tried to delete non existent epic wallet file with walletId=$walletId"; } else { try { - return compute(_deleteWalletWrapper, wallet); + return _deleteWalletWrapper(Tuple2(wallet, config!)); } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Error); return "deleteEpicWallet($walletId) failed..."; @@ -456,7 +449,10 @@ class EpicCashWallet extends CoinServiceAPI Future confirmSend({required Map txData}) async { try { final wallet = await _secureStore.read(key: '${_walletId}_wallet'); - final epicboxConfig = await getEpicBoxConfig(); + + EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); + + print("EPICBOX CONFIG HERE IS $epicboxConfig"); // TODO determine whether it is worth sending change to a change address. dynamic message; @@ -465,10 +461,8 @@ class EpicCashWallet extends CoinServiceAPI if (!receiverAddress.startsWith("http://") || !receiverAddress.startsWith("https://")) { - final decoded = json.decode(epicboxConfig); bool isEpicboxConnected = await testEpicboxServer( - decoded["epicbox_domain"] as String, - decoded["epicbox_port"] as int); + epicboxConfig.host, epicboxConfig.port ?? 443); if (!isEpicboxConnected) { throw Exception("Failed to send TX : Unable to reach epicbox server"); } @@ -505,7 +499,7 @@ class EpicCashWallet extends CoinServiceAPI "amount": txData['recipientAmt'], "address": txData['addresss'], "secretKeyIndex": 0, - "epicboxConfig": epicboxConfig!, + "epicboxConfig": epicboxConfig.toString(), "minimumConfirmations": MINIMUM_CONFIRMATIONS, }, name: walletName); @@ -567,13 +561,13 @@ class EpicCashWallet extends CoinServiceAPI if (address == null) { final wallet = await _secureStore.read(key: '${_walletId}_wallet'); - final epicboxConfig = await getEpicBoxConfig(); + EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); String? walletAddress; await m.protect(() async { walletAddress = await compute( _initGetAddressInfoWrapper, - Tuple3(wallet!, index, epicboxConfig!), + Tuple3(wallet!, index, epicboxConfig.toString()), ); }); Logging.instance @@ -727,12 +721,13 @@ class EpicCashWallet extends CoinServiceAPI int index = 0; Logging.instance.log("This index is $index", level: LogLevel.Info); - final epicboxConfig = await getEpicBoxConfig(); + EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); + String? walletAddress; await m.protect(() async { walletAddress = await compute( _initGetAddressInfoWrapper, - Tuple3(wallet!, index, epicboxConfig!), + Tuple3(wallet!, index, epicboxConfig.toString()), ); }); Logging.instance @@ -770,14 +765,14 @@ class EpicCashWallet extends CoinServiceAPI final String password = generatePassword(); String stringConfig = await getConfig(); - String epicboxConfig = await getEpicBoxConfig(); + EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); await _secureStore.write( key: '${_walletId}_mnemonic', value: mnemonicString); await _secureStore.write(key: '${_walletId}_config', value: stringConfig); await _secureStore.write(key: '${_walletId}_password', value: password); await _secureStore.write( - key: '${_walletId}_epicboxConfig', value: epicboxConfig); + key: '${_walletId}_epicboxConfig', value: epicboxConfig.toString()); String name = _walletId; @@ -993,6 +988,7 @@ class EpicCashWallet extends CoinServiceAPI } Future testEpicboxServer(String host, int port) async { + // TODO use an EpicBoxServerModel as the only param final websocketConnectionUri = 'wss://$host:$port'; const connectionOptions = SocketConnectionOptions( pingIntervalMs: 3000, @@ -1035,33 +1031,49 @@ class EpicCashWallet extends CoinServiceAPI return isConnected; } - Future getEpicBoxConfig() async { + Future getEpicBoxConfig() async { + EpicBoxConfigModel? _epicBoxConfig; + // read epicbox config from secure store String? storedConfig = await _secureStore.read(key: '${_walletId}_epicboxConfig'); + // we should move to storing the primary server model like we do with nodes, and build the config from that (see epic-mobile) + // EpicBoxServerModel? _epicBox = epicBox ?? + // DB.instance.get( + // boxName: DB.boxNamePrimaryEpicBox, key: 'primary'); + // Logging.instance.log( + // "Read primary Epic Box config: ${jsonEncode(_epicBox)}", + // level: LogLevel.Info); + if (storedConfig == null) { - storedConfig = DefaultNodes.defaultEpicBoxConfig; + // if no config stored, use the default epicbox server as config + _epicBoxConfig = + EpicBoxConfigModel.fromServer(DefaultEpicBoxes.defaultEpicBoxServer); } else { - dynamic decoded = json.decode(storedConfig!); - final domain = decoded["domain"] ?? "empty"; - if (domain != "empty") { - //If we have the old invalid config, use the new default one - // new storage format stores domain under "epicbox_domain", old storage format used "domain" - storedConfig = DefaultNodes.defaultEpicBoxConfig; - } + // if a config is stored, test it + + _epicBoxConfig = EpicBoxConfigModel.fromString( + storedConfig); // fromString handles checking old config formats } - final decoded = json.decode(storedConfig); - //Check Epicbox is up before returning it + bool isEpicboxConnected = await testEpicboxServer( - decoded["epicbox_domain"] as String, decoded["epicbox_port"] as int); + _epicBoxConfig.host, _epicBoxConfig.port ?? 443); if (!isEpicboxConnected) { - //Default Epicbox is not connected, Defaulting to Europe - storedConfig = json.encode(DefaultNodes.epicBoxConfigEUR); - // TODO test this connection before returning it, iterating through the list of default Epic Box servers + // default Epicbox is not connected, default to Europe + _epicBoxConfig = EpicBoxConfigModel.fromServer(DefaultEpicBoxes.europe); + + // example of selecting another random server from the default list + // alternative servers: copy list of all default EB servers but remove the default default + // List alternativeServers = DefaultEpicBoxes.all; + // alternativeServers.removeWhere((opt) => opt.name == DefaultEpicBoxes.defaultEpicBoxServer.name); + // alternativeServers.shuffle(); // randomize which server is used + // _epicBoxConfig = EpicBoxConfigModel.fromServer(alternativeServers.first); + + // TODO test this connection before returning it } - return storedConfig; + return _epicBoxConfig; } Future getRealConfig() async { @@ -1171,14 +1183,16 @@ class EpicCashWallet extends CoinServiceAPI final String password = generatePassword(); String stringConfig = await getConfig(); - String epicboxConfig = await getEpicBoxConfig(); + EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); final String name = _walletName.trim(); await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); await _secureStore.write(key: '${_walletId}_config', value: stringConfig); await _secureStore.write(key: '${_walletId}_password', value: password); + + print("EPIC BOX MODEL IS ${epicboxConfig.toString()}"); await _secureStore.write( - key: '${_walletId}_epicboxConfig', value: epicboxConfig); + key: '${_walletId}_epicboxConfig', value: epicboxConfig.toString()); await compute( _recoverWrapper, @@ -1379,14 +1393,14 @@ class EpicCashWallet extends CoinServiceAPI Future listenForSlates() async { final wallet = await _secureStore.read(key: '${_walletId}_wallet'); - final epicboxConfig = await getEpicBoxConfig(); + EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); await m.protect(() async { Logging.instance.log("CALLING LISTEN FOR SLATES", level: LogLevel.Info); ReceivePort receivePort = await getIsolate({ "function": "listenForSlates", "wallet": wallet, - "epicboxConfig": epicboxConfig, + "epicboxConfig": epicboxConfig.toString(), }, name: walletName); var result = await receivePort.first; @@ -1816,6 +1830,11 @@ class EpicCashWallet extends CoinServiceAPI @override bool validateAddress(String address) { + //Invalid address that contains HTTP and epicbox domain + if ((address.startsWith("http://") || address.startsWith("https://")) && + address.contains("@")) { + return false; + } if (address.startsWith("http://") || address.startsWith("https://")) { if (Uri.tryParse(address) != null) { return true; @@ -1824,7 +1843,11 @@ class EpicCashWallet extends CoinServiceAPI String validate = validateSendAddress(address); if (int.parse(validate) == 1) { - return true; + //Check if address contrains a domain + if (address.contains("@")) { + return true; + } + return false; } else { return false; } diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index d5eb20de2..ca94db6e3 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/lelantus_coin.dart'; import 'package:stackwallet/models/lelantus_fee_data.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; @@ -36,6 +37,7 @@ import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/format.dart'; @@ -1367,7 +1369,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { .log("Attempting to send all $coin", level: LogLevel.Info); final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [_recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], @@ -1383,7 +1384,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { final int amount = satoshiAmountToSend - feeForOneOutput; dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: [amount], @@ -1399,13 +1399,11 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { } final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [_recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; final int vSizeForTwoOutPuts = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [ _recipientAddress, @@ -1484,7 +1482,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { Logging.instance .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -1512,7 +1509,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -1541,7 +1537,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -1570,7 +1565,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -1599,7 +1593,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -1629,119 +1622,141 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { } } - Future> fetchBuildTxData( + Future> fetchBuildTxData( List utxosToUse, ) async { // return data - Map results = {}; - Map> addressTxid = {}; - - // addresses to check - List addresses = []; + List signingData = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - final tx = await _cachedElectrumXClient.getTransaction( - txHash: txid, - coin: coin, + if (utxosToUse[i].address == null) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + utxosToUse[i] = utxosToUse[i].copyWith( + address: output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]["address"] as String, + ); + } + } + } + + signingData.add( + SigningData( + derivePathType: DerivePathType.bip44, + utxo: utxosToUse[i], + ), + ); + } + + Map> receiveDerivations = {}; + Map> changeDerivations = {}; + + for (final sd in signingData) { + String? pubKey; + String? wif; + + // fetch receiving derivations if null + receiveDerivations[sd.derivePathType] ??= Map.from( + jsonDecode((await _secureStore.read( + key: "${walletId}_receiveDerivations", + )) ?? + "{}") as Map, ); - for (final output in tx["vout"] as List) { - final n = output["n"]; - if (n != null && n == utxosToUse[i].vout) { - final address = output["scriptPubKey"]["addresses"][0] as String; - - if (!addressTxid.containsKey(address)) { - addressTxid[address] = []; - } - (addressTxid[address] as List).add(txid); - - addresses.add(address); + dynamic receiveDerivation; + for (int j = 0; + j < receiveDerivations[sd.derivePathType]!.length && + receiveDerivation == null; + j++) { + if (receiveDerivations[sd.derivePathType]!["$j"]["address"] == + sd.utxo.address!) { + receiveDerivation = receiveDerivations[sd.derivePathType]!["$j"]; } } - } - // p2pkh / bip44 - final addressesLength = addresses.length; - if (addressesLength > 0) { - final receiveDerivationsString = - await _secureStore.read(key: "${walletId}_receiveDerivations"); - final receiveDerivations = Map.from( - jsonDecode(receiveDerivationsString ?? "{}") as Map); + if (receiveDerivation != null) { + pubKey = receiveDerivation["publicKey"] as String; + wif = receiveDerivation["wif"] as String; + } else { + // fetch change derivations if null + changeDerivations[sd.derivePathType] ??= Map.from( + jsonDecode((await _secureStore.read( + key: "${walletId}_changeDerivations", + )) ?? + "{}") as Map, + ); - final changeDerivationsString = - await _secureStore.read(key: "${walletId}_changeDerivations"); - final changeDerivations = Map.from( - jsonDecode(changeDerivationsString ?? "{}") as Map); - - for (int i = 0; i < addressesLength; i++) { - // receives - - dynamic receiveDerivation; - - for (int j = 0; j < receiveDerivations.length; j++) { - if (receiveDerivations["$j"]["address"] == addresses[i]) { - receiveDerivation = receiveDerivations["$j"]; + dynamic changeDerivation; + for (int j = 0; + j < changeDerivations[sd.derivePathType]!.length && + changeDerivation == null; + j++) { + if (changeDerivations[sd.derivePathType]!["$j"]["address"] == + sd.utxo.address!) { + changeDerivation = changeDerivations[sd.derivePathType]!["$j"]; } } - // receiveDerivation = receiveDerivations[addresses[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2PKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["publicKey"] as String)), - network: _network, - ).data; + if (changeDerivation != null) { + pubKey = changeDerivation["publicKey"] as String; + wif = changeDerivation["wif"] as String; + } + } - for (String tx in addressTxid[addresses[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - }; - } - } else { - // if its not a receive, check change + if (wif == null || pubKey == null) { + final address = await db.getAddress(walletId, sd.utxo.address!); + if (address?.derivationPath != null) { + final node = await Bip32Utils.getBip32Node( + (await mnemonicString)!, + (await mnemonicPassphrase)!, + _network, + address!.derivationPath!.value, + ); - dynamic changeDerivation; + wif = node.toWIF(); + pubKey = Format.uint8listToString(node.publicKey); + } + } - for (int j = 0; j < changeDerivations.length; j++) { - if (changeDerivations["$j"]["address"] == addresses[i]) { - changeDerivation = changeDerivations["$j"]; - } - } + if (wif != null && pubKey != null) { + final PaymentData data; + final Uint8List? redeemScript; - // final changeDerivation = changeDerivations[addresses[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2PKH( + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = P2PKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["publicKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: _network, ).data; + redeemScript = null; + break; - for (String tx in addressTxid[addresses[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } + default: + throw Exception("DerivePathType unsupported"); } + + final keyPair = ECPair.fromWIF( + wif, + network: _network, + ); + + sd.redeemScript = redeemScript; + sd.output = data.output; + sd.keyPair = keyPair; } } - return results; + return signingData; } catch (e, s) { Logging.instance .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); @@ -1751,8 +1766,7 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { /// Builds and signs a transaction Future> buildTransaction({ - required List utxosToUse, - required Map utxoSigningData, + required List utxoSigningData, required List recipients, required List satoshiAmounts, }) async { @@ -1763,10 +1777,14 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { txb.setVersion(1); // Add transaction inputs - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - txb.addInput(txid, utxosToUse[i].vout, null, - utxoSigningData[txid]["output"] as Uint8List); + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + txb.addInput( + txid, + utxoSigningData[i].utxo.vout, + null, + utxoSigningData[i].output!, + ); } // Add transaction output @@ -1776,13 +1794,12 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { try { // Sign the transaction accordingly - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; + for (var i = 0; i < utxoSigningData.length; i++) { txb.sign( vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, + keyPair: utxoSigningData[i].keyPair!, + witnessValue: utxoSigningData[i].utxo.value, + redeemScript: utxoSigningData[i].redeemScript, ); } } catch (e, s) { diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 762a39cb2..542c88b60 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -17,12 +17,14 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/mixins/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/electrum_x_parsing.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; @@ -90,7 +92,7 @@ String constructDerivePath({ } class LitecoinWallet extends CoinServiceAPI - with WalletCache, WalletDB, ElectrumXParsing { + with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface { LitecoinWallet({ required String walletId, required String walletName, @@ -110,6 +112,17 @@ class LitecoinWallet extends CoinServiceAPI _secureStore = secureStore; initCache(walletId, coin); initWalletDB(mockableOverride: mockableOverride); + initCoinControlInterface( + walletId: walletId, + walletName: walletName, + coin: coin, + db: db, + getChainHeight: () => chainHeight, + refreshedBalanceCallback: (balance) async { + _balance = balance; + await updateCachedBalance(_balance!); + }, + ); } static const integrationTestFlag = @@ -1018,6 +1031,7 @@ class LitecoinWallet extends CoinServiceAPI try { final feeRateType = args?["feeRate"]; final feeRateAmount = args?["feeRateAmount"]; + final utxos = args?["UTXOs"] as Set?; if (feeRateType is FeeRateType || feeRateAmount is int) { late final int rate; if (feeRateType is FeeRateType) { @@ -1045,8 +1059,16 @@ class LitecoinWallet extends CoinServiceAPI isSendAll = true; } - final txData = - await coinSelection(satoshiAmount, rate, address, isSendAll); + final bool coinControl = utxos != null; + + final txData = await coinSelection( + satoshiAmountToSend: satoshiAmount, + selectedTxFeeRate: rate, + recipientAddress: address, + isSendAll: isSendAll, + utxos: utxos?.toList(), + coinControl: coinControl, + ); Logging.instance.log("prepare send: $txData", level: LogLevel.Info); try { @@ -1109,6 +1131,11 @@ class LitecoinWallet extends CoinServiceAPI final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex); Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + final utxos = txData["usedUTXOs"] as List; + + // mark utxos as used + await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList()); + return txHash; } catch (e, s) { Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", @@ -1724,49 +1751,47 @@ class LitecoinWallet extends CoinServiceAPI } } - final currentChainHeight = await chainHeight; - final List outputArray = []; - int satoshiBalanceTotal = 0; - int satoshiBalancePending = 0; - int satoshiBalanceSpendable = 0; - int satoshiBalanceBlocked = 0; for (int i = 0; i < fetchedUtxoList.length; i++) { for (int j = 0; j < fetchedUtxoList[i].length; j++) { + final jsonUTXO = fetchedUtxoList[i][j]; + final txn = await cachedElectrumXClient.getTransaction( - txHash: fetchedUtxoList[i][j]["tx_hash"] as String, + txHash: jsonUTXO["tx_hash"] as String, verbose: true, coin: coin, ); - // todo check here if we should mark as blocked + final vout = jsonUTXO["tx_pos"] as int; + + final outputs = txn["vout"] as List; + + String? utxoOwnerAddress; + // get UTXO owner address + for (final output in outputs) { + if (output["n"] == vout) { + utxoOwnerAddress = + output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]?["address"] as String?; + } + } + final utxo = isar_models.UTXO( walletId: walletId, txid: txn["txid"] as String, - vout: fetchedUtxoList[i][j]["tx_pos"] as int, - value: fetchedUtxoList[i][j]["value"] as int, + vout: vout, + value: jsonUTXO["value"] as int, name: "", isBlocked: false, blockedReason: null, isCoinbase: txn["is_coinbase"] as bool? ?? false, blockHash: txn["blockhash"] as String?, - blockHeight: fetchedUtxoList[i][j]["height"] as int?, + blockHeight: jsonUTXO["height"] as int?, blockTime: txn["blocktime"] as int?, + address: utxoOwnerAddress, ); - satoshiBalanceTotal += utxo.value; - - if (utxo.isBlocked) { - satoshiBalanceBlocked += utxo.value; - } else { - if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - satoshiBalanceSpendable += utxo.value; - } else { - satoshiBalancePending += utxo.value; - } - } - outputArray.add(utxo); } } @@ -1774,27 +1799,20 @@ class LitecoinWallet extends CoinServiceAPI Logging.instance .log('Outputs fetched: $outputArray', level: LogLevel.Info); - // TODO move this out of here and into IDB - await db.isar.writeTxn(() async { - await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll(); - await db.isar.utxos.putAll(outputArray); - }); + await db.updateUTXOs(walletId, outputArray); // finally update balance - _balance = Balance( - coin: coin, - total: satoshiBalanceTotal, - spendable: satoshiBalanceSpendable, - blockedTotal: satoshiBalanceBlocked, - pendingSpendable: satoshiBalancePending, - ); - await updateCachedBalance(_balance!); + await _updateBalance(); } catch (e, s) { Logging.instance .log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error); } } + Future _updateBalance() async { + await refreshBalance(); + } + @override Balance get balance => _balance ??= getCachedBalance(); Balance? _balance; @@ -2214,11 +2232,12 @@ class LitecoinWallet extends CoinServiceAPI /// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return /// a map containing the tx hex along with other important information. If not, then it will return /// an integer (1 or 2) - dynamic coinSelection( - int satoshiAmountToSend, - int selectedTxFeeRate, - String _recipientAddress, - bool isSendAll, { + dynamic coinSelection({ + required int satoshiAmountToSend, + required int selectedTxFeeRate, + required String recipientAddress, + required bool coinControl, + required bool isSendAll, int additionalOutputs = 0, List? utxos, }) async { @@ -2230,18 +2249,26 @@ class LitecoinWallet extends CoinServiceAPI int spendableSatoshiValue = 0; // Build list of spendable outputs and totaling their satoshi amount - for (var i = 0; i < availableOutputs.length; i++) { - if (availableOutputs[i].isBlocked == false && - availableOutputs[i] - .isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) == - true) { - spendableOutputs.add(availableOutputs[i]); - spendableSatoshiValue += availableOutputs[i].value; + for (final utxo in availableOutputs) { + if (utxo.isBlocked == false && + utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) && + utxo.used != true) { + spendableOutputs.add(utxo); + spendableSatoshiValue += utxo.value; } } - // sort spendable by age (oldest first) - spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); + if (coinControl) { + if (spendableOutputs.length < availableOutputs.length) { + throw ArgumentError("Attempted to use an unavailable utxo"); + } + } + + // don't care about sorting if using all utxos + if (!coinControl) { + // sort spendable by age (oldest first) + spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); + } Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}", level: LogLevel.Info); @@ -2270,19 +2297,27 @@ class LitecoinWallet extends CoinServiceAPI int inputsBeingConsumed = 0; List utxoObjectsToUse = []; - for (var i = 0; - satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; - i++) { - utxoObjectsToUse.add(spendableOutputs[i]); - satoshisBeingUsed += spendableOutputs[i].value; - inputsBeingConsumed += 1; - } - for (int i = 0; - i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length; - i++) { - utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); - satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; - inputsBeingConsumed += 1; + if (!coinControl) { + for (var i = 0; + satoshisBeingUsed < satoshiAmountToSend && + i < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[i]); + satoshisBeingUsed += spendableOutputs[i].value; + inputsBeingConsumed += 1; + } + for (int i = 0; + i < additionalOutputs && + inputsBeingConsumed < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); + satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; + inputsBeingConsumed += 1; + } + } else { + satoshisBeingUsed = spendableSatoshiValue; + utxoObjectsToUse = spendableOutputs; + inputsBeingConsumed = spendableOutputs.length; } Logging.instance @@ -2293,7 +2328,7 @@ class LitecoinWallet extends CoinServiceAPI .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info); // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray - List recipientsArray = [_recipientAddress]; + List recipientsArray = [recipientAddress]; List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data @@ -2304,9 +2339,8 @@ class LitecoinWallet extends CoinServiceAPI .log("Attempting to send all $coin", level: LogLevel.Info); final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, - recipients: [_recipientAddress], + recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; int feeForOneOutput = estimateTxFee( @@ -2322,7 +2356,6 @@ class LitecoinWallet extends CoinServiceAPI final int amount = satoshiAmountToSend - feeForOneOutput; dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: [amount], @@ -2333,21 +2366,20 @@ class LitecoinWallet extends CoinServiceAPI "recipientAmt": amount, "fee": feeForOneOutput, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, - recipients: [_recipientAddress], + recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; final int vSizeForTwoOutPuts = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [ - _recipientAddress, + recipientAddress, await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)), ], satoshiAmounts: [ @@ -2410,7 +2442,6 @@ class LitecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2438,7 +2469,6 @@ class LitecoinWallet extends CoinServiceAPI Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2451,6 +2481,7 @@ class LitecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeBeingPaid, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2467,7 +2498,6 @@ class LitecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2478,6 +2508,7 @@ class LitecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2496,7 +2527,6 @@ class LitecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2507,6 +2537,7 @@ class LitecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2525,7 +2556,6 @@ class LitecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2536,6 +2566,7 @@ class LitecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeForOneOutput, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2547,253 +2578,160 @@ class LitecoinWallet extends CoinServiceAPI level: LogLevel.Warning); // try adding more outputs if (spendableOutputs.length > inputsBeingConsumed) { - return coinSelection(satoshiAmountToSend, selectedTxFeeRate, - _recipientAddress, isSendAll, - additionalOutputs: additionalOutputs + 1, utxos: utxos); + return coinSelection( + satoshiAmountToSend: satoshiAmountToSend, + selectedTxFeeRate: selectedTxFeeRate, + recipientAddress: recipientAddress, + isSendAll: isSendAll, + additionalOutputs: additionalOutputs + 1, + utxos: utxos, + coinControl: coinControl, + ); } return 2; } } - Future> fetchBuildTxData( + Future> fetchBuildTxData( List utxosToUse, ) async { // return data - Map results = {}; - Map> addressTxid = {}; - - // addresses to check - List addressesP2PKH = []; - List addressesP2SH = []; - List addressesP2WPKH = []; + List signingData = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - final tx = await _cachedElectrumXClient.getTransaction( - txHash: txid, - coin: coin, - ); - - for (final output in tx["vout"] as List) { - final n = output["n"]; - if (n != null && n == utxosToUse[i].vout) { - final address = output["scriptPubKey"]["addresses"][0] as String; - if (!addressTxid.containsKey(address)) { - addressTxid[address] = []; - } - (addressTxid[address] as List).add(txid); - switch (addressType(address: address)) { - case DerivePathType.bip44: - addressesP2PKH.add(address); - break; - case DerivePathType.bip49: - addressesP2SH.add(address); - break; - case DerivePathType.bip84: - addressesP2WPKH.add(address); - break; - default: - throw Exception("DerivePathType unsupported"); + if (utxosToUse[i].address == null) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + utxosToUse[i] = utxosToUse[i].copyWith( + address: output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]["address"] as String, + ); } } } + + final derivePathType = addressType(address: utxosToUse[i].address!); + + signingData.add( + SigningData( + derivePathType: derivePathType, + utxo: utxosToUse[i], + ), + ); } - // p2pkh / bip44 - final p2pkhLength = addressesP2PKH.length; - if (p2pkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip44, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip44, - ); - for (int i = 0; i < p2pkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2PKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - ).data; + Map> receiveDerivations = {}; + Map> changeDerivations = {}; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2PKH( + for (final sd in signingData) { + String? pubKey; + String? wif; + + // fetch receiving derivations if null + receiveDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 0, + derivePathType: sd.derivePathType, + ); + final receiveDerivation = + receiveDerivations[sd.derivePathType]![sd.utxo.address!]; + + if (receiveDerivation != null) { + pubKey = receiveDerivation["pubKey"] as String; + wif = receiveDerivation["wif"] as String; + } else { + // fetch change derivations if null + changeDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 1, + derivePathType: sd.derivePathType, + ); + final changeDerivation = + changeDerivations[sd.derivePathType]![sd.utxo.address!]; + if (changeDerivation != null) { + pubKey = changeDerivation["pubKey"] as String; + wif = changeDerivation["wif"] as String; + } + } + + if (wif == null || pubKey == null) { + final address = await db.getAddress(walletId, sd.utxo.address!); + if (address?.derivationPath != null) { + final node = await Bip32Utils.getBip32Node( + (await mnemonicString)!, + (await mnemonicPassphrase)!, + _network, + address!.derivationPath!.value, + ); + + wif = node.toWIF(); + pubKey = Format.uint8listToString(node.publicKey); + } + } + + if (wif != null && pubKey != null) { + final PaymentData data; + final Uint8List? redeemScript; + + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = P2PKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: _network, ).data; + redeemScript = null; + break; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } - } - } - } - - // p2sh / bip49 - final p2shLength = addressesP2SH.length; - if (p2shLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip49, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip49, - ); - for (int i = 0; i < p2shLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2SH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final p2wpkh = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - overridePrefix: _network.bech32!) - .data; - - final redeemScript = p2wpkh.output; - - final data = - P2SH(data: PaymentData(redeem: p2wpkh), network: _network).data; - - for (String tx in addressTxid[addressesP2SH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - "redeemScript": redeemScript, - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2SH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { + case DerivePathType.bip49: final p2wpkh = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), - network: _network, - overridePrefix: _network.bech32!) - .data; - - final redeemScript = p2wpkh.output; - - final data = - P2SH(data: PaymentData(redeem: p2wpkh), network: _network) - .data; - - for (String tx in addressTxid[addressesP2SH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - "redeemScript": redeemScript, - }; - } - } - } - } - } - - // p2wpkh / bip84 - final p2wpkhLength = addressesP2WPKH.length; - if (p2wpkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip84, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip84, - ); - - for (int i = 0; i < p2wpkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - overridePrefix: _network.bech32!) - .data; - - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, + data: PaymentData( + pubkey: Format.stringToUint8List(pubKey), ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), - network: _network, - overridePrefix: _network.bech32!) - .data; + network: _network, + overridePrefix: _network.bech32!, + ).data; + redeemScript = p2wpkh.output; + data = P2SH( + data: PaymentData(redeem: p2wpkh), + network: _network, + ).data; + break; - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } + case DerivePathType.bip84: + data = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List(pubKey), + ), + network: _network, + overridePrefix: _network.bech32!, + ).data; + redeemScript = null; + break; + + default: + throw Exception("DerivePathType unsupported"); } + + final keyPair = ECPair.fromWIF( + wif, + network: _network, + ); + + sd.redeemScript = redeemScript; + sd.output = data.output; + sd.keyPair = keyPair; } } - return results; + return signingData; } catch (e, s) { Logging.instance .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); @@ -2803,8 +2741,7 @@ class LitecoinWallet extends CoinServiceAPI /// Builds and signs a transaction Future> buildTransaction({ - required List utxosToUse, - required Map utxoSigningData, + required List utxoSigningData, required List recipients, required List satoshiAmounts, }) async { @@ -2815,10 +2752,15 @@ class LitecoinWallet extends CoinServiceAPI txb.setVersion(1); // Add transaction inputs - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - txb.addInput(txid, utxosToUse[i].vout, null, - utxoSigningData[txid]["output"] as Uint8List, _network.bech32!); + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + txb.addInput( + txid, + utxoSigningData[i].utxo.vout, + null, + utxoSigningData[i].output!, + _network.bech32!, + ); } // Add transaction output @@ -2828,14 +2770,14 @@ class LitecoinWallet extends CoinServiceAPI try { // Sign the transaction accordingly - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; + for (var i = 0; i < utxoSigningData.length; i++) { txb.sign( - vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, - overridePrefix: _network.bech32!); + vin: i, + keyPair: utxoSigningData[i].keyPair!, + witnessValue: utxoSigningData[i].utxo.value, + redeemScript: utxoSigningData[i].redeemScript, + overridePrefix: _network.bech32!, + ); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s", diff --git a/lib/services/coins/manager.dart b/lib/services/coins/manager.dart index 51fd1121e..5f232ec4b 100644 --- a/lib/services/coins/manager.dart +++ b/lib/services/coins/manager.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/mixins/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -229,6 +230,10 @@ class Manager with ChangeNotifier { bool get hasPaynymSupport => _currentWallet is PaynymWalletInterface; + bool get hasCoinControlSupport => _currentWallet is CoinControlInterface; + + bool get hasWhirlpoolSupport => false; + int get rescanOnOpenVersion => DB.instance.get( boxName: DB.boxNameDBInfo, diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index 6ce0581e0..400726329 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -17,12 +17,14 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/mixins/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/electrum_x_parsing.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; @@ -87,7 +89,7 @@ String constructDerivePath({ } class NamecoinWallet extends CoinServiceAPI - with WalletCache, WalletDB, ElectrumXParsing { + with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface { NamecoinWallet({ required String walletId, required String walletName, @@ -107,6 +109,17 @@ class NamecoinWallet extends CoinServiceAPI _secureStore = secureStore; initCache(walletId, coin); initWalletDB(mockableOverride: mockableOverride); + initCoinControlInterface( + walletId: walletId, + walletName: walletName, + coin: coin, + db: db, + getChainHeight: () => chainHeight, + refreshedBalanceCallback: (balance) async { + _balance = balance; + await updateCachedBalance(_balance!); + }, + ); } static const integrationTestFlag = @@ -1009,6 +1022,7 @@ class NamecoinWallet extends CoinServiceAPI try { final feeRateType = args?["feeRate"]; final feeRateAmount = args?["feeRateAmount"]; + final utxos = args?["UTXOs"] as Set?; if (feeRateType is FeeRateType || feeRateAmount is int) { late final int rate; if (feeRateType is FeeRateType) { @@ -1036,8 +1050,16 @@ class NamecoinWallet extends CoinServiceAPI isSendAll = true; } - final txData = - await coinSelection(satoshiAmount, rate, address, isSendAll); + final bool coinControl = utxos != null; + + final txData = await coinSelection( + satoshiAmountToSend: satoshiAmount, + selectedTxFeeRate: rate, + recipientAddress: address, + isSendAll: isSendAll, + utxos: utxos?.toList(), + coinControl: coinControl, + ); Logging.instance.log("prepare send: $txData", level: LogLevel.Info); try { @@ -1100,6 +1122,11 @@ class NamecoinWallet extends CoinServiceAPI final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex); Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + final utxos = txData["usedUTXOs"] as List; + + // mark utxos as used + await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList()); + return txHash; } catch (e, s) { Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", @@ -1709,49 +1736,47 @@ class NamecoinWallet extends CoinServiceAPI } } - final currentChainHeight = await chainHeight; - final List outputArray = []; - int satoshiBalanceTotal = 0; - int satoshiBalancePending = 0; - int satoshiBalanceSpendable = 0; - int satoshiBalanceBlocked = 0; for (int i = 0; i < fetchedUtxoList.length; i++) { for (int j = 0; j < fetchedUtxoList[i].length; j++) { + final jsonUTXO = fetchedUtxoList[i][j]; + final txn = await cachedElectrumXClient.getTransaction( - txHash: fetchedUtxoList[i][j]["tx_hash"] as String, + txHash: jsonUTXO["tx_hash"] as String, verbose: true, coin: coin, ); - // todo check here if we should mark as blocked + final vout = jsonUTXO["tx_pos"] as int; + + final outputs = txn["vout"] as List; + + String? utxoOwnerAddress; + // get UTXO owner address + for (final output in outputs) { + if (output["n"] == vout) { + utxoOwnerAddress = + output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]?["address"] as String?; + } + } + final utxo = isar_models.UTXO( walletId: walletId, txid: txn["txid"] as String, - vout: fetchedUtxoList[i][j]["tx_pos"] as int, - value: fetchedUtxoList[i][j]["value"] as int, + vout: vout, + value: jsonUTXO["value"] as int, name: "", isBlocked: false, blockedReason: null, isCoinbase: txn["is_coinbase"] as bool? ?? false, blockHash: txn["blockhash"] as String?, - blockHeight: fetchedUtxoList[i][j]["height"] as int?, + blockHeight: jsonUTXO["height"] as int?, blockTime: txn["blocktime"] as int?, + address: utxoOwnerAddress, ); - satoshiBalanceTotal += utxo.value; - - if (utxo.isBlocked) { - satoshiBalanceBlocked += utxo.value; - } else { - if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - satoshiBalanceSpendable += utxo.value; - } else { - satoshiBalancePending += utxo.value; - } - } - outputArray.add(utxo); } } @@ -1759,27 +1784,20 @@ class NamecoinWallet extends CoinServiceAPI Logging.instance .log('Outputs fetched: $outputArray', level: LogLevel.Info); - // TODO move this out of here and into IDB - await db.isar.writeTxn(() async { - await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll(); - await db.isar.utxos.putAll(outputArray); - }); + await db.updateUTXOs(walletId, outputArray); // finally update balance - _balance = Balance( - coin: coin, - total: satoshiBalanceTotal, - spendable: satoshiBalanceSpendable, - blockedTotal: satoshiBalanceBlocked, - pendingSpendable: satoshiBalancePending, - ); - await updateCachedBalance(_balance!); + await _updateBalance(); } catch (e, s) { Logging.instance .log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error); } } + Future _updateBalance() async { + await refreshBalance(); + } + @override Balance get balance => _balance ??= getCachedBalance(); Balance? _balance; @@ -2207,11 +2225,12 @@ class NamecoinWallet extends CoinServiceAPI /// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return /// a map containing the tx hex along with other important information. If not, then it will return /// an integer (1 or 2) - dynamic coinSelection( - int satoshiAmountToSend, - int selectedTxFeeRate, - String _recipientAddress, - bool isSendAll, { + dynamic coinSelection({ + required int satoshiAmountToSend, + required int selectedTxFeeRate, + required String recipientAddress, + required bool coinControl, + required bool isSendAll, int additionalOutputs = 0, List? utxos, }) async { @@ -2223,18 +2242,26 @@ class NamecoinWallet extends CoinServiceAPI int spendableSatoshiValue = 0; // Build list of spendable outputs and totaling their satoshi amount - for (var i = 0; i < availableOutputs.length; i++) { - if (availableOutputs[i].isBlocked == false && - availableOutputs[i] - .isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) == - true) { - spendableOutputs.add(availableOutputs[i]); - spendableSatoshiValue += availableOutputs[i].value; + for (final utxo in availableOutputs) { + if (utxo.isBlocked == false && + utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) && + utxo.used != true) { + spendableOutputs.add(utxo); + spendableSatoshiValue += utxo.value; } } - // sort spendable by age (oldest first) - spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); + if (coinControl) { + if (spendableOutputs.length < availableOutputs.length) { + throw ArgumentError("Attempted to use an unavailable utxo"); + } + } + + // don't care about sorting if using all utxos + if (!coinControl) { + // sort spendable by age (oldest first) + spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); + } Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}", level: LogLevel.Info); @@ -2263,19 +2290,27 @@ class NamecoinWallet extends CoinServiceAPI int inputsBeingConsumed = 0; List utxoObjectsToUse = []; - for (var i = 0; - satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; - i++) { - utxoObjectsToUse.add(spendableOutputs[i]); - satoshisBeingUsed += spendableOutputs[i].value; - inputsBeingConsumed += 1; - } - for (int i = 0; - i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length; - i++) { - utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); - satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; - inputsBeingConsumed += 1; + if (!coinControl) { + for (var i = 0; + satoshisBeingUsed < satoshiAmountToSend && + i < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[i]); + satoshisBeingUsed += spendableOutputs[i].value; + inputsBeingConsumed += 1; + } + for (int i = 0; + i < additionalOutputs && + inputsBeingConsumed < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); + satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; + inputsBeingConsumed += 1; + } + } else { + satoshisBeingUsed = spendableSatoshiValue; + utxoObjectsToUse = spendableOutputs; + inputsBeingConsumed = spendableOutputs.length; } Logging.instance @@ -2286,7 +2321,7 @@ class NamecoinWallet extends CoinServiceAPI .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info); // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray - List recipientsArray = [_recipientAddress]; + List recipientsArray = [recipientAddress]; List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data @@ -2297,9 +2332,8 @@ class NamecoinWallet extends CoinServiceAPI .log("Attempting to send all $coin", level: LogLevel.Info); final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, - recipients: [_recipientAddress], + recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; int feeForOneOutput = estimateTxFee( @@ -2315,7 +2349,6 @@ class NamecoinWallet extends CoinServiceAPI final int amount = satoshiAmountToSend - feeForOneOutput; dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: [amount], @@ -2326,21 +2359,20 @@ class NamecoinWallet extends CoinServiceAPI "recipientAmt": amount, "fee": feeForOneOutput, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, - recipients: [_recipientAddress], + recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; final int vSizeForTwoOutPuts = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [ - _recipientAddress, + recipientAddress, await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)), ], satoshiAmounts: [ @@ -2403,7 +2435,6 @@ class NamecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2431,7 +2462,6 @@ class NamecoinWallet extends CoinServiceAPI Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2444,6 +2474,7 @@ class NamecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeBeingPaid, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2460,7 +2491,6 @@ class NamecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2471,6 +2501,7 @@ class NamecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2489,7 +2520,6 @@ class NamecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2500,6 +2530,7 @@ class NamecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2518,7 +2549,6 @@ class NamecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2529,6 +2559,7 @@ class NamecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeForOneOutput, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2540,256 +2571,158 @@ class NamecoinWallet extends CoinServiceAPI level: LogLevel.Warning); // try adding more outputs if (spendableOutputs.length > inputsBeingConsumed) { - return coinSelection(satoshiAmountToSend, selectedTxFeeRate, - _recipientAddress, isSendAll, - additionalOutputs: additionalOutputs + 1, utxos: utxos); + return coinSelection( + satoshiAmountToSend: satoshiAmountToSend, + selectedTxFeeRate: selectedTxFeeRate, + recipientAddress: recipientAddress, + isSendAll: isSendAll, + additionalOutputs: additionalOutputs + 1, + utxos: utxos, + coinControl: coinControl, + ); } return 2; } } - Future> fetchBuildTxData( + Future> fetchBuildTxData( List utxosToUse, ) async { // return data - Map results = {}; - Map> addressTxid = {}; - - // addresses to check - List addressesP2PKH = []; - List addressesP2SH = []; - List addressesP2WPKH = []; - Logging.instance.log("utxos: $utxosToUse", level: LogLevel.Info); + List signingData = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - final tx = await _cachedElectrumXClient.getTransaction( - txHash: txid, - coin: coin, - ); - Logging.instance.log("tx: ${json.encode(tx)}", - level: LogLevel.Info, printFullLength: true); - - for (final output in tx["vout"] as List) { - final n = output["n"]; - if (n != null && n == utxosToUse[i].vout) { - final address = output["scriptPubKey"]["address"] as String; - if (!addressTxid.containsKey(address)) { - addressTxid[address] = []; - } - (addressTxid[address] as List).add(txid); - switch (addressType(address: address)) { - case DerivePathType.bip44: - addressesP2PKH.add(address); - break; - case DerivePathType.bip49: - addressesP2SH.add(address); - break; - case DerivePathType.bip84: - addressesP2WPKH.add(address); - break; - default: - throw Exception("DerivePathType unsupported"); + if (utxosToUse[i].address == null) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + utxosToUse[i] = utxosToUse[i].copyWith( + address: output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]["address"] as String, + ); } } } + + final derivePathType = addressType(address: utxosToUse[i].address!); + + signingData.add( + SigningData( + derivePathType: derivePathType, + utxo: utxosToUse[i], + ), + ); } - // p2pkh / bip44 - final p2pkhLength = addressesP2PKH.length; - if (p2pkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip44, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip44, - ); - for (int i = 0; i < p2pkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2PKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - ).data; + Map> receiveDerivations = {}; + Map> changeDerivations = {}; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2PKH( + for (final sd in signingData) { + String? pubKey; + String? wif; + + // fetch receiving derivations if null + receiveDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 0, + derivePathType: sd.derivePathType, + ); + final receiveDerivation = + receiveDerivations[sd.derivePathType]![sd.utxo.address!]; + + if (receiveDerivation != null) { + pubKey = receiveDerivation["pubKey"] as String; + wif = receiveDerivation["wif"] as String; + } else { + // fetch change derivations if null + changeDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 1, + derivePathType: sd.derivePathType, + ); + final changeDerivation = + changeDerivations[sd.derivePathType]![sd.utxo.address!]; + if (changeDerivation != null) { + pubKey = changeDerivation["pubKey"] as String; + wif = changeDerivation["wif"] as String; + } + } + + if (wif == null || pubKey == null) { + final address = await db.getAddress(walletId, sd.utxo.address!); + if (address?.derivationPath != null) { + final node = await Bip32Utils.getBip32Node( + (await mnemonicString)!, + (await mnemonicPassphrase)!, + _network, + address!.derivationPath!.value, + ); + + wif = node.toWIF(); + pubKey = Format.uint8listToString(node.publicKey); + } + } + + if (wif != null && pubKey != null) { + final PaymentData data; + final Uint8List? redeemScript; + + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = P2PKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: _network, ).data; + redeemScript = null; + break; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } - } - } - } - - // p2sh / bip49 - final p2shLength = addressesP2SH.length; - if (p2shLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip49, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip49, - ); - for (int i = 0; i < p2shLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2SH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final p2wpkh = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - overridePrefix: namecoin.bech32!) - .data; - - final redeemScript = p2wpkh.output; - - final data = - P2SH(data: PaymentData(redeem: p2wpkh), network: _network).data; - - for (String tx in addressTxid[addressesP2SH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - "redeemScript": redeemScript, - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2SH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { + case DerivePathType.bip49: final p2wpkh = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), - network: _network, - overridePrefix: namecoin.bech32!) - .data; - - final redeemScript = p2wpkh.output; - - final data = - P2SH(data: PaymentData(redeem: p2wpkh), network: _network) - .data; - - for (String tx in addressTxid[addressesP2SH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - "redeemScript": redeemScript, - }; - } - } - } - } - } - - // p2wpkh / bip84 - final p2wpkhLength = addressesP2WPKH.length; - if (p2wpkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip84, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip84, - ); - - for (int i = 0; i < p2wpkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - overridePrefix: namecoin.bech32!) - .data; - - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, + data: PaymentData( + pubkey: Format.stringToUint8List(pubKey), ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), - network: _network, - overridePrefix: namecoin.bech32!) + network: _network, + overridePrefix: _network.bech32!, + ).data; + redeemScript = p2wpkh.output; + data = P2SH(data: PaymentData(redeem: p2wpkh), network: _network) .data; + break; - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } + case DerivePathType.bip84: + data = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List(pubKey), + ), + network: _network, + overridePrefix: _network.bech32!, + ).data; + redeemScript = null; + break; + + default: + throw Exception("DerivePathType unsupported"); } + + final keyPair = ECPair.fromWIF( + wif, + network: _network, + ); + + sd.redeemScript = redeemScript; + sd.output = data.output; + sd.keyPair = keyPair; } } - return results; + return signingData; } catch (e, s) { Logging.instance .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); @@ -2799,8 +2732,7 @@ class NamecoinWallet extends CoinServiceAPI /// Builds and signs a transaction Future> buildTransaction({ - required List utxosToUse, - required Map utxoSigningData, + required List utxoSigningData, required List recipients, required List satoshiAmounts, }) async { @@ -2811,10 +2743,15 @@ class NamecoinWallet extends CoinServiceAPI txb.setVersion(2); // Add transaction inputs - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - txb.addInput(txid, utxosToUse[i].vout, null, - utxoSigningData[txid]["output"] as Uint8List, namecoin.bech32!); + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + txb.addInput( + txid, + utxoSigningData[i].utxo.vout, + null, + utxoSigningData[i].output!, + _network.bech32!, + ); } // Add transaction output @@ -2824,14 +2761,15 @@ class NamecoinWallet extends CoinServiceAPI try { // Sign the transaction accordingly - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - txb.sign( - vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, - overridePrefix: namecoin.bech32!); + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + txb.addInput( + txid, + utxoSigningData[i].utxo.vout, + null, + utxoSigningData[i].output!, + _network.bech32!, + ); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s", diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 4142053c4..692ad7e62 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -17,12 +17,14 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/mixins/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; @@ -81,7 +83,8 @@ String constructDerivePath({ return "m/$purpose'/$coinType'/$account'/$chain/$index"; } -class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { +class ParticlWallet extends CoinServiceAPI + with WalletCache, WalletDB, CoinControlInterface { ParticlWallet({ required String walletId, required String walletName, @@ -101,6 +104,17 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { _secureStore = secureStore; initCache(walletId, coin); initWalletDB(mockableOverride: mockableOverride); + initCoinControlInterface( + walletId: walletId, + walletName: walletName, + coin: coin, + db: db, + getChainHeight: () => chainHeight, + refreshedBalanceCallback: (balance) async { + _balance = balance; + await updateCachedBalance(_balance!); + }, + ); } static const integrationTestFlag = @@ -935,6 +949,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { try { final feeRateType = args?["feeRate"]; final feeRateAmount = args?["feeRateAmount"]; + final utxos = args?["UTXOs"] as Set?; if (feeRateType is FeeRateType || feeRateAmount is int) { late final int rate; if (feeRateType is FeeRateType) { @@ -962,8 +977,16 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { isSendAll = true; } - final txData = - await coinSelection(satoshiAmount, rate, address, isSendAll); + final bool coinControl = utxos != null; + + final txData = await coinSelection( + satoshiAmountToSend: satoshiAmount, + selectedTxFeeRate: rate, + recipientAddress: address, + isSendAll: isSendAll, + utxos: utxos?.toList(), + coinControl: coinControl, + ); Logging.instance.log("prepare send: $txData", level: LogLevel.Info); try { @@ -1026,6 +1049,11 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex); Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + final utxos = txData["usedUTXOs"] as List; + + // mark utxos as used + await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList()); + return txHash; } catch (e, s) { Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", @@ -1595,49 +1623,47 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { } } - final currentChainHeight = await chainHeight; - final List outputArray = []; - int satoshiBalanceTotal = 0; - int satoshiBalancePending = 0; - int satoshiBalanceSpendable = 0; - int satoshiBalanceBlocked = 0; for (int i = 0; i < fetchedUtxoList.length; i++) { for (int j = 0; j < fetchedUtxoList[i].length; j++) { + final jsonUTXO = fetchedUtxoList[i][j]; + final txn = await cachedElectrumXClient.getTransaction( - txHash: fetchedUtxoList[i][j]["tx_hash"] as String, + txHash: jsonUTXO["tx_hash"] as String, verbose: true, coin: coin, ); - // todo check here if we should mark as blocked + final vout = jsonUTXO["tx_pos"] as int; + + final outputs = txn["vout"] as List; + + String? utxoOwnerAddress; + // get UTXO owner address + for (final output in outputs) { + if (output["n"] == vout) { + utxoOwnerAddress = + output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]?["address"] as String?; + } + } + final utxo = isar_models.UTXO( walletId: walletId, txid: txn["txid"] as String, - vout: fetchedUtxoList[i][j]["tx_pos"] as int, - value: fetchedUtxoList[i][j]["value"] as int, + vout: vout, + value: jsonUTXO["value"] as int, name: "", isBlocked: false, blockedReason: null, isCoinbase: txn["is_coinbase"] as bool? ?? false, blockHash: txn["blockhash"] as String?, - blockHeight: fetchedUtxoList[i][j]["height"] as int?, + blockHeight: jsonUTXO["height"] as int?, blockTime: txn["blocktime"] as int?, + address: utxoOwnerAddress, ); - satoshiBalanceTotal += utxo.value; - - if (utxo.isBlocked) { - satoshiBalanceBlocked += utxo.value; - } else { - if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - satoshiBalanceSpendable += utxo.value; - } else { - satoshiBalancePending += utxo.value; - } - } - outputArray.add(utxo); } } @@ -1645,27 +1671,20 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { Logging.instance .log('Outputs fetched: $outputArray', level: LogLevel.Info); - // TODO move this out of here and into IDB - await db.isar.writeTxn(() async { - await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll(); - await db.isar.utxos.putAll(outputArray); - }); + await db.updateUTXOs(walletId, outputArray); // finally update balance - _balance = Balance( - coin: coin, - total: satoshiBalanceTotal, - spendable: satoshiBalanceSpendable, - blockedTotal: satoshiBalanceBlocked, - pendingSpendable: satoshiBalancePending, - ); - await updateCachedBalance(_balance!); + await _updateBalance(); } catch (e, s) { Logging.instance .log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error); } } + Future _updateBalance() async { + await refreshBalance(); + } + @override Balance get balance => _balance ??= getCachedBalance(); Balance? _balance; @@ -2367,11 +2386,12 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { /// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return /// a map containing the tx hex along with other important information. If not, then it will return /// an integer (1 or 2) - dynamic coinSelection( - int satoshiAmountToSend, - int selectedTxFeeRate, - String _recipientAddress, - bool isSendAll, { + dynamic coinSelection({ + required int satoshiAmountToSend, + required int selectedTxFeeRate, + required String recipientAddress, + required bool coinControl, + required bool isSendAll, int additionalOutputs = 0, List? utxos, }) async { @@ -2381,19 +2401,28 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { final currentChainHeight = await chainHeight; final List spendableOutputs = []; int spendableSatoshiValue = 0; + // Build list of spendable outputs and totaling their satoshi amount - for (var i = 0; i < availableOutputs.length; i++) { - if (availableOutputs[i].isBlocked == false && - availableOutputs[i] - .isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) == - true) { - spendableOutputs.add(availableOutputs[i]); - spendableSatoshiValue += availableOutputs[i].value; + for (final utxo in availableOutputs) { + if (utxo.isBlocked == false && + utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) && + utxo.used != true) { + spendableOutputs.add(utxo); + spendableSatoshiValue += utxo.value; } } - // sort spendable by age (oldest first) - spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); + if (coinControl) { + if (spendableOutputs.length < availableOutputs.length) { + throw ArgumentError("Attempted to use an unavailable utxo"); + } + } + + // don't care about sorting if using all utxos + if (!coinControl) { + // sort spendable by age (oldest first) + spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); + } Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}", level: LogLevel.Info); @@ -2422,19 +2451,27 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { int inputsBeingConsumed = 0; List utxoObjectsToUse = []; - for (var i = 0; - satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; - i++) { - utxoObjectsToUse.add(spendableOutputs[i]); - satoshisBeingUsed += spendableOutputs[i].value; - inputsBeingConsumed += 1; - } - for (int i = 0; - i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length; - i++) { - utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); - satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; - inputsBeingConsumed += 1; + if (!coinControl) { + for (var i = 0; + satoshisBeingUsed < satoshiAmountToSend && + i < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[i]); + satoshisBeingUsed += spendableOutputs[i].value; + inputsBeingConsumed += 1; + } + for (int i = 0; + i < additionalOutputs && + inputsBeingConsumed < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); + satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; + inputsBeingConsumed += 1; + } + } else { + satoshisBeingUsed = spendableSatoshiValue; + utxoObjectsToUse = spendableOutputs; + inputsBeingConsumed = spendableOutputs.length; } Logging.instance @@ -2445,7 +2482,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info); // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray - List recipientsArray = [_recipientAddress]; + List recipientsArray = [recipientAddress]; List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data @@ -2456,9 +2493,8 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { .log("Attempting to send all $coin", level: LogLevel.Info); final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, - recipients: [_recipientAddress], + recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; int feeForOneOutput = estimateTxFee( @@ -2474,7 +2510,6 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { final int amount = satoshiAmountToSend - feeForOneOutput; dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: [amount], @@ -2485,21 +2520,20 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { "recipientAmt": amount, "fee": feeForOneOutput, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, - recipients: [_recipientAddress], + recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; final int vSizeForTwoOutPuts = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [ - _recipientAddress, + recipientAddress, await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)), ], satoshiAmounts: [ @@ -2562,7 +2596,6 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { Logging.instance .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2590,7 +2623,6 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2603,6 +2635,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { "recipientAmt": recipientsAmtArray[0], "fee": feeBeingPaid, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2619,7 +2652,6 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2630,6 +2662,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2648,7 +2681,6 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2659,6 +2691,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2677,7 +2710,6 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2688,6 +2720,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { "recipientAmt": recipientsAmtArray[0], "fee": feeForOneOutput, "vSize": txn["vSize"], + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2699,176 +2732,144 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { level: LogLevel.Warning); // try adding more outputs if (spendableOutputs.length > inputsBeingConsumed) { - return coinSelection(satoshiAmountToSend, selectedTxFeeRate, - _recipientAddress, isSendAll, - additionalOutputs: additionalOutputs + 1, utxos: utxos); + return coinSelection( + satoshiAmountToSend: satoshiAmountToSend, + selectedTxFeeRate: selectedTxFeeRate, + recipientAddress: recipientAddress, + isSendAll: isSendAll, + additionalOutputs: additionalOutputs + 1, + utxos: utxos, + coinControl: coinControl, + ); } return 2; } } - Future> fetchBuildTxData( + Future> fetchBuildTxData( List utxosToUse, ) async { // return data - Map results = {}; - Map> addressTxid = {}; - - // addresses to check - List addressesP2PKH = []; - List addressesP2WPKH = []; + List signingData = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - final tx = await _cachedElectrumXClient.getTransaction( - txHash: txid, - coin: coin, - ); - - for (final output in tx["vout"] as List) { - final n = output["n"]; - if (n != null && n == utxosToUse[i].vout) { - final address = output["scriptPubKey"]["addresses"][0] as String; - if (!addressTxid.containsKey(address)) { - addressTxid[address] = []; - } - (addressTxid[address] as List).add(txid); - switch (addressType(address: address)) { - case DerivePathType.bip44: - addressesP2PKH.add(address); - break; - case DerivePathType.bip84: - addressesP2WPKH.add(address); - break; - default: - throw Exception( - "DerivePathType ${addressType(address: address)} not supported"); + if (utxosToUse[i].address == null) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + utxosToUse[i] = utxosToUse[i].copyWith( + address: output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]["address"] as String, + ); } } } + + final derivePathType = addressType(address: utxosToUse[i].address!); + + signingData.add( + SigningData( + derivePathType: derivePathType, + utxo: utxosToUse[i], + ), + ); } - // p2pkh / bip44 - final p2pkhLength = addressesP2PKH.length; - if (p2pkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip44, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip44, - ); - for (int i = 0; i < p2pkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2PKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - ).data; + Map> receiveDerivations = {}; + Map> changeDerivations = {}; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2PKH( + for (final sd in signingData) { + String? pubKey; + String? wif; + + // fetch receiving derivations if null + receiveDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 0, + derivePathType: sd.derivePathType, + ); + final receiveDerivation = + receiveDerivations[sd.derivePathType]![sd.utxo.address!]; + + if (receiveDerivation != null) { + pubKey = receiveDerivation["pubKey"] as String; + wif = receiveDerivation["wif"] as String; + } else { + // fetch change derivations if null + changeDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 1, + derivePathType: sd.derivePathType, + ); + final changeDerivation = + changeDerivations[sd.derivePathType]![sd.utxo.address!]; + if (changeDerivation != null) { + pubKey = changeDerivation["pubKey"] as String; + wif = changeDerivation["wif"] as String; + } + } + + if (wif == null || pubKey == null) { + final address = await db.getAddress(walletId, sd.utxo.address!); + if (address?.derivationPath != null) { + final node = await Bip32Utils.getBip32Node( + (await mnemonicString)!, + (await mnemonicPassphrase)!, + _network, + address!.derivationPath!.value, + ); + + wif = node.toWIF(); + pubKey = Format.uint8listToString(node.publicKey); + } + } + + if (wif != null && pubKey != null) { + final PaymentData data; + final Uint8List? redeemScript; + + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = P2PKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: _network, ).data; + redeemScript = null; + break; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } - } - } - } - - // p2wpkh / bip84 - final p2wpkhLength = addressesP2WPKH.length; - if (p2wpkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip84, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip84, - ); - - for (int i = 0; i < p2wpkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - ).data; - - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2WPKH( + case DerivePathType.bip84: + data = P2WPKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: _network, ).data; + redeemScript = null; + break; - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } + default: + throw Exception("DerivePathType unsupported"); } + + final keyPair = ECPair.fromWIF( + wif, + network: _network, + ); + + sd.redeemScript = redeemScript; + sd.output = data.output; + sd.keyPair = keyPair; } } - Logging.instance.log("FETCHED TX BUILD DATA IS -----$results", - level: LogLevel.Info, printFullLength: true); - return results; + + return signingData; } catch (e, s) { Logging.instance .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); @@ -2878,8 +2879,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { /// Builds and signs a transaction Future> buildTransaction({ - required List utxosToUse, - required Map utxoSigningData, + required List utxoSigningData, required List recipients, required List satoshiAmounts, }) async { @@ -2893,11 +2893,15 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { txb.setVersion(160); // Add transaction inputs - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - - txb.addInput(txid, utxosToUse[i].vout, null, - utxoSigningData[txid]["output"] as Uint8List, ''); + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + txb.addInput( + txid, + utxoSigningData[i].utxo.vout, + null, + utxoSigningData[i].output!, + '', + ); } // Add transaction output @@ -2907,13 +2911,13 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB { try { // Sign the transaction accordingly - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; + for (var i = 0; i < utxoSigningData.length; i++) { txb.sign( - vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?); + vin: i, + keyPair: utxoSigningData[i].keyPair!, + witnessValue: utxoSigningData[i].utxo.value, + redeemScript: utxoSigningData[i].redeemScript, + ); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s", diff --git a/lib/services/event_bus/events/global/balance_refreshed_event.dart b/lib/services/event_bus/events/global/balance_refreshed_event.dart new file mode 100644 index 000000000..cc6e6efc3 --- /dev/null +++ b/lib/services/event_bus/events/global/balance_refreshed_event.dart @@ -0,0 +1,12 @@ +import 'package:stackwallet/utilities/logger.dart'; + +class BalanceRefreshedEvent { + final String walletId; + + BalanceRefreshedEvent(this.walletId) { + Logging.instance.log( + "BalanceRefreshedEvent fired on $walletId", + level: LogLevel.Info, + ); + } +} diff --git a/lib/services/exchange/majestic_bank/majestic_bank_api.dart b/lib/services/exchange/majestic_bank/majestic_bank_api.dart index 1aa2394c9..61eb886a4 100644 --- a/lib/services/exchange/majestic_bank/majestic_bank_api.dart +++ b/lib/services/exchange/majestic_bank/majestic_bank_api.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:decimal/decimal.dart'; import 'package:http/http.dart' as http; import 'package:stackwallet/exceptions/exchange/exchange_exception.dart'; +import 'package:stackwallet/exceptions/exchange/majestic_bank/mb_exception.dart'; import 'package:stackwallet/exceptions/exchange/pair_unavailable_exception.dart'; import 'package:stackwallet/models/exchange/majestic_bank/mb_limit.dart'; import 'package:stackwallet/models/exchange/majestic_bank/mb_order.dart'; @@ -335,6 +336,15 @@ class MajesticBankAPI { final jsonObject = await _makeGetRequest(uri); final json = Map.from(jsonObject as Map); + if (json.length == 2) { + return ExchangeResponse( + exception: MBException( + json["status"] as String, + ExchangeExceptionType.orderNotFound, + ), + ); + } + final status = MBOrderStatus( orderId: json["trx"] as String, status: json["status"] as String, @@ -349,8 +359,10 @@ class MajesticBankAPI { return ExchangeResponse(value: status); } catch (e, s) { - Logging.instance - .log("createOrder exception: $e\n$s", level: LogLevel.Error); + Logging.instance.log( + "trackOrder exception when trying to parse $json: $e\n$s", + level: LogLevel.Error, + ); return ExchangeResponse( exception: ExchangeException( e.toString(), diff --git a/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart b/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart index 24403b722..dcc63c68b 100644 --- a/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart +++ b/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart @@ -283,6 +283,33 @@ class MajesticBankExchange extends Exchange { return ExchangeResponse(value: updatedTrade); } else { + if (response.exception?.type == ExchangeExceptionType.orderNotFound) { + final updatedTrade = Trade( + uuid: trade.uuid, + tradeId: trade.tradeId, + rateType: trade.rateType, + direction: trade.direction, + timestamp: trade.timestamp, + updatedAt: DateTime.now(), + payInCurrency: trade.payInCurrency, + payInAmount: trade.payInAmount, + payInAddress: trade.payInAddress, + payInNetwork: trade.payInNetwork, + payInExtraId: trade.payInExtraId, + payInTxid: trade.payInTxid, + payOutCurrency: trade.payOutCurrency, + payOutAmount: trade.payOutAmount, + payOutAddress: trade.payOutAddress, + payOutNetwork: trade.payOutNetwork, + payOutExtraId: trade.payOutExtraId, + payOutTxid: trade.payOutTxid, + refundAddress: trade.refundAddress, + refundExtraId: trade.refundExtraId, + status: "Completed", + exchangeName: exchangeName, + ); + return ExchangeResponse(value: updatedTrade); + } return ExchangeResponse(exception: response.exception); } } diff --git a/lib/services/mixins/coin_control_interface.dart b/lib/services/mixins/coin_control_interface.dart new file mode 100644 index 000000000..ceebb6416 --- /dev/null +++ b/lib/services/mixins/coin_control_interface.dart @@ -0,0 +1,78 @@ +import 'dart:async'; + +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/services/event_bus/events/global/balance_refreshed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +mixin CoinControlInterface { + late final String _walletId; + late final String _walletName; + late final Coin _coin; + late final MainDB _db; + late final Future Function() _getChainHeight; + late final Future Function(Balance) _refreshedBalanceCallback; + + void initCoinControlInterface({ + required String walletId, + required String walletName, + required Coin coin, + required MainDB db, + required Future Function() getChainHeight, + required Future Function(Balance) refreshedBalanceCallback, + }) { + _walletId = walletId; + _walletName = walletName; + _coin = coin; + _db = db; + _getChainHeight = getChainHeight; + _refreshedBalanceCallback = refreshedBalanceCallback; + } + + Future refreshBalance({bool notify = false}) async { + final utxos = await _db.getUTXOs(_walletId).findAll(); + final currentChainHeight = await _getChainHeight(); + + int satoshiBalanceTotal = 0; + int satoshiBalancePending = 0; + int satoshiBalanceSpendable = 0; + int satoshiBalanceBlocked = 0; + + for (final utxo in utxos) { + satoshiBalanceTotal += utxo.value; + + if (utxo.isBlocked) { + satoshiBalanceBlocked += utxo.value; + } else { + if (utxo.isConfirmed( + currentChainHeight, + _coin.requiredConfirmations, + )) { + satoshiBalanceSpendable += utxo.value; + } else { + satoshiBalancePending += utxo.value; + } + } + } + + final balance = Balance( + coin: _coin, + total: satoshiBalanceTotal, + spendable: satoshiBalanceSpendable, + blockedTotal: satoshiBalanceBlocked, + pendingSpendable: satoshiBalancePending, + ); + + await _refreshedBalanceCallback(balance); + + if (notify) { + GlobalEventBus.instance.fire( + BalanceRefreshedEvent( + _walletId, + ), + ); + } + } +} diff --git a/lib/services/mixins/paynym_wallet_interface.dart b/lib/services/mixins/paynym_wallet_interface.dart index 730dfd2bf..8ef6b8f2c 100644 --- a/lib/services/mixins/paynym_wallet_interface.dart +++ b/lib/services/mixins/paynym_wallet_interface.dart @@ -15,6 +15,7 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/exceptions/wallet/insufficient_balance_exception.dart'; import 'package:stackwallet/exceptions/wallet/paynym_send_exception.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/bip47_utils.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -57,7 +58,7 @@ mixin PaynymWalletInterface { late final Future Function({ required String address, }) _getTxCount; - late final Future> Function( + late final Future> Function( List utxosToUse, ) _fetchBuildTxData; late final Future Function() _refresh; @@ -100,7 +101,7 @@ mixin PaynymWalletInterface { required String address, }) getTxCount, - required Future> Function( + required Future> Function( List utxosToUse, ) fetchBuildTxData, @@ -455,7 +456,6 @@ mixin PaynymWalletInterface { final int vSizeForNoChange = (await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, change: 0, dustLimit: @@ -465,7 +465,6 @@ mixin PaynymWalletInterface { final int vSizeForWithChange = (await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, change: satoshisBeingUsed - amountToSend, )) @@ -503,7 +502,6 @@ mixin PaynymWalletInterface { feeForWithChange) { var txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, change: changeAmount, ); @@ -516,7 +514,6 @@ mixin PaynymWalletInterface { feeBeingPaid += 1; txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, change: changeAmount, ); @@ -528,6 +525,7 @@ mixin PaynymWalletInterface { "amount": amountToSend, "fee": feeBeingPaid, "vSize": txn.item2, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -535,7 +533,6 @@ mixin PaynymWalletInterface { // than the dust limit. Try without change final txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, change: 0, ); @@ -548,6 +545,7 @@ mixin PaynymWalletInterface { "amount": amountToSend, "fee": feeBeingPaid, "vSize": txn.item2, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -556,7 +554,6 @@ mixin PaynymWalletInterface { // build without change here final txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, change: 0, ); @@ -569,6 +566,7 @@ mixin PaynymWalletInterface { "amount": amountToSend, "fee": feeBeingPaid, "vSize": txn.item2, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -594,8 +592,7 @@ mixin PaynymWalletInterface { // equal to its vSize Future> _createNotificationTx({ required String targetPaymentCodeString, - required List utxosToUse, - required Map utxoSigningData, + required List utxoSigningData, required int change, int? dustLimit, }) async { @@ -604,7 +601,7 @@ mixin PaynymWalletInterface { PaymentCode.fromPaymentCode(targetPaymentCodeString, _network); final myCode = await getPaymentCode(DerivePathType.bip44); - final utxo = utxosToUse.first; + final utxo = utxoSigningData.first.utxo; final txPoint = utxo.txid.fromHex.reversed.toList(); final txPointIndex = utxo.vout; @@ -613,8 +610,7 @@ mixin PaynymWalletInterface { final buffer = rev.buffer.asByteData(); buffer.setUint32(txPoint.length, txPointIndex, Endian.little); - final myKeyPair = - utxoSigningData[utxo.txid]["keyPair"] as btc_dart.ECPair; + final myKeyPair = utxoSigningData.first.keyPair!; final S = SecretPoint( myKeyPair.privateKey!, @@ -642,17 +638,17 @@ mixin PaynymWalletInterface { utxo.txid, txPointIndex, null, - utxoSigningData[utxo.txid]["output"] as Uint8List, + utxoSigningData.first.output!, ); // add rest of possible inputs - for (var i = 1; i < utxosToUse.length; i++) { - final utxo = utxosToUse[i]; + for (var i = 1; i < utxoSigningData.length; i++) { + final utxo = utxoSigningData[i].utxo; txb.addInput( utxo.txid, utxo.vout, null, - utxoSigningData[utxo.txid]["output"] as Uint8List, + utxoSigningData[i].output!, ); } @@ -675,18 +671,16 @@ mixin PaynymWalletInterface { vin: 0, keyPair: myKeyPair, witnessValue: utxo.value, - witnessScript: utxoSigningData[utxo.txid]["redeemScript"] as Uint8List?, + witnessScript: utxoSigningData.first.redeemScript, ); // sign rest of possible inputs - for (var i = 1; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; + for (var i = 1; i < utxoSigningData.length; i++) { txb.sign( vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as btc_dart.ECPair, - witnessValue: utxosToUse[i].value, - witnessScript: - utxoSigningData[utxo.txid]["redeemScript"] as Uint8List?, + keyPair: utxoSigningData[i].keyPair!, + witnessValue: utxoSigningData[i].utxo.value, + witnessScript: utxoSigningData[i].redeemScript, ); } diff --git a/lib/services/notifications_service.dart b/lib/services/notifications_service.dart index c21eab41b..54f6d60ae 100644 --- a/lib/services/notifications_service.dart +++ b/lib/services/notifications_service.dart @@ -223,6 +223,9 @@ class NotificationsService extends ChangeNotifier { case "expired": case "Finished": case "finished": + case "Completed": + case "completed": + case "Not found": shouldWatchForUpdates = false; break; default: diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 308c7b49a..189a34c3b 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; abstract class Assets { static const svg = _SVG(); @@ -10,49 +12,107 @@ abstract class Assets { static const socials = _SOCIALS(); static const exchange = _EXCHANGE(); static const buy = _BUY(); + static const gif = _GIF(); + + static Future precache(BuildContext context) async { + final assets = [ + svg.iconFor(coin: Coin.dogecoin), + svg.stack(context), + svg.personaEasy(context), + svg.personaIncognito(context), + ...Coin.values.map( + (e) => svg.imageFor(context: context, coin: e), + ), + ]; + + if (Util.isDesktop) { + assets.add(svg.themeChan); + } + + final futures = assets.map( + (e) => precachePicture( + ExactAssetPicture( + SvgPicture.svgStringDecoderBuilder, + e, + ), + context), + ); + + await Future.wait(futures); + } } class _SOCIALS { const _SOCIALS(); - String get discord => "assets/svg/socials/discord.svg"; - String get reddit => "assets/svg/socials/reddit-alien-brands.svg"; - String get twitter => "assets/svg/socials/twitter-brands.svg"; - String get telegram => "assets/svg/socials/telegram-brands.svg"; + static const _path = "assets/svg/socials/"; + + String get discord => "${_path}discord.svg"; + String get reddit => "${_path}reddit-alien-brands.svg"; + String get twitter => "${_path}twitter-brands.svg"; + String get telegram => "${_path}telegram-brands.svg"; } class _EXCHANGE { const _EXCHANGE(); - String get changeNow => "assets/svg/exchange_icons/change_now_logo_1.svg"; - String get simpleSwap => "assets/svg/exchange_icons/simpleswap-icon.svg"; - String get majesticBankBlue => "assets/svg/exchange_icons/mb_blue.svg"; - String get majesticBankGreen => "assets/svg/exchange_icons/mb_green.svg"; + static const _path = "assets/svg/exchange_icons/"; + + String get changeNow => "${_path}change_now_logo_1.svg"; + String get simpleSwap => "${_path}simpleswap-icon.svg"; + String get majesticBankBlue => "${_path}mb_blue.svg"; + String get majesticBankGreen => "${_path}mb_green.svg"; } class _BUY { const _BUY(); - // TODO: switch this to something like - String buy(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/buy-coins-icon.svg"; - String simplexLogo(BuildContext context) { - return (Theme.of(context).extension()!.themeType == - ThemeType.dark || - Theme.of(context).extension()!.themeType == - ThemeType - .oledBlack) // TODO make sure this cover OLED black, too - ? "assets/svg/buy/Simplex-Nuvei-Logo-light.svg" - : "assets/svg/buy/Simplex-Nuvei-Logo.svg"; + switch (Theme.of(context).extension()!.themeType) { + case ThemeType.dark: + case ThemeType.oledBlack: + return "assets/svg/buy/Simplex-Nuvei-Logo-light.svg"; + + case ThemeType.fruitSorbet: + case ThemeType.forest: + case ThemeType.oceanBreeze: + case ThemeType.light: + case ThemeType.chan: + return "assets/svg/buy/Simplex-Nuvei-Logo.svg"; + } } } +class _COIN_CONTROL { + const _COIN_CONTROL(); + + static const _path = "assets/svg/coin_control/"; + + String get blocked => "${_path}frozen.svg"; + String get unBlocked => "${_path}unfrozen.svg"; + String get gamePad => "${_path}gamepad.svg"; + String get selected => "${_path}selected.svg"; +} + class _SVG { const _SVG(); + + static String _path(BuildContext context) { + switch (Theme.of(context).extension()!.themeType) { + // chan theme uses all the same assets as the light theme + case ThemeType.chan: + return "assets/svg/themed/${ThemeType.light.name}"; + default: + return "assets/svg/themed/${Theme.of(context).extension()!.themeType.name}"; + } + } + + final coinControl = const _COIN_CONTROL(); + String? background(BuildContext context) { switch (Theme.of(context).extension()!.themeType) { case ThemeType.light: + case ThemeType.chan: case ThemeType.dark: case ThemeType.oledBlack: return null; @@ -60,47 +120,41 @@ class _SVG { case ThemeType.oceanBreeze: case ThemeType.fruitSorbet: case ThemeType.forest: - return "assets/svg/${Theme.of(context).extension()!.themeType.name}/bg.svg"; + return "${_path(context)}/bg.svg"; } } - String bellNew(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/bell-new.svg"; - String stackIcon(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/stack-icon1.svg"; - String exchange(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/exchange-2.svg"; - String buy(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/buy-coins-icon.svg"; + String bellNew(BuildContext context) => "${_path(context)}/bell-new.svg"; + String stackIcon(BuildContext context) => "${_path(context)}/stack-icon1.svg"; + String exchange(BuildContext context) => "${_path(context)}/exchange-2.svg"; + String buy(BuildContext context) => "${_path(context)}/buy-coins-icon.svg"; String receive(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/tx-icon-receive.svg"; + "${_path(context)}/tx-icon-receive.svg"; String receivePending(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/tx-icon-receive-pending.svg"; + "${_path(context)}/tx-icon-receive-pending.svg"; String receiveCancelled(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/tx-icon-receive-failed.svg"; + "${_path(context)}/tx-icon-receive-failed.svg"; - String send(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/tx-icon-send.svg"; + String send(BuildContext context) => "${_path(context)}/tx-icon-send.svg"; String sendPending(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/tx-icon-send-pending.svg"; + "${_path(context)}/tx-icon-send-pending.svg"; String sendCancelled(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/tx-icon-send-failed.svg"; + "${_path(context)}/tx-icon-send-failed.svg"; String txExchange(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/tx-exchange-icon.svg"; + "${_path(context)}/tx-exchange-icon.svg"; String txExchangePending(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/tx-exchange-icon-pending.svg"; + "${_path(context)}/tx-exchange-icon-pending.svg"; String txExchangeFailed(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/tx-exchange-icon-failed.svg"; + "${_path(context)}/tx-exchange-icon-failed.svg"; String personaIncognito(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/persona-incognito-1.svg"; + "${_path(context)}/persona-incognito-1.svg"; String personaEasy(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/persona-easy-1.svg"; + "${_path(context)}/persona-easy-1.svg"; - String stack(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/stack.svg"; + String stack(BuildContext context) => "${_path(context)}/stack.svg"; String get themeFruit => "assets/svg/fruit-sorbet-theme.svg"; String get themeForest => "assets/svg/forest-theme.svg"; @@ -108,6 +162,7 @@ class _SVG { String get themeOcean => "assets/svg/ocean-breeze-theme.svg"; String get themeLight => "assets/svg/light-mode.svg"; String get themeDark => "assets/svg/dark-theme.svg"; + String get themeChan => "assets/svg/chanstheme.svg"; String get circleSliders => "assets/svg/configuration.svg"; String get circlePlus => "assets/svg/plus-circle.svg"; @@ -198,7 +253,6 @@ class _SVG { String get anonymizeFailed => "assets/svg/tx-icon-anonymize-failed.svg"; String get addressBookDesktop => "assets/svg/address-book-desktop.svg"; String get exchangeDesktop => "assets/svg/exchange-desktop.svg"; - String get buyDesktop => "assets/svg/light/buy-coins-icon.svg"; String get aboutDesktop => "assets/svg/about-desktop.svg"; String get walletDesktop => "assets/svg/wallet-desktop.svg"; String get exitDesktop => "assets/svg/exit-desktop.svg"; @@ -214,6 +268,20 @@ class _SVG { String get ellipse1 => "assets/svg/Ellipse-43.svg"; String get ellipse2 => "assets/svg/Ellipse-42.svg"; + String get chevronRight => "assets/svg/chevron-right.svg"; + String get minimize => "assets/svg/minimize.svg"; + String get walletFa => "assets/svg/wallet-fa.svg"; + String get exchange3 => "assets/svg/exchange-3.svg"; + String get messageQuestion => "assets/svg/message-question-1.svg"; + String get list => "assets/svg/list-ul.svg"; + String get unclaimedPaynym => "assets/svg/unclaimed.png"; + +// TODO provide proper assets + String get bitcoinTestnet => "assets/svg/coin_icons/Bitcoin.svg"; + String get bitcoincashTestnet => "assets/svg/coin_icons/Bitcoincash.svg"; + String get firoTestnet => "assets/svg/coin_icons/Firo.svg"; + String get dogecoinTestnet => "assets/svg/coin_icons/Dogecoin.svg"; + String get particlTestnet => "assets/svg/coin_icons/Particl.svg"; // small icons String get bitcoin => "assets/svg/coin_icons/Bitcoin.svg"; @@ -288,28 +356,22 @@ class _SVG { } // big icons - String bitcoinImage(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/bitcoin.svg"; + String bitcoinImage(BuildContext context) => "${_path(context)}/bitcoin.svg"; String bitcoincashImage(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/bitcoincash.svg"; - String dogecoinImage(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/doge.svg"; + "${_path(context)}/bitcoincash.svg"; + String dogecoinImage(BuildContext context) => "${_path(context)}/doge.svg"; String epicCashImage(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/epic-cash.svg"; + "${_path(context)}/epic-cash.svg"; String ethereumImage(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/ethereum.svg"; - String firoImage(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/firo.svg"; + "${_path(context)}/ethereum.svg"; + String firoImage(BuildContext context) => "${_path(context)}/firo.svg"; String litecoinImage(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/litecoin.svg"; - String moneroImage(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/monero.svg"; - String wowneroImage(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/wownero.svg"; + "${_path(context)}/litecoin.svg"; + String moneroImage(BuildContext context) => "${_path(context)}/monero.svg"; + String wowneroImage(BuildContext context) => "${_path(context)}/wownero.svg"; String namecoinImage(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/namecoin.svg"; - String particlImage(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/particl.svg"; + "${_path(context)}/namecoin.svg"; + String particlImage(BuildContext context) => "${_path(context)}/particl.svg"; String imageFor({required BuildContext context, required Coin coin}) { switch (coin) { @@ -351,79 +413,26 @@ class _SVG { class _PNG { const _PNG(); - String get unclaimedPaynym => "assets/images/unclaimed.png"; - String stack(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/stack.png"; String get splash => "assets/images/splash.png"; - String monero(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/monero.png"; - String wownero(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/wownero.png"; - String firo(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/firo.png"; - String dogecoin(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/doge.png"; - String bitcoin(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/bitcoin.png"; - String litecoin(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/litecoin.png"; - String epicCash(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/epic-cash.png"; - String ethereum(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/ethereum.png"; - String bitcoincash(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/bitcoincash.png"; - String namecoin(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/namecoin.png"; - String particl(BuildContext context) => - "assets/images/${Theme.of(context).extension()!.themeType.name}/particl.png"; - - String personaIncognito(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/persona-incognito-1.png"; - String personaEasy(BuildContext context) => - "assets/svg/${Theme.of(context).extension()!.themeType.name}/persona-easy-1.png"; - String get glasses => "assets/images/glasses.png"; String get glassesHidden => "assets/images/glasses-hidden.png"; - - String imageFor({required BuildContext context, required Coin coin}) { - switch (coin) { - case Coin.bitcoin: - case Coin.bitcoinTestNet: - return bitcoin(context); - case Coin.litecoin: - case Coin.litecoinTestNet: - return litecoin(context); - case Coin.bitcoincash: - case Coin.bitcoincashTestnet: - return bitcoincash(context); - case Coin.dogecoin: - case Coin.dogecoinTestNet: - return dogecoin(context); - case Coin.epicCash: - return epicCash(context); - case Coin.ethereum: - return ethereum(context); - case Coin.firo: - return firo(context); - case Coin.firoTestNet: - return firo(context); - case Coin.monero: - return monero(context); - case Coin.wownero: - return wownero(context); - case Coin.namecoin: - return namecoin(context); - case Coin.particl: - return particl(context); - } - } } class _ANIMATIONS { const _ANIMATIONS(); - String get test => "assets/lottie/test.json"; String get test2 => "assets/lottie/test2.json"; } + +class _GIF { + const _GIF(); + + String plain(Coin coin) { + return "assets/gif/coins/${coin.mainNetVersion.name}/plain.gif"; + } + + String kiss(Coin coin) { + return "assets/gif/coins/${coin.mainNetVersion.name}/kiss.gif"; + } +} diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index dbb50a554..242992233 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -18,12 +18,12 @@ abstract class Constants { static void exchangeForExperiencedUsers(int count) { enableExchange = Util.isDesktop || Platform.isAndroid || count > 5 || !Platform.isIOS; - enableBuy = - Util.isDesktop || Platform.isAndroid || count > 5 || !Platform.isIOS; } static bool enableExchange = Util.isDesktop || !Platform.isIOS; - static bool enableBuy = Util.isDesktop || !Platform.isIOS; + // just use enable exchange flag + // static bool enableBuy = enableExchange; + // // true; // true for development, static const int _satsPerCoinEthereum = 1000000000000000000; static const int _satsPerCoinMonero = 1000000000000; diff --git a/lib/utilities/default_epicboxes.dart b/lib/utilities/default_epicboxes.dart new file mode 100644 index 000000000..ecbd4524a --- /dev/null +++ b/lib/utilities/default_epicboxes.dart @@ -0,0 +1,43 @@ +import 'package:stackwallet/models/epicbox_server_model.dart'; + +abstract class DefaultEpicBoxes { + static const String defaultName = "Default"; + + static List get all => [americas, asia, europe]; + static List get defaultIds => ['americas', 'asia', 'europe']; + + static EpicBoxServerModel get americas => EpicBoxServerModel( + host: 'epicbox.epic.tech', + port: 443, + name: 'Americas', + id: 'americas', + useSSL: true, + enabled: true, + isFailover: true, + isDown: false, + ); + + static EpicBoxServerModel get asia => EpicBoxServerModel( + host: 'epicbox.hyperbig.com', + port: 443, + name: 'Asia', + id: 'asia', + useSSL: true, + enabled: true, + isFailover: true, + isDown: false, + ); + + static EpicBoxServerModel get europe => EpicBoxServerModel( + host: 'epicbox.fastepic.eu', + port: 443, + name: 'Europe', + id: 'europe', + useSSL: true, + enabled: true, + isFailover: true, + isDown: false, + ); + + static final defaultEpicBoxServer = americas; +} diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 02865d372..785c5561b 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; // import 'package:web3dart/browser.dart'; @@ -272,18 +270,4 @@ abstract class DefaultNodes { return dogecoinTestnet; } } - - static final String defaultEpicBoxConfig = jsonEncode({ - "epicbox_domain": "epicbox.epic.tech", - "epicbox_port": 443, - "epicbox_protocol_unsecure": false, - "epicbox_address_index": 0, - }); - - static final String epicBoxConfigEUR = jsonEncode({ - "epicbox_domain": "epicbox.fastepic.eu", - "epicbox_port": 443, - "epicbox_protocol_unsecure": false, - "epicbox_address_index": 0, - }); } diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index d65ccb976..0463b4fa4 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -16,6 +16,7 @@ import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' import 'package:stackwallet/services/coins/particl/particl_wallet.dart' as particl; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; +import 'package:stackwallet/utilities/constants.dart'; enum Coin { bitcoin, @@ -205,6 +206,60 @@ extension CoinExt on Coin { } } + bool get isTestNet { + switch (this) { + case Coin.bitcoin: + case Coin.litecoin: + case Coin.bitcoincash: + case Coin.dogecoin: + case Coin.firo: + case Coin.namecoin: + case Coin.particl: + case Coin.epicCash: + case Coin.monero: + case Coin.wownero: + return false; + + case Coin.dogecoinTestNet: + case Coin.bitcoinTestNet: + case Coin.litecoinTestNet: + case Coin.bitcoincashTestnet: + case Coin.firoTestNet: + return true; + } + } + + Coin get mainNetVersion { + switch (this) { + case Coin.bitcoin: + case Coin.litecoin: + case Coin.bitcoincash: + case Coin.dogecoin: + case Coin.firo: + case Coin.namecoin: + case Coin.particl: + case Coin.epicCash: + case Coin.monero: + case Coin.wownero: + return this; + + case Coin.dogecoinTestNet: + return Coin.dogecoin; + + case Coin.bitcoinTestNet: + return Coin.bitcoin; + + case Coin.litecoinTestNet: + return Coin.litecoin; + + case Coin.bitcoincashTestnet: + return Coin.bitcoincash; + + case Coin.firoTestNet: + return Coin.firo; + } + } + int get requiredConfirmations { switch (this) { case Coin.bitcoin: @@ -246,6 +301,8 @@ extension CoinExt on Coin { return nmc.MINIMUM_CONFIRMATIONS; } } + + int get decimals => Constants.decimalPlacesForCoin(this); } Coin coinFromPrettyName(String name) { diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index f6e247312..fc511ff10 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -40,6 +40,7 @@ class Prefs extends ChangeNotifier { _familiarity = await _getHasFamiliarity(); _userId = await _getUserId(); _signupEpoch = await _getSignupEpoch(); + _enableCoinControl = await _getEnableCoinControl(); _initialized = true; } @@ -645,4 +646,27 @@ class Prefs extends ChangeNotifier { boxName: DB.boxNamePrefs, key: "signupEpoch", value: _signupEpoch); // notifyListeners(); } + + // show testnet coins + + bool _enableCoinControl = false; + + bool get enableCoinControl => _enableCoinControl; + + set enableCoinControl(bool enableCoinControl) { + if (_enableCoinControl != enableCoinControl) { + DB.instance.put( + boxName: DB.boxNamePrefs, + key: "enableCoinControl", + value: enableCoinControl); + _enableCoinControl = enableCoinControl; + notifyListeners(); + } + } + + Future _getEnableCoinControl() async { + return await DB.instance.get( + boxName: DB.boxNamePrefs, key: "enableCoinControl") as bool? ?? + false; + } } diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index 6ff8cdaf1..27f49e060 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -9,78 +9,24 @@ class STextStyles { static TextStyle sectionLabelMedium12(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 14, - ); case ThemeType.forest: return GoogleFonts.inter( color: _theme(context).textDark3, fontWeight: FontWeight.w500, fontSize: 12, ); + default: + return GoogleFonts.inter( + color: _theme(context).textDark3, + fontWeight: FontWeight.w500, + fontSize: 14, + ); } } static TextStyle pageTitleH1(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 20, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 20, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 20, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 20, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 20, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w600, @@ -91,37 +37,7 @@ class STextStyles { static TextStyle pageTitleH2(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 18, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 18, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 18, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 18, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 18, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w600, @@ -132,37 +48,7 @@ class STextStyles { static TextStyle navBarTitle(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w600, @@ -173,119 +59,18 @@ class STextStyles { static TextStyle titleBold12(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w600, fontSize: 16, ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.forest: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - } - } - - static TextStyle titleBold12_400(BuildContext context) { - switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 16, - ); - case ThemeType.forest: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 16, - ); } } static TextStyle subtitle(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 16, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w400, @@ -296,37 +81,7 @@ class STextStyles { static TextStyle subtitle500(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w500, @@ -337,37 +92,7 @@ class STextStyles { static TextStyle subtitle600(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w600, @@ -378,37 +103,7 @@ class STextStyles { static TextStyle button(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimary, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimary, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimary, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimary, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimary, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).buttonTextPrimary, fontWeight: FontWeight.w500, @@ -419,37 +114,7 @@ class STextStyles { static TextStyle largeMedium14(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w500, @@ -460,37 +125,7 @@ class STextStyles { static TextStyle smallMed14(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 16, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark3, fontWeight: FontWeight.w500, @@ -501,37 +136,7 @@ class STextStyles { static TextStyle smallMed12(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark3, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark3, fontWeight: FontWeight.w500, @@ -542,37 +147,7 @@ class STextStyles { static TextStyle label(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textSubtitle1, fontWeight: FontWeight.w500, @@ -583,42 +158,7 @@ class STextStyles { static TextStyle labelExtraExtraSmall(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textFieldActiveSearchIconRight, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 14 / 14, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textFieldActiveSearchIconRight, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 14 / 14, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textFieldActiveSearchIconRight, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 14 / 14, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textFieldActiveSearchIconRight, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 14 / 14, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textFieldActiveSearchIconRight, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 14 / 14, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textFieldActiveSearchIconRight, fontWeight: FontWeight.w500, @@ -630,37 +170,7 @@ class STextStyles { static TextStyle label700(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w700, - fontSize: 12, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w700, - fontSize: 12, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w700, - fontSize: 12, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w700, - fontSize: 12, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w700, - fontSize: 12, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textSubtitle1, fontWeight: FontWeight.w700, @@ -671,37 +181,7 @@ class STextStyles { static TextStyle itemSubtitle(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).infoItemLabel, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).infoItemLabel, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).infoItemLabel, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).infoItemLabel, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).infoItemLabel, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).infoItemLabel, fontWeight: FontWeight.w500, @@ -712,78 +192,7 @@ class STextStyles { static TextStyle itemSubtitle12(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.forest: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - } - } - - static TextStyle itemSubtitle12_600(BuildContext context) { - switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w500, @@ -794,42 +203,7 @@ class STextStyles { static TextStyle fieldLabel(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textSubtitle2, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 1.5, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textSubtitle2, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 1.5, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textSubtitle2, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 1.5, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textSubtitle2, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 1.5, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textSubtitle2, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 1.5, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textSubtitle2, fontWeight: FontWeight.w500, @@ -841,42 +215,7 @@ class STextStyles { static TextStyle field(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 1.5, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 1.5, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 1.5, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 1.5, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 1.5, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w500, @@ -888,37 +227,7 @@ class STextStyles { static TextStyle baseXS(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 14, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 14, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 14, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 14, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 14, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w400, @@ -929,37 +238,7 @@ class STextStyles { static TextStyle link(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).accentColorRed, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).accentColorRed, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).accentColorRed, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).accentColorRed, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).accentColorRed, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).accentColorRed, fontWeight: FontWeight.w500, @@ -970,37 +249,13 @@ class STextStyles { static TextStyle link2(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).infoItemIcons, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).infoItemIcons, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).infoItemIcons, - fontWeight: FontWeight.w500, - fontSize: 14, - ); case ThemeType.oledBlack: return GoogleFonts.inter( color: _theme(context).checkboxBGChecked, fontWeight: FontWeight.w500, fontSize: 14, ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).infoItemIcons, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).infoItemIcons, fontWeight: FontWeight.w500, @@ -1011,37 +266,7 @@ class STextStyles { static TextStyle richLink(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).accentColorBlue, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).accentColorBlue, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).accentColorBlue, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).accentColorBlue, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).accentColorBlue, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).accentColorBlue, fontWeight: FontWeight.w500, @@ -1052,37 +277,7 @@ class STextStyles { static TextStyle w600_12(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 12, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 12, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 12, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 12, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 12, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w600, @@ -1093,37 +288,7 @@ class STextStyles { static TextStyle w600_14(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 14, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 14, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 14, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 14, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 14, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w600, @@ -1134,37 +299,7 @@ class STextStyles { static TextStyle w500_14(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w500, @@ -1175,37 +310,7 @@ class STextStyles { static TextStyle w500_12(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w500, @@ -1216,37 +321,7 @@ class STextStyles { static TextStyle w500_10(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w500, @@ -1255,39 +330,21 @@ class STextStyles { } } + static TextStyle w600_20(BuildContext context) { + switch (_theme(context).themeType) { + default: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 20, + height: 30 / 20, + ); + } + } + static TextStyle syncPercent(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w500, @@ -1298,37 +355,13 @@ class STextStyles { static TextStyle buttonSmall(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); case ThemeType.fruitSorbet: return GoogleFonts.inter( color: _theme(context).bottomNavIconIcon, fontWeight: FontWeight.w500, fontSize: 12, ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w500, @@ -1339,37 +372,7 @@ class STextStyles { static TextStyle errorSmall(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textError, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textError, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textError, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textError, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textError, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textError, fontWeight: FontWeight.w500, @@ -1380,37 +383,7 @@ class STextStyles { static TextStyle infoSmall(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 10, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textSubtitle1, fontWeight: FontWeight.w500, @@ -1423,42 +396,7 @@ class STextStyles { static TextStyle desktopH1(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 40, - height: 40 / 40, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 40, - height: 40 / 40, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 40, - height: 40 / 40, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 40, - height: 40 / 40, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 40, - height: 40 / 40, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w600, @@ -1470,42 +408,7 @@ class STextStyles { static TextStyle desktopH2(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 32, - height: 32 / 32, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 32, - height: 32 / 32, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 32, - height: 32 / 32, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 32, - height: 32 / 32, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 32, - height: 32 / 32, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w600, @@ -1517,42 +420,7 @@ class STextStyles { static TextStyle desktopH3(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 24, - height: 24 / 24, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 24, - height: 24 / 24, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 24, - height: 24 / 24, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 24, - height: 24 / 24, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 24, - height: 24 / 24, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w600, @@ -1564,42 +432,7 @@ class STextStyles { static TextStyle w500_24(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 24, - height: 24 / 24, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 24, - height: 24 / 24, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 24, - height: 24 / 24, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 24, - height: 24 / 24, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 24, - height: 24 / 24, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w500, @@ -1611,42 +444,7 @@ class STextStyles { static TextStyle desktopTextMedium(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w500, @@ -1658,42 +456,7 @@ class STextStyles { static TextStyle desktopTextMediumRegular(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w400, @@ -1705,42 +468,7 @@ class STextStyles { static TextStyle desktopSubtitleH2(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 20, - height: 28 / 20, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 20, - height: 28 / 20, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 20, - height: 28 / 20, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 20, - height: 28 / 20, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 20, - height: 28 / 20, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w400, @@ -1752,42 +480,7 @@ class STextStyles { static TextStyle desktopSubtitleH1(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 24, - height: 33 / 24, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 24, - height: 33 / 24, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 24, - height: 33 / 24, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 24, - height: 33 / 24, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w400, - fontSize: 24, - height: 33 / 24, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w400, @@ -1799,42 +492,7 @@ class STextStyles { static TextStyle desktopButtonEnabled(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimary, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimary, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimary, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimary, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimary, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).buttonTextPrimary, fontWeight: FontWeight.w500, @@ -1846,42 +504,7 @@ class STextStyles { static TextStyle desktopButtonDisabled(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, fontWeight: FontWeight.w500, @@ -1893,42 +516,7 @@ class STextStyles { static TextStyle desktopButtonSecondaryEnabled(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondary, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondary, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondary, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondary, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondary, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).buttonTextSecondary, fontWeight: FontWeight.w500, @@ -1940,42 +528,7 @@ class STextStyles { static TextStyle desktopButtonSecondaryDisabled(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).buttonTextSecondaryDisabled, fontWeight: FontWeight.w500, @@ -1987,20 +540,6 @@ class STextStyles { static TextStyle desktopTextSmall(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 18, - height: 27 / 18, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 18, - height: 27 / 18, - ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -2008,21 +547,8 @@ class STextStyles { fontSize: 18, height: 27 / 18, ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 18, - height: 27 / 18, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 18, - height: 27 / 18, - ); - case ThemeType.forest: + + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w500, @@ -2034,27 +560,7 @@ class STextStyles { static TextStyle desktopTextSmallBold(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w700, - fontSize: 18, - height: 27 / 18, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w700, - fontSize: 18, - height: 27 / 18, - ); case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimaryDisabled, - fontWeight: FontWeight.w700, - fontSize: 18, - height: 27 / 18, - ); case ThemeType.oledBlack: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -2062,14 +568,8 @@ class STextStyles { fontSize: 18, height: 27 / 18, ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w700, - fontSize: 18, - height: 27 / 18, - ); - case ThemeType.forest: + + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w700, @@ -2081,42 +581,7 @@ class STextStyles { static TextStyle desktopTextExtraSmall(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 24 / 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 24 / 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 24 / 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 24 / 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).buttonTextPrimaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 24 / 16, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, fontWeight: FontWeight.w500, @@ -2128,42 +593,7 @@ class STextStyles { static TextStyle desktopTextExtraExtraSmall(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 21 / 14, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 21 / 14, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 21 / 14, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 21 / 14, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 21 / 14, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textSubtitle1, fontWeight: FontWeight.w500, @@ -2175,42 +605,7 @@ class STextStyles { static TextStyle desktopTextExtraExtraSmall600(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 14, - height: 21 / 14, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 14, - height: 21 / 14, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 14, - height: 21 / 14, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 14, - height: 21 / 14, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 14, - height: 21 / 14, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w600, @@ -2222,42 +617,7 @@ class STextStyles { static TextStyle desktopButtonSmallSecondaryEnabled(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondary, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 24 / 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondary, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 24 / 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondary, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 24 / 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondary, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 24 / 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).buttonTextSecondary, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 24 / 16, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).buttonTextSecondary, fontWeight: FontWeight.w500, @@ -2269,42 +629,7 @@ class STextStyles { static TextStyle desktopTextFieldLabel(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textSubtitle2, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textSubtitle2, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textSubtitle2, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textSubtitle2, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textSubtitle2, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 30 / 20, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textSubtitle2, fontWeight: FontWeight.w500, @@ -2316,42 +641,7 @@ class STextStyles { static TextStyle desktopMenuItem(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark.withOpacity(0.8), - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark.withOpacity(0.8), - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark.withOpacity(0.8), - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark.withOpacity(0.8), - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark.withOpacity(0.8), - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark.withOpacity(0.8), fontWeight: FontWeight.w500, @@ -2363,42 +653,7 @@ class STextStyles { static TextStyle desktopMenuItemSelected(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w500, @@ -2410,42 +665,7 @@ class STextStyles { static TextStyle settingsMenuItem(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark.withOpacity(0.5), - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark.withOpacity(0.5), - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark.withOpacity(0.5), - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark.withOpacity(0.5), - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark.withOpacity(0.5), - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark.withOpacity(0.5), fontWeight: FontWeight.w500, @@ -2457,42 +677,7 @@ class STextStyles { static TextStyle settingsMenuItemSelected(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - color: _theme(context).textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( color: _theme(context).textDark, fontWeight: FontWeight.w500, @@ -2504,37 +689,7 @@ class STextStyles { static TextStyle stepIndicator(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.roboto( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 8, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.roboto( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 8, - ); - case ThemeType.dark: - return GoogleFonts.roboto( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 8, - ); - case ThemeType.oledBlack: - return GoogleFonts.roboto( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 8, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.roboto( - color: _theme(context).textDark, - fontWeight: FontWeight.w600, - fontSize: 8, - ); - case ThemeType.forest: + default: return GoogleFonts.roboto( color: _theme(context).textDark, fontWeight: FontWeight.w600, @@ -2545,37 +700,7 @@ class STextStyles { static TextStyle numberDefault(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.roboto( - color: _theme(context).numberTextDefault, - fontWeight: FontWeight.w400, - fontSize: 26, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.roboto( - color: _theme(context).numberTextDefault, - fontWeight: FontWeight.w400, - fontSize: 26, - ); - case ThemeType.dark: - return GoogleFonts.roboto( - color: _theme(context).numberTextDefault, - fontWeight: FontWeight.w400, - fontSize: 26, - ); - case ThemeType.oledBlack: - return GoogleFonts.roboto( - color: _theme(context).numberTextDefault, - fontWeight: FontWeight.w400, - fontSize: 26, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.roboto( - color: _theme(context).numberTextDefault, - fontWeight: FontWeight.w400, - fontSize: 26, - ); - case ThemeType.forest: + default: return GoogleFonts.roboto( color: _theme(context).numberTextDefault, fontWeight: FontWeight.w400, @@ -2586,42 +711,7 @@ class STextStyles { static TextStyle datePicker400(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - letterSpacing: 0.5, - color: _theme(context).accentColorDark, - fontWeight: FontWeight.w400, - fontSize: 12, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - letterSpacing: 0.5, - color: _theme(context).accentColorDark, - fontWeight: FontWeight.w400, - fontSize: 12, - ); - case ThemeType.dark: - return GoogleFonts.inter( - letterSpacing: 0.5, - color: _theme(context).accentColorDark, - fontWeight: FontWeight.w400, - fontSize: 12, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - letterSpacing: 0.5, - color: _theme(context).accentColorDark, - fontWeight: FontWeight.w400, - fontSize: 12, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - letterSpacing: 0.5, - color: _theme(context).accentColorDark, - fontWeight: FontWeight.w400, - fontSize: 12, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( letterSpacing: 0.5, color: _theme(context).accentColorDark, @@ -2633,42 +723,7 @@ class STextStyles { static TextStyle datePicker600(BuildContext context) { switch (_theme(context).themeType) { - case ThemeType.light: - return GoogleFonts.inter( - letterSpacing: 0.5, - color: _theme(context).accentColorDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.oceanBreeze: - return GoogleFonts.inter( - letterSpacing: 0.5, - color: _theme(context).accentColorDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.dark: - return GoogleFonts.inter( - letterSpacing: 0.5, - color: _theme(context).accentColorDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.oledBlack: - return GoogleFonts.inter( - letterSpacing: 0.5, - color: _theme(context).accentColorDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.fruitSorbet: - return GoogleFonts.inter( - letterSpacing: 0.5, - color: _theme(context).accentColorDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); - case ThemeType.forest: + default: return GoogleFonts.inter( letterSpacing: 0.5, color: _theme(context).accentColorDark, diff --git a/lib/utilities/theme/chan_colors.dart b/lib/utilities/theme/chan_colors.dart new file mode 100644 index 000000000..f9ea1bb54 --- /dev/null +++ b/lib/utilities/theme/chan_colors.dart @@ -0,0 +1,351 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/theme/color_theme.dart'; + +class ChanColors extends StackColorTheme { + @override + ThemeType get themeType => ThemeType.chan; + @override + Brightness get brightness => Brightness.light; + + @override + Color get background => const Color(0xFFF7F7F7); + @override + Color get backgroundAppBar => background; + @override + Gradient? get gradientBackground => null; + + @override + Color get overlay => const Color(0xFF111215); + + @override + Color get accentColorBlue => const Color(0xFF0052DF); + @override + Color get accentColorGreen => const Color(0xFF4CC0A0); + @override + Color get accentColorYellow => const Color(0xFFF7D65D); + @override + Color get accentColorRed => const Color(0xFFD34E50); + @override + Color get accentColorOrange => const Color(0xFFFEA68D); + @override + Color get accentColorDark => const Color(0xFF232323); + + @override + Color get shadow => const Color(0x0F2D3132); + + @override + Color get textDark => const Color(0xFF232323); + @override + Color get textDark2 => const Color(0xFF414141); + @override + Color get textDark3 => const Color(0xFF747778); + @override + Color get textSubtitle1 => const Color(0xFF8E9192); + @override + Color get textSubtitle2 => const Color(0xFFA9ACAC); + @override + Color get textSubtitle3 => const Color(0xFFC4C7C7); + @override + Color get textSubtitle4 => const Color(0xFFE0E3E3); + @override + Color get textSubtitle5 => const Color(0xFFEEEFF1); + @override + Color get textSubtitle6 => const Color(0xFFF5F5F5); + @override + Color get textWhite => const Color(0xFFFFFFFF); + @override + Color get textFavoriteCard => const Color(0xFF232323); + @override + Color get textError => const Color(0xFF930006); + @override + Color get textRestore => overlay; + + // button background + @override + Color get buttonBackPrimary => const Color(0xFF232323); + @override + Color get buttonBackSecondary => const Color(0xFFE0E3E3); + @override + Color get buttonBackPrimaryDisabled => const Color(0xFFD7D7D7); + @override + Color get buttonBackSecondaryDisabled => const Color(0xFFF0F1F1); + @override + Color get buttonBackBorder => const Color(0xFF232323); + @override + Color get buttonBackBorderDisabled => const Color(0xFFB6B6B6); + @override + Color get buttonBackBorderSecondary => buttonBackSecondary; + @override + Color get buttonBackBorderSecondaryDisabled => buttonBackSecondaryDisabled; + + @override + Color get numberBackDefault => const Color(0xFFFFFFFF); + @override + Color get numpadBackDefault => const Color(0xFF232323); + @override + Color get bottomNavBack => const Color(0xFFFFFFFF); + + // button text/element + @override + Color get buttonTextPrimary => const Color(0xFFFFFFFF); + @override + Color get buttonTextSecondary => const Color(0xFF232323); + @override + Color get buttonTextPrimaryDisabled => const Color(0xFFF8F8F8); + @override + Color get buttonTextSecondaryDisabled => const Color(0xFFB7B7B7); + @override + Color get buttonTextBorder => const Color(0xFF232323); + @override + Color get buttonTextDisabled => const Color(0xFFB6B6B6); + @override + Color get buttonTextBorderless => const Color(0xFF0052DF); + @override + Color get buttonTextBorderlessDisabled => const Color(0xFFB6B6B6); + @override + Color get numberTextDefault => const Color(0xFF232323); + @override + Color get numpadTextDefault => const Color(0xFFFFFFFF); + @override + Color get bottomNavText => const Color(0xFF232323); + @override + Color get customTextButtonEnabledText => buttonTextBorderless; + @override + Color get customTextButtonDisabledText => textSubtitle1; + + // switch + @override + Color get switchBGOn => const Color(0xFF0052DF); + @override + Color get switchBGOff => const Color(0xFFD8E4FB); + @override + Color get switchBGDisabled => const Color(0xFFC5C6C9); + @override + Color get switchCircleOn => const Color(0xFFDAE2FF); + @override + Color get switchCircleOff => const Color(0xFFFBFCFF); + @override + Color get switchCircleDisabled => const Color(0xFFFBFCFF); + + // step indicator background + @override + Color get stepIndicatorBGCheck => const Color(0xFFD9E2FF); + @override + Color get stepIndicatorBGNumber => const Color(0xFFD9E2FF); + @override + Color get stepIndicatorBGInactive => const Color(0xFFCDCDCD); + @override + Color get stepIndicatorBGLines => const Color(0xFF0056D2); + @override + Color get stepIndicatorBGLinesInactive => const Color(0xFFCDCDCD); + @override + Color get stepIndicatorIconText => const Color(0xFF0056D2); + @override + Color get stepIndicatorIconNumber => const Color(0xFF0056D2); + @override + Color get stepIndicatorIconInactive => const Color(0xFFF7F7F7); + + // checkbox + @override + Color get checkboxBGChecked => const Color(0xFF0056D2); + @override + Color get checkboxBorderEmpty => const Color(0xFF8E9192); + @override + Color get checkboxBGDisabled => const Color(0xFFADC7EC); + @override + Color get checkboxIconChecked => const Color(0xFFFFFFFF); + @override + Color get checkboxIconDisabled => const Color(0xFFFFFFFF); + @override + Color get checkboxTextLabel => const Color(0xFF232323); + + // snack bar + @override + Color get snackBarBackSuccess => const Color(0xFFB9E9D4); + @override + Color get snackBarBackError => const Color(0xFFFFDAD4); + @override + Color get snackBarBackInfo => const Color(0xFFDAE2FF); + @override + Color get snackBarTextSuccess => const Color(0xFF006C4D); + @override + Color get snackBarTextError => const Color(0xFF930006); + @override + Color get snackBarTextInfo => const Color(0xFF002A78); + + // icons + @override + Color get bottomNavIconBack => const Color(0xFFA2A2A2); + @override + Color get bottomNavIconIcon => const Color(0xFF232323); + + @override + Color get topNavIconPrimary => const Color(0xFF232323); + @override + Color get topNavIconGreen => const Color(0xFF00A578); + @override + Color get topNavIconYellow => const Color(0xFFF4C517); + @override + Color get topNavIconRed => const Color(0xFFC00205); + + @override + Color get settingsIconBack => const Color(0xFFE0E3E3); + @override + Color get settingsIconIcon => const Color(0xFF232323); + @override + Color get settingsIconBack2 => const Color(0xFF94D6C4); + @override + Color get settingsIconElement => const Color(0xFF00A578); + + // text field + @override + Color get textFieldActiveBG => const Color(0xFFEEEFF1); + @override + Color get textFieldDefaultBG => const Color(0xFFEEEFF1); + @override + Color get textFieldErrorBG => const Color(0xFFFFDAD4); + @override + Color get textFieldSuccessBG => const Color(0xFFB9E9D4); + @override + Color get textFieldErrorBorder => textFieldErrorBG; + @override + Color get textFieldSuccessBorder => textFieldSuccessBG; + + @override + Color get textFieldActiveSearchIconLeft => const Color(0xFFA9ACAC); + @override + Color get textFieldDefaultSearchIconLeft => const Color(0xFFA9ACAC); + @override + Color get textFieldErrorSearchIconLeft => const Color(0xFF930006); + @override + Color get textFieldSuccessSearchIconLeft => const Color(0xFF006C4D); + + @override + Color get textFieldActiveText => const Color(0xFF232323); + @override + Color get textFieldDefaultText => const Color(0xFFA9ACAC); + @override + Color get textFieldErrorText => const Color(0xFF000000); + @override + Color get textFieldSuccessText => const Color(0xFF000000); + + @override + Color get textFieldActiveLabel => const Color(0xFFA9ACAC); + @override + Color get textFieldErrorLabel => const Color(0xFF930006); + @override + Color get textFieldSuccessLabel => const Color(0xFF006C4D); + + @override + Color get textFieldActiveSearchIconRight => const Color(0xFF747778); + @override + Color get textFieldDefaultSearchIconRight => const Color(0xFF747778); + @override + Color get textFieldErrorSearchIconRight => const Color(0xFF930006); + @override + Color get textFieldSuccessSearchIconRight => const Color(0xFF006C4D); + + // settings item level2 + @override + Color get settingsItem2ActiveBG => const Color(0xFFFFFFFF); + @override + Color get settingsItem2ActiveText => const Color(0xFF232323); + @override + Color get settingsItem2ActiveSub => const Color(0xFF8E9192); + + // radio buttons + @override + Color get radioButtonIconBorder => const Color(0xFF0056D2); + @override + Color get radioButtonIconBorderDisabled => const Color(0xFF8F909A); + @override + Color get radioButtonBorderEnabled => const Color(0xFF0056D2); + @override + Color get radioButtonBorderDisabled => const Color(0xFF8F909A); + @override + Color get radioButtonIconCircle => const Color(0xFF0056D2); + @override + Color get radioButtonIconEnabled => const Color(0xFF0056D2); + @override + Color get radioButtonTextEnabled => const Color(0xFF44464E); + @override + Color get radioButtonTextDisabled => const Color(0xFF44464E); + @override + Color get radioButtonLabelEnabled => const Color(0xFF8E9192); + @override + Color get radioButtonLabelDisabled => const Color(0xFF8E9192); + + // info text + @override + Color get infoItemBG => const Color(0xFFFFFFFF); + @override + Color get infoItemLabel => const Color(0xFF8E9192); + @override + Color get infoItemText => const Color(0xFF232323); + @override + Color get infoItemIcons => const Color(0xFF0056D2); + + // popup + @override + Color get popupBG => const Color(0xFFFFFFFF); + + // currency list + @override + Color get currencyListItemBG => const Color(0xFFF9F9FC); + + // bottom nav + @override + Color get stackWalletBG => const Color(0xFFFFFFFF); + @override + Color get stackWalletMid => const Color(0xFFFFFFFF); + @override + Color get stackWalletBottom => const Color(0xFF232323); + @override + Color get bottomNavShadow => const Color(0xFF282E33); + + @override + Color get favoriteStarActive => infoItemIcons; + @override + Color get favoriteStarInactive => textSubtitle3; + + @override + Color get splash => const Color(0x358E9192); + @override + Color get highlight => const Color(0x44A9ACAC); + @override + Color get warningForeground => textDark; + @override + Color get warningBackground => const Color(0xFFFFDAD3); + @override + Color get loadingOverlayTextColor => const Color(0xFFF7F7F7); + @override + Color get myStackContactIconBG => textFieldDefaultBG; + @override + Color get textConfirmTotalAmount => const Color(0xFF232323); + @override + Color get textSelectedWordTableItem => const Color(0xFF232323); + + //rate type toggle + @override + Color get rateTypeToggleColorOn => textFieldDefaultBG; + @override + Color get rateTypeToggleColorOff => popupBG; + @override + Color get rateTypeToggleDesktopColorOn => textFieldDefaultBG; + @override + Color get rateTypeToggleDesktopColorOff => buttonBackSecondary; + + @override + BoxShadow get standardBoxShadow => BoxShadow( + color: shadow, + spreadRadius: 3, + blurRadius: 4, + ); + + @override + BoxShadow? get homeViewButtonBarBoxShadow => BoxShadow( + color: shadow, + spreadRadius: 3, + blurRadius: 4, + ); +} diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index 8bdea9ca9..99df536ac 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -7,7 +7,17 @@ import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/ocean_breeze_colors.dart'; import 'package:stackwallet/utilities/theme/oled_black_colors.dart'; -enum ThemeType { light, dark, oceanBreeze, oledBlack, fruitSorbet, forest } +import 'chan_colors.dart'; + +enum ThemeType { + light, + dark, + oceanBreeze, + oledBlack, + fruitSorbet, + forest, + chan; +} // adjust this file @@ -16,6 +26,8 @@ extension ThemeTypeExt on ThemeType { switch (this) { case ThemeType.light: return LightColors(); + case ThemeType.chan: + return ChanColors(); case ThemeType.dark: return DarkColors(); case ThemeType.oceanBreeze: @@ -33,6 +45,8 @@ extension ThemeTypeExt on ThemeType { switch (this) { case ThemeType.light: return "Light"; + case ThemeType.chan: + return "Crypto Chans"; case ThemeType.dark: return "Dark"; case ThemeType.oceanBreeze: @@ -49,6 +63,7 @@ extension ThemeTypeExt on ThemeType { abstract class StackColorTheme { ThemeType get themeType; + Brightness get brightness; Color get background; Color get backgroundAppBar; diff --git a/lib/utilities/theme/dark_colors.dart b/lib/utilities/theme/dark_colors.dart index e91c7d126..fbbf113ef 100644 --- a/lib/utilities/theme/dark_colors.dart +++ b/lib/utilities/theme/dark_colors.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/utilities/theme/color_theme.dart'; class DarkColors extends StackColorTheme { @override ThemeType get themeType => ThemeType.dark; + @override + Brightness get brightness => Brightness.dark; @override Color get background => const Color(0xFF2A2D34); diff --git a/lib/utilities/theme/forest_colors.dart b/lib/utilities/theme/forest_colors.dart index e4be1cfd2..176b74e50 100644 --- a/lib/utilities/theme/forest_colors.dart +++ b/lib/utilities/theme/forest_colors.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/utilities/theme/color_theme.dart'; class ForestColors extends StackColorTheme { @override ThemeType get themeType => ThemeType.forest; + @override + Brightness get brightness => Brightness.light; @override Color get background => const Color(0xFFF3FAF5); diff --git a/lib/utilities/theme/fruit_sorbet_colors.dart b/lib/utilities/theme/fruit_sorbet_colors.dart index ef021fad1..a13985a93 100644 --- a/lib/utilities/theme/fruit_sorbet_colors.dart +++ b/lib/utilities/theme/fruit_sorbet_colors.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/utilities/theme/color_theme.dart'; class FruitSorbetColors extends StackColorTheme { @override ThemeType get themeType => ThemeType.fruitSorbet; + @override + Brightness get brightness => Brightness.light; @override Color get background => Colors.transparent; @@ -197,7 +199,7 @@ class FruitSorbetColors extends StackColorTheme { // text field @override - Color get textFieldActiveBG => const Color(0xFFFFFBF6); + Color get textFieldActiveBG => const Color(0xFFF7EEE1); @override Color get textFieldDefaultBG => const Color(0xFFFFF8EE); @override diff --git a/lib/utilities/theme/light_colors.dart b/lib/utilities/theme/light_colors.dart index 8c9b4dcc5..b42f2dd9a 100644 --- a/lib/utilities/theme/light_colors.dart +++ b/lib/utilities/theme/light_colors.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/utilities/theme/color_theme.dart'; class LightColors extends StackColorTheme { @override ThemeType get themeType => ThemeType.light; + @override + Brightness get brightness => Brightness.light; @override Color get background => const Color(0xFFF7F7F7); diff --git a/lib/utilities/theme/ocean_breeze_colors.dart b/lib/utilities/theme/ocean_breeze_colors.dart index 51831f2e5..9390c55b5 100644 --- a/lib/utilities/theme/ocean_breeze_colors.dart +++ b/lib/utilities/theme/ocean_breeze_colors.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/utilities/theme/color_theme.dart'; class OceanBreezeColors extends StackColorTheme { @override ThemeType get themeType => ThemeType.oceanBreeze; + @override + Brightness get brightness => Brightness.light; @override Color get background => Colors.transparent; diff --git a/lib/utilities/theme/oled_black_colors.dart b/lib/utilities/theme/oled_black_colors.dart index 025f76f7d..71d4b2361 100644 --- a/lib/utilities/theme/oled_black_colors.dart +++ b/lib/utilities/theme/oled_black_colors.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/utilities/theme/color_theme.dart'; class OledBlackColors extends StackColorTheme { @override ThemeType get themeType => ThemeType.oledBlack; + @override + Brightness get brightness => Brightness.dark; @override Color get background => const Color(0xFF000000); diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 483d6db62..cbba0bb36 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -4,6 +4,7 @@ import 'package:stackwallet/utilities/theme/color_theme.dart'; class StackColors extends ThemeExtension { final ThemeType themeType; + final Brightness brightness; final Color background; final Color backgroundAppBar; @@ -191,6 +192,7 @@ class StackColors extends ThemeExtension { StackColors({ required this.themeType, + required this.brightness, required this.background, required this.backgroundAppBar, required this.gradientBackground, @@ -340,6 +342,7 @@ class StackColors extends ThemeExtension { factory StackColors.fromStackColorTheme(StackColorTheme colorTheme) { return StackColors( themeType: colorTheme.themeType, + brightness: colorTheme.brightness, background: colorTheme.background, backgroundAppBar: colorTheme.backgroundAppBar, gradientBackground: colorTheme.gradientBackground, @@ -493,6 +496,7 @@ class StackColors extends ThemeExtension { @override ThemeExtension copyWith({ ThemeType? themeType, + Brightness? brightness, Color? background, Color? backgroundAppBar, Gradient? gradientBackground, @@ -640,6 +644,7 @@ class StackColors extends ThemeExtension { }) { return StackColors( themeType: themeType ?? this.themeType, + brightness: brightness ?? this.brightness, background: background ?? this.background, backgroundAppBar: backgroundAppBar ?? this.backgroundAppBar, gradientBackground: gradientBackground ?? this.gradientBackground, @@ -845,6 +850,7 @@ class StackColors extends ThemeExtension { return StackColors( themeType: other.themeType, + brightness: other.brightness, gradientBackground: other.gradientBackground, homeViewButtonBarBoxShadow: other.homeViewButtonBarBoxShadow, standardBoxShadow: other.standardBoxShadow, @@ -1605,6 +1611,7 @@ class StackColors extends ThemeExtension { return const Color(0xFFD3A90F); case "Finished": case "finished": + case "Completed": return accentColorGreen; case "Failed": case "failed": diff --git a/lib/widgets/animated_widgets/rotate_icon.dart b/lib/widgets/animated_widgets/rotate_icon.dart index c818628d0..d93f6b36a 100644 --- a/lib/widgets/animated_widgets/rotate_icon.dart +++ b/lib/widgets/animated_widgets/rotate_icon.dart @@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart'; class RotateIconController { VoidCallback? forward; VoidCallback? reverse; + VoidCallback? reset; } class RotateIcon extends StatefulWidget { @@ -52,6 +53,7 @@ class _RotateIconState extends State widget.controller?.forward = animationController.forward; widget.controller?.reverse = animationController.reverse; + widget.controller?.reset = animationController.reset; super.initState(); } @@ -59,6 +61,8 @@ class _RotateIconState extends State @override void dispose() { animationController.dispose(); + widget.controller?.forward = null; + widget.controller?.reverse = null; super.dispose(); } diff --git a/lib/widgets/app_bar_field.dart b/lib/widgets/app_bar_field.dart new file mode 100644 index 000000000..d579dc3db --- /dev/null +++ b/lib/widgets/app_bar_field.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; + +class AppBarSearchField extends StatefulWidget { + const AppBarSearchField({ + Key? key, + required this.controller, + this.focusNode, + }) : super(key: key); + + final TextEditingController? controller; + final FocusNode? focusNode; + + @override + State createState() => _AppBarSearchFieldState(); +} + +class _AppBarSearchFieldState extends State { + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + width: 16, + ), + Expanded( + child: TextField( + autofocus: true, + focusNode: widget.focusNode, + controller: widget.controller, + style: STextStyles.field(context), + decoration: InputDecoration( + fillColor: Colors.transparent, + hintText: "Search...", + hintStyle: STextStyles.fieldLabel(context), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/background.dart b/lib/widgets/background.dart index a0ae868e3..ed74e36ff 100644 --- a/lib/widgets/background.dart +++ b/lib/widgets/background.dart @@ -20,14 +20,6 @@ class Background extends StatelessWidget { bool shouldPad = false; switch (Theme.of(context).extension()!.themeType) { - case ThemeType.light: - case ThemeType.dark: - case ThemeType.oledBlack: - color = Theme.of(context).extension()!.background; - break; - case ThemeType.forest: - color = Theme.of(context).extension()!.background; - break; case ThemeType.oceanBreeze: shouldPad = true; color = null; @@ -35,6 +27,9 @@ class Background extends StatelessWidget { case ThemeType.fruitSorbet: color = null; break; + default: + color = Theme.of(context).extension()!.background; + break; } final bgAsset = Assets.svg.background(context); diff --git a/lib/widgets/custom_buttons/dropdown_button.dart b/lib/widgets/custom_buttons/dropdown_button.dart new file mode 100644 index 000000000..d639ccf54 --- /dev/null +++ b/lib/widgets/custom_buttons/dropdown_button.dart @@ -0,0 +1,374 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class JDropdownButton extends StatefulWidget { + const JDropdownButton({ + Key? key, + this.label, + required this.items, + this.width, + this.onSelectionChanged, + this.groupValue, + this.redrawOnScreenSizeChanged = false, + this.showIcon = false, + }) : super(key: key); + + final String? label; + final double? width; + final void Function(T?)? onSelectionChanged; + final T? groupValue; + final Set items; + final bool showIcon; + + /// setting this to true should be done carefully + final bool redrawOnScreenSizeChanged; + + @override + State> createState() => _JDropdownButtonState(); +} + +class _JDropdownButtonState extends State> { + final _key = GlobalKey(); + final _rotateIconController = RotateIconController(); + + bool _isOpen = false; + + OverlayEntry? _entry; + + void close() { + if (_isOpen) { + _rotateIconController.reverse?.call(); + _entry?.remove(); + _isOpen = false; + } + } + + void open() { + final size = (_key.currentContext!.findRenderObject() as RenderBox).size; + _entry = OverlayEntry( + builder: (_) { + final position = (_key.currentContext!.findRenderObject() as RenderBox) + .localToGlobal(Offset.zero); + + if (widget.redrawOnScreenSizeChanged) { + // trigger rebuild + MediaQuery.of(context).size; + } + + return GestureDetector( + onTap: close, + child: _JDropdownButtonMenu( + size: size, + position: position, + items: widget.items + .map( + (e) => _JDropdownButtonItem( + value: e, + groupValue: widget.groupValue, + onSelected: (T value) { + widget.onSelectionChanged?.call(value); + close(); + }, + ), + ) + .toList(), + ), + ); + }, + ); + _rotateIconController.forward?.call(); + Overlay.of(context, rootOverlay: true).insert(_entry!); + _isOpen = true; + } + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (widget.redrawOnScreenSizeChanged && _isOpen) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _entry?.markNeedsBuild(); + }); + } + return SecondaryButton( + key: _key, + buttonHeight: ButtonHeight.l, + trailingIcon: widget.showIcon + ? RotateIcon( + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + curve: Curves.easeInOutCubic, + controller: _rotateIconController, + animationDurationMultiplier: 0.1, + ) + : null, + width: widget.width, + label: widget.label ?? widget.groupValue.toString(), + onPressed: _isOpen ? close : open, + ); + } +} + +class JDropdownIconButton extends StatefulWidget { + const JDropdownIconButton({ + Key? key, + required this.items, + required this.displayPrefix, + this.onSelectionChanged, + this.groupValue, + this.redrawOnScreenSizeChanged = false, + this.mobileAppBar = false, + }) : super(key: key); + + final String displayPrefix; + final void Function(T?)? onSelectionChanged; + final T? groupValue; + final Set items; + final bool mobileAppBar; + + /// setting this to true should be done carefully + final bool redrawOnScreenSizeChanged; + + @override + State> createState() => _JDropdownIconButtonState(); +} + +class _JDropdownIconButtonState extends State> { + final _key = GlobalKey(); + + bool _isOpen = false; + + OverlayEntry? _entry; + + void close() { + if (_isOpen) { + _entry?.remove(); + _isOpen = false; + } + } + + void open() { + final size = (_key.currentContext!.findRenderObject() as RenderBox).size; + _entry = OverlayEntry( + builder: (_) { + final position = (_key.currentContext!.findRenderObject() as RenderBox) + .localToGlobal(Offset.zero); + + if (widget.redrawOnScreenSizeChanged) { + // trigger rebuild + MediaQuery.of(context).size; + } + + return GestureDetector( + onTap: close, + child: _JDropdownButtonMenu( + size: Size(200, size.height), + position: Offset(position.dx - 144, position.dy), + items: widget.items + .map( + (e) => _JDropdownButtonItem( + value: e, + groupValue: widget.groupValue, + displayPrefix: widget.displayPrefix, + onSelected: (T value) { + widget.onSelectionChanged?.call(value); + close(); + }, + ), + ) + .toList(), + ), + ); + }, + ); + Overlay.of(context, rootOverlay: true).insert(_entry!); + _isOpen = true; + } + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (widget.redrawOnScreenSizeChanged && _isOpen) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _entry?.markNeedsBuild(); + }); + } + + if (widget.mobileAppBar) { + return AppBarIconButton( + key: _key, + size: 36, + icon: SvgPicture.asset( + Assets.svg.list, + width: 20, + height: 20, + color: Theme.of(context).extension()!.topNavIconPrimary, + ), + onPressed: _isOpen ? close : open, + ); + } else { + return SizedBox( + key: _key, + height: 56, + width: 56, + child: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context) + ?.copyWith( + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context) + .extension()! + .buttonBackBorderSecondary, + width: 1, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ), + onPressed: _isOpen ? close : open, + child: SvgPicture.asset( + Assets.svg.list, + width: 20, + height: 20, + ), + ), + ); + } + } +} + +// ============================================================================= + +class _JDropdownButtonMenu extends StatefulWidget { + const _JDropdownButtonMenu( + {Key? key, + required this.items, + required this.size, + required this.position}) + : super(key: key); + + final List<_JDropdownButtonItem> items; + final Size size; + final Offset position; + + @override + State<_JDropdownButtonMenu> createState() => _JDropdownButtonMenuState(); +} + +class _JDropdownButtonMenuState extends State<_JDropdownButtonMenu> { + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Stack( + children: [ + Container( + color: Colors.black.withOpacity(0.2), + // child: widget.content, + ), + Positioned( + top: widget.size.height + widget.position.dy + 10, + left: widget.position.dx, + width: widget.size.width, + child: RoundedWhiteContainer( + padding: EdgeInsets.zero, + radiusMultiplier: 2.5, + boxShadow: [ + Theme.of(context).extension()!.standardBoxShadow, + ], + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 20, + ), + ...widget.items, + const SizedBox( + height: 20, + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +// ============================================================================= + +class _JDropdownButtonItem extends StatelessWidget { + const _JDropdownButtonItem({ + Key? key, + required this.value, + required this.groupValue, + required this.onSelected, + this.height = 53, + this.displayPrefix, + }) : super(key: key); + + final T value; + final T? groupValue; + final double height; + final void Function(T) onSelected; + final String? displayPrefix; + + @override + Widget build(BuildContext context) { + return RawMaterialButton( + fillColor: groupValue == value + ? Theme.of(context).extension()!.textFieldDefaultBG + : Colors.transparent, + elevation: 0, + focusElevation: 0, + hoverElevation: 0, + highlightElevation: 0, + disabledElevation: 0, + padding: EdgeInsets.zero, + onPressed: () => onSelected(value), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + displayPrefix == null + ? value.toString() + : "$displayPrefix ${value.toString().toLowerCase()}", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/custom_buttons/simple_copy_button.dart b/lib/widgets/custom_buttons/simple_copy_button.dart new file mode 100644 index 000000000..1db61db45 --- /dev/null +++ b/lib/widgets/custom_buttons/simple_copy_button.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class SimpleCopyButton extends StatelessWidget { + const SimpleCopyButton({ + Key? key, + required this.data, + }) : super(key: key); + + final String data; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () async { + await Clipboard.setData(ClipboardData(text: data)); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 10, + height: 10, + color: Theme.of(context).extension()!.infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/custom_buttons/simple_edit_button.dart b/lib/widgets/custom_buttons/simple_edit_button.dart new file mode 100644 index 000000000..931de7aa4 --- /dev/null +++ b/lib/widgets/custom_buttons/simple_edit_button.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/pages/generic/single_field_edit_view.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:tuple/tuple.dart'; + +import '../desktop/desktop_dialog.dart'; +import '../icon_widgets/pencil_icon.dart'; + +class SimpleEditButton extends StatelessWidget { + const SimpleEditButton({ + Key? key, + required this.editValue, + required this.editLabel, + required this.onValueChanged, + }) : super(key: key); + + final String editValue; + final String editLabel; + final void Function(String) onValueChanged; + + @override + Widget build(BuildContext context) { + if (Util.isDesktop) { + return SizedBox( + height: 26, + width: 26, + child: RawMaterialButton( + fillColor: + Theme.of(context).extension()!.buttonBackSecondary, + elevation: 0, + hoverElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + onPressed: () async { + final result = await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 360, + child: SingleFieldEditView( + initialValue: editValue, + label: editLabel, + ), + ); + }, + ); + if (result is String && result != editValue) { + onValueChanged(result); + } + }, + child: Padding( + padding: const EdgeInsets.all(5), + child: PencilIcon( + width: 16, + height: 16, + color: Theme.of(context).extension()!.textDark, + ), + ), + ), + ); + } else { + return GestureDetector( + onTap: () async { + final result = await Navigator.of(context).pushNamed( + SingleFieldEditView.routeName, + arguments: Tuple2( + editValue, + editLabel, + ), + ); + if (result is String && result != editValue) { + onValueChanged(result); + } + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: Theme.of(context).extension()!.infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Edit", + style: STextStyles.link2(context), + ), + ], + ), + ); + } + } +} diff --git a/lib/widgets/desktop/secondary_button.dart b/lib/widgets/desktop/secondary_button.dart index 6b4e46221..5d1ba1970 100644 --- a/lib/widgets/desktop/secondary_button.dart +++ b/lib/widgets/desktop/secondary_button.dart @@ -13,6 +13,7 @@ class SecondaryButton extends StatelessWidget { this.height, this.label, this.icon, + this.trailingIcon, this.onPressed, this.enabled = true, this.buttonHeight, @@ -25,6 +26,7 @@ class SecondaryButton extends StatelessWidget { final VoidCallback? onPressed; final bool enabled; final Widget? icon; + final Widget? trailingIcon; final ButtonHeight? buttonHeight; final double iconSpacing; @@ -155,6 +157,7 @@ class SecondaryButton extends StatelessWidget { .getSecondaryDisabledButtonStyle(context), child: Row( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ if (icon != null) icon!, if (icon != null && label != null) @@ -177,6 +180,11 @@ class SecondaryButton extends StatelessWidget { ), ], ), + if (trailingIcon != null) + SizedBox( + width: iconSpacing, + ), + if (trailingIcon != null) trailingIcon!, ], ), ), diff --git a/lib/widgets/expandable2.dart b/lib/widgets/expandable2.dart new file mode 100644 index 000000000..4439d83e6 --- /dev/null +++ b/lib/widgets/expandable2.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +enum Expandable2State { + collapsed, + expanded, +} + +class Expandable2Controller { + VoidCallback? toggle; + Expandable2State state = Expandable2State.collapsed; +} + +class Expandable2 extends StatefulWidget { + const Expandable2({ + Key? key, + required this.header, + required this.children, + this.background = Colors.white, + this.border = Colors.black, + this.animationController, + this.animation, + this.animationDurationMultiplier = 1.0, + this.onExpandWillChange, + this.onExpandChanged, + this.controller, + this.expandOverride, + }) : super(key: key); + + final Widget header; + final List children; + final Color background; + final Color border; + final AnimationController? animationController; + final Animation? animation; + final double animationDurationMultiplier; + final void Function(Expandable2State)? onExpandWillChange; + final void Function(Expandable2State)? onExpandChanged; + final Expandable2Controller? controller; + final VoidCallback? expandOverride; + + @override + State createState() => _Expandable2State(); +} + +class _Expandable2State extends State + with TickerProviderStateMixin { + final _key = GlobalKey(); + + late final AnimationController animationController; + late final Animation animation; + late final Duration duration; + late final Expandable2Controller? controller; + + Expandable2State _toggleState = Expandable2State.collapsed; + + void toggle() { + if (animation.isDismissed) { + _toggleState = Expandable2State.expanded; + widget.onExpandWillChange?.call(_toggleState); + animationController + .forward() + .then((_) => widget.onExpandChanged?.call(_toggleState)); + } else if (animation.isCompleted) { + _toggleState = Expandable2State.collapsed; + widget.onExpandWillChange?.call(_toggleState); + animationController + .reverse() + .then((_) => widget.onExpandChanged?.call(_toggleState)); + } + controller?.state = _toggleState; + setState(() {}); + } + + @override + void initState() { + controller = widget.controller; + controller?.toggle = toggle; + + duration = Duration( + milliseconds: (500 * widget.animationDurationMultiplier).toInt(), + ); + animationController = widget.animationController ?? + AnimationController( + vsync: this, + duration: duration, + ); + animation = widget.animation ?? + Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + curve: Curves.easeInOut, + parent: animationController, + ), + ); + super.initState(); + } + + double _top = 0; + + void getHeaderHeight() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_key.currentContext?.size?.height != null && + _top != _key.currentContext!.size!.height) { + setState(() { + _top = _key.currentContext!.size!.height; + }); + } + }); + } + + @override + void dispose() { + animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + getHeaderHeight(); + + return AnimatedContainer( + duration: duration, + decoration: _toggleState == Expandable2State.expanded + ? BoxDecoration( + color: widget.background, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + border: Border.all(color: widget.border), + boxShadow: [ + Theme.of(context).extension()!.standardBoxShadow, + ], + ) + : BoxDecoration( + color: widget.background, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + border: Border.all(color: widget.border), + ), + child: Stack( + children: [ + Padding( + padding: EdgeInsets.only(top: _top), + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 1.0, + child: Column( + children: widget.children + .map( + (e) => Column( + children: [ + Container( + height: 1, + width: double.infinity, + color: widget.border, + ), + e, + ], + ), + ) + .toList(), + ), + ), + ), + MouseRegion( + key: _key, + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: widget.expandOverride ?? toggle, + child: Container( + color: Colors.transparent, + child: widget.header, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/icon_widgets/utxo_status_icon.dart b/lib/widgets/icon_widgets/utxo_status_icon.dart new file mode 100644 index 000000000..a6ae2f4da --- /dev/null +++ b/lib/widgets/icon_widgets/utxo_status_icon.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; + +import '../../utilities/theme/stack_colors.dart'; + +enum UTXOStatusIconStatus { + confirmed, + unconfirmed; +} + +class UTXOStatusIcon extends StatelessWidget { + const UTXOStatusIcon({ + Key? key, + required this.width, + required this.height, + required this.blocked, + required this.selected, + required this.status, + required this.background, + }) : super(key: key); + + final double width; + final double height; + final bool blocked; + final bool selected; + final UTXOStatusIconStatus status; + final Color background; + + final _availableColor = const Color(0xFFF7931A); + final _blockedColor = const Color(0xFF96B0D6); + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: status == UTXOStatusIconStatus.unconfirmed, + builder: (child) => Stack( + children: [ + child, + Positioned( + right: 0, + bottom: 0, + child: Stack( + children: [ + RoundedContainer( + radiusMultiplier: 100, + color: background, + width: width / 2.8, + height: height / 2.8, + ), + Positioned( + right: width / 2.8 - width / 3, + left: width / 2.8 - width / 3, + top: height / 2.8 - height / 3, + child: SvgPicture.asset( + Assets.svg.pending, + width: width / 3, + height: height / 3, + ), + ), + ], + ), + ), + ], + ), + child: Stack( + alignment: Alignment.center, + children: [ + RoundedContainer( + radiusMultiplier: 100, + color: selected + ? Theme.of(context).extension()!.infoItemIcons + : blocked + ? _blockedColor.withOpacity(0.3) + : _availableColor.withOpacity(0.2), + width: width, + height: height, + ), + SvgPicture.asset( + selected + ? Assets.svg.coinControl.selected + : blocked + ? Assets.svg.coinControl.blocked + : Assets.svg.coinControl.unBlocked, + width: 20, + height: 20, + color: selected + ? Colors.white + : blocked + ? _blockedColor + : _availableColor, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 4e51e0d0d..f6c4642b9 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -13,7 +13,6 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/test_epic_box_connection.dart'; @@ -149,6 +148,7 @@ class _NodeCardState extends ConsumerState { case Coin.litecoin: case Coin.dogecoin: case Coin.firo: + case Coin.particl: case Coin.bitcoinTestNet: case Coin.firoTestNet: case Coin.dogecoinTestNet: diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index 7542a42a6..e89da2e42 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -13,7 +13,6 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/test_epic_box_connection.dart'; @@ -132,6 +131,7 @@ class NodeOptionsSheet extends ConsumerWidget { case Coin.litecoin: case Coin.dogecoin: case Coin.firo: + case Coin.particl: case Coin.bitcoinTestNet: case Coin.firoTestNet: case Coin.dogecoinTestNet: diff --git a/lib/widgets/rounded_container.dart b/lib/widgets/rounded_container.dart index d689d9ca8..7f8456da8 100644 --- a/lib/widgets/rounded_container.dart +++ b/lib/widgets/rounded_container.dart @@ -1,5 +1,6 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; class RoundedContainer extends StatelessWidget { const RoundedContainer({ @@ -12,6 +13,7 @@ class RoundedContainer extends StatelessWidget { this.height, this.borderColor, this.boxShadow, + this.onPressed, }) : super(key: key); final Widget? child; @@ -22,24 +24,39 @@ class RoundedContainer extends StatelessWidget { final double? height; final Color? borderColor; final List? boxShadow; + final VoidCallback? onPressed; @override Widget build(BuildContext context) { - return Container( - width: width, - height: height, - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius * radiusMultiplier, + return ConditionalParent( + condition: onPressed != null, + builder: (child) => RawMaterialButton( + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius * radiusMultiplier, + ), ), - border: borderColor == null ? null : Border.all(color: borderColor!), - boxShadow: boxShadow, - ), - child: Padding( - padding: padding, + onPressed: onPressed, child: child, ), + child: Container( + width: width, + height: height, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius * radiusMultiplier, + ), + border: borderColor == null ? null : Border.all(color: borderColor!), + boxShadow: boxShadow, + ), + child: Padding( + padding: padding, + child: child, + ), + ), ); } } diff --git a/lib/widgets/stack_dialog.dart b/lib/widgets/stack_dialog.dart index f9b156831..b3cdaef70 100644 --- a/lib/widgets/stack_dialog.dart +++ b/lib/widgets/stack_dialog.dart @@ -21,20 +21,24 @@ class StackDialogBase extends StatelessWidget { mainAxisAlignment: !Util.isDesktop ? MainAxisAlignment.end : MainAxisAlignment.center, children: [ - Material( - borderRadius: BorderRadius.circular( - 20, - ), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, + Flexible( + child: SingleChildScrollView( + child: Material( borderRadius: BorderRadius.circular( 20, ), - ), - child: Padding( - padding: padding, - child: child, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + ), + child: Padding( + padding: padding, + child: child, + ), + ), ), ), ), diff --git a/lib/widgets/trade_card.dart b/lib/widgets/trade_card.dart index 5a14a0777..f4162fc5e 100644 --- a/lib/widgets/trade_card.dart +++ b/lib/widgets/trade_card.dart @@ -25,11 +25,21 @@ class TradeCard extends ConsumerWidget { ChangeNowTransactionStatus? status; try { if (statusString.toLowerCase().startsWith("waiting")) { - statusString = "waiting"; + statusString = "Waiting"; } status = changeNowTransactionStatusFromStringIgnoreCase(statusString); } on ArgumentError catch (_) { - status = ChangeNowTransactionStatus.Failed; + switch (statusString.toLowerCase()) { + case "funds confirming": + case "processing payment": + return Assets.svg.txExchangePending(context); + + case "completed": + return Assets.svg.txExchange(context); + + default: + status = ChangeNowTransactionStatus.Failed; + } } switch (status) { diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index 1b19a2380..e269ace4d 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -82,6 +82,8 @@ class _TransactionCardState extends ConsumerState { } else { return "Sending"; } + } else if (type == TransactionType.sentToSelf) { + return "Sent to self"; } else { return type.name; } @@ -222,8 +224,7 @@ class _TransactionCardState extends ConsumerState { final amount = _transaction.amount; return Text( "$prefix${Format.satoshiAmountToPrettyString(amount, locale, coin)} ${coin.ticker}", - style: - STextStyles.itemSubtitle12_600(context), + style: STextStyles.itemSubtitle12(context), ); }, ), diff --git a/lib/widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart b/lib/widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart new file mode 100644 index 000000000..fa2975f1a --- /dev/null +++ b/lib/widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; + +class BuyNavIcon extends StatelessWidget { + const BuyNavIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + Assets.svg.buy(context), + width: 24, + height: 24, + ); + } +} diff --git a/lib/widgets/wallet_navigation_bar/components/icons/coin_control_nav_icon.dart b/lib/widgets/wallet_navigation_bar/components/icons/coin_control_nav_icon.dart new file mode 100644 index 000000000..d55cd463f --- /dev/null +++ b/lib/widgets/wallet_navigation_bar/components/icons/coin_control_nav_icon.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class CoinControlNavIcon extends StatelessWidget { + const CoinControlNavIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + Assets.svg.coinControl.gamePad, + height: 20, + width: 20, + color: Theme.of(context).extension()!.bottomNavIconIcon, + ); + } +} diff --git a/lib/widgets/wallet_navigation_bar/components/icons/exchange_nav_icon.dart b/lib/widgets/wallet_navigation_bar/components/icons/exchange_nav_icon.dart new file mode 100644 index 000000000..9735fa989 --- /dev/null +++ b/lib/widgets/wallet_navigation_bar/components/icons/exchange_nav_icon.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; + +class ExchangeNavIcon extends StatelessWidget { + const ExchangeNavIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + Assets.svg.exchange(context), + width: 24, + height: 24, + ); + } +} diff --git a/lib/widgets/wallet_navigation_bar/components/icons/paynym_nav_icon.dart b/lib/widgets/wallet_navigation_bar/components/icons/paynym_nav_icon.dart new file mode 100644 index 000000000..26a2fd96d --- /dev/null +++ b/lib/widgets/wallet_navigation_bar/components/icons/paynym_nav_icon.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class PaynymNavIcon extends StatelessWidget { + const PaynymNavIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + Assets.svg.robotHead, + height: 20, + width: 20, + color: Theme.of(context).extension()!.bottomNavIconIcon, + ); + } +} diff --git a/lib/widgets/wallet_navigation_bar/components/icons/receive_nav_icon.dart b/lib/widgets/wallet_navigation_bar/components/icons/receive_nav_icon.dart new file mode 100644 index 000000000..54420543f --- /dev/null +++ b/lib/widgets/wallet_navigation_bar/components/icons/receive_nav_icon.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class ReceiveNavIcon extends StatelessWidget { + const ReceiveNavIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .accentColorDark + .withOpacity(0.4), + borderRadius: BorderRadius.circular( + 24, + ), + ), + child: Padding( + padding: const EdgeInsets.all(6.0), + child: SvgPicture.asset( + Assets.svg.arrowDownLeft, + width: 12, + height: 12, + color: Theme.of(context).extension()!.accentColorDark, + ), + ), + ); + } +} diff --git a/lib/widgets/wallet_navigation_bar/components/icons/send_nav_icon.dart b/lib/widgets/wallet_navigation_bar/components/icons/send_nav_icon.dart new file mode 100644 index 000000000..7b7da5799 --- /dev/null +++ b/lib/widgets/wallet_navigation_bar/components/icons/send_nav_icon.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class SendNavIcon extends StatelessWidget { + const SendNavIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .accentColorDark + .withOpacity(0.4), + borderRadius: BorderRadius.circular( + 24, + ), + ), + child: Padding( + padding: const EdgeInsets.all(6.0), + child: SvgPicture.asset( + Assets.svg.arrowUpRight, + width: 12, + height: 12, + color: Theme.of(context).extension()!.accentColorDark, + ), + ), + ); + } +} diff --git a/lib/widgets/wallet_navigation_bar/components/icons/whirlpool_nav_icon.dart b/lib/widgets/wallet_navigation_bar/components/icons/whirlpool_nav_icon.dart new file mode 100644 index 000000000..bc7b0f399 --- /dev/null +++ b/lib/widgets/wallet_navigation_bar/components/icons/whirlpool_nav_icon.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class WhirlpoolNavIcon extends StatelessWidget { + const WhirlpoolNavIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + Assets.svg.whirlPool, + height: 20, + width: 20, + color: Theme.of(context).extension()!.bottomNavIconIcon, + ); + } +} diff --git a/lib/widgets/wallet_navigation_bar/components/wallet_navigation_bar_item.dart b/lib/widgets/wallet_navigation_bar/components/wallet_navigation_bar_item.dart new file mode 100644 index 000000000..8f1cade50 --- /dev/null +++ b/lib/widgets/wallet_navigation_bar/components/wallet_navigation_bar_item.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/wallet_navigation_bar/wallet_navigation_bar.dart'; + +class WalletNavigationBarItemData { + WalletNavigationBarItemData({ + required this.icon, + required this.label, + required this.onTap, + this.isMore = false, + this.overrideText, + }); + + final Widget icon; + final String? label; + final VoidCallback? onTap; + final bool isMore; + final Widget? overrideText; +} + +class WalletNavigationBarItem extends ConsumerWidget { + const WalletNavigationBarItem({ + Key? key, + required this.data, + required this.disableDuration, + }) : super(key: key); + + final WalletNavigationBarItemData data; + final Duration disableDuration; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return GestureDetector( + onTap: data.isMore || !ref.watch(walletNavBarMore.state).state + ? data.onTap + : null, + child: RoundedContainer( + color: Colors.transparent, + padding: const EdgeInsets.all(0), + radiusMultiplier: 2, + child: AnimatedOpacity( + opacity: + data.isMore || !ref.watch(walletNavBarMore.state).state ? 1 : 0.2, + duration: disableDuration, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 45, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 24, + height: 24, + child: Center( + child: data.icon, + ), + ), + const Spacer(), + data.overrideText ?? + Text( + data.label ?? "", + style: STextStyles.buttonSmall(context), + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class WalletNavigationBarMoreItem extends ConsumerWidget { + const WalletNavigationBarMoreItem({ + Key? key, + required this.data, + }) : super(key: key); + + final WalletNavigationBarItemData data; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return GestureDetector( + onTap: () { + data.onTap?.call(); + ref.read(walletNavBarMore.state).state = false; + }, + child: Material( + color: Colors.transparent, + child: RoundedContainer( + color: Theme.of(context).extension()!.bottomNavBack, + radiusMultiplier: 100, + padding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 30, + ), + child: Row( + children: [ + Expanded( + child: Text( + data.label ?? "", + textAlign: TextAlign.center, + style: STextStyles.buttonSmall(context), + ), + ), + const SizedBox( + width: 10, + ), + data.icon, + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/wallet_navigation_bar/wallet_navigation_bar.dart b/lib/widgets/wallet_navigation_bar/wallet_navigation_bar.dart new file mode 100644 index 000000000..d8f58450f --- /dev/null +++ b/lib/widgets/wallet_navigation_bar/wallet_navigation_bar.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/wallet_navigation_bar/components/wallet_navigation_bar_item.dart'; + +final walletNavBarMore = StateProvider.autoDispose((ref) => false); + +class WalletNavigationBar extends ConsumerStatefulWidget { + const WalletNavigationBar({ + Key? key, + required this.items, + required this.moreItems, + }) : super(key: key); + + final List items; + final List moreItems; + + @override + ConsumerState createState() => + _WalletNavigationBarState(); +} + +class _WalletNavigationBarState extends ConsumerState { + static const double horizontalPadding = 16; + + final _moreDuration = const Duration(milliseconds: 200); + + void _onMorePressed() { + ref.read(walletNavBarMore.state).state = + !ref.read(walletNavBarMore.state).state; + } + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width - 40; + + final hasMore = widget.moreItems.isNotEmpty; + final buttonCount = widget.items.length + (hasMore ? 1 : 0); + + return Stack( + alignment: Alignment.bottomCenter, + children: [ + IgnorePointer( + ignoring: !ref.read(walletNavBarMore.state).state, + child: GestureDetector( + onTap: () { + if (ref.read(walletNavBarMore.state).state) { + ref.read(walletNavBarMore.state).state = false; + } + }, + child: AnimatedOpacity( + opacity: ref.watch(walletNavBarMore.state).state ? 1 : 0, + duration: _moreDuration, + child: Container( + color: Colors.black.withOpacity(0.7), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only( + left: horizontalPadding, + right: horizontalPadding, + bottom: horizontalPadding, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedScale( + scale: ref.watch(walletNavBarMore.state).state ? 1 : 0, + duration: _moreDuration, + alignment: const Alignment( + 0.5, + 1.0, + ), + child: AnimatedOpacity( + opacity: ref.watch(walletNavBarMore.state).state ? 1 : 0, + duration: _moreDuration, + child: IntrinsicWidth( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + ...widget.moreItems.map( + (e) { + return Column( + children: [ + WalletNavigationBarMoreItem(data: e), + const SizedBox( + height: 8, + ), + ], + ); + }, + ), + ], + ), + ), + ), + ), + Material( + color: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .bottomNavBack, + boxShadow: [ + Theme.of(context) + .extension()! + .standardBoxShadow + ], + borderRadius: BorderRadius.circular( + 1000, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 6, + horizontal: 20, + ), + // child: IntrinsicWidth( + child: ConditionalParent( + condition: buttonCount > 4, + builder: (child) => SizedBox( + width: width * 0.9, + child: child, + ), + child: ConditionalParent( + condition: buttonCount <= 4, + builder: (child) => SizedBox( + width: width * 0.2 * buttonCount, + child: child, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ...widget.items.map( + (e) => Expanded( + child: WalletNavigationBarItem( + data: e, + disableDuration: _moreDuration, + ), + ), + ), + if (hasMore) + Expanded( + child: WalletNavigationBarItem( + data: WalletNavigationBarItemData( + icon: AnimatedCrossFade( + firstChild: SvgPicture.asset( + Assets.svg.bars, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .bottomNavIconIcon, + ), + secondChild: SvgPicture.asset( + Assets.svg.bars, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .infoItemIcons, + ), + crossFadeState: ref + .watch(walletNavBarMore.state) + .state + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: _moreDuration, + ), + overrideText: AnimatedCrossFade( + firstChild: Text( + "More", + style: STextStyles.buttonSmall( + context), + ), + secondChild: Text( + "More", + style: + STextStyles.buttonSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .infoItemIcons, + ), + ), + crossFadeState: ref + .watch(walletNavBarMore.state) + .state + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: _moreDuration, + ), + label: null, + isMore: true, + onTap: _onMorePressed, + ), + disableDuration: _moreDuration, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index ec3dde2f7..fdf6de547 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -66,7 +66,7 @@ packages: source: hosted version: "1.4.0" async: - dependency: transitive + dependency: "direct main" description: name: async sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 @@ -395,7 +395,7 @@ packages: source: hosted version: "1.0.0" dart_bs58: - dependency: "direct main" + dependency: transitive description: name: dart_bs58 sha256: e2fff08fca810d5215f6fca3ea713d8a4a9728aaf1b1658472863b2de7377234 @@ -403,7 +403,7 @@ packages: source: hosted version: "1.0.1" dart_bs58check: - dependency: "direct main" + dependency: transitive description: name: dart_bs58check sha256: "4284e606795a18c1df5a955928bdc4e1b6f908da7ab0e87f49db51b3774e9e6c" @@ -514,14 +514,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.5" - ethereum_addresses: - dependency: "direct main" - description: - name: ethereum_addresses - sha256: e6ba01d44ecb9c5634367b017d6e94598fc937be8b28fc406d0e51ed6e9513dd - url: "https://pub.dev" - source: hosted - version: "1.0.2" event_bus: dependency: "direct main" description: @@ -802,7 +794,7 @@ packages: source: hosted version: "2.2.0" hex: - dependency: "direct main" + dependency: transitive description: name: hex sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" @@ -950,14 +942,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" - json_rpc_2: - dependency: transitive - description: - name: json_rpc_2 - sha256: "5e469bffa23899edacb7b22787780068d650b106a21c76db3c49218ab7ca447e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" jsonrpc2: dependency: "direct main" description: @@ -1595,14 +1579,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" - string_to_hex: - dependency: "direct main" - description: - name: string_to_hex - sha256: "63e5dc1f4821a2449d505033fbd4569f7020ebf30ddffb54d00ebaba8e144a49" - url: "https://pub.dev" - source: hosted - version: "0.2.2" string_validator: dependency: "direct main" description: @@ -1851,14 +1827,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - web3dart: - dependency: "direct main" - description: - name: web3dart - sha256: "48b89a5fac0029770a18d1a8bd05ce8431722bacf76184e4301dae05781565e5" - url: "https://pub.dev" - source: hosted - version: "2.3.5" web_socket_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b0b362827..4403ea462 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.46+133 +version: 1.6.3+145 environment: sdk: ">=2.17.0 <3.0.0" @@ -146,6 +146,7 @@ dependencies: dropdown_button2: 1.7.2 string_validator: ^0.3.0 equatable: ^2.0.5 + async: ^2.10.0 dart_bs58: ^1.0.1 dart_bs58check: ^3.0.2 hex: ^0.2.0 @@ -214,9 +215,9 @@ flutter: - google_fonts/ - assets/svg/circle-check.svg - assets/svg/clipboard.svg - - assets/images/unclaimed.png - assets/images/glasses.png - assets/images/glasses-hidden.png + - assets/svg/unclaimed.svg - assets/svg/plus.svg - assets/svg/gear.svg - assets/svg/bell.svg @@ -313,36 +314,6 @@ flutter: - assets/svg/whirlpool.svg - assets/svg/fingerprint.svg - assets/svg/faceid.svg - - assets/svg/cc.svg - # light theme coin - - assets/images/light/ - - # dark theme coin - - assets/images/dark/ - - # oled black theme coin - - assets/images/oledBlack/ - - # ocean breeze theme coin - - assets/images/oceanBreeze/ - - # fruit sorbet theme coin - - assets/images/fruitSorbet/ - - # forest theme coin - - assets/images/forest/ - - # coin icons - - assets/svg/coin_icons/ - - # lottie animations - - assets/lottie/test.json - - assets/lottie/test2.json - # socials - - assets/svg/socials/discord.svg - - assets/svg/socials/reddit-alien-brands.svg - - assets/svg/socials/twitter-brands.svg - - assets/svg/socials/telegram-brands.svg - assets/svg/chevron-right.svg - assets/svg/minimize.svg - assets/svg/wallet-fa.svg @@ -352,11 +323,20 @@ flutter: - assets/svg/box-auto.svg - assets/svg/framed-address-book.svg - assets/svg/framed-gear.svg + - assets/svg/list-ul.svg + + + # coin icons + - assets/svg/coin_icons/ + + # coin control icons + - assets/svg/coin_control/ + + # socials + - assets/svg/socials/ + # exchange icons - - assets/svg/exchange_icons/change_now_logo_1.svg - - assets/svg/exchange_icons/simpleswap-icon.svg - - assets/svg/exchange_icons/mb_green.svg - - assets/svg/exchange_icons/mb_blue.svg + - assets/svg/exchange_icons/ # theme selectors - assets/svg/dark-theme.svg @@ -365,28 +345,46 @@ flutter: - assets/svg/oled-black-theme.svg - assets/svg/fruit-sorbet-theme.svg - assets/svg/forest-theme.svg + - assets/svg/chanstheme.svg # light theme specific - - assets/svg/light/ + - assets/svg/themed/light/ # dark theme specific - - assets/svg/dark/ + - assets/svg/themed/dark/ # ocean theme specific - - assets/svg/oceanBreeze/ + - assets/svg/themed/oceanBreeze/ # OLED black theme specific - - assets/svg/oledBlack/ + - assets/svg/themed/oledBlack/ # fruit sorbet theme specific - - assets/svg/fruitSorbet/ + - assets/svg/themed/fruitSorbet/ # forest theme specific - - assets/svg/forest/ + - assets/svg/themed/forest/ # buy - - assets/svg/buy/Simplex-Nuvei-Logo.svg - - assets/svg/buy/Simplex-Nuvei-Logo-light.svg + - assets/svg/buy/ + + + # lottie animations + # basic + - assets/lottie/test2.json + + # coin gifs + - assets/gif/coins/bitcoin/ + - assets/gif/coins/bitcoincash/ + - assets/gif/coins/dogecoin/ + - assets/gif/coins/epicCash/ + - assets/gif/coins/ethereum/ + - assets/gif/coins/firo/ + - assets/gif/coins/litecoin/ + - assets/gif/coins/monero/ + - assets/gif/coins/namecoin/ + - assets/gif/coins/particl/ + - assets/gif/coins/wownero/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. diff --git a/scripts/setup.sh b/scripts/setup.sh index d9c716546..8be59f8c4 100644 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -12,7 +12,7 @@ sudo apt install -y unzip pkg-config clang cmake ninja-build libgtk-3-dev cd $DEVELOPMENT git clone https://github.com/flutter/flutter.git cd flutter -git checkout 3.3.4 +git checkout 3.7.6 export FLUTTER_DIR=$(pwd)/bin echo 'export PATH="$PATH:'${FLUTTER_DIR}'"' >> ~/.bashrc source ~/.bashrc diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index e447b3384..ca8544cc4 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -652,6 +652,19 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { returnValueForMissingStub: null, ); @override + bool get enableCoinControl => (super.noSuchMethod( + Invocation.getter(#enableCoinControl), + returnValue: false, + ) as bool); + @override + set enableCoinControl(bool? enableCoinControl) => super.noSuchMethod( + Invocation.setter( + #enableCoinControl, + enableCoinControl, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/electrumx_test.mocks.dart b/test/electrumx_test.mocks.dart index 299386599..b7147a3b8 100644 --- a/test/electrumx_test.mocks.dart +++ b/test/electrumx_test.mocks.dart @@ -373,6 +373,19 @@ class MockPrefs extends _i1.Mock implements _i4.Prefs { returnValueForMissingStub: null, ); @override + bool get enableCoinControl => (super.noSuchMethod( + Invocation.getter(#enableCoinControl), + returnValue: false, + ) as bool); + @override + set enableCoinControl(bool? enableCoinControl) => super.noSuchMethod( + Invocation.setter( + #enableCoinControl, + enableCoinControl, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/pages/send_view/send_view_test.dart b/test/pages/send_view/send_view_test.dart index 341032973..a0215046b 100644 --- a/test/pages/send_view/send_view_test.dart +++ b/test/pages/send_view/send_view_test.dart @@ -52,6 +52,7 @@ void main() { when(mockLocaleService.locale).thenAnswer((_) => "en_US"); when(mockPrefs.currency).thenAnswer((_) => "USD"); + when(mockPrefs.enableCoinControl).thenAnswer((_) => false); when(wallet.validateAddress("send to address")) .thenAnswer((realInvocation) => true); @@ -114,6 +115,7 @@ void main() { when(mockLocaleService.locale).thenAnswer((_) => "en_US"); when(mockPrefs.currency).thenAnswer((_) => "USD"); + when(mockPrefs.enableCoinControl).thenAnswer((_) => false); when(wallet.validateAddress("send to address")) .thenAnswer((realInvocation) => false); diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 2f18d2d02..9c2b16e64 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -4,7 +4,7 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i22; -import 'dart:typed_data' as _i28; +import 'dart:typed_data' as _i29; import 'dart:ui' as _i24; import 'package:bip32/bip32.dart' as _i17; @@ -20,19 +20,20 @@ import 'package:stackwallet/models/balance.dart' as _i12; import 'package:stackwallet/models/isar/models/isar_models.dart' as _i16; import 'package:stackwallet/models/node_model.dart' as _i25; import 'package:stackwallet/models/paymint/fee_object_model.dart' as _i9; +import 'package:stackwallet/models/signing_data.dart' as _i28; import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart' as _i26; import 'package:stackwallet/services/coins/coin_service.dart' as _i19; import 'package:stackwallet/services/coins/manager.dart' as _i6; -import 'package:stackwallet/services/locale_service.dart' as _i29; +import 'package:stackwallet/services/locale_service.dart' as _i30; import 'package:stackwallet/services/node_service.dart' as _i3; import 'package:stackwallet/services/transaction_notification_tracker.dart' as _i8; import 'package:stackwallet/services/wallets.dart' as _i20; import 'package:stackwallet/services/wallets_service.dart' as _i2; -import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i31; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i32; import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i21; import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart' as _i27; -import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i30; +import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i31; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' as _i7; import 'package:stackwallet/utilities/prefs.dart' as _i23; @@ -1415,29 +1416,30 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { returnValue: 0, ) as int); @override - dynamic coinSelection( - int? satoshiAmountToSend, - int? selectedTxFeeRate, - String? _recipientAddress, - bool? isSendAll, { + dynamic coinSelection({ + required int? satoshiAmountToSend, + required int? selectedTxFeeRate, + required String? recipientAddress, + required bool? coinControl, + required bool? isSendAll, int? additionalOutputs = 0, List<_i16.UTXO>? utxos, }) => super.noSuchMethod(Invocation.method( #coinSelection, - [ - satoshiAmountToSend, - selectedTxFeeRate, - _recipientAddress, - isSendAll, - ], + [], { + #satoshiAmountToSend: satoshiAmountToSend, + #selectedTxFeeRate: selectedTxFeeRate, + #recipientAddress: recipientAddress, + #coinControl: coinControl, + #isSendAll: isSendAll, #additionalOutputs: additionalOutputs, #utxos: utxos, }, )); @override - _i22.Future> fetchBuildTxData( + _i22.Future> fetchBuildTxData( List<_i16.UTXO>? utxosToUse) => (super.noSuchMethod( Invocation.method( @@ -1445,12 +1447,11 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { [utxosToUse], ), returnValue: - _i22.Future>.value({}), - ) as _i22.Future>); + _i22.Future>.value(<_i28.SigningData>[]), + ) as _i22.Future>); @override _i22.Future> buildTransaction({ - required List<_i16.UTXO>? utxosToUse, - required Map? utxoSigningData, + required List<_i28.SigningData>? utxoSigningData, required List? recipients, required List? satoshiAmounts, }) => @@ -1459,7 +1460,6 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { #buildTransaction, [], { - #utxosToUse: utxosToUse, #utxoSigningData: utxoSigningData, #recipients: recipients, #satoshiAmounts: satoshiAmounts, @@ -1713,7 +1713,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { })? prepareSend, required _i22.Future Function({required String address})? getTxCount, - required _i22.Future> Function(List<_i16.UTXO>)? + required _i22.Future> Function(List<_i16.UTXO>)? fetchBuildTxData, required _i22.Future Function()? refresh, required _i22.Future Function()? checkChangeAddressForTransactions, @@ -1865,14 +1865,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { )), ) as _i22.Future<_i18.PaymentCode>); @override - _i22.Future<_i28.Uint8List> signWithNotificationKey(_i28.Uint8List? data) => + _i22.Future<_i29.Uint8List> signWithNotificationKey(_i29.Uint8List? data) => (super.noSuchMethod( Invocation.method( #signWithNotificationKey, [data], ), - returnValue: _i22.Future<_i28.Uint8List>.value(_i28.Uint8List(0)), - ) as _i22.Future<_i28.Uint8List>); + returnValue: _i22.Future<_i29.Uint8List>.value(_i29.Uint8List(0)), + ) as _i22.Future<_i29.Uint8List>); @override _i22.Future signStringWithNotificationKey(String? data) => (super.noSuchMethod( @@ -2168,12 +2168,47 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { ), returnValue: _i22.Future.value(''), ) as _i22.Future); + @override + void initCoinControlInterface({ + required String? walletId, + required String? walletName, + required _i21.Coin? coin, + required _i13.MainDB? db, + required _i22.Future Function()? getChainHeight, + required _i22.Future Function(_i12.Balance)? refreshedBalanceCallback, + }) => + super.noSuchMethod( + Invocation.method( + #initCoinControlInterface, + [], + { + #walletId: walletId, + #walletName: walletName, + #coin: coin, + #db: db, + #getChainHeight: getChainHeight, + #refreshedBalanceCallback: refreshedBalanceCallback, + }, + ), + returnValueForMissingStub: null, + ); + @override + _i22.Future refreshBalance({bool? notify = false}) => + (super.noSuchMethod( + Invocation.method( + #refreshBalance, + [], + {#notify: notify}, + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); } /// A class which mocks [LocaleService]. /// /// See the documentation for Mockito's code generation for more information. -class MockLocaleService extends _i1.Mock implements _i29.LocaleService { +class MockLocaleService extends _i1.Mock implements _i30.LocaleService { MockLocaleService() { _i1.throwOnMissingStub(this); } @@ -2291,12 +2326,12 @@ class MockPrefs extends _i1.Mock implements _i23.Prefs { returnValueForMissingStub: null, ); @override - _i30.SyncingType get syncType => (super.noSuchMethod( + _i31.SyncingType get syncType => (super.noSuchMethod( Invocation.getter(#syncType), - returnValue: _i30.SyncingType.currentWalletOnly, - ) as _i30.SyncingType); + returnValue: _i31.SyncingType.currentWalletOnly, + ) as _i31.SyncingType); @override - set syncType(_i30.SyncingType? syncType) => super.noSuchMethod( + set syncType(_i31.SyncingType? syncType) => super.noSuchMethod( Invocation.setter( #syncType, syncType, @@ -2429,12 +2464,12 @@ class MockPrefs extends _i1.Mock implements _i23.Prefs { returnValueForMissingStub: null, ); @override - _i31.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( + _i32.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( Invocation.getter(#backupFrequencyType), - returnValue: _i31.BackupFrequencyType.everyTenMinutes, - ) as _i31.BackupFrequencyType); + returnValue: _i32.BackupFrequencyType.everyTenMinutes, + ) as _i32.BackupFrequencyType); @override - set backupFrequencyType(_i31.BackupFrequencyType? backupFrequencyType) => + set backupFrequencyType(_i32.BackupFrequencyType? backupFrequencyType) => super.noSuchMethod( Invocation.setter( #backupFrequencyType, @@ -2499,6 +2534,19 @@ class MockPrefs extends _i1.Mock implements _i23.Prefs { returnValueForMissingStub: null, ); @override + bool get enableCoinControl => (super.noSuchMethod( + Invocation.getter(#enableCoinControl), + returnValue: false, + ) as bool); + @override + set enableCoinControl(bool? enableCoinControl) => super.noSuchMethod( + Invocation.setter( + #enableCoinControl, + enableCoinControl, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, @@ -2728,6 +2776,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart index 38d959835..010eb83a3 100644 --- a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart @@ -373,6 +373,11 @@ class MockManager extends _i1.Mock implements _i11.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart index 0d3123461..addfaf6ee 100644 --- a/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart @@ -334,6 +334,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart index fa15063f7..ed28d7edc 100644 --- a/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart @@ -332,6 +332,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index 5ef51904d..b692ea887 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -322,6 +322,19 @@ class MockPrefs extends _i1.Mock implements _i3.Prefs { returnValueForMissingStub: null, ); @override + bool get enableCoinControl => (super.noSuchMethod( + Invocation.getter(#enableCoinControl), + returnValue: false, + ) as bool); + @override + set enableCoinControl(bool? enableCoinControl) => super.noSuchMethod( + Invocation.setter( + #enableCoinControl, + enableCoinControl, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/screen_tests/lockscreen_view_screen_test.mocks.dart b/test/screen_tests/lockscreen_view_screen_test.mocks.dart index a527edbe2..09c20af69 100644 --- a/test/screen_tests/lockscreen_view_screen_test.mocks.dart +++ b/test/screen_tests/lockscreen_view_screen_test.mocks.dart @@ -641,6 +641,11 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart index d3409d31a..83f17a9da 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart @@ -428,6 +428,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart index 8d391409d..092a08b8d 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart @@ -428,6 +428,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart index f1f5581d6..8ac5c53a2 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart @@ -428,6 +428,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart b/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart index 88b477129..e11d6e753 100644 --- a/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart @@ -203,6 +203,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart b/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart index b3d727de7..3070b5d69 100644 --- a/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart @@ -426,6 +426,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart index a70f86c6f..80b636a27 100644 --- a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart @@ -641,6 +641,11 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart index dfa8052fd..208bdfab9 100644 --- a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart @@ -482,6 +482,11 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart b/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart index e11f58e31..f7f149b62 100644 --- a/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart @@ -203,6 +203,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart index eef71451f..2c8902e82 100644 --- a/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart @@ -203,6 +203,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart index 7d6ac052f..4894da3fe 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart @@ -418,6 +418,11 @@ class MockManager extends _i1.Mock implements _i11.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart index 7c7319cd8..55a581fbd 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart @@ -418,6 +418,11 @@ class MockManager extends _i1.Mock implements _i11.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart index 78fafef16..1ddfc95bc 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart @@ -203,6 +203,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart index 099e486be..dd8109ad4 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart @@ -203,6 +203,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart index 69f7b2af4..f25778670 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart @@ -426,6 +426,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart index 9e553a09d..373541050 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart @@ -683,6 +683,11 @@ class MockManager extends _i1.Mock implements _i15.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart index 2eec54013..5a96f3a7b 100644 --- a/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart @@ -426,6 +426,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart b/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart index 541ebe771..031b57e12 100644 --- a/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart +++ b/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart @@ -205,6 +205,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart index 0fd8166a6..8df9045fd 100644 --- a/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart @@ -204,6 +204,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart index 66bf7ae61..c8f0c873a 100644 --- a/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart @@ -203,6 +203,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart index 18318af3e..0f6a3d551 100644 --- a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart @@ -245,6 +245,11 @@ class MockManager extends _i1.Mock implements _i8.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart index a20a9a381..4656dacc4 100644 --- a/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart @@ -205,6 +205,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/services/coins/manager_test.mocks.dart b/test/services/coins/manager_test.mocks.dart index fdc1e7e2b..2cbe98c5c 100644 --- a/test/services/coins/manager_test.mocks.dart +++ b/test/services/coins/manager_test.mocks.dart @@ -12,8 +12,9 @@ import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i5; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i4; import 'package:stackwallet/models/balance.dart' as _i6; import 'package:stackwallet/models/isar/models/isar_models.dart' as _i12; -import 'package:stackwallet/models/lelantus_coin.dart' as _i13; +import 'package:stackwallet/models/lelantus_coin.dart' as _i14; import 'package:stackwallet/models/paymint/fee_object_model.dart' as _i3; +import 'package:stackwallet/models/signing_data.dart' as _i13; import 'package:stackwallet/services/coins/firo/firo_wallet.dart' as _i9; import 'package:stackwallet/services/transaction_notification_tracker.dart' as _i2; @@ -485,7 +486,7 @@ class MockFiroWallet extends _i1.Mock implements _i9.FiroWallet { }, )); @override - _i10.Future> fetchBuildTxData( + _i10.Future> fetchBuildTxData( List<_i12.UTXO>? utxosToUse) => (super.noSuchMethod( Invocation.method( @@ -493,12 +494,11 @@ class MockFiroWallet extends _i1.Mock implements _i9.FiroWallet { [utxosToUse], ), returnValue: - _i10.Future>.value({}), - ) as _i10.Future>); + _i10.Future>.value(<_i13.SigningData>[]), + ) as _i10.Future>); @override _i10.Future> buildTransaction({ - required List<_i12.UTXO>? utxosToUse, - required Map? utxoSigningData, + required List<_i13.SigningData>? utxoSigningData, required List? recipients, required List? satoshiAmounts, }) => @@ -507,7 +507,6 @@ class MockFiroWallet extends _i1.Mock implements _i9.FiroWallet { #buildTransaction, [], { - #utxosToUse: utxosToUse, #utxoSigningData: utxoSigningData, #recipients: recipients, #satoshiAmounts: satoshiAmounts, @@ -570,14 +569,14 @@ class MockFiroWallet extends _i1.Mock implements _i9.FiroWallet { returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); @override - List> getLelantusCoinMap() => + List> getLelantusCoinMap() => (super.noSuchMethod( Invocation.method( #getLelantusCoinMap, [], ), - returnValue: >[], - ) as List>); + returnValue: >[], + ) as List>); @override _i10.Future anonymizeAllPublicFunds() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index 921091fb4..351704fa0 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -4,7 +4,7 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i22; -import 'dart:typed_data' as _i27; +import 'dart:typed_data' as _i28; import 'dart:ui' as _i24; import 'package:bip32/bip32.dart' as _i16; @@ -18,12 +18,13 @@ import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; import 'package:stackwallet/models/isar/models/isar_models.dart' as _i15; -import 'package:stackwallet/models/node_model.dart' as _i29; +import 'package:stackwallet/models/node_model.dart' as _i30; import 'package:stackwallet/models/paymint/fee_object_model.dart' as _i8; +import 'package:stackwallet/models/signing_data.dart' as _i27; import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart' as _i25; import 'package:stackwallet/services/coins/coin_service.dart' as _i19; import 'package:stackwallet/services/coins/manager.dart' as _i6; -import 'package:stackwallet/services/locale_service.dart' as _i28; +import 'package:stackwallet/services/locale_service.dart' as _i29; import 'package:stackwallet/services/node_service.dart' as _i3; import 'package:stackwallet/services/transaction_notification_tracker.dart' as _i7; @@ -1207,29 +1208,30 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValue: 0, ) as int); @override - dynamic coinSelection( - int? satoshiAmountToSend, - int? selectedTxFeeRate, - String? _recipientAddress, - bool? isSendAll, { + dynamic coinSelection({ + required int? satoshiAmountToSend, + required int? selectedTxFeeRate, + required String? recipientAddress, + required bool? coinControl, + required bool? isSendAll, int? additionalOutputs = 0, List<_i15.UTXO>? utxos, }) => super.noSuchMethod(Invocation.method( #coinSelection, - [ - satoshiAmountToSend, - selectedTxFeeRate, - _recipientAddress, - isSendAll, - ], + [], { + #satoshiAmountToSend: satoshiAmountToSend, + #selectedTxFeeRate: selectedTxFeeRate, + #recipientAddress: recipientAddress, + #coinControl: coinControl, + #isSendAll: isSendAll, #additionalOutputs: additionalOutputs, #utxos: utxos, }, )); @override - _i22.Future> fetchBuildTxData( + _i22.Future> fetchBuildTxData( List<_i15.UTXO>? utxosToUse) => (super.noSuchMethod( Invocation.method( @@ -1237,12 +1239,11 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [utxosToUse], ), returnValue: - _i22.Future>.value({}), - ) as _i22.Future>); + _i22.Future>.value(<_i27.SigningData>[]), + ) as _i22.Future>); @override _i22.Future> buildTransaction({ - required List<_i15.UTXO>? utxosToUse, - required Map? utxoSigningData, + required List<_i27.SigningData>? utxoSigningData, required List? recipients, required List? satoshiAmounts, }) => @@ -1251,7 +1252,6 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { #buildTransaction, [], { - #utxosToUse: utxosToUse, #utxoSigningData: utxoSigningData, #recipients: recipients, #satoshiAmounts: satoshiAmounts, @@ -1505,7 +1505,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { })? prepareSend, required _i22.Future Function({required String address})? getTxCount, - required _i22.Future> Function(List<_i15.UTXO>)? + required _i22.Future> Function(List<_i15.UTXO>)? fetchBuildTxData, required _i22.Future Function()? refresh, required _i22.Future Function()? checkChangeAddressForTransactions, @@ -1657,14 +1657,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { )), ) as _i22.Future<_i17.PaymentCode>); @override - _i22.Future<_i27.Uint8List> signWithNotificationKey(_i27.Uint8List? data) => + _i22.Future<_i28.Uint8List> signWithNotificationKey(_i28.Uint8List? data) => (super.noSuchMethod( Invocation.method( #signWithNotificationKey, [data], ), - returnValue: _i22.Future<_i27.Uint8List>.value(_i27.Uint8List(0)), - ) as _i22.Future<_i27.Uint8List>); + returnValue: _i22.Future<_i28.Uint8List>.value(_i28.Uint8List(0)), + ) as _i22.Future<_i28.Uint8List>); @override _i22.Future signStringWithNotificationKey(String? data) => (super.noSuchMethod( @@ -1960,12 +1960,47 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { ), returnValue: _i22.Future.value(''), ) as _i22.Future); + @override + void initCoinControlInterface({ + required String? walletId, + required String? walletName, + required _i21.Coin? coin, + required _i12.MainDB? db, + required _i22.Future Function()? getChainHeight, + required _i22.Future Function(_i11.Balance)? refreshedBalanceCallback, + }) => + super.noSuchMethod( + Invocation.method( + #initCoinControlInterface, + [], + { + #walletId: walletId, + #walletName: walletName, + #coin: coin, + #db: db, + #getChainHeight: getChainHeight, + #refreshedBalanceCallback: refreshedBalanceCallback, + }, + ), + returnValueForMissingStub: null, + ); + @override + _i22.Future refreshBalance({bool? notify = false}) => + (super.noSuchMethod( + Invocation.method( + #refreshBalance, + [], + {#notify: notify}, + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); } /// A class which mocks [LocaleService]. /// /// See the documentation for Mockito's code generation for more information. -class MockLocaleService extends _i1.Mock implements _i28.LocaleService { +class MockLocaleService extends _i1.Mock implements _i29.LocaleService { MockLocaleService() { _i1.throwOnMissingStub(this); } @@ -2037,15 +2072,15 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ), ) as _i18.SecureStorageInterface); @override - List<_i29.NodeModel> get primaryNodes => (super.noSuchMethod( + List<_i30.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), - returnValue: <_i29.NodeModel>[], - ) as List<_i29.NodeModel>); + returnValue: <_i30.NodeModel>[], + ) as List<_i30.NodeModel>); @override - List<_i29.NodeModel> get nodes => (super.noSuchMethod( + List<_i30.NodeModel> get nodes => (super.noSuchMethod( Invocation.getter(#nodes), - returnValue: <_i29.NodeModel>[], - ) as List<_i29.NodeModel>); + returnValue: <_i30.NodeModel>[], + ) as List<_i30.NodeModel>); @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -2063,7 +2098,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { @override _i22.Future setPrimaryNodeFor({ required _i21.Coin? coin, - required _i29.NodeModel? node, + required _i30.NodeModel? node, bool? shouldNotifyListeners = false, }) => (super.noSuchMethod( @@ -2080,40 +2115,40 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override - _i29.NodeModel? getPrimaryNodeFor({required _i21.Coin? coin}) => + _i30.NodeModel? getPrimaryNodeFor({required _i21.Coin? coin}) => (super.noSuchMethod(Invocation.method( #getPrimaryNodeFor, [], {#coin: coin}, - )) as _i29.NodeModel?); + )) as _i30.NodeModel?); @override - List<_i29.NodeModel> getNodesFor(_i21.Coin? coin) => (super.noSuchMethod( + List<_i30.NodeModel> getNodesFor(_i21.Coin? coin) => (super.noSuchMethod( Invocation.method( #getNodesFor, [coin], ), - returnValue: <_i29.NodeModel>[], - ) as List<_i29.NodeModel>); + returnValue: <_i30.NodeModel>[], + ) as List<_i30.NodeModel>); @override - _i29.NodeModel? getNodeById({required String? id}) => + _i30.NodeModel? getNodeById({required String? id}) => (super.noSuchMethod(Invocation.method( #getNodeById, [], {#id: id}, - )) as _i29.NodeModel?); + )) as _i30.NodeModel?); @override - List<_i29.NodeModel> failoverNodesFor({required _i21.Coin? coin}) => + List<_i30.NodeModel> failoverNodesFor({required _i21.Coin? coin}) => (super.noSuchMethod( Invocation.method( #failoverNodesFor, [], {#coin: coin}, ), - returnValue: <_i29.NodeModel>[], - ) as List<_i29.NodeModel>); + returnValue: <_i30.NodeModel>[], + ) as List<_i30.NodeModel>); @override _i22.Future add( - _i29.NodeModel? node, + _i30.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => @@ -2165,7 +2200,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) as _i22.Future); @override _i22.Future edit( - _i29.NodeModel? editedNode, + _i30.NodeModel? editedNode, String? password, bool? shouldNotifyListeners, ) => @@ -2371,6 +2406,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index 3fd613fd2..862afbb16 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -561,6 +561,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); @override + bool get enableCoinControl => (super.noSuchMethod( + Invocation.getter(#enableCoinControl), + returnValue: false, + ) as bool); + @override + set enableCoinControl(bool? enableCoinControl) => super.noSuchMethod( + Invocation.setter( + #enableCoinControl, + enableCoinControl, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/widget_tests/table_view/table_view_row_test.mocks.dart b/test/widget_tests/table_view/table_view_row_test.mocks.dart index 3ec849e3e..5eacbe02a 100644 --- a/test/widget_tests/table_view/table_view_row_test.mocks.dart +++ b/test/widget_tests/table_view/table_view_row_test.mocks.dart @@ -4,7 +4,7 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i21; -import 'dart:typed_data' as _i27; +import 'dart:typed_data' as _i28; import 'dart:ui' as _i23; import 'package:bip32/bip32.dart' as _i16; @@ -19,6 +19,7 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; import 'package:stackwallet/models/isar/models/isar_models.dart' as _i15; import 'package:stackwallet/models/paymint/fee_object_model.dart' as _i8; +import 'package:stackwallet/models/signing_data.dart' as _i26; import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart' as _i24; import 'package:stackwallet/services/coins/coin_service.dart' as _i18; import 'package:stackwallet/services/coins/manager.dart' as _i6; @@ -30,7 +31,7 @@ import 'package:stackwallet/services/wallets_service.dart' as _i2; import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i20; import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart' as _i25; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' - as _i26; + as _i27; import 'package:stackwallet/utilities/prefs.dart' as _i22; import 'package:tuple/tuple.dart' as _i14; @@ -1194,29 +1195,30 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { returnValue: 0, ) as int); @override - dynamic coinSelection( - int? satoshiAmountToSend, - int? selectedTxFeeRate, - String? _recipientAddress, - bool? isSendAll, { + dynamic coinSelection({ + required int? satoshiAmountToSend, + required int? selectedTxFeeRate, + required String? recipientAddress, + required bool? coinControl, + required bool? isSendAll, int? additionalOutputs = 0, List<_i15.UTXO>? utxos, }) => super.noSuchMethod(Invocation.method( #coinSelection, - [ - satoshiAmountToSend, - selectedTxFeeRate, - _recipientAddress, - isSendAll, - ], + [], { + #satoshiAmountToSend: satoshiAmountToSend, + #selectedTxFeeRate: selectedTxFeeRate, + #recipientAddress: recipientAddress, + #coinControl: coinControl, + #isSendAll: isSendAll, #additionalOutputs: additionalOutputs, #utxos: utxos, }, )); @override - _i21.Future> fetchBuildTxData( + _i21.Future> fetchBuildTxData( List<_i15.UTXO>? utxosToUse) => (super.noSuchMethod( Invocation.method( @@ -1224,12 +1226,11 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { [utxosToUse], ), returnValue: - _i21.Future>.value({}), - ) as _i21.Future>); + _i21.Future>.value(<_i26.SigningData>[]), + ) as _i21.Future>); @override _i21.Future> buildTransaction({ - required List<_i15.UTXO>? utxosToUse, - required Map? utxoSigningData, + required List<_i26.SigningData>? utxoSigningData, required List? recipients, required List? satoshiAmounts, }) => @@ -1238,7 +1239,6 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { #buildTransaction, [], { - #utxosToUse: utxosToUse, #utxoSigningData: utxoSigningData, #recipients: recipients, #satoshiAmounts: satoshiAmounts, @@ -1473,7 +1473,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { required _i20.Coin? coin, required _i12.MainDB? db, required _i9.ElectrumX? electrumXClient, - required _i26.SecureStorageInterface? secureStorage, + required _i27.SecureStorageInterface? secureStorage, required int? dustLimitP2PKH, required int? minConfirms, required _i21.Future Function()? getMnemonicString, @@ -1492,7 +1492,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { })? prepareSend, required _i21.Future Function({required String address})? getTxCount, - required _i21.Future> Function(List<_i15.UTXO>)? + required _i21.Future> Function(List<_i15.UTXO>)? fetchBuildTxData, required _i21.Future Function()? refresh, required _i21.Future Function()? checkChangeAddressForTransactions, @@ -1644,14 +1644,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { )), ) as _i21.Future<_i17.PaymentCode>); @override - _i21.Future<_i27.Uint8List> signWithNotificationKey(_i27.Uint8List? data) => + _i21.Future<_i28.Uint8List> signWithNotificationKey(_i28.Uint8List? data) => (super.noSuchMethod( Invocation.method( #signWithNotificationKey, [data], ), - returnValue: _i21.Future<_i27.Uint8List>.value(_i27.Uint8List(0)), - ) as _i21.Future<_i27.Uint8List>); + returnValue: _i21.Future<_i28.Uint8List>.value(_i28.Uint8List(0)), + ) as _i21.Future<_i28.Uint8List>); @override _i21.Future signStringWithNotificationKey(String? data) => (super.noSuchMethod( @@ -1947,6 +1947,41 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { ), returnValue: _i21.Future.value(''), ) as _i21.Future); + @override + void initCoinControlInterface({ + required String? walletId, + required String? walletName, + required _i20.Coin? coin, + required _i12.MainDB? db, + required _i21.Future Function()? getChainHeight, + required _i21.Future Function(_i11.Balance)? refreshedBalanceCallback, + }) => + super.noSuchMethod( + Invocation.method( + #initCoinControlInterface, + [], + { + #walletId: walletId, + #walletName: walletName, + #coin: coin, + #db: db, + #getChainHeight: getChainHeight, + #refreshedBalanceCallback: refreshedBalanceCallback, + }, + ), + returnValueForMissingStub: null, + ); + @override + _i21.Future refreshBalance({bool? notify = false}) => + (super.noSuchMethod( + Invocation.method( + #refreshBalance, + [], + {#notify: notify}, + ), + returnValue: _i21.Future.value(), + returnValueForMissingStub: _i21.Future.value(), + ) as _i21.Future); } /// A class which mocks [Manager]. @@ -2096,6 +2131,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 7077d7db5..3774b9124 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -16,20 +16,21 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i11; import 'package:stackwallet/models/balance.dart' as _i9; import 'package:stackwallet/models/isar/models/isar_models.dart' as _i21; import 'package:stackwallet/models/models.dart' as _i8; +import 'package:stackwallet/models/signing_data.dart' as _i23; import 'package:stackwallet/services/coins/coin_service.dart' as _i7; import 'package:stackwallet/services/coins/firo/firo_wallet.dart' as _i22; import 'package:stackwallet/services/coins/manager.dart' as _i6; -import 'package:stackwallet/services/locale_service.dart' as _i23; +import 'package:stackwallet/services/locale_service.dart' as _i24; import 'package:stackwallet/services/node_service.dart' as _i3; -import 'package:stackwallet/services/notes_service.dart' as _i27; -import 'package:stackwallet/services/price_service.dart' as _i26; +import 'package:stackwallet/services/notes_service.dart' as _i28; +import 'package:stackwallet/services/price_service.dart' as _i27; import 'package:stackwallet/services/transaction_notification_tracker.dart' as _i10; import 'package:stackwallet/services/wallets.dart' as _i16; import 'package:stackwallet/services/wallets_service.dart' as _i2; -import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i25; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i26; import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i17; -import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i24; +import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i25; import 'package:stackwallet/utilities/prefs.dart' as _i19; import 'package:tuple/tuple.dart' as _i15; @@ -549,6 +550,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, @@ -1438,7 +1444,7 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { }, )); @override - _i18.Future> fetchBuildTxData( + _i18.Future> fetchBuildTxData( List<_i21.UTXO>? utxosToUse) => (super.noSuchMethod( Invocation.method( @@ -1446,12 +1452,11 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { [utxosToUse], ), returnValue: - _i18.Future>.value({}), - ) as _i18.Future>); + _i18.Future>.value(<_i23.SigningData>[]), + ) as _i18.Future>); @override _i18.Future> buildTransaction({ - required List<_i21.UTXO>? utxosToUse, - required Map? utxoSigningData, + required List<_i23.SigningData>? utxoSigningData, required List? recipients, required List? satoshiAmounts, }) => @@ -1460,7 +1465,6 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { #buildTransaction, [], { - #utxosToUse: utxosToUse, #utxoSigningData: utxoSigningData, #recipients: recipients, #satoshiAmounts: satoshiAmounts, @@ -1996,7 +2000,7 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { /// A class which mocks [LocaleService]. /// /// See the documentation for Mockito's code generation for more information. -class MockLocaleService extends _i1.Mock implements _i23.LocaleService { +class MockLocaleService extends _i1.Mock implements _i24.LocaleService { MockLocaleService() { _i1.throwOnMissingStub(this); } @@ -2114,12 +2118,12 @@ class MockPrefs extends _i1.Mock implements _i19.Prefs { returnValueForMissingStub: null, ); @override - _i24.SyncingType get syncType => (super.noSuchMethod( + _i25.SyncingType get syncType => (super.noSuchMethod( Invocation.getter(#syncType), - returnValue: _i24.SyncingType.currentWalletOnly, - ) as _i24.SyncingType); + returnValue: _i25.SyncingType.currentWalletOnly, + ) as _i25.SyncingType); @override - set syncType(_i24.SyncingType? syncType) => super.noSuchMethod( + set syncType(_i25.SyncingType? syncType) => super.noSuchMethod( Invocation.setter( #syncType, syncType, @@ -2252,12 +2256,12 @@ class MockPrefs extends _i1.Mock implements _i19.Prefs { returnValueForMissingStub: null, ); @override - _i25.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( + _i26.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( Invocation.getter(#backupFrequencyType), - returnValue: _i25.BackupFrequencyType.everyTenMinutes, - ) as _i25.BackupFrequencyType); + returnValue: _i26.BackupFrequencyType.everyTenMinutes, + ) as _i26.BackupFrequencyType); @override - set backupFrequencyType(_i25.BackupFrequencyType? backupFrequencyType) => + set backupFrequencyType(_i26.BackupFrequencyType? backupFrequencyType) => super.noSuchMethod( Invocation.setter( #backupFrequencyType, @@ -2322,6 +2326,19 @@ class MockPrefs extends _i1.Mock implements _i19.Prefs { returnValueForMissingStub: null, ); @override + bool get enableCoinControl => (super.noSuchMethod( + Invocation.getter(#enableCoinControl), + returnValue: false, + ) as bool); + @override + set enableCoinControl(bool? enableCoinControl) => super.noSuchMethod( + Invocation.setter( + #enableCoinControl, + enableCoinControl, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, @@ -2407,7 +2424,7 @@ class MockPrefs extends _i1.Mock implements _i19.Prefs { /// A class which mocks [PriceService]. /// /// See the documentation for Mockito's code generation for more information. -class MockPriceService extends _i1.Mock implements _i26.PriceService { +class MockPriceService extends _i1.Mock implements _i27.PriceService { MockPriceService() { _i1.throwOnMissingStub(this); } @@ -2530,7 +2547,7 @@ class MockPriceService extends _i1.Mock implements _i26.PriceService { /// A class which mocks [NotesService]. /// /// See the documentation for Mockito's code generation for more information. -class MockNotesService extends _i1.Mock implements _i27.NotesService { +class MockNotesService extends _i1.Mock implements _i28.NotesService { MockNotesService() { _i1.throwOnMissingStub(this); } diff --git a/test/widget_tests/wallet_card_test.mocks.dart b/test/widget_tests/wallet_card_test.mocks.dart index 0fd68c3bb..93e6b562e 100644 --- a/test/widget_tests/wallet_card_test.mocks.dart +++ b/test/widget_tests/wallet_card_test.mocks.dart @@ -4,7 +4,7 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i20; -import 'dart:typed_data' as _i26; +import 'dart:typed_data' as _i27; import 'dart:ui' as _i22; import 'package:bip32/bip32.dart' as _i16; @@ -19,9 +19,10 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; import 'package:stackwallet/models/isar/models/isar_models.dart' as _i15; import 'package:stackwallet/models/paymint/fee_object_model.dart' as _i8; +import 'package:stackwallet/models/signing_data.dart' as _i25; import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart' as _i23; import 'package:stackwallet/services/coins/manager.dart' as _i6; -import 'package:stackwallet/services/locale_service.dart' as _i27; +import 'package:stackwallet/services/locale_service.dart' as _i28; import 'package:stackwallet/services/node_service.dart' as _i3; import 'package:stackwallet/services/transaction_notification_tracker.dart' as _i7; @@ -30,7 +31,7 @@ import 'package:stackwallet/services/wallets_service.dart' as _i2; import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i19; import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart' as _i24; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' - as _i25; + as _i26; import 'package:stackwallet/utilities/prefs.dart' as _i21; import 'package:tuple/tuple.dart' as _i14; @@ -957,29 +958,30 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { returnValue: 0, ) as int); @override - dynamic coinSelection( - int? satoshiAmountToSend, - int? selectedTxFeeRate, - String? _recipientAddress, - bool? isSendAll, { + dynamic coinSelection({ + required int? satoshiAmountToSend, + required int? selectedTxFeeRate, + required String? recipientAddress, + required bool? coinControl, + required bool? isSendAll, int? additionalOutputs = 0, List<_i15.UTXO>? utxos, }) => super.noSuchMethod(Invocation.method( #coinSelection, - [ - satoshiAmountToSend, - selectedTxFeeRate, - _recipientAddress, - isSendAll, - ], + [], { + #satoshiAmountToSend: satoshiAmountToSend, + #selectedTxFeeRate: selectedTxFeeRate, + #recipientAddress: recipientAddress, + #coinControl: coinControl, + #isSendAll: isSendAll, #additionalOutputs: additionalOutputs, #utxos: utxos, }, )); @override - _i20.Future> fetchBuildTxData( + _i20.Future> fetchBuildTxData( List<_i15.UTXO>? utxosToUse) => (super.noSuchMethod( Invocation.method( @@ -987,12 +989,11 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { [utxosToUse], ), returnValue: - _i20.Future>.value({}), - ) as _i20.Future>); + _i20.Future>.value(<_i25.SigningData>[]), + ) as _i20.Future>); @override _i20.Future> buildTransaction({ - required List<_i15.UTXO>? utxosToUse, - required Map? utxoSigningData, + required List<_i25.SigningData>? utxoSigningData, required List? recipients, required List? satoshiAmounts, }) => @@ -1001,7 +1002,6 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { #buildTransaction, [], { - #utxosToUse: utxosToUse, #utxoSigningData: utxoSigningData, #recipients: recipients, #satoshiAmounts: satoshiAmounts, @@ -1236,7 +1236,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { required _i19.Coin? coin, required _i12.MainDB? db, required _i9.ElectrumX? electrumXClient, - required _i25.SecureStorageInterface? secureStorage, + required _i26.SecureStorageInterface? secureStorage, required int? dustLimitP2PKH, required int? minConfirms, required _i20.Future Function()? getMnemonicString, @@ -1255,7 +1255,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { })? prepareSend, required _i20.Future Function({required String address})? getTxCount, - required _i20.Future> Function(List<_i15.UTXO>)? + required _i20.Future> Function(List<_i15.UTXO>)? fetchBuildTxData, required _i20.Future Function()? refresh, required _i20.Future Function()? checkChangeAddressForTransactions, @@ -1407,14 +1407,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { )), ) as _i20.Future<_i17.PaymentCode>); @override - _i20.Future<_i26.Uint8List> signWithNotificationKey(_i26.Uint8List? data) => + _i20.Future<_i27.Uint8List> signWithNotificationKey(_i27.Uint8List? data) => (super.noSuchMethod( Invocation.method( #signWithNotificationKey, [data], ), - returnValue: _i20.Future<_i26.Uint8List>.value(_i26.Uint8List(0)), - ) as _i20.Future<_i26.Uint8List>); + returnValue: _i20.Future<_i27.Uint8List>.value(_i27.Uint8List(0)), + ) as _i20.Future<_i27.Uint8List>); @override _i20.Future signStringWithNotificationKey(String? data) => (super.noSuchMethod( @@ -1710,12 +1710,47 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { ), returnValue: _i20.Future.value(''), ) as _i20.Future); + @override + void initCoinControlInterface({ + required String? walletId, + required String? walletName, + required _i19.Coin? coin, + required _i12.MainDB? db, + required _i20.Future Function()? getChainHeight, + required _i20.Future Function(_i11.Balance)? refreshedBalanceCallback, + }) => + super.noSuchMethod( + Invocation.method( + #initCoinControlInterface, + [], + { + #walletId: walletId, + #walletName: walletName, + #coin: coin, + #db: db, + #getChainHeight: getChainHeight, + #refreshedBalanceCallback: refreshedBalanceCallback, + }, + ), + returnValueForMissingStub: null, + ); + @override + _i20.Future refreshBalance({bool? notify = false}) => + (super.noSuchMethod( + Invocation.method( + #refreshBalance, + [], + {#notify: notify}, + ), + returnValue: _i20.Future.value(), + returnValueForMissingStub: _i20.Future.value(), + ) as _i20.Future); } /// A class which mocks [LocaleService]. /// /// See the documentation for Mockito's code generation for more information. -class MockLocaleService extends _i1.Mock implements _i27.LocaleService { +class MockLocaleService extends _i1.Mock implements _i28.LocaleService { MockLocaleService() { _i1.throwOnMissingStub(this); } diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index eb6bc6dac..f75530def 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -4,7 +4,7 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i22; -import 'dart:typed_data' as _i27; +import 'dart:typed_data' as _i28; import 'dart:ui' as _i24; import 'package:bip32/bip32.dart' as _i16; @@ -18,8 +18,9 @@ import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; import 'package:stackwallet/models/isar/models/isar_models.dart' as _i15; -import 'package:stackwallet/models/node_model.dart' as _i28; +import 'package:stackwallet/models/node_model.dart' as _i29; import 'package:stackwallet/models/paymint/fee_object_model.dart' as _i8; +import 'package:stackwallet/models/signing_data.dart' as _i27; import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart' as _i25; import 'package:stackwallet/services/coins/coin_service.dart' as _i19; import 'package:stackwallet/services/coins/manager.dart' as _i6; @@ -1206,29 +1207,30 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValue: 0, ) as int); @override - dynamic coinSelection( - int? satoshiAmountToSend, - int? selectedTxFeeRate, - String? _recipientAddress, - bool? isSendAll, { + dynamic coinSelection({ + required int? satoshiAmountToSend, + required int? selectedTxFeeRate, + required String? recipientAddress, + required bool? coinControl, + required bool? isSendAll, int? additionalOutputs = 0, List<_i15.UTXO>? utxos, }) => super.noSuchMethod(Invocation.method( #coinSelection, - [ - satoshiAmountToSend, - selectedTxFeeRate, - _recipientAddress, - isSendAll, - ], + [], { + #satoshiAmountToSend: satoshiAmountToSend, + #selectedTxFeeRate: selectedTxFeeRate, + #recipientAddress: recipientAddress, + #coinControl: coinControl, + #isSendAll: isSendAll, #additionalOutputs: additionalOutputs, #utxos: utxos, }, )); @override - _i22.Future> fetchBuildTxData( + _i22.Future> fetchBuildTxData( List<_i15.UTXO>? utxosToUse) => (super.noSuchMethod( Invocation.method( @@ -1236,12 +1238,11 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [utxosToUse], ), returnValue: - _i22.Future>.value({}), - ) as _i22.Future>); + _i22.Future>.value(<_i27.SigningData>[]), + ) as _i22.Future>); @override _i22.Future> buildTransaction({ - required List<_i15.UTXO>? utxosToUse, - required Map? utxoSigningData, + required List<_i27.SigningData>? utxoSigningData, required List? recipients, required List? satoshiAmounts, }) => @@ -1250,7 +1251,6 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { #buildTransaction, [], { - #utxosToUse: utxosToUse, #utxoSigningData: utxoSigningData, #recipients: recipients, #satoshiAmounts: satoshiAmounts, @@ -1504,7 +1504,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { })? prepareSend, required _i22.Future Function({required String address})? getTxCount, - required _i22.Future> Function(List<_i15.UTXO>)? + required _i22.Future> Function(List<_i15.UTXO>)? fetchBuildTxData, required _i22.Future Function()? refresh, required _i22.Future Function()? checkChangeAddressForTransactions, @@ -1656,14 +1656,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { )), ) as _i22.Future<_i17.PaymentCode>); @override - _i22.Future<_i27.Uint8List> signWithNotificationKey(_i27.Uint8List? data) => + _i22.Future<_i28.Uint8List> signWithNotificationKey(_i28.Uint8List? data) => (super.noSuchMethod( Invocation.method( #signWithNotificationKey, [data], ), - returnValue: _i22.Future<_i27.Uint8List>.value(_i27.Uint8List(0)), - ) as _i22.Future<_i27.Uint8List>); + returnValue: _i22.Future<_i28.Uint8List>.value(_i28.Uint8List(0)), + ) as _i22.Future<_i28.Uint8List>); @override _i22.Future signStringWithNotificationKey(String? data) => (super.noSuchMethod( @@ -1959,6 +1959,41 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { ), returnValue: _i22.Future.value(''), ) as _i22.Future); + @override + void initCoinControlInterface({ + required String? walletId, + required String? walletName, + required _i21.Coin? coin, + required _i12.MainDB? db, + required _i22.Future Function()? getChainHeight, + required _i22.Future Function(_i11.Balance)? refreshedBalanceCallback, + }) => + super.noSuchMethod( + Invocation.method( + #initCoinControlInterface, + [], + { + #walletId: walletId, + #walletName: walletName, + #coin: coin, + #db: db, + #getChainHeight: getChainHeight, + #refreshedBalanceCallback: refreshedBalanceCallback, + }, + ), + returnValueForMissingStub: null, + ); + @override + _i22.Future refreshBalance({bool? notify = false}) => + (super.noSuchMethod( + Invocation.method( + #refreshBalance, + [], + {#notify: notify}, + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); } /// A class which mocks [NodeService]. @@ -1974,15 +2009,15 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ), ) as _i18.SecureStorageInterface); @override - List<_i28.NodeModel> get primaryNodes => (super.noSuchMethod( + List<_i29.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), - returnValue: <_i28.NodeModel>[], - ) as List<_i28.NodeModel>); + returnValue: <_i29.NodeModel>[], + ) as List<_i29.NodeModel>); @override - List<_i28.NodeModel> get nodes => (super.noSuchMethod( + List<_i29.NodeModel> get nodes => (super.noSuchMethod( Invocation.getter(#nodes), - returnValue: <_i28.NodeModel>[], - ) as List<_i28.NodeModel>); + returnValue: <_i29.NodeModel>[], + ) as List<_i29.NodeModel>); @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -2000,7 +2035,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { @override _i22.Future setPrimaryNodeFor({ required _i21.Coin? coin, - required _i28.NodeModel? node, + required _i29.NodeModel? node, bool? shouldNotifyListeners = false, }) => (super.noSuchMethod( @@ -2017,40 +2052,40 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override - _i28.NodeModel? getPrimaryNodeFor({required _i21.Coin? coin}) => + _i29.NodeModel? getPrimaryNodeFor({required _i21.Coin? coin}) => (super.noSuchMethod(Invocation.method( #getPrimaryNodeFor, [], {#coin: coin}, - )) as _i28.NodeModel?); + )) as _i29.NodeModel?); @override - List<_i28.NodeModel> getNodesFor(_i21.Coin? coin) => (super.noSuchMethod( + List<_i29.NodeModel> getNodesFor(_i21.Coin? coin) => (super.noSuchMethod( Invocation.method( #getNodesFor, [coin], ), - returnValue: <_i28.NodeModel>[], - ) as List<_i28.NodeModel>); + returnValue: <_i29.NodeModel>[], + ) as List<_i29.NodeModel>); @override - _i28.NodeModel? getNodeById({required String? id}) => + _i29.NodeModel? getNodeById({required String? id}) => (super.noSuchMethod(Invocation.method( #getNodeById, [], {#id: id}, - )) as _i28.NodeModel?); + )) as _i29.NodeModel?); @override - List<_i28.NodeModel> failoverNodesFor({required _i21.Coin? coin}) => + List<_i29.NodeModel> failoverNodesFor({required _i21.Coin? coin}) => (super.noSuchMethod( Invocation.method( #failoverNodesFor, [], {#coin: coin}, ), - returnValue: <_i28.NodeModel>[], - ) as List<_i28.NodeModel>); + returnValue: <_i29.NodeModel>[], + ) as List<_i29.NodeModel>); @override _i22.Future add( - _i28.NodeModel? node, + _i29.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => @@ -2102,7 +2137,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) as _i22.Future); @override _i22.Future edit( - _i28.NodeModel? editedNode, + _i29.NodeModel? editedNode, String? password, bool? shouldNotifyListeners, ) => @@ -2308,6 +2343,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index f34be63e9..a9730c670 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -4,7 +4,7 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i22; -import 'dart:typed_data' as _i27; +import 'dart:typed_data' as _i28; import 'dart:ui' as _i24; import 'package:bip32/bip32.dart' as _i16; @@ -18,8 +18,9 @@ import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; import 'package:stackwallet/models/isar/models/isar_models.dart' as _i15; -import 'package:stackwallet/models/node_model.dart' as _i28; +import 'package:stackwallet/models/node_model.dart' as _i29; import 'package:stackwallet/models/paymint/fee_object_model.dart' as _i8; +import 'package:stackwallet/models/signing_data.dart' as _i27; import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart' as _i25; import 'package:stackwallet/services/coins/coin_service.dart' as _i19; import 'package:stackwallet/services/coins/manager.dart' as _i6; @@ -1206,29 +1207,30 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValue: 0, ) as int); @override - dynamic coinSelection( - int? satoshiAmountToSend, - int? selectedTxFeeRate, - String? _recipientAddress, - bool? isSendAll, { + dynamic coinSelection({ + required int? satoshiAmountToSend, + required int? selectedTxFeeRate, + required String? recipientAddress, + required bool? coinControl, + required bool? isSendAll, int? additionalOutputs = 0, List<_i15.UTXO>? utxos, }) => super.noSuchMethod(Invocation.method( #coinSelection, - [ - satoshiAmountToSend, - selectedTxFeeRate, - _recipientAddress, - isSendAll, - ], + [], { + #satoshiAmountToSend: satoshiAmountToSend, + #selectedTxFeeRate: selectedTxFeeRate, + #recipientAddress: recipientAddress, + #coinControl: coinControl, + #isSendAll: isSendAll, #additionalOutputs: additionalOutputs, #utxos: utxos, }, )); @override - _i22.Future> fetchBuildTxData( + _i22.Future> fetchBuildTxData( List<_i15.UTXO>? utxosToUse) => (super.noSuchMethod( Invocation.method( @@ -1236,12 +1238,11 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [utxosToUse], ), returnValue: - _i22.Future>.value({}), - ) as _i22.Future>); + _i22.Future>.value(<_i27.SigningData>[]), + ) as _i22.Future>); @override _i22.Future> buildTransaction({ - required List<_i15.UTXO>? utxosToUse, - required Map? utxoSigningData, + required List<_i27.SigningData>? utxoSigningData, required List? recipients, required List? satoshiAmounts, }) => @@ -1250,7 +1251,6 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { #buildTransaction, [], { - #utxosToUse: utxosToUse, #utxoSigningData: utxoSigningData, #recipients: recipients, #satoshiAmounts: satoshiAmounts, @@ -1504,7 +1504,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { })? prepareSend, required _i22.Future Function({required String address})? getTxCount, - required _i22.Future> Function(List<_i15.UTXO>)? + required _i22.Future> Function(List<_i15.UTXO>)? fetchBuildTxData, required _i22.Future Function()? refresh, required _i22.Future Function()? checkChangeAddressForTransactions, @@ -1656,14 +1656,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { )), ) as _i22.Future<_i17.PaymentCode>); @override - _i22.Future<_i27.Uint8List> signWithNotificationKey(_i27.Uint8List? data) => + _i22.Future<_i28.Uint8List> signWithNotificationKey(_i28.Uint8List? data) => (super.noSuchMethod( Invocation.method( #signWithNotificationKey, [data], ), - returnValue: _i22.Future<_i27.Uint8List>.value(_i27.Uint8List(0)), - ) as _i22.Future<_i27.Uint8List>); + returnValue: _i22.Future<_i28.Uint8List>.value(_i28.Uint8List(0)), + ) as _i22.Future<_i28.Uint8List>); @override _i22.Future signStringWithNotificationKey(String? data) => (super.noSuchMethod( @@ -1959,6 +1959,41 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { ), returnValue: _i22.Future.value(''), ) as _i22.Future); + @override + void initCoinControlInterface({ + required String? walletId, + required String? walletName, + required _i21.Coin? coin, + required _i12.MainDB? db, + required _i22.Future Function()? getChainHeight, + required _i22.Future Function(_i11.Balance)? refreshedBalanceCallback, + }) => + super.noSuchMethod( + Invocation.method( + #initCoinControlInterface, + [], + { + #walletId: walletId, + #walletName: walletName, + #coin: coin, + #db: db, + #getChainHeight: getChainHeight, + #refreshedBalanceCallback: refreshedBalanceCallback, + }, + ), + returnValueForMissingStub: null, + ); + @override + _i22.Future refreshBalance({bool? notify = false}) => + (super.noSuchMethod( + Invocation.method( + #refreshBalance, + [], + {#notify: notify}, + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); } /// A class which mocks [NodeService]. @@ -1974,15 +2009,15 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ), ) as _i18.SecureStorageInterface); @override - List<_i28.NodeModel> get primaryNodes => (super.noSuchMethod( + List<_i29.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), - returnValue: <_i28.NodeModel>[], - ) as List<_i28.NodeModel>); + returnValue: <_i29.NodeModel>[], + ) as List<_i29.NodeModel>); @override - List<_i28.NodeModel> get nodes => (super.noSuchMethod( + List<_i29.NodeModel> get nodes => (super.noSuchMethod( Invocation.getter(#nodes), - returnValue: <_i28.NodeModel>[], - ) as List<_i28.NodeModel>); + returnValue: <_i29.NodeModel>[], + ) as List<_i29.NodeModel>); @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -2000,7 +2035,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { @override _i22.Future setPrimaryNodeFor({ required _i21.Coin? coin, - required _i28.NodeModel? node, + required _i29.NodeModel? node, bool? shouldNotifyListeners = false, }) => (super.noSuchMethod( @@ -2017,40 +2052,40 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override - _i28.NodeModel? getPrimaryNodeFor({required _i21.Coin? coin}) => + _i29.NodeModel? getPrimaryNodeFor({required _i21.Coin? coin}) => (super.noSuchMethod(Invocation.method( #getPrimaryNodeFor, [], {#coin: coin}, - )) as _i28.NodeModel?); + )) as _i29.NodeModel?); @override - List<_i28.NodeModel> getNodesFor(_i21.Coin? coin) => (super.noSuchMethod( + List<_i29.NodeModel> getNodesFor(_i21.Coin? coin) => (super.noSuchMethod( Invocation.method( #getNodesFor, [coin], ), - returnValue: <_i28.NodeModel>[], - ) as List<_i28.NodeModel>); + returnValue: <_i29.NodeModel>[], + ) as List<_i29.NodeModel>); @override - _i28.NodeModel? getNodeById({required String? id}) => + _i29.NodeModel? getNodeById({required String? id}) => (super.noSuchMethod(Invocation.method( #getNodeById, [], {#id: id}, - )) as _i28.NodeModel?); + )) as _i29.NodeModel?); @override - List<_i28.NodeModel> failoverNodesFor({required _i21.Coin? coin}) => + List<_i29.NodeModel> failoverNodesFor({required _i21.Coin? coin}) => (super.noSuchMethod( Invocation.method( #failoverNodesFor, [], {#coin: coin}, ), - returnValue: <_i28.NodeModel>[], - ) as List<_i28.NodeModel>); + returnValue: <_i29.NodeModel>[], + ) as List<_i29.NodeModel>); @override _i22.Future add( - _i28.NodeModel? node, + _i29.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => @@ -2102,7 +2137,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) as _i22.Future); @override _i22.Future edit( - _i28.NodeModel? editedNode, + _i29.NodeModel? editedNode, String? password, bool? shouldNotifyListeners, ) => @@ -2308,6 +2343,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasCoinControlSupport => (super.noSuchMethod( + Invocation.getter(#hasCoinControlSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0,