Merge pull request #721 from cypherstack/staging

1.9.0 staging to main
This commit is contained in:
Diego Salazar 2024-01-18 11:40:04 -07:00 committed by GitHub
commit e128c054e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
575 changed files with 86434 additions and 99515 deletions

View file

@ -8,12 +8,12 @@ jobs:
- name: Prepare repository
uses: actions/checkout@v3
with:
flutter-version: '3.10.3'
flutter-version: '3.10.6'
channel: 'stable'
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.10.3'
flutter-version: '3.10.6'
channel: 'stable'
- name: Setup | Rust
uses: ATiltedTree/setup-rust@v1

3
.gitignore vendored
View file

@ -55,4 +55,7 @@ libcw_monero.dll
libcw_wownero.dll
libepic_cash_wallet.dll
libmobileliblelantus.dll
libtor_ffi.dll
flutter_libsparkmobile.dll
secp256k1.dll
/libisar.so

View file

@ -34,7 +34,9 @@ if (keystorePropertiesFile.exists()) {
android {
compileSdkVersion 33
ndkVersion = "21.1.6352462"
// ndkVersion = "21.1.6352462"
// ndkVersion = "25.2.9519653"
ndkVersion = "23.1.7779620"
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@ -49,7 +51,9 @@ android {
applicationId "com.cypherstack.stackwallet"
minSdkVersion 23
targetSdkVersion 33
ndkVersion = "21.1.6352462"
// ndkVersion = "21.1.6352462"
// ndkVersion = "25.2.9519653"
ndkVersion = "23.1.7779620"
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View file

@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.7.10'
ext.kotlin_version = '1.8.0'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

Binary file not shown.

Binary file not shown.

BIN
assets/gif/stacy_onion.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

3
assets/svg/fusing.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.768 12.7778C22.0427 9.72145 19.529 7.28212 16.4516 6.67196C14.9906 6.37118 13.4811 6.47323 12.0731 6.95663C12.3953 4.84042 13.7811 3.04089 15.7469 2.19569C16.3218 1.94862 16.1929 1.09999 15.575 1.04069C14.6349 0.944011 13.6895 1.01383 12.7765 1.22868C9.68399 1.95571 7.28204 4.46938 6.63321 7.54852C6.33243 9.00946 6.47423 10.5177 6.95548 11.9227C4.84012 11.6005 3.04059 10.2169 2.19755 8.24891C1.95048 7.67743 1.10176 7.80633 1.04265 8.42079C0.945952 9.3618 1.01579 10.3071 1.23066 11.2224C1.95598 14.2787 4.46965 16.7181 7.54707 17.3282C9.00801 17.629 10.5175 17.5269 11.9256 17.0435C11.6033 19.1598 10.2176 20.9593 8.25176 21.8045C7.67684 22.0515 7.80574 22.9002 8.42363 22.9595C9.36379 23.0562 10.3091 22.9863 11.2222 22.7715C14.2786 22.0462 16.7179 19.5325 17.328 16.4551C17.6288 14.9941 17.5268 13.4847 17.0434 12.0766C19.1596 12.3988 20.9591 13.7846 21.8043 15.7504C22.0514 16.3253 22.9 16.1964 22.9593 15.5785C23.0559 14.6384 22.9828 13.6931 22.768 12.7778ZM11.9613 14.0626C10.8613 14.0626 9.89884 13.1388 9.89884 12.0001C9.89884 10.8614 10.8227 9.93758 11.9613 9.93758C13.1 9.93758 14.0625 10.8614 14.0625 12.0001C14.0625 13.1388 13.1387 14.0626 11.9613 14.0626Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

3
assets/svg/monkey.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.32099 5.11654C6.36635 4.1732 5.61228 3.38665 4.66934 3.38665H4.33832C3.26477 3.38665 2.34238 4.25531 2.34238 5.35395V13.0533C2.34238 15.0164 3.93487 16.6089 5.89793 16.6089H6.38407L6.39773 16.1229L6.42638 15.1044L6.42658 15.1044V15.0904C6.42658 12.5838 8.06723 10.4614 10.3343 9.74146C11.2194 10.8231 12.5669 11.5163 14.0747 11.5163C14.341 11.5163 14.5977 11.4793 14.8307 11.4349L17.2447 13.7633L17.2463 13.7649C18.1535 14.6318 18.6673 15.8317 18.6673 17.0892V17.1274C18.6673 17.4146 18.436 17.6459 18.1488 17.6459C17.8616 17.6459 17.6303 17.4146 17.6303 17.1274V17.0892C17.6303 16.1162 17.2343 15.1861 16.5298 14.5151L14.4197 12.5036L13.5747 11.6981V12.8655V16.1089V16.6089H14.0747H15.0932C15.3805 16.6089 15.6118 16.8402 15.6118 17.1274C15.6118 17.4146 15.3805 17.6459 15.0932 17.6459H5.92658C3.38996 17.6459 1.33398 15.5908 1.33398 13.0533V5.35395C1.33398 3.6952 2.67931 2.34962 4.33832 2.34962H4.66934C6.20478 2.34962 7.42841 3.6318 7.35595 5.16484L7.35592 5.16564C7.29694 6.45768 6.32086 7.52368 5.03701 7.70095L5.03698 7.70076L5.0232 7.70306L4.97418 7.71122C4.69124 7.74677 4.43295 7.54849 4.39441 7.26866C4.35516 6.98361 4.55436 6.72184 4.83595 6.68323L4.83596 6.68333L4.84599 6.68174L4.90097 6.67306C5.6898 6.56499 6.28431 5.90776 6.32099 5.11654ZM6.32099 5.11654C6.32098 5.11674 6.32097 5.11695 6.32096 5.11715L5.82154 5.09295L6.32101 5.11592C6.321 5.11612 6.32099 5.11633 6.32099 5.11654ZM11.4399 7.74158L11.3069 7.46881L11.0036 7.46089C10.1647 7.43901 9.48213 6.76462 9.48213 5.92368C9.48213 5.07034 10.1733 4.38712 10.9908 4.38665L11.308 4.38646C11.1303 4.68787 11.0284 5.03924 11.0284 5.41442C11.0284 6.26965 11.5569 6.99893 12.3044 7.29712C12.3563 8.22999 13.1288 8.96998 14.0747 8.96998C15.0206 8.96998 15.7931 8.22999 15.8451 7.29712C16.5926 6.99893 17.121 6.26965 17.121 5.41442C17.121 5.03925 17.0191 4.68789 16.8415 4.38648L17.13 4.38665C17.9808 4.38713 18.6673 5.07495 18.6673 5.92368C18.6673 6.76031 17.9892 7.43872 17.1176 7.46088L16.7929 7.46914L16.6685 7.76911C16.247 8.78503 15.2445 9.49776 14.0747 9.49776C13.2838 9.49776 12.5655 9.16901 12.0541 8.64197C11.812 8.38874 11.6082 8.08661 11.4399 7.74158ZM13.0562 3.38665C12.3341 3.38665 11.6999 3.76432 11.3406 4.33302L11.4483 4.08766C11.8986 3.0621 12.9068 2.34961 14.0747 2.34961C15.2444 2.34961 16.2469 3.06287 16.6685 4.07834L16.7212 4.20536C16.3516 3.7085 15.7599 3.38665 15.0932 3.38665H13.0562ZM13.0469 5.41442C13.0469 5.41314 13.0471 5.41269 13.0473 5.4122C13.0477 5.41135 13.0485 5.40988 13.0501 5.40831C13.0517 5.40674 13.0531 5.4059 13.054 5.40554C13.0542 5.40545 13.0544 5.40538 13.0546 5.40531C13.055 5.40522 13.0555 5.40517 13.0562 5.40517C13.0575 5.40517 13.0579 5.40533 13.0584 5.40554C13.0593 5.4059 13.0607 5.40674 13.0623 5.40831C13.0639 5.40988 13.0647 5.41135 13.0651 5.4122C13.0653 5.41269 13.0655 5.41314 13.0655 5.41442C13.0655 5.415 13.0654 5.41541 13.0654 5.41573C13.0653 5.41612 13.0652 5.41638 13.0651 5.41665C13.0647 5.41749 13.0639 5.41897 13.0623 5.42054C13.0607 5.4221 13.0593 5.42295 13.0584 5.42331C13.0579 5.42352 13.0575 5.42368 13.0562 5.42368C13.0556 5.42368 13.0551 5.42364 13.0548 5.42358C13.0545 5.42351 13.0542 5.42341 13.054 5.42331C13.0531 5.42295 13.0517 5.4221 13.0501 5.42054C13.0485 5.41897 13.0477 5.41749 13.0473 5.41665C13.0471 5.41616 13.0469 5.41571 13.0469 5.41442ZM15.1025 5.41442C15.1025 5.41571 15.1023 5.41616 15.1021 5.41665C15.1018 5.41749 15.1009 5.41897 15.0994 5.42054C15.0978 5.4221 15.0963 5.42295 15.0955 5.42331C15.095 5.42352 15.0945 5.42368 15.0932 5.42368C15.092 5.42368 15.0915 5.42352 15.091 5.42331C15.0902 5.42295 15.0887 5.4221 15.0871 5.42054C15.0856 5.41897 15.0847 5.41749 15.0844 5.41665C15.0843 5.41643 15.0842 5.41621 15.0841 5.41592C15.084 5.41557 15.084 5.41512 15.084 5.41442C15.084 5.41314 15.0842 5.41269 15.0844 5.4122C15.0845 5.41178 15.0848 5.41121 15.0853 5.41054C15.0857 5.40987 15.0863 5.4091 15.0871 5.40831C15.0887 5.40674 15.0902 5.4059 15.091 5.40554C15.0915 5.40533 15.092 5.40517 15.0932 5.40517C15.0939 5.40517 15.0943 5.4052 15.0946 5.40527C15.095 5.40534 15.0952 5.40543 15.0955 5.40554C15.0963 5.4059 15.0978 5.40674 15.0994 5.40831C15.1009 5.40988 15.1018 5.41135 15.1021 5.4122C15.1023 5.41269 15.1025 5.41314 15.1025 5.41442Z" fill="#8E9192" stroke="#8E9192"/>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

12
assets/svg/ordinal.svg Normal file
View file

@ -0,0 +1,12 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="ordinal">
<path id="Rectangle 43 (Stroke)" fill-rule="evenodd" clip-rule="evenodd" d="M4.99935 3.33366C4.07887 3.33366 3.33268 4.07985 3.33268 5.00033V15.0003C3.33268 15.9208 4.07887 16.667 4.99935 16.667H14.9993C15.9198 16.667 16.666 15.9208 16.666 15.0003V5.00033C16.666 4.07985 15.9198 3.33366 14.9993 3.33366H4.99935ZM1.66602 5.00033C1.66602 3.15938 3.1584 1.66699 4.99935 1.66699H14.9993C16.8403 1.66699 18.3327 3.15938 18.3327 5.00033V15.0003C18.3327 16.8413 16.8403 18.3337 14.9993 18.3337H4.99935C3.1584 18.3337 1.66602 16.8413 1.66602 15.0003V5.00033Z" fill="#8E9192"/>
<circle id="Ellipse 76" cx="6.25" cy="6.25" r="1.25" fill="#8E9192"/>
<g id="Vector">
<path d="M6.89025 11.0396L5.11402 13.704C4.74482 14.2578 5.14181 14.9996 5.80739 14.9996H9.35986C10.0254 14.9996 10.4224 14.2578 10.0532 13.704L8.277 11.0396C7.94715 10.5449 7.2201 10.5449 6.89025 11.0396Z" fill="#8E9192"/>
<path d="M10.5883 8.15695L7.76997 13.7936C7.49292 14.3476 7.89584 14.9996 8.51532 14.9996H14.1519C14.7714 14.9996 15.1743 14.3476 14.8973 13.7936L12.079 8.15694C11.7719 7.54274 10.8954 7.54274 10.5883 8.15695Z" fill="#8E9192"/>
<path d="M6.89025 11.0396L5.11402 13.704C4.74482 14.2578 5.14181 14.9996 5.80739 14.9996H9.35986C10.0254 14.9996 10.4224 14.2578 10.0532 13.704L8.277 11.0396C7.94715 10.5449 7.2201 10.5449 6.89025 11.0396Z" stroke="#8E9192"/>
<path d="M10.5883 8.15695L7.76997 13.7936C7.49292 14.3476 7.89584 14.9996 8.51532 14.9996H14.1519C14.7714 14.9996 15.1743 14.3476 14.8973 13.7936L12.079 8.15694C11.7719 7.54274 10.8954 7.54274 10.5883 8.15695Z" stroke="#8E9192"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

3
assets/svg/peers.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.7 12.0002C11.1303 12.0002 13.1 10.0302 13.1 7.6002C13.1 5.17023 11.1303 3.2002 8.7 3.2002C6.26969 3.2002 4.3 5.17023 4.3 7.6002C4.3 10.0302 6.26969 12.0002 8.7 12.0002ZM10.4428 13.6502H6.95719C3.66647 13.6502 1 16.3177 1 19.6074C1 20.2674 1.5335 20.8002 2.19144 20.8002H15.2072C15.8672 20.8002 16.4 20.2674 16.4 19.6074C16.4 16.3177 13.7325 13.6502 10.4428 13.6502ZM17.4691 14.2002H14.9305C16.51 15.4961 17.5 17.4349 17.5 19.6074C17.5 20.0474 17.3694 20.453 17.1562 20.8002H21.9C22.5084 20.8002 23 20.3052 23 19.6693C23 16.6614 20.5387 14.2002 17.4691 14.2002ZM15.85 12.0002C17.9778 12.0002 19.7 10.278 19.7 8.1502C19.7 6.02238 17.9778 4.3002 15.85 4.3002C14.9868 4.3002 14.1986 4.59427 13.5565 5.07398C13.9525 5.83435 14.2 6.68582 14.2 7.6002C14.2 8.8212 13.7899 9.94251 13.1141 10.8559C13.8116 11.5602 14.7775 12.0002 15.85 12.0002Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 966 B

5
assets/svg/send.svg Normal file
View file

@ -0,0 +1,5 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="send">
<path id="Vector" d="M9.16537 4.9832C9.16537 5.1919 9.04104 5.3807 8.84889 5.46272L1.55925 8.60772C1.49302 8.63625 1.4232 8.65001 1.35397 8.65001C1.21334 8.65001 1.07532 8.59295 0.974398 8.48615C0.824086 8.32692 0.789503 8.09076 0.887266 7.89478L2.08827 5.49311L6.03562 4.99794L2.08801 4.50221L0.887005 2.10053C0.789292 1.90488 0.823936 1.66862 0.974235 1.50949C1.12505 1.35026 1.35837 1.30161 1.55908 1.38797L8.86373 4.51851C9.05612 4.60004 9.16537 4.78917 9.16537 4.9832Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 612 B

1
assets/svg/spark.svg Normal file
View file

@ -0,0 +1 @@
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><path d="m24.5 31.9-4.9 16.2h12.5l-4.2 13.9 16.5-20.2h-11.9l2.9-9.9z" fill="#ffce31" transform="matrix(1.25 0 0 2 -6 -62)"/></svg>

After

Width:  |  Height:  |  Size: 190 B

View file

@ -0,0 +1,5 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="24" fill="#E0E3E3"/>
<path d="M19.0284 34.8483C18.826 35.4564 19.1553 36.1134 19.7627 36.3159C19.884 36.3565 20.0097 36.3759 20.1306 36.3759C20.6169 36.3759 21.0689 36.0685 21.2318 35.5826L21.8017 33.8723C21.0171 33.7401 20.2679 33.5325 19.5553 33.27L19.0284 34.8483ZM28.9671 34.8483L28.4412 33.2695C27.7286 33.5322 26.9789 33.7398 26.1948 33.8718L26.7647 35.5821C26.9272 36.0665 27.3767 36.3759 27.865 36.3759C27.9858 36.3759 28.1097 36.357 28.232 36.3162C28.8414 36.1148 29.1266 35.4139 28.9671 34.8483ZM22.8376 34.0024V35.2157C22.8376 35.8586 23.3597 36.3759 23.9978 36.3759C24.6359 36.3759 25.1579 35.8562 25.1579 35.2157V34.0033C24.776 34.0362 24.3893 34.0555 23.9978 34.0555C23.6062 34.0555 23.2195 34.0362 22.8376 34.0024Z" fill="black"/>
<path d="M30.3295 18.0837C33.1429 19.7321 34.8203 22.4053 34.8203 25.2332C34.8203 30.0971 29.9622 34.0552 23.9922 34.0552C18.0222 34.0552 13.1641 30.0962 13.1641 25.2332C13.1641 22.4053 14.8439 19.7321 17.6568 18.0837C17.657 18.0836 17.6575 18.0833 17.6582 18.0828C17.7403 18.033 20.8389 16.1495 21.8067 12.2147C21.8744 11.9329 22.0951 11.7139 22.3776 11.6467C22.6532 11.5787 22.9529 11.6746 23.1414 11.8944L23.9922 12.8833L24.8449 11.8943C25.032 11.6745 25.3356 11.5785 25.6106 11.6465C25.893 11.7137 26.1135 11.9328 26.1815 12.2145C27.1555 16.1847 30.2978 18.0656 30.3295 18.0837Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.31521 18.0367C6.16525 18.4871 6.4092 18.9737 6.85912 19.1238C6.949 19.1538 7.04209 19.1682 7.13161 19.1682C7.49183 19.1682 7.82663 18.9405 7.9473 18.5806L8.36947 17.3137C7.78832 17.2157 7.23331 17.062 6.70551 16.8675L6.31521 18.0367ZM13.6772 18.0367L13.2876 16.8672C12.7598 17.0618 12.2044 17.2156 11.6236 17.3133L12.0458 18.5802C12.1661 18.939 12.4991 19.1682 12.8608 19.1682C12.9503 19.1682 13.042 19.1542 13.1327 19.124C13.5841 18.9748 13.7953 18.4556 13.6772 18.0367ZM9.13682 17.41V18.3088C9.13682 18.785 9.52354 19.1682 9.9962 19.1682C10.4689 19.1682 10.8556 18.7832 10.8556 18.3088V17.4107C10.5727 17.4351 10.2862 17.4494 9.9962 17.4494C9.70616 17.4494 9.4197 17.4351 9.13682 17.41Z" fill="#00A578"/>
<path d="M14.6878 5.61724C16.7718 6.83827 18.0143 8.81841 18.0143 10.9131C18.0143 14.5161 14.4157 17.448 9.99349 17.448C5.57129 17.448 1.97266 14.5154 1.97266 10.9131C1.97266 8.81841 3.21696 6.83827 5.30059 5.61724C5.30078 5.61712 5.30113 5.61691 5.30164 5.6166C5.36244 5.57964 7.6577 4.18451 8.37464 1.26979C8.42477 1.0611 8.58827 0.898861 8.79753 0.849054C9.00163 0.798701 9.22363 0.869768 9.36328 1.03255L9.99349 1.76509L10.6251 1.03245C10.7637 0.869643 10.9886 0.798597 11.1923 0.848943C11.4015 0.898715 11.5649 1.06099 11.6152 1.26968C12.3367 4.21053 14.6643 5.60381 14.6878 5.61724Z" fill="#00A578"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.31521 18.0367C6.16525 18.4871 6.4092 18.9737 6.85912 19.1238C6.949 19.1538 7.04209 19.1682 7.13161 19.1682C7.49183 19.1682 7.82663 18.9405 7.9473 18.5806L8.36947 17.3137C7.78832 17.2157 7.23331 17.062 6.70551 16.8675L6.31521 18.0367ZM13.6772 18.0367L13.2876 16.8672C12.7598 17.0618 12.2044 17.2156 11.6236 17.3133L12.0458 18.5802C12.1661 18.939 12.4991 19.1682 12.8608 19.1682C12.9503 19.1682 13.042 19.1542 13.1327 19.124C13.5841 18.9748 13.7953 18.4556 13.6772 18.0367ZM9.13682 17.41V18.3088C9.13682 18.785 9.52354 19.1682 9.9962 19.1682C10.4689 19.1682 10.8556 18.7832 10.8556 18.3088V17.4107C10.5727 17.4351 10.2862 17.4494 9.9962 17.4494C9.70616 17.4494 9.4197 17.4351 9.13682 17.41Z" fill="#F4C517"/>
<path d="M14.6878 5.61724C16.7718 6.83827 18.0143 8.81841 18.0143 10.9131C18.0143 14.5161 14.4157 17.448 9.99349 17.448C5.57129 17.448 1.97266 14.5154 1.97266 10.9131C1.97266 8.81841 3.21696 6.83827 5.30059 5.61724C5.30078 5.61712 5.30113 5.61691 5.30164 5.6166C5.36244 5.57964 7.6577 4.18451 8.37464 1.26979C8.42477 1.0611 8.58827 0.898861 8.79753 0.849054C9.00163 0.798701 9.22363 0.869768 9.36328 1.03255L9.99349 1.76509L10.6251 1.03245C10.7637 0.869643 10.9886 0.798597 11.1923 0.848943C11.4015 0.898715 11.5649 1.06099 11.6152 1.26968C12.3367 4.21053 14.6643 5.60381 14.6878 5.61724Z" fill="#F4C517"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

4
assets/svg/tor.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.31521 18.0367C6.16525 18.4871 6.4092 18.9737 6.85912 19.1238C6.949 19.1538 7.04209 19.1682 7.13161 19.1682C7.49183 19.1682 7.82663 18.9405 7.9473 18.5806L8.36947 17.3137C7.78832 17.2157 7.23331 17.062 6.70551 16.8675L6.31521 18.0367ZM13.6772 18.0367L13.2876 16.8672C12.7598 17.0618 12.2044 17.2156 11.6236 17.3133L12.0458 18.5802C12.1661 18.939 12.4991 19.1682 12.8608 19.1682C12.9503 19.1682 13.042 19.1542 13.1327 19.124C13.5841 18.9748 13.7953 18.4556 13.6772 18.0367ZM9.13682 17.41V18.3088C9.13682 18.785 9.52354 19.1682 9.9962 19.1682C10.4689 19.1682 10.8556 18.7832 10.8556 18.3088V17.4107C10.5727 17.4351 10.2862 17.4494 9.9962 17.4494C9.70616 17.4494 9.4197 17.4351 9.13682 17.41Z" fill="#C4C7C7"/>
<path d="M14.6878 5.61724C16.7718 6.83827 18.0143 8.81841 18.0143 10.9131C18.0143 14.5161 14.4157 17.448 9.99349 17.448C5.57129 17.448 1.97266 14.5154 1.97266 10.9131C1.97266 8.81841 3.21696 6.83827 5.30059 5.61724C5.30078 5.61712 5.30113 5.61691 5.30164 5.6166C5.36244 5.57964 7.6577 4.18451 8.37464 1.26979C8.42477 1.0611 8.58827 0.898861 8.79753 0.849054C9.00163 0.798701 9.22363 0.869768 9.36328 1.03255L9.99349 1.76509L10.6251 1.03245C10.7637 0.869643 10.9886 0.798597 11.1923 0.848943C11.4015 0.898715 11.5649 1.06099 11.6152 1.26968C12.3367 4.21053 14.6643 5.60381 14.6878 5.61724Z" fill="#C4C7C7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,3 @@
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.875 16.8755H2.08633C1.36574 16.8755 0.75 17.4899 0.75 18.2118C0.75 18.9337 1.36574 19.6255 2.08633 19.6255H15.875C16.6355 19.6255 17.25 19.011 17.25 18.2505C17.25 17.4899 16.6355 16.8755 15.875 16.8755ZM3.15625 8.61687H6.25V14.1212C6.25 14.8813 6.86574 15.4979 7.625 15.4979H10.375C11.1343 15.4979 11.75 14.8813 11.75 14.1212V8.61687H14.8438C15.2553 8.61687 15.6279 8.3716 15.7912 7.99339C15.9537 7.61514 15.8765 7.1757 15.5938 6.8762L9.75006 0.684407C9.36068 0.271864 8.63975 0.271864 8.25002 0.684407L2.40627 6.8762C2.12363 7.17587 2.04641 7.61527 2.20887 7.99339C2.37207 8.37195 2.74461 8.61687 3.15625 8.61687Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 747 B

@ -1 +1 @@
Subproject commit f677dec0b34d3f9fe8fce2bc8ff5c508c3f3bb9a
Subproject commit 5566f2bdb3d960cbda44e049a2ec11c363053dab

@ -1 +1 @@
Subproject commit 407425c9fcf7a30c81f1345246c7225bc18b5cd5
Subproject commit cb876251b97d20b12ddd05268913d2cf4b78f0bf

View file

@ -1,17 +0,0 @@
FROM ubuntu:20.04 as base
COPY . /stack_wallet
WORKDIR /stack_wallet/scripts/linux
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y git=1:2.25.1-1ubuntu3.6 make=4.2.1-1.2 curl=7.68.0-1ubuntu2.14 cargo=0.62.0ubuntu0libgit2-0ubuntu0.20.04.1 \
file=1:5.38-4 ca-certificates=20211016ubuntu0.20.04.1 cmake=3.16.3-1ubuntu1.20.04.1 cmake-data=3.16.3-1ubuntu1.20.04.1 g++=4:9.3.0-1ubuntu2 libgmp-dev=2:6.2.0+dfsg-4ubuntu0.1 libssl-dev=1.1.1f-1ubuntu2.16 \
libclang-dev=1:10.0-50~exp1 unzip=6.0-25ubuntu1.1 python3=3.8.2-0ubuntu2 pkg-config=0.29.1-0ubuntu4 libglib2.0-dev=2.64.6-1~ubuntu20.04.4 libgcrypt20-dev=1.8.5-5ubuntu1.1 gettext-base=0.19.8.1-10build1 \
libgirepository1.0-dev=1.64.1-1~ubuntu20.04.1 valac=0.48.6-0ubuntu1 xsltproc=1.1.34-4ubuntu0.20.04.1 docbook-xsl=1.79.1+dfsg-2 python3-pip=20.0.2-5ubuntu1.6 ninja-build=1.10.0-1build1 clang=1:10.0-50~exp1 \
libgtk-3-dev=3.24.20-0ubuntu1.1 libunbound-dev=1.9.4-2ubuntu1.4 libzmq3-dev=4.3.2-2ubuntu1 libtool=2.4.6-14 autoconf=2.69-11.1 automake=1:1.16.1-4ubuntu6 bison=2:3.5.1+dfsg-1 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& pip3 install --upgrade meson==0.64.1 markdown==3.4.1 markupsafe==2.1.1 jinja2==3.1.2 pygments==2.13.0 toml==0.10.2 typogrify==2.0.7 tomli==2.0.1 && cd .. && ./prebuild.sh && cd linux && ./build_all.sh
WORKDIR /
RUN git clone https://github.com/flutter/flutter.git -b 3.3.4
ENV PATH "$PATH:/flutter/bin"
WORKDIR /stack_wallet
RUN flutter pub get Linux && flutter build linux
ENTRYPOINT ["/bin/bash"]

View file

@ -123,7 +123,7 @@ flutter run android
Note on Emulators: Only x86_64 emulators are supported, x86 emulators will not work
#### Linux
Plug in your android device or use the emulator available via Android Studio and then run the following commands:
Run the following commands or launch via Android Studio:
```
flutter pub get
flutter run linux

View file

@ -61,6 +61,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B999088F2ABE1E170012A442 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
E6F536731AC506735EB76340 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -140,6 +141,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
B999088F2ABE1E170012A442 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@ -321,8 +323,8 @@
"${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework",
"${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework",
"${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework",
"${BUILT_PRODUCTS_DIR}/stack_wallet_backup/stack_wallet_backup.framework",
"${BUILT_PRODUCTS_DIR}/tor_ffi_plugin/tor_ffi_plugin.framework",
"${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework",
"${BUILT_PRODUCTS_DIR}/wakelock/wakelock.framework",
);
@ -354,8 +356,8 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/stack_wallet_backup.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/tor_ffi_plugin.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock.framework",
);
@ -455,6 +457,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@ -645,6 +648,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@ -727,6 +731,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

View file

@ -12,7 +12,8 @@ import 'package:hive/hive.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/db/migrate_wallets_to_isar.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:stackwallet/models/contact.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
@ -47,14 +48,13 @@ class DbVersionMigrator with WalletDB {
case 0:
await Hive.openBox<dynamic>(DB.boxNameAllWalletsData);
await Hive.openBox<dynamic>(DB.boxNamePrefs);
final walletsService =
WalletsService(secureStorageInterface: secureStore);
final walletsService = WalletsService();
final nodeService = NodeService(secureStorageInterface: secureStore);
final prefs = Prefs.instance;
final walletInfoList = await walletsService.walletNames;
await prefs.init();
ElectrumX? client;
ElectrumXClient? client;
int? latestSetId;
// only instantiate client if there are firo wallets
@ -76,7 +76,7 @@ class DbVersionMigrator with WalletDB {
)
.toList();
client = ElectrumX.from(
client = ElectrumXClient.from(
node: ElectrumXNode(
address: node.host,
port: node.port,
@ -88,7 +88,7 @@ class DbVersionMigrator with WalletDB {
);
try {
latestSetId = await client.getLatestCoinId();
latestSetId = await client.getLelantusLatestCoinId();
} catch (e) {
// default to 2 for now
latestSetId = 2;
@ -180,7 +180,6 @@ class DbVersionMigrator with WalletDB {
// clear possible broken firo cache
await DB.instance.clearSharedTransactionCache(coin: Coin.firo);
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 4);
@ -304,8 +303,7 @@ class DbVersionMigrator with WalletDB {
case 8:
// migrate
await Hive.openBox<dynamic>(DB.boxNameAllWalletsData);
final walletsService =
WalletsService(secureStorageInterface: secureStore);
final walletsService = WalletsService();
final walletInfoList = await walletsService.walletNames;
await MainDB.instance.initMainDB();
for (final walletId in walletInfoList.keys) {
@ -343,6 +341,28 @@ class DbVersionMigrator with WalletDB {
// try to continue migrating
return await migrate(10, secureStore: secureStore);
case 10:
// migrate
await _v10(secureStore);
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 11);
// try to continue migrating
return await migrate(11, secureStore: secureStore);
case 11:
// migrate
await _v11(secureStore);
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 12);
// try to continue migrating
return await migrate(12, secureStore: secureStore);
default:
// finally return
return;
@ -352,7 +372,7 @@ class DbVersionMigrator with WalletDB {
Future<void> _v4(SecureStorageInterface secureStore) async {
await Hive.openBox<dynamic>(DB.boxNameAllWalletsData);
await Hive.openBox<dynamic>(DB.boxNamePrefs);
final walletsService = WalletsService(secureStorageInterface: secureStore);
final walletsService = WalletsService();
final prefs = Prefs.instance;
final walletInfoList = await walletsService.walletNames;
await prefs.init();
@ -465,7 +485,7 @@ class DbVersionMigrator with WalletDB {
Future<void> _v7(SecureStorageInterface secureStore) async {
await Hive.openBox<dynamic>(DB.boxNameAllWalletsData);
final walletsService = WalletsService(secureStorageInterface: secureStore);
final walletsService = WalletsService();
final walletInfoList = await walletsService.walletNames;
await MainDB.instance.initMainDB();
@ -547,4 +567,70 @@ class DbVersionMigrator with WalletDB {
await addressBookBox.deleteFromDisk();
}
Future<void> _v10(SecureStorageInterface secureStore) async {
await Hive.openBox<dynamic>(DB.boxNameAllWalletsData);
await Hive.openBox<dynamic>(DB.boxNamePrefs);
final walletsService = WalletsService();
final prefs = Prefs.instance;
final walletInfoList = await walletsService.walletNames;
await prefs.init();
await MainDB.instance.initMainDB();
for (final walletId in walletInfoList.keys) {
final info = walletInfoList[walletId]!;
assert(info.walletId == walletId);
if (info.coin == Coin.firo &&
MainDB.instance.isar.lelantusCoins
.where()
.walletIdEqualTo(walletId)
.countSync() ==
0) {
final walletBox = await Hive.openBox<dynamic>(walletId);
final hiveLCoins = DB.instance.get<dynamic>(
boxName: walletId,
key: "_lelantus_coins",
) as List? ??
[];
final jindexes = (DB.instance
.get<dynamic>(boxName: walletId, key: "jindex") as List? ??
[])
.cast<int>();
final List<isar_models.LelantusCoin> coins = [];
for (final e in hiveLCoins) {
final map = e as Map;
final lcoin = map.values.first as LelantusCoin;
final isJMint = jindexes.contains(lcoin.index);
final coin = isar_models.LelantusCoin(
walletId: walletId,
txid: lcoin.txId,
value: lcoin.value.toString(),
mintIndex: lcoin.index,
anonymitySetId: lcoin.anonymitySetId,
isUsed: lcoin.isUsed,
isJMint: isJMint,
otherData: null,
);
coins.add(coin);
}
if (coins.isNotEmpty) {
await MainDB.instance.isar.writeTxn(() async {
await MainDB.instance.isar.lelantusCoins.putAll(coins);
});
}
}
}
}
Future<void> _v11(SecureStorageInterface secureStore) async {
await migrateWalletsToIsar(secureStore: secureStore);
}
}

View file

@ -26,12 +26,13 @@ class DB {
@Deprecated("Left over for migration from old versions of Stack Wallet")
static const String boxNameAddressBook = "addressBook";
static const String boxNameTrades = "exchangeTransactionsBox";
static const String boxNameAllWalletsData = "wallets";
static const String boxNameFavoriteWallets = "favoriteWallets";
// in use
// TODO: migrate
static const String boxNameNodeModels = "nodeModels";
static const String boxNamePrimaryNodes = "primaryNodes";
static const String boxNameAllWalletsData = "wallets";
static const String boxNameNotifications = "notificationModels";
static const String boxNameWatchedTransactions =
"watchedTxNotificationModels";
@ -39,21 +40,25 @@ class DB {
static const String boxNameTradesV2 = "exchangeTradesBox";
static const String boxNameTradeNotes = "tradeNotesBox";
static const String boxNameTradeLookup = "tradeToTxidLookUpBox";
static const String boxNameFavoriteWallets = "favoriteWallets";
static const String boxNameWalletsToDeleteOnStart = "walletsToDeleteOnStart";
static const String boxNamePriceCache = "priceAPIPrice24hCache";
// in use (keep for now)
static const String boxNameDBInfo = "dbInfo";
static const String boxNamePrefs = "prefs";
static const String boxNameOneTimeDialogsShown = "oneTimeDialogsShown";
String _boxNameTxCache({required Coin coin}) => "${coin.name}_txCache";
// firo only
String _boxNameSetCache({required Coin coin}) =>
"${coin.name}_anonymitySetCache";
String _boxNameSetSparkCache({required Coin coin}) =>
"${coin.name}_anonymitySetSparkCache";
String _boxNameUsedSerialsCache({required Coin coin}) =>
"${coin.name}_usedSerialsCache";
String _boxNameSparkUsedCoinsTagsCache({required Coin coin}) =>
"${coin.name}_sparkUsedCoinsTagsCache";
Box<NodeModel>? _boxNodeModels;
Box<NodeModel>? _boxPrimaryNodes;
@ -74,7 +79,9 @@ class DB {
final Map<Coin, Box<dynamic>> _txCacheBoxes = {};
final Map<Coin, Box<dynamic>> _setCacheBoxes = {};
final Map<Coin, Box<dynamic>> _setSparkCacheBoxes = {};
final Map<Coin, Box<dynamic>> _usedSerialsCacheBoxes = {};
final Map<Coin, Box<dynamic>> _getSparkUsedCoinsTagsCacheBoxes = {};
// exposed for monero
Box<xmr.WalletInfo> get moneroWalletInfoBox => _walletInfoSource!;
@ -177,6 +184,9 @@ class DB {
}
Future<Box<dynamic>> getTxCacheBox({required Coin coin}) async {
if (_txCacheBoxes[coin]?.isOpen != true) {
_txCacheBoxes.remove(coin);
}
return _txCacheBoxes[coin] ??=
await Hive.openBox<dynamic>(_boxNameTxCache(coin: coin));
}
@ -186,19 +196,44 @@ class DB {
}
Future<Box<dynamic>> getAnonymitySetCacheBox({required Coin coin}) async {
if (_setCacheBoxes[coin]?.isOpen != true) {
_setCacheBoxes.remove(coin);
}
return _setCacheBoxes[coin] ??=
await Hive.openBox<dynamic>(_boxNameSetCache(coin: coin));
}
Future<Box<dynamic>> getSparkAnonymitySetCacheBox(
{required Coin coin}) async {
if (_setSparkCacheBoxes[coin]?.isOpen != true) {
_setSparkCacheBoxes.remove(coin);
}
return _setSparkCacheBoxes[coin] ??=
await Hive.openBox<dynamic>(_boxNameSetSparkCache(coin: coin));
}
Future<void> closeAnonymitySetCacheBox({required Coin coin}) async {
await _setCacheBoxes[coin]?.close();
}
Future<Box<dynamic>> getUsedSerialsCacheBox({required Coin coin}) async {
if (_usedSerialsCacheBoxes[coin]?.isOpen != true) {
_usedSerialsCacheBoxes.remove(coin);
}
return _usedSerialsCacheBoxes[coin] ??=
await Hive.openBox<dynamic>(_boxNameUsedSerialsCache(coin: coin));
}
Future<Box<dynamic>> getSparkUsedCoinsTagsCacheBox(
{required Coin coin}) async {
if (_getSparkUsedCoinsTagsCacheBoxes[coin]?.isOpen != true) {
_getSparkUsedCoinsTagsCacheBoxes.remove(coin);
}
return _getSparkUsedCoinsTagsCacheBoxes[coin] ??=
await Hive.openBox<dynamic>(
_boxNameSparkUsedCoinsTagsCache(coin: coin));
}
Future<void> closeUsedSerialsCacheBox({required Coin coin}) async {
await _usedSerialsCacheBoxes[coin]?.close();
}
@ -206,9 +241,12 @@ class DB {
/// Clear all cached transactions for the specified coin
Future<void> clearSharedTransactionCache({required Coin coin}) async {
await deleteAll<dynamic>(boxName: _boxNameTxCache(coin: coin));
if (coin == Coin.firo) {
if (coin == Coin.firo || coin == Coin.firoTestNet) {
await deleteAll<dynamic>(boxName: _boxNameSetCache(coin: coin));
await deleteAll<dynamic>(boxName: _boxNameSetSparkCache(coin: coin));
await deleteAll<dynamic>(boxName: _boxNameUsedSerialsCache(coin: coin));
await deleteAll<dynamic>(
boxName: _boxNameSparkUsedCoinsTagsCache(coin: coin));
}
}
@ -265,8 +303,12 @@ class DB {
{required dynamic key, required String boxName}) async =>
await mutex.protect(() async => await Hive.box<T>(boxName).delete(key));
Future<void> deleteAll<T>({required String boxName}) async =>
await mutex.protect(() async => await Hive.box<T>(boxName).clear());
Future<void> deleteAll<T>({required String boxName}) async {
await mutex.protect(() async {
final box = await Hive.openBox<T>(boxName);
await box.clear();
});
}
Future<void> deleteBoxFromDisk({required String boxName}) async =>
await mutex.protect(() async => await Hive.deleteBoxFromDisk(boxName));
@ -308,5 +350,4 @@ abstract class DBKeys {
static const String isFavorite = "isFavorite";
static const String id = "id";
static const String storedChainHeight = "storedChainHeight";
static const String ethTokenContracts = "ethTokenContracts";
}

View file

@ -13,12 +13,18 @@ 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/models/isar/models/block_explorer.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart';
import 'package:stackwallet/models/isar/models/contact_entry.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/isar/ordinal.dart';
import 'package:stackwallet/models/isar/stack_theme.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/stack_file_system.dart';
import 'package:stackwallet/wallets/isar/models/spark_coin.dart';
import 'package:stackwallet/wallets/isar/models/token_wallet_info.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info_meta.dart';
import 'package:tuple/tuple.dart';
part '../queries/queries.dart';
@ -54,6 +60,13 @@ class MainDB {
TransactionBlockExplorerSchema,
StackThemeSchema,
ContactEntrySchema,
OrdinalSchema,
LelantusCoinSchema,
WalletInfoSchema,
TransactionV2Schema,
SparkCoinSchema,
WalletInfoMetaSchema,
TokenWalletInfoSchema,
],
directory: (await StackFileSystem.applicationIsarDirectory()).path,
// inspector: kDebugMode,
@ -64,6 +77,36 @@ class MainDB {
return true;
}
Future<void> putWalletInfo(WalletInfo walletInfo) async {
try {
await isar.writeTxn(() async {
await isar.walletInfo.put(walletInfo);
});
} catch (e) {
throw MainDBException("failed putWalletInfo()", e);
}
}
Future<void> updateWalletInfo(WalletInfo walletInfo) async {
try {
await isar.writeTxn(() async {
final info = await isar.walletInfo
.where()
.walletIdEqualTo(walletInfo.walletId)
.findFirst();
if (info == null) {
throw Exception("updateWalletInfo() called with new WalletInfo."
" Use putWalletInfo()");
}
await isar.walletInfo.deleteByWalletId(walletInfo.walletId);
await isar.walletInfo.put(walletInfo);
});
} catch (e) {
throw MainDBException("failed updateWalletInfo()", e);
}
}
// contact entries
List<ContactEntry> getContactEntries() {
return isar.contactEntrys.where().sortByName().findAllSync();
@ -238,6 +281,14 @@ class MainDB {
QueryBuilder<UTXO, UTXO, QAfterWhereClause> getUTXOs(String walletId) =>
isar.utxos.where().walletIdEqualTo(walletId);
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> getUTXOsByAddress(
String walletId, String address) =>
isar.utxos
.where()
.walletIdEqualTo(walletId)
.filter()
.addressEqualTo(address);
Future<void> putUTXO(UTXO utxo) => isar.writeTxn(() async {
await isar.utxos.put(utxo);
});
@ -246,7 +297,8 @@ class MainDB {
await isar.utxos.putAll(utxos);
});
Future<void> updateUTXOs(String walletId, List<UTXO> utxos) async {
Future<bool> updateUTXOs(String walletId, List<UTXO> utxos) async {
bool newUTXO = false;
await isar.writeTxn(() async {
final set = utxos.toSet();
for (final utxo in utxos) {
@ -268,12 +320,16 @@ class MainDB {
blockHash: utxo.blockHash,
),
);
} else {
newUTXO = true;
}
}
await isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await isar.utxos.putAll(set.toList());
});
return newUTXO;
}
Stream<UTXO?> watchUTXO({
@ -368,8 +424,12 @@ class MainDB {
//
Future<void> deleteWalletBlockchainData(String walletId) async {
final transactionCount = await getTransactions(walletId).count();
final transactionCountV2 =
await isar.transactionV2s.where().walletIdEqualTo(walletId).count();
final addressCount = await getAddresses(walletId).count();
final utxoCount = await getUTXOs(walletId).count();
// final lelantusCoinCount =
// await isar.lelantusCoins.where().walletIdEqualTo(walletId).count();
await isar.writeTxn(() async {
const paginateLimit = 50;
@ -384,6 +444,18 @@ class MainDB {
await isar.transactions.deleteAll(txnIds);
}
// transactions V2
for (int i = 0; i < transactionCountV2; i += paginateLimit) {
final txnIds = await isar.transactionV2s
.where()
.walletIdEqualTo(walletId)
.offset(i)
.limit(paginateLimit)
.idProperty()
.findAll();
await isar.transactionV2s.deleteAll(txnIds);
}
// addresses
for (int i = 0; i < addressCount; i += paginateLimit) {
final addressIds = await getAddresses(walletId)
@ -403,6 +475,25 @@ class MainDB {
.findAll();
await isar.utxos.deleteAll(utxoIds);
}
// lelantusCoins
await isar.lelantusCoins.where().walletIdEqualTo(walletId).deleteAll();
// for (int i = 0; i < lelantusCoinCount; i += paginateLimit) {
// final lelantusCoinIds = await isar.lelantusCoins
// .where()
// .walletIdEqualTo(walletId)
// .offset(i)
// .limit(paginateLimit)
// .idProperty()
// .findAll();
// await isar.lelantusCoins.deleteAll(lelantusCoinIds);
// }
// spark coins
await isar.sparkCoins
.where()
.walletIdEqualToAnyLTagHash(walletId)
.deleteAll();
});
}
@ -476,6 +567,36 @@ class MainDB {
}
}
Future<List<int>> updateOrPutTransactionV2s(
List<TransactionV2> transactions,
) async {
try {
List<int> ids = [];
await isar.writeTxn(() async {
for (final tx in transactions) {
final storedTx = await isar.transactionV2s
.where()
.txidWalletIdEqualTo(tx.txid, tx.walletId)
.findFirst();
Id id;
if (storedTx == null) {
id = await isar.transactionV2s.put(tx);
} else {
tx.id = storedTx.id;
await isar.transactionV2s.delete(storedTx.id);
id = await isar.transactionV2s.put(tx);
}
ids.add(id);
}
});
return ids;
} catch (e) {
throw MainDBException(
"failed updateOrPutTransactionV2s: $transactions", e);
}
}
// ========== Ethereum =======================================================
// eth contracts
@ -497,4 +618,15 @@ class MainDB {
isar.writeTxn(() async {
await isar.ethContracts.putAll(contracts);
});
// ========== Lelantus =======================================================
Future<int?> getHighestUsedMintIndex({required String walletId}) async {
return await isar.lelantusCoins
.where()
.walletIdEqualTo(walletId)
.sortByMintIndexDesc()
.mintIndexProperty()
.findFirst();
}
}

View file

@ -0,0 +1,214 @@
import 'dart:convert';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/wallets/isar/models/token_wallet_info.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info_meta.dart';
import 'package:stackwallet/wallets/wallet/supporting/epiccash_wallet_info_extension.dart';
Future<void> migrateWalletsToIsar({
required SecureStorageInterface secureStore,
}) async {
await MainDB.instance.initMainDB();
// ensure fresh
await MainDB.instance.isar
.writeTxn(() async => await MainDB.instance.isar.transactionV2s.clear());
final allWalletsBox = await Hive.openBox<dynamic>(DB.boxNameAllWalletsData);
final names = DB.instance
.get<dynamic>(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?;
if (names == null) {
// no wallets to migrate
return;
}
//
// Parse the old data from the Hive map into a nice list
//
final List<
({
Coin coin,
String name,
String walletId,
})> oldInfo = Map<String, dynamic>.from(names).values.map((e) {
final map = e as Map;
return (
coin: Coin.values.byName(map["coin"] as String),
walletId: map["id"] as String,
name: map["name"] as String,
);
}).toList();
//
// Get current ordered list of favourite wallet Ids
//
final List<String> favourites =
(await Hive.openBox<String>(DB.boxNameFavoriteWallets)).values.toList();
final List<(WalletInfo, WalletInfoMeta)> newInfo = [];
final List<TokenWalletInfo> tokenInfo = [];
final List<TransactionNote> migratedNotes = [];
//
// Convert each old info into the new Isar WalletInfo
//
for (final old in oldInfo) {
final walletBox = await Hive.openBox<dynamic>(old.walletId);
//
// First handle transaction notes
//
final newNoteCount = await MainDB.instance.isar.transactionNotes
.where()
.walletIdEqualTo(old.walletId)
.count();
if (newNoteCount == 0) {
final map = walletBox.get('notes') as Map?;
if (map != null) {
final notes = Map<String, String>.from(map);
for (final txid in notes.keys) {
final note = notes[txid];
if (note != null && note.isNotEmpty) {
final newNote = TransactionNote(
walletId: old.walletId,
txid: txid,
value: note,
);
migratedNotes.add(newNote);
}
}
}
}
// reset stellar + tezos address type
if (old.coin == Coin.stellar ||
old.coin == Coin.stellarTestnet ||
old.coin == Coin.tezos) {
await MainDB.instance.deleteWalletBlockchainData(old.walletId);
}
//
// Set other data values
//
Map<String, dynamic> otherData = {};
final List<String>? tokenContractAddresses = walletBox.get(
"ethTokenContracts",
) as List<String>?;
if (tokenContractAddresses?.isNotEmpty == true) {
otherData[WalletInfoKeys.tokenContractAddresses] = tokenContractAddresses;
for (final address in tokenContractAddresses!) {
final contract = await MainDB.instance.isar.ethContracts
.where()
.addressEqualTo(address)
.findFirst();
if (contract != null) {
tokenInfo.add(
TokenWalletInfo(
walletId: old.walletId,
tokenAddress: address,
tokenFractionDigits: contract.decimals,
),
);
}
}
}
// epiccash specifics
if (old.coin == Coin.epicCash) {
final epicWalletInfo = ExtraEpiccashWalletInfo.fromMap({
"receivingIndex": walletBox.get("receivingIndex") as int? ?? 0,
"changeIndex": walletBox.get("changeIndex") as int? ?? 0,
"slatesToAddresses": walletBox.get("slate_to_address") as Map? ?? {},
"slatesToCommits": walletBox.get("slatesToCommits") as Map? ?? {},
"lastScannedBlock": walletBox.get("lastScannedBlock") as int? ?? 0,
"restoreHeight": walletBox.get("restoreHeight") as int? ?? 0,
"creationHeight": walletBox.get("creationHeight") as int? ?? 0,
});
otherData[WalletInfoKeys.epiccashData] = jsonEncode(
epicWalletInfo.toMap(),
);
} else if (old.coin == Coin.firo || old.coin == Coin.firoTestNet) {
otherData[WalletInfoKeys.lelantusCoinIsarRescanRequired] = walletBox
.get(WalletInfoKeys.lelantusCoinIsarRescanRequired) as bool? ??
true;
}
//
// Clear out any keys with null values as they are not needed
//
otherData.removeWhere((key, value) => value == null);
final infoMeta = WalletInfoMeta(
walletId: old.walletId,
isMnemonicVerified: allWalletsBox
.get("${old.walletId}_mnemonicHasBeenVerified") as bool? ??
false,
);
final info = WalletInfo(
coinName: old.coin.name,
walletId: old.walletId,
name: old.name,
mainAddressType: old.coin.primaryAddressType,
favouriteOrderIndex: favourites.indexOf(old.walletId),
cachedChainHeight: walletBox.get(
DBKeys.storedChainHeight,
) as int? ??
0,
cachedBalanceString: walletBox.get(
DBKeys.cachedBalance,
) as String?,
cachedBalanceSecondaryString: walletBox.get(
DBKeys.cachedBalanceSecondary,
) as String?,
otherDataJsonString: jsonEncode(otherData),
);
newInfo.add((info, infoMeta));
}
if (migratedNotes.isNotEmpty) {
await MainDB.instance.isar.writeTxn(() async {
await MainDB.instance.isar.transactionNotes.putAll(migratedNotes);
});
}
if (newInfo.isNotEmpty) {
await MainDB.instance.isar.writeTxn(() async {
await MainDB.instance.isar.walletInfo
.putAll(newInfo.map((e) => e.$1).toList());
await MainDB.instance.isar.walletInfoMeta
.putAll(newInfo.map((e) => e.$2).toList());
if (tokenInfo.isNotEmpty) {
await MainDB.instance.isar.tokenWalletInfo.putAll(tokenInfo);
}
});
}
await _cleanupOnSuccess(
walletIds: newInfo.map((e) => e.$1.walletId).toList());
}
Future<void> _cleanupOnSuccess({required List<String> walletIds}) async {
await Hive.deleteBoxFromDisk(DB.boxNameFavoriteWallets);
await Hive.deleteBoxFromDisk(DB.boxNameAllWalletsData);
for (final walletId in walletIds) {
await Hive.deleteBoxFromDisk(walletId);
}
}

View file

@ -26,7 +26,6 @@ class EthTokenTxDto {
required this.topics,
required this.data,
required this.articulatedLog,
required this.compressedLog,
required this.transactionHash,
required this.transactionIndex,
});
@ -44,7 +43,6 @@ class EthTokenTxDto {
map['articulatedLog'] as Map,
),
),
compressedLog = map['compressedLog'] as String,
transactionHash = map['transactionHash'] as String,
transactionIndex = map['transactionIndex'] as int;
@ -54,7 +52,6 @@ class EthTokenTxDto {
final List<String> topics;
final String data;
final ArticulatedLog? articulatedLog;
final String compressedLog;
final String transactionHash;
final int transactionIndex;
@ -76,7 +73,6 @@ class EthTokenTxDto {
topics: topics ?? this.topics,
data: data ?? this.data,
articulatedLog: articulatedLog ?? this.articulatedLog,
compressedLog: compressedLog ?? this.compressedLog,
transactionHash: transactionHash ?? this.transactionHash,
transactionIndex: transactionIndex ?? this.transactionIndex,
);
@ -89,7 +85,6 @@ class EthTokenTxDto {
map['topics'] = topics;
map['data'] = data;
map['articulatedLog'] = articulatedLog?.toMap();
map['compressedLog'] = compressedLog;
map['transactionHash'] = transactionHash;
map['transactionIndex'] = transactionIndex;
return map;

View file

@ -29,7 +29,6 @@ class EthTxDTO {
required this.maxPriorityFeePerGas,
required this.isError,
required this.hasToken,
required this.compressedTx,
required this.gasCost,
required this.gasUsed,
});
@ -42,16 +41,15 @@ class EthTxDTO {
timestamp: map['timestamp'] as int,
from: map['from'] as String,
to: map['to'] as String,
value: _amountFromJsonNum(map['value']),
gas: _amountFromJsonNum(map['gas']),
gasPrice: _amountFromJsonNum(map['gasPrice']),
value: _amountFromJsonNum(map['value'])!,
gas: _amountFromJsonNum(map['gas'])!,
gasPrice: _amountFromJsonNum(map['gasPrice'])!,
maxFeePerGas: _amountFromJsonNum(map['maxFeePerGas']),
maxPriorityFeePerGas: _amountFromJsonNum(map['maxPriorityFeePerGas']),
isError: map['isError'] as int,
hasToken: map['hasToken'] as int,
compressedTx: map['compressedTx'] as String,
gasCost: _amountFromJsonNum(map['gasCost']),
gasUsed: _amountFromJsonNum(map['gasUsed']),
isError: map['isError'] as bool? ?? false,
hasToken: map['hasToken'] as bool? ?? false,
gasCost: _amountFromJsonNum(map['gasCost'])!,
gasUsed: _amountFromJsonNum(map['gasUsed'])!,
);
final String hash;
@ -64,17 +62,19 @@ class EthTxDTO {
final Amount value;
final Amount gas;
final Amount gasPrice;
final Amount maxFeePerGas;
final Amount maxPriorityFeePerGas;
final int isError;
final int hasToken;
final String compressedTx;
final Amount? maxFeePerGas;
final Amount? maxPriorityFeePerGas;
final bool isError;
final bool hasToken;
final Amount gasCost;
final Amount gasUsed;
static Amount _amountFromJsonNum(dynamic json) {
static Amount? _amountFromJsonNum(dynamic json) {
if (json == null) {
return null;
}
return Amount(
rawValue: BigInt.from(json as num),
rawValue: BigInt.parse(json.toString()),
fractionDigits: Coin.ethereum.decimals,
);
}
@ -92,8 +92,8 @@ class EthTxDTO {
Amount? gasPrice,
Amount? maxFeePerGas,
Amount? maxPriorityFeePerGas,
int? isError,
int? hasToken,
bool? isError,
bool? hasToken,
String? compressedTx,
Amount? gasCost,
Amount? gasUsed,
@ -113,7 +113,6 @@ class EthTxDTO {
maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas,
isError: isError ?? this.isError,
hasToken: hasToken ?? this.hasToken,
compressedTx: compressedTx ?? this.compressedTx,
gasCost: gasCost ?? this.gasCost,
gasUsed: gasUsed ?? this.gasUsed,
);
@ -134,7 +133,6 @@ class EthTxDTO {
map['maxPriorityFeePerGas'] = maxPriorityFeePerGas.toString();
map['isError'] = isError;
map['hasToken'] = hasToken;
map['compressedTx'] = compressedTx;
map['gasCost'] = gasCost.toString();
map['gasUsed'] = gasUsed.toString();
return map;

View file

@ -0,0 +1,39 @@
import 'package:stackwallet/dto/ordinals/litescribe_response.dart';
import 'package:stackwallet/dto/ordinals/inscription_data.dart';
class AddressInscriptionResponse extends LitescribeResponse<AddressInscriptionResponse> {
final int status;
final String message;
final AddressInscriptionResult result;
AddressInscriptionResponse({
required this.status,
required this.message,
required this.result,
});
factory AddressInscriptionResponse.fromJson(Map<String, dynamic> json) {
return AddressInscriptionResponse(
status: json['status'] as int,
message: json['message'] as String,
result: AddressInscriptionResult.fromJson(json['result'] as Map<String, dynamic>),
);
}
}
class AddressInscriptionResult {
final List<InscriptionData> list;
final int total;
AddressInscriptionResult({
required this.list,
required this.total,
});
factory AddressInscriptionResult.fromJson(Map<String, dynamic> json) {
return AddressInscriptionResult(
list: (json['list'] as List).map((item) => InscriptionData.fromJson(item as Map<String, dynamic>)).toList(),
total: json['total'] as int,
);
}
}

View file

@ -0,0 +1,73 @@
// inscription data from litescribe /address/inscriptions endpoint
class InscriptionData {
final String inscriptionId;
final int inscriptionNumber;
final String address;
final String preview;
final String content;
final int contentLength;
final String contentType;
final String contentBody;
final int timestamp;
final String genesisTransaction;
final String location;
final String output;
final int outputValue;
final int offset;
InscriptionData({
required this.inscriptionId,
required this.inscriptionNumber,
required this.address,
required this.preview,
required this.content,
required this.contentLength,
required this.contentType,
required this.contentBody,
required this.timestamp,
required this.genesisTransaction,
required this.location,
required this.output,
required this.outputValue,
required this.offset,
});
factory InscriptionData.fromJson(Map<String, dynamic> json) {
return InscriptionData(
inscriptionId: json['inscriptionId'] as String,
inscriptionNumber: json['inscriptionNumber'] as int,
address: json['address'] as String,
preview: json['preview'] as String,
content: json['content'] as String,
contentLength: json['contentLength'] as int,
contentType: json['contentType'] as String,
contentBody: json['contentBody'] as String,
timestamp: json['timestamp'] as int,
genesisTransaction: json['genesisTransaction'] as String,
location: json['location'] as String,
output: json['output'] as String,
outputValue: json['outputValue'] as int,
offset: json['offset'] as int,
);
}
@override
String toString() {
return 'InscriptionData {'
' inscriptionId: $inscriptionId,'
' inscriptionNumber: $inscriptionNumber,'
' address: $address,'
' preview: $preview,'
' content: $content,'
' contentLength: $contentLength,'
' contentType: $contentType,'
' contentBody: $contentBody,'
' timestamp: $timestamp,'
' genesisTransaction: $genesisTransaction,'
' location: $location,'
' output: $output,'
' outputValue: $outputValue,'
' offset: $offset'
' }';
}
}

View file

@ -0,0 +1,6 @@
class LitescribeResponse<T> {
final T? data;
final String? error;
LitescribeResponse({this.data, this.error});
}

View file

@ -9,26 +9,27 @@
*/
import 'dart:convert';
import 'dart:math';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:string_validator/string_validator.dart';
class CachedElectrumX {
final ElectrumX electrumXClient;
class CachedElectrumXClient {
final ElectrumXClient electrumXClient;
static const minCacheConfirms = 30;
const CachedElectrumX({
const CachedElectrumXClient({
required this.electrumXClient,
});
factory CachedElectrumX.from({
required ElectrumX electrumXClient,
factory CachedElectrumXClient.from({
required ElectrumXClient electrumXClient,
}) =>
CachedElectrumX(
CachedElectrumXClient(
electrumXClient: electrumXClient,
);
@ -55,7 +56,7 @@ class CachedElectrumX {
set = Map<String, dynamic>.from(cachedSet);
}
final newSet = await electrumXClient.getAnonymitySet(
final newSet = await electrumXClient.getLelantusAnonymitySet(
groupId: groupId,
blockhash: set["blockHash"] as String,
);
@ -106,6 +107,63 @@ class CachedElectrumX {
}
}
Future<Map<String, dynamic>> getSparkAnonymitySet({
required String groupId,
String blockhash = "",
required Coin coin,
}) async {
try {
final box = await DB.instance.getSparkAnonymitySetCacheBox(coin: coin);
final cachedSet = box.get(groupId) as Map?;
Map<String, dynamic> set;
// null check to see if there is a cached set
if (cachedSet == null) {
set = {
"coinGroupID": int.parse(groupId),
"blockHash": blockhash,
"setHash": "",
"coins": <dynamic>[],
};
} else {
set = Map<String, dynamic>.from(cachedSet);
}
final newSet = await electrumXClient.getSparkAnonymitySet(
coinGroupId: groupId,
startBlockHash: set["blockHash"] as String,
);
// update set with new data
if (newSet["setHash"] != "" && set["setHash"] != newSet["setHash"]) {
set["setHash"] = newSet["setHash"];
set["blockHash"] = newSet["blockHash"];
for (int i = (newSet["coins"] as List).length - 1; i >= 0; i--) {
// TODO verify this is correct (or append?)
if ((set["coins"] as List)
.where((e) => e[0] == newSet["coins"][i][0])
.isEmpty) {
set["coins"].insert(0, newSet["coins"][i]);
}
}
// save set to db
await box.put(groupId, set);
Logging.instance.log(
"Updated current anonymity set for ${coin.name} with group ID $groupId",
level: LogLevel.Info,
);
}
return set;
} catch (e, s) {
Logging.instance.log(
"Failed to process CachedElectrumX.getSparkAnonymitySet(): $e\n$s",
level: LogLevel.Error);
rethrow;
}
}
String base64ToHex(String source) =>
base64Decode(LineSplitter.split(source).join())
.map((e) => e.toRadixString(16).padLeft(2, '0'))
@ -135,16 +193,17 @@ class CachedElectrumX {
result.remove("hex");
result.remove("lelantusData");
result.remove("sparkData");
if (result["confirmations"] != null &&
result["confirmations"] as int > minCacheConfirms) {
await box.put(txHash, result);
}
Logging.instance.log("using fetched result", level: LogLevel.Info);
// Logging.instance.log("using fetched result", level: LogLevel.Info);
return result;
} else {
Logging.instance.log("using cached result", level: LogLevel.Info);
// Logging.instance.log("using cached result", level: LogLevel.Info);
return Map<String, dynamic>.from(cachedTx);
}
} catch (e, s) {
@ -164,40 +223,95 @@ class CachedElectrumX {
final _list = box.get("serials") as List?;
List<String> cachedSerials =
_list == null ? [] : List<String>.from(_list);
Set<String> cachedSerials =
_list == null ? {} : List<String>.from(_list).toSet();
final startNumber = cachedSerials.length;
startNumber = max(
max(0, startNumber),
cachedSerials.length - 100, // 100 being some arbitrary buffer
);
final serials =
await electrumXClient.getUsedCoinSerials(startNumber: startNumber);
List<String> newSerials = [];
final serials = await electrumXClient.getLelantusUsedCoinSerials(
startNumber: startNumber,
);
for (final element in (serials["serials"] as List)) {
if (!isHexadecimal(element as String)) {
newSerials.add(base64ToHex(element));
} else {
newSerials.add(element);
}
final newSerials = List<String>.from(serials["serials"] as List)
.map((e) => !isHexadecimal(e) ? base64ToHex(e) : e)
.toSet();
// ensure we are getting some overlap so we know we are not missing any
if (cachedSerials.isNotEmpty && newSerials.isNotEmpty) {
assert(cachedSerials.intersection(newSerials).isNotEmpty);
}
cachedSerials.addAll(newSerials);
final resultingList = cachedSerials.toList();
await box.put(
"serials",
cachedSerials,
resultingList,
);
return cachedSerials;
return resultingList;
} catch (e, s) {
Logging.instance.log(
"Failed to process CachedElectrumX.getTransaction(): $e\n$s",
level: LogLevel.Error);
"Failed to process CachedElectrumX.getUsedCoinSerials(): $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
Future<Set<String>> getSparkUsedCoinsTags({
required Coin coin,
}) async {
try {
final box = await DB.instance.getSparkUsedCoinsTagsCacheBox(coin: coin);
final _list = box.get("tags") as List?;
Set<String> cachedTags =
_list == null ? {} : List<String>.from(_list).toSet();
final startNumber = max(
0,
cachedTags.length - 100, // 100 being some arbitrary buffer
);
final tags = await electrumXClient.getSparkUsedCoinsTags(
startNumber: startNumber,
);
// final newSerials = List<String>.from(serials["serials"] as List)
// .map((e) => !isHexadecimal(e) ? base64ToHex(e) : e)
// .toSet();
// ensure we are getting some overlap so we know we are not missing any
if (cachedTags.isNotEmpty && tags.isNotEmpty) {
assert(cachedTags.intersection(tags).isNotEmpty);
}
cachedTags.addAll(tags);
await box.put(
"tags",
cachedTags.toList(),
);
return cachedTags;
} catch (e, s) {
Logging.instance.log(
"Failed to process CachedElectrumX.getSparkUsedCoinsTags(): $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
/// Clear all cached transactions for the specified coin
Future<void> clearSharedTransactionCache({required Coin coin}) async {
await DB.instance.clearSharedTransactionCache(coin: coin);
await DB.instance.closeAnonymitySetCacheBox(coin: coin);
}
}

View file

@ -8,13 +8,22 @@
*
*/
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:decimal/decimal.dart';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
import 'package:mutex/mutex.dart';
import 'package:stackwallet/electrumx_rpc/rpc.dart';
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:uuid/uuid.dart';
@ -51,7 +60,7 @@ class ElectrumXNode {
}
}
class ElectrumX {
class ElectrumXClient {
String get host => _host;
late String _host;
@ -65,36 +74,109 @@ class ElectrumX {
JsonRPC? _rpcClient;
late Prefs _prefs;
late TorService _torService;
List<ElectrumXNode>? failovers;
int currentFailoverIndex = -1;
ElectrumX(
{required String host,
required int port,
required bool useSSL,
required Prefs prefs,
required List<ElectrumXNode> failovers,
JsonRPC? client}) {
final Duration connectionTimeoutForSpecialCaseJsonRPCClients;
// add finalizer to cancel stream subscription when all references to an
// instance of ElectrumX becomes inaccessible
static final Finalizer<ElectrumXClient> _finalizer = Finalizer(
(p0) {
p0._torPreferenceListener?.cancel();
p0._torStatusListener?.cancel();
},
);
StreamSubscription<TorPreferenceChangedEvent>? _torPreferenceListener;
StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;
final Mutex _torConnectingLock = Mutex();
bool _requireMutex = false;
ElectrumXClient({
required String host,
required int port,
required bool useSSL,
required Prefs prefs,
required List<ElectrumXNode> failovers,
JsonRPC? client,
this.connectionTimeoutForSpecialCaseJsonRPCClients =
const Duration(seconds: 60),
TorService? torService,
EventBus? globalEventBusForTesting,
}) {
_prefs = prefs;
_torService = torService ?? TorService.sharedInstance;
_host = host;
_port = port;
_useSSL = useSSL;
_rpcClient = client;
final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
_torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen(
(event) async {
switch (event.newStatus) {
case TorConnectionStatus.connecting:
await _torConnectingLock.acquire();
_requireMutex = true;
break;
case TorConnectionStatus.connected:
case TorConnectionStatus.disconnected:
if (_torConnectingLock.isLocked) {
_torConnectingLock.release();
}
_requireMutex = false;
break;
}
},
);
_torPreferenceListener = bus.on<TorPreferenceChangedEvent>().listen(
(event) async {
// not sure if we need to do anything specific here
// switch (event.status) {
// case TorStatus.enabled:
// case TorStatus.disabled:
// }
// might be ok to just reset/kill the current _jsonRpcClient
// since disconnecting is async and we want to ensure instant change over
// we will keep temp reference to current rpc client to call disconnect
// on before awaiting the disconnection future
final temp = _rpcClient;
// setting to null should force the creation of a new json rpc client
// on the next request sent through this electrumx instance
_rpcClient = null;
await temp?.disconnect(
reason: "Tor status changed to \"${event.status}\"",
);
},
);
}
factory ElectrumX.from({
factory ElectrumXClient.from({
required ElectrumXNode node,
required Prefs prefs,
required List<ElectrumXNode> failovers,
}) =>
ElectrumX(
host: node.address,
port: node.port,
useSSL: node.useSSL,
prefs: prefs,
failovers: failovers,
);
TorService? torService,
EventBus? globalEventBusForTesting,
}) {
return ElectrumXClient(
host: node.address,
port: node.port,
useSSL: node.useSSL,
prefs: prefs,
torService: torService,
failovers: failovers,
globalEventBusForTesting: globalEventBusForTesting,
);
}
Future<bool> _allow() async {
if (_prefs.wifiOnly) {
@ -104,16 +186,54 @@ class ElectrumX {
return true;
}
/// Send raw rpc command
Future<dynamic> request({
required String command,
List<dynamic> args = const [],
Duration connectionTimeout = const Duration(seconds: 60),
String? requestID,
int retries = 2,
}) async {
if (!(await _allow())) {
throw WifiOnlyException();
void _checkRpcClient() {
// If we're supposed to use Tor...
if (_prefs.useTor) {
// But Tor isn't running...
if (_torService.status != TorConnectionStatus.connected) {
// And the killswitch isn't set...
if (!_prefs.torKillSwitch) {
// Then we'll just proceed and connect to ElectrumX through clearnet at the bottom of this function.
Logging.instance.log(
"Tor preference set but Tor is not enabled, killswitch not set, connecting to ElectrumX through clearnet",
level: LogLevel.Warning,
);
} else {
// ... But if the killswitch is set, then we throw an exception.
throw Exception(
"Tor preference and killswitch set but Tor is not enabled, not connecting to ElectrumX");
}
} else {
// Get the proxy info from the TorService.
final proxyInfo = _torService.getProxyInfo();
if (currentFailoverIndex == -1) {
_rpcClient ??= JsonRPC(
host: host,
port: port,
useSSL: useSSL,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
proxyInfo: proxyInfo,
);
} else {
_rpcClient ??= JsonRPC(
host: failovers![currentFailoverIndex].address,
port: failovers![currentFailoverIndex].port,
useSSL: failovers![currentFailoverIndex].useSSL,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
proxyInfo: proxyInfo,
);
}
if (_rpcClient!.proxyInfo != proxyInfo) {
_rpcClient!.proxyInfo = proxyInfo;
_rpcClient!.disconnect(
reason: "Tor proxyInfo does not match current info",
);
}
return;
}
}
if (currentFailoverIndex == -1) {
@ -121,26 +241,52 @@ class ElectrumX {
host: host,
port: port,
useSSL: useSSL,
connectionTimeout: connectionTimeout,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
proxyInfo: null,
);
} else {
_rpcClient = JsonRPC(
_rpcClient ??= JsonRPC(
host: failovers![currentFailoverIndex].address,
port: failovers![currentFailoverIndex].port,
useSSL: failovers![currentFailoverIndex].useSSL,
connectionTimeout: connectionTimeout,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
proxyInfo: null,
);
}
}
/// Send raw rpc command
Future<dynamic> request({
required String command,
List<dynamic> args = const [],
String? requestID,
int retries = 2,
Duration requestTimeout = const Duration(seconds: 60),
}) async {
if (!(await _allow())) {
throw WifiOnlyException();
}
if (_requireMutex) {
await _torConnectingLock.protect(() async => _checkRpcClient());
} else {
_checkRpcClient();
}
try {
final requestId = requestID ?? const Uuid().v1();
final jsonArgs = json.encode(args);
final jsonRequestString =
'{"jsonrpc": "2.0", "id": "$requestId","method": "$command","params": $jsonArgs}';
final jsonRequestString = '{"jsonrpc": "2.0", '
'"id": "$requestId",'
'"method": "$command",'
'"params": $jsonArgs}';
// Logging.instance.log("ElectrumX jsonRequestString: $jsonRequestString");
final response = await _rpcClient!.request(jsonRequestString);
final response = await _rpcClient!.request(
jsonRequestString,
requestTimeout,
);
if (response.exception != null) {
throw response.exception!;
@ -159,8 +305,8 @@ class ElectrumX {
throw Exception(
"JSONRPC response\n"
" command: $command\n"
" args: $args\n"
" error: ${response.data}",
" error: ${response.data}"
" args: $args\n",
);
}
@ -174,7 +320,7 @@ class ElectrumX {
return request(
command: command,
args: args,
connectionTimeout: connectionTimeout,
requestTimeout: requestTimeout,
requestID: requestID,
retries: retries - 1,
);
@ -187,7 +333,7 @@ class ElectrumX {
return request(
command: command,
args: args,
connectionTimeout: connectionTimeout,
requestTimeout: requestTimeout,
requestID: requestID,
);
} else {
@ -204,27 +350,17 @@ class ElectrumX {
Future<List<Map<String, dynamic>>> batchRequest({
required String command,
required Map<String, List<dynamic>> args,
Duration connectionTimeout = const Duration(seconds: 60),
Duration requestTimeout = const Duration(seconds: 60),
int retries = 2,
}) async {
if (!(await _allow())) {
throw WifiOnlyException();
}
if (currentFailoverIndex == -1) {
_rpcClient ??= JsonRPC(
host: host,
port: port,
useSSL: useSSL,
connectionTimeout: connectionTimeout,
);
if (_requireMutex) {
await _torConnectingLock.protect(() async => _checkRpcClient());
} else {
_rpcClient = JsonRPC(
host: failovers![currentFailoverIndex].address,
port: failovers![currentFailoverIndex].port,
useSSL: failovers![currentFailoverIndex].useSSL,
connectionTimeout: connectionTimeout,
);
_checkRpcClient();
}
try {
@ -246,13 +382,21 @@ class ElectrumX {
// Logging.instance.log("batch request: $request");
// send batch request
final jsonRpcResponse = (await _rpcClient!.request(request));
final jsonRpcResponse =
(await _rpcClient!.request(request, requestTimeout));
if (jsonRpcResponse.exception != null) {
throw jsonRpcResponse.exception!;
}
final response = jsonRpcResponse.data as List;
final List<dynamic> response;
try {
response = jsonRpcResponse.data as List;
} catch (_) {
throw Exception(
"Expected json list but got a map: ${jsonRpcResponse.data}",
);
}
// check for errors, format and throw if there are any
final List<String> errors = [];
@ -281,7 +425,7 @@ class ElectrumX {
return batchRequest(
command: command,
args: args,
connectionTimeout: connectionTimeout,
requestTimeout: requestTimeout,
retries: retries - 1,
);
} else {
@ -293,7 +437,7 @@ class ElectrumX {
return batchRequest(
command: command,
args: args,
connectionTimeout: connectionTimeout,
requestTimeout: requestTimeout,
);
} else {
currentFailoverIndex = -1;
@ -310,7 +454,7 @@ class ElectrumX {
final response = await request(
requestID: requestID,
command: 'server.ping',
connectionTimeout: const Duration(seconds: 2),
requestTimeout: const Duration(seconds: 2),
retries: retryCount,
).timeout(const Duration(seconds: 2)) as Map<String, dynamic>;
return response.keys.contains("result") && response["result"] == null;
@ -325,9 +469,9 @@ class ElectrumX {
/// and the binary header as a hexadecimal string.
/// Ex:
/// {
// "height": 520481,
// "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4"
// }
/// "height": 520481,
/// "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4"
/// }
Future<Map<String, dynamic>> getBlockHeadTip({String? requestID}) async {
try {
final response = await request(
@ -351,15 +495,15 @@ class ElectrumX {
///
/// Returns a map with server information
/// Ex:
// {
// "genesis_hash": "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943",
// "hosts": {"14.3.140.101": {"tcp_port": 51001, "ssl_port": 51002}},
// "protocol_max": "1.0",
// "protocol_min": "1.0",
// "pruning": null,
// "server_version": "ElectrumX 1.0.17",
// "hash_function": "sha256"
// }
/// {
/// "genesis_hash": "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943",
/// "hosts": {"14.3.140.101": {"tcp_port": 51001, "ssl_port": 51002}},
/// "protocol_max": "1.0",
/// "protocol_min": "1.0",
/// "pruning": null,
/// "server_version": "ElectrumX 1.0.17",
/// "hash_function": "sha256"
/// }
Future<Map<String, dynamic>> getServerFeatures({String? requestID}) async {
try {
final response = await request(
@ -425,29 +569,38 @@ class ElectrumX {
/// Returns a list of maps that contain the tx_hash and height of the tx.
/// Ex:
/// [
// {
// "height": 200004,
// "tx_hash": "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412"
// },
// {
// "height": 215008,
// "tx_hash": "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403"
// }
// ]
/// {
/// "height": 200004,
/// "tx_hash": "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412"
/// },
/// {
/// "height": 215008,
/// "tx_hash": "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403"
/// }
/// ]
Future<List<Map<String, dynamic>>> getHistory({
required String scripthash,
String? requestID,
}) async {
try {
final response = await request(
requestID: requestID,
command: 'blockchain.scripthash.get_history',
connectionTimeout: const Duration(minutes: 5),
args: [
scripthash,
],
);
return List<Map<String, dynamic>>.from(response["result"] as List);
int retryCount = 3;
dynamic result;
while (retryCount > 0 && result is! List) {
final response = await request(
requestID: requestID,
command: 'blockchain.scripthash.get_history',
requestTimeout: const Duration(minutes: 5),
args: [
scripthash,
],
);
result = response["result"];
retryCount--;
}
return List<Map<String, dynamic>>.from(result as List);
} catch (e) {
rethrow;
}
@ -476,19 +629,19 @@ class ElectrumX {
/// Returns a list of maps.
/// Ex:
/// [
// {
// "tx_pos": 0,
// "value": 45318048,
// "tx_hash": "9f2c45a12db0144909b5db269415f7319179105982ac70ed80d76ea79d923ebf",
// "height": 437146
// },
// {
// "tx_pos": 0,
// "value": 919195,
// "tx_hash": "3d2290c93436a3e964cfc2f0950174d8847b1fbe3946432c4784e168da0f019f",
// "height": 441696
// }
// ]
/// {
/// "tx_pos": 0,
/// "value": 45318048,
/// "tx_hash": "9f2c45a12db0144909b5db269415f7319179105982ac70ed80d76ea79d923ebf",
/// "height": 437146
/// },
/// {
/// "tx_pos": 0,
/// "value": 919195,
/// "tx_hash": "3d2290c93436a3e964cfc2f0950174d8847b1fbe3946432c4784e168da0f019f",
/// "height": 441696
/// }
/// ]
Future<List<Map<String, dynamic>>> getUTXOs({
required String scripthash,
String? requestID,
@ -591,6 +744,14 @@ class ElectrumX {
return {"rawtx": response["result"] as String};
}
if (response["result"] == null) {
Logging.instance.log(
"getTransaction($txHash) returned null response",
level: LogLevel.Error,
);
throw 'getTransaction($txHash) returned null response';
}
return Map<String, dynamic>.from(response["result"] as Map);
} catch (e) {
Logging.instance.log(
@ -601,7 +762,7 @@ class ElectrumX {
}
}
/// Returns the whole anonymity set for denomination in the groupId.
/// Returns the whole Lelantus anonymity set for denomination in the groupId.
///
/// ex:
/// {
@ -615,7 +776,7 @@ class ElectrumX {
/// [dynamic list of length 4],
/// ]
/// }
Future<Map<String, dynamic>> getAnonymitySet({
Future<Map<String, dynamic>> getLelantusAnonymitySet({
String groupId = "1",
String blockhash = "",
String? requestID,
@ -642,8 +803,11 @@ class ElectrumX {
//TODO add example to docs
///
///
/// Returns the block height and groupId of pubcoin.
Future<dynamic> getMintData({dynamic mints, String? requestID}) async {
/// Returns the block height and groupId of a Lelantus pubcoin.
Future<dynamic> getLelantusMintData({
dynamic mints,
String? requestID,
}) async {
try {
final response = await request(
requestID: requestID,
@ -659,29 +823,40 @@ class ElectrumX {
}
//TODO add example to docs
/// Returns the whole set of the used coin serials.
Future<Map<String, dynamic>> getUsedCoinSerials({
/// Returns the whole set of the used Lelantus coin serials.
Future<Map<String, dynamic>> getLelantusUsedCoinSerials({
String? requestID,
required int startNumber,
}) async {
try {
final response = await request(
int retryCount = 3;
dynamic result;
while (retryCount > 0 && result is! List) {
final response = await request(
requestID: requestID,
command: 'lelantus.getusedcoinserials',
args: [
"$startNumber",
]);
return Map<String, dynamic>.from(response["result"] as Map);
],
requestTimeout: const Duration(minutes: 2),
);
result = response["result"];
retryCount--;
}
return Map<String, dynamic>.from(result as Map);
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
}
}
/// Returns the latest set id
/// Returns the latest Lelantus set id
///
/// ex: 1
Future<int> getLatestCoinId({String? requestID}) async {
Future<int> getLelantusLatestCoinId({String? requestID}) async {
try {
final response = await request(
requestID: requestID,
@ -694,31 +869,127 @@ class ElectrumX {
}
}
// /// Returns about 13 megabytes of json data as of march 2, 2022
// Future<Map<String, dynamic>> getCoinsForRecovery(
// {dynamic setId, String requestID}) async {
// try {
// final response = await request(
// requestID: requestID,
// command: 'lelantus.getcoinsforrecovery',
// args: [
// setId ?? 1,
// ],
// );
// return response["result"];
// } catch (e) {
// Logging.instance.log(e);
// throw e;
// }
// }
// ============== Spark ======================================================
// New Spark ElectrumX methods:
// > Functions provided by ElectrumX servers
// > // >
/// Returns the whole Spark anonymity set for denomination in the groupId.
///
/// Takes [coinGroupId] and [startBlockHash], if the last is empty it returns full set,
/// otherwise returns mint after that block, we need to call this to keep our
/// anonymity set data up to date.
///
/// Returns blockHash (last block hash),
/// setHash (hash of current set)
/// and coins (the list of pairs serialized coin and tx hash)
Future<Map<String, dynamic>> getSparkAnonymitySet({
String coinGroupId = "1",
String startBlockHash = "",
String? requestID,
}) async {
try {
Logging.instance.log("attempting to fetch spark.getsparkanonymityset...",
level: LogLevel.Info);
final response = await request(
requestID: requestID,
command: 'spark.getsparkanonymityset',
args: [
coinGroupId,
startBlockHash,
],
);
Logging.instance.log("Fetching spark.getsparkanonymityset finished",
level: LogLevel.Info);
return Map<String, dynamic>.from(response["result"] as Map);
} catch (e) {
rethrow;
}
}
/// Takes [startNumber], if it is 0, we get the full set,
/// otherwise the used tags after that number
Future<Set<String>> getSparkUsedCoinsTags({
String? requestID,
required int startNumber,
}) async {
try {
final response = await request(
requestID: requestID,
command: 'spark.getusedcoinstags',
args: [
"$startNumber",
],
requestTimeout: const Duration(minutes: 2),
);
final map = Map<String, dynamic>.from(response["result"] as Map);
final set = Set<String>.from(map["tags"] as List);
return await compute(_ffiHashTagsComputeWrapper, set);
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
}
}
/// Takes a list of [sparkCoinHashes] and returns the set id and block height
/// for each coin
///
/// arg:
/// {
/// "coinHashes": [
/// "b476ed2b374bb081ea51d111f68f0136252521214e213d119b8dc67b92f5a390",
/// "b476ed2b374bb081ea51d111f68f0136252521214e213d119b8dc67b92f5a390",
/// ]
/// }
Future<List<Map<String, dynamic>>> getSparkMintMetaData({
String? requestID,
required List<String> sparkCoinHashes,
}) async {
try {
final response = await request(
requestID: requestID,
command: 'spark.getsparkmintmetadata',
args: [
{
"coinHashes": sparkCoinHashes,
},
],
);
return List<Map<String, dynamic>>.from(response["result"] as List);
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
}
}
/// Returns the latest Spark set id
///
/// ex: 1
Future<int> getSparkLatestCoinId({
String? requestID,
}) async {
try {
final response = await request(
requestID: requestID,
command: 'spark.getsparklatestcoinid',
);
return response["result"] as int;
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
}
}
// ===========================================================================
/// Get the current fee rate.
///
/// Returns a map with the kay "rate" that corresponds to the free rate in satoshis
/// Ex:
/// {
// "rate": 1000,
// }
/// "rate": 1000,
/// }
Future<Map<String, dynamic>> getFeeRate({String? requestID}) async {
try {
final response = await request(
@ -768,3 +1039,7 @@ class ElectrumX {
}
}
}
Set<String> _ffiHashTagsComputeWrapper(Set<String> base64Tags) {
return LibSpark.hashTags(base64Tags: base64Tags);
}

View file

@ -14,7 +14,10 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:mutex/mutex.dart';
import 'package:stackwallet/exceptions/json_rpc/json_rpc_exception.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:tor_ffi_plugin/socks_socket.dart';
// Json RPC class to handle connecting to electrumx servers
class JsonRPC {
@ -23,16 +26,19 @@ class JsonRPC {
required this.port,
this.useSSL = false,
this.connectionTimeout = const Duration(seconds: 60),
required ({InternetAddress host, int port})? proxyInfo,
});
final bool useSSL;
final String host;
final int port;
final Duration connectionTimeout;
({InternetAddress host, int port})? proxyInfo;
final _requestMutex = Mutex();
final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue();
Socket? _socket;
StreamSubscription<Uint8List>? _subscription;
SOCKSSocket? _socksSocket;
StreamSubscription<List<int>>? _subscription;
void _dataHandler(List<int> data) {
_requestQueue.nextIncompleteReq.then((req) {
@ -75,11 +81,15 @@ class JsonRPC {
_requestQueue.nextIncompleteReq.then((req) {
if (req != null) {
// \r\n required by electrumx server
_socket!.write('${req.jsonRequest}\r\n');
if (_socket != null) {
_socket!.write('${req.jsonRequest}\r\n');
}
if (_socksSocket != null) {
_socksSocket!.write('${req.jsonRequest}\r\n');
}
// TODO different timeout length?
req.initiateTimeout(
const Duration(seconds: 10),
onTimedOut: () {
_requestQueue.remove(req);
},
@ -88,19 +98,37 @@ class JsonRPC {
});
}
Future<JsonRPCResponse> request(String jsonRpcRequest) async {
Future<JsonRPCResponse> request(
String jsonRpcRequest,
Duration requestTimeout,
) async {
await _requestMutex.protect(() async {
if (_socket == null) {
Logging.instance.log(
"JsonRPC request: opening socket $host:$port",
level: LogLevel.Info,
);
await connect();
if (!Prefs.instance.useTor) {
if (_socket == null) {
Logging.instance.log(
"JsonRPC request: opening socket $host:$port",
level: LogLevel.Info,
);
await connect().timeout(requestTimeout, onTimeout: () {
throw Exception("Request timeout: $jsonRpcRequest");
});
}
} else {
if (_socksSocket == null) {
Logging.instance.log(
"JsonRPC request: opening SOCKS socket to $host:$port",
level: LogLevel.Info,
);
await connect().timeout(requestTimeout, onTimeout: () {
throw Exception("Request timeout: $jsonRpcRequest");
});
}
}
});
final req = _JsonRPCRequest(
jsonRequest: jsonRpcRequest,
requestTimeout: requestTimeout,
completer: Completer<JsonRPCResponse>(),
);
@ -110,9 +138,9 @@ class JsonRPC {
reason: "return req.completer.future.onError: $error\n$stackTrace",
);
return JsonRPCResponse(
exception: error is Exception
exception: error is JsonRpcException
? error
: Exception(
: JsonRpcException(
"req.completer.future.onError: $error\n$stackTrace",
),
);
@ -134,6 +162,8 @@ class JsonRPC {
_subscription = null;
_socket?.destroy();
_socket = null;
await _socksSocket?.close();
_socksSocket = null;
// clean up remaining queue
await _requestQueue.completeRemainingWithError(
@ -143,33 +173,86 @@ class JsonRPC {
}
Future<void> connect() async {
if (_socket != null) {
throw Exception(
"JsonRPC attempted to connect to an already existing socket!",
);
}
if (!Prefs.instance.useTor) {
if (useSSL) {
_socket = await SecureSocket.connect(
host,
port,
timeout: connectionTimeout,
onBadCertificate: (_) => true,
); // TODO do not automatically trust bad certificates
} else {
_socket = await Socket.connect(
host,
port,
timeout: connectionTimeout,
);
}
if (useSSL) {
_socket = await SecureSocket.connect(
host,
port,
timeout: connectionTimeout,
onBadCertificate: (_) => true,
); // TODO do not automatically trust bad certificates
_subscription = _socket!.listen(
_dataHandler,
onError: _errorHandler,
onDone: _doneHandler,
cancelOnError: true,
);
} else {
_socket = await Socket.connect(
host,
port,
timeout: connectionTimeout,
if (proxyInfo == null) {
throw JsonRpcException(
"JsonRPC.connect failed with useTor=${Prefs.instance.useTor} and proxyInfo is null");
}
// instantiate a socks socket at localhost and on the port selected by the tor service
_socksSocket = await SOCKSSocket.create(
proxyHost: proxyInfo!.host.address,
proxyPort: proxyInfo!.port,
sslEnabled: useSSL,
);
try {
Logging.instance.log(
"JsonRPC.connect(): connecting to SOCKS socket at $proxyInfo (SSL $useSSL)...",
level: LogLevel.Info);
await _socksSocket?.connect();
Logging.instance.log(
"JsonRPC.connect(): connected to SOCKS socket at $proxyInfo...",
level: LogLevel.Info);
} catch (e) {
Logging.instance.log(
"JsonRPC.connect(): failed to connect to SOCKS socket at $proxyInfo, $e",
level: LogLevel.Error);
throw JsonRpcException(
"JsonRPC.connect(): failed to connect to SOCKS socket at $proxyInfo, $e");
}
try {
Logging.instance.log(
"JsonRPC.connect(): connecting to $host:$port over SOCKS socket at $proxyInfo...",
level: LogLevel.Info);
await _socksSocket?.connectTo(host, port);
Logging.instance.log(
"JsonRPC.connect(): connected to $host:$port over SOCKS socket at $proxyInfo",
level: LogLevel.Info);
} catch (e) {
Logging.instance.log(
"JsonRPC.connect(): failed to connect to $host over tor proxy at $proxyInfo, $e",
level: LogLevel.Error);
throw JsonRpcException(
"JsonRPC.connect(): failed to connect to tor proxy, $e");
}
_subscription = _socksSocket!.listen(
_dataHandler,
onError: _errorHandler,
onDone: _doneHandler,
cancelOnError: true,
);
}
_subscription = _socket!.listen(
_dataHandler,
onError: _errorHandler,
onDone: _doneHandler,
cancelOnError: true,
);
return;
}
}
@ -243,9 +326,14 @@ class _JsonRPCRequest {
final String jsonRequest;
final Completer<JsonRPCResponse> completer;
final Duration requestTimeout;
final List<int> _responseData = [];
_JsonRPCRequest({required this.jsonRequest, required this.completer});
_JsonRPCRequest({
required this.jsonRequest,
required this.completer,
required this.requestTimeout,
});
void appendDataAndCheckIfComplete(List<int> data) {
_responseData.addAll(data);
@ -263,14 +351,13 @@ class _JsonRPCRequest {
}
}
void initiateTimeout(
Duration timeout, {
void initiateTimeout({
VoidCallback? onTimedOut,
}) {
Future<void>.delayed(timeout).then((_) {
Future<void>.delayed(requestTimeout).then((_) {
if (!isComplete) {
try {
throw Exception("_JsonRPCRequest timed out: $jsonRequest");
throw JsonRpcException("_JsonRPCRequest timed out: $jsonRequest");
} catch (e, s) {
completer.completeError(e, s);
onTimedOut?.call();
@ -284,7 +371,18 @@ class _JsonRPCRequest {
class JsonRPCResponse {
final dynamic data;
final Exception? exception;
final JsonRpcException? exception;
JsonRPCResponse({this.data, this.exception});
}
bool isIpAddress(String host) {
try {
// if the string can be parsed into an InternetAddress, it's an IP.
InternetAddress(host);
return true;
} catch (e) {
// if parsing fails, it's not an IP.
return false;
}
}

View file

@ -1,324 +0,0 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:stackwallet/utilities/logger.dart';
class ElectrumXSubscription with ChangeNotifier {
dynamic _response;
dynamic get response => _response;
set response(dynamic newData) {
_response = newData;
notifyListeners();
}
}
class SocketTask {
SocketTask({this.completer, this.subscription});
final Completer<dynamic>? completer;
final ElectrumXSubscription? subscription;
bool get isSubscription => subscription != null;
}
class SubscribableElectrumXClient {
int _currentRequestID = 0;
bool _isConnected = false;
List<int> _responseData = [];
final Map<String, SocketTask> _tasks = {};
Timer? _aliveTimer;
Socket? _socket;
late final bool _useSSL;
late final Duration _connectionTimeout;
late final Duration _keepAlive;
bool get isConnected => _isConnected;
bool get useSSL => _useSSL;
void Function(bool)? onConnectionStatusChanged;
SubscribableElectrumXClient({
bool useSSL = true,
this.onConnectionStatusChanged,
Duration connectionTimeout = const Duration(seconds: 5),
Duration keepAlive = const Duration(seconds: 10),
}) {
_useSSL = useSSL;
_connectionTimeout = connectionTimeout;
_keepAlive = keepAlive;
}
Future<void> connect({required String host, required int port}) async {
try {
await _socket?.close();
} catch (_) {}
if (_useSSL) {
_socket = await SecureSocket.connect(
host,
port,
timeout: _connectionTimeout,
onBadCertificate: (_) => true,
);
} else {
_socket = await Socket.connect(
host,
port,
timeout: _connectionTimeout,
);
}
_updateConnectionStatus(true);
_socket!.listen(
_dataHandler,
onError: _errorHandler,
onDone: _doneHandler,
cancelOnError: true,
);
_aliveTimer?.cancel();
_aliveTimer = Timer.periodic(
_keepAlive,
(_) async => _updateConnectionStatus(await ping()),
);
}
Future<void> disconnect() async {
_aliveTimer?.cancel();
await _socket?.close();
onConnectionStatusChanged = null;
}
String _buildJsonRequestString({
required String method,
required String id,
required List<dynamic> params,
}) {
final paramString = jsonEncode(params);
return '{"jsonrpc": "2.0", "id": "$id","method": "$method","params": $paramString}\r\n';
}
void _updateConnectionStatus(bool connectionStatus) {
if (_isConnected != connectionStatus && onConnectionStatusChanged != null) {
onConnectionStatusChanged!(connectionStatus);
}
_isConnected = connectionStatus;
}
void _dataHandler(List<int> data) {
_responseData.addAll(data);
// 0x0A is newline
// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html
if (data.last == 0x0A) {
try {
final response = jsonDecode(String.fromCharCodes(_responseData))
as Map<String, dynamic>;
_responseHandler(response);
} catch (e, s) {
Logging.instance
.log("JsonRPC jsonDecode: $e\n$s", level: LogLevel.Error);
rethrow;
} finally {
_responseData = [];
}
}
}
void _responseHandler(Map<String, dynamic> response) {
// subscriptions will have a method in the response
if (response['method'] is String) {
_subscriptionHandler(response: response);
return;
}
final id = response['id'] as String;
final result = response['result'];
_complete(id, result);
}
void _subscriptionHandler({
required Map<String, dynamic> response,
}) {
final method = response['method'];
switch (method) {
case "blockchain.scripthash.subscribe":
final params = response["params"] as List<dynamic>;
final scripthash = params.first as String;
final taskId = "blockchain.scripthash.subscribe:$scripthash";
_tasks[taskId]?.subscription?.response = params.last;
break;
case "blockchain.headers.subscribe":
final params = response["params"];
const taskId = "blockchain.headers.subscribe";
_tasks[taskId]?.subscription?.response = params.first;
break;
default:
break;
}
}
void _errorHandler(Object error, StackTrace trace) {
_updateConnectionStatus(false);
Logging.instance.log(
"SubscribableElectrumXClient called _errorHandler with: $error\n$trace",
level: LogLevel.Info);
}
void _doneHandler() {
_updateConnectionStatus(false);
Logging.instance.log("SubscribableElectrumXClient called _doneHandler",
level: LogLevel.Info);
}
void _complete(String id, dynamic data) {
if (_tasks[id] == null) {
return;
}
if (!(_tasks[id]?.completer?.isCompleted ?? false)) {
_tasks[id]?.completer?.complete(data);
}
if (!(_tasks[id]?.isSubscription ?? false)) {
_tasks.remove(id);
} else {
_tasks[id]?.subscription?.response = data;
}
}
void _addTask({
required String id,
required Completer<dynamic> completer,
}) {
_tasks[id] = SocketTask(completer: completer, subscription: null);
}
void _addSubscriptionTask({
required String id,
required ElectrumXSubscription subscription,
}) {
_tasks[id] = SocketTask(completer: null, subscription: subscription);
}
Future<dynamic> _call({
required String method,
List<dynamic> params = const [],
}) async {
final completer = Completer<dynamic>();
_currentRequestID++;
final id = _currentRequestID.toString();
_addTask(id: id, completer: completer);
_socket?.write(
_buildJsonRequestString(
method: method,
id: id,
params: params,
),
);
return completer.future;
}
Future<dynamic> _callWithTimeout({
required String method,
List<dynamic> params = const [],
Duration timeout = const Duration(seconds: 2),
}) async {
final completer = Completer<dynamic>();
_currentRequestID++;
final id = _currentRequestID.toString();
_addTask(id: id, completer: completer);
_socket?.write(
_buildJsonRequestString(
method: method,
id: id,
params: params,
),
);
Timer(timeout, () {
if (!completer.isCompleted) {
completer.completeError(
Exception("Request \"id: $id, method: $method\" timed out!"),
);
}
});
return completer.future;
}
ElectrumXSubscription _subscribe({
required String taskId,
required String method,
List<dynamic> params = const [],
}) {
// try {
final subscription = ElectrumXSubscription();
_addSubscriptionTask(id: taskId, subscription: subscription);
_currentRequestID++;
_socket?.write(
_buildJsonRequestString(
method: method,
id: taskId,
params: params,
),
);
return subscription;
// } catch (e, s) {
// Logging.instance.log("SubscribableElectrumXClient _subscribe: $e\n$s", level: LogLevel.Error);
// return null;
// }
}
/// Ping the server to ensure it is responding
///
/// Returns true if ping succeeded
Future<bool> ping() async {
try {
final response = (await _callWithTimeout(method: "server.ping")) as Map;
return response.keys.contains("result") && response["result"] == null;
} catch (_) {
return false;
}
}
/// Subscribe to a scripthash to receive notifications on status changes
ElectrumXSubscription subscribeToScripthash({required String scripthash}) {
return _subscribe(
taskId: 'blockchain.scripthash.subscribe:$scripthash',
method: 'blockchain.scripthash.subscribe',
params: [scripthash],
);
}
/// Subscribe to block headers to receive notifications on new blocks found
///
/// Returns the existing subscription if found
ElectrumXSubscription subscribeToBlockHeaders() {
return _tasks["blockchain.headers.subscribe"]?.subscription ??
_subscribe(
taskId: "blockchain.headers.subscribe",
method: "blockchain.headers.subscribe",
params: [],
);
}
}

View file

@ -0,0 +1,324 @@
// /*
// * This file is part of Stack Wallet.
// *
// * Copyright (c) 2023 Cypher Stack
// * All Rights Reserved.
// * The code is distributed under GPLv3 license, see LICENSE file for details.
// * Generated by Cypher Stack on 2023-05-26
// *
// */
//
// import 'dart:async';
// import 'dart:convert';
// import 'dart:io';
//
// import 'package:flutter/foundation.dart';
// import 'package:stackwallet/utilities/logger.dart';
//
// class ElectrumXSubscription with ChangeNotifier {
// dynamic _response;
// dynamic get response => _response;
// set response(dynamic newData) {
// _response = newData;
// notifyListeners();
// }
// }
//
// class SocketTask {
// SocketTask({this.completer, this.subscription});
//
// final Completer<dynamic>? completer;
// final ElectrumXSubscription? subscription;
//
// bool get isSubscription => subscription != null;
// }
//
// class SubscribableElectrumXClient {
// int _currentRequestID = 0;
// bool _isConnected = false;
// List<int> _responseData = [];
// final Map<String, SocketTask> _tasks = {};
// Timer? _aliveTimer;
// Socket? _socket;
// late final bool _useSSL;
// late final Duration _connectionTimeout;
// late final Duration _keepAlive;
//
// bool get isConnected => _isConnected;
// bool get useSSL => _useSSL;
//
// void Function(bool)? onConnectionStatusChanged;
//
// SubscribableElectrumXClient({
// bool useSSL = true,
// this.onConnectionStatusChanged,
// Duration connectionTimeout = const Duration(seconds: 5),
// Duration keepAlive = const Duration(seconds: 10),
// }) {
// _useSSL = useSSL;
// _connectionTimeout = connectionTimeout;
// _keepAlive = keepAlive;
// }
//
// Future<void> connect({required String host, required int port}) async {
// try {
// await _socket?.close();
// } catch (_) {}
//
// if (_useSSL) {
// _socket = await SecureSocket.connect(
// host,
// port,
// timeout: _connectionTimeout,
// onBadCertificate: (_) => true,
// );
// } else {
// _socket = await Socket.connect(
// host,
// port,
// timeout: _connectionTimeout,
// );
// }
// _updateConnectionStatus(true);
//
// _socket!.listen(
// _dataHandler,
// onError: _errorHandler,
// onDone: _doneHandler,
// cancelOnError: true,
// );
//
// _aliveTimer?.cancel();
// _aliveTimer = Timer.periodic(
// _keepAlive,
// (_) async => _updateConnectionStatus(await ping()),
// );
// }
//
// Future<void> disconnect() async {
// _aliveTimer?.cancel();
// await _socket?.close();
// onConnectionStatusChanged = null;
// }
//
// String _buildJsonRequestString({
// required String method,
// required String id,
// required List<dynamic> params,
// }) {
// final paramString = jsonEncode(params);
// return '{"jsonrpc": "2.0", "id": "$id","method": "$method","params": $paramString}\r\n';
// }
//
// void _updateConnectionStatus(bool connectionStatus) {
// if (_isConnected != connectionStatus && onConnectionStatusChanged != null) {
// onConnectionStatusChanged!(connectionStatus);
// }
// _isConnected = connectionStatus;
// }
//
// void _dataHandler(List<int> data) {
// _responseData.addAll(data);
//
// // 0x0A is newline
// // https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html
// if (data.last == 0x0A) {
// try {
// final response = jsonDecode(String.fromCharCodes(_responseData))
// as Map<String, dynamic>;
// _responseHandler(response);
// } catch (e, s) {
// Logging.instance
// .log("JsonRPC jsonDecode: $e\n$s", level: LogLevel.Error);
// rethrow;
// } finally {
// _responseData = [];
// }
// }
// }
//
// void _responseHandler(Map<String, dynamic> response) {
// // subscriptions will have a method in the response
// if (response['method'] is String) {
// _subscriptionHandler(response: response);
// return;
// }
//
// final id = response['id'] as String;
// final result = response['result'];
//
// _complete(id, result);
// }
//
// void _subscriptionHandler({
// required Map<String, dynamic> response,
// }) {
// final method = response['method'];
// switch (method) {
// case "blockchain.scripthash.subscribe":
// final params = response["params"] as List<dynamic>;
// final scripthash = params.first as String;
// final taskId = "blockchain.scripthash.subscribe:$scripthash";
//
// _tasks[taskId]?.subscription?.response = params.last;
// break;
// case "blockchain.headers.subscribe":
// final params = response["params"];
// const taskId = "blockchain.headers.subscribe";
//
// _tasks[taskId]?.subscription?.response = params.first;
// break;
// default:
// break;
// }
// }
//
// void _errorHandler(Object error, StackTrace trace) {
// _updateConnectionStatus(false);
// Logging.instance.log(
// "SubscribableElectrumXClient called _errorHandler with: $error\n$trace",
// level: LogLevel.Info);
// }
//
// void _doneHandler() {
// _updateConnectionStatus(false);
// Logging.instance.log("SubscribableElectrumXClient called _doneHandler",
// level: LogLevel.Info);
// }
//
// void _complete(String id, dynamic data) {
// if (_tasks[id] == null) {
// return;
// }
//
// if (!(_tasks[id]?.completer?.isCompleted ?? false)) {
// _tasks[id]?.completer?.complete(data);
// }
//
// if (!(_tasks[id]?.isSubscription ?? false)) {
// _tasks.remove(id);
// } else {
// _tasks[id]?.subscription?.response = data;
// }
// }
//
// void _addTask({
// required String id,
// required Completer<dynamic> completer,
// }) {
// _tasks[id] = SocketTask(completer: completer, subscription: null);
// }
//
// void _addSubscriptionTask({
// required String id,
// required ElectrumXSubscription subscription,
// }) {
// _tasks[id] = SocketTask(completer: null, subscription: subscription);
// }
//
// Future<dynamic> _call({
// required String method,
// List<dynamic> params = const [],
// }) async {
// final completer = Completer<dynamic>();
// _currentRequestID++;
// final id = _currentRequestID.toString();
// _addTask(id: id, completer: completer);
//
// _socket?.write(
// _buildJsonRequestString(
// method: method,
// id: id,
// params: params,
// ),
// );
//
// return completer.future;
// }
//
// Future<dynamic> _callWithTimeout({
// required String method,
// List<dynamic> params = const [],
// Duration timeout = const Duration(seconds: 2),
// }) async {
// final completer = Completer<dynamic>();
// _currentRequestID++;
// final id = _currentRequestID.toString();
// _addTask(id: id, completer: completer);
//
// _socket?.write(
// _buildJsonRequestString(
// method: method,
// id: id,
// params: params,
// ),
// );
//
// Timer(timeout, () {
// if (!completer.isCompleted) {
// completer.completeError(
// Exception("Request \"id: $id, method: $method\" timed out!"),
// );
// }
// });
//
// return completer.future;
// }
//
// ElectrumXSubscription _subscribe({
// required String taskId,
// required String method,
// List<dynamic> params = const [],
// }) {
// // try {
// final subscription = ElectrumXSubscription();
// _addSubscriptionTask(id: taskId, subscription: subscription);
// _currentRequestID++;
// _socket?.write(
// _buildJsonRequestString(
// method: method,
// id: taskId,
// params: params,
// ),
// );
//
// return subscription;
// // } catch (e, s) {
// // Logging.instance.log("SubscribableElectrumXClient _subscribe: $e\n$s", level: LogLevel.Error);
// // return null;
// // }
// }
//
// /// Ping the server to ensure it is responding
// ///
// /// Returns true if ping succeeded
// Future<bool> ping() async {
// try {
// final response = (await _callWithTimeout(method: "server.ping")) as Map;
// return response.keys.contains("result") && response["result"] == null;
// } catch (_) {
// return false;
// }
// }
//
// /// Subscribe to a scripthash to receive notifications on status changes
// ElectrumXSubscription subscribeToScripthash({required String scripthash}) {
// return _subscribe(
// taskId: 'blockchain.scripthash.subscribe:$scripthash',
// method: 'blockchain.scripthash.subscribe',
// params: [scripthash],
// );
// }
//
// /// Subscribe to block headers to receive notifications on new blocks found
// ///
// /// Returns the existing subscription if found
// ElectrumXSubscription subscribeToBlockHeaders() {
// return _tasks["blockchain.headers.subscribe"]?.subscription ??
// _subscribe(
// taskId: "blockchain.headers.subscribe",
// method: "blockchain.headers.subscribe",
// params: [],
// );
// }
// }

View file

@ -0,0 +1,21 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'package:stackwallet/exceptions/sw_exception.dart';
class JsonRpcException implements SWException {
JsonRpcException(this.message);
@override
final String message;
@override
toString() => message;
}

View file

@ -12,11 +12,11 @@ import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:coinlib_flutter/coinlib_flutter.dart';
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';
@ -28,6 +28,7 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:isar/isar.dart';
import 'package:keyboard_dismisser/keyboard_dismisser.dart';
import 'package:path_provider/path_provider.dart';
import 'package:stackwallet/db/db_version_migration.dart';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
@ -45,6 +46,7 @@ import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart';
import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart';
import 'package:stackwallet/pages_desktop_specific/password/desktop_login_view.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart';
import 'package:stackwallet/providers/global/auto_swb_service_provider.dart';
import 'package:stackwallet/providers/global/base_currencies_provider.dart';
@ -59,17 +61,18 @@ import 'package:stackwallet/services/locale_service.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/notifications_api.dart';
import 'package:stackwallet/services/notifications_service.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/services/trade_service.dart';
import 'package:stackwallet/themes/theme_providers.dart';
import 'package:stackwallet/themes/theme_service.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/db_version_migration.dart';
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/prefs.dart';
import 'package:stackwallet/utilities/stack_file_system.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/providers/all_wallets_info_provider.dart';
import 'package:stackwallet/widgets/crypto_notifications.dart';
import 'package:window_size/window_size.dart';
@ -79,8 +82,15 @@ final openedFromSWBFileStringStateProvider =
// main() is the entry point to the app. It initializes Hive (local database),
// runs the MyApp widget and checks for new users, caching the value in the
// miscellaneous box for later use
void main() async {
void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
if (Util.isDesktop && args.length == 2 && args.first == "-d") {
StackFileSystem.overrideDir = args.last;
}
final loadCoinlibFuture = loadCoinlib();
GoogleFonts.config.allowRuntimeFetching = false;
if (Platform.isIOS) {
Util.libraryPath = await getLibraryDirectory();
@ -97,7 +107,7 @@ void main() async {
setWindowMaxSize(Size.infinite);
final screenHeight = screen?.frame.height;
if (screenHeight != null && !kDebugMode) {
if (screenHeight != null) {
// starting to height be 3/4 screen height or 900, whichever is smaller
final height = min<double>(screenHeight * 0.75, 900);
setWindowFrame(
@ -167,6 +177,18 @@ void main() async {
await Hive.openBox<dynamic>(DB.boxNamePrefs);
await Prefs.instance.init();
// TODO:
// This should be moved to happen during the loading animation instead of
// showing a blank screen for 4-10 seconds.
// Some refactoring will need to be done here to make sure we don't make any
// network calls before starting up tor
if (Prefs.instance.useTor) {
TorService.sharedInstance.init(
torDataDirPath: (await StackFileSystem.applicationTorDirectory()).path,
);
await TorService.sharedInstance.start();
}
await StackFileSystem.initThemesDir();
// Desktop migrate handled elsewhere (currently desktop_login_view.dart)
@ -201,6 +223,8 @@ void main() async {
// overlays: [SystemUiOverlay.bottom]);
await NotificationApi.init();
await loadCoinlibFuture;
await MainDB.instance.initMainDB();
ThemeService.instance.init(MainDB.instance);
@ -293,13 +317,6 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
_desktopHasPassword =
await ref.read(storageCryptoHandlerProvider).hasPassword();
}
ref
.read(priceAnd24hChangeNotifierProvider)
.tokenContractAddressesToCheck
.addAll(
await MainDB.instance.getEthContracts().addressProperty().findAll(),
);
}
Future<void> load() async {
@ -332,9 +349,10 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
prefs: ref.read(prefsChangeNotifierProvider),
);
ref.read(priceAnd24hChangeNotifierProvider).start(true);
await ref
.read(walletsChangeNotifierProvider)
.load(ref.read(prefsChangeNotifierProvider));
await ref.read(pWallets).load(
ref.read(prefsChangeNotifierProvider),
ref.read(mainDBProvider),
);
loadingCompleter.complete();
// TODO: this should probably run unawaited. Keep commented out for now as proper community nodes ui hasn't been implemented yet
// unawaited(_nodeService.updateCommunityNodes());
@ -723,7 +741,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// FlutterNativeSplash.remove();
if (ref.read(walletsChangeNotifierProvider).hasWallets ||
if (ref.read(pAllWalletsInfo).isNotEmpty ||
ref.read(prefsChangeNotifierProvider).hasPin) {
// return HomeView();

View file

@ -11,6 +11,7 @@
import 'dart:convert';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
class Balance {
final Amount total;
@ -25,6 +26,20 @@ class Balance {
required this.pendingSpendable,
});
factory Balance.zeroForCoin({required Coin coin}) {
final amount = Amount(
rawValue: BigInt.zero,
fractionDigits: coin.decimals,
);
return Balance(
total: amount,
spendable: amount,
blockedTotal: amount,
pendingSpendable: amount,
);
}
String toJsonIgnoreCoin() => jsonEncode({
"total": total.toJsonString(),
"spendable": spendable.toJsonString(),

View file

@ -0,0 +1,187 @@
import 'package:flutter/cupertino.dart';
import 'package:stackwallet/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart';
class FusionProgressUIState extends ChangeNotifier {
/// Whether we are able to connect to the server.
bool _ableToConnect = false;
// _ableToConnect setter.
set ableToConnect(bool ableToConnect) {
_ableToConnect = ableToConnect;
notifyListeners();
}
bool get done {
if (!_ableToConnect) {
return false;
}
bool _done = (_connecting.status == CashFusionStatus.success) ||
(_connecting.status == CashFusionStatus.failed);
_done &= (_outputs.status == CashFusionStatus.success) ||
(_outputs.status == CashFusionStatus.failed);
_done &= (_peers.status == CashFusionStatus.success) ||
(_peers.status == CashFusionStatus.failed);
_done &= (_fusing.status == CashFusionStatus.success) ||
(_fusing.status == CashFusionStatus.failed);
_done &= (_complete.status == CashFusionStatus.success) ||
(_complete.status == CashFusionStatus.failed);
_done &= (fusionState.status == CashFusionStatus.success) ||
(fusionState.status == CashFusionStatus.failed);
return _done;
}
bool get succeeded {
if (!_ableToConnect) {
return false;
}
bool _succeeded = _connecting.status == CashFusionStatus.success;
_succeeded &= _outputs.status == CashFusionStatus.success;
_succeeded &= _peers.status == CashFusionStatus.success;
_succeeded &= _fusing.status == CashFusionStatus.success;
_succeeded &= _complete.status == CashFusionStatus.success;
_succeeded &= fusionState.status == CashFusionStatus.success;
return _succeeded;
}
CashFusionState _connecting =
CashFusionState(status: CashFusionStatus.waiting, info: null);
CashFusionState get connecting => _connecting;
void setConnecting(CashFusionState state, {bool shouldNotify = true}) {
_connecting = state;
_running = true;
if (shouldNotify) {
notifyListeners();
}
}
CashFusionState _outputs =
CashFusionState(status: CashFusionStatus.waiting, info: null);
CashFusionState get outputs => _outputs;
void setOutputs(CashFusionState state, {bool shouldNotify = true}) {
_outputs = state;
_running = true;
if (shouldNotify) {
notifyListeners();
}
}
CashFusionState _peers =
CashFusionState(status: CashFusionStatus.waiting, info: null);
CashFusionState get peers => _peers;
void setPeers(CashFusionState state, {bool shouldNotify = true}) {
_peers = state;
_running = true;
if (shouldNotify) {
notifyListeners();
}
}
CashFusionState _fusing =
CashFusionState(status: CashFusionStatus.waiting, info: null);
CashFusionState get fusing => _fusing;
void setFusing(CashFusionState state, {bool shouldNotify = true}) {
_fusing = state;
_running = true;
if (shouldNotify) {
notifyListeners();
}
}
CashFusionState _complete =
CashFusionState(status: CashFusionStatus.waiting, info: null);
CashFusionState get complete => _complete;
void setComplete(CashFusionState state, {bool shouldNotify = true}) {
_complete = state;
_running = true;
if (shouldNotify) {
notifyListeners();
}
}
CashFusionState _fusionStatus =
CashFusionState(status: CashFusionStatus.waiting, info: null);
CashFusionState get fusionState => _fusionStatus;
void setFusionState(CashFusionState state, {bool shouldNotify = true}) {
_fusionStatus = state;
_updateRunningState(state.status);
if (shouldNotify) {
notifyListeners();
}
}
/// An int storing the number of successfully completed fusion rounds.
int _fusionRoundsCompleted = 0;
int get fusionRoundsCompleted => _fusionRoundsCompleted;
set fusionRoundsCompleted(int fusionRoundsCompleted) {
_fusionRoundsCompleted = fusionRoundsCompleted;
notifyListeners();
}
/// A helper for incrementing the number of successfully completed fusion rounds.
void incrementFusionRoundsCompleted() {
_fusionRoundsCompleted++;
_fusionRoundsFailed = 0; // Reset failed round count on success.
_failed = false; // Reset failed flag on success.
notifyListeners();
}
/// An int storing the number of failed fusion rounds.
int _fusionRoundsFailed = 0;
int get fusionRoundsFailed => _fusionRoundsFailed;
set fusionRoundsFailed(int fusionRoundsFailed) {
_fusionRoundsFailed = fusionRoundsFailed;
notifyListeners();
}
/// A helper for incrementing the number of failed fusion rounds.
void incrementFusionRoundsFailed() {
_fusionRoundsFailed++;
notifyListeners();
}
/// A flag indicating that fusion has stopped because the maximum number of
/// consecutive failed fusion rounds has been reached.
///
/// Set from the interface. I didn't want to have to configure
///
/// Used to be named maxConsecutiveFusionRoundsFailed.
bool _failed = false;
bool get failed => _failed;
void setFailed(bool failed, {bool shouldNotify = true}) {
_failed = failed;
if (shouldNotify) {
notifyListeners();
}
}
/// A flag indicating that fusion is running.
bool _running = false;
bool get running => _running;
void setRunning(bool running, {bool shouldNotify = true}) {
_running = running;
if (shouldNotify) {
notifyListeners();
}
}
/// A helper method for setting the running flag.
///
/// Sets the running flag to true if the status is running. Sets the flag to
/// false if succeeded or failed or the global failed flag is set.
void _updateRunningState(CashFusionStatus status) {
if (status == CashFusionStatus.running) {
_running = true;
} else if (((status == CashFusionStatus.success ||
status == CashFusionStatus.failed) &&
(done || succeeded)) ||
_failed) {
_running = false;
}
}
}

View file

@ -70,6 +70,30 @@ class Address extends CryptoCurrencyAddress {
subType == AddressSubType.paynymSend ||
subType == AddressSubType.paynymReceive;
/// If called on an [Address] already stored in the DB be sure to update the
/// [transactions] Isar Links if required
Address copyWith({
String? walletId,
String? value,
List<byte>? publicKey,
int? derivationIndex,
AddressType? type,
AddressSubType? subType,
DerivationPath? derivationPath,
String? otherData,
}) {
return Address(
walletId: walletId ?? this.walletId,
value: value ?? this.value,
publicKey: publicKey ?? this.publicKey,
derivationIndex: derivationIndex ?? this.derivationIndex,
type: type ?? this.type,
subType: subType ?? this.subType,
derivationPath: derivationPath ?? this.derivationPath,
otherData: otherData ?? this.otherData,
);
}
@override
String toString() => "{ "
"id: $id, "
@ -124,7 +148,7 @@ class Address extends CryptoCurrencyAddress {
}
}
// do not modify
// do not modify unless you know what the consequences are
enum AddressType {
p2pkh,
p2sh,
@ -135,7 +159,11 @@ enum AddressType {
nonWallet,
ethereum,
nano,
banano;
banano,
spark,
stellar,
tezos,
;
String get readableName {
switch (this) {
@ -159,6 +187,12 @@ enum AddressType {
return "Nano";
case AddressType.banano:
return "Banano";
case AddressType.spark:
return "Spark";
case AddressType.stellar:
return "Stellar";
case AddressType.tezos:
return "Tezos";
}
}
}

View file

@ -263,6 +263,9 @@ const _AddresstypeEnumValueMap = {
'ethereum': 7,
'nano': 8,
'banano': 9,
'spark': 10,
'stellar': 11,
'tezos': 12,
};
const _AddresstypeValueEnumMap = {
0: AddressType.p2pkh,
@ -275,6 +278,9 @@ const _AddresstypeValueEnumMap = {
7: AddressType.ethereum,
8: AddressType.nano,
9: AddressType.banano,
10: AddressType.spark,
11: AddressType.stellar,
12: AddressType.tezos,
};
Id _addressGetId(Address object) {

View file

@ -22,7 +22,6 @@ part 'transaction.g.dart';
@Collection()
class Transaction {
Transaction({
required this.walletId,
required this.txid,
@ -252,5 +251,9 @@ enum TransactionSubType {
bip47Notification, // bip47 payment code notification transaction flag
mint, // firo specific
join, // firo specific
ethToken; // eth token
ethToken, // eth token
cashFusion,
sparkMint, // firo specific
sparkSpend, // firo specific
ordinal;
}

View file

@ -364,6 +364,10 @@ const _TransactionsubTypeEnumValueMap = {
'mint': 2,
'join': 3,
'ethToken': 4,
'cashFusion': 5,
'sparkMint': 6,
'sparkSpend': 7,
'ordinal': 8,
};
const _TransactionsubTypeValueEnumMap = {
0: TransactionSubType.none,
@ -371,6 +375,10 @@ const _TransactionsubTypeValueEnumMap = {
2: TransactionSubType.mint,
3: TransactionSubType.join,
4: TransactionSubType.ethToken,
5: TransactionSubType.cashFusion,
6: TransactionSubType.sparkMint,
7: TransactionSubType.sparkSpend,
8: TransactionSubType.ordinal,
};
const _TransactiontypeEnumValueMap = {
'outgoing': 0,

View file

@ -0,0 +1,164 @@
import 'dart:convert';
import 'package:isar/isar.dart';
part 'input_v2.g.dart';
@Embedded()
class OutpointV2 {
late final String txid;
late final int vout;
OutpointV2();
static OutpointV2 isarCantDoRequiredInDefaultConstructor({
required String txid,
required int vout,
}) =>
OutpointV2()
..vout = vout
..txid = txid;
@override
String toString() {
return 'OutpointV2(\n'
' txid: $txid,\n'
' vout: $vout,\n'
')';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is OutpointV2 && other.txid == txid && other.vout == vout;
}
@override
int get hashCode {
return Object.hash(
txid.hashCode,
vout.hashCode,
);
}
}
@Embedded()
class InputV2 {
late final String? scriptSigHex;
late final String? scriptSigAsm;
late final int? sequence;
late final OutpointV2? outpoint;
late final List<String> addresses;
late final String valueStringSats;
late final String? coinbase;
late final String? witness;
late final String? innerRedeemScriptAsm;
late final bool walletOwns;
@ignore
BigInt get value => BigInt.parse(valueStringSats);
InputV2();
static InputV2 isarCantDoRequiredInDefaultConstructor({
required String? scriptSigHex,
required String? scriptSigAsm,
required int? sequence,
required OutpointV2? outpoint,
required List<String> addresses,
required String valueStringSats,
required String? witness,
required String? innerRedeemScriptAsm,
required String? coinbase,
required bool walletOwns,
}) =>
InputV2()
..scriptSigHex = scriptSigHex
..scriptSigAsm = scriptSigAsm
..sequence = sequence
..outpoint = outpoint
..addresses = List.unmodifiable(addresses)
..valueStringSats = valueStringSats
..witness = witness
..innerRedeemScriptAsm = innerRedeemScriptAsm
..coinbase = coinbase
..walletOwns = walletOwns;
static InputV2 fromElectrumxJson({
required Map<String, dynamic> json,
required OutpointV2? outpoint,
required List<String> addresses,
required String valueStringSats,
required String? coinbase,
required bool walletOwns,
}) {
final dynamicWitness = json["witness"] ?? json["txinwitness"];
final String? witness;
if (dynamicWitness is Map || dynamicWitness is List) {
witness = jsonEncode(dynamicWitness);
} else if (dynamicWitness is String) {
witness = dynamicWitness;
} else {
witness = null;
}
return InputV2()
..scriptSigHex = json["scriptSig"]?["hex"] as String?
..scriptSigAsm = json["scriptSig"]?["asm"] as String?
..sequence = json["sequence"] as int?
..outpoint = outpoint
..addresses = List.unmodifiable(addresses)
..valueStringSats = valueStringSats
..witness = witness
..innerRedeemScriptAsm = json["innerRedeemscriptAsm"] as String?
..coinbase = coinbase
..walletOwns = walletOwns;
}
InputV2 copyWith({
String? scriptSigHex,
String? scriptSigAsm,
int? sequence,
OutpointV2? outpoint,
List<String>? addresses,
String? valueStringSats,
String? coinbase,
String? witness,
String? innerRedeemScriptAsm,
bool? walletOwns,
}) {
return InputV2.isarCantDoRequiredInDefaultConstructor(
scriptSigHex: scriptSigHex ?? this.scriptSigHex,
scriptSigAsm: scriptSigAsm ?? this.scriptSigAsm,
sequence: sequence ?? this.sequence,
outpoint: outpoint ?? this.outpoint,
addresses: addresses ?? this.addresses,
valueStringSats: valueStringSats ?? this.valueStringSats,
coinbase: coinbase ?? this.coinbase,
witness: witness ?? this.witness,
innerRedeemScriptAsm: innerRedeemScriptAsm ?? this.innerRedeemScriptAsm,
walletOwns: walletOwns ?? this.walletOwns,
);
}
@override
String toString() {
return 'InputV2(\n'
' scriptSigHex: $scriptSigHex,\n'
' scriptSigAsm: $scriptSigAsm,\n'
' sequence: $sequence,\n'
' outpoint: $outpoint,\n'
' addresses: $addresses,\n'
' valueStringSats: $valueStringSats,\n'
' coinbase: $coinbase,\n'
' witness: $witness,\n'
' innerRedeemScriptAsm: $innerRedeemScriptAsm,\n'
' walletOwns: $walletOwns,\n'
')';
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,115 @@
import 'package:decimal/decimal.dart';
import 'package:isar/isar.dart';
part 'output_v2.g.dart';
@Embedded()
class OutputV2 {
late final String scriptPubKeyHex;
late final String? scriptPubKeyAsm;
late final String valueStringSats;
late final List<String> addresses;
late final bool walletOwns;
@ignore
BigInt get value => BigInt.parse(valueStringSats);
OutputV2();
static OutputV2 isarCantDoRequiredInDefaultConstructor({
required String scriptPubKeyHex,
String? scriptPubKeyAsm,
required String valueStringSats,
required List<String> addresses,
required bool walletOwns,
}) =>
OutputV2()
..scriptPubKeyHex = scriptPubKeyHex
..scriptPubKeyAsm = scriptPubKeyAsm
..valueStringSats = valueStringSats
..walletOwns = walletOwns
..addresses = List.unmodifiable(addresses);
OutputV2 copyWith({
String? scriptPubKeyHex,
String? scriptPubKeyAsm,
String? valueStringSats,
List<String>? addresses,
bool? walletOwns,
}) {
return OutputV2.isarCantDoRequiredInDefaultConstructor(
scriptPubKeyHex: scriptPubKeyHex ?? this.scriptPubKeyHex,
scriptPubKeyAsm: scriptPubKeyAsm ?? this.scriptPubKeyAsm,
valueStringSats: valueStringSats ?? this.valueStringSats,
addresses: addresses ?? this.addresses,
walletOwns: walletOwns ?? this.walletOwns,
);
}
static OutputV2 fromElectrumXJson(
Map<String, dynamic> json, {
required bool walletOwns,
required int decimalPlaces,
bool isFullAmountNotSats = false,
}) {
try {
List<String> addresses = [];
if (json["scriptPubKey"]?["addresses"] is List) {
for (final e in json["scriptPubKey"]["addresses"] as List) {
addresses.add(e as String);
}
} else if (json["scriptPubKey"]?["address"] is String) {
addresses.add(json["scriptPubKey"]?["address"] as String);
}
return OutputV2.isarCantDoRequiredInDefaultConstructor(
scriptPubKeyHex: json["scriptPubKey"]["hex"] as String,
scriptPubKeyAsm: json["scriptPubKey"]["asm"] as String?,
valueStringSats: parseOutputAmountString(
json["value"].toString(),
decimalPlaces: decimalPlaces,
isFullAmountNotSats: isFullAmountNotSats,
),
addresses: addresses,
walletOwns: walletOwns,
);
} catch (e) {
throw Exception("Failed to parse OutputV2 from $json");
}
}
static String parseOutputAmountString(
String amount, {
required int decimalPlaces,
bool isFullAmountNotSats = false,
}) {
final temp = Decimal.parse(amount);
if (temp < Decimal.zero) {
throw Exception("Negative value found");
}
final String valueStringSats;
if (isFullAmountNotSats) {
valueStringSats = temp.shift(decimalPlaces).toBigInt().toString();
} else if (temp.isInteger) {
valueStringSats = temp.toString();
} else {
valueStringSats = temp.shift(decimalPlaces).toBigInt().toString();
}
return valueStringSats;
}
@override
String toString() {
return 'OutputV2(\n'
' scriptPubKeyHex: $scriptPubKeyHex,\n'
' scriptPubKeyAsm: $scriptPubKeyAsm,\n'
' value: $value,\n'
' walletOwns: $walletOwns,\n'
' addresses: $addresses,\n'
')';
}
}

View file

@ -0,0 +1,786 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'output_v2.dart';
// **************************************************************************
// IsarEmbeddedGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters
const OutputV2Schema = Schema(
name: r'OutputV2',
id: -6134367361914065515,
properties: {
r'addresses': PropertySchema(
id: 0,
name: r'addresses',
type: IsarType.stringList,
),
r'scriptPubKeyAsm': PropertySchema(
id: 1,
name: r'scriptPubKeyAsm',
type: IsarType.string,
),
r'scriptPubKeyHex': PropertySchema(
id: 2,
name: r'scriptPubKeyHex',
type: IsarType.string,
),
r'valueStringSats': PropertySchema(
id: 3,
name: r'valueStringSats',
type: IsarType.string,
),
r'walletOwns': PropertySchema(
id: 4,
name: r'walletOwns',
type: IsarType.bool,
)
},
estimateSize: _outputV2EstimateSize,
serialize: _outputV2Serialize,
deserialize: _outputV2Deserialize,
deserializeProp: _outputV2DeserializeProp,
);
int _outputV2EstimateSize(
OutputV2 object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.addresses.length * 3;
{
for (var i = 0; i < object.addresses.length; i++) {
final value = object.addresses[i];
bytesCount += value.length * 3;
}
}
{
final value = object.scriptPubKeyAsm;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
bytesCount += 3 + object.scriptPubKeyHex.length * 3;
bytesCount += 3 + object.valueStringSats.length * 3;
return bytesCount;
}
void _outputV2Serialize(
OutputV2 object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeStringList(offsets[0], object.addresses);
writer.writeString(offsets[1], object.scriptPubKeyAsm);
writer.writeString(offsets[2], object.scriptPubKeyHex);
writer.writeString(offsets[3], object.valueStringSats);
writer.writeBool(offsets[4], object.walletOwns);
}
OutputV2 _outputV2Deserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = OutputV2();
object.addresses = reader.readStringList(offsets[0]) ?? [];
object.scriptPubKeyAsm = reader.readStringOrNull(offsets[1]);
object.scriptPubKeyHex = reader.readString(offsets[2]);
object.valueStringSats = reader.readString(offsets[3]);
object.walletOwns = reader.readBool(offsets[4]);
return object;
}
P _outputV2DeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readStringList(offset) ?? []) as P;
case 1:
return (reader.readStringOrNull(offset)) as P;
case 2:
return (reader.readString(offset)) as P;
case 3:
return (reader.readString(offset)) as P;
case 4:
return (reader.readBool(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
extension OutputV2QueryFilter
on QueryBuilder<OutputV2, OutputV2, QFilterCondition> {
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesElementEqualTo(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'addresses',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesElementGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'addresses',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesElementLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'addresses',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesElementBetween(
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'addresses',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesElementStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'addresses',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesElementEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'addresses',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesElementContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'addresses',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesElementMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'addresses',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesElementIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'addresses',
value: '',
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesElementIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'addresses',
value: '',
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesLengthEqualTo(int length) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'addresses',
length,
true,
length,
true,
);
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition> addressesIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'addresses',
0,
true,
0,
true,
);
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'addresses',
0,
false,
999999,
true,
);
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesLengthLessThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'addresses',
0,
true,
length,
include,
);
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesLengthGreaterThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'addresses',
length,
include,
999999,
true,
);
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
addressesLengthBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'addresses',
lower,
includeLower,
upper,
includeUpper,
);
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyAsmIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'scriptPubKeyAsm',
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyAsmIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'scriptPubKeyAsm',
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyAsmEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'scriptPubKeyAsm',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyAsmGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'scriptPubKeyAsm',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyAsmLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'scriptPubKeyAsm',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyAsmBetween(
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'scriptPubKeyAsm',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyAsmStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'scriptPubKeyAsm',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyAsmEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'scriptPubKeyAsm',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyAsmContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'scriptPubKeyAsm',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyAsmMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'scriptPubKeyAsm',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyAsmIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'scriptPubKeyAsm',
value: '',
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyAsmIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'scriptPubKeyAsm',
value: '',
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyHexEqualTo(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'scriptPubKeyHex',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyHexGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'scriptPubKeyHex',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyHexLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'scriptPubKeyHex',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyHexBetween(
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'scriptPubKeyHex',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyHexStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'scriptPubKeyHex',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyHexEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'scriptPubKeyHex',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyHexContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'scriptPubKeyHex',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyHexMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'scriptPubKeyHex',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyHexIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'scriptPubKeyHex',
value: '',
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
scriptPubKeyHexIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'scriptPubKeyHex',
value: '',
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
valueStringSatsEqualTo(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'valueStringSats',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
valueStringSatsGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'valueStringSats',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
valueStringSatsLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'valueStringSats',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
valueStringSatsBetween(
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'valueStringSats',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
valueStringSatsStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'valueStringSats',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
valueStringSatsEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'valueStringSats',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
valueStringSatsContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'valueStringSats',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
valueStringSatsMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'valueStringSats',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
valueStringSatsIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'valueStringSats',
value: '',
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition>
valueStringSatsIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'valueStringSats',
value: '',
));
});
}
QueryBuilder<OutputV2, OutputV2, QAfterFilterCondition> walletOwnsEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'walletOwns',
value: value,
));
});
}
}
extension OutputV2QueryObject
on QueryBuilder<OutputV2, OutputV2, QFilterCondition> {}

View file

@ -0,0 +1,251 @@
import 'dart:convert';
import 'dart:math';
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/extensions/extensions.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
part 'transaction_v2.g.dart';
@Collection()
class TransactionV2 {
Id id = Isar.autoIncrement;
@Index()
final String walletId;
@Index(unique: true, composite: [CompositeIndex("walletId")])
final String txid;
final String hash;
@Index()
late final int timestamp;
final int? height;
final String? blockHash;
final int version;
final List<InputV2> inputs;
final List<OutputV2> outputs;
@enumerated
final TransactionType type;
@enumerated
final TransactionSubType subType;
final String? otherData;
TransactionV2({
required this.walletId,
required this.blockHash,
required this.hash,
required this.txid,
required this.timestamp,
required this.height,
required this.inputs,
required this.outputs,
required this.version,
required this.type,
required this.subType,
required this.otherData,
});
bool get isEpiccashTransaction =>
_getFromOtherData(key: "isEpiccashTransaction") == true;
int? get numberOfMessages =>
_getFromOtherData(key: "numberOfMessages") as int?;
String? get slateId => _getFromOtherData(key: "slateId") as String?;
String? get onChainNote => _getFromOtherData(key: "onChainNote") as String?;
bool get isCancelled => _getFromOtherData(key: "isCancelled") == true;
String? get contractAddress =>
_getFromOtherData(key: "contractAddress") as String?;
int? get nonce => _getFromOtherData(key: "nonce") as int?;
int getConfirmations(int currentChainHeight) {
if (height == null || height! <= 0) return 0;
return max(0, currentChainHeight - (height! - 1));
}
bool isConfirmed(int currentChainHeight, int minimumConfirms) {
final confirmations = getConfirmations(currentChainHeight);
return confirmations >= minimumConfirms;
}
Amount getFee({required int fractionDigits}) {
// check for override fee
final fee = _getOverrideFee();
if (fee != null) {
return fee;
}
final inSum =
inputs.map((e) => e.value).reduce((value, element) => value += element);
final outSum = outputs
.map((e) => e.value)
.reduce((value, element) => value += element);
return Amount(rawValue: inSum - outSum, fractionDigits: fractionDigits);
}
Amount getAmountReceivedInThisWallet({required int fractionDigits}) {
final outSum = outputs
.where((e) => e.walletOwns)
.fold(BigInt.zero, (p, e) => p + e.value);
return Amount(rawValue: outSum, fractionDigits: fractionDigits);
}
Amount getAmountSparkSelfMinted({required int fractionDigits}) {
final outSum = outputs.where((e) {
final op = e.scriptPubKeyHex.substring(0, 2).toUint8ListFromHex.first;
return e.walletOwns && (op == OP_SPARKMINT);
}).fold(BigInt.zero, (p, e) => p + e.value);
return Amount(rawValue: outSum, fractionDigits: fractionDigits);
}
Amount getAmountSentFromThisWallet({required int fractionDigits}) {
final inSum = inputs
.where((e) => e.walletOwns)
.fold(BigInt.zero, (p, e) => p + e.value);
Amount amount = Amount(
rawValue: inSum,
fractionDigits: fractionDigits,
) -
getAmountReceivedInThisWallet(
fractionDigits: fractionDigits,
);
if (subType != TransactionSubType.ethToken) {
amount = amount - getFee(fractionDigits: fractionDigits);
}
// negative amounts are likely an error or can happen with coins such as eth
// that don't use the btc style inputs/outputs
if (amount.raw < BigInt.zero) {
return Amount.zeroWith(fractionDigits: fractionDigits);
}
return amount;
}
Set<String> associatedAddresses() => {
...inputs.map((e) => e.addresses).expand((e) => e),
...outputs.map((e) => e.addresses).expand((e) => e),
};
Amount? _getOverrideFee() {
try {
return Amount.fromSerializedJsonString(
_getFromOtherData(key: "overrideFee") as String,
);
} catch (_) {
return null;
}
}
String statusLabel({
required int currentChainHeight,
required int minConfirms,
}) {
if (subType == TransactionSubType.cashFusion ||
subType == TransactionSubType.mint ||
(subType == TransactionSubType.sparkMint &&
type == TransactionType.sentToSelf)) {
if (isConfirmed(currentChainHeight, minConfirms)) {
return "Anonymized";
} else {
return "Anonymizing";
}
}
if (isEpiccashTransaction) {
if (slateId == null) {
return "Restored Funds";
}
if (isCancelled) {
return "Cancelled";
} else if (type == TransactionType.incoming) {
if (isConfirmed(currentChainHeight, minConfirms)) {
return "Received";
} else {
if (numberOfMessages == 1) {
return "Receiving (waiting for sender)";
} else if ((numberOfMessages ?? 0) > 1) {
return "Receiving (waiting for confirmations)"; // TODO test if the sender still has to open again after the receiver has 2 messages present, ie. sender->receiver->sender->node (yes) vs. sender->receiver->node (no)
} else {
return "Receiving";
}
}
} else if (type == TransactionType.outgoing) {
if (isConfirmed(currentChainHeight, minConfirms)) {
return "Sent (confirmed)";
} else {
if (numberOfMessages == 1) {
return "Sending (waiting for receiver)";
} else if ((numberOfMessages ?? 0) > 1) {
return "Sending (waiting for confirmations)";
} else {
return "Sending";
}
}
}
}
if (type == TransactionType.incoming) {
// if (_transaction.isMinting) {
// return "Minting";
// } else
if (isConfirmed(currentChainHeight, minConfirms)) {
return "Received";
} else {
return "Receiving";
}
} else if (type == TransactionType.outgoing) {
if (isConfirmed(currentChainHeight, minConfirms)) {
return "Sent";
} else {
return "Sending";
}
} else if (type == TransactionType.sentToSelf) {
return "Sent to self";
} else {
return type.name;
}
}
dynamic _getFromOtherData({required dynamic key}) {
if (otherData == null) {
return null;
}
final map = jsonDecode(otherData!);
return map[key];
}
@override
String toString() {
return 'TransactionV2(\n'
' walletId: $walletId,\n'
' hash: $hash,\n'
' txid: $txid,\n'
' type: $type,\n'
' subType: $subType,\n'
' timestamp: $timestamp,\n'
' height: $height,\n'
' blockHash: $blockHash,\n'
' version: $version,\n'
' inputs: $inputs,\n'
' outputs: $outputs,\n'
' otherData: $otherData,\n'
')';
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,81 @@
import 'package:isar/isar.dart';
part 'lelantus_coin.g.dart';
@collection
class LelantusCoin {
Id id = Isar.autoIncrement;
@Index()
final String walletId;
final String txid;
final String value; // can't use BigInt in isar :shrug:
@Index(
unique: true,
replace: false,
composite: [
CompositeIndex("walletId"),
],
)
final int mintIndex;
final int anonymitySetId;
final bool isUsed;
final bool isJMint;
final String? otherData;
LelantusCoin({
required this.walletId,
required this.txid,
required this.value,
required this.mintIndex,
required this.anonymitySetId,
required this.isUsed,
required this.isJMint,
required this.otherData,
});
LelantusCoin copyWith({
String? walletId,
String? publicCoin,
String? txid,
String? value,
int? mintIndex,
int? anonymitySetId,
bool? isUsed,
bool? isJMint,
String? otherData,
}) {
return LelantusCoin(
walletId: walletId ?? this.walletId,
txid: txid ?? this.txid,
value: value ?? this.value,
mintIndex: mintIndex ?? this.mintIndex,
anonymitySetId: anonymitySetId ?? this.anonymitySetId,
isUsed: isUsed ?? this.isUsed,
isJMint: isJMint ?? this.isJMint,
otherData: otherData ?? this.otherData,
);
}
@override
String toString() {
return 'LelantusCoin{'
'id: $id, '
'walletId: $walletId, '
'txid: $txid, '
'value: $value, '
'mintIndex: $mintIndex, '
'anonymitySetId: $anonymitySetId, '
'otherData: $otherData, '
'isJMint: $isJMint, '
'isUsed: $isUsed'
'}';
}
}

File diff suppressed because it is too large Load diff

View file

@ -15,5 +15,6 @@ export 'blockchain_data/output.dart';
export 'blockchain_data/transaction.dart';
export 'blockchain_data/utxo.dart';
export 'ethereum/eth_contract.dart';
export 'firo_specific/lelantus_coin.dart';
export 'log.dart';
export 'transaction_note.dart';

View file

@ -25,8 +25,22 @@ class TransactionNote {
@Index()
late String walletId;
@Index(unique: true, composite: [CompositeIndex("walletId")])
@Index(
unique: true,
replace: true,
composite: [CompositeIndex("walletId")],
)
late String txid;
late String value;
TransactionNote copyWith({
String? value,
}) {
return TransactionNote(
walletId: walletId,
txid: txid,
value: value ?? this.value,
);
}
}

View file

@ -56,7 +56,7 @@ const TransactionNoteSchema = CollectionSchema(
id: -2771771174176035985,
name: r'txid_walletId',
unique: true,
replace: false,
replace: true,
properties: [
IndexPropertySchema(
name: r'txid',

View file

@ -0,0 +1,89 @@
import 'package:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/dto/ordinals/inscription_data.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
part 'ordinal.g.dart';
@collection
class Ordinal {
Id id = Isar.autoIncrement;
final String walletId;
@Index(unique: true, replace: true, composite: [
CompositeIndex("utxoTXID"),
CompositeIndex("utxoVOUT"),
])
final String inscriptionId;
final int inscriptionNumber;
final String content;
// following two are used to look up the UTXO object in isar combined w/ walletId
final String utxoTXID;
final int utxoVOUT;
Ordinal({
required this.walletId,
required this.inscriptionId,
required this.inscriptionNumber,
required this.content,
required this.utxoTXID,
required this.utxoVOUT,
});
factory Ordinal.fromInscriptionData(InscriptionData data, String walletId) {
return Ordinal(
walletId: walletId,
inscriptionId: data.inscriptionId,
inscriptionNumber: data.inscriptionNumber,
content: data.content,
utxoTXID: data.output.split(':')[
0], // "output": "062f32e21aa04246b8873b5d9a929576addd0339881e1ea478b406795d6b6c47:0"
utxoVOUT: int.parse(data.output.split(':')[1]),
);
}
Ordinal copyWith({
String? walletId,
String? inscriptionId,
int? inscriptionNumber,
String? content,
String? utxoTXID,
int? utxoVOUT,
}) {
return Ordinal(
walletId: walletId ?? this.walletId,
inscriptionId: inscriptionId ?? this.inscriptionId,
inscriptionNumber: inscriptionNumber ?? this.inscriptionNumber,
content: content ?? this.content,
utxoTXID: utxoTXID ?? this.utxoTXID,
utxoVOUT: utxoVOUT ?? this.utxoVOUT,
);
}
UTXO? getUTXO(MainDB db) {
return db.isar.utxos
.where()
.walletIdEqualTo(walletId)
.filter()
.txidEqualTo(utxoTXID)
.and()
.voutEqualTo(utxoVOUT)
.findFirstSync();
}
@override
String toString() {
return 'Ordinal {'
' walletId: $walletId,'
' inscriptionId: $inscriptionId,'
' inscriptionNumber: $inscriptionNumber,'
' content: $content,'
' utxoTXID: $utxoTXID,'
' utxoVOUT: $utxoVOUT'
' }';
}
}

File diff suppressed because it is too large Load diff

View file

@ -45,7 +45,7 @@ class StackTheme {
case "dark":
return Brightness.dark;
default:
// just return light instead of a possible crash causing error
// just return light instead of a possible crash causing error
return Brightness.light;
}
}
@ -131,8 +131,8 @@ class StackTheme {
@ignore
Color get accentColorBlue => _accentColorBlue ??= Color(
accentColorBlueInt,
);
accentColorBlueInt,
);
@ignore
Color? _accentColorBlue;
late final int accentColorBlueInt;
@ -141,8 +141,8 @@ class StackTheme {
@ignore
Color get accentColorGreen => _accentColorGreen ??= Color(
accentColorGreenInt,
);
accentColorGreenInt,
);
@ignore
Color? _accentColorGreen;
late final int accentColorGreenInt;
@ -151,8 +151,8 @@ class StackTheme {
@ignore
Color get accentColorYellow => _accentColorYellow ??= Color(
accentColorYellowInt,
);
accentColorYellowInt,
);
@ignore
Color? _accentColorYellow;
late final int accentColorYellowInt;
@ -161,8 +161,8 @@ class StackTheme {
@ignore
Color get accentColorRed => _accentColorRed ??= Color(
accentColorRedInt,
);
accentColorRedInt,
);
@ignore
Color? _accentColorRed;
late final int accentColorRedInt;
@ -171,8 +171,8 @@ class StackTheme {
@ignore
Color get accentColorOrange => _accentColorOrange ??= Color(
accentColorOrangeInt,
);
accentColorOrangeInt,
);
@ignore
Color? _accentColorOrange;
late final int accentColorOrangeInt;
@ -181,8 +181,8 @@ class StackTheme {
@ignore
Color get accentColorDark => _accentColorDark ??= Color(
accentColorDarkInt,
);
accentColorDarkInt,
);
@ignore
Color? _accentColorDark;
late final int accentColorDarkInt;
@ -191,8 +191,8 @@ class StackTheme {
@ignore
Color get shadow => _shadow ??= Color(
shadowInt,
);
shadowInt,
);
@ignore
Color? _shadow;
late final int shadowInt;
@ -201,8 +201,8 @@ class StackTheme {
@ignore
Color get textDark => _textDark ??= Color(
textDarkInt,
);
textDarkInt,
);
@ignore
Color? _textDark;
late final int textDarkInt;
@ -211,8 +211,8 @@ class StackTheme {
@ignore
Color get textDark2 => _textDark2 ??= Color(
textDark2Int,
);
textDark2Int,
);
@ignore
Color? _textDark2;
late final int textDark2Int;
@ -221,8 +221,8 @@ class StackTheme {
@ignore
Color get textDark3 => _textDark3 ??= Color(
textDark3Int,
);
textDark3Int,
);
@ignore
Color? _textDark3;
late final int textDark3Int;
@ -231,8 +231,8 @@ class StackTheme {
@ignore
Color get textSubtitle1 => _textSubtitle1 ??= Color(
textSubtitle1Int,
);
textSubtitle1Int,
);
@ignore
Color? _textSubtitle1;
late final int textSubtitle1Int;
@ -241,8 +241,8 @@ class StackTheme {
@ignore
Color get textSubtitle2 => _textSubtitle2 ??= Color(
textSubtitle2Int,
);
textSubtitle2Int,
);
@ignore
Color? _textSubtitle2;
late final int textSubtitle2Int;
@ -251,8 +251,8 @@ class StackTheme {
@ignore
Color get textSubtitle3 => _textSubtitle3 ??= Color(
textSubtitle3Int,
);
textSubtitle3Int,
);
@ignore
Color? _textSubtitle3;
late final int textSubtitle3Int;
@ -261,8 +261,8 @@ class StackTheme {
@ignore
Color get textSubtitle4 => _textSubtitle4 ??= Color(
textSubtitle4Int,
);
textSubtitle4Int,
);
@ignore
Color? _textSubtitle4;
late final int textSubtitle4Int;
@ -271,8 +271,8 @@ class StackTheme {
@ignore
Color get textSubtitle5 => _textSubtitle5 ??= Color(
textSubtitle5Int,
);
textSubtitle5Int,
);
@ignore
Color? _textSubtitle5;
late final int textSubtitle5Int;
@ -281,8 +281,8 @@ class StackTheme {
@ignore
Color get textSubtitle6 => _textSubtitle6 ??= Color(
textSubtitle6Int,
);
textSubtitle6Int,
);
@ignore
Color? _textSubtitle6;
late final int textSubtitle6Int;
@ -291,8 +291,8 @@ class StackTheme {
@ignore
Color get textWhite => _textWhite ??= Color(
textWhiteInt,
);
textWhiteInt,
);
@ignore
Color? _textWhite;
late final int textWhiteInt;
@ -301,8 +301,8 @@ class StackTheme {
@ignore
Color get textFavoriteCard => _textFavoriteCard ??= Color(
textFavoriteCardInt,
);
textFavoriteCardInt,
);
@ignore
Color? _textFavoriteCard;
late final int textFavoriteCardInt;
@ -311,8 +311,8 @@ class StackTheme {
@ignore
Color get textError => _textError ??= Color(
textErrorInt,
);
textErrorInt,
);
@ignore
Color? _textError;
late final int textErrorInt;
@ -321,8 +321,8 @@ class StackTheme {
@ignore
Color get textRestore => _textRestore ??= Color(
textRestoreInt,
);
textRestoreInt,
);
@ignore
Color? _textRestore;
late final int textRestoreInt;
@ -331,8 +331,8 @@ class StackTheme {
@ignore
Color get buttonBackPrimary => _buttonBackPrimary ??= Color(
buttonBackPrimaryInt,
);
buttonBackPrimaryInt,
);
@ignore
Color? _buttonBackPrimary;
late final int buttonBackPrimaryInt;
@ -341,8 +341,8 @@ class StackTheme {
@ignore
Color get buttonBackSecondary => _buttonBackSecondary ??= Color(
buttonBackSecondaryInt,
);
buttonBackSecondaryInt,
);
@ignore
Color? _buttonBackSecondary;
late final int buttonBackSecondaryInt;
@ -351,8 +351,8 @@ class StackTheme {
@ignore
Color get buttonBackPrimaryDisabled => _buttonBackPrimaryDisabled ??= Color(
buttonBackPrimaryDisabledInt,
);
buttonBackPrimaryDisabledInt,
);
@ignore
Color? _buttonBackPrimaryDisabled;
late final int buttonBackPrimaryDisabledInt;
@ -372,8 +372,8 @@ class StackTheme {
@ignore
Color get buttonBackBorder => _buttonBackBorder ??= Color(
buttonBackBorderInt,
);
buttonBackBorderInt,
);
@ignore
Color? _buttonBackBorder;
late final int buttonBackBorderInt;
@ -382,8 +382,8 @@ class StackTheme {
@ignore
Color get buttonBackBorderDisabled => _buttonBackBorderDisabled ??= Color(
buttonBackBorderDisabledInt,
);
buttonBackBorderDisabledInt,
);
@ignore
Color? _buttonBackBorderDisabled;
late final int buttonBackBorderDisabledInt;
@ -392,8 +392,8 @@ class StackTheme {
@ignore
Color get buttonBackBorderSecondary => _buttonBackBorderSecondary ??= Color(
buttonBackBorderSecondaryInt,
);
buttonBackBorderSecondaryInt,
);
@ignore
Color? _buttonBackBorderSecondary;
late final int buttonBackBorderSecondaryInt;
@ -413,8 +413,8 @@ class StackTheme {
@ignore
Color get numberBackDefault => _numberBackDefault ??= Color(
numberBackDefaultInt,
);
numberBackDefaultInt,
);
@ignore
Color? _numberBackDefault;
late final int numberBackDefaultInt;
@ -423,8 +423,8 @@ class StackTheme {
@ignore
Color get numpadBackDefault => _numpadBackDefault ??= Color(
numpadBackDefaultInt,
);
numpadBackDefaultInt,
);
@ignore
Color? _numpadBackDefault;
late final int numpadBackDefaultInt;
@ -433,8 +433,8 @@ class StackTheme {
@ignore
Color get bottomNavBack => _bottomNavBack ??= Color(
bottomNavBackInt,
);
bottomNavBackInt,
);
@ignore
Color? _bottomNavBack;
late final int bottomNavBackInt;
@ -443,8 +443,8 @@ class StackTheme {
@ignore
Color get buttonTextPrimary => _buttonTextPrimary ??= Color(
buttonTextPrimaryInt,
);
buttonTextPrimaryInt,
);
@ignore
Color? _buttonTextPrimary;
late final int buttonTextPrimaryInt;
@ -453,8 +453,8 @@ class StackTheme {
@ignore
Color get buttonTextSecondary => _buttonTextSecondary ??= Color(
buttonTextSecondaryInt,
);
buttonTextSecondaryInt,
);
@ignore
Color? _buttonTextSecondary;
late final int buttonTextSecondaryInt;
@ -463,8 +463,8 @@ class StackTheme {
@ignore
Color get buttonTextPrimaryDisabled => _buttonTextPrimaryDisabled ??= Color(
buttonTextPrimaryDisabledInt,
);
buttonTextPrimaryDisabledInt,
);
@ignore
Color? _buttonTextPrimaryDisabled;
late final int buttonTextPrimaryDisabledInt;
@ -1517,117 +1517,117 @@ class StackTheme {
..version = version
..assetsV1 = version == 1
? ThemeAssets.fromJson(
json: Map<String, dynamic>.from(json["assets"] as Map),
themeId: json["id"] as String,
)
json: Map<String, dynamic>.from(json["assets"] as Map),
themeId: json["id"] as String,
)
: null
..assetsV2 = version == 2
? ThemeAssetsV2.fromJson(
json: Map<String, dynamic>.from(json["assets"] as Map),
themeId: json["id"] as String,
)
json: Map<String, dynamic>.from(json["assets"] as Map),
themeId: json["id"] as String,
)
: null
..assetsV3 = version >= 3
? ThemeAssetsV3.fromJson(
json: Map<String, dynamic>.from(json["assets"] as Map),
themeId: json["id"] as String,
)
json: Map<String, dynamic>.from(json["assets"] as Map),
themeId: json["id"] as String,
)
: null
..themeId = json["id"] as String
..name = json["name"] as String
..brightnessString = json["brightness"] as String
..backgroundInt = parseColor(json["colors"]["background"] as String)
..backgroundAppBarInt =
parseColor(json["colors"]["background_app_bar"] as String)
parseColor(json["colors"]["background_app_bar"] as String)
..gradientBackgroundString = json["colors"]["gradients"] != null
? jsonEncode(json["colors"]["gradients"])
: null
..standardBoxShadowString =
jsonEncode(json["colors"]["box_shadows"]["standard"] as Map)
jsonEncode(json["colors"]["box_shadows"]["standard"] as Map)
..homeViewButtonBarBoxShadowString =
json["colors"]["box_shadows"]["home_view_button_bar"] == null
? null
: jsonEncode(
json["colors"]["box_shadows"]["home_view_button_bar"] as Map)
json["colors"]["box_shadows"]["home_view_button_bar"] == null
? null
: jsonEncode(
json["colors"]["box_shadows"]["home_view_button_bar"] as Map)
..coinColorsJsonString = jsonEncode(json["colors"]['coin'] as Map)
..overlayInt = parseColor(json["colors"]["overlay"] as String)
..accentColorBlueInt =
parseColor(json["colors"]["accent_color_blue"] as String)
parseColor(json["colors"]["accent_color_blue"] as String)
..accentColorGreenInt =
parseColor(json["colors"]["accent_color_green"] as String)
parseColor(json["colors"]["accent_color_green"] as String)
..accentColorYellowInt =
parseColor(json["colors"]["accent_color_yellow"] as String)
parseColor(json["colors"]["accent_color_yellow"] as String)
..accentColorRedInt =
parseColor(json["colors"]["accent_color_red"] as String)
parseColor(json["colors"]["accent_color_red"] as String)
..accentColorOrangeInt =
parseColor(json["colors"]["accent_color_orange"] as String)
parseColor(json["colors"]["accent_color_orange"] as String)
..accentColorDarkInt =
parseColor(json["colors"]["accent_color_dark"] as String)
parseColor(json["colors"]["accent_color_dark"] as String)
..shadowInt = parseColor(json["colors"]["shadow"] as String)
..textDarkInt = parseColor(json["colors"]["text_dark_one"] as String)
..textDark2Int = parseColor(json["colors"]["text_dark_two"] as String)
..textDark3Int = parseColor(json["colors"]["text_dark_three"] as String)
..textWhiteInt = parseColor(json["colors"]["text_white"] as String)
..textFavoriteCardInt =
parseColor(json["colors"]["text_favorite"] as String)
parseColor(json["colors"]["text_favorite"] as String)
..textErrorInt = parseColor(json["colors"]["text_error"] as String)
..textRestoreInt = parseColor(json["colors"]["text_restore"] as String)
..buttonBackPrimaryInt =
parseColor(json["colors"]["button_back_primary"] as String)
parseColor(json["colors"]["button_back_primary"] as String)
..buttonBackSecondaryInt =
parseColor(json["colors"]["button_back_secondary"] as String)
parseColor(json["colors"]["button_back_secondary"] as String)
..buttonBackPrimaryDisabledInt =
parseColor(json["colors"]["button_back_primary_disabled"] as String)
parseColor(json["colors"]["button_back_primary_disabled"] as String)
..buttonBackSecondaryDisabledInt =
parseColor(json["colors"]["button_back_secondary_disabled"] as String)
parseColor(json["colors"]["button_back_secondary_disabled"] as String)
..buttonBackBorderInt =
parseColor(json["colors"]["button_back_border"] as String)
parseColor(json["colors"]["button_back_border"] as String)
..buttonBackBorderDisabledInt =
parseColor(json["colors"]["button_back_border_disabled"] as String)
parseColor(json["colors"]["button_back_border_disabled"] as String)
..buttonBackBorderSecondaryInt =
parseColor(json["colors"]["button_back_border_secondary"] as String)
parseColor(json["colors"]["button_back_border_secondary"] as String)
..buttonBackBorderSecondaryDisabledInt = parseColor(
json["colors"]["button_back_border_secondary_disabled"] as String)
..numberBackDefaultInt =
parseColor(json["colors"]["number_back_default"] as String)
parseColor(json["colors"]["number_back_default"] as String)
..numpadBackDefaultInt =
parseColor(json["colors"]["numpad_back_default"] as String)
parseColor(json["colors"]["numpad_back_default"] as String)
..bottomNavBackInt =
parseColor(json["colors"]["bottom_nav_back"] as String)
parseColor(json["colors"]["bottom_nav_back"] as String)
..textSubtitle1Int =
parseColor(json["colors"]["text_subtitle_one"] as String)
parseColor(json["colors"]["text_subtitle_one"] as String)
..textSubtitle2Int =
parseColor(json["colors"]["text_subtitle_two"] as String)
parseColor(json["colors"]["text_subtitle_two"] as String)
..textSubtitle3Int =
parseColor(json["colors"]["text_subtitle_three"] as String)
parseColor(json["colors"]["text_subtitle_three"] as String)
..textSubtitle4Int =
parseColor(json["colors"]["text_subtitle_four"] as String)
parseColor(json["colors"]["text_subtitle_four"] as String)
..textSubtitle5Int =
parseColor(json["colors"]["text_subtitle_five"] as String)
parseColor(json["colors"]["text_subtitle_five"] as String)
..textSubtitle6Int =
parseColor(json["colors"]["text_subtitle_six"] as String)
parseColor(json["colors"]["text_subtitle_six"] as String)
..buttonTextPrimaryInt =
parseColor(json["colors"]["button_text_primary"] as String)
parseColor(json["colors"]["button_text_primary"] as String)
..buttonTextSecondaryInt =
parseColor(json["colors"]["button_text_secondary"] as String)
parseColor(json["colors"]["button_text_secondary"] as String)
..buttonTextPrimaryDisabledInt =
parseColor(json["colors"]["button_text_primary_disabled"] as String)
parseColor(json["colors"]["button_text_primary_disabled"] as String)
..buttonTextSecondaryDisabledInt =
parseColor(json["colors"]["button_text_secondary_disabled"] as String)
parseColor(json["colors"]["button_text_secondary_disabled"] as String)
..buttonTextBorderInt =
parseColor(json["colors"]["button_text_border"] as String)
parseColor(json["colors"]["button_text_border"] as String)
..buttonTextDisabledInt =
parseColor(json["colors"]["button_text_disabled"] as String)
parseColor(json["colors"]["button_text_disabled"] as String)
..buttonTextBorderlessInt =
parseColor(json["colors"]["button_text_borderless"] as String)
parseColor(json["colors"]["button_text_borderless"] as String)
..buttonTextBorderlessDisabledInt = parseColor(
json["colors"]["button_text_borderless_disabled"] as String)
..numberTextDefaultInt =
parseColor(json["colors"]["number_text_default"] as String)
parseColor(json["colors"]["number_text_default"] as String)
..numpadTextDefaultInt =
parseColor(json["colors"]["numpad_text_default"] as String)
parseColor(json["colors"]["numpad_text_default"] as String)
..bottomNavTextInt =
parseColor(json["colors"]["bottom_nav_text"] as String)
parseColor(json["colors"]["bottom_nav_text"] as String)
..customTextButtonEnabledTextInt = parseColor(
json["colors"]["custom_text_button_enabled_text"] as String)
..customTextButtonDisabledTextInt = parseColor(
@ -1635,87 +1635,87 @@ class StackTheme {
..switchBGOnInt = parseColor(json["colors"]["switch_bg_on"] as String)
..switchBGOffInt = parseColor(json["colors"]["switch_bg_off"] as String)
..switchBGDisabledInt =
parseColor(json["colors"]["switch_bg_disabled"] as String)
parseColor(json["colors"]["switch_bg_disabled"] as String)
..switchCircleOnInt =
parseColor(json["colors"]["switch_circle_on"] as String)
parseColor(json["colors"]["switch_circle_on"] as String)
..switchCircleOffInt =
parseColor(json["colors"]["switch_circle_off"] as String)
parseColor(json["colors"]["switch_circle_off"] as String)
..switchCircleDisabledInt =
parseColor(json["colors"]["switch_circle_disabled"] as String)
parseColor(json["colors"]["switch_circle_disabled"] as String)
..stepIndicatorBGCheckInt =
parseColor(json["colors"]["step_indicator_bg_check"] as String)
parseColor(json["colors"]["step_indicator_bg_check"] as String)
..stepIndicatorBGNumberInt =
parseColor(json["colors"]["step_indicator_bg_number"] as String)
parseColor(json["colors"]["step_indicator_bg_number"] as String)
..stepIndicatorBGInactiveInt =
parseColor(json["colors"]["step_indicator_bg_inactive"] as String)
parseColor(json["colors"]["step_indicator_bg_inactive"] as String)
..stepIndicatorBGLinesInt =
parseColor(json["colors"]["step_indicator_bg_lines"] as String)
parseColor(json["colors"]["step_indicator_bg_lines"] as String)
..stepIndicatorBGLinesInactiveInt = parseColor(
json["colors"]["step_indicator_bg_lines_inactive"] as String)
..stepIndicatorIconTextInt =
parseColor(json["colors"]["step_indicator_icon_text"] as String)
parseColor(json["colors"]["step_indicator_icon_text"] as String)
..stepIndicatorIconNumberInt =
parseColor(json["colors"]["step_indicator_icon_number"] as String)
parseColor(json["colors"]["step_indicator_icon_number"] as String)
..stepIndicatorIconInactiveInt =
parseColor(json["colors"]["step_indicator_icon_inactive"] as String)
parseColor(json["colors"]["step_indicator_icon_inactive"] as String)
..checkboxBGCheckedInt =
parseColor(json["colors"]["checkbox_bg_checked"] as String)
parseColor(json["colors"]["checkbox_bg_checked"] as String)
..checkboxBorderEmptyInt =
parseColor(json["colors"]["checkbox_border_empty"] as String)
parseColor(json["colors"]["checkbox_border_empty"] as String)
..checkboxBGDisabledInt =
parseColor(json["colors"]["checkbox_bg_disabled"] as String)
parseColor(json["colors"]["checkbox_bg_disabled"] as String)
..checkboxIconCheckedInt =
parseColor(json["colors"]["checkbox_icon_checked"] as String)
parseColor(json["colors"]["checkbox_icon_checked"] as String)
..checkboxIconDisabledInt =
parseColor(json["colors"]["checkbox_icon_disabled"] as String)
parseColor(json["colors"]["checkbox_icon_disabled"] as String)
..checkboxTextLabelInt =
parseColor(json["colors"]["checkbox_text_label"] as String)
parseColor(json["colors"]["checkbox_text_label"] as String)
..snackBarBackSuccessInt =
parseColor(json["colors"]["snack_bar_back_success"] as String)
parseColor(json["colors"]["snack_bar_back_success"] as String)
..snackBarBackErrorInt =
parseColor(json["colors"]["snack_bar_back_error"] as String)
parseColor(json["colors"]["snack_bar_back_error"] as String)
..snackBarBackInfoInt =
parseColor(json["colors"]["snack_bar_back_info"] as String)
parseColor(json["colors"]["snack_bar_back_info"] as String)
..snackBarTextSuccessInt =
parseColor(json["colors"]["snack_bar_text_success"] as String)
parseColor(json["colors"]["snack_bar_text_success"] as String)
..snackBarTextErrorInt =
parseColor(json["colors"]["snack_bar_text_error"] as String)
parseColor(json["colors"]["snack_bar_text_error"] as String)
..snackBarTextInfoInt =
parseColor(json["colors"]["snack_bar_text_info"] as String)
parseColor(json["colors"]["snack_bar_text_info"] as String)
..bottomNavIconBackInt =
parseColor(json["colors"]["bottom_nav_icon_back"] as String)
parseColor(json["colors"]["bottom_nav_icon_back"] as String)
..bottomNavIconIconInt =
parseColor(json["colors"]["bottom_nav_icon_icon"] as String)
parseColor(json["colors"]["bottom_nav_icon_icon"] as String)
..bottomNavIconIconHighlightedInt = parseColor(
json["colors"]["bottom_nav_icon_icon_highlighted"] as String)
..topNavIconPrimaryInt =
parseColor(json["colors"]["top_nav_icon_primary"] as String)
parseColor(json["colors"]["top_nav_icon_primary"] as String)
..topNavIconGreenInt =
parseColor(json["colors"]["top_nav_icon_green"] as String)
parseColor(json["colors"]["top_nav_icon_green"] as String)
..topNavIconYellowInt =
parseColor(json["colors"]["top_nav_icon_yellow"] as String)
parseColor(json["colors"]["top_nav_icon_yellow"] as String)
..topNavIconRedInt =
parseColor(json["colors"]["top_nav_icon_red"] as String)
parseColor(json["colors"]["top_nav_icon_red"] as String)
..settingsIconBackInt =
parseColor(json["colors"]["settings_icon_back"] as String)
parseColor(json["colors"]["settings_icon_back"] as String)
..settingsIconIconInt =
parseColor(json["colors"]["settings_icon_icon"] as String)
parseColor(json["colors"]["settings_icon_icon"] as String)
..settingsIconBack2Int =
parseColor(json["colors"]["settings_icon_back_two"] as String)
parseColor(json["colors"]["settings_icon_back_two"] as String)
..settingsIconElementInt =
parseColor(json["colors"]["settings_icon_element"] as String)
parseColor(json["colors"]["settings_icon_element"] as String)
..textFieldActiveBGInt =
parseColor(json["colors"]["text_field_active_bg"] as String)
parseColor(json["colors"]["text_field_active_bg"] as String)
..textFieldDefaultBGInt =
parseColor(json["colors"]["text_field_default_bg"] as String)
parseColor(json["colors"]["text_field_default_bg"] as String)
..textFieldErrorBGInt =
parseColor(json["colors"]["text_field_error_bg"] as String)
parseColor(json["colors"]["text_field_error_bg"] as String)
..textFieldSuccessBGInt =
parseColor(json["colors"]["text_field_success_bg"] as String)
parseColor(json["colors"]["text_field_success_bg"] as String)
..textFieldErrorBorderInt =
parseColor(json["colors"]["text_field_error_border"] as String)
parseColor(json["colors"]["text_field_error_border"] as String)
..textFieldSuccessBorderInt =
parseColor(json["colors"]["text_field_success_border"] as String)
parseColor(json["colors"]["text_field_success_border"] as String)
..textFieldActiveSearchIconLeftInt = parseColor(
json["colors"]["text_field_active_search_icon_left"] as String)
..textFieldDefaultSearchIconLeftInt = parseColor(
@ -1725,19 +1725,19 @@ class StackTheme {
..textFieldSuccessSearchIconLeftInt = parseColor(
json["colors"]["text_field_success_search_icon_left"] as String)
..textFieldActiveTextInt =
parseColor(json["colors"]["text_field_active_text"] as String)
parseColor(json["colors"]["text_field_active_text"] as String)
..textFieldDefaultTextInt =
parseColor(json["colors"]["text_field_default_text"] as String)
parseColor(json["colors"]["text_field_default_text"] as String)
..textFieldErrorTextInt =
parseColor(json["colors"]["text_field_error_text"] as String)
parseColor(json["colors"]["text_field_error_text"] as String)
..textFieldSuccessTextInt =
parseColor(json["colors"]["text_field_success_text"] as String)
parseColor(json["colors"]["text_field_success_text"] as String)
..textFieldActiveLabelInt =
parseColor(json["colors"]["text_field_active_label"] as String)
parseColor(json["colors"]["text_field_active_label"] as String)
..textFieldErrorLabelInt =
parseColor(json["colors"]["text_field_error_label"] as String)
parseColor(json["colors"]["text_field_error_label"] as String)
..textFieldSuccessLabelInt =
parseColor(json["colors"]["text_field_success_label"] as String)
parseColor(json["colors"]["text_field_success_label"] as String)
..textFieldActiveSearchIconRightInt = parseColor(
json["colors"]["text_field_active_search_icon_right"] as String)
..textFieldDefaultSearchIconRightInt = parseColor(
@ -1753,61 +1753,61 @@ class StackTheme {
..settingsItem2ActiveSubInt = parseColor(
json["colors"]["settings_item_level_two_active_sub"] as String)
..radioButtonIconBorderInt =
parseColor(json["colors"]["radio_button_icon_border"] as String)
parseColor(json["colors"]["radio_button_icon_border"] as String)
..radioButtonIconBorderDisabledInt = parseColor(
json["colors"]["radio_button_icon_border_disabled"] as String)
..radioButtonBorderEnabledInt =
parseColor(json["colors"]["radio_button_border_enabled"] as String)
parseColor(json["colors"]["radio_button_border_enabled"] as String)
..radioButtonBorderDisabledInt =
parseColor(json["colors"]["radio_button_border_disabled"] as String)
parseColor(json["colors"]["radio_button_border_disabled"] as String)
..radioButtonIconCircleInt =
parseColor(json["colors"]["radio_button_icon_circle"] as String)
parseColor(json["colors"]["radio_button_icon_circle"] as String)
..radioButtonIconEnabledInt =
parseColor(json["colors"]["radio_button_icon_enabled"] as String)
parseColor(json["colors"]["radio_button_icon_enabled"] as String)
..radioButtonTextEnabledInt =
parseColor(json["colors"]["radio_button_text_enabled"] as String)
parseColor(json["colors"]["radio_button_text_enabled"] as String)
..radioButtonTextDisabledInt =
parseColor(json["colors"]["radio_button_text_disabled"] as String)
parseColor(json["colors"]["radio_button_text_disabled"] as String)
..radioButtonLabelEnabledInt =
parseColor(json["colors"]["radio_button_label_enabled"] as String)
parseColor(json["colors"]["radio_button_label_enabled"] as String)
..radioButtonLabelDisabledInt =
parseColor(json["colors"]["radio_button_label_disabled"] as String)
parseColor(json["colors"]["radio_button_label_disabled"] as String)
..infoItemBGInt = parseColor(json["colors"]["info_item_bg"] as String)
..infoItemLabelInt =
parseColor(json["colors"]["info_item_label"] as String)
parseColor(json["colors"]["info_item_label"] as String)
..infoItemTextInt = parseColor(json["colors"]["info_item_text"] as String)
..infoItemIconsInt =
parseColor(json["colors"]["info_item_icons"] as String)
parseColor(json["colors"]["info_item_icons"] as String)
..popupBGInt = parseColor(json["colors"]["popup_bg"] as String)
..currencyListItemBGInt =
parseColor(json["colors"]["currency_list_item_bg"] as String)
parseColor(json["colors"]["currency_list_item_bg"] as String)
..stackWalletBGInt = parseColor(json["colors"]["sw_bg"] as String)
..stackWalletMidInt = parseColor(json["colors"]["sw_mid"] as String)
..stackWalletBottomInt = parseColor(json["colors"]["sw_bottom"] as String)
..bottomNavShadowInt =
parseColor(json["colors"]["bottom_nav_shadow"] as String)
parseColor(json["colors"]["bottom_nav_shadow"] as String)
..splashInt = parseColor(json["colors"]["splash"] as String)
..highlightInt = parseColor(json["colors"]["highlight"] as String)
..warningForegroundInt =
parseColor(json["colors"]["warning_foreground"] as String)
parseColor(json["colors"]["warning_foreground"] as String)
..warningBackgroundInt =
parseColor(json["colors"]["warning_background"] as String)
parseColor(json["colors"]["warning_background"] as String)
..loadingOverlayTextColorInt =
parseColor(json["colors"]["loading_overlay_text_color"] as String)
parseColor(json["colors"]["loading_overlay_text_color"] as String)
..myStackContactIconBGInt =
parseColor(json["colors"]["my_stack_contact_icon_bg"] as String)
parseColor(json["colors"]["my_stack_contact_icon_bg"] as String)
..textConfirmTotalAmountInt =
parseColor(json["colors"]["text_confirm_total_amount"] as String)
parseColor(json["colors"]["text_confirm_total_amount"] as String)
..textSelectedWordTableItemInt =
parseColor(json["colors"]["text_selected_word_table_iterm"] as String)
parseColor(json["colors"]["text_selected_word_table_iterm"] as String)
..favoriteStarActiveInt =
parseColor(json["colors"]["favorite_star_active"] as String)
parseColor(json["colors"]["favorite_star_active"] as String)
..favoriteStarInactiveInt =
parseColor(json["colors"]["favorite_star_inactive"] as String)
parseColor(json["colors"]["favorite_star_inactive"] as String)
..rateTypeToggleColorOnInt =
parseColor(json["colors"]["rate_type_toggle_color_on"] as String)
parseColor(json["colors"]["rate_type_toggle_color_on"] as String)
..rateTypeToggleColorOffInt =
parseColor(json["colors"]["rate_type_toggle_color_off"] as String)
parseColor(json["colors"]["rate_type_toggle_color_off"] as String)
..rateTypeToggleDesktopColorOnInt = parseColor(
json["colors"]["rate_type_toggle_desktop_color_on"] as String)
..rateTypeToggleDesktopColorOffInt = parseColor(
@ -1815,19 +1815,19 @@ class StackTheme {
..ethTagTextInt = parseColor(json["colors"]["eth_tag_text"] as String)
..ethTagBGInt = parseColor(json["colors"]["eth_tag_bg"] as String)
..ethWalletTagTextInt =
parseColor(json["colors"]["eth_wallet_tag_text"] as String)
parseColor(json["colors"]["eth_wallet_tag_text"] as String)
..ethWalletTagBGInt =
parseColor(json["colors"]["eth_wallet_tag_bg"] as String)
parseColor(json["colors"]["eth_wallet_tag_bg"] as String)
..tokenSummaryTextPrimaryInt =
parseColor(json["colors"]["token_summary_text_primary"] as String)
parseColor(json["colors"]["token_summary_text_primary"] as String)
..tokenSummaryTextSecondaryInt =
parseColor(json["colors"]["token_summary_text_secondary"] as String)
parseColor(json["colors"]["token_summary_text_secondary"] as String)
..tokenSummaryBGInt =
parseColor(json["colors"]["token_summary_bg"] as String)
parseColor(json["colors"]["token_summary_bg"] as String)
..tokenSummaryButtonBGInt =
parseColor(json["colors"]["token_summary_button_bg"] as String)
parseColor(json["colors"]["token_summary_button_bg"] as String)
..tokenSummaryIconInt =
parseColor(json["colors"]["token_summary_icon"] as String);
parseColor(json["colors"]["token_summary_icon"] as String);
}
/// Grab the int value of the hex color string.
@ -1840,7 +1840,7 @@ class StackTheme {
} else {
throw ArgumentError(
'"$colorHex" and corresponding int '
'value "$colorValue" is not a valid color.',
'value "$colorValue" is not a valid color.',
);
}
} catch (_) {
@ -2078,18 +2078,18 @@ class ThemeAssetsV2 implements IThemeAssets {
@ignore
Map<Coin, String> get coinIcons => _coinIcons ??= parseCoinAssetsString(
coinIconsString,
placeHolder: coinPlaceholder,
);
coinIconsString,
placeHolder: coinPlaceholder,
);
@ignore
Map<Coin, String>? _coinIcons;
late final String coinIconsString;
@ignore
Map<Coin, String> get coinImages => _coinImages ??= parseCoinAssetsString(
coinImagesString,
placeHolder: coinPlaceholder,
);
coinImagesString,
placeHolder: coinPlaceholder,
);
@ignore
Map<Coin, String>? _coinImages;
late final String coinImagesString;
@ -2164,9 +2164,9 @@ class ThemeAssetsV2 implements IThemeAssets {
}
static Map<Coin, String> parseCoinAssetsString(
String jsonString, {
required String placeHolder,
}) {
String jsonString, {
required String placeHolder,
}) {
final json = jsonDecode(jsonString) as Map;
final map = Map<String, dynamic>.from(json);
@ -2348,18 +2348,18 @@ class ThemeAssetsV3 implements IThemeAssets {
@ignore
Map<Coin, String> get coinIcons => _coinIcons ??= parseCoinAssetsString(
coinIconsString,
placeHolder: coinPlaceholder,
);
coinIconsString,
placeHolder: coinPlaceholder,
);
@ignore
Map<Coin, String>? _coinIcons;
late final String coinIconsString;
@ignore
Map<Coin, String> get coinImages => _coinImages ??= parseCoinAssetsString(
coinImagesString,
placeHolder: coinPlaceholder,
);
coinImagesString,
placeHolder: coinPlaceholder,
);
@ignore
Map<Coin, String>? _coinImages;
late final String coinImagesString;
@ -2379,9 +2379,9 @@ class ThemeAssetsV3 implements IThemeAssets {
_coinCardImages ??= coinCardImagesString == null
? null
: parseCoinAssetsString(
coinCardImagesString!,
placeHolder: coinPlaceholder,
);
coinCardImagesString!,
placeHolder: coinPlaceholder,
);
@ignore
Map<Coin, String>? _coinCardImages;
late final String? coinCardImagesString;
@ -2391,9 +2391,9 @@ class ThemeAssetsV3 implements IThemeAssets {
_coinCardFavoritesImages ??= coinCardFavoritesImagesString == null
? null
: parseCoinAssetsString(
coinCardFavoritesImagesString!,
placeHolder: coinPlaceholder,
);
coinCardFavoritesImagesString!,
placeHolder: coinPlaceholder,
);
@ignore
Map<Coin, String>? _coinCardFavoritesImages;
@Name("otherStringParam1")
@ -2450,15 +2450,15 @@ class ThemeAssetsV3 implements IThemeAssets {
)
..coinCardImagesString = json["coins"]["cards"] is Map
? createCoinAssetsString(
"$themeId/assets",
Map<String, dynamic>.from(json["coins"]["cards"] as Map),
)
"$themeId/assets",
Map<String, dynamic>.from(json["coins"]["cards"] as Map),
)
: null
..coinCardFavoritesImagesString = json["coins"]["favoriteCards"] is Map
? createCoinAssetsString(
"$themeId/assets",
Map<String, dynamic>.from(json["coins"]["favoriteCards"] as Map),
)
"$themeId/assets",
Map<String, dynamic>.from(json["coins"]["favoriteCards"] as Map),
)
: null
..loadingGifRelative = json["loading_gif"] is String
? "$themeId/assets/${json["loading_gif"] as String}"
@ -2499,9 +2499,9 @@ class ThemeAssetsV3 implements IThemeAssets {
}
static Map<Coin, String> parseCoinAssetsString(
String jsonString, {
required String placeHolder,
}) {
String jsonString, {
required String placeHolder,
}) {
final json = jsonDecode(jsonString) as Map;
final map = Map<String, dynamic>.from(json);
@ -2571,4 +2571,4 @@ abstract class IThemeAssets {
String? get loadingGif;
String? get background;
}
}

View file

@ -12,6 +12,7 @@ import 'package:hive/hive.dart';
part 'type_adaptors/lelantus_coin.g.dart';
@Deprecated("Use Isar object instead")
// @HiveType(typeId: 9)
class LelantusCoin {
// @HiveField(0)
@ -27,6 +28,7 @@ class LelantusCoin {
// @HiveField(5)
bool isUsed;
@Deprecated("Use Isar object instead")
LelantusCoin(
this.index,
this.value,

View file

@ -28,4 +28,15 @@ class SigningData {
Uint8List? output;
ECPair? keyPair;
Uint8List? redeemScript;
@override
String toString() {
return "SigningData{\n"
" derivePathType: $derivePathType,\n"
" utxo: $utxo,\n"
" output: $output,\n"
" keyPair: $keyPair,\n"
" redeemScript: $redeemScript,\n"
"}";
}
}

View file

@ -11,8 +11,8 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/wallet_restore_state.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/utilities/enums/stack_restoring_status.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
class StackRestoringUIState extends ChangeNotifier {
bool _walletsWasSet = false;
@ -93,14 +93,14 @@ class StackRestoringUIState extends ChangeNotifier {
notifyListeners();
}
List<Manager> get managers {
List<Manager> _managers = [];
List<Wallet> get wallets {
List<Wallet> _wallets = [];
for (final item in _walletStates.values) {
if (item.manager != null) {
_managers.add(item.manager!);
if (item.wallet != null) {
_wallets.add(item.wallet!);
}
}
return _managers;
return _wallets;
}
Map<String, WalletRestoreState> _walletStates = {};
@ -132,15 +132,14 @@ class StackRestoringUIState extends ChangeNotifier {
void update({
required String walletId,
required StackRestoringStatus restoringStatus,
Manager? manager,
Wallet? wallet,
String? address,
String? mnemonic,
String? mnemonicPassphrase,
int? height,
}) {
_walletStates[walletId]!.restoringState = restoringStatus;
_walletStates[walletId]!.manager =
manager ?? _walletStates[walletId]!.manager;
_walletStates[walletId]!.wallet = wallet ?? _walletStates[walletId]!.wallet;
_walletStates[walletId]!.address =
address ?? _walletStates[walletId]!.address;
_walletStates[walletId]!.mnemonic =

View file

@ -10,6 +10,7 @@
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/wallets/models/tx_recipient.dart';
// TODO use something like this instead of Map<String, dynamic> transactionObject
@ -43,13 +44,3 @@ class TxInfo {
recipients: recipients ?? this.recipients,
);
}
class TxRecipient {
final String address;
final Amount amount;
TxRecipient({
required this.address,
required this.amount,
});
}

View file

@ -8,17 +8,17 @@
*
*/
import 'package:flutter/cupertino.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:flutter/material.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/stack_restoring_status.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
class WalletRestoreState extends ChangeNotifier {
final String walletId;
final String walletName;
final Coin coin;
late StackRestoringStatus _restoringStatus;
Manager? manager;
Wallet? wallet;
String? address;
String? mnemonic;
String? mnemonicPassphrase;
@ -35,7 +35,7 @@ class WalletRestoreState extends ChangeNotifier {
required this.walletName,
required this.coin,
required StackRestoringStatus restoringStatus,
this.manager,
this.wallet,
this.address,
this.mnemonic,
this.mnemonicPassphrase,
@ -54,7 +54,7 @@ class WalletRestoreState extends ChangeNotifier {
walletName: walletName,
coin: coin,
restoringStatus: restoringStatus ?? _restoringStatus,
manager: manager,
wallet: wallet,
address: this.address,
mnemonic: mnemonic,
mnemonicPassphrase: mnemonicPassphrase,

124
lib/networking/http.dart Normal file
View file

@ -0,0 +1,124 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:socks5_proxy/socks_client.dart';
import 'package:stackwallet/utilities/logger.dart';
// WIP wrapper layer
// TODO expand this class
class Response {
final int code;
final List<int> bodyBytes;
String get body => utf8.decode(bodyBytes, allowMalformed: true);
Response(this.bodyBytes, this.code);
}
class HTTP {
Future<Response> get({
required Uri url,
Map<String, String>? headers,
required ({
InternetAddress host,
int port,
})? proxyInfo,
}) async {
final httpClient = HttpClient();
try {
if (proxyInfo != null) {
SocksTCPClient.assignToHttpClient(httpClient, [
ProxySettings(
proxyInfo.host,
proxyInfo.port,
),
]);
}
final HttpClientRequest request = await httpClient.getUrl(
url,
);
if (headers != null) {
headers.forEach((key, value) => request.headers.add(key, value));
}
final response = await request.close();
return Response(
await _bodyBytes(response),
response.statusCode,
);
} catch (e, s) {
Logging.instance.log(
"HTTP.get() rethrew: $e\n$s",
level: LogLevel.Info,
);
rethrow;
} finally {
httpClient.close(force: true);
}
}
Future<Response> post({
required Uri url,
Map<String, String>? headers,
Object? body,
Encoding? encoding,
required ({
InternetAddress host,
int port,
})? proxyInfo,
}) async {
final httpClient = HttpClient();
try {
if (proxyInfo != null) {
SocksTCPClient.assignToHttpClient(httpClient, [
ProxySettings(
proxyInfo.host,
proxyInfo.port,
),
]);
}
final HttpClientRequest request = await httpClient.postUrl(
url,
);
if (headers != null) {
headers.forEach((key, value) => request.headers.add(key, value));
}
request.write(body);
final response = await request.close();
return Response(
await _bodyBytes(response),
response.statusCode,
);
} catch (e, s) {
Logging.instance.log(
"HTTP.post() rethrew: $e\n$s",
level: LogLevel.Info,
);
rethrow;
} finally {
httpClient.close(force: true);
}
}
Future<Uint8List> _bodyBytes(HttpClientResponse response) {
final completer = Completer<Uint8List>();
final List<int> bytes = [];
response.listen(
(data) {
bytes.addAll(data);
},
onDone: () => completer.complete(
Uint8List.fromList(bytes),
),
);
return completer.future;
}
}

View file

@ -142,7 +142,7 @@ class _AddCustomTokenViewState extends ConsumerState<AddCustomTokenView> {
context: context,
message: "Looking up contract",
);
currentToken = response.value;
currentToken = response!.value;
if (currentToken != null) {
nameController.text = currentToken!.name;
symbolController.text = currentToken!.symbol;
@ -157,7 +157,7 @@ class _AddCustomTokenViewState extends ConsumerState<AddCustomTokenView> {
context: context,
builder: (context) => StackOkDialog(
title: "Failed to look up token",
message: response.exception?.message,
message: response!.exception?.message,
),
),
);

View file

@ -23,14 +23,16 @@ import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/ad
import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/providers/global/price_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/default_eth_tokens.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/wallet/impl/ethereum_wallet.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';
@ -98,10 +100,8 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
.map((e) => e.token.address)
.toList();
final ethWallet = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as EthereumWallet;
final ethWallet =
ref.read(pWallets).getWallet(widget.walletId) as EthereumWallet;
await ethWallet.updateTokenContracts(selectedTokens);
if (mounted) {
@ -122,7 +122,7 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "${ethWallet.walletName} tokens saved",
message: "${ethWallet.info.name} tokens saved",
context: context,
),
);
@ -152,6 +152,7 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
if (contract != null) {
await MainDB.instance.putEthContract(contract);
unawaited(ref.read(priceAnd24hChangeNotifierProvider).updatePrice());
if (mounted) {
setState(() {
if (tokenEntities
@ -176,16 +177,13 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
if (contracts.isEmpty) {
contracts.addAll(DefaultTokens.list);
MainDB.instance.putEthContracts(contracts);
MainDB.instance.putEthContracts(contracts).then(
(_) => ref.read(priceAnd24hChangeNotifierProvider).updatePrice());
}
tokenEntities.addAll(contracts.map((e) => AddTokenListElementData(e)));
final walletContracts = (ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as EthereumWallet)
.getWalletTokenContractAddresses();
final walletContracts = ref.read(pWalletTokenAddresses(widget.walletId));
final shouldMarkAsSelectedContracts = [
...walletContracts,
@ -209,8 +207,7 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final walletName = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId).walletName));
final walletName = ref.watch(pWalletName(widget.walletId));
if (isDesktop) {
return ConditionalParent(

View file

@ -8,7 +8,10 @@
*
*/
import 'dart:io';
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/models/isar/exchange_cache/currency.dart';
@ -16,6 +19,7 @@ import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/themes/theme_providers.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
@ -30,16 +34,17 @@ class AddTokenListElementData {
bool selected = false;
}
class AddTokenListElement extends StatefulWidget {
class AddTokenListElement extends ConsumerStatefulWidget {
const AddTokenListElement({Key? key, required this.data}) : super(key: key);
final AddTokenListElementData data;
@override
State<AddTokenListElement> createState() => _AddTokenListElementState();
ConsumerState<AddTokenListElement> createState() =>
_AddTokenListElementState();
}
class _AddTokenListElementState extends State<AddTokenListElement> {
class _AddTokenListElementState extends ConsumerState<AddTokenListElement> {
final bool isDesktop = Util.isDesktop;
@override
@ -74,6 +79,17 @@ class _AddTokenListElementState extends State<AddTokenListElement> {
currency.image,
width: iconSize,
height: iconSize,
placeholderBuilder: (_) => SvgPicture.file(
File(
ref.watch(
themeAssetsProvider.select(
(value) => value.stackIcon,
),
),
),
width: iconSize,
height: iconSize,
),
)
: SvgPicture.asset(
widget.data.token.symbol == "BNB"

View file

@ -8,6 +8,7 @@
*
*/
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
@ -60,11 +61,9 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
String _searchTerm = "";
final List<Coin> _coinsTestnet = [
...Coin.values.sublist(Coin.values.length - kTestNetCoinCount - 1),
];
final List<Coin> _coins = [
...Coin.values.sublist(0, Coin.values.length - kTestNetCoinCount - 1)
...Coin.values.where((e) => e.isTestNet),
];
final List<Coin> _coins = [...Coin.values.where((e) => !e.isTestNet)];
final List<AddWalletListEntity> coinEntities = [];
final List<EthTokenEntity> tokenEntities = [];
@ -109,6 +108,7 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
if (contract != null) {
await MainDB.instance.putEthContract(contract);
unawaited(ref.read(priceAnd24hChangeNotifierProvider).updatePrice());
if (mounted) {
setState(() {
if (tokenEntities
@ -126,7 +126,7 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
void initState() {
_searchFieldController = TextEditingController();
_searchFocusNode = FocusNode();
_coinsTestnet.remove(Coin.firoTestNet);
// _coinsTestnet.remove(Coin.firoTestNet);
if (Platform.isWindows) {
_coins.remove(Coin.monero);
_coins.remove(Coin.wownero);
@ -145,7 +145,8 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
if (contracts.isEmpty) {
contracts.addAll(DefaultTokens.list);
MainDB.instance.putEthContracts(contracts);
MainDB.instance.putEthContracts(contracts).then(
(value) => ref.read(priceAnd24hChangeNotifierProvider).updatePrice());
}
tokenEntities.addAll(contracts.map((e) => EthTokenEntity(e)));

View file

@ -12,21 +12,22 @@ 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:isar/isar.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_options/new_wallet_options_view.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';
import 'package:stackwallet/providers/global/wallets_service_provider.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/ui/verify_recovery_phrase/mnemonic_word_count_state_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/name_generator.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
@ -80,10 +81,15 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
void initState() {
isDesktop = Util.isDesktop;
ref.read(walletsServiceChangeNotifierProvider).walletNames.then(
(value) => namesToExclude.addAll(
value.values.map((e) => e.name),
),
ref
.read(mainDBProvider)
.isar
.walletInfo
.where()
.nameProperty()
.findAll()
.then(
(values) => namesToExclude.addAll(values),
);
generator = NameGenerator();
addWalletType = widget.addWalletType;
@ -332,18 +338,9 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
child: TextButton(
onPressed: _nextEnabled
? () async {
final walletsService =
ref.read(walletsServiceChangeNotifierProvider);
final name = textEditingController.text;
if (await walletsService.checkForDuplicate(name)) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "Wallet name already in use.",
iconAsset: Assets.svg.circleAlert,
context: context,
));
} else {
if (mounted) {
// hide keyboard if has focus
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
@ -352,29 +349,36 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
}
if (mounted) {
ref.read(mnemonicWordCountStateProvider.state).state =
Constants.possibleLengthsForCoin(coin).last;
ref.read(pNewWalletOptions.notifier).state = null;
switch (widget.addWalletType) {
case AddWalletType.New:
unawaited(Navigator.of(context).pushNamed(
NewWalletRecoveryPhraseWarningView.routeName,
arguments: Tuple2(
name,
coin,
unawaited(
Navigator.of(context).pushNamed(
coin.hasMnemonicPassphraseSupport
? NewWalletOptionsView.routeName
: NewWalletRecoveryPhraseWarningView
.routeName,
arguments: Tuple2(
name,
coin,
),
),
));
);
break;
case AddWalletType.Restore:
ref
.read(mnemonicWordCountStateProvider.state)
.state = Constants.possibleLengthsForCoin(
coin)
.first;
unawaited(Navigator.of(context).pushNamed(
RestoreOptionsView.routeName,
arguments: Tuple2(
name,
coin,
unawaited(
Navigator.of(context).pushNamed(
RestoreOptionsView.routeName,
arguments: Tuple2(
name,
coin,
),
),
));
);
break;
}
}

View file

@ -0,0 +1,410 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.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/sub_widgets/mobile_mnemonic_length_selector.dart';
import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/ui/verify_recovery_phrase/mnemonic_word_count_state_provider.dart';
import 'package:stackwallet/themes/stack_colors.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/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_app_bar.dart';
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 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:tuple/tuple.dart';
final pNewWalletOptions =
StateProvider<({String mnemonicPassphrase, int mnemonicWordsCount})?>(
(ref) => null);
enum NewWalletOptions {
Default,
Advanced;
}
class NewWalletOptionsView extends ConsumerStatefulWidget {
const NewWalletOptionsView({
Key? key,
required this.walletName,
required this.coin,
}) : super(key: key);
static const routeName = "/newWalletOptionsView";
final String walletName;
final Coin coin;
@override
ConsumerState<NewWalletOptionsView> createState() =>
_NewWalletOptionsViewState();
}
class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
late final FocusNode passwordFocusNode;
late final TextEditingController passwordController;
bool hidePassword = true;
NewWalletOptions _selectedOptions = NewWalletOptions.Default;
@override
void initState() {
passwordController = TextEditingController();
passwordFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
passwordController.dispose();
passwordFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final lengths = Constants.possibleLengthsForCoin(widget.coin).toList();
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: const AppBarBackButton(),
title: Text(
"Wallet Options",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
children: [
if (Util.isDesktop)
const Spacer(
flex: 10,
),
if (!Util.isDesktop)
const SizedBox(
height: 16,
),
if (!Util.isDesktop)
CoinImage(
coin: widget.coin,
height: 100,
width: 100,
),
if (Util.isDesktop)
Text(
"Wallet options",
textAlign: TextAlign.center,
style: Util.isDesktop
? STextStyles.desktopH2(context)
: STextStyles.pageTitleH1(context),
),
SizedBox(
height: Util.isDesktop ? 32 : 16,
),
DropdownButtonHideUnderline(
child: DropdownButton2<NewWalletOptions>(
value: _selectedOptions,
items: [
...NewWalletOptions.values.map(
(e) => DropdownMenuItem(
value: e,
child: Text(
e.name,
style: STextStyles.desktopTextMedium(context),
),
),
),
],
onChanged: (value) {
if (value is NewWalletOptions) {
setState(() {
_selectedOptions = value;
});
}
},
isExpanded: true,
iconStyleData: IconStyleData(
icon: SvgPicture.asset(
Assets.svg.chevronDown,
width: 12,
height: 6,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconRight,
),
),
dropdownStyleData: DropdownStyleData(
offset: const Offset(0, -10),
elevation: 0,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
),
),
const SizedBox(
height: 24,
),
if (_selectedOptions == NewWalletOptions.Advanced)
Column(
children: [
if (Util.isDesktop)
DropdownButtonHideUnderline(
child: DropdownButton2<int>(
value: ref
.watch(mnemonicWordCountStateProvider.state)
.state,
items: [
...lengths.map(
(e) => DropdownMenuItem(
value: e,
child: Text(
"$e word seed",
style: STextStyles.desktopTextMedium(context),
),
),
),
],
onChanged: (value) {
if (value is int) {
ref
.read(mnemonicWordCountStateProvider.state)
.state = value;
}
},
isExpanded: true,
iconStyleData: IconStyleData(
icon: SvgPicture.asset(
Assets.svg.chevronDown,
width: 12,
height: 6,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconRight,
),
),
dropdownStyleData: DropdownStyleData(
offset: const Offset(0, -10),
elevation: 0,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
),
),
if (!Util.isDesktop)
MobileMnemonicLengthSelector(
chooseMnemonicLength: () {
showModalBottomSheet<dynamic>(
backgroundColor: Colors.transparent,
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
builder: (_) {
return MnemonicWordCountSelectSheet(
lengthOptions: lengths,
);
},
);
},
),
const SizedBox(
height: 24,
),
RoundedWhiteContainer(
child: Center(
child: Text(
"You may add a BIP39 passphrase. This is optional. "
"You will need BOTH your seed and your passphrase to recover the wallet.",
style: Util.isDesktop
? STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.itemSubtitle(context),
),
),
),
const SizedBox(
height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("mnemonicPassphraseFieldKey1"),
focusNode: passwordFocusNode,
controller: passwordController,
style: Util.isDesktop
? STextStyles.desktopTextMedium(context).copyWith(
height: 2,
)
: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"BIP39 passphrase",
passwordFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => SizedBox(
height: 70,
child: child,
),
child: Row(
children: [
SizedBox(
width: Util.isDesktop ? 24 : 16,
),
GestureDetector(
key: const Key(
"mnemonicPassphraseFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: Util.isDesktop ? 24 : 16,
height: Util.isDesktop ? 24 : 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
),
),
),
],
),
if (!Util.isDesktop) const Spacer(),
SizedBox(
height: Util.isDesktop ? 32 : 16,
),
PrimaryButton(
label: "Continue",
onPressed: () {
if (_selectedOptions == NewWalletOptions.Advanced) {
ref.read(pNewWalletOptions.notifier).state = (
mnemonicWordsCount:
ref.read(mnemonicWordCountStateProvider.state).state,
mnemonicPassphrase: passwordController.text,
);
} else {
ref.read(pNewWalletOptions.notifier).state = null;
}
Navigator.of(context).pushNamed(
NewWalletRecoveryPhraseWarningView.routeName,
arguments: Tuple2(
widget.walletName,
widget.coin,
),
);
},
),
if (!Util.isDesktop)
const SizedBox(
height: 16,
),
if (Util.isDesktop)
const Spacer(
flex: 15,
),
],
),
),
);
}
}

View file

@ -21,14 +21,16 @@ import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_wa
import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
@ -37,14 +39,14 @@ import 'package:tuple/tuple.dart';
class NewWalletRecoveryPhraseView extends ConsumerStatefulWidget {
const NewWalletRecoveryPhraseView({
Key? key,
required this.manager,
required this.wallet,
required this.mnemonic,
this.clipboardInterface = const ClipboardWrapper(),
}) : super(key: key);
static const routeName = "/newWalletRecoveryPhrase";
final Manager manager;
final Wallet wallet;
final List<String> mnemonic;
final ClipboardInterface clipboardInterface;
@ -58,14 +60,14 @@ class _NewWalletRecoveryPhraseViewState
extends ConsumerState<NewWalletRecoveryPhraseView>
// with WidgetsBindingObserver
{
late Manager _manager;
late Wallet _wallet;
late List<String> _mnemonic;
late ClipboardInterface _clipboardInterface;
late final bool isDesktop;
@override
void initState() {
_manager = widget.manager;
_wallet = widget.wallet;
_mnemonic = widget.mnemonic;
_clipboardInterface = widget.clipboardInterface;
isDesktop = Util.isDesktop;
@ -78,14 +80,14 @@ class _NewWalletRecoveryPhraseViewState
}
Future<void> delete() async {
await _wallet.exit();
await ref
.read(walletsServiceChangeNotifierProvider)
.deleteWallet(_manager.walletName, false);
await _manager.exitCurrentWallet();
.read(pWallets)
.deleteWallet(_wallet.info, ref.read(secureStoreProvider));
}
Future<void> _copy() async {
final words = await _manager.mnemonic;
final words = _mnemonic;
await _clipboardInterface.setData(ClipboardData(text: words.join(" ")));
unawaited(showFloatingFlushBar(
type: FlushBarType.info,
@ -191,7 +193,7 @@ class _NewWalletRecoveryPhraseViewState
),
if (!isDesktop)
Text(
_manager.walletName,
ref.watch(pWalletName(_wallet.walletId)),
textAlign: TextAlign.center,
style: STextStyles.label(context).copyWith(
fontSize: 12,
@ -305,7 +307,7 @@ class _NewWalletRecoveryPhraseViewState
unawaited(Navigator.of(context).pushNamed(
VerifyRecoveryPhraseView.routeName,
arguments: Tuple2(_manager, _mnemonic),
arguments: Tuple2(_wallet, _mnemonic),
));
},
style: Theme.of(context)

View file

@ -9,17 +9,19 @@
*/
import 'dart:async';
import 'dart:convert';
import 'package:bip39/bip39.dart' as bip39;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/recovery_phrase_explanation_dialog.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
@ -29,6 +31,10 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.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_app_bar.dart';
@ -38,7 +44,7 @@ import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:tuple/tuple.dart';
class NewWalletRecoveryPhraseWarningView extends StatefulWidget {
class NewWalletRecoveryPhraseWarningView extends ConsumerStatefulWidget {
const NewWalletRecoveryPhraseWarningView({
Key? key,
required this.coin,
@ -51,12 +57,12 @@ class NewWalletRecoveryPhraseWarningView extends StatefulWidget {
final String walletName;
@override
State<NewWalletRecoveryPhraseWarningView> createState() =>
ConsumerState<NewWalletRecoveryPhraseWarningView> createState() =>
_NewWalletRecoveryPhraseWarningViewState();
}
class _NewWalletRecoveryPhraseWarningViewState
extends State<NewWalletRecoveryPhraseWarningView> {
extends ConsumerState<NewWalletRecoveryPhraseWarningView> {
late final Coin coin;
late final String walletName;
late final bool isDesktop;
@ -72,6 +78,10 @@ class _NewWalletRecoveryPhraseWarningViewState
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final options = ref.read(pNewWalletOptions.state).state;
final seedCount = options?.mnemonicWordsCount ??
Constants.defaultSeedPhraseLengthFor(coin: coin);
return MasterScaffold(
isDesktop: isDesktop,
@ -172,7 +182,7 @@ class _NewWalletRecoveryPhraseWarningViewState
child: isDesktop
? Text(
"On the next screen you will see "
"${Constants.defaultSeedPhraseLengthFor(coin: coin)} "
"$seedCount "
"words that make up your recovery phrase.\n\nPlease "
"write it down. Keep it safe and never share it with "
"anyone. Your recovery phrase is the only way you can"
@ -216,9 +226,7 @@ class _NewWalletRecoveryPhraseWarningViewState
),
),
TextSpan(
text:
"${Constants.defaultSeedPhraseLengthFor(coin: coin)}"
" words",
text: "$seedCount words",
style: STextStyles.desktopH3(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
@ -450,14 +458,17 @@ class _NewWalletRecoveryPhraseWarningViewState
},
));
final walletsService = ref.read(
walletsServiceChangeNotifierProvider);
final walletId =
await walletsService.addNewWallet(
name: walletName,
coin: coin,
shouldNotifyListeners: false,
final info = WalletInfo.createNew(
coin: widget.coin,
name: widget.walletName,
otherDataJsonString: coin == Coin.tezos
? jsonEncode({
WalletInfoKeys
.tezosDerivationPath:
Tezos.standardDerivationPath
.value,
})
: null,
);
var node = ref
@ -477,26 +488,72 @@ class _NewWalletRecoveryPhraseWarningViewState
final txTracker =
TransactionNotificationTracker(
walletId: walletId!);
final failovers = ref
.read(nodeServiceChangeNotifierProvider)
.failoverNodesFor(coin: widget.coin);
final wallet = CoinServiceAPI.from(
coin,
walletId,
walletName,
ref.read(secureStoreProvider),
node,
txTracker,
ref.read(prefsChangeNotifierProvider),
failovers,
walletId: info.walletId,
);
final manager = Manager(wallet);
int? wordCount;
String? mnemonicPassphrase;
String? mnemonic;
String? privateKey;
await manager.initializeNew();
wordCount =
Constants.defaultSeedPhraseLengthFor(
coin: info.coin,
);
if (coin == Coin.monero ||
coin == Coin.wownero) {
// currently a special case due to the
// xmr/wow libraries handling their
// own mnemonic generation
} else if (wordCount > 0) {
if (ref
.read(pNewWalletOptions.state)
.state !=
null) {
if (coin.hasMnemonicPassphraseSupport) {
mnemonicPassphrase = ref
.read(pNewWalletOptions.state)
.state!
.mnemonicPassphrase;
} else {}
wordCount = ref
.read(pNewWalletOptions.state)
.state!
.mnemonicWordsCount;
} else {
mnemonicPassphrase = "";
}
if (wordCount < 12 ||
24 < wordCount ||
wordCount % 3 != 0) {
throw Exception("Invalid word count");
}
final strength = (wordCount ~/ 3) * 32;
mnemonic = bip39.generateMnemonic(
strength: strength,
);
}
final wallet = await Wallet.create(
walletInfo: info,
mainDB: ref.read(mainDBProvider),
secureStorageInterface:
ref.read(secureStoreProvider),
nodeService: ref.read(
nodeServiceChangeNotifierProvider),
prefs:
ref.read(prefsChangeNotifierProvider),
mnemonicPassphrase: mnemonicPassphrase,
mnemonic: mnemonic,
privateKey: privateKey,
);
await wallet.init();
// pop progress dialog
if (mounted) {
@ -511,8 +568,9 @@ class _NewWalletRecoveryPhraseWarningViewState
unawaited(Navigator.of(context).pushNamed(
NewWalletRecoveryPhraseView.routeName,
arguments: Tuple2(
manager,
await manager.mnemonic,
wallet,
await (wallet as MnemonicInterface)
.getMnemonicAsWords(),
),
));
}

View file

@ -535,7 +535,7 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Recovery phrase password",
"BIP39 passphrase",
passwordFocusNode,
context,
).copyWith(
@ -586,7 +586,9 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
RoundedWhiteContainer(
child: Center(
child: Text(
"If the recovery phrase you are about to restore was created with an optional passphrase you can enter it here.",
"If the recovery phrase you are about to restore "
"was created with an optional BIP39 passphrase "
"you can enter it here.",
style: isDesktop
? STextStyles.desktopTextExtraSmall(context)
.copyWith(

View file

@ -10,6 +10,7 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
@ -32,10 +33,9 @@ import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/v
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/address_utils.dart';
@ -50,6 +50,12 @@ import 'package:stackwallet/utilities/enums/form_input_status_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/monero_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/wownero_wallet.dart';
import 'package:stackwallet/wallets/wallet/supporting/epiccash_wallet_info_extension.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
@ -98,7 +104,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
final List<TextEditingController> _controllers = [];
final List<FormInputStatus> _inputStatuses = [];
final List<FocusNode> _focusNodes = [];
// final List<FocusNode> _focusNodes = [];
late final BarcodeScannerInterface scanner;
@ -152,7 +158,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
for (int i = 0; i < _seedWordCount; i++) {
_controllers.add(TextEditingController());
_inputStatuses.add(FormInputStatus.empty);
_focusNodes.add(FocusNode());
// _focusNodes.add(FocusNode());
}
super.initState();
@ -201,6 +207,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
mnemonic = mnemonic.trim();
int height = 0;
String? otherDataJsonString;
if (widget.coin == Coin.monero) {
height = monero.getHeigthByDate(date: widget.restoreFromDate);
@ -227,6 +234,22 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
if (height < 0) {
height = 0;
}
otherDataJsonString = jsonEncode(
{
WalletInfoKeys.epiccashData: jsonEncode(
ExtraEpiccashWalletInfo(
receivingIndex: 0,
changeIndex: 0,
slatesToAddresses: {},
slatesToCommits: {},
lastScannedBlock: height,
restoreHeight: height,
creationHeight: height,
).toMap(),
),
},
);
}
// TODO: do actual check to make sure it is a valid mnemonic for monero
@ -239,13 +262,14 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
));
} else {
if (!Platform.isLinux) await Wakelock.enable();
final walletsService = ref.read(walletsServiceChangeNotifierProvider);
final walletId = await walletsService.addNewWallet(
name: widget.walletName,
final info = WalletInfo.createNew(
coin: widget.coin,
shouldNotifyListeners: false,
name: widget.walletName,
restoreHeight: height,
otherDataJsonString: otherDataJsonString,
);
bool isRestoring = true;
// show restoring in progress
unawaited(showDialog<dynamic>(
@ -256,14 +280,11 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
return RestoringDialog(
onCancel: () async {
isRestoring = false;
ref
.read(walletsChangeNotifierProvider.notifier)
.removeWallet(walletId: walletId!);
await walletsService.deleteWallet(
widget.walletName,
false,
);
await ref.read(pWallets).deleteWallet(
info,
ref.read(secureStoreProvider),
);
},
);
},
@ -281,49 +302,47 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
);
}
final txTracker = TransactionNotificationTracker(walletId: walletId!);
final failovers = ref
.read(nodeServiceChangeNotifierProvider)
.failoverNodesFor(coin: widget.coin);
final wallet = CoinServiceAPI.from(
widget.coin,
walletId,
widget.walletName,
ref.read(secureStoreProvider),
node,
txTracker,
ref.read(prefsChangeNotifierProvider),
failovers,
);
final manager = Manager(wallet);
final txTracker =
TransactionNotificationTracker(walletId: info.walletId);
try {
// TODO GUI option to set maxUnusedAddressGap?
// default is 20 but it may miss some transactions if
// the previous wallet software generated many addresses
// without using them
await manager.recoverFromMnemonic(
mnemonic: mnemonic,
final wallet = await Wallet.create(
walletInfo: info,
mainDB: ref.read(mainDBProvider),
secureStorageInterface: ref.read(secureStoreProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
prefs: ref.read(prefsChangeNotifierProvider),
mnemonicPassphrase: widget.mnemonicPassphrase,
maxUnusedAddressGap: widget.coin == Coin.firo ? 50 : 20,
maxNumberOfIndexesToCheck: 1000,
height: height,
mnemonic: mnemonic,
);
// TODO: extract interface with isRestore param
switch (wallet.runtimeType) {
case EpiccashWallet:
await (wallet as EpiccashWallet).init(isRestore: true);
break;
case MoneroWallet:
await (wallet as MoneroWallet).init(isRestore: true);
break;
case WowneroWallet:
await (wallet as WowneroWallet).init(isRestore: true);
break;
default:
await wallet.init();
}
await wallet.recover(isRescan: false);
// check if state is still active before continuing
if (mounted) {
await ref
.read(walletsServiceChangeNotifierProvider)
.setMnemonicVerified(
walletId: manager.walletId,
);
await wallet.info.setMnemonicVerified(
isar: ref.read(mainDBProvider).isar,
);
ref
.read(walletsChangeNotifierProvider.notifier)
.addWallet(walletId: manager.walletId, manager: manager);
ref.read(pWallets).addWallet(wallet);
final isCreateSpecialEthWallet =
ref.read(createSpecialEthWalletRoutingFlag);
@ -360,11 +379,11 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
(route) => false,
),
);
if (manager.coin == Coin.ethereum) {
if (info.coin == Coin.ethereum) {
unawaited(
Navigator.of(context).pushNamed(
EditWalletTokensView.routeName,
arguments: manager.walletId,
arguments: wallet.walletId,
),
);
}
@ -410,8 +429,8 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
builder: (context) {
return RestoreFailedDialog(
errorMessage: e.toString(),
walletId: wallet.walletId,
walletName: wallet.walletName,
walletId: info.walletId,
walletName: info.name,
);
},
);
@ -821,8 +840,8 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
i * 4 + j - 1 == 1
? textSelectionControls
: null,
focusNode:
_focusNodes[i * 4 + j - 1],
// focusNode:
// _focusNodes[i * 4 + j - 1],
onChanged: (value) {
final FormInputStatus
formInputStatus;
@ -841,18 +860,18 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
FormInputStatus.invalid;
}
if (formInputStatus ==
FormInputStatus.valid) {
if (i * 4 + j <
_focusNodes.length) {
_focusNodes[i * 4 + j]
.requestFocus();
} else if (i * 4 + j ==
_focusNodes.length) {
_focusNodes[i * 4 + j - 1]
.unfocus();
}
}
// if (formInputStatus ==
// FormInputStatus.valid) {
// if (i * 4 + j <
// _focusNodes.length) {
// _focusNodes[i * 4 + j]
// .requestFocus();
// } else if (i * 4 + j ==
// _focusNodes.length) {
// _focusNodes[i * 4 + j - 1]
// .unfocus();
// }
// }
setState(() {
_inputStatuses[i * 4 +
j -
@ -929,7 +948,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
selectionControls: i == 1
? textSelectionControls
: null,
focusNode: _focusNodes[i],
// focusNode: _focusNodes[i],
onChanged: (value) {
final FormInputStatus
formInputStatus;
@ -948,27 +967,27 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
FormInputStatus.invalid;
}
if (formInputStatus ==
FormInputStatus
.valid &&
(i - 1) <
_focusNodes.length) {
Focus.of(context)
.requestFocus(
_focusNodes[i]);
}
// if (formInputStatus ==
// FormInputStatus
// .valid &&
// (i - 1) <
// _focusNodes.length) {
// Focus.of(context)
// .requestFocus(
// _focusNodes[i]);
// }
if (formInputStatus ==
FormInputStatus.valid) {
if (i + 1 <
_focusNodes.length) {
_focusNodes[i + 1]
.requestFocus();
} else if (i + 1 ==
_focusNodes.length) {
_focusNodes[i].unfocus();
}
}
// if (formInputStatus ==
// FormInputStatus.valid) {
// if (i + 1 <
// _focusNodes.length) {
// _focusNodes[i + 1]
// .requestFocus();
// } else if (i + 1 ==
// _focusNodes.length) {
// _focusNodes[i].unfocus();
// }
// }
},
controller: _controllers[i],
style:
@ -1068,7 +1087,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
AutovalidateMode.onUserInteraction,
selectionControls:
i == 1 ? textSelectionControls : null,
focusNode: _focusNodes[i - 1],
// focusNode: _focusNodes[i - 1],
onChanged: (value) {
final FormInputStatus formInputStatus;
@ -1084,14 +1103,14 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
FormInputStatus.invalid;
}
if (formInputStatus ==
FormInputStatus.valid) {
if (i < _focusNodes.length) {
_focusNodes[i].requestFocus();
} else if (i == _focusNodes.length) {
_focusNodes[i - 1].unfocus();
}
}
// if (formInputStatus ==
// FormInputStatus.valid) {
// if (i < _focusNodes.length) {
// _focusNodes[i].requestFocus();
// } else if (i == _focusNodes.length) {
// _focusNodes[i - 1].unfocus();
// }
// }
setState(() {
_inputStatuses[i - 1] =
formInputStatus;

View file

@ -10,9 +10,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class RestoreFailedDialog extends ConsumerStatefulWidget {
@ -63,14 +65,11 @@ class _RestoreFailedDialogState extends ConsumerState<RestoreFailedDialog> {
style: STextStyles.itemSubtitle12(context),
),
onPressed: () async {
ref
.read(walletsChangeNotifierProvider.notifier)
.removeWallet(walletId: walletId);
await ref.read(walletsServiceChangeNotifierProvider).deleteWallet(
walletName,
false,
await ref.read(pWallets).deleteWallet(
ref.read(pWalletInfo(walletId)),
ref.read(secureStoreProvider),
);
if (mounted) {
Navigator.of(context).pop();
}

View file

@ -10,17 +10,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/coin_entity.dart';
import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart';
import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart';
import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart';
import 'package:stackwallet/providers/global/wallets_service_provider.dart';
import 'package:stackwallet/services/wallets_service.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/providers/all_wallets_info_provider.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.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';
@ -54,8 +53,6 @@ class SelectWalletForTokenView extends ConsumerStatefulWidget {
class _SelectWalletForTokenViewState
extends ConsumerState<SelectWalletForTokenView> {
final isDesktop = Util.isDesktop;
late final List<String> ethWalletIds;
bool _hasEthWallets = false;
String? _selectedWalletId;
@ -77,49 +74,23 @@ class _SelectWalletForTokenViewState
);
}
late int _cachedWalletCount;
@override
Widget build(BuildContext context) {
final ethWalletInfos = ref
.watch(pAllWalletsInfo)
.where((e) => e.coin == widget.entity.coin)
.toList();
void _updateWalletsList(Map<String, WalletInfo> walletsData) {
_cachedWalletCount = walletsData.length;
final _hasEthWallets = ethWalletInfos.isNotEmpty;
walletsData.removeWhere((key, value) => value.coin != widget.entity.coin);
ethWalletIds.clear();
final List<String> ethWalletIds = [];
_hasEthWallets = walletsData.isNotEmpty;
// TODO: proper wallet data class instead of this Hive silliness
for (final walletId in walletsData.values.map((e) => e.walletId).toList()) {
final walletContracts = DB.instance.get<dynamic>(
boxName: walletId,
key: DBKeys.ethTokenContracts,
) as List<String>? ??
[];
for (final walletId in ethWalletInfos.map((e) => e.walletId).toList()) {
final walletContracts = ref.read(pWalletTokenAddresses(walletId));
if (!walletContracts.contains(widget.entity.token.address)) {
ethWalletIds.add(walletId);
}
}
}
@override
void initState() {
ethWalletIds = [];
final walletsData =
ref.read(walletsServiceChangeNotifierProvider).fetchWalletsData();
_updateWalletsList(walletsData);
super.initState();
}
@override
Widget build(BuildContext context) {
// dumb hack
ref.watch(newEthWalletTriggerTempUntilHiveCompletelyDeleted);
final walletsData =
ref.read(walletsServiceChangeNotifierProvider).fetchWalletsData();
if (walletsData.length != _cachedWalletCount) {
_updateWalletsList(walletsData);
}
return WillPopScope(
onWillPop: () async {

View file

@ -0,0 +1,218 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
class VerifyMnemonicPassphraseDialog extends ConsumerStatefulWidget {
const VerifyMnemonicPassphraseDialog({super.key});
@override
ConsumerState<VerifyMnemonicPassphraseDialog> createState() =>
_VerifyMnemonicPassphraseDialogState();
}
class _VerifyMnemonicPassphraseDialogState
extends ConsumerState<VerifyMnemonicPassphraseDialog> {
late final FocusNode passwordFocusNode;
late final TextEditingController passwordController;
bool hidePassword = true;
bool _verifyLock = false;
void _verify() {
if (_verifyLock) {
return;
}
_verifyLock = true;
if (passwordController.text ==
ref.read(pNewWalletOptions.state).state!.mnemonicPassphrase) {
Navigator.of(context, rootNavigator: Util.isDesktop).pop("verified");
} else {
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Passphrase does not match",
context: context,
);
}
_verifyLock = false;
}
@override
void initState() {
passwordController = TextEditingController();
passwordFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
passwordController.dispose();
passwordFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopDialog(
maxHeight: double.infinity,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(
left: 32,
),
child: Text(
"Verify mnemonic passphrase",
style: STextStyles.desktopH3(context),
),
),
const DesktopDialogCloseButton(),
],
),
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
bottom: 32,
),
child: child,
),
],
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => StackDialogBase(
keyboardPaddingAmount: MediaQuery.of(context).viewInsets.bottom,
child: child,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!Util.isDesktop)
Text(
"Verify BIP39 passphrase",
style: STextStyles.pageTitleH2(context),
),
const SizedBox(
height: 24,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("mnemonicPassphraseFieldKey1"),
focusNode: passwordFocusNode,
controller: passwordController,
style: Util.isDesktop
? STextStyles.desktopTextMedium(context).copyWith(
height: 2,
)
: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Enter your BIP39 passphrase",
passwordFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => SizedBox(
height: 70,
child: child,
),
child: Row(
children: [
SizedBox(
width: Util.isDesktop ? 24 : 16,
),
GestureDetector(
key: const Key(
"mnemonicPassphraseFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: Util.isDesktop ? 24 : 16,
height: Util.isDesktop ? 24 : 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
),
),
),
SizedBox(
height: Util.isDesktop ? 48 : 24,
),
ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
onPressed: Navigator.of(
context,
rootNavigator: Util.isDesktop,
).pop,
),
),
const SizedBox(
width: 16,
),
Expanded(
child: child,
),
],
),
child: PrimaryButton(
label: "Verify",
onPressed: _verify,
),
),
],
),
),
);
}
}

View file

@ -16,20 +16,25 @@ 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/add_token_view/edit_wallet_tokens_view.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
import 'package:stackwallet/pages/add_wallet_views/select_wallet_for_token_view.dart';
import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/sub_widgets/word_table.dart';
import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/verify_mnemonic_passphrase_dialog.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/themes/stack_colors.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/util.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
@ -40,13 +45,13 @@ final createSpecialEthWalletRoutingFlag = StateProvider((ref) => false);
class VerifyRecoveryPhraseView extends ConsumerStatefulWidget {
const VerifyRecoveryPhraseView({
Key? key,
required this.manager,
required this.wallet,
required this.mnemonic,
}) : super(key: key);
static const routeName = "/verifyRecoveryPhrase";
final Manager manager;
final Wallet wallet;
final List<String> mnemonic;
@override
@ -58,13 +63,13 @@ class _VerifyRecoveryPhraseViewState
extends ConsumerState<VerifyRecoveryPhraseView>
// with WidgetsBindingObserver
{
late Manager _manager;
late Wallet _wallet;
late List<String> _mnemonic;
late final bool isDesktop;
@override
void initState() {
_manager = widget.manager;
_wallet = widget.wallet;
_mnemonic = widget.mnemonic;
isDesktop = Util.isDesktop;
// WidgetsBinding.instance?.addObserver(this);
@ -98,15 +103,30 @@ class _VerifyRecoveryPhraseViewState
// }
// }
Future<bool> _verifyMnemonicPassphrase() async {
final result = await showDialog<String?>(
context: context,
builder: (_) => const VerifyMnemonicPassphraseDialog(),
);
return result == "verified";
}
Future<void> _continue(bool isMatch) async {
if (isMatch) {
await ref.read(walletsServiceChangeNotifierProvider).setMnemonicVerified(
walletId: _manager.walletId,
if (ref.read(pNewWalletOptions.state).state != null) {
final passphraseVerified = await _verifyMnemonicPassphrase();
if (!passphraseVerified) {
return;
}
}
await ref.read(pWalletInfo(_wallet.walletId)).setMnemonicVerified(
isar: ref.read(mainDBProvider).isar,
);
ref
.read(walletsChangeNotifierProvider.notifier)
.addWallet(walletId: _manager.walletId, manager: _manager);
ref.read(pWallets).addWallet(_wallet);
final isCreateSpecialEthWallet =
ref.read(createSpecialEthWalletRoutingFlag);
@ -134,11 +154,11 @@ class _VerifyRecoveryPhraseViewState
DesktopHomeView.routeName,
),
);
if (widget.manager.coin == Coin.ethereum) {
if (widget.wallet.info.coin == Coin.ethereum) {
unawaited(
Navigator.of(context).pushNamed(
EditWalletTokensView.routeName,
arguments: widget.manager.walletId,
arguments: widget.wallet.walletId,
),
);
}
@ -157,11 +177,11 @@ class _VerifyRecoveryPhraseViewState
(route) => false,
),
);
if (widget.manager.coin == Coin.ethereum) {
if (widget.wallet.info.coin == Coin.ethereum) {
unawaited(
Navigator.of(context).pushNamed(
EditWalletTokensView.routeName,
arguments: widget.manager.walletId,
arguments: widget.wallet.walletId,
),
);
}
@ -241,10 +261,10 @@ class _VerifyRecoveryPhraseViewState
}
Future<void> delete() async {
await ref
.read(walletsServiceChangeNotifierProvider)
.deleteWallet(_manager.walletName, false);
await _manager.exitCurrentWallet();
await ref.read(pWallets).deleteWallet(
_wallet.info,
ref.read(secureStoreProvider),
);
}
@override

View file

@ -71,7 +71,7 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> {
} else {
ref
.read(addressBookFilterProvider)
.addAll(coins.getRange(0, coins.length - kTestNetCoinCount), false);
.addAll(coins.where((e) => !e.isTestNet), false);
}
} else {
ref.read(addressBookFilterProvider).add(widget.coin!, false);
@ -79,14 +79,14 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> {
WidgetsBinding.instance.addPostFrameCallback((_) async {
List<ContactAddressEntry> addresses = [];
final managers = ref.read(walletsChangeNotifierProvider).managers;
for (final manager in managers) {
final wallets = ref.read(pWallets).wallets;
for (final wallet in wallets) {
addresses.add(
ContactAddressEntry()
..coinName = manager.coin.name
..address = await manager.currentReceivingAddress
..coinName = wallet.info.coin.name
..address = (await wallet.getCurrentReceivingAddress())!.value
..label = "Current Receiving"
..other = manager.walletName,
..other = wallet.info.name,
);
}
final self = ContactEntry(

View file

@ -47,10 +47,7 @@ class _AddressBookFilterViewState extends ConsumerState<AddressBookFilterView> {
if (showTestNet) {
_coins = coins.toList(growable: false);
} else {
_coins = coins
.toList(growable: false)
.getRange(0, coins.length - kTestNetCoinCount)
.toList(growable: false);
_coins = coins.where((e) => !e.isTestNet).toList(growable: false);
}
super.initState();
}

View file

@ -85,7 +85,7 @@ class CoinSelectSheet extends StatelessWidget {
shrinkWrap: true,
itemCount: showTestNet
? coins_.length
: coins_.length - kTestNetCoinCount,
: coins_.where((e) => !e.isTestNet).length,
itemBuilder: (builderContext, index) {
final coin = coins_[index];
return Padding(

View file

@ -15,16 +15,14 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/address_book_views/subviews/add_new_contact_address_view.dart';
import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_address_view.dart';
import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/global/address_book_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/address_book_providers/address_entry_data_provider.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/themes/coin_icon_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
@ -63,29 +61,33 @@ class _ContactDetailsViewState extends ConsumerState<ContactDetailsView> {
List<Tuple2<String, Transaction>> _cachedTransactions = [];
Future<List<Tuple2<String, Transaction>>> _filteredTransactionsByContact(
List<Manager> managers,
) async {
Future<List<Tuple2<String, Transaction>>>
_filteredTransactionsByContact() async {
final contact =
ref.read(addressBookServiceProvider).getContactById(_contactId);
// TODO: optimise
List<Tuple2<String, Transaction>> result = [];
for (final manager in managers) {
final transactions = await MainDB.instance
.getTransactions(manager.walletId)
.filter()
.anyOf(contact.addresses.map((e) => e.address),
(q, String e) => q.address((q) => q.valueEqualTo(e)))
.sortByTimestampDesc()
.findAll();
final transactions = await ref
.read(mainDBProvider)
.isar
.transactions
.where()
.filter()
.anyOf(contact.addresses.map((e) => e.address),
(q, String e) => q.address((q) => q.valueEqualTo(e)))
.sortByTimestampDesc()
.findAll();
for (final tx in transactions) {
result.add(Tuple2(manager.walletId, tx));
}
List<Tuple2<String, Transaction>> result = [];
for (final tx in transactions) {
result.add(Tuple2(tx.walletId, tx));
}
// sort by date
result.sort((a, b) => b.item2.timestamp - a.item2.timestamp);
return result;
}
@ -461,8 +463,7 @@ class _ContactDetailsViewState extends ConsumerState<ContactDetailsView> {
height: 12,
),
FutureBuilder(
future: _filteredTransactionsByContact(
ref.watch(walletsChangeNotifierProvider).managers),
future: _filteredTransactionsByContact(),
builder: (_,
AsyncSnapshot<List<Tuple2<String, Transaction>>>
snapshot) {

View file

@ -19,6 +19,7 @@ import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/address_book_views/subviews/contact_details_view.dart';
import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_2_view.dart';
import 'package:stackwallet/pages/send_view/send_view.dart';
import 'package:stackwallet/providers/global/active_wallet_provider.dart';
import 'package:stackwallet/providers/global/address_book_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/themes/coin_icon_provider.dart';
@ -28,6 +29,7 @@ import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:tuple/tuple.dart';
@ -51,21 +53,15 @@ class ContactPopUp extends ConsumerWidget {
final contact = ref.watch(addressBookServiceProvider
.select((value) => value.getContactById(contactId)));
final active = ref
.read(walletsChangeNotifierProvider)
.managers
.where((e) => e.isActiveWallet)
.toList(growable: false);
final active = ref.read(currentWalletIdProvider);
assert(active.isEmpty || active.length == 1);
bool hasActiveWallet = active.length == 1;
bool hasActiveWallet = active != null;
bool isExchangeFlow =
ref.watch(exchangeFlowIsActiveStateProvider.state).state;
final addresses = contact.addressesSorted.where((e) {
if (hasActiveWallet && !isExchangeFlow) {
return e.coin == active[0].coin;
return e.coin == ref.watch(pWalletCoin(active));
} else {
return true;
}
@ -201,7 +197,7 @@ class ContactPopUp extends ConsumerWidget {
child: RoundedWhiteContainer(
child: Center(
child: Text(
"No ${active[0].coin.prettyName} addresses found",
"No ${ref.watch(pWalletCoin(active!)).prettyName} addresses found",
style:
STextStyles.itemSubtitle(context),
),
@ -372,8 +368,9 @@ class ContactPopUp extends ConsumerWidget {
.pushNamed(
SendView.routeName,
arguments: Tuple3(
active[0].walletId,
active[0].coin,
active,
ref.read(
pWalletCoin(active)),
SendViewAutoFillData(
address: address,
contactLabel:

View file

@ -96,7 +96,7 @@ class _NewContactAddressEntryFormState
coins.remove(Coin.firoTestNet);
if (showTestNet) {
coins = coins.sublist(0, coins.length - kTestNetCoinCount);
coins = coins.where((e) => !e.isTestNet).toList();
}
}

View file

@ -1160,15 +1160,14 @@ class _BuyFormState extends ConsumerState<BuyForm> {
)
.then((value) async {
if (value is String) {
final manager = ref
.read(walletsChangeNotifierProvider)
.getManager(value);
final wallet = ref.read(pWallets).getWallet(value);
// _toController.text = manager.walletName;
// model.recipientAddress =
// await manager.currentReceivingAddress;
_receiveAddressController.text =
await manager.currentReceivingAddress;
(await wallet.getCurrentReceivingAddress())!
.value;
setState(() {
_addressToggleFlag =

View file

@ -9,37 +9,88 @@
*/
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart';
import 'package:stackwallet/pages/buy_view/buy_form.dart';
import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/tor_subscription.dart';
class BuyView extends StatelessWidget {
class BuyView extends ConsumerStatefulWidget {
const BuyView({
Key? key,
this.coin,
this.tokenContract,
}) : super(key: key);
static const String routeName = "/stackBuyView";
final Coin? coin;
final EthContract? tokenContract;
static const String routeName = "/stackBuyView";
@override
ConsumerState<BuyView> createState() => _BuyViewState();
}
class _BuyViewState extends ConsumerState<BuyView> {
Coin? coin;
EthContract? tokenContract;
late bool torEnabled;
@override
void initState() {
coin = widget.coin;
tokenContract = widget.tokenContract;
torEnabled =
ref.read(pTorService).status != TorConnectionStatus.disconnected;
super.initState();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
return SafeArea(
child: Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
),
child: BuyForm(
coin: coin,
tokenContract: tokenContract,
),
return TorSubscription(
onTorStatusChanged: (status) {
setState(() {
torEnabled = status != TorConnectionStatus.disconnected;
});
},
child: Stack(
children: [
SafeArea(
child: Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
),
child: BuyForm(
coin: coin,
tokenContract: tokenContract,
),
),
),
if (torEnabled)
Container(
color: Theme.of(context)
.extension<StackColors>()!
.overlay
.withOpacity(0.7),
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: const StackDialog(
title: "Tor is enabled",
message: "Purchasing not available while Tor is enabled",
),
),
],
),
);
}

View file

@ -0,0 +1,451 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by sneurlax on 2023-07-26
*
*/
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_native_splash/cli_commands.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/cashfusion/fusion_progress_view.dart';
import 'package:stackwallet/pages/cashfusion/fusion_rounds_selection_sheet.dart';
import 'package:stackwallet/providers/cash_fusion/fusion_progress_ui_state_provider.dart';
import 'package:stackwallet/providers/global/prefs_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/themes/stack_colors.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/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart';
import 'package:stackwallet/widgets/background.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/desktop/primary_button.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
class CashFusionView extends ConsumerStatefulWidget {
const CashFusionView({
super.key,
required this.walletId,
});
static const routeName = "/cashFusionView";
final String walletId;
@override
ConsumerState<CashFusionView> createState() => _CashFusionViewState();
}
class _CashFusionViewState extends ConsumerState<CashFusionView> {
late final TextEditingController serverController;
late final FocusNode serverFocusNode;
late final TextEditingController portController;
late final FocusNode portFocusNode;
late final TextEditingController fusionRoundController;
late final FocusNode fusionRoundFocusNode;
late final Coin coin;
bool _enableSSLCheckbox = false;
bool _enableStartButton = false;
FusionOption _option = FusionOption.continuous;
Future<void> _startFusion() async {
final fusionWallet =
ref.read(pWallets).getWallet(widget.walletId) as CashFusionInterface;
try {
fusionWallet.uiState = ref.read(
fusionProgressUIStateProvider(widget.walletId),
);
} catch (e) {
if (!e.toString().contains(
"FusionProgressUIState was already set for ${widget.walletId}")) {
rethrow;
}
}
final int rounds = _option == FusionOption.continuous
? 0
: int.parse(fusionRoundController.text);
final newInfo = FusionInfo(
host: serverController.text,
port: int.parse(portController.text),
ssl: _enableSSLCheckbox,
rounds: rounds,
);
// update user prefs (persistent)
ref.read(prefsChangeNotifierProvider).setFusionServerInfo(coin, newInfo);
unawaited(
fusionWallet.fuse(
fusionInfo: newInfo,
),
);
await Navigator.of(context).pushNamed(
FusionProgressView.routeName,
arguments: widget.walletId,
);
}
@override
void initState() {
serverController = TextEditingController();
portController = TextEditingController();
fusionRoundController = TextEditingController();
serverFocusNode = FocusNode();
portFocusNode = FocusNode();
fusionRoundFocusNode = FocusNode();
coin = ref.read(pWalletCoin(widget.walletId));
final info =
ref.read(prefsChangeNotifierProvider).getFusionServerInfo(coin);
serverController.text = info.host;
portController.text = info.port.toString();
_enableSSLCheckbox = info.ssl;
_option = info.rounds == 0 ? FusionOption.continuous : FusionOption.custom;
fusionRoundController.text = info.rounds.toString();
_enableStartButton =
serverController.text.isNotEmpty && portController.text.isNotEmpty;
super.initState();
}
@override
void dispose() {
serverController.dispose();
portController.dispose();
fusionRoundController.dispose();
serverFocusNode.dispose();
portFocusNode.dispose();
fusionRoundFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Background(
child: SafeArea(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
automaticallyImplyLeading: false,
leading: const AppBarBackButton(),
title: Text(
"Fusion",
style: STextStyles.navBarTitle(context),
),
titleSpacing: 0,
actions: [
AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
size: 36,
icon: SvgPicture.asset(
Assets.svg.circleQuestion,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: () async {
//' TODO show about?
},
),
),
],
),
body: LayoutBuilder(
builder: (builderContext, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RoundedWhiteContainer(
child: Text(
"Fusion helps anonymize your coins by mixing them.",
style: STextStyles.w500_12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
),
const SizedBox(
height: 16,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Server settings",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
),
),
CustomTextButton(
text: "Default",
onTap: () {
final def = kFusionServerInfoDefaults[coin]!;
serverController.text = def.host;
portController.text = def.port.toString();
fusionRoundController.text =
def.rounds.toString();
_option = FusionOption.continuous;
setState(() {
_enableSSLCheckbox = def.ssl;
});
},
),
],
),
const SizedBox(
height: 12,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: false,
enableSuggestions: false,
controller: serverController,
focusNode: serverFocusNode,
onChanged: (value) {
setState(() {
_enableStartButton = value.isNotEmpty &&
portController.text.isNotEmpty &&
fusionRoundController.text.isNotEmpty;
});
},
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Server",
serverFocusNode,
context,
desktopMed: true,
),
),
),
const SizedBox(
height: 10,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: false,
enableSuggestions: false,
controller: portController,
focusNode: portFocusNode,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
],
keyboardType: TextInputType.number,
onChanged: (value) {
setState(() {
_enableStartButton = value.isNotEmpty &&
serverController.text.isNotEmpty &&
fusionRoundController.text.isNotEmpty;
});
},
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Port",
portFocusNode,
context,
),
),
),
const SizedBox(
height: 10,
),
GestureDetector(
onTap: () {
setState(() {
_enableSSLCheckbox = !_enableSSLCheckbox;
});
},
child: Container(
color: Colors.transparent,
child: Row(
children: [
SizedBox(
width: 20,
height: 20,
child: Checkbox(
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
value: _enableSSLCheckbox,
onChanged: (newValue) {
setState(
() {
_enableSSLCheckbox =
!_enableSSLCheckbox;
},
);
},
),
),
const SizedBox(
width: 12,
),
Text(
"Use SSL",
style: STextStyles.itemSubtitle12(context),
),
],
),
),
),
const SizedBox(
height: 16,
),
Text(
"Rounds of fusion",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
),
),
const SizedBox(
height: 12,
),
RoundedContainer(
onPressed: () async {
final option =
await showModalBottomSheet<FusionOption?>(
backgroundColor: Colors.transparent,
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
builder: (_) {
return FusionRoundCountSelectSheet(
currentOption: _option,
);
},
);
if (option != null) {
setState(() {
_option = option;
});
}
},
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveBG,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
_option.name.capitalize(),
style: STextStyles.w500_12(context),
),
SvgPicture.asset(
Assets.svg.chevronDown,
width: 12,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
],
),
),
),
if (_option == FusionOption.custom)
const SizedBox(
height: 10,
),
if (_option == FusionOption.custom)
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: false,
enableSuggestions: false,
controller: fusionRoundController,
focusNode: fusionRoundFocusNode,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
],
keyboardType: TextInputType.number,
onChanged: (value) {
setState(() {
_enableStartButton = value.isNotEmpty &&
serverController.text.isNotEmpty &&
portController.text.isNotEmpty;
});
},
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Number of fusions",
fusionRoundFocusNode,
context,
).copyWith(
labelText: "Enter number of fusions.."),
),
),
const SizedBox(
height: 16,
),
const Spacer(),
PrimaryButton(
label: "Start",
enabled: _enableStartButton,
onPressed: _startFusion,
),
],
),
),
),
),
);
},
),
),
),
);
}
}

View file

@ -0,0 +1,254 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by julian on 2023-10-16
*
*/
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages_desktop_specific/cashfusion/sub_widgets/fusion_progress.dart';
import 'package:stackwallet/providers/cash_fusion/fusion_progress_ui_state_provider.dart';
import 'package:stackwallet/providers/global/prefs_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class FusionProgressView extends ConsumerStatefulWidget {
const FusionProgressView({
super.key,
required this.walletId,
});
static const routeName = "/cashFusionProgressView";
final String walletId;
@override
ConsumerState<FusionProgressView> createState() => _FusionProgressViewState();
}
class _FusionProgressViewState extends ConsumerState<FusionProgressView> {
late final Coin coin;
Future<bool> _requestAndProcessCancel() async {
final shouldCancel = await showDialog<bool?>(
context: context,
barrierDismissible: false,
builder: (_) => StackDialog(
title: "Cancel fusion?",
leftButton: SecondaryButton(
label: "No",
buttonHeight: null,
onPressed: () {
Navigator.of(context).pop(false);
},
),
rightButton: PrimaryButton(
label: "Yes",
buttonHeight: null,
onPressed: () {
Navigator.of(context).pop(true);
},
),
),
);
if (shouldCancel == true && mounted) {
final fusionWallet =
ref.read(pWallets).getWallet(widget.walletId) as CashFusionInterface;
await showLoading(
whileFuture: Future.wait([
fusionWallet.stop(),
Future<void>.delayed(const Duration(seconds: 2)),
]),
context: context,
isDesktop: Util.isDesktop,
message: "Stopping fusion",
);
return true;
} else {
return false;
}
}
@override
void initState() {
coin = ref.read(pWalletCoin(widget.walletId));
super.initState();
}
@override
Widget build(BuildContext context) {
final bool _succeeded =
ref.watch(fusionProgressUIStateProvider(widget.walletId)).succeeded;
final bool _failed =
ref.watch(fusionProgressUIStateProvider(widget.walletId)).failed;
final int _fusionRoundsCompleted = ref
.watch(fusionProgressUIStateProvider(widget.walletId))
.fusionRoundsCompleted;
return WillPopScope(
onWillPop: () async {
return await _requestAndProcessCancel();
},
child: Background(
child: SafeArea(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
automaticallyImplyLeading: false,
leading: AppBarBackButton(
onPressed: () async {
if (await _requestAndProcessCancel()) {
if (mounted) {
Navigator.of(context).pop();
}
}
},
),
title: Text(
"Fusion progress",
style: STextStyles.navBarTitle(context),
),
titleSpacing: 0,
),
body: LayoutBuilder(
builder: (builderContext, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_fusionRoundsCompleted == 0)
RoundedContainer(
color: Theme.of(context)
.extension<StackColors>()!
.snackBarBackError,
child: Text(
"Do not close this window. If you exit, "
"the process will be canceled.",
style:
STextStyles.smallMed14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.snackBarTextError,
),
textAlign: TextAlign.center,
),
),
if (_fusionRoundsCompleted > 0)
RoundedContainer(
color: Theme.of(context)
.extension<StackColors>()!
.snackBarBackInfo,
child: Text(
"Fusion rounds completed: $_fusionRoundsCompleted",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.snackBarTextInfo,
),
textAlign: TextAlign.center,
),
),
const SizedBox(
height: 20,
),
FusionProgress(
walletId: widget.walletId,
),
const Spacer(),
const SizedBox(
height: 16,
),
if (_succeeded)
PrimaryButton(
label: "Fuse again",
onPressed: _fuseAgain,
),
if (_succeeded)
const SizedBox(
height: 16,
),
if (_failed)
PrimaryButton(
label: "Try again",
onPressed: _fuseAgain,
),
if (_failed)
const SizedBox(
height: 16,
),
SecondaryButton(
label: "Cancel",
onPressed: () async {
if (await _requestAndProcessCancel()) {
if (mounted) {
Navigator.of(context).pop();
}
}
},
),
],
),
),
),
),
);
},
),
),
),
),
);
}
/// Fuse again.
void _fuseAgain() async {
final fusionWallet =
ref.read(pWallets).getWallet(widget.walletId) as CashFusionInterface;
final fusionInfo =
ref.read(prefsChangeNotifierProvider).getFusionServerInfo(coin);
try {
fusionWallet.uiState = ref.read(
fusionProgressUIStateProvider(widget.walletId),
);
} catch (e) {
if (!e.toString().contains(
"FusionProgressUIState was already set for ${widget.walletId}")) {
rethrow;
}
}
unawaited(fusionWallet.fuse(fusionInfo: fusionInfo));
}
}

View file

@ -0,0 +1,168 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by julian on 2023-10-16
*
*/
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_native_splash/cli_commands.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
enum FusionOption {
continuous,
custom;
}
class FusionRoundCountSelectSheet extends HookWidget {
const FusionRoundCountSelectSheet({
Key? key,
required this.currentOption,
}) : super(key: key);
final FusionOption currentOption;
@override
Widget build(BuildContext context) {
final option = useState(currentOption);
return WillPopScope(
onWillPop: () async {
Navigator.of(context).pop(option.value);
return false;
},
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(20),
),
),
child: Padding(
padding: const EdgeInsets.only(
left: 24,
right: 24,
top: 10,
bottom: 0,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
width: 60,
height: 4,
),
),
const SizedBox(
height: 36,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Rounds of fusion",
style: STextStyles.pageTitleH2(context),
textAlign: TextAlign.left,
),
const SizedBox(
height: 20,
),
for (int i = 0; i < FusionOption.values.length; i++)
Column(
children: [
GestureDetector(
onTap: () {
option.value = FusionOption.values[i];
Navigator.of(context).pop(option.value);
},
child: Container(
color: Colors.transparent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Column(
// mainAxisAlignment: MainAxisAlignment.start,
// children: [
SizedBox(
width: 20,
height: 20,
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: FusionOption.values[i],
groupValue: option.value,
onChanged: (_) {
option.value = FusionOption.values[i];
Navigator.of(context).pop(option.value);
},
),
),
// ],
// ),
const SizedBox(
width: 12,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
FusionOption.values[i].name.capitalize(),
style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
const SizedBox(
height: 2,
),
Text(
FusionOption.values[i] ==
FusionOption.continuous
? "Keep fusing until manually stopped"
: "Stop after a set number of fusions",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
),
textAlign: TextAlign.left,
),
],
),
],
),
),
),
const SizedBox(
height: 16,
),
],
),
const SizedBox(
height: 16,
),
],
),
],
),
),
),
);
}
}

View file

@ -19,7 +19,6 @@ 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/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart';
@ -27,6 +26,8 @@ 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/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart';
import 'package:stackwallet/widgets/app_bar_field.dart';
import 'package:stackwallet/widgets/background.dart';
@ -82,11 +83,9 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
final Set<UTXO> _selectedBlocked = {};
Future<void> _refreshBalance() async {
final coinControlInterface = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as CoinControlInterface;
await coinControlInterface.refreshBalance(notify: true);
final coinControlInterface =
ref.read(pWallets).getWallet(widget.walletId) as CoinControlInterface;
await coinControlInterface.updateBalance();
}
@override
@ -113,25 +112,14 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final coin = ref.watch(
walletsChangeNotifierProvider.select(
(value) => value
.getManager(
widget.walletId,
)
.coin,
),
);
final minConfirms = ref
.watch(pWallets)
.getWallet(widget.walletId)
.cryptoCurrency
.minConfirms;
final currentChainHeight = ref.watch(
walletsChangeNotifierProvider.select(
(value) => value
.getManager(
widget.walletId,
)
.currentHeight,
),
);
final coin = ref.watch(pWalletCoin(widget.walletId));
final currentHeight = ref.watch(pWalletChainHeight(widget.walletId));
if (_sort == CCSortDescriptor.address && !_isSearching) {
_list = null;
@ -357,8 +345,8 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
(widget.type == CoinControlViewType.use &&
!utxo.isBlocked &&
utxo.isConfirmed(
currentChainHeight,
coin.requiredConfirmations,
currentHeight,
minConfirms,
)),
initialSelectedState: isSelected,
onSelectedChanged: (value) {
@ -420,8 +408,8 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
CoinControlViewType.use &&
!_showBlocked &&
utxo.isConfirmed(
currentChainHeight,
coin.requiredConfirmations,
currentHeight,
minConfirms,
)),
initialSelectedState: isSelected,
onSelectedChanged: (value) {
@ -562,8 +550,8 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
.use &&
!utxo.isBlocked &&
utxo.isConfirmed(
currentChainHeight,
coin.requiredConfirmations,
currentHeight,
minConfirms,
)),
initialSelectedState: isSelected,
onSelectedChanged: (value) {

View file

@ -19,6 +19,7 @@ import 'package:stackwallet/utilities/amount/amount_formatter.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/wallets/isar/providers/wallet_info_provider.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';
@ -64,11 +65,8 @@ class _UtxoCardState extends ConsumerState<UtxoCard> {
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));
final coin = ref.watch(pWalletCoin(widget.walletId));
final currentHeight = ref.watch(pWalletChainHeight(widget.walletId));
return ConditionalParent(
condition: widget.onPressed != null,
@ -113,8 +111,12 @@ class _UtxoCardState extends ConsumerState<UtxoCard> {
child: UTXOStatusIcon(
blocked: utxo.isBlocked,
status: utxo.isConfirmed(
currentChainHeight,
coin.requiredConfirmations,
currentHeight,
ref
.watch(pWallets)
.getWallet(widget.walletId)
.cryptoCurrency
.minConfirms,
)
? UTXOStatusIconStatus.confirmed
: UTXOStatusIconStatus.unconfirmed,

View file

@ -23,6 +23,7 @@ import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.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';
@ -91,21 +92,12 @@ class _UtxoDetailsViewState extends ConsumerState<UtxoDetailsView> {
@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 coin = ref.watch(pWalletCoin(widget.walletId));
final currentHeight = ref.watch(pWalletChainHeight(widget.walletId));
final confirmed = utxo!.isConfirmed(
currentHeight,
coin.requiredConfirmations,
ref.watch(pWallets).getWallet(widget.walletId).cryptoCurrency.minConfirms,
);
return ConditionalParent(

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