Merge branch 'main' of https://github.com/cake-tech/cake_wallet into bitcoin-derivations

This commit is contained in:
Matthew Fosse 2024-04-16 09:20:01 -07:00
commit 27ab2011eb
204 changed files with 7074 additions and 2054 deletions

BIN
.github/assets/Logo_CakeWallet.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

48
.github/assets/NOTICE.txt vendored Normal file
View file

@ -0,0 +1,48 @@
Notice for linux-badge.svg:
1:
This is the Linux-penguin again...
Originally drewn by Larry Ewing (http://www.isc.tamu.edu/~lewing/)
(with the GIMP) the Linux Logo has been vectorized by me (Simon Budig,
http://www.home.unix-ag.org/simon/).
This happened quite some time ago with Corel Draw 4. But luckily
meanwhile there are tools available to handle vector graphics with
Linux. Bernhard Herzog (bernhard@users.sourceforge.net) deserves kudos
for creating Sketch (http://sketch.sourceforge.net), a powerful free
tool for creating vector graphics. He converted the Corel Draw file to
the Sketch native format. Since I am unable to maintain the Corel Draw
file any longer, the Sketch version now is the "official" one.
Anja Gerwinski (anja@gerwinski.de) has created an alternate version of
the penguin (penguin-variant.sk) with a thinner mouth line and slightly
altered gradients. It also features a nifty drop shadow.
The third bird (penguin-flat.sk) is a version reduced to three colors
(black/white/yellow) for e.g. silk screen printing. I made this version
for a mug, available at the friendly folks at
http://www.kernelconcepts.de/ - they do good stuff, mail Petra
(pinguin@kernelconcepts.de) if you need something special or don't
understand the german :-)
These drawings are copyrighted by Larry Ewing and Simon Budig
(penguin-variant.sk also by Anja Gerwinski), redistribution is free but
has to include this README/Copyright notice.
The use of these drawings is free. However I am happy about a sample of
your mug/t-shirt/whatever with this penguin on it...
Have fun
Simon Budig
Simon.Budig@unix-ag.org
http://www.home.unix-ag.org/simon/
Simon Budig
Am Hardtkoeppel 2
D-61279 Graevenwiesbach
2:
Attribution: lewing@isc.tamu.edu Larry Ewing and The GIMP

46
.github/assets/app-store-badge.svg vendored Executable file
View file

@ -0,0 +1,46 @@
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
<title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
<g>
<g>
<g>
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<g id="_Group_3" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
</g>
</g>
<g>
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
</g>
</g>
</g>
<g id="_Group_4" data-name="&lt;Group&gt;">
<g>
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

BIN
.github/assets/devices.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
.github/assets/f-droid-badge.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
.github/assets/google-play-badge.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

1071
.github/assets/linux-badge.svg vendored Executable file

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 67 KiB

51
.github/assets/mac-store-badge.svg vendored Executable file
View file

@ -0,0 +1,51 @@
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="156.10054" height="40" viewBox="0 0 156.10054 40">
<title>Download_on_the_Mac_App_Store_Badge_US-UK_RGB_blk_092917</title>
<g>
<g>
<g>
<path d="M146.57123,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.4378,6.4378,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01514.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27446,6.27446,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H146.57123c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
<path d="M8.44483,39.125c-.30468,0-.60205-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72184,5.72184,0,0,1-.543-1.6572,12.41339,12.41339,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37032,12.37032,0,0,1,.16553-1.87207,5.75552,5.75552,0,0,1,.54346-1.6621A5.3735,5.3735,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875h139.205l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73127,12.73127,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<g id="_Group_3" data-name="&lt;Group&gt;">
<g id="_Group_4" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
</g>
</g>
<g>
<path d="M46.14895,30.49609V21.35645H46.0884l-3.74316,9.04492H40.91652l-3.75293-9.04492H37.104v9.13965H35.34816v-12.418h2.22949l4.01855,9.80176h.06836l4.01074-9.80176h2.2373v12.418Z" style="fill: #fff"/>
<path d="M49.396,27.92285c0-1.583,1.21289-2.53906,3.36523-2.668l2.47852-.1377v-.68848c0-1.00684-.66309-1.5752-1.791-1.5752a1.73035,1.73035,0,0,0-1.90137,1.27441H49.8091c.05176-1.63574,1.5752-2.79687,3.69141-2.79687,2.16016,0,3.58887,1.17871,3.58887,2.96v6.20508H55.30813V29.00684h-.043a3.23683,3.23683,0,0,1-2.85742,1.64453A2.74447,2.74447,0,0,1,49.396,27.92285Zm5.84375-.81738V26.4082l-2.22949.1377c-1.11035.06934-1.73828.55078-1.73828,1.3252,0,.792.6543,1.30859,1.65234,1.30859A2.17046,2.17046,0,0,0,55.23977,27.10547Z" style="fill: #fff"/>
<path d="M64.89309,24.55762a1.99909,1.99909,0,0,0-2.13379-1.66895c-1.42871,0-2.375,1.19629-2.375,3.08105,0,1.92773.95508,3.08887,2.3916,3.08887a1.94829,1.94829,0,0,0,2.11719-1.626h1.79A3.61835,3.61835,0,0,1,62.7593,30.6084c-2.582,0-4.26855-1.76465-4.26855-4.63867,0-2.81445,1.68652-4.63867,4.251-4.63867a3.63931,3.63931,0,0,1,3.9248,3.22656Z" style="fill: #fff"/>
<path d="M78.7593,27.13965H74.0259l-1.13672,3.35645H70.8843l4.4834-12.418h2.083l4.4834,12.418H79.895Zm-4.24316-1.54883h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
<path d="M91.61672,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438H83.0884V21.44238h1.79883v1.50586h.03418a3.21161,3.21161,0,0,1,2.88281-1.60059C90.10207,21.34766,91.61672,23.16406,91.61672,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C88.7593,29.01563,89.70656,27.81934,89.70656,25.96973Z" style="fill: #fff"/>
<path d="M101.58156,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238h1.79883v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C100.06691,21.34766,101.58156,23.16406,101.58156,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C98.72414,29.01563,99.67141,27.81934,99.67141,25.96973Z" style="fill: #fff"/>
<path d="M108.1675,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52149.75684-2.52149,1.8584c0,.87793.65431,1.39453,2.25489,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
<path d="M119.80324,19.2998v2.14258h1.72168v1.47168h-1.72168v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406h-1.31641V21.44238h1.31641V19.2998Z" style="fill: #fff"/>
<path d="M122.521,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C124.18215,30.6084,122.521,28.82617,122.521,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40137,1.16211-2.40137,3.10742c0,1.96191.89551,3.10645,2.40137,3.10645S129.21633,27.93164,129.21633,25.96973Z" style="fill: #fff"/>
<path d="M132.64309,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.598,2.598,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93651,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
<path d="M145.84035,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,139.55813,25.13477Z" style="fill: #fff"/>
</g>
</g>
</g>
</g>
<g id="_Group_5" data-name="&lt;Group&gt;">
<g>
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57521,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
<path d="M61.21779,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
<path d="M66.40041,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.40041,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29494,13.03955Z" style="fill: #fff"/>
<path d="M71.34768,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.07131,14.772,71.34768,13.87061,71.34768,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72072,10.91846,72.26564,11.49707,72.26564,12.44434Z" style="fill: #fff"/>
<path d="M79.22951,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.124,13.99463,82.56252,13.42432,82.56252,12.44434Z" style="fill: #fff"/>
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -139,11 +139,13 @@ jobs:
echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart
echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart
echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart
echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart
echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart
echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart
echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart
echo "const robinhoodCIdApiSecret = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart

View file

@ -1,15 +1,35 @@
# Cake Wallet for Mobile and Desktop <div align="center">
## Open Source Multi-Currency Wallet <img height="100" src=".github/assets/Logo_CakeWallet.png">
## Links </div>
* Website: https://cakewallet.com ![devices](.github/assets/devices.png)
* App Store (iOS / MacOS): https://cakewallet.com/ios
* Google Play: https://cakewallet.com/gp <div align="center">
* F-Droid: https://fdroid.cakelabs.com
* APK: https://github.com/cake-tech/cake_wallet/releases [<img height="42" src=".github/assets/app-store-badge.svg">](https://apps.apple.com/us/app/cake-wallet/id1334702542?platform=iphone)
* Linux: https://github.com/cake-tech/cake_wallet/releases [<img height="42" src=".github/assets/google-play-badge.png">](https://play.google.com/store/apps/details?id=com.cakewallet.cake_wallet)
[<img height="42" src=".github/assets/f-droid-badge.png">](https://fdroid.cakelabs.com)
[<img height="42" src=".github/assets/mac-store-badge.svg">](https://apps.apple.com/us/app/cake-wallet/id1334702542?platform=mac)
[<img height="42" src=".github/assets/linux-badge.svg">](https://github.com/cake-tech/cake_wallet/releases)
</div>
# Cake Wallet
Cake Wallet is an open source, non-custodial, and private multi-currency crypto wallet for Android, iOS, macOS, and Linux.
Cake Wallet includes support for several cryptocurrencies, including:
* Monero (XMR)
* Bitcoin (BTC)
* Ethereum (ETH)
* Litecoin (LTC)
* Bitcoin Cash (BCH)
* Polygon (MATIC)
* Solana (SOL)
* Nano (XNO)
* Haven (XHV)
## Features ## Features

12
SECURITY.md Normal file
View file

@ -0,0 +1,12 @@
# Security Policy
## Reporting a Vulnerability
If you need to report a vulnerability, please either:
* Open a security advisory: https://github.com/cake-tech/cake_wallet/security/advisories/new
* Send an email to `dev@cakewallet.com` with details on the vulnerability
## Supported Versions
As we don't maintain prevoius versions of the app, only the latest release for each platform is supported and any updates will bump the version number.

BIN
assets/images/thorchain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -3,4 +3,26 @@
useSSL: true useSSL: true
is_default: true is_default: true
- -
uri: node.perish.co:9076 uri: node.nautilus.io
path: /api
useSSL: true
-
uri: app.natrium.io
path: /api
useSSL: true
-
uri: rainstorm.city
path: /api
useSSL: true
-
uri: node.somenano.com
path: /proxy
useSSL: true
-
uri: nanoslo.0x.no
path: /proxy
useSSL: true
-
uri: www.bitrequest.app
port: 8020
useSSL: true

View file

@ -1,4 +1,2 @@
Monero enhancements UI enhancements
In-App live status page for the app services Bug fixes
Add Exolix exchange provider
Bug fixes and enhancements

View file

@ -1,5 +1,7 @@
Monero enhancements Add Replace-By-Fee to boost pending Bitcoin transactions
Bitcoin support different address types (Taproot, Segwit P2WPKH/P2WSH, Legacy) Enable WalletConnect for Solana
In-App live status page for the app services WalletConnect Enhancements
Add Exolix exchange provider Enhancements for ERC-20 tokens and Solana tokens
Bug fixes and enhancements Enhancements for Nano wallet
UI enhancements
Bug fixes

View file

@ -3,6 +3,9 @@ import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin;
List<int> addressToOutputScript(String address, bitcoin.BasedUtxoNetwork network) { List<int> addressToOutputScript(String address, bitcoin.BasedUtxoNetwork network) {
try { try {
if (network == bitcoin.BitcoinCashNetwork.mainnet) {
return bitcoin.BitcoinCashAddress(address).baseAddress.toScriptPubKey().toBytes();
}
return bitcoin.addressToOutputScript(address: address, network: network); return bitcoin.addressToOutputScript(address: address, network: network);
} catch (err) { } catch (err) {
print(err); print(err);

View file

@ -1,5 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/script_hash.dart' as sh; import 'package:cw_bitcoin/script_hash.dart' as sh;
@ -20,10 +19,9 @@ class BitcoinAddressRecord {
_balance = balance, _balance = balance,
_name = name, _name = name,
_isUsed = isUsed, _isUsed = isUsed,
scriptHash = scriptHash = scriptHash ?? sh.scriptHash(address, network: network);
scriptHash ?? (network != null ? sh.scriptHash(address, network: network) : null);
factory BitcoinAddressRecord.fromJSON(String jsonSource, BasedUtxoNetwork? network) { factory BitcoinAddressRecord.fromJSON(String jsonSource, BasedUtxoNetwork network) {
final decoded = json.decode(jsonSource) as Map; final decoded = json.decode(jsonSource) as Map;
return BitcoinAddressRecord( return BitcoinAddressRecord(
@ -39,9 +37,7 @@ class BitcoinAddressRecord {
.firstWhere((type) => type.toString() == decoded['type'] as String) .firstWhere((type) => type.toString() == decoded['type'] as String)
: SegwitAddresType.p2wpkh, : SegwitAddresType.p2wpkh,
scriptHash: decoded['scriptHash'] as String?, scriptHash: decoded['scriptHash'] as String?,
network: (decoded['network'] as String?) == null network: network,
? network
: BasedUtxoNetwork.fromName(decoded['network'] as String),
); );
} }
@ -56,7 +52,7 @@ class BitcoinAddressRecord {
String _name; String _name;
bool _isUsed; bool _isUsed;
String? scriptHash; String? scriptHash;
BasedUtxoNetwork? network; BasedUtxoNetwork network;
int get txCount => _txCount; int get txCount => _txCount;
@ -76,8 +72,6 @@ class BitcoinAddressRecord {
@override @override
int get hashCode => address.hashCode; int get hashCode => address.hashCode;
String get cashAddr => bitbox.Address.toCashAddress(address);
BitcoinAddressType type; BitcoinAddressType type;
String updateScriptHash(BasedUtxoNetwork network) { String updateScriptHash(BasedUtxoNetwork network) {
@ -95,6 +89,5 @@ class BitcoinAddressRecord {
'balance': balance, 'balance': balance,
'type': type.toString(), 'type': type.toString(),
'scriptHash': scriptHash, 'scriptHash': scriptHash,
'network': network?.value,
}); });
} }

View file

@ -1,4 +1,8 @@
class BitcoinCommitTransactionException implements Exception { class BitcoinCommitTransactionException implements Exception {
String errorMessage;
BitcoinCommitTransactionException(this.errorMessage);
@override @override
String toString() => 'Transaction commit is failed.'; String toString() => errorMessage;
} }

View file

@ -2,7 +2,8 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_core/output_info.dart'; import 'package:cw_core/output_info.dart';
class BitcoinTransactionCredentials { class BitcoinTransactionCredentials {
BitcoinTransactionCredentials(this.outputs, {required this.priority, this.feeRate}); BitcoinTransactionCredentials(this.outputs,
{required this.priority, this.feeRate});
final List<OutputInfo> outputs; final List<OutputInfo> outputs;
final BitcoinTransactionPriority? priority; final BitcoinTransactionPriority? priority;

View file

@ -1,4 +0,0 @@
class BitcoinTransactionNoInputsException implements Exception {
@override
String toString() => 'Not enough inputs available. Please select more under Coin Control';
}

View file

@ -4,13 +4,15 @@ class BitcoinTransactionPriority extends TransactionPriority {
const BitcoinTransactionPriority({required String title, required int raw}) const BitcoinTransactionPriority({required String title, required int raw})
: super(title: title, raw: raw); : super(title: title, raw: raw);
static const List<BitcoinTransactionPriority> all = [fast, medium, slow]; static const List<BitcoinTransactionPriority> all = [fast, medium, slow, custom];
static const BitcoinTransactionPriority slow = static const BitcoinTransactionPriority slow =
BitcoinTransactionPriority(title: 'Slow', raw: 0); BitcoinTransactionPriority(title: 'Slow', raw: 0);
static const BitcoinTransactionPriority medium = static const BitcoinTransactionPriority medium =
BitcoinTransactionPriority(title: 'Medium', raw: 1); BitcoinTransactionPriority(title: 'Medium', raw: 1);
static const BitcoinTransactionPriority fast = static const BitcoinTransactionPriority fast =
BitcoinTransactionPriority(title: 'Fast', raw: 2); BitcoinTransactionPriority(title: 'Fast', raw: 2);
static const BitcoinTransactionPriority custom =
BitcoinTransactionPriority(title: 'Custom', raw: 3);
static BitcoinTransactionPriority deserialize({required int raw}) { static BitcoinTransactionPriority deserialize({required int raw}) {
switch (raw) { switch (raw) {
@ -20,6 +22,8 @@ class BitcoinTransactionPriority extends TransactionPriority {
return medium; return medium;
case 2: case 2:
return fast; return fast;
case 3:
return custom;
default: default:
throw Exception('Unexpected token: $raw for BitcoinTransactionPriority deserialize'); throw Exception('Unexpected token: $raw for BitcoinTransactionPriority deserialize');
} }
@ -39,7 +43,10 @@ class BitcoinTransactionPriority extends TransactionPriority {
label = 'Medium'; // S.current.transaction_priority_medium; label = 'Medium'; // S.current.transaction_priority_medium;
break; break;
case BitcoinTransactionPriority.fast: case BitcoinTransactionPriority.fast:
label = 'Fast'; // S.current.transaction_priority_fast; label = 'Fast';
break; // S.current.transaction_priority_fast;
case BitcoinTransactionPriority.custom:
label = 'Custom';
break; break;
default: default:
break; break;
@ -48,7 +55,10 @@ class BitcoinTransactionPriority extends TransactionPriority {
return label; return label;
} }
String labelWithRate(int rate) => '${toString()} ($rate ${units}/byte)'; String labelWithRate(int rate, int? customRate) {
final rateValue = this == custom ? customRate ??= 0 : rate;
return '${toString()} ($rateValue ${units}/byte)';
}
} }
class LitecoinTransactionPriority extends BitcoinTransactionPriority { class LitecoinTransactionPriority extends BitcoinTransactionPriority {

View file

@ -1,10 +0,0 @@
import 'package:cw_core/crypto_currency.dart';
class BitcoinTransactionWrongBalanceException implements Exception {
BitcoinTransactionWrongBalanceException(this.currency);
final CryptoCurrency currency;
@override
String toString() => 'You do not have enough ${currency.title} to send this amount.';
}

View file

@ -115,8 +115,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password, required String password,
}) async { }) async {
final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, final network = walletInfo.network != null
walletInfo.network != null ? BasedUtxoNetwork.fromName(walletInfo.network!) : null); ? BasedUtxoNetwork.fromName(walletInfo.network!)
: BitcoinNetwork.mainnet;
final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network);
walletInfo.derivationInfo ??= DerivationInfo( walletInfo.derivationInfo ??= DerivationInfo(
derivationType: snp.derivationType ?? DerivationType.electrum2, derivationType: snp.derivationType ?? DerivationType.electrum2,
@ -149,7 +151,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialRegularAddressIndex: snp.regularAddressIndex, initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex,
addressPageType: snp.addressPageType, addressPageType: snp.addressPageType,
networkParam: snp.network, networkParam: network,
); );
} }
} }

View file

@ -7,10 +7,9 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_bitcoin/script_hash.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'package:http/http.dart' as http;
String jsonrpcparams(List<Object> params) { String jsonrpcparams(List<Object> params) {
final _params = params?.map((val) => '"${val.toString()}"')?.join(','); final _params = params.map((val) => '"${val.toString()}"').join(',');
return '[$_params]'; return '[$_params]';
} }
@ -34,6 +33,7 @@ class ElectrumClient {
: _id = 0, : _id = 0,
_isConnected = false, _isConnected = false,
_tasks = {}, _tasks = {},
_errors = {},
unterminatedString = ''; unterminatedString = '';
static const connectionTimeout = Duration(seconds: 5); static const connectionTimeout = Duration(seconds: 5);
@ -44,6 +44,7 @@ class ElectrumClient {
void Function(bool)? onConnectionStatusChange; void Function(bool)? onConnectionStatusChange;
int _id; int _id;
final Map<String, SocketTask> _tasks; final Map<String, SocketTask> _tasks;
final Map<String, String> _errors;
bool _isConnected; bool _isConnected;
Timer? _aliveTimer; Timer? _aliveTimer;
String unterminatedString; String unterminatedString;
@ -243,30 +244,20 @@ class ElectrumClient {
}); });
Future<String> broadcastTransaction( Future<String> broadcastTransaction(
{required String transactionRaw, BasedUtxoNetwork? network}) async { {required String transactionRaw,
if (network == BitcoinNetwork.testnet) { BasedUtxoNetwork? network,
return http Function(int)? idCallback}) async =>
.post(Uri(scheme: 'https', host: 'blockstream.info', path: '/testnet/api/tx'), call(
headers: <String, String>{'Content-Type': 'application/json; charset=utf-8'}, method: 'blockchain.transaction.broadcast',
body: transactionRaw) params: [transactionRaw],
.then((http.Response response) { idCallback: idCallback)
if (response.statusCode == 200) { .then((dynamic result) {
return response.body; if (result is String) {
return result;
} }
throw Exception('Failed to broadcast transaction: ${response.body}'); return '';
}); });
}
return call(method: 'blockchain.transaction.broadcast', params: [transactionRaw])
.then((dynamic result) {
if (result is String) {
return result;
}
return '';
});
}
Future<Map<String, dynamic>> getMerkle({required String hash, required int height}) async => Future<Map<String, dynamic>> getMerkle({required String hash, required int height}) async =>
await call(method: 'blockchain.transaction.get_merkle', params: [hash, height]) await call(method: 'blockchain.transaction.get_merkle', params: [hash, height])
@ -371,10 +362,12 @@ class ElectrumClient {
} }
} }
Future<dynamic> call({required String method, List<Object> params = const []}) async { Future<dynamic> call(
{required String method, List<Object> params = const [], Function(int)? idCallback}) async {
final completer = Completer<dynamic>(); final completer = Completer<dynamic>();
_id += 1; _id += 1;
final id = _id; final id = _id;
idCallback?.call(id);
_registryTask(id, completer); _registryTask(id, completer);
socket!.write(jsonrpc(method: method, id: id, params: params)); socket!.write(jsonrpc(method: method, id: id, params: params));
@ -456,6 +449,23 @@ class ElectrumClient {
final id = response['id'] as String?; final id = response['id'] as String?;
final result = response['result']; final result = response['result'];
try {
final error = response['error'] as Map<String, dynamic>?;
if (error != null) {
final errorMessage = error['message'] as String?;
if (errorMessage != null) {
_errors[id!] = errorMessage;
}
}
} catch (_) {}
try {
final error = response['error'] as String?;
if (error != null) {
_errors[id!] = error;
}
} catch (_) {}
if (method is String) { if (method is String) {
_methodHandler(method: method, request: response); _methodHandler(method: method, request: response);
return; return;
@ -465,6 +475,8 @@ class ElectrumClient {
_finish(id, result); _finish(id, result);
} }
} }
String getErrorMessage(int id) => _errors[id.toString()] ?? '';
} }
// FIXME: move me // FIXME: move me

View file

@ -11,12 +11,11 @@ import 'package:cw_core/wallet_type.dart';
class ElectrumTransactionBundle { class ElectrumTransactionBundle {
ElectrumTransactionBundle(this.originalTransaction, ElectrumTransactionBundle(this.originalTransaction,
{required this.ins, required this.confirmations, this.time, required this.height}); {required this.ins, required this.confirmations, this.time});
final BtcTransaction originalTransaction; final BtcTransaction originalTransaction;
final List<BtcTransaction> ins; final List<BtcTransaction> ins;
final int? time; final int? time;
final int confirmations; final int confirmations;
final int height;
} }
class ElectrumTransactionInfo extends TransactionInfo { class ElectrumTransactionInfo extends TransactionInfo {
@ -25,6 +24,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
required int height, required int height,
required int amount, required int amount,
int? fee, int? fee,
List<String>? inputAddresses,
List<String>? outputAddresses,
required TransactionDirection direction, required TransactionDirection direction,
required bool isPending, required bool isPending,
required DateTime date, required DateTime date,
@ -32,6 +33,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
this.id = id; this.id = id;
this.height = height; this.height = height;
this.amount = amount; this.amount = amount;
this.inputAddresses = inputAddresses;
this.outputAddresses = outputAddresses;
this.fee = fee; this.fee = fee;
this.direction = direction; this.direction = direction;
this.date = date; this.date = date;
@ -100,6 +103,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
var amount = 0; var amount = 0;
var inputAmount = 0; var inputAmount = 0;
var totalOutAmount = 0; var totalOutAmount = 0;
List<String> inputAddresses = [];
List<String> outputAddresses = [];
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) { for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i]; final input = bundle.originalTransaction.inputs[i];
@ -108,6 +113,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
inputAmount += outTransaction.amount.toInt(); inputAmount += outTransaction.amount.toInt();
if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) { if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) {
direction = TransactionDirection.outgoing; direction = TransactionDirection.outgoing;
inputAddresses.add(addressFromOutputScript(outTransaction.scriptPubKey, network));
} }
} }
@ -115,6 +121,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
for (final out in bundle.originalTransaction.outputs) { for (final out in bundle.originalTransaction.outputs) {
totalOutAmount += out.amount.toInt(); totalOutAmount += out.amount.toInt();
final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network)); final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network));
outputAddresses.add(addressFromOutputScript(out.scriptPubKey, network));
if (addressExists) { if (addressExists) {
receivedAmounts.add(out.amount.toInt()); receivedAmounts.add(out.amount.toInt());
@ -137,6 +144,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
id: bundle.originalTransaction.txId(), id: bundle.originalTransaction.txId(),
height: height, height: height,
isPending: bundle.confirmations == 0, isPending: bundle.confirmations == 0,
inputAddresses: inputAddresses,
outputAddresses: outputAddresses,
fee: fee, fee: fee,
direction: direction, direction: direction,
amount: amount, amount: amount,
@ -187,6 +196,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
direction: parseTransactionDirectionFromInt(data['direction'] as int), direction: parseTransactionDirectionFromInt(data['direction'] as int),
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
isPending: data['isPending'] as bool, isPending: data['isPending'] as bool,
inputAddresses: data['inputAddresses'] as List<String>,
outputAddresses: data['outputAddresses'] as List<String>,
confirmations: data['confirmations'] as int); confirmations: data['confirmations'] as int);
} }
@ -218,6 +229,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
direction: direction, direction: direction,
date: date, date: date,
isPending: isPending, isPending: isPending,
inputAddresses: inputAddresses,
outputAddresses: outputAddresses,
confirmations: info.confirmations); confirmations: info.confirmations);
} }
@ -231,6 +244,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
m['isPending'] = isPending; m['isPending'] = isPending;
m['confirmations'] = confirmations; m['confirmations'] = confirmations;
m['fee'] = fee; m['fee'] = fee;
m['inputAddresses'] = inputAddresses;
m['outputAddresses'] = outputAddresses;
return m; return m;
} }
} }

View file

@ -7,11 +7,11 @@ import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin_base; import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin_base;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/bitcoin_wallet_keys.dart'; import 'package:cw_bitcoin/bitcoin_wallet_keys.dart';
import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum.dart';
@ -19,6 +19,7 @@ import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_history.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/litecoin_network.dart'; import 'package:cw_bitcoin/litecoin_network.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_bitcoin/script_hash.dart';
@ -75,11 +76,7 @@ abstract class ElectrumWalletBase
} }
: {}), : {}),
this.unspentCoinsInfo = unspentCoinsInfo, this.unspentCoinsInfo = unspentCoinsInfo,
this.network = networkType == bitcoin.bitcoin this.network = _getNetwork(networkType, currency),
? BitcoinNetwork.mainnet
: networkType == litecoinNetwork
? LitecoinNetwork.mainnet
: BitcoinNetwork.testnet,
this.isTestnet = networkType == bitcoin.testnet, this.isTestnet = networkType == bitcoin.testnet,
super(walletInfo) { super(walletInfo) {
this.electrumClient = electrumClient ?? ElectrumClient(); this.electrumClient = electrumClient ?? ElectrumClient();
@ -192,27 +189,27 @@ abstract class ElectrumWalletBase
} }
} }
Future<EstimatedTxResult> _estimateTxFeeAndInputsToUse( int get _dustAmount => 546;
int credentialsAmount,
bool sendAll, bool _isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet;
List<BitcoinBaseAddress> outputAddresses,
List<BitcoinOutput> outputs, Future<EstimatedTxResult> estimateSendAllTx(
BitcoinTransactionCredentials transactionCredentials, List<BitcoinOutput> outputs,
{int? inputsCount}) async { int feeRate, {
String? memo,
int credentialsAmount = 0,
}) async {
final utxos = <UtxoWithAddress>[]; final utxos = <UtxoWithAddress>[];
List<ECPrivate> privateKeys = []; List<ECPrivate> privateKeys = [];
int allInputsAmount = 0;
var leftAmount = credentialsAmount;
var allInputsAmount = 0;
for (int i = 0; i < unspentCoins.length; i++) { for (int i = 0; i < unspentCoins.length; i++) {
final utx = unspentCoins[i]; final utx = unspentCoins[i];
if (utx.isSending) { if (utx.isSending) {
allInputsAmount += utx.value; allInputsAmount += utx.value;
leftAmount = leftAmount - utx.value;
final address = _addressTypeFromStr(utx.address, network); final address = addressTypeFromStr(utx.address, network);
final privkey = generateECPrivate( final privkey = generateECPrivate(
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: utx.bitcoinAddressRecord.index, index: utx.bitcoinAddressRecord.index,
@ -228,15 +225,12 @@ abstract class ElectrumWalletBase
vout: utx.vout, vout: utx.vout,
scriptType: _getScriptType(address), scriptType: _getScriptType(address),
), ),
ownerDetails: ownerDetails: UtxoAddressDetails(
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address), publicKey: privkey.getPublic().toHex(),
address: address,
),
), ),
); );
bool amountIsAcquired = !sendAll && leftAmount <= 0;
if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
break;
}
} }
} }
@ -244,114 +238,314 @@ abstract class ElectrumWalletBase
throw BitcoinTransactionNoInputsException(); throw BitcoinTransactionNoInputsException();
} }
var changeValue = allInputsAmount - credentialsAmount; int estimatedSize;
if (network is BitcoinCashNetwork) {
if (!sendAll) { estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
if (changeValue > 0) { utxos: utxos,
final changeAddress = await walletAddresses.getChangeAddress(); outputs: outputs,
final address = _addressTypeFromStr(changeAddress, network); network: network as BitcoinCashNetwork,
outputAddresses.add(address); memo: memo,
outputs.add(BitcoinOutput(address: address, value: BigInt.from(changeValue))); );
} } else {
estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network,
memo: memo,
);
} }
final estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
utxos: utxos, outputs: outputs, network: network);
final fee = transactionCredentials.feeRate != null
? feeAmountWithFeeRate(transactionCredentials.feeRate!, 0, 0, size: estimatedSize)
: feeAmountForPriority(transactionCredentials.priority!, 0, 0, size: estimatedSize);
if (fee == 0) { if (fee == 0) {
throw BitcoinTransactionWrongBalanceException(currency); throw BitcoinTransactionNoFeeException();
} }
var amount = credentialsAmount; // Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change
int amount = allInputsAmount - fee;
final lastOutput = outputs.last; // Attempting to send less than the dust limit
if (!sendAll) { if (_isBelowDust(amount)) {
if (changeValue > fee) { throw BitcoinTransactionNoDustException();
// Here, lastOutput is change, deduct the fee from it }
outputs[outputs.length - 1] =
BitcoinOutput(address: lastOutput.address, value: lastOutput.value - BigInt.from(fee)); if (credentialsAmount > 0) {
final amountLeftForFee = amount - credentialsAmount;
if (amountLeftForFee > 0 && _isBelowDust(amountLeftForFee)) {
amount -= amountLeftForFee;
fee += amountLeftForFee;
} }
}
outputs[outputs.length - 1] =
BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount));
return EstimatedTxResult(
utxos: utxos,
privateKeys: privateKeys,
fee: fee,
amount: amount,
isSendAll: true,
hasChange: false,
memo: memo,
);
}
Future<EstimatedTxResult> estimateTxForAmount(
int credentialsAmount,
List<BitcoinOutput> outputs,
int feeRate, {
int? inputsCount,
String? memo,
}) async {
final utxos = <UtxoWithAddress>[];
List<ECPrivate> privateKeys = [];
int allInputsAmount = 0;
int leftAmount = credentialsAmount;
final sendingCoins = unspentCoins.where((utx) => utx.isSending).toList();
for (int i = 0; i < sendingCoins.length; i++) {
final utx = sendingCoins[i];
allInputsAmount += utx.value;
leftAmount = leftAmount - utx.value;
final address = addressTypeFromStr(utx.address, network);
final privkey = generateECPrivate(
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: utx.bitcoinAddressRecord.index,
network: network);
privateKeys.add(privkey);
utxos.add(
UtxoWithAddress(
utxo: BitcoinUtxo(
txHash: utx.hash,
value: BigInt.from(utx.value),
vout: utx.vout,
scriptType: _getScriptType(address),
),
ownerDetails: UtxoAddressDetails(
publicKey: privkey.getPublic().toHex(),
address: address,
),
),
);
bool amountIsAcquired = leftAmount <= 0;
if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
break;
}
}
if (utxos.isEmpty) {
throw BitcoinTransactionNoInputsException();
}
final spendingAllCoins = sendingCoins.length == utxos.length;
// How much is being spent - how much is being sent
int amountLeftForChangeAndFee = allInputsAmount - credentialsAmount;
if (amountLeftForChangeAndFee <= 0) {
throw BitcoinTransactionWrongBalanceException();
}
final changeAddress = await walletAddresses.getChangeAddress();
final address = addressTypeFromStr(changeAddress, network);
outputs.add(BitcoinOutput(
address: address,
value: BigInt.from(amountLeftForChangeAndFee),
));
int estimatedSize;
if (network is BitcoinCashNetwork) {
estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network as BitcoinCashNetwork,
memo: memo,
);
} else { } else {
// Here, if sendAll, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount for change estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
amount = allInputsAmount - fee; utxos: utxos,
outputs: outputs,
network: network,
memo: memo,
);
}
int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
if (fee == 0) {
throw BitcoinTransactionNoFeeException();
}
int amount = credentialsAmount;
final lastOutput = outputs.last;
final amountLeftForChange = amountLeftForChangeAndFee - fee;
if (!_isBelowDust(amountLeftForChange)) {
// Here, lastOutput already is change, return the amount left without the fee to the user's address.
outputs[outputs.length - 1] = outputs[outputs.length - 1] =
BitcoinOutput(address: lastOutput.address, value: BigInt.from(amount)); BitcoinOutput(address: lastOutput.address, value: BigInt.from(amountLeftForChange));
} else {
// If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change
outputs.removeLast();
// Still has inputs to spend before failing
if (!spendingAllCoins) {
return estimateTxForAmount(
credentialsAmount,
outputs,
feeRate,
inputsCount: utxos.length + 1,
memo: memo,
);
}
final estimatedSendAll = await estimateSendAllTx(
outputs,
feeRate,
memo: memo,
);
if (estimatedSendAll.amount == credentialsAmount) {
return estimatedSendAll;
}
// Estimate to user how much is needed to send to cover the fee
final maxAmountWithReturningChange = allInputsAmount - _dustAmount - fee - 1;
throw BitcoinTransactionNoDustOnChangeException(
bitcoinAmountToString(amount: maxAmountWithReturningChange),
bitcoinAmountToString(amount: estimatedSendAll.amount),
);
}
// Attempting to send less than the dust limit
if (_isBelowDust(amount)) {
throw BitcoinTransactionNoDustException();
} }
final totalAmount = amount + fee; final totalAmount = amount + fee;
if (totalAmount > balance[currency]!.confirmed) { if (totalAmount > balance[currency]!.confirmed) {
throw BitcoinTransactionWrongBalanceException(currency); throw BitcoinTransactionWrongBalanceException();
} }
if (totalAmount > allInputsAmount) { if (totalAmount > allInputsAmount) {
if (unspentCoins.where((utx) => utx.isSending).length == utxos.length) { if (spendingAllCoins) {
throw BitcoinTransactionWrongBalanceException(currency); throw BitcoinTransactionWrongBalanceException();
} else { } else {
if (changeValue > fee) { if (amountLeftForChangeAndFee > fee) {
outputAddresses.removeLast();
outputs.removeLast(); outputs.removeLast();
} }
return _estimateTxFeeAndInputsToUse( return estimateTxForAmount(
credentialsAmount, sendAll, outputAddresses, outputs, transactionCredentials, credentialsAmount,
inputsCount: utxos.length + 1); outputs,
feeRate,
inputsCount: utxos.length + 1,
memo: memo,
);
} }
} }
return EstimatedTxResult(utxos: utxos, privateKeys: privateKeys, fee: fee, amount: amount); return EstimatedTxResult(
utxos: utxos,
privateKeys: privateKeys,
fee: fee,
amount: amount,
hasChange: true,
isSendAll: false,
memo: memo,
);
} }
@override @override
Future<PendingTransaction> createTransaction(Object credentials) async { Future<PendingTransaction> createTransaction(Object credentials) async {
try { try {
final outputs = <BitcoinOutput>[]; final outputs = <BitcoinOutput>[];
final outputAddresses = <BitcoinBaseAddress>[];
final transactionCredentials = credentials as BitcoinTransactionCredentials; final transactionCredentials = credentials as BitcoinTransactionCredentials;
final hasMultiDestination = transactionCredentials.outputs.length > 1; final hasMultiDestination = transactionCredentials.outputs.length > 1;
final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll; final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll;
final memo = transactionCredentials.outputs.first.memo;
var credentialsAmount = 0; int credentialsAmount = 0;
for (final out in transactionCredentials.outputs) { for (final out in transactionCredentials.outputs) {
final outputAddress = out.isParsedAddress ? out.extractedAddress! : out.address; final outputAmount = out.formattedCryptoAmount!;
final address = _addressTypeFromStr(outputAddress, network);
outputAddresses.add(address); if (!sendAll && _isBelowDust(outputAmount)) {
throw BitcoinTransactionNoDustException();
}
if (hasMultiDestination) { if (hasMultiDestination) {
if (out.sendAll || out.formattedCryptoAmount! <= 0) { if (out.sendAll) {
throw BitcoinTransactionWrongBalanceException(currency); throw BitcoinTransactionWrongBalanceException();
} }
}
final outputAmount = out.formattedCryptoAmount!; credentialsAmount += outputAmount;
credentialsAmount += outputAmount;
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount))); final address =
addressTypeFromStr(out.isParsedAddress ? out.extractedAddress! : out.address, network);
if (sendAll) {
// The value will be changed after estimating the Tx size and deducting the fee from the total to be sent
outputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
} else { } else {
if (!sendAll) { outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
final outputAmount = out.formattedCryptoAmount!;
credentialsAmount += outputAmount;
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
} else {
// The value will be changed after estimating the Tx size and deducting the fee from the total
outputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
}
} }
} }
final estimatedTx = await _estimateTxFeeAndInputsToUse( final feeRateInt = transactionCredentials.feeRate != null
credentialsAmount, sendAll, outputAddresses, outputs, transactionCredentials); ? transactionCredentials.feeRate!
: feeRate(transactionCredentials.priority!);
final txb = BitcoinTransactionBuilder( EstimatedTxResult estimatedTx;
if (sendAll) {
estimatedTx = await estimateSendAllTx(
outputs,
feeRateInt,
memo: memo,
credentialsAmount: credentialsAmount,
);
} else {
estimatedTx = await estimateTxForAmount(
credentialsAmount,
outputs,
feeRateInt,
memo: memo,
);
}
BasedBitcoinTransacationBuilder txb;
if (network is BitcoinCashNetwork) {
txb = ForkedTransactionBuilder(
utxos: estimatedTx.utxos, utxos: estimatedTx.utxos,
outputs: outputs, outputs: outputs,
fee: BigInt.from(estimatedTx.fee), fee: BigInt.from(estimatedTx.fee),
network: network); network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
enableRBF: true,
);
} else {
txb = BitcoinTransactionBuilder(
utxos: estimatedTx.utxos,
outputs: outputs,
fee: BigInt.from(estimatedTx.fee),
network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
enableRBF: true,
);
}
bool hasTaprootInputs = false;
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
final key = estimatedTx.privateKeys final key = estimatedTx.privateKeys
@ -362,18 +556,25 @@ abstract class ElectrumWalletBase
} }
if (utxo.utxo.isP2tr()) { if (utxo.utxo.isP2tr()) {
hasTaprootInputs = true;
return key.signTapRoot(txDigest, sighash: sighash); return key.signTapRoot(txDigest, sighash: sighash);
} else { } else {
return key.signInput(txDigest, sigHash: sighash); return key.signInput(txDigest, sigHash: sighash);
} }
}); });
return PendingBitcoinTransaction(transaction, type, return PendingBitcoinTransaction(
electrumClient: electrumClient, transaction,
amount: estimatedTx.amount, type,
fee: estimatedTx.fee, electrumClient: electrumClient,
network: network) amount: estimatedTx.amount,
..addListener((transaction) async { fee: estimatedTx.fee,
feeRate: feeRateInt.toString(),
network: network,
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: hasTaprootInputs,
)..addListener((transaction) async {
transactionHistory.addOne(transaction); transactionHistory.addOne(transaction);
await updateBalance(); await updateBalance();
}); });
@ -391,7 +592,6 @@ abstract class ElectrumWalletBase
? SegwitAddresType.p2wpkh.toString() ? SegwitAddresType.p2wpkh.toString()
: walletInfo.addressPageType.toString(), : walletInfo.addressPageType.toString(),
'balance': balance[currency]?.toJSON(), 'balance': balance[currency]?.toJSON(),
'network_type': network == BitcoinNetwork.testnet ? 'testnet' : 'mainnet',
}); });
int feeRate(TransactionPriority priority) { int feeRate(TransactionPriority priority) {
@ -406,7 +606,7 @@ abstract class ElectrumWalletBase
} }
} }
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount, int feeAmountForPriority(TransactionPriority priority, int inputsCount, int outputsCount,
{int? size}) => {int? size}) =>
feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount)); feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount));
@ -595,8 +795,180 @@ abstract class ElectrumWalletBase
} }
} }
Future<ElectrumTransactionBundle> getTransactionExpanded( Future<bool> canReplaceByFee(String hash) async {
{required String hash, required int height}) async { final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash);
final confirmations = verboseTransaction['confirmations'] as int? ?? 0;
final transactionHex = verboseTransaction['hex'] as String?;
if (confirmations > 0) return false;
if (transactionHex == null) {
return false;
}
final original = bitcoin.Transaction.fromHex(transactionHex);
return original.ins
.any((element) => element.sequence != null && element.sequence! < 4294967293);
}
Future<bool> isChangeSufficientForFee(String txId, int newFee) async {
final bundle = await getTransactionExpanded(hash: txId);
final outputs = bundle.originalTransaction.outputs;
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
// look for a change address in the outputs
final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any(
(element) => element.address == addressFromOutputScript(output.scriptPubKey, network)));
var allInputsAmount = 0;
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i];
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
allInputsAmount += outTransaction.amount.toInt();
}
int totalOutAmount = bundle.originalTransaction.outputs
.fold<int>(0, (previousValue, element) => previousValue + element.amount.toInt());
var currentFee = allInputsAmount - totalOutAmount;
int remainingFee = (newFee - currentFee > 0) ? newFee - currentFee : newFee;
return changeOutput != null && changeOutput.amount.toInt() - remainingFee >= 0;
}
Future<PendingBitcoinTransaction> replaceByFee(String hash, int newFee) async {
try {
final bundle = await getTransactionExpanded(hash: hash);
final utxos = <UtxoWithAddress>[];
List<ECPrivate> privateKeys = [];
var allInputsAmount = 0;
// Add inputs
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i];
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
allInputsAmount += outTransaction.amount.toInt();
final addressRecord =
walletAddresses.allAddresses.firstWhere((element) => element.address == address);
final btcAddress = addressTypeFromStr(addressRecord.address, network);
final privkey = generateECPrivate(
hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: addressRecord.index,
network: network);
privateKeys.add(privkey);
utxos.add(
UtxoWithAddress(
utxo: BitcoinUtxo(
txHash: input.txId,
value: outTransaction.amount,
vout: vout,
scriptType: _getScriptType(btcAddress),
),
ownerDetails:
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: btcAddress),
),
);
}
int totalOutAmount = bundle.originalTransaction.outputs
.fold<int>(0, (previousValue, element) => previousValue + element.amount.toInt());
var currentFee = allInputsAmount - totalOutAmount;
int remainingFee = newFee - currentFee;
final outputs = <BitcoinOutput>[];
// Add outputs and deduct the fees from it
for (int i = bundle.originalTransaction.outputs.length - 1; i >= 0; i--) {
final out = bundle.originalTransaction.outputs[i];
final address = addressFromOutputScript(out.scriptPubKey, network);
final btcAddress = addressTypeFromStr(address, network);
int newAmount;
if (out.amount.toInt() >= remainingFee) {
newAmount = out.amount.toInt() - remainingFee;
remainingFee = 0;
// if new amount of output is less than dust amount, then don't add this output as well
if (newAmount <= _dustAmount) {
continue;
}
} else {
remainingFee -= out.amount.toInt();
continue;
}
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(newAmount)));
}
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
// look for a change address in the outputs
final changeOutput = outputs.firstWhereOrNull((output) =>
changeAddresses.any((element) => element.address == output.address.toAddress(network)));
// deduct the change amount from the output amount
if (changeOutput != null) {
totalOutAmount -= changeOutput.value.toInt();
}
final txb = BitcoinTransactionBuilder(
utxos: utxos,
outputs: outputs,
fee: BigInt.from(newFee),
network: network,
enableRBF: true,
);
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
final key =
privateKeys.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey);
if (key == null) {
throw Exception("Cannot find private key");
}
if (utxo.utxo.isP2tr()) {
return key.signTapRoot(txDigest, sighash: sighash);
} else {
return key.signInput(txDigest, sigHash: sighash);
}
});
return PendingBitcoinTransaction(
transaction,
type,
electrumClient: electrumClient,
amount: totalOutAmount,
fee: newFee,
network: network,
hasChange: changeOutput != null,
feeRate: newFee.toString(),
)..addListener((transaction) async {
transactionHistory.addOne(transaction);
await updateBalance();
});
} catch (e) {
throw e;
}
}
Future<ElectrumTransactionBundle> getTransactionExpanded({required String hash}) async {
String transactionHex; String transactionHex;
int? time; int? time;
int confirmations = 0; int confirmations = 0;
@ -627,8 +999,12 @@ abstract class ElectrumWalletBase
ins.add(tx); ins.add(tx);
} }
return ElectrumTransactionBundle(original, return ElectrumTransactionBundle(
ins: ins, time: time, confirmations: confirmations, height: height); original,
ins: ins,
time: time,
confirmations: confirmations,
);
} }
Future<ElectrumTransactionInfo?> fetchTransactionInfo( Future<ElectrumTransactionInfo?> fetchTransactionInfo(
@ -638,7 +1014,7 @@ abstract class ElectrumWalletBase
bool? retryOnFailure}) async { bool? retryOnFailure}) async {
try { try {
return ElectrumTransactionInfo.fromElectrumBundle( return ElectrumTransactionInfo.fromElectrumBundle(
await getTransactionExpanded(hash: hash, height: height), walletInfo.type, network, await getTransactionExpanded(hash: hash), walletInfo.type, network,
addresses: myAddresses, height: height); addresses: myAddresses, height: height);
} catch (e) { } catch (e) {
if (e is FormatException && retryOnFailure == true) { if (e is FormatException && retryOnFailure == true) {
@ -852,6 +1228,22 @@ abstract class ElectrumWalletBase
final HD = index == null ? hd : hd.derive(index); final HD = index == null ? hd : hd.derive(index);
return base64Encode(HD.signMessage(message)); return base64Encode(HD.signMessage(message));
} }
static BasedUtxoNetwork _getNetwork(bitcoin.NetworkType networkType, CryptoCurrency? currency) {
if (networkType == bitcoin.bitcoin && currency == CryptoCurrency.bch) {
return BitcoinCashNetwork.mainnet;
}
if (networkType == litecoinNetwork) {
return LitecoinNetwork.mainnet;
}
if (networkType == bitcoin.testnet) {
return BitcoinNetwork.testnet;
}
return BitcoinNetwork.mainnet;
}
} }
class EstimateTxParams { class EstimateTxParams {
@ -870,16 +1262,35 @@ class EstimateTxParams {
} }
class EstimatedTxResult { class EstimatedTxResult {
EstimatedTxResult( EstimatedTxResult({
{required this.utxos, required this.privateKeys, required this.fee, required this.amount}); required this.utxos,
required this.privateKeys,
required this.fee,
required this.amount,
required this.hasChange,
required this.isSendAll,
this.memo,
});
final List<UtxoWithAddress> utxos; final List<UtxoWithAddress> utxos;
final List<ECPrivate> privateKeys; final List<ECPrivate> privateKeys;
final int fee; final int fee;
final int amount; final int amount;
final bool hasChange;
final bool isSendAll;
final String? memo;
} }
BitcoinBaseAddress _addressTypeFromStr(String address, BasedUtxoNetwork network) { BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) {
if (network is BitcoinCashNetwork) {
if (!address.startsWith("bitcoincash:") &&
(address.startsWith("q") || address.startsWith("p"))) {
address = "bitcoincash:$address";
}
return BitcoinCashAddress(address).baseAddress;
}
if (P2pkhAddress.regex.hasMatch(address)) { if (P2pkhAddress.regex.hasMatch(address)) {
return P2pkhAddress.fromAddress(address: address, network: network); return P2pkhAddress.fromAddress(address: address, network: network);
} else if (P2shAddress.regex.hasMatch(address)) { } else if (P2shAddress.regex.hasMatch(address)) {

View file

@ -1,6 +1,5 @@
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum.dart';
import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_addresses.dart';
@ -30,6 +29,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
Map<String, int>? initialRegularAddressIndex, Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex, Map<String, int>? initialChangeAddressIndex,
BitcoinAddressType? initialAddressPageType,
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()), }) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
addressesByReceiveType = addressesByReceiveType =
ObservableList<BitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()), ObservableList<BitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
@ -41,9 +41,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
.toSet()), .toSet()),
currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {}, currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {},
currentChangeAddressIndexByType = initialChangeAddressIndex ?? {}, currentChangeAddressIndexByType = initialChangeAddressIndex ?? {},
_addressPageType = walletInfo.addressPageType != null _addressPageType = initialAddressPageType ??
? BitcoinAddressType.fromValue(walletInfo.addressPageType!) (walletInfo.addressPageType != null
: SegwitAddresType.p2wpkh, ? BitcoinAddressType.fromValue(walletInfo.addressPageType!)
: SegwitAddresType.p2wpkh),
super(walletInfo) { super(walletInfo) {
updateAddressesByMatch(); updateAddressesByMatch();
} }
@ -52,10 +53,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
static const defaultChangeAddressesCount = 17; static const defaultChangeAddressesCount = 17;
static const gap = 20; static const gap = 20;
static String toCashAddr(String address) => bitbox.Address.toCashAddress(address);
static String toLegacy(String address) => bitbox.Address.toLegacyAddress(address);
final ObservableList<BitcoinAddressRecord> _addresses; final ObservableList<BitcoinAddressRecord> _addresses;
// Matched by addressPageType // Matched by addressPageType
late ObservableList<BitcoinAddressRecord> addressesByReceiveType; late ObservableList<BitcoinAddressRecord> addressesByReceiveType;
@ -67,7 +64,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final bitcoin.HDWallet sideHd; final bitcoin.HDWallet sideHd;
@observable @observable
BitcoinAddressType _addressPageType = SegwitAddresType.p2wpkh; late BitcoinAddressType _addressPageType;
@computed @computed
BitcoinAddressType get addressPageType => _addressPageType; BitcoinAddressType get addressPageType => _addressPageType;
@ -80,7 +77,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
String get address { String get address {
String receiveAddress; String receiveAddress;
final typeMatchingReceiveAddresses = receiveAddresses.where(_isAddressPageTypeMatch); final typeMatchingReceiveAddresses =
receiveAddresses.where(_isAddressPageTypeMatch).where((addr) => !addr.isUsed);
if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) || if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) ||
typeMatchingReceiveAddresses.isEmpty) { typeMatchingReceiveAddresses.isEmpty) {
@ -97,7 +95,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
} }
} }
return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(receiveAddress) : receiveAddress; return receiveAddress;
} }
@observable @observable
@ -105,9 +103,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@override @override
set address(String addr) { set address(String addr) {
if (addr.startsWith('bitcoincash:')) {
addr = toLegacy(addr);
}
final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr); final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr);
previousAddressRecord = addressRecord; previousAddressRecord = addressRecord;
@ -155,11 +150,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@override @override
Future<void> init() async { Future<void> init() async {
await _generateInitialAddresses(); if (walletInfo.type == WalletType.bitcoinCash) {
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); } else if (walletInfo.type == WalletType.litecoin) {
await _generateInitialAddresses(type: SegwitAddresType.p2tr); await _generateInitialAddresses();
await _generateInitialAddresses(type: SegwitAddresType.p2wsh); } else if (walletInfo.type == WalletType.bitcoin) {
await _generateInitialAddresses();
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
await _generateInitialAddresses(type: SegwitAddresType.p2tr);
await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
}
updateAddressesByMatch(); updateAddressesByMatch();
updateReceiveAddresses(); updateReceiveAddresses();
updateChangeAddresses(); updateChangeAddresses();
@ -221,6 +222,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
try { try {
addressesMap.clear(); addressesMap.clear();
addressesMap[address] = ''; addressesMap[address] = '';
allAddressesMap.clear();
_addresses.forEach((addressRecord) {
allAddressesMap[addressRecord.address] = addressRecord.name;
});
await saveAddressesInBox(); await saveAddressesInBox();
} catch (e) { } catch (e) {
print(e.toString()); print(e.toString());
@ -229,15 +235,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action @action
void updateAddress(String address, String label) { void updateAddress(String address, String label) {
if (address.startsWith('bitcoincash:')) {
address = toLegacy(address);
}
final addressRecord = final addressRecord =
_addresses.firstWhere((addressRecord) => addressRecord.address == address); _addresses.firstWhere((addressRecord) => addressRecord.address == address);
addressRecord.setNewName(label); addressRecord.setNewName(label);
final index = _addresses.indexOf(addressRecord); final index = _addresses.indexOf(addressRecord);
_addresses.remove(addressRecord); _addresses.remove(addressRecord);
_addresses.insert(index, addressRecord); _addresses.insert(index, addressRecord);
updateAddressesByMatch();
} }
@action @action
@ -261,7 +266,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
addressRecord.isHidden && addressRecord.isHidden &&
!addressRecord.isUsed && !addressRecord.isUsed &&
// TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type // TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type
addressRecord.type == SegwitAddresType.p2wpkh); (walletInfo.type != WalletType.bitcoin || addressRecord.type == SegwitAddresType.p2wpkh));
changeAddresses.addAll(newAddresses); changeAddresses.addAll(newAddresses);
} }

View file

@ -18,7 +18,6 @@ class ElectrumWalletSnapshot {
required this.regularAddressIndex, required this.regularAddressIndex,
required this.changeAddressIndex, required this.changeAddressIndex,
required this.addressPageType, required this.addressPageType,
required this.network,
this.derivationType, this.derivationType,
this.derivationPath, this.derivationPath,
}); });
@ -26,8 +25,7 @@ class ElectrumWalletSnapshot {
final String name; final String name;
final String password; final String password;
final WalletType type; final WalletType type;
final String addressPageType; final String? addressPageType;
final BasedUtxoNetwork network;
String mnemonic; String mnemonic;
List<BitcoinAddressRecord> addresses; List<BitcoinAddressRecord> addresses;
@ -38,7 +36,7 @@ class ElectrumWalletSnapshot {
String? derivationPath; String? derivationPath;
static Future<ElectrumWalletSnapshot> load( static Future<ElectrumWalletSnapshot> load(
String name, WalletType type, String password, BasedUtxoNetwork? network) async { String name, WalletType type, String password, BasedUtxoNetwork network) async {
final path = await pathForWallet(name: name, type: type); final path = await pathForWallet(name: name, type: type);
final jsonSource = await read(path: path, password: password); final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map; final data = json.decode(jsonSource) as Map;
@ -80,8 +78,7 @@ class ElectrumWalletSnapshot {
balance: balance, balance: balance,
regularAddressIndex: regularAddressIndexByType, regularAddressIndex: regularAddressIndexByType,
changeAddressIndex: changeAddressIndexByType, changeAddressIndex: changeAddressIndexByType,
addressPageType: data['address_page_type'] as String? ?? SegwitAddresType.p2wpkh.toString(), addressPageType: data['address_page_type'] as String?,
network: data['network_type'] == 'testnet' ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet,
derivationType: derivationType, derivationType: derivationType,
derivationPath: derivationPath, derivationPath: derivationPath,
); );

View file

@ -0,0 +1,27 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/exceptions.dart';
class BitcoinTransactionWrongBalanceException extends TransactionWrongBalanceException {
BitcoinTransactionWrongBalanceException() : super(CryptoCurrency.btc);
}
class BitcoinTransactionNoInputsException extends TransactionNoInputsException {}
class BitcoinTransactionNoFeeException extends TransactionNoFeeException {}
class BitcoinTransactionNoDustException extends TransactionNoDustException {}
class BitcoinTransactionNoDustOnChangeException extends TransactionNoDustOnChangeException {
BitcoinTransactionNoDustOnChangeException(super.max, super.min);
}
class BitcoinTransactionCommitFailed extends TransactionCommitFailed {}
class BitcoinTransactionCommitFailedDustChange extends TransactionCommitFailedDustChange {}
class BitcoinTransactionCommitFailedDustOutput extends TransactionCommitFailedDustOutput {}
class BitcoinTransactionCommitFailedDustOutputSendAll
extends TransactionCommitFailedDustOutputSendAll {}
class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailedVoutNegative {}

View file

@ -1,4 +1,4 @@
import 'package:cw_bitcoin/bitcoin_commit_transaction_exception.dart'; import 'package:cw_bitcoin/exceptions.dart';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/pending_transaction.dart';
import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum.dart';
@ -8,16 +8,29 @@ import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
class PendingBitcoinTransaction with PendingTransaction { class PendingBitcoinTransaction with PendingTransaction {
PendingBitcoinTransaction(this._tx, this.type, PendingBitcoinTransaction(
{required this.electrumClient, required this.amount, required this.fee, this.network}) this._tx,
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[]; this.type, {
required this.electrumClient,
required this.amount,
required this.fee,
required this.feeRate,
this.network,
required this.hasChange,
this.isSendAll = false,
this.hasTaprootInputs = false,
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type; final WalletType type;
final BtcTransaction _tx; final BtcTransaction _tx;
final ElectrumClient electrumClient; final ElectrumClient electrumClient;
final int amount; final int amount;
final int fee; final int fee;
final String feeRate;
final BasedUtxoNetwork? network; final BasedUtxoNetwork? network;
final bool hasChange;
final bool isSendAll;
final bool hasTaprootInputs;
@override @override
String get id => _tx.txId(); String get id => _tx.txId();
@ -31,14 +44,37 @@ class PendingBitcoinTransaction with PendingTransaction {
@override @override
String get feeFormatted => bitcoinAmountToString(amount: fee); String get feeFormatted => bitcoinAmountToString(amount: fee);
@override
int? get outputCount => _tx.outputs.length;
final List<void Function(ElectrumTransactionInfo transaction)> _listeners; final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
@override @override
Future<void> commit() async { Future<void> commit() async {
final result = await electrumClient.broadcastTransaction(transactionRaw: hex, network: network); int? callId;
final result = await electrumClient.broadcastTransaction(
transactionRaw: hex, network: network, idCallback: (id) => callId = id);
if (result.isEmpty) { if (result.isEmpty) {
throw BitcoinCommitTransactionException(); if (callId != null) {
final error = electrumClient.getErrorMessage(callId!);
if (error.contains("dust")) {
if (hasChange) {
throw BitcoinTransactionCommitFailedDustChange();
} else if (!isSendAll) {
throw BitcoinTransactionCommitFailedDustOutput();
} else {
throw BitcoinTransactionCommitFailedDustOutputSendAll();
}
}
if (error.contains("bad-txns-vout-negative")) {
throw BitcoinTransactionCommitFailedVoutNegative();
}
}
throw BitcoinTransactionCommitFailed();
} }
_listeners.forEach((listener) => listener(transactionInfo())); _listeners.forEach((listener) => listener(transactionInfo()));

View file

@ -1,8 +1,9 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:cw_bitcoin/address_to_output_script.dart';
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin;
String scriptHash(String address, {required BasedUtxoNetwork network}) { String scriptHash(String address, {required bitcoin.BasedUtxoNetwork network}) {
final outputScript = addressToOutputScript(address: address, network: network); final outputScript = addressToOutputScript(address, network);
final parts = sha256.convert(outputScript).toString().split(''); final parts = sha256.convert(outputScript).toString().split('');
var res = ''; var res = '';

View file

@ -70,8 +70,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: master ref: Add-Support-For-OP-Return-data
resolved-ref: ea65073efbaf395a5557e8cd7bd72f195cd7eb11 resolved-ref: "57b78afb85bd2c30d3cdb9f7884f3878a62be442"
url: "https://github.com/cake-tech/bitbox-flutter.git" url: "https://github.com/cake-tech/bitbox-flutter.git"
source: git source: git
version: "1.0.1" version: "1.0.1"
@ -79,11 +79,11 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: cake-update-v1 ref: cake-update-v2
resolved-ref: "9611e9db77e92a8434e918cdfb620068f6fcb1aa" resolved-ref: "3fd81d238b990bb767fc7a4fdd5053a22a142e2e"
url: "https://github.com/cake-tech/bitcoin_base.git" url: "https://github.com/cake-tech/bitcoin_base.git"
source: git source: git
version: "4.0.0" version: "4.2.0"
bitcoin_flutter: bitcoin_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@ -97,10 +97,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: blockchain_utils name: blockchain_utils
sha256: "9701dfaa74caad4daae1785f1ec4445cf7fb94e45620bc3a4aca1b9b281dc6c9" sha256: "38ef5f4a22441ac4370aed9071dc71c460acffc37c79b344533f67d15f24c13c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.0" version: "2.1.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:

View file

@ -26,15 +26,15 @@ dependencies:
bitbox: bitbox:
git: git:
url: https://github.com/cake-tech/bitbox-flutter.git url: https://github.com/cake-tech/bitbox-flutter.git
ref: master ref: Add-Support-For-OP-Return-data
rxdart: ^0.27.5 rxdart: ^0.27.5
unorm_dart: ^0.2.0 unorm_dart: ^0.2.0
cryptography: ^2.0.5 cryptography: ^2.0.5
bitcoin_base: bitcoin_base:
git: git:
url: https://github.com/cake-tech/bitcoin_base.git url: https://github.com/cake-tech/bitcoin_base.git
ref: cake-update-v1 ref: cake-update-v2
blockchain_utils: ^1.6.0 blockchain_utils: ^2.1.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -4,15 +4,10 @@ import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin_cash/src/pending_bitcoin_cash_transaction.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
@ -34,7 +29,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Uint8List seedBytes, required Uint8List seedBytes,
String? addressPageType, BitcoinAddressType? addressPageType,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance, ElectrumBalance? initialBalance,
Map<String, int>? initialRegularAddressIndex, Map<String, int>? initialRegularAddressIndex,
@ -58,6 +53,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
mainHd: hd, mainHd: hd,
sideHd: bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/1"), sideHd: bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/1"),
network: network, network: network,
initialAddressPageType: addressPageType,
); );
autorun((_) { autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
@ -84,7 +80,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
seedBytes: await Mnemonic.toSeed(mnemonic), seedBytes: await Mnemonic.toSeed(mnemonic),
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: addressPageType, addressPageType: P2pkhAddressType.p2pkh,
); );
} }
@ -101,193 +97,37 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp.addresses, initialAddresses: snp.addresses.map((addr) {
try {
BitcoinCashAddress(addr.address);
return BitcoinAddressRecord(
addr.address,
index: addr.index,
isHidden: addr.isHidden,
type: P2pkhAddressType.p2pkh,
network: BitcoinCashNetwork.mainnet,
);
} catch (_) {
return BitcoinAddressRecord(
AddressUtils.getCashAddrFormat(addr.address),
index: addr.index,
isHidden: addr.isHidden,
type: P2pkhAddressType.p2pkh,
network: BitcoinCashNetwork.mainnet,
);
}
}).toList(),
initialBalance: snp.balance, initialBalance: snp.balance,
seedBytes: await Mnemonic.toSeed(snp.mnemonic), seedBytes: await Mnemonic.toSeed(snp.mnemonic),
initialRegularAddressIndex: snp.regularAddressIndex, initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex,
addressPageType: snp.addressPageType, addressPageType: P2pkhAddressType.p2pkh,
); );
} }
@override
Future<PendingBitcoinCashTransaction> createTransaction(Object credentials) async {
const minAmount = 546;
final transactionCredentials = credentials as BitcoinTransactionCredentials;
final inputs = <BitcoinUnspent>[];
final outputs = transactionCredentials.outputs;
final hasMultiDestination = outputs.length > 1;
var allInputsAmount = 0;
if (unspentCoins.isEmpty) await updateUnspent();
for (final utx in unspentCoins) {
if (utx.isSending) {
allInputsAmount += utx.value;
inputs.add(utx);
}
}
if (inputs.isEmpty) throw BitcoinTransactionNoInputsException();
final allAmountFee = transactionCredentials.feeRate != null
? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length)
: feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length);
final allAmount = allInputsAmount - allAmountFee;
var credentialsAmount = 0;
var amount = 0;
var fee = 0;
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) {
throw BitcoinTransactionWrongBalanceException(currency);
}
credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!);
if (allAmount - credentialsAmount < minAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
amount = credentialsAmount;
if (transactionCredentials.feeRate != null) {
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount,
outputsCount: outputs.length + 1);
} else {
fee = calculateEstimatedFee(transactionCredentials.priority, amount,
outputsCount: outputs.length + 1);
}
} else {
final output = outputs.first;
credentialsAmount = !output.sendAll ? output.formattedCryptoAmount! : 0;
if (credentialsAmount > allAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
amount = output.sendAll || allAmount - credentialsAmount < minAmount
? allAmount
: credentialsAmount;
if (output.sendAll || amount == allAmount) {
fee = allAmountFee;
} else if (transactionCredentials.feeRate != null) {
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount);
} else {
fee = calculateEstimatedFee(transactionCredentials.priority, amount);
}
}
if (fee == 0) {
throw BitcoinTransactionWrongBalanceException(currency);
}
final totalAmount = amount + fee;
if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
final txb = bitbox.Bitbox.transactionBuilder(testnet: false);
final changeAddress = await walletAddresses.getChangeAddress();
var leftAmount = totalAmount;
var totalInputAmount = 0;
inputs.clear();
for (final utx in unspentCoins) {
if (utx.isSending) {
leftAmount = leftAmount - utx.value;
totalInputAmount += utx.value;
inputs.add(utx);
if (leftAmount <= 0) {
break;
}
}
}
if (inputs.isEmpty) throw BitcoinTransactionNoInputsException();
if (amount <= 0 || totalInputAmount < totalAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
inputs.forEach((input) {
txb.addInput(input.hash, input.vout);
});
final String bchPrefix = "bitcoincash:";
outputs.forEach((item) {
final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount;
String outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address;
if (!outputAddress.startsWith(bchPrefix)) {
outputAddress = "$bchPrefix$outputAddress";
}
bool isP2sh = outputAddress.startsWith("p", bchPrefix.length);
if (isP2sh) {
final p2sh = P2shAddress.fromAddress(
address: outputAddress,
network: BitcoinCashNetwork.mainnet,
);
txb.addOutput(Uint8List.fromList(p2sh.toScriptPubKey().toBytes()), outputAmount!);
return;
}
txb.addOutput(outputAddress, outputAmount!);
});
final estimatedSize = bitbox.BitcoinCash.getByteCount(inputs.length, outputs.length + 1);
var feeAmount = 0;
if (transactionCredentials.feeRate != null) {
feeAmount = transactionCredentials.feeRate! * estimatedSize;
} else {
feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize;
}
final changeValue = totalInputAmount - amount - feeAmount;
if (changeValue > minAmount) {
txb.addOutput(changeAddress, changeValue);
}
for (var i = 0; i < inputs.length; i++) {
final input = inputs[i];
final keyPair = generateKeyPair(
hd: input.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: input.bitcoinAddressRecord.index);
txb.sign(i, keyPair, input.value);
}
// Build the transaction
final tx = txb.build();
return PendingBitcoinCashTransaction(tx, type,
electrumClient: electrumClient, amount: amount, fee: fee);
}
bitbox.ECPair generateKeyPair({required bitcoin.HDWallet hd, required int index}) => bitbox.ECPair generateKeyPair({required bitcoin.HDWallet hd, required int index}) =>
bitbox.ECPair.fromWIF(hd.derive(index).wif!); bitbox.ECPair.fromWIF(hd.derive(index).wif!);
@override
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount,
{int? size}) =>
feeRate(priority) * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount);
int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount, {int? size}) =>
feeRate * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount);
int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) { int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) {
int inputsCount = 0; int inputsCount = 0;
int totalValue = 0; int totalValue = 0;

View file

@ -19,6 +19,7 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi
super.initialAddresses, super.initialAddresses,
super.initialRegularAddressIndex, super.initialRegularAddressIndex,
super.initialChangeAddressIndex, super.initialChangeAddressIndex,
super.initialAddressPageType,
}) : super(walletInfo); }) : super(walletInfo);
@override @override

View file

@ -1,4 +1,4 @@
import 'package:cw_bitcoin/bitcoin_commit_transaction_exception.dart'; import 'package:cw_bitcoin/exceptions.dart';
import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/pending_transaction.dart';
import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum.dart';
@ -11,7 +11,9 @@ class PendingBitcoinCashTransaction with PendingTransaction {
PendingBitcoinCashTransaction(this._tx, this.type, PendingBitcoinCashTransaction(this._tx, this.type,
{required this.electrumClient, {required this.electrumClient,
required this.amount, required this.amount,
required this.fee}) required this.fee,
required this.hasChange,
required this.isSendAll})
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[]; : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type; final WalletType type;
@ -19,6 +21,8 @@ class PendingBitcoinCashTransaction with PendingTransaction {
final ElectrumClient electrumClient; final ElectrumClient electrumClient;
final int amount; final int amount;
final int fee; final int fee;
final bool hasChange;
final bool isSendAll;
@override @override
String get id => _tx.getId(); String get id => _tx.getId();
@ -36,18 +40,36 @@ class PendingBitcoinCashTransaction with PendingTransaction {
@override @override
Future<void> commit() async { Future<void> commit() async {
final result = int? callId;
await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex());
final result = await electrumClient.broadcastTransaction(
transactionRaw: hex, idCallback: (id) => callId = id);
if (result.isEmpty) { if (result.isEmpty) {
throw BitcoinCommitTransactionException(); if (callId != null) {
final error = electrumClient.getErrorMessage(callId!);
if (error.contains("dust")) {
if (hasChange) {
throw BitcoinTransactionCommitFailedDustChange();
} else if (!isSendAll) {
throw BitcoinTransactionCommitFailedDustOutput();
} else {
throw BitcoinTransactionCommitFailedDustOutputSendAll();
}
}
if (error.contains("bad-txns-vout-negative")) {
throw BitcoinTransactionCommitFailedVoutNegative();
}
}
throw BitcoinTransactionCommitFailed();
} }
_listeners?.forEach((listener) => listener(transactionInfo())); _listeners.forEach((listener) => listener(transactionInfo()));
} }
void addListener( void addListener(void Function(ElectrumTransactionInfo transaction) listener) =>
void Function(ElectrumTransactionInfo transaction) listener) =>
_listeners.add(listener); _listeners.add(listener);
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,

View file

@ -28,11 +28,11 @@ dependencies:
bitbox: bitbox:
git: git:
url: https://github.com/cake-tech/bitbox-flutter.git url: https://github.com/cake-tech/bitbox-flutter.git
ref: master ref: Add-Support-For-OP-Return-data
bitcoin_base: bitcoin_base:
git: git:
url: https://github.com/cake-tech/bitcoin_base.git url: https://github.com/cake-tech/bitcoin_base.git
ref: cake-update-v1 ref: cake-update-v2

View file

@ -38,6 +38,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.trx, CryptoCurrency.trx,
CryptoCurrency.usdt, CryptoCurrency.usdt,
CryptoCurrency.usdterc20, CryptoCurrency.usdterc20,
CryptoCurrency.sol,
CryptoCurrency.maticpoly,
CryptoCurrency.xlm, CryptoCurrency.xlm,
CryptoCurrency.xrp, CryptoCurrency.xrp,
CryptoCurrency.xhv, CryptoCurrency.xhv,
@ -50,7 +52,6 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.usdttrc20, CryptoCurrency.usdttrc20,
CryptoCurrency.hbar, CryptoCurrency.hbar,
CryptoCurrency.sc, CryptoCurrency.sc,
CryptoCurrency.sol,
CryptoCurrency.usdc, CryptoCurrency.usdc,
CryptoCurrency.usdcsol, CryptoCurrency.usdcsol,
CryptoCurrency.zaddr, CryptoCurrency.zaddr,
@ -61,7 +62,6 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.dcr, CryptoCurrency.dcr,
CryptoCurrency.kmd, CryptoCurrency.kmd,
CryptoCurrency.mana, CryptoCurrency.mana,
CryptoCurrency.maticpoly,
CryptoCurrency.matic, CryptoCurrency.matic,
CryptoCurrency.mkr, CryptoCurrency.mkr,
CryptoCurrency.near, CryptoCurrency.near,

View file

@ -0,0 +1,30 @@
import 'package:cw_core/crypto_currency.dart';
class TransactionWrongBalanceException implements Exception {
TransactionWrongBalanceException(this.currency);
final CryptoCurrency currency;
}
class TransactionNoInputsException implements Exception {}
class TransactionNoFeeException implements Exception {}
class TransactionNoDustException implements Exception {}
class TransactionNoDustOnChangeException implements Exception {
TransactionNoDustOnChangeException(this.max, this.min);
final String max;
final String min;
}
class TransactionCommitFailed implements Exception {}
class TransactionCommitFailedDustChange implements Exception {}
class TransactionCommitFailedDustOutput implements Exception {}
class TransactionCommitFailedDustOutputSendAll implements Exception {}
class TransactionCommitFailedVoutNegative implements Exception {}

31
cw_core/lib/n2_node.dart Normal file
View file

@ -0,0 +1,31 @@
class N2Node {
N2Node({
this.weight,
this.uptime,
this.score,
this.account,
this.alias,
});
String? uptime;
double? weight;
int? score;
String? account;
String? alias;
factory N2Node.fromJson(Map<String, dynamic> json) => N2Node(
weight: double.tryParse((json['weight'] as num?).toString()),
uptime: json['uptime'] as String?,
score: json['score'] as int?,
account: json['rep_address'] as String?,
alias: json['alias'] as String?,
);
Map<String, dynamic> toJson() => <String, dynamic>{
'uptime': uptime,
'weight': weight,
'score': score,
'rep_address': account,
'alias': alias,
};
}

View file

@ -21,6 +21,7 @@ class Node extends HiveObject with Keyable {
this.trusted = false, this.trusted = false,
this.socksProxyAddress, this.socksProxyAddress,
String? uri, String? uri,
String? path,
WalletType? type, WalletType? type,
}) { }) {
if (uri != null) { if (uri != null) {
@ -29,10 +30,14 @@ class Node extends HiveObject with Keyable {
if (type != null) { if (type != null) {
this.type = type; this.type = type;
} }
if (path != null) {
this.path = path;
}
} }
Node.fromMap(Map<String, Object?> map) Node.fromMap(Map<String, Object?> map)
: uriRaw = map['uri'] as String? ?? '', : uriRaw = map['uri'] as String? ?? '',
path = map['path'] as String? ?? '',
login = map['login'] as String?, login = map['login'] as String?,
password = map['password'] as String?, password = map['password'] as String?,
useSSL = map['useSSL'] as bool?, useSSL = map['useSSL'] as bool?,
@ -63,6 +68,9 @@ class Node extends HiveObject with Keyable {
@HiveField(6) @HiveField(6)
String? socksProxyAddress; String? socksProxyAddress;
@HiveField(7, defaultValue: '')
String? path;
bool get isSSL => useSSL ?? false; bool get isSSL => useSSL ?? false;
bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty; bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty;
@ -79,9 +87,9 @@ class Node extends HiveObject with Keyable {
case WalletType.nano: case WalletType.nano:
case WalletType.banano: case WalletType.banano:
if (isSSL) { if (isSSL) {
return Uri.https(uriRaw, ''); return Uri.https(uriRaw, path ?? '');
} else { } else {
return Uri.http(uriRaw, ''); return Uri.http(uriRaw, path ?? '');
} }
case WalletType.ethereum: case WalletType.ethereum:
case WalletType.polygon: case WalletType.polygon:
@ -103,7 +111,8 @@ class Node extends HiveObject with Keyable {
other.typeRaw == typeRaw && other.typeRaw == typeRaw &&
other.useSSL == useSSL && other.useSSL == useSSL &&
other.trusted == trusted && other.trusted == trusted &&
other.socksProxyAddress == socksProxyAddress); other.socksProxyAddress == socksProxyAddress &&
other.path == path);
@override @override
int get hashCode => int get hashCode =>
@ -113,7 +122,8 @@ class Node extends HiveObject with Keyable {
typeRaw.hashCode ^ typeRaw.hashCode ^
useSSL.hashCode ^ useSSL.hashCode ^
trusted.hashCode ^ trusted.hashCode ^
socksProxyAddress.hashCode; socksProxyAddress.hashCode ^
path.hashCode;
@override @override
dynamic get keyIndex { dynamic get keyIndex {

View file

@ -7,7 +7,8 @@ class OutputInfo {
this.formattedCryptoAmount, this.formattedCryptoAmount,
this.fiatAmount, this.fiatAmount,
this.note, this.note,
this.extractedAddress,}); this.extractedAddress,
this.memo});
final String? fiatAmount; final String? fiatAmount;
final String? cryptoAmount; final String? cryptoAmount;
@ -17,4 +18,5 @@ class OutputInfo {
final bool sendAll; final bool sendAll;
final bool isParsedAddress; final bool isParsedAddress;
final int? formattedCryptoAmount; final int? formattedCryptoAmount;
final String? memo;
} }

View file

@ -2,7 +2,9 @@ mixin PendingTransaction {
String get id; String get id;
String get amountFormatted; String get amountFormatted;
String get feeFormatted; String get feeFormatted;
String? feeRate;
String get hex; String get hex;
int? get outputCount => null;
Future<void> commit(); Future<void> commit();
} }

View file

@ -16,6 +16,8 @@ abstract class TransactionInfo extends Object with Keyable {
void changeFiatAmount(String amount); void changeFiatAmount(String amount);
String? to; String? to;
String? from; String? from;
List<String>? inputAddresses;
List<String>? outputAddresses;
@override @override
dynamic get keyIndex => id; dynamic get keyIndex => id;

View file

@ -3,8 +3,9 @@ import 'package:cw_core/wallet_info.dart';
abstract class WalletAddresses { abstract class WalletAddresses {
WalletAddresses(this.walletInfo) WalletAddresses(this.walletInfo)
: addressesMap = {}, : addressesMap = {},
addressInfos = {}; allAddressesMap = {},
addressInfos = {};
final WalletInfo walletInfo; final WalletInfo walletInfo;
@ -15,6 +16,7 @@ abstract class WalletAddresses {
set address(String address); set address(String address);
Map<String, String> addressesMap; Map<String, String> addressesMap;
Map<String, String> allAddressesMap;
Map<int, List<AddressInfo>> addressInfos; Map<int, List<AddressInfo>> addressInfos;
@ -39,5 +41,6 @@ abstract class WalletAddresses {
} }
} }
bool containsAddress(String address) => addressesMap.containsKey(address); bool containsAddress(String address) =>
addressesMap.containsKey(address) || allAddressesMap.containsKey(address);
} }

View file

@ -67,6 +67,7 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
int calculateEstimatedFee(TransactionPriority priority, int? amount); int calculateEstimatedFee(TransactionPriority priority, int? amount);
// void fetchTransactionsAsync( // void fetchTransactionsAsync(
// void Function(TransactionType transaction) onTransactionLoaded, // void Function(TransactionType transaction) onTransactionLoaded,
// {void Function() onFinished}); // {void Function() onFinished});

View file

@ -41,4 +41,29 @@ class EthereumClient extends EVMChainClient {
return []; return [];
} }
} }
@override
Future<List<EVMChainTransactionModel>> fetchInternalTransactions(String address) async {
try {
final response = await httpClient.get(Uri.https("api.etherscan.io", "/api", {
"module": "account",
"action": "txlistinternal",
"address": address,
"apikey": secrets.etherScanApiKey,
}));
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) {
return (jsonResponse['result'] as List)
.map((e) => EVMChainTransactionModel.fromJson(e as Map<String, dynamic>, 'ETH'))
.toList();
}
return [];
} catch (e) {
log(e.toString());
return [];
}
}
} }

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'package:cw_core/node.dart'; import 'package:cw_core/node.dart';
@ -9,11 +10,13 @@ import 'package:cw_evm/evm_erc20_balance.dart';
import 'package:cw_evm/evm_chain_transaction_model.dart'; import 'package:cw_evm/evm_chain_transaction_model.dart';
import 'package:cw_evm/pending_evm_chain_transaction.dart'; import 'package:cw_evm/pending_evm_chain_transaction.dart';
import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart';
import 'package:cw_evm/.secrets.g.dart' as secrets;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:erc20/erc20.dart'; import 'package:erc20/erc20.dart';
import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart';
import 'package:hex/hex.dart' as hex;
abstract class EVMChainClient { abstract class EVMChainClient {
final httpClient = Client(); final httpClient = Client();
@ -26,6 +29,8 @@ abstract class EVMChainClient {
Future<List<EVMChainTransactionModel>> fetchTransactions(String address, Future<List<EVMChainTransactionModel>> fetchTransactions(String address,
{String? contractAddress}); {String? contractAddress});
Future<List<EVMChainTransactionModel>> fetchInternalTransactions(String address);
Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction); Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction);
//! Common methods across all child classes //! Common methods across all child classes
@ -79,12 +84,13 @@ abstract class EVMChainClient {
Future<PendingEVMChainTransaction> signTransaction({ Future<PendingEVMChainTransaction> signTransaction({
required EthPrivateKey privateKey, required EthPrivateKey privateKey,
required String toAddress, required String toAddress,
required String amount, required BigInt amount,
required int gas, required int gas,
required EVMChainTransactionPriority priority, required EVMChainTransactionPriority priority,
required CryptoCurrency currency, required CryptoCurrency currency,
required int exponent, required int exponent,
String? contractAddress, String? contractAddress,
String? data,
}) async { }) async {
assert(currency == CryptoCurrency.eth || assert(currency == CryptoCurrency.eth ||
currency == CryptoCurrency.maticpoly || currency == CryptoCurrency.maticpoly ||
@ -99,7 +105,8 @@ abstract class EVMChainClient {
from: privateKey.address, from: privateKey.address,
to: EthereumAddress.fromHex(toAddress), to: EthereumAddress.fromHex(toAddress),
maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
amount: isEVMCompatibleChain ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(), amount: isEVMCompatibleChain ? EtherAmount.inWei(amount) : EtherAmount.zero(),
data: data != null ? hexToBytes(data) : null,
); );
final signedTransaction = final signedTransaction =
@ -119,7 +126,7 @@ abstract class EVMChainClient {
_sendTransaction = () async { _sendTransaction = () async {
await erc20.transfer( await erc20.transfer(
EthereumAddress.fromHex(toAddress), EthereumAddress.fromHex(toAddress),
BigInt.parse(amount), amount,
credentials: privateKey, credentials: privateKey,
transaction: transaction, transaction: transaction,
); );
@ -128,7 +135,7 @@ abstract class EVMChainClient {
return PendingEVMChainTransaction( return PendingEVMChainTransaction(
signedTransaction: signedTransaction, signedTransaction: signedTransaction,
amount: amount, amount: amount.toString(),
fee: BigInt.from(gas) * (await price).getInWei, fee: BigInt.from(gas) * (await price).getInWei,
sendTransaction: _sendTransaction, sendTransaction: _sendTransaction,
exponent: exponent, exponent: exponent,
@ -140,12 +147,14 @@ abstract class EVMChainClient {
required EthereumAddress to, required EthereumAddress to,
required EtherAmount amount, required EtherAmount amount,
EtherAmount? maxPriorityFeePerGas, EtherAmount? maxPriorityFeePerGas,
Uint8List? data,
}) { }) {
return Transaction( return Transaction(
from: from, from: from,
to: to, to: to,
maxPriorityFeePerGas: maxPriorityFeePerGas, maxPriorityFeePerGas: maxPriorityFeePerGas,
value: amount, value: amount,
data: data,
); );
} }
@ -204,24 +213,63 @@ abstract class EVMChainClient {
return EVMChainERC20Balance(balance, exponent: exponent); return EVMChainERC20Balance(balance, exponent: exponent);
} }
Future<Erc20Token?> getErc20Token(String contractAddress) async { Future<Erc20Token?> getErc20Token(String contractAddress, String chainName) async {
try { try {
final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); final uri = Uri.https(
final name = await erc20.name(); 'deep-index.moralis.io',
final symbol = await erc20.symbol(); '/api/v2.2/erc20/metadata',
final decimal = await erc20.decimals(); {
"chain": chainName,
"addresses": contractAddress,
},
);
final response = await httpClient.get(
uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
final decodedResponse = jsonDecode(response.body)[0] as Map<String, dynamic>;
final name = decodedResponse['name'] ?? '';
final symbol = decodedResponse['symbol'] ?? '';
final decimal = decodedResponse['decimals'] ?? '0';
final iconPath = decodedResponse['logo'] ?? '';
return Erc20Token( return Erc20Token(
name: name, name: name,
symbol: symbol, symbol: symbol,
contractAddress: contractAddress, contractAddress: contractAddress,
decimal: decimal.toInt(), decimal: int.tryParse(decimal) ?? 0,
iconPath: iconPath,
); );
} catch (e) { } catch (e) {
try {
final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
final name = await erc20.name();
final symbol = await erc20.symbol();
final decimal = await erc20.decimals();
return Erc20Token(
name: name,
symbol: symbol,
contractAddress: contractAddress,
decimal: decimal.toInt(),
);
} catch (_) {}
return null; return null;
} }
} }
Uint8List hexToBytes(String hexString) {
return Uint8List.fromList(
hex.HEX.decode(hexString.startsWith('0x') ? hexString.substring(2) : hexString));
}
void stop() { void stop() {
_client?.dispose(); _client?.dispose();
} }

View file

@ -9,3 +9,14 @@ class EVMChainTransactionCreationException implements Exception {
@override @override
String toString() => exceptionMessage; String toString() => exceptionMessage;
} }
class EVMChainTransactionFeesException implements Exception {
final String exceptionMessage;
EVMChainTransactionFeesException()
: exceptionMessage = 'Current balance is less than the estimated fees for this transaction.';
@override
String toString() => exceptionMessage;
}

View file

@ -32,15 +32,15 @@ class EVMChainTransactionModel {
factory EVMChainTransactionModel.fromJson(Map<String, dynamic> json, String defaultSymbol) => factory EVMChainTransactionModel.fromJson(Map<String, dynamic> json, String defaultSymbol) =>
EVMChainTransactionModel( EVMChainTransactionModel(
date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000), date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000),
hash: json["hash"], hash: json["hash"] ?? "",
from: json["from"], from: json["from"] ?? "",
to: json["to"], to: json["to"] ?? "",
amount: BigInt.parse(json["value"]), amount: BigInt.parse(json["value"] ?? "0"),
gasUsed: int.parse(json["gasUsed"]), gasUsed: int.parse(json["gasUsed"] ?? "0"),
gasPrice: BigInt.parse(json["gasPrice"]), gasPrice: BigInt.parse(json["gasPrice"] ?? "0"),
contractAddress: json["contractAddress"], contractAddress: json["contractAddress"] ?? "",
confirmations: int.parse(json["confirmations"]), confirmations: int.parse(json["confirmations"] ?? "0"),
blockNumber: int.parse(json["blockNumber"]), blockNumber: int.parse(json["blockNumber"] ?? "0"),
tokenSymbol: json["tokenSymbol"] ?? defaultSymbol, tokenSymbol: json["tokenSymbol"] ?? defaultSymbol,
tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""), tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""),
isError: json["isError"] == "1", isError: json["isError"] == "1",

View file

@ -224,10 +224,17 @@ abstract class EVMChainWalletBase
final outputs = _credentials.outputs; final outputs = _credentials.outputs;
final hasMultiDestination = outputs.length > 1; final hasMultiDestination = outputs.length > 1;
final String? opReturnMemo = outputs.first.memo;
String? hexOpReturnMemo;
if (opReturnMemo != null) {
hexOpReturnMemo = '0x${opReturnMemo.codeUnits.map((char) => char.toRadixString(16).padLeft(2, '0')).join()}';
}
final CryptoCurrency transactionCurrency = final CryptoCurrency transactionCurrency =
balance.keys.firstWhere((element) => element.title == _credentials.currency.title); balance.keys.firstWhere((element) => element.title == _credentials.currency.title);
final _erc20Balance = balance[transactionCurrency]!; final erc20Balance = balance[transactionCurrency]!;
BigInt totalAmount = BigInt.zero; BigInt totalAmount = BigInt.zero;
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18; int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
num amountToEVMChainMultiplier = pow(10, exponent); num amountToEVMChainMultiplier = pow(10, exponent);
@ -242,7 +249,7 @@ abstract class EVMChainWalletBase
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0))); outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier); totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
if (_erc20Balance.balance < totalAmount) { if (erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency); throw EVMChainTransactionCreationException(transactionCurrency);
} }
} else { } else {
@ -251,18 +258,27 @@ abstract class EVMChainWalletBase
// then no need to subtract the fees from the amount if send all // then no need to subtract the fees from the amount if send all
final BigInt allAmount; final BigInt allAmount;
if (transactionCurrency is Erc20Token) { if (transactionCurrency is Erc20Token) {
allAmount = _erc20Balance.balance; allAmount = erc20Balance.balance;
} else { } else {
allAmount = _erc20Balance.balance - final estimatedFee = BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
}
final totalOriginalAmount =
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
totalAmount = output.sendAll
? allAmount
: BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
if (_erc20Balance.balance < totalAmount) { if (estimatedFee > erc20Balance.balance) {
throw EVMChainTransactionFeesException();
}
allAmount = erc20Balance.balance - estimatedFee;
}
if (output.sendAll) {
totalAmount = allAmount;
} else {
final totalOriginalAmount =
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
}
if (erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency); throw EVMChainTransactionCreationException(transactionCurrency);
} }
} }
@ -272,13 +288,14 @@ abstract class EVMChainWalletBase
toAddress: _credentials.outputs.first.isParsedAddress toAddress: _credentials.outputs.first.isParsedAddress
? _credentials.outputs.first.extractedAddress! ? _credentials.outputs.first.extractedAddress!
: _credentials.outputs.first.address, : _credentials.outputs.first.address,
amount: totalAmount.toString(), amount: totalAmount,
gas: _estimatedGas!, gas: _estimatedGas!,
priority: _credentials.priority!, priority: _credentials.priority!,
currency: transactionCurrency, currency: transactionCurrency,
exponent: exponent, exponent: exponent,
contractAddress: contractAddress:
transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null, transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null,
data: hexOpReturnMemo,
); );
return pendingEVMChainTransaction; return pendingEVMChainTransaction;
@ -310,6 +327,7 @@ abstract class EVMChainWalletBase
Future<Map<String, EVMChainTransactionInfo>> fetchTransactions() async { Future<Map<String, EVMChainTransactionInfo>> fetchTransactions() async {
final address = _evmChainPrivateKey.address.hex; final address = _evmChainPrivateKey.address.hex;
final transactions = await _client.fetchTransactions(address); final transactions = await _client.fetchTransactions(address);
final internalTransactions = await _client.fetchInternalTransactions(address);
final List<Future<List<EVMChainTransactionModel>>> erc20TokensTransactions = []; final List<Future<List<EVMChainTransactionModel>>> erc20TokensTransactions = [];
@ -324,6 +342,7 @@ abstract class EVMChainWalletBase
final tokensTransaction = await Future.wait(erc20TokensTransactions); final tokensTransaction = await Future.wait(erc20TokensTransactions);
transactions.addAll(tokensTransaction.expand((element) => element)); transactions.addAll(tokensTransaction.expand((element) => element));
transactions.addAll(internalTransactions);
final Map<String, EVMChainTransactionInfo> result = {}; final Map<String, EVMChainTransactionInfo> result = {};
@ -420,11 +439,16 @@ abstract class EVMChainWalletBase
Future<void> addErc20Token(Erc20Token token) async { Future<void> addErc20Token(Erc20Token token) async {
String? iconPath; String? iconPath;
try {
iconPath = CryptoCurrency.all if (token.iconPath == null || token.iconPath!.isEmpty) {
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) try {
.iconPath; iconPath = CryptoCurrency.all
} catch (_) {} .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
.iconPath;
} catch (_) {}
} else {
iconPath = token.iconPath;
}
final newToken = createNewErc20TokenObject(token, iconPath); final newToken = createNewErc20TokenObject(token, iconPath);
@ -447,8 +471,8 @@ abstract class EVMChainWalletBase
_updateBalance(); _updateBalance();
} }
Future<Erc20Token?> getErc20Token(String contractAddress) async => Future<Erc20Token?> getErc20Token(String contractAddress, String chainName) async =>
await _client.getErc20Token(contractAddress); await _client.getErc20Token(contractAddress, chainName);
void _onNewTransaction() { void _onNewTransaction() {
_updateBalance(); _updateBalance();
@ -484,7 +508,7 @@ abstract class EVMChainWalletBase
_transactionsUpdateTimer!.cancel(); _transactionsUpdateTimer!.cancel();
} }
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) { _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 15), (_) {
_updateTransactions(); _updateTransactions();
_updateBalance(); _updateBalance();
}); });

View file

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/pending_transaction.dart';
import 'package:web3dart/crypto.dart'; import 'package:web3dart/crypto.dart';
import 'package:hex/hex.dart' as Hex;
class PendingEVMChainTransaction with PendingTransaction { class PendingEVMChainTransaction with PendingTransaction {
final Function sendTransaction; final Function sendTransaction;
@ -38,5 +39,12 @@ class PendingEVMChainTransaction with PendingTransaction {
String get hex => bytesToHex(signedTransaction, include0x: true); String get hex => bytesToHex(signedTransaction, include0x: true);
@override @override
String get id => ''; String get id {
final String eip1559Hex = '0x02${hex.substring(2)}';
final Uint8List bytes = Uint8List.fromList(Hex.HEX.decode(eip1559Hex.substring(2)));
var txid = keccak256(bytes);
return '0x${Hex.HEX.encode(txid)}';
}
} }

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:cw_core/nano_account_info_response.dart'; import 'package:cw_core/nano_account_info_response.dart';
import 'package:cw_core/n2_node.dart';
import 'package:cw_nano/nano_balance.dart'; import 'package:cw_nano/nano_balance.dart';
import 'package:cw_nano/nano_transaction_model.dart'; import 'package:cw_nano/nano_transaction_model.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -16,6 +17,8 @@ class NanoClient {
"nano-app": "cake-wallet" "nano-app": "cake-wallet"
}; };
static const String N2_REPS_ENDPOINT = "https://rpc.nano.to";
NanoClient() { NanoClient() {
SharedPreferences.getInstance().then((value) => prefs = value); SharedPreferences.getInstance().then((value) => prefs = value);
} }
@ -418,7 +421,7 @@ class NanoClient {
body: jsonEncode({ body: jsonEncode({
"action": "account_history", "action": "account_history",
"account": address, "account": address,
"count": "250", // TODO: pick a number "count": "100",
// "raw": true, // "raw": true,
})); }));
final data = await jsonDecode(response.body); final data = await jsonDecode(response.body);
@ -434,4 +437,37 @@ class NanoClient {
return []; return [];
} }
} }
Future<List<N2Node>> getN2Reps() async {
final response = await http.post(
Uri.parse(N2_REPS_ENDPOINT),
headers: CAKE_HEADERS,
body: jsonEncode({"action": "reps"}),
);
try {
final List<N2Node> nodes = (json.decode(response.body) as List<dynamic>)
.map((dynamic e) => N2Node.fromJson(e as Map<String, dynamic>))
.toList();
return nodes;
} catch (error) {
return [];
}
}
Future<int> getRepScore(String rep) async {
final response = await http.post(
Uri.parse(N2_REPS_ENDPOINT),
headers: CAKE_HEADERS,
body: jsonEncode({
"action": "rep_info",
"account": rep,
}),
);
try {
final N2Node node = N2Node.fromJson(json.decode(response.body) as Map<String, dynamic>);
return node.score ?? 100;
} catch (error) {
return 100;
}
}
} }

View file

@ -13,6 +13,7 @@ import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_nano/file.dart'; import 'package:cw_nano/file.dart';
import 'package:cw_core/nano_account.dart'; import 'package:cw_core/nano_account.dart';
import 'package:cw_core/n2_node.dart';
import 'package:cw_nano/nano_balance.dart'; import 'package:cw_nano/nano_balance.dart';
import 'package:cw_nano/nano_client.dart'; import 'package:cw_nano/nano_client.dart';
import 'package:cw_nano/nano_transaction_credentials.dart'; import 'package:cw_nano/nano_transaction_credentials.dart';
@ -65,9 +66,11 @@ abstract class NanoWalletBase
String? _privateKey; String? _privateKey;
String? _publicAddress; String? _publicAddress;
String? _hexSeed; String? _hexSeed;
Timer? _receiveTimer;
String? _representativeAddress; String? _representativeAddress;
Timer? _receiveTimer; int repScore = 100;
bool get isRepOk => repScore >= 90;
late final NanoClient _client; late final NanoClient _client;
bool _isTransactionUpdating; bool _isTransactionUpdating;
@ -375,7 +378,7 @@ abstract class NanoWalletBase
final data = json.decode(jsonSource) as Map; final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String; final mnemonic = data['mnemonic'] as String;
final balance = NanoBalance.fromRawString( final balance = NanoBalance.fromRawString(
currentBalance: data['currentBalance'] as String? ?? "0", currentBalance: data['currentBalance'] as String? ?? "0",
receivableBalance: data['receivableBalance'] as String? ?? "0", receivableBalance: data['receivableBalance'] as String? ?? "0",
@ -432,6 +435,8 @@ abstract class NanoWalletBase
_representativeAddress = await _client.getRepFromPrefs(); _representativeAddress = await _client.getRepFromPrefs();
throw Exception("Failed to get representative address $e"); throw Exception("Failed to get representative address $e");
} }
repScore = await _client.getRepScore(_representativeAddress!);
} }
Future<void> regenerateAddress() async { Future<void> regenerateAddress() async {
@ -468,6 +473,10 @@ abstract class NanoWalletBase
} }
} }
Future<List<N2Node>> getN2Reps() async {
return _client.getN2Reps();
}
Future<void>? updateBalance() async => await _updateBalance(); Future<void>? updateBalance() async => await _updateBalance();
@override @override

View file

@ -13,6 +13,7 @@ class PolygonClient extends EVMChainClient {
required EthereumAddress to, required EthereumAddress to,
required EtherAmount amount, required EtherAmount amount,
EtherAmount? maxPriorityFeePerGas, EtherAmount? maxPriorityFeePerGas,
Uint8List? data,
}) { }) {
return Transaction( return Transaction(
from: from, from: from,
@ -54,4 +55,28 @@ class PolygonClient extends EVMChainClient {
return []; return [];
} }
} }
@override
Future<List<EVMChainTransactionModel>> fetchInternalTransactions(String address) async {
try {
final response = await httpClient.get(Uri.https("api.polygonscan.io", "/api", {
"module": "account",
"action": "txlistinternal",
"address": address,
"apikey": secrets.polygonScanApiKey,
}));
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) {
return (jsonResponse['result'] as List)
.map((e) => EVMChainTransactionModel.fromJson(e as Map<String, dynamic>, 'MATIC'))
.toList();
}
return [];
} catch (_) {
return [];
}
}
} }

View file

@ -96,16 +96,30 @@ class SolanaWalletClient {
return SolanaBalance(totalBalance); return SolanaBalance(totalBalance);
} }
Future<double> getGasForMessage(String message) async { Future<double> getFeeForMessage(String message, Commitment commitment) async {
try { try {
final gasPrice = await _client!.rpcClient.getFeeForMessage(message) ?? 0; final feeForMessage =
final fee = gasPrice / lamportsPerSol; await _client!.rpcClient.getFeeForMessage(message, commitment: commitment);
final fee = (feeForMessage ?? 0.0) / lamportsPerSol;
return fee; return fee;
} catch (_) { } catch (_) {
return 0; return 0.0;
} }
} }
Future<double> getEstimatedFee(Ed25519HDKeyPair ownerKeypair) async {
const commitment = Commitment.confirmed;
final message =
_getMessageForNativeTransaction(ownerKeypair, ownerKeypair.address, lamportsPerSol);
final recentBlockhash = await _getRecentBlockhash(commitment);
final estimatedFee =
_getFeeFromCompiledMessage(message, ownerKeypair.publicKey, recentBlockhash, commitment);
return estimatedFee;
}
/// Load the Address's transactions into the account /// Load the Address's transactions into the account
Future<List<SolanaTransactionModel>> fetchTransactions( Future<List<SolanaTransactionModel>> fetchTransactions(
Ed25519HDPublicKey publicKey, { Ed25519HDPublicKey publicKey, {
@ -257,24 +271,15 @@ class SolanaWalletClient {
Future<PendingSolanaTransaction> signSolanaTransaction({ Future<PendingSolanaTransaction> signSolanaTransaction({
required String tokenTitle, required String tokenTitle,
required int tokenDecimals, required int tokenDecimals,
String? tokenMint,
required double inputAmount, required double inputAmount,
required String destinationAddress, required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair, required Ed25519HDKeyPair ownerKeypair,
required bool isSendAll,
String? tokenMint,
List<String> references = const [], List<String> references = const [],
}) async { }) async {
const commitment = Commitment.confirmed; const commitment = Commitment.confirmed;
final latestBlockhash =
await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value;
final recentBlockhash = RecentBlockhash(
blockhash: latestBlockhash.blockhash,
feeCalculator: const FeeCalculator(
lamportsPerSignature: 500,
),
);
if (tokenTitle == CryptoCurrency.sol.title) { if (tokenTitle == CryptoCurrency.sol.title) {
final pendingNativeTokenTransaction = await _signNativeTokenTransaction( final pendingNativeTokenTransaction = await _signNativeTokenTransaction(
tokenTitle: tokenTitle, tokenTitle: tokenTitle,
@ -282,8 +287,8 @@ class SolanaWalletClient {
inputAmount: inputAmount, inputAmount: inputAmount,
destinationAddress: destinationAddress, destinationAddress: destinationAddress,
ownerKeypair: ownerKeypair, ownerKeypair: ownerKeypair,
recentBlockhash: recentBlockhash,
commitment: commitment, commitment: commitment,
isSendAll: isSendAll,
); );
return pendingNativeTokenTransaction; return pendingNativeTokenTransaction;
} else { } else {
@ -294,25 +299,29 @@ class SolanaWalletClient {
inputAmount: inputAmount, inputAmount: inputAmount,
destinationAddress: destinationAddress, destinationAddress: destinationAddress,
ownerKeypair: ownerKeypair, ownerKeypair: ownerKeypair,
recentBlockhash: recentBlockhash,
commitment: commitment, commitment: commitment,
); );
return pendingSPLTokenTransaction; return pendingSPLTokenTransaction;
} }
} }
Future<PendingSolanaTransaction> _signNativeTokenTransaction({ Future<RecentBlockhash> _getRecentBlockhash(Commitment commitment) async {
required String tokenTitle, final latestBlockhash =
required int tokenDecimals, await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value;
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required RecentBlockhash recentBlockhash,
required Commitment commitment,
}) async {
// Convert SOL to lamport
int lamports = (inputAmount * lamportsPerSol).toInt();
final recentBlockhash = RecentBlockhash(
blockhash: latestBlockhash.blockhash,
feeCalculator: const FeeCalculator(lamportsPerSignature: 500),
);
return recentBlockhash;
}
Message _getMessageForNativeTransaction(
Ed25519HDKeyPair ownerKeypair,
String destinationAddress,
int lamports,
) {
final instructions = [ final instructions = [
SystemInstruction.transfer( SystemInstruction.transfer(
fundingAccount: ownerKeypair.publicKey, fundingAccount: ownerKeypair.publicKey,
@ -322,21 +331,75 @@ class SolanaWalletClient {
]; ];
final message = Message(instructions: instructions); final message = Message(instructions: instructions);
return message;
}
Future<double> _getFeeFromCompiledMessage(
Message message,
Ed25519HDPublicKey feePayer,
RecentBlockhash recentBlockhash,
Commitment commitment,
) async {
final compile = message.compile(
recentBlockhash: recentBlockhash.blockhash,
feePayer: feePayer,
);
final base64Message = base64Encode(compile.toByteArray().toList());
final fee = await getFeeForMessage(base64Message, commitment);
return fee;
}
Future<PendingSolanaTransaction> _signNativeTokenTransaction({
required String tokenTitle,
required int tokenDecimals,
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required Commitment commitment,
required bool isSendAll,
}) async {
// Convert SOL to lamport
int lamports = (inputAmount * lamportsPerSol).toInt();
Message message = _getMessageForNativeTransaction(ownerKeypair, destinationAddress, lamports);
final signers = [ownerKeypair]; final signers = [ownerKeypair];
final signedTx = await _signTransactionInternal( RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment);
message: message,
signers: signers,
commitment: commitment,
recentBlockhash: recentBlockhash,
);
final fee = await _getFeeFromCompiledMessage( final fee = await _getFeeFromCompiledMessage(
message, message,
recentBlockhash,
signers.first.publicKey, signers.first.publicKey,
recentBlockhash,
commitment,
); );
SignedTx signedTx;
if (isSendAll) {
final feeInLamports = (fee * lamportsPerSol).toInt();
final updatedLamports = lamports - feeInLamports;
final updatedMessage =
_getMessageForNativeTransaction(ownerKeypair, destinationAddress, updatedLamports);
signedTx = await _signTransactionInternal(
message: updatedMessage,
signers: signers,
commitment: commitment,
recentBlockhash: recentBlockhash,
);
} else {
signedTx = await _signTransactionInternal(
message: message,
signers: signers,
commitment: commitment,
recentBlockhash: recentBlockhash,
);
}
sendTx() async => await sendTransaction( sendTx() async => await sendTransaction(
signedTransaction: signedTx, signedTransaction: signedTx,
commitment: commitment, commitment: commitment,
@ -360,7 +423,6 @@ class SolanaWalletClient {
required double inputAmount, required double inputAmount,
required String destinationAddress, required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair, required Ed25519HDKeyPair ownerKeypair,
required RecentBlockhash recentBlockhash,
required Commitment commitment, required Commitment commitment,
}) async { }) async {
final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress); final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress);
@ -408,8 +470,18 @@ class SolanaWalletClient {
); );
final message = Message(instructions: [instruction]); final message = Message(instructions: [instruction]);
final signers = [ownerKeypair]; final signers = [ownerKeypair];
RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment);
final fee = await _getFeeFromCompiledMessage(
message,
signers.first.publicKey,
recentBlockhash,
commitment,
);
final signedTx = await _signTransactionInternal( final signedTx = await _signTransactionInternal(
message: message, message: message,
signers: signers, signers: signers,
@ -417,12 +489,6 @@ class SolanaWalletClient {
recentBlockhash: recentBlockhash, recentBlockhash: recentBlockhash,
); );
final fee = await _getFeeFromCompiledMessage(
message,
recentBlockhash,
signers.first.publicKey,
);
sendTx() async => await sendTransaction( sendTx() async => await sendTransaction(
signedTransaction: signedTx, signedTransaction: signedTx,
commitment: commitment, commitment: commitment,
@ -438,19 +504,6 @@ class SolanaWalletClient {
return pendingTransaction; return pendingTransaction;
} }
Future<double> _getFeeFromCompiledMessage(
Message message, RecentBlockhash recentBlockhash, Ed25519HDPublicKey feePayer) async {
final compile = message.compile(
recentBlockhash: recentBlockhash.blockhash,
feePayer: feePayer,
);
final base64Message = base64Encode(compile.toByteArray().toList());
final fee = await getGasForMessage(base64Message);
return fee;
}
Future<SignedTx> _signTransactionInternal({ Future<SignedTx> _signTransactionInternal({
required Message message, required Message message,
required List<Ed25519HDKeyPair> signers, required List<Ed25519HDKeyPair> signers,
@ -466,13 +519,35 @@ class SolanaWalletClient {
required SignedTx signedTransaction, required SignedTx signedTransaction,
required Commitment commitment, required Commitment commitment,
}) async { }) async {
final signature = await _client!.rpcClient.sendTransaction( try {
signedTransaction.encode(), final signature = await _client!.rpcClient.sendTransaction(
preflightCommitment: commitment, signedTransaction.encode(),
); preflightCommitment: commitment,
);
_client!.waitForSignatureStatus(signature, status: commitment); _client!.waitForSignatureStatus(signature, status: commitment);
return signature; return signature;
} catch (e) {
print('Error while sending transaction: ${e.toString()}');
throw Exception(e);
}
}
Future<String?> getIconImageFromTokenUri(String uri) async {
try {
final response = await httpClient.get(Uri.parse(uri));
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 200 && response.statusCode < 300) {
return jsonResponse['image'];
} else {
return null;
}
} catch (e) {
print('Error occurred while fetching token image: \n${e.toString()}');
return null;
}
} }
} }

View file

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
@ -30,7 +29,6 @@ import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:solana/metaplex.dart' as metaplex; import 'package:solana/metaplex.dart' as metaplex;
import 'package:solana/solana.dart'; import 'package:solana/solana.dart';
import 'package:web3dart/crypto.dart';
part 'solana_wallet.g.dart'; part 'solana_wallet.g.dart';
@ -77,6 +75,9 @@ abstract class SolanaWalletBase
late SolanaWalletClient _client; late SolanaWalletClient _client;
@observable
double? estimatedFee;
Timer? _transactionsUpdateTimer; Timer? _transactionsUpdateTimer;
late final Box<SPLToken> splTokensBox; late final Box<SPLToken> splTokensBox;
@ -134,7 +135,7 @@ abstract class SolanaWalletBase
assert(mnemonic != null || privateKey != null); assert(mnemonic != null || privateKey != null);
if (privateKey != null) { if (privateKey != null) {
final privateKeyBytes = hexToBytes(privateKey); final privateKeyBytes = HEX.decode(privateKey);
return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes); return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes);
} }
@ -173,6 +174,14 @@ abstract class SolanaWalletBase
} }
} }
Future<void> _getEstimatedFees() async {
try {
estimatedFee = await _client.getEstimatedFee(_walletKeyPair!);
} catch (e) {
estimatedFee = 0.0;
}
}
@override @override
Future<PendingTransaction> createTransaction(Object credentials) async { Future<PendingTransaction> createTransaction(Object credentials) async {
final solCredentials = credentials as SolanaTransactionCredentials; final solCredentials = credentials as SolanaTransactionCredentials;
@ -190,6 +199,8 @@ abstract class SolanaWalletBase
double totalAmount = 0.0; double totalAmount = 0.0;
bool isSendAll = false;
if (hasMultiDestination) { if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
throw SolanaTransactionWrongBalanceException(transactionCurrency); throw SolanaTransactionWrongBalanceException(transactionCurrency);
@ -206,9 +217,15 @@ abstract class SolanaWalletBase
} else { } else {
final output = outputs.first; final output = outputs.first;
final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0'); isSendAll = output.sendAll;
totalAmount = output.sendAll ? walletBalanceForCurrency : totalOriginalAmount; if (isSendAll) {
totalAmount = walletBalanceForCurrency;
} else {
final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
totalAmount = totalOriginalAmount;
}
if (walletBalanceForCurrency < totalAmount) { if (walletBalanceForCurrency < totalAmount) {
throw SolanaTransactionWrongBalanceException(transactionCurrency); throw SolanaTransactionWrongBalanceException(transactionCurrency);
@ -230,6 +247,7 @@ abstract class SolanaWalletBase
destinationAddress: solCredentials.outputs.first.isParsedAddress destinationAddress: solCredentials.outputs.first.isParsedAddress
? solCredentials.outputs.first.extractedAddress! ? solCredentials.outputs.first.extractedAddress!
: solCredentials.outputs.first.address, : solCredentials.outputs.first.address,
isSendAll: isSendAll,
); );
return pendingSolanaTransaction; return pendingSolanaTransaction;
@ -271,7 +289,10 @@ abstract class SolanaWalletBase
Future<void> _updateSPLTokenTransactions() async { Future<void> _updateSPLTokenTransactions() async {
List<SolanaTransactionModel> splTokenTransactions = []; List<SolanaTransactionModel> splTokenTransactions = [];
for (var token in balance.keys) { // Make a copy of keys to avoid concurrent modification
var tokenKeys = List<CryptoCurrency>.from(balance.keys);
for (var token in tokenKeys) {
if (token is SPLToken) { if (token is SPLToken) {
final tokenTxs = await _client.getSPLTokenTransfers( final tokenTxs = await _client.getSPLTokenTransfers(
token.mintAddress, token.mintAddress,
@ -328,6 +349,7 @@ abstract class SolanaWalletBase
_updateBalance(), _updateBalance(),
_updateNativeSOLTransactions(), _updateNativeSOLTransactions(),
_updateSPLTokenTransactions(), _updateSPLTokenTransactions(),
_getEstimatedFees(),
]); ]);
syncStatus = SyncedSyncStatus(); syncStatus = SyncedSyncStatus();
@ -435,18 +457,28 @@ abstract class SolanaWalletBase
final mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress); final mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress);
// Fetch token's metadata account // Fetch token's metadata account
final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey); try {
final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey);
if (token == null) { if (token == null) {
return null;
}
String? iconPath;
try {
iconPath = await _client.getIconImageFromTokenUri(token.uri);
} catch (_) {}
return SPLToken.fromMetadata(
name: token.name,
mint: token.mint,
symbol: token.symbol,
mintAddress: mintAddress,
iconPath: iconPath,
);
} catch (e) {
return null; return null;
} }
return SPLToken.fromMetadata(
name: token.name,
mint: token.mint,
symbol: token.symbol,
mintAddress: mintAddress,
);
} }
@override @override
@ -477,9 +509,9 @@ abstract class SolanaWalletBase
} }
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) { _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
_updateSPLTokenTransactions();
_updateNativeSOLTransactions();
_updateBalance(); _updateBalance();
_updateNativeSOLTransactions();
_updateSPLTokenTransactions();
}); });
} }
@ -491,7 +523,7 @@ abstract class SolanaWalletBase
final signature = await _walletKeyPair!.sign(messageBytes); final signature = await _walletKeyPair!.sign(messageBytes);
// Convert the signature to a hexadecimal string // Convert the signature to a hexadecimal string
final hex = bytesToHex(signature.bytes); final hex = HEX.encode(signature.bytes);
return hex; return hex;
} }

View file

@ -32,6 +32,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
await wallet.init(); await wallet.init();
wallet.addInitialTokens(); wallet.addInitialTokens();
await wallet.save();
return wallet; return wallet;
} }
@ -46,16 +47,31 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
Future<SolanaWallet> openWallet(String name, String password) async { Future<SolanaWallet> openWallet(String name, String password) async {
final walletInfo = final walletInfo =
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
final wallet = await SolanaWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
);
await wallet.init(); try {
await wallet.save(); final wallet = await SolanaWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
);
return wallet; await wallet.init();
await wallet.save();
saveBackup(name);
return wallet;
} catch (_) {
await restoreWalletFilesFromBackup(name);
final wallet = await SolanaWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
);
await wallet.init();
await wallet.save();
return wallet;
}
} }
@override @override
@ -110,6 +126,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
password: password, name: currentName, walletInfo: currentWalletInfo); password: password, name: currentName, walletInfo: currentWalletInfo);
await currentWallet.renameWalletFiles(newName); await currentWallet.renameWalletFiles(newName);
await saveBackup(newName);
final newWalletInfo = currentWalletInfo; final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType()); newWalletInfo.id = WalletBase.idFor(newName, getType());

View file

@ -55,6 +55,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
required String mint, required String mint,
required String symbol, required String symbol,
required String mintAddress, required String mintAddress,
String? iconPath
}) { }) {
return SPLToken( return SPLToken(
name: name, name: name,
@ -62,7 +63,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
mintAddress: mintAddress, mintAddress: mintAddress,
decimal: 0, decimal: 0,
mint: mint, mint: mint,
iconPath: '', iconPath: iconPath,
); );
} }

View file

@ -19,7 +19,6 @@ dependencies:
bip39: ^1.0.6 bip39: ^1.0.6
mobx: ^2.3.0+1 mobx: ^2.3.0+1
shared_preferences: ^2.0.15 shared_preferences: ^2.0.15
web3dart: ^2.7.1
bip32: ^2.0.0 bip32: ^2.0.0
hex: ^0.2.0 hex: ^0.2.0
@ -34,4 +33,4 @@ dev_dependencies:
flutter: flutter:
# assets: # assets:
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg

View file

@ -58,16 +58,16 @@ post_install do |installer|
'PERMISSION_CONTACTS=0', 'PERMISSION_CONTACTS=0',
## dart: PermissionGroup.camera ## dart: PermissionGroup.camera
'PERMISSION_CAMERA=0', 'PERMISSION_CAMERA=1',
## dart: PermissionGroup.microphone ## dart: PermissionGroup.microphone
'PERMISSION_MICROPHONE=0', 'PERMISSION_MICROPHONE=1',
## dart: PermissionGroup.speech ## dart: PermissionGroup.speech
'PERMISSION_SPEECH_RECOGNIZER=0', 'PERMISSION_SPEECH_RECOGNIZER=0',
## dart: PermissionGroup.photos ## dart: PermissionGroup.photos
'PERMISSION_PHOTOS=0', 'PERMISSION_PHOTOS=1',
## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
'PERMISSION_LOCATION=0', 'PERMISSION_LOCATION=0',

View file

@ -277,7 +277,7 @@ SPEC CHECKSUMS:
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0 fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
@ -300,6 +300,6 @@ SPEC CHECKSUMS:
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: fcb1b8418441a35b438585c9dd8374e722e6c6ca PODFILE CHECKSUM: a2fe518be61cdbdc5b0e2da085ab543d556af2d3
COCOAPODS: 1.12.1 COCOAPODS: 1.15.2

View file

@ -83,21 +83,25 @@ class CWBitcoin extends Bitcoin {
@override @override
Object createBitcoinTransactionCredentials(List<Output> outputs, Object createBitcoinTransactionCredentials(List<Output> outputs,
{required TransactionPriority priority, int? feeRate}) => {required TransactionPriority priority, int? feeRate}) {
BitcoinTransactionCredentials( final bitcoinFeeRate =
outputs priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null;
.map((out) => OutputInfo( return BitcoinTransactionCredentials(
fiatAmount: out.fiatAmount, outputs
cryptoAmount: out.cryptoAmount, .map((out) => OutputInfo(
address: out.address, fiatAmount: out.fiatAmount,
note: out.note, cryptoAmount: out.cryptoAmount,
sendAll: out.sendAll, address: out.address,
extractedAddress: out.extractedAddress, note: out.note,
isParsedAddress: out.isParsedAddress, sendAll: out.sendAll,
formattedCryptoAmount: out.formattedCryptoAmount)) extractedAddress: out.extractedAddress,
.toList(), isParsedAddress: out.isParsedAddress,
priority: priority as BitcoinTransactionPriority, formattedCryptoAmount: out.formattedCryptoAmount,
feeRate: feeRate); memo: out.memo))
.toList(),
priority: priority as BitcoinTransactionPriority,
feeRate: bitcoinFeeRate);
}
@override @override
Object createBitcoinTransactionCredentialsRaw(List<OutputInfo> outputs, Object createBitcoinTransactionCredentialsRaw(List<OutputInfo> outputs,
@ -122,13 +126,46 @@ class CWBitcoin extends Bitcoin {
.map((BitcoinAddressRecord addr) => ElectrumSubAddress( .map((BitcoinAddressRecord addr) => ElectrumSubAddress(
id: addr.index, id: addr.index,
name: addr.name, name: addr.name,
address: electrumWallet.type == WalletType.bitcoinCash ? addr.cashAddr : addr.address, address: addr.address,
txCount: addr.txCount, txCount: addr.txCount,
balance: addr.balance, balance: addr.balance,
isChange: addr.isHidden)) isChange: addr.isHidden))
.toList(); .toList();
} }
@override
Future<int> estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority) async {
try {
final sk = ECPrivate.random();
final electrumWallet = wallet as ElectrumWallet;
if (wallet.type == WalletType.bitcoinCash) {
final p2pkhAddr = sk.getPublic().toP2pkhAddress();
final estimatedTx = await electrumWallet.estimateSendAllTx(
[BitcoinOutput(address: p2pkhAddr, value: BigInt.zero)],
getFeeRate(wallet, priority as BitcoinCashTransactionPriority),
);
return estimatedTx.amount;
}
final p2shAddr = sk.getPublic().toP2pkhInP2sh();
final estimatedTx = await electrumWallet.estimateSendAllTx(
[BitcoinOutput(address: p2shAddr, value: BigInt.zero)],
getFeeRate(
wallet,
wallet.type == WalletType.litecoin
? priority as LitecoinTransactionPriority
: priority as BitcoinTransactionPriority,
),
);
return estimatedTx.amount;
} catch (_) {
return 0;
}
}
@override @override
String getAddress(Object wallet) { String getAddress(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet; final bitcoinWallet = wallet as ElectrumWallet;
@ -147,8 +184,9 @@ class CWBitcoin extends Bitcoin {
int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount); int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount);
@override @override
String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate) => String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate,
(priority as BitcoinTransactionPriority).labelWithRate(rate); {int? customRate}) =>
(priority as BitcoinTransactionPriority).labelWithRate(rate, customRate);
@override @override
List<BitcoinUnspent> getUnspents(Object wallet) { List<BitcoinUnspent> getUnspents(Object wallet) {
@ -174,6 +212,9 @@ class CWBitcoin extends Bitcoin {
@override @override
TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium; TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium;
@override
TransactionPriority getBitcoinTransactionPriorityCustom() => BitcoinTransactionPriority.custom;
@override @override
TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium; TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium;
@ -312,4 +353,48 @@ class CWBitcoin extends Bitcoin {
return list; return list;
} }
@override
bool hasTaprootInput(PendingTransaction pendingTransaction) {
return (pendingTransaction as PendingBitcoinTransaction).hasTaprootInputs;
}
@override
Future<PendingBitcoinTransaction> replaceByFee(
Object wallet, String transactionHash, String fee) async {
final bitcoinWallet = wallet as ElectrumWallet;
return await bitcoinWallet.replaceByFee(transactionHash, int.parse(fee));
}
@override
Future<bool> canReplaceByFee(Object wallet, String transactionHash) async {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.canReplaceByFee(transactionHash);
}
@override
Future<bool> isChangeSufficientForFee(Object wallet, String txId, String newFee) async {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.isChangeSufficientForFee(txId, int.parse(newFee));
}
@override
int getFeeAmountForPriority(
Object wallet, TransactionPriority priority, int inputsCount, int outputsCount,
{int? size}) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.feeAmountForPriority(
priority as BitcoinTransactionPriority, inputsCount, outputsCount);
}
@override
int getFeeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount,
{int? size}) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.feeAmountWithFeeRate(
feeRate,
inputsCount,
outputsCount,
);
}
} }

View file

@ -1,4 +1,13 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/buy/buy_amount.dart';
import 'package:cake_wallet/buy/buy_exception.dart';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
@ -6,34 +15,31 @@ import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/device_info.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:cake_wallet/buy/buy_exception.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:cake_wallet/buy/buy_amount.dart';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:flutter/material.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:http/http.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class MoonPaySellProvider extends BuyProvider { class MoonPayProvider extends BuyProvider {
MoonPaySellProvider({ MoonPayProvider({
required SettingsStore settingsStore, required SettingsStore settingsStore,
required WalletBase wallet, required WalletBase wallet,
bool isTestEnvironment = false, bool isTestEnvironment = false,
}) : baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl, }) : baseSellUrl = isTestEnvironment ? _baseSellTestUrl : _baseSellProductUrl,
baseBuyUrl = isTestEnvironment ? _baseBuyTestUrl : _baseBuyProductUrl,
this._settingsStore = settingsStore, this._settingsStore = settingsStore,
super(wallet: wallet, isTestEnvironment: isTestEnvironment); super(wallet: wallet, isTestEnvironment: isTestEnvironment);
final SettingsStore _settingsStore; final SettingsStore _settingsStore;
static const _baseTestUrl = 'sell-sandbox.moonpay.com'; static const _baseSellTestUrl = 'sell-sandbox.moonpay.com';
static const _baseProductUrl = 'sell.moonpay.com'; static const _baseSellProductUrl = 'sell.moonpay.com';
static const _baseBuyTestUrl = 'buy-staging.moonpay.com';
static const _baseBuyProductUrl = 'buy.moonpay.com';
static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
static const _apiUrl = 'https://api.moonpay.com';
@override @override
String get providerDescription => String get providerDescription =>
@ -60,146 +66,121 @@ class MoonPaySellProvider extends BuyProvider {
static String get _apiKey => secrets.moonPayApiKey; static String get _apiKey => secrets.moonPayApiKey;
static String get _secretKey => secrets.moonPaySecretKey; final String baseBuyUrl;
final String baseUrl; final String baseSellUrl;
Future<Uri> requestMoonPayUrl({ String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
Future<String> getMoonpaySignature(String query) async {
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
final response = await post(
uri,
headers: {
'Content-Type': 'application/json',
'x-api-key': _exchangeHelperApiKey,
},
body: json.encode({'query': query}),
);
if (response.statusCode == 200) {
return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
} else {
throw Exception(
'Provider currently unavailable. Status: ${response.statusCode} ${response.body}');
}
}
Future<Uri> requestSellMoonPayUrl({
required CryptoCurrency currency, required CryptoCurrency currency,
required String refundWalletAddress, required String refundWalletAddress,
required SettingsStore settingsStore, required SettingsStore settingsStore,
}) async { }) async {
final customParams = { final params = {
'theme': themeToMoonPayTheme(settingsStore.currentTheme), 'theme': themeToMoonPayTheme(settingsStore.currentTheme),
'language': settingsStore.languageCode, 'language': settingsStore.languageCode,
'colorCode': settingsStore.currentTheme.type == ThemeType.dark 'colorCode': settingsStore.currentTheme.type == ThemeType.dark
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}' ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}', : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
'defaultCurrencyCode': _normalizeCurrency(currency),
'refundWalletAddress': refundWalletAddress,
}; };
final originalUri = Uri.https( if (_apiKey.isNotEmpty) {
baseUrl, params['apiKey'] = _apiKey;
'', }
<String, dynamic>{
'apiKey': _apiKey,
'defaultBaseCurrencyCode': currency.toString().toLowerCase(),
'refundWalletAddress': refundWalletAddress,
}..addAll(customParams),
);
final messageBytes = utf8.encode('?${originalUri.query}'); final originalUri = Uri.https(
final key = utf8.encode(_secretKey); baseSellUrl,
final hmac = Hmac(sha256, key); '',
final digest = hmac.convert(messageBytes); params,
final signature = base64.encode(digest.bytes); );
if (isTestEnvironment) { if (isTestEnvironment) {
return originalUri; return originalUri;
} }
final signature = await getMoonpaySignature('?${originalUri.query}');
final query = Map<String, dynamic>.from(originalUri.queryParameters); final query = Map<String, dynamic>.from(originalUri.queryParameters);
query['signature'] = signature; query['signature'] = signature;
final signedUri = originalUri.replace(queryParameters: query); final signedUri = originalUri.replace(queryParameters: query);
return signedUri; return signedUri;
} }
@override // BUY:
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
try {
final uri = await requestMoonPayUrl(
currency: wallet.currency,
refundWalletAddress: wallet.walletAddresses.address,
settingsStore: _settingsStore,
);
if (await canLaunchUrl(uri)) {
if (DeviceInfo.instance.isMobile) {
Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]);
} else {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} else {
throw Exception('Could not launch URL');
}
} catch (e) {
await showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: 'MoonPay',
alertContent: 'The MoonPay service is currently unavailable: $e',
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
},
);
}
}
}
class MoonPayBuyProvider extends BuyProvider {
MoonPayBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
: baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl,
super(wallet: wallet, isTestEnvironment: isTestEnvironment);
static const _baseTestUrl = 'https://buy-staging.moonpay.com';
static const _baseProductUrl = 'https://buy.moonpay.com';
static const _apiUrl = 'https://api.moonpay.com';
static const _currenciesSuffix = '/v3/currencies'; static const _currenciesSuffix = '/v3/currencies';
static const _quoteSuffix = '/buy_quote'; static const _quoteSuffix = '/buy_quote';
static const _transactionsSuffix = '/v1/transactions'; static const _transactionsSuffix = '/v1/transactions';
static const _ipAddressSuffix = '/v4/ip_address'; static const _ipAddressSuffix = '/v4/ip_address';
static const _apiKey = secrets.moonPayApiKey;
static const _secretKey = secrets.moonPaySecretKey;
@override Future<Uri> requestBuyMoonPayUrl({
String get title => 'MoonPay'; required CryptoCurrency currency,
required SettingsStore settingsStore,
required String walletAddress,
String? amount,
}) async {
final params = {
'theme': themeToMoonPayTheme(settingsStore.currentTheme),
'language': settingsStore.languageCode,
'colorCode': settingsStore.currentTheme.type == ThemeType.dark
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
'defaultCurrencyCode': _normalizeCurrency(currency),
'baseCurrencyCode': _normalizeCurrency(currency),
'baseCurrencyAmount': amount ?? '0',
'currencyCode': currencyCode,
'walletAddress': walletAddress,
'lockAmount': 'false',
'showAllCurrencies': 'false',
'showWalletAddressForm': 'false',
'enabledPaymentMethods':
'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
};
@override if (_apiKey.isNotEmpty) {
String get providerDescription => params['apiKey'] = _apiKey;
'MoonPay offers a fast and simple way to buy and sell cryptocurrencies'; }
@override final originalUri = Uri.https(
String get lightIcon => 'assets/images/moonpay_light.png'; baseBuyUrl,
'',
params,
);
@override if (isTestEnvironment) {
String get darkIcon => 'assets/images/moonpay_dark.png'; return originalUri;
}
String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase(); final signature = await getMoonpaySignature('?${originalUri.query}');
final query = Map<String, dynamic>.from(originalUri.queryParameters);
String get trackUrl => baseUrl + '/transaction_receipt?transactionId='; query['signature'] = signature;
final signedUri = originalUri.replace(queryParameters: query);
String baseUrl; return signedUri;
Future<String> requestUrl(String amount, String sourceCurrency) async {
final enabledPaymentMethods = 'credit_debit_card%2Capple_pay%2Cgoogle_pay%2Csamsung_pay'
'%2Csepa_bank_transfer%2Cgbp_bank_transfer%2Cgbp_open_banking_payment';
final suffix = '?apiKey=' +
_apiKey +
'&currencyCode=' +
currencyCode +
'&enabledPaymentMethods=' +
enabledPaymentMethods +
'&walletAddress=' +
wallet.walletAddresses.address +
'&baseCurrencyCode=' +
sourceCurrency.toLowerCase() +
'&baseCurrencyAmount=' +
amount +
'&lockAmount=true' +
'&showAllCurrencies=false' +
'&showWalletAddressForm=false';
final originalUrl = baseUrl + suffix;
final messageBytes = utf8.encode(suffix);
final key = utf8.encode(_secretKey);
final hmac = Hmac(sha256, key);
final digest = hmac.convert(messageBytes);
final signature = base64.encode(digest.bytes);
final urlWithSignature = originalUrl + '&signature=${Uri.encodeComponent(signature)}';
return isTestEnvironment ? originalUrl : urlWithSignature;
} }
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async { Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
@ -274,6 +255,52 @@ class MoonPayBuyProvider extends BuyProvider {
} }
@override @override
Future<void> launchProvider(BuildContext context, bool? isBuyAction) => Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
throw UnimplementedError(); try {
late final Uri uri;
if (isBuyAction ?? true) {
uri = await requestBuyMoonPayUrl(
currency: wallet.currency,
walletAddress: wallet.walletAddresses.address,
settingsStore: _settingsStore,
);
} else {
uri = await requestSellMoonPayUrl(
currency: wallet.currency,
refundWalletAddress: wallet.walletAddresses.address,
settingsStore: _settingsStore,
);
}
if (await canLaunchUrl(uri)) {
if (DeviceInfo.instance.isMobile) {
Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]);
} else {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} else {
throw Exception('Could not launch URL');
}
} catch (e) {
await showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: 'MoonPay',
alertContent: 'The MoonPay service is currently unavailable: $e',
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
},
);
}
}
String _normalizeCurrency(CryptoCurrency currency) {
if (currency == CryptoCurrency.maticpoly) {
return "MATIC_POLYGON";
}
return currency.toString().toLowerCase();
}
} }

View file

@ -32,11 +32,12 @@ class RobinhoodBuyProvider extends BuyProvider {
String get _applicationId => secrets.robinhoodApplicationId; String get _applicationId => secrets.robinhoodApplicationId;
String get _apiSecret => secrets.robinhoodCIdApiSecret; String get _apiSecret => secrets.exchangeHelperApiKey;
String getSignature(String message) { String getSignature(String message) {
switch (wallet.type) { switch (wallet.type) {
case WalletType.ethereum: case WalletType.ethereum:
case WalletType.polygon:
return wallet.signMessage(message); return wallet.signMessage(message);
case WalletType.litecoin: case WalletType.litecoin:
case WalletType.bitcoin: case WalletType.bitcoin:

View file

@ -274,7 +274,7 @@ class AddressValidator extends TextValidator {
'|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type '|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)'; //P2trAddress type '|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)'; //P2trAddress type
case CryptoCurrency.ltc: case CryptoCurrency.ltc:
return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)'

View file

@ -34,6 +34,10 @@ class AmountValidator extends TextValidator {
late final DecimalAmountValidator decimalAmountValidator; late final DecimalAmountValidator decimalAmountValidator;
String? call(String? value) { String? call(String? value) {
if (value == null || value.isEmpty) {
return S.current.error_text_amount;
}
//* Validate for Text(length, symbols, decimals etc) //* Validate for Text(length, symbols, decimals etc)
final textValidation = symbolsAmountValidator(value) ?? decimalAmountValidator(value); final textValidation = symbolsAmountValidator(value) ?? decimalAmountValidator(value);

View file

@ -1,5 +1,7 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart';
@ -64,7 +66,7 @@ class AuthService with Store {
Future<bool> authenticate(String pin) async { Future<bool> authenticate(String pin) async {
final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
final encodedPin = await secureStorage.read(key: key); final encodedPin = await readSecureStorage(secureStorage, key);
final decodedPin = decodedPinCode(pin: encodedPin!); final decodedPin = decodedPinCode(pin: encodedPin!);
return decodedPin == pin; return decodedPin == pin;
@ -76,7 +78,8 @@ class AuthService with Store {
} }
Future<bool> requireAuth() async { Future<bool> requireAuth() async {
final timestamp = int.tryParse(await secureStorage.read(key: SecureKey.lastAuthTimeMilliseconds) ?? '0'); final timestamp =
int.tryParse(await secureStorage.read(key: SecureKey.lastAuthTimeMilliseconds) ?? '0');
final duration = _durationToRequireAuth(timestamp ?? 0); final duration = _durationToRequireAuth(timestamp ?? 0);
final requiredPinInterval = settingsStore.pinTimeOutDuration; final requiredPinInterval = settingsStore.pinTimeOutDuration;

View file

@ -14,4 +14,13 @@ class FailureState extends ExecutionState {
FailureState(this.error); FailureState(this.error);
final String error; final String error;
}
class AwaitingConfirmationState extends ExecutionState {
AwaitingConfirmationState({this.title, this.message, this.onConfirm, this.onCancel});
final String? title;
final String? message;
final Function()? onConfirm;
final Function()? onCancel;
} }

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:cake_wallet/entities/encrypt.dart'; import 'package:cake_wallet/entities/encrypt.dart';
@ -10,7 +11,7 @@ class KeyService {
Future<String> getWalletPassword({required String walletName}) async { Future<String> getWalletPassword({required String walletName}) async {
final key = generateStoreKeyFor( final key = generateStoreKeyFor(
key: SecretStoreKey.moneroWalletPassword, walletName: walletName); key: SecretStoreKey.moneroWalletPassword, walletName: walletName);
final encodedPassword = await _secureStorage.read(key: key); final encodedPassword = await readSecureStorage(_secureStorage, key);
return decodeWalletPassword(password: encodedPassword!); return decodeWalletPassword(password: encodedPassword!);
} }

View file

@ -8,3 +8,8 @@ class NodeAddressValidator extends TextValidator {
pattern: pattern:
'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\$|^[0-9a-zA-Z.\-]+\$'); '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\$|^[0-9a-zA-Z.\-]+\$');
} }
class NodePathValidator extends TextValidator {
NodePathValidator()
: super(errorMessage: S.current.error_text_node_address, pattern: '^([/0-9a-zA-Z.\-]+)?\$');
}

View file

@ -0,0 +1,27 @@
import 'dart:async';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
// For now, we can create a utility function to handle this.
//
// However, we could look into abstracting the entire FlutterSecureStorage package
// so the app doesn't depend on the package directly but an absraction.
// It'll make these kind of modifications to read/write come from a single point.
Future<String?> readSecureStorage(FlutterSecureStorage secureStorage, String key) async {
String? result;
const maxWait = Duration(seconds: 3);
const checkInterval = Duration(milliseconds: 200);
DateTime start = DateTime.now();
while (result == null && DateTime.now().difference(start) < maxWait) {
result = await secureStorage.read(key: key);
if (result != null) {
break;
}
await Future.delayed(checkInterval);
}
return result;
}

View file

@ -2,8 +2,8 @@ import 'solana_chain_service.dart';
enum SolanaChainId { enum SolanaChainId {
mainnet, mainnet,
testnet, // testnet,
devnet, // devnet,
} }
extension SolanaChainIdX on SolanaChainId { extension SolanaChainIdX on SolanaChainId {
@ -13,13 +13,16 @@ extension SolanaChainIdX on SolanaChainId {
switch (this) { switch (this) {
case SolanaChainId.mainnet: case SolanaChainId.mainnet:
name = '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ'; name = '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ';
// solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp
break; break;
case SolanaChainId.testnet: // case SolanaChainId.devnet:
name = '8E9rvCKLFQia2Y35HXjjpWzj8weVo44K'; // name = '8E9rvCKLFQia2Y35HXjjpWzj8weVo44K';
break; // // solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1
case SolanaChainId.devnet: // break;
name = ''; // case SolanaChainId.testnet:
break; // name = '';
// // solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z
// break;
} }
return '${SolanaChainServiceImpl.namespace}:$name'; return '${SolanaChainServiceImpl.namespace}:$name';

View file

@ -43,7 +43,7 @@ class SolanaChainServiceImpl implements ChainService {
SolanaClient( SolanaClient(
rpcUrl: rpcUrl, rpcUrl: rpcUrl,
websocketUrl: Uri.parse(webSocketUrl), websocketUrl: Uri.parse(webSocketUrl),
timeout: const Duration(minutes: 2), timeout: const Duration(minutes: 5),
) { ) {
for (final String event in getEvents()) { for (final String event in getEvents()) {
wallet.registerEventEmitter(chainId: getChainId(), event: event); wallet.registerEventEmitter(chainId: getChainId(), event: event);
@ -72,7 +72,7 @@ class SolanaChainServiceImpl implements ChainService {
@override @override
List<String> getEvents() { List<String> getEvents() {
return ['']; return ['chainChanged', 'accountsChanged'];
} }
Future<String?> requestAuthorization(String? text) async { Future<String?> requestAuthorization(String? text) async {
@ -100,8 +100,7 @@ class SolanaChainServiceImpl implements ChainService {
Future<String> solanaSignTransaction(String topic, dynamic parameters) async { Future<String> solanaSignTransaction(String topic, dynamic parameters) async {
log('received solana sign transaction request $parameters'); log('received solana sign transaction request $parameters');
final solanaSignTx = final solanaSignTx = SolanaSignTransaction.fromJson(parameters as Map<String, dynamic>);
SolanaSignTransaction.fromJson(parameters as Map<String, dynamic>);
final String? authError = await requestAuthorization('Confirm request to sign transaction?'); final String? authError = await requestAuthorization('Confirm request to sign transaction?');
@ -122,10 +121,13 @@ class SolanaChainServiceImpl implements ChainService {
return ''; return '';
} }
String signature = sign.signatures.first.toBase58(); String signature = await solanaClient.sendAndConfirmTransaction(
message: message,
signers: [ownerKeyPair!],
commitment: Commitment.confirmed,
);
print(signature); print(signature);
print(signature.runtimeType);
bottomSheetService.queueBottomSheet( bottomSheetService.queueBottomSheet(
isModalDismissible: true, isModalDismissible: true,

View file

@ -1,10 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart'; import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart'; import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart';
import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart'; import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/wallet_connect/models/auth_request_model.dart'; import 'package:cake_wallet/core/wallet_connect/models/auth_request_model.dart';
import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart'; import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
@ -19,6 +21,7 @@ import 'package:cw_core/wallet_type.dart';
import 'package:eth_sig_util/eth_sig_util.dart'; import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import 'chain_service/solana/solana_chain_id.dart'; import 'chain_service/solana/solana_chain_id.dart';
@ -32,6 +35,7 @@ class Web3WalletService = Web3WalletServiceBase with _$Web3WalletService;
abstract class Web3WalletServiceBase with Store { abstract class Web3WalletServiceBase with Store {
final AppStore appStore; final AppStore appStore;
final SharedPreferences sharedPreferences;
final BottomSheetService _bottomSheetHandler; final BottomSheetService _bottomSheetHandler;
final WalletConnectKeyService walletKeyService; final WalletConnectKeyService walletKeyService;
@ -52,7 +56,8 @@ abstract class Web3WalletServiceBase with Store {
@observable @observable
ObservableList<StoredCacao> auth; ObservableList<StoredCacao> auth;
Web3WalletServiceBase(this._bottomSheetHandler, this.walletKeyService, this.appStore) Web3WalletServiceBase(
this._bottomSheetHandler, this.walletKeyService, this.appStore, this.sharedPreferences)
: pairings = ObservableList<PairingInfo>(), : pairings = ObservableList<PairingInfo>(),
sessions = ObservableList<SessionData>(), sessions = ObservableList<SessionData>(),
auth = ObservableList<StoredCacao>(), auth = ObservableList<StoredCacao>(),
@ -133,13 +138,27 @@ abstract class Web3WalletServiceBase with Store {
if (appStore.wallet!.type == WalletType.solana) { if (appStore.wallet!.type == WalletType.solana) {
for (final cId in SolanaChainId.values) { for (final cId in SolanaChainId.values) {
final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type); final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type);
final rpcUri = node.uri;
final webSocketUri = 'wss://${node.uriRaw}/ws${node.uri.path}'; Uri? rpcUri;
String webSocketUrl;
bool isModifiedNodeUri = false;
if (node.uriRaw == 'rpc.ankr.com') {
isModifiedNodeUri = true;
//A better way to handle this instead of adding this to the general secrets?
String ankrApiKey = secrets.ankrApiKey;
rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey');
webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey';
} else {
webSocketUrl = 'wss://${node.uriRaw}';
}
SolanaChainServiceImpl( SolanaChainServiceImpl(
reference: cId, reference: cId,
rpcUrl: rpcUri, rpcUrl: isModifiedNodeUri ? rpcUri! : node.uri,
webSocketUrl: webSocketUri, webSocketUrl: webSocketUrl,
wcKeyService: walletKeyService, wcKeyService: walletKeyService,
bottomSheetService: _bottomSheetHandler, bottomSheetService: _bottomSheetHandler,
wallet: _web3Wallet, wallet: _web3Wallet,
@ -177,13 +196,6 @@ abstract class Web3WalletServiceBase with Store {
_refreshPairings(); _refreshPairings();
} }
@action
void _refreshPairings() {
pairings.clear();
final allPairings = _web3Wallet.pairings.getAll();
pairings.addAll(allPairings);
}
Future<void> _onSessionProposalError(SessionProposalErrorEvent? args) async { Future<void> _onSessionProposalError(SessionProposalErrorEvent? args) async {
log(args.toString()); log(args.toString());
} }
@ -246,14 +258,37 @@ abstract class Web3WalletServiceBase with Store {
} }
} }
@action
void _refreshPairings() {
print('Refreshing pairings');
pairings.clear();
final allPairings = _web3Wallet.pairings.getAll();
final keyForWallet = getKeyForStoringTopicsForWallet();
final currentTopicsForWallet = getPairingTopicsForWallet(keyForWallet);
final filteredPairings =
allPairings.where((pairing) => currentTopicsForWallet.contains(pairing.topic)).toList();
pairings.addAll(filteredPairings);
}
void _onPairingCreate(PairingEvent? args) { void _onPairingCreate(PairingEvent? args) {
log('Pairing Create Event: $args'); log('Pairing Create Event: $args');
} }
@action @action
void _onSessionConnect(SessionConnect? args) { Future<void> _onSessionConnect(SessionConnect? args) async {
if (args != null) { if (args != null) {
log('Session Connected $args');
await savePairingTopicToLocalStorage(args.session.pairingTopic);
sessions.add(args.session); sessions.add(args.session);
_refreshPairings();
} }
} }
@ -321,4 +356,53 @@ abstract class Web3WalletServiceBase with Store {
List<SessionData> getSessionsForPairingInfo(PairingInfo pairing) { List<SessionData> getSessionsForPairingInfo(PairingInfo pairing) {
return sessions.where((element) => element.pairingTopic == pairing.topic).toList(); return sessions.where((element) => element.pairingTopic == pairing.topic).toList();
} }
String getKeyForStoringTopicsForWallet() {
List<ChainKeyModel> chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
final keyForPairingTopic =
PreferencesKey.walletConnectPairingTopicsListForWallet(chainKeys.first.publicKey);
return keyForPairingTopic;
}
List<String> getPairingTopicsForWallet(String key) {
// Get the JSON-encoded string from shared preferences
final jsonString = sharedPreferences.getString(key);
// If the string is null, return an empty list
if (jsonString == null) {
return [];
}
// Decode the JSON string to a list of strings
final List<dynamic> jsonList = jsonDecode(jsonString) as List<dynamic>;
// Cast each item to a string
return jsonList.map((item) => item as String).toList();
}
Future<void> savePairingTopicToLocalStorage(String pairingTopic) async {
// Get key specific to the current wallet
final key = getKeyForStoringTopicsForWallet();
// Get all pairing topics attached to this key
final pairingTopicsForWallet = getPairingTopicsForWallet(key);
print(pairingTopicsForWallet);
bool isPairingTopicAlreadySaved = pairingTopicsForWallet.contains(pairingTopic);
print('Is Pairing Topic Saved: $isPairingTopicAlreadySaved');
if (!isPairingTopicAlreadySaved) {
// Update the list with the most recent pairing topic
pairingTopicsForWallet.add(pairingTopic);
// Convert the list of updated pairing topics to a JSON-encoded string
final jsonString = jsonEncode(pairingTopicsForWallet);
// Save the encoded string to shared preferences
await sharedPreferences.setString(key, jsonString);
}
}
} }

View file

@ -13,6 +13,7 @@ import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/background_tasks.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/receive_page_option.dart';
import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/nano/nano.dart';
@ -198,6 +199,7 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart';
import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; import 'package:cake_wallet/view_model/wallet_seed_view_model.dart';
import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -491,6 +493,7 @@ Future<void> setup({
getIt.get<BottomSheetService>(), getIt.get<BottomSheetService>(),
getIt.get<WalletConnectKeyService>(), getIt.get<WalletConnectKeyService>(),
appStore, appStore,
getIt.get<SharedPreferences>()
); );
web3WalletService.create(); web3WalletService.create();
return web3WalletService; return web3WalletService;
@ -806,8 +809,11 @@ Future<void> setup({
getIt getIt
.registerFactory<DFXBuyProvider>(() => DFXBuyProvider(wallet: getIt.get<AppStore>().wallet!)); .registerFactory<DFXBuyProvider>(() => DFXBuyProvider(wallet: getIt.get<AppStore>().wallet!));
getIt.registerFactory<MoonPaySellProvider>(() => MoonPaySellProvider( getIt.registerFactory<MoonPayProvider>(() => MoonPayProvider(
settingsStore: getIt.get<AppStore>().settingsStore, wallet: getIt.get<AppStore>().wallet!)); settingsStore: getIt.get<AppStore>().settingsStore,
wallet: getIt.get<AppStore>().wallet!,
isTestEnvironment: kDebugMode,
));
getIt.registerFactory<OnRamperBuyProvider>(() => OnRamperBuyProvider( getIt.registerFactory<OnRamperBuyProvider>(() => OnRamperBuyProvider(
getIt.get<AppStore>().settingsStore, getIt.get<AppStore>().settingsStore,
@ -910,7 +916,8 @@ Future<void> setup({
transactionInfo: transactionInfo, transactionInfo: transactionInfo,
transactionDescriptionBox: _transactionDescriptionBox, transactionDescriptionBox: _transactionDescriptionBox,
wallet: wallet, wallet: wallet,
settingsStore: getIt.get<SettingsStore>()); settingsStore: getIt.get<SettingsStore>(),
sendViewModel: getIt.get<SendViewModel>());
}); });
getIt.registerFactoryParam<TransactionDetailsPage, TransactionInfo, void>( getIt.registerFactoryParam<TransactionDetailsPage, TransactionInfo, void>(
@ -1133,6 +1140,11 @@ Future<void> setup({
getIt.registerFactory(() => IoniaAccountCardsPage(getIt.get<IoniaAccountViewModel>())); getIt.registerFactory(() => IoniaAccountCardsPage(getIt.get<IoniaAccountViewModel>()));
getIt.registerFactoryParam<RBFDetailsPage, TransactionInfo, void>(
(TransactionInfo transactionInfo, _) => RBFDetailsPage(
transactionDetailsViewModel:
getIt.get<TransactionDetailsViewModel>(param1: transactionInfo)));
getIt.registerFactory(() => AnonPayApi( getIt.registerFactory(() => AnonPayApi(
useTorOnly: getIt.get<SettingsStore>().exchangeStatus == ExchangeApiMode.torOnly, useTorOnly: getIt.get<SettingsStore>().exchangeStatus == ExchangeApiMode.torOnly,
wallet: getIt.get<AppStore>().wallet!)); wallet: getIt.get<AppStore>().wallet!));

View file

@ -4,6 +4,7 @@ import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/view_model/settings/sync_mode.dart'; import 'package:cake_wallet/view_model/settings/sync_mode.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
@ -107,7 +108,7 @@ class BackgroundTasks {
final SyncMode syncMode = settingsStore.currentSyncMode; final SyncMode syncMode = settingsStore.currentSyncMode;
final bool syncAll = settingsStore.currentSyncAll; final bool syncAll = settingsStore.currentSyncAll;
if (syncMode.type == SyncType.disabled) { if (syncMode.type == SyncType.disabled || !FeatureFlag.isBackgroundSyncEnabled) {
cancelSyncTask(); cancelSyncTask();
return; return;
} }

View file

@ -10,6 +10,7 @@ class BiometricAuth {
return await _localAuth.authenticate( return await _localAuth.authenticate(
localizedReason: S.current.biometric_auth_reason, localizedReason: S.current.biometric_auth_reason,
options: AuthenticationOptions( options: AuthenticationOptions(
biometricOnly: true,
useErrorDialogs: true, useErrorDialogs: true,
stickyAuth: false)); stickyAuth: false));
} on PlatformException catch (e) { } on PlatformException catch (e) {

View file

@ -1,6 +1,7 @@
import 'dart:io' show Directory, File, Platform; import 'dart:io' show Directory, File, Platform;
import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pathForWallet.dart';
import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@ -211,10 +212,17 @@ Future<void> defaultSettingsMigration(
await changeDefaultBitcoinNode(nodes, sharedPreferences); await changeDefaultBitcoinNode(nodes, sharedPreferences);
break; break;
case 31: case 30:
await updateBtcNanoWalletInfos(walletInfoSource); await disableServiceStatusFiatDisabled(sharedPreferences);
break; break;
case 31:
await updateNanoNodeList(nodes: nodes);
break;
case 32:
await updateBtcNanoWalletInfos(walletInfoSource);
default: default:
break; break;
} }
@ -229,6 +237,44 @@ Future<void> defaultSettingsMigration(
await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version); await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version);
} }
Future<void> updateNanoNodeList({required Box<Node> nodes}) async {
final nodeList = await loadDefaultNanoNodes();
var listOfNewEndpoints = <String>[
"app.natrium.io",
"rainstorm.city",
"node.somenano.com",
"nanoslo.0x.no",
"www.bitrequest.app",
];
// add new nodes:
for (final node in nodeList) {
if (listOfNewEndpoints.contains(node.uriRaw)) {
await nodes.add(node);
}
}
// update the nautilus node:
final nautilusNode =
nodes.values.firstWhereOrNull((element) => element.uriRaw == "node.perish.co");
if (nautilusNode != null) {
nautilusNode.uriRaw = "node.nautilus.io";
nautilusNode.path = "/api";
nautilusNode.useSSL = true;
await nautilusNode.save();
}
}
Future<void> disableServiceStatusFiatDisabled(SharedPreferences sharedPreferences) async {
final currentFiat = await sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey) ?? -1;
if (currentFiat == -1 || currentFiat == FiatApiMode.enabled.raw) {
return;
}
if (currentFiat == FiatApiMode.disabled.raw || currentFiat == FiatApiMode.torOnly.raw) {
await sharedPreferences.setBool(PreferencesKey.disableBulletinKey, true);
}
}
Future<void> _updateMoneroPriority(SharedPreferences sharedPreferences) async { Future<void> _updateMoneroPriority(SharedPreferences sharedPreferences) async {
final currentPriority = final currentPriority =
await sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority) ?? await sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority) ??

View file

@ -20,6 +20,7 @@ class PreferencesKey {
static const isAppSecureKey = 'is_app_secure'; static const isAppSecureKey = 'is_app_secure';
static const disableBuyKey = 'disable_buy'; static const disableBuyKey = 'disable_buy';
static const disableSellKey = 'disable_sell'; static const disableSellKey = 'disable_sell';
static const disableBulletinKey = 'disable_bulletin';
static const defaultBuyProvider = 'default_buy_provider'; static const defaultBuyProvider = 'default_buy_provider';
static const walletListOrder = 'wallet_list_order'; static const walletListOrder = 'wallet_list_order';
static const walletListAscending = 'wallet_list_ascending'; static const walletListAscending = 'wallet_list_ascending';
@ -41,8 +42,10 @@ class PreferencesKey {
static const ethereumTransactionPriority = 'current_fee_priority_ethereum'; static const ethereumTransactionPriority = 'current_fee_priority_ethereum';
static const polygonTransactionPriority = 'current_fee_priority_polygon'; static const polygonTransactionPriority = 'current_fee_priority_polygon';
static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash'; static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash';
static const customBitcoinFeeRate = 'custom_electrum_fee_rate';
static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowReceiveWarning = 'should_show_receive_warning';
static const shouldShowYatPopup = 'should_show_yat_popup'; static const shouldShowYatPopup = 'should_show_yat_popup';
static const shouldShowRepWarning = 'should_show_rep_warning';
static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1'; static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1';
static const syncModeKey = 'sync_mode'; static const syncModeKey = 'sync_mode';
static const syncAllKey = 'sync_all'; static const syncAllKey = 'sync_all';
@ -73,4 +76,7 @@ class PreferencesKey {
static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard'; static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard';
static const isNewInstall = 'is_new_install'; static const isNewInstall = 'is_new_install';
static const serviceStatusShaKey = 'service_status_sha_key'; static const serviceStatusShaKey = 'service_status_sha_key';
static const walletConnectPairingTopicsList = 'wallet_connect_pairing_topics_list';
static String walletConnectPairingTopicsListForWallet(String publicKey) =>
'${PreferencesKey.walletConnectPairingTopicsList}_${publicKey}';
} }

View file

@ -11,7 +11,7 @@ enum ProviderType {
robinhood, robinhood,
dfx, dfx,
onramper, onramper,
moonpaySell, moonpay,
} }
extension ProviderTypeName on ProviderType { extension ProviderTypeName on ProviderType {
@ -25,7 +25,7 @@ extension ProviderTypeName on ProviderType {
return 'DFX Connect'; return 'DFX Connect';
case ProviderType.onramper: case ProviderType.onramper:
return 'Onramper'; return 'Onramper';
case ProviderType.moonpaySell: case ProviderType.moonpay:
return 'MoonPay'; return 'MoonPay';
} }
} }
@ -40,7 +40,7 @@ extension ProviderTypeName on ProviderType {
return 'dfx_connect_provider'; return 'dfx_connect_provider';
case ProviderType.onramper: case ProviderType.onramper:
return 'onramper_provider'; return 'onramper_provider';
case ProviderType.moonpaySell: case ProviderType.moonpay:
return 'moonpay_provider'; return 'moonpay_provider';
} }
} }
@ -55,18 +55,18 @@ class ProvidersHelper {
case WalletType.monero: case WalletType.monero:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx]; return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
case WalletType.bitcoin: case WalletType.bitcoin:
case WalletType.polygon:
case WalletType.ethereum: case WalletType.ethereum:
return [ return [
ProviderType.askEachTime, ProviderType.askEachTime,
ProviderType.onramper, ProviderType.onramper,
ProviderType.dfx, ProviderType.dfx,
ProviderType.robinhood, ProviderType.robinhood,
ProviderType.moonpay,
]; ];
case WalletType.litecoin: case WalletType.litecoin:
case WalletType.bitcoinCash: case WalletType.bitcoinCash:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood]; return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay];
case WalletType.polygon:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
case WalletType.solana: case WalletType.solana:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood]; return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
case WalletType.none: case WalletType.none:
@ -79,23 +79,22 @@ class ProvidersHelper {
switch (walletType) { switch (walletType) {
case WalletType.bitcoin: case WalletType.bitcoin:
case WalletType.ethereum: case WalletType.ethereum:
case WalletType.polygon:
return [ return [
ProviderType.askEachTime, ProviderType.askEachTime,
ProviderType.onramper, ProviderType.onramper,
ProviderType.moonpaySell, ProviderType.moonpay,
ProviderType.dfx, ProviderType.dfx,
]; ];
case WalletType.litecoin: case WalletType.litecoin:
case WalletType.bitcoinCash: case WalletType.bitcoinCash:
return [ProviderType.askEachTime, ProviderType.moonpaySell]; return [ProviderType.askEachTime, ProviderType.moonpay];
case WalletType.polygon:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
case WalletType.solana: case WalletType.solana:
return [ return [
ProviderType.askEachTime, ProviderType.askEachTime,
ProviderType.onramper, ProviderType.onramper,
ProviderType.robinhood, ProviderType.robinhood,
ProviderType.moonpaySell, ProviderType.moonpay,
]; ];
case WalletType.monero: case WalletType.monero:
case WalletType.nano: case WalletType.nano:
@ -114,10 +113,10 @@ class ProvidersHelper {
return getIt.get<DFXBuyProvider>(); return getIt.get<DFXBuyProvider>();
case ProviderType.onramper: case ProviderType.onramper:
return getIt.get<OnRamperBuyProvider>(); return getIt.get<OnRamperBuyProvider>();
case ProviderType.moonpay:
return getIt.get<MoonPayProvider>();
case ProviderType.askEachTime: case ProviderType.askEachTime:
return null; return null;
case ProviderType.moonpaySell:
return getIt.get<MoonPaySellProvider>();
} }
} }
} }

View file

@ -76,7 +76,8 @@ class CWEthereum extends Ethereum {
sendAll: out.sendAll, sendAll: out.sendAll,
extractedAddress: out.extractedAddress, extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress, isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount)) formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo))
.toList(), .toList(),
priority: priority as EVMChainTransactionPriority, priority: priority as EVMChainTransactionPriority,
currency: currency, currency: currency,
@ -130,7 +131,7 @@ class CWEthereum extends Ethereum {
@override @override
Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) async { Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) async {
final ethereumWallet = wallet as EthereumWallet; final ethereumWallet = wallet as EthereumWallet;
return await ethereumWallet.getErc20Token(contractAddress); return await ethereumWallet.getErc20Token(contractAddress, 'eth');
} }
@override @override

View file

@ -22,6 +22,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png'); ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png');
static const exolix = static const exolix =
ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png'); ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png');
static const thorChain =
ExchangeProviderDescription(title: 'ThorChain' , raw: 8, image: 'assets/images/thorchain.png');
static const all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: ''); static const all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: '');
@ -41,6 +43,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
return trocador; return trocador;
case 6: case 6:
return exolix; return exolix;
case 8:
return thorChain;
case 7: case 7:
return all; return all;
default: default:

View file

@ -133,7 +133,11 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
} }
@override @override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async { Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
final distributionPath = await DistributionInfo.instance.getDistributionPath(); final distributionPath = await DistributionInfo.instance.getDistributionPath();
final formattedAppVersion = int.tryParse(_settingsStore.appVersion.replaceAll('.', '')) ?? 0; final formattedAppVersion = int.tryParse(_settingsStore.appVersion.replaceAll('.', '')) ?? 0;
final payload = { final payload = {
@ -202,7 +206,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
createdAt: DateTime.now(), createdAt: DateTime.now(),
amount: responseJSON['fromAmount']?.toString() ?? request.fromAmount, amount: responseJSON['fromAmount']?.toString() ?? request.fromAmount,
state: TradeState.created, state: TradeState.created,
payoutAddress: payoutAddress); payoutAddress: payoutAddress,
isSendAll: isSendAll);
} }
@override @override

View file

@ -28,7 +28,8 @@ abstract class ExchangeProvider {
Future<Limits> fetchLimits( Future<Limits> fetchLimits(
{required CryptoCurrency from, required CryptoCurrency to, required bool isFixedRateMode}); {required CryptoCurrency from, required CryptoCurrency to, required bool isFixedRateMode});
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}); Future<Trade> createTrade(
{required TradeRequest request, required bool isFixedRateMode, required bool isSendAll});
Future<Trade> findTradeById({required String id}); Future<Trade> findTradeById({required String id});

View file

@ -130,7 +130,11 @@ class ExolixExchangeProvider extends ExchangeProvider {
} }
@override @override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async { Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
final headers = {'Content-Type': 'application/json'}; final headers = {'Content-Type': 'application/json'};
final body = { final body = {
'coinFrom': _normalizeCurrency(request.fromCurrency), 'coinFrom': _normalizeCurrency(request.fromCurrency),
@ -180,7 +184,8 @@ class ExolixExchangeProvider extends ExchangeProvider {
createdAt: DateTime.now(), createdAt: DateTime.now(),
amount: amount, amount: amount,
state: TradeState.created, state: TradeState.created,
payoutAddress: payoutAddress); payoutAddress: payoutAddress,
isSendAll: isSendAll);
} }
@override @override

View file

@ -144,7 +144,11 @@ class SideShiftExchangeProvider extends ExchangeProvider {
} }
@override @override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async { Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
String url = ''; String url = '';
final body = { final body = {
'affiliateId': affiliateId, 'affiliateId': affiliateId,
@ -197,6 +201,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
amount: depositAmount ?? request.fromAmount, amount: depositAmount ?? request.fromAmount,
payoutAddress: settleAddress, payoutAddress: settleAddress,
createdAt: DateTime.now(), createdAt: DateTime.now(),
isSendAll: isSendAll,
); );
} }

View file

@ -117,7 +117,11 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
} }
@override @override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async { Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
final headers = {'Content-Type': 'application/json'}; final headers = {'Content-Type': 'application/json'};
final params = {'api_key': apiKey}; final params = {'api_key': apiKey};
final body = <String, dynamic>{ final body = <String, dynamic>{
@ -162,6 +166,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
amount: request.fromAmount, amount: request.fromAmount,
payoutAddress: payoutAddress, payoutAddress: payoutAddress,
createdAt: DateTime.now(), createdAt: DateTime.now(),
isSendAll: isSendAll,
); );
} }

View file

@ -0,0 +1,255 @@
import 'dart:convert';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_request.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:hive/hive.dart';
import 'package:http/http.dart' as http;
class ThorChainExchangeProvider extends ExchangeProvider {
ThorChainExchangeProvider({required this.tradesStore})
: super(pairList: supportedPairs(_notSupported));
static final List<CryptoCurrency> _notSupported = [
...(CryptoCurrency.all
.where((element) => ![
CryptoCurrency.btc,
CryptoCurrency.eth,
CryptoCurrency.ltc,
CryptoCurrency.bch,
CryptoCurrency.aave,
CryptoCurrency.dai,
CryptoCurrency.gusd,
CryptoCurrency.usdc,
CryptoCurrency.usdterc20,
CryptoCurrency.wbtc,
].contains(element))
.toList())
];
static final isRefundAddressSupported = [CryptoCurrency.eth];
static const _baseURL = 'thornode.ninerealms.com';
static const _quotePath = '/thorchain/quote/swap';
static const _txInfoPath = '/thorchain/tx/status/';
static const _affiliateName = 'cakewallet';
static const _affiliateBps = '175';
final Box<Trade> tradesStore;
@override
String get title => 'THORChain';
@override
bool get isAvailable => true;
@override
bool get isEnabled => true;
@override
bool get supportsFixedRate => false;
@override
ExchangeProviderDescription get description => ExchangeProviderDescription.thorChain;
@override
Future<bool> checkIsAvailable() async => true;
@override
Future<double> fetchRate(
{required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
try {
if (amount == 0) return 0.0;
final params = {
'from_asset': _normalizeCurrency(from),
'to_asset': _normalizeCurrency(to),
'amount': _doubleToThorChainString(amount),
'affiliate': _affiliateName,
'affiliate_bps': _affiliateBps
};
final responseJSON = await _getSwapQuote(params);
final expectedAmountOut = responseJSON['expected_amount_out'] as String? ?? '0.0';
return _thorChainAmountToDouble(expectedAmountOut) / amount;
} catch (e) {
print(e.toString());
return 0.0;
}
}
@override
Future<Limits> fetchLimits(
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
final params = {
'from_asset': _normalizeCurrency(from),
'to_asset': _normalizeCurrency(to),
'amount': _doubleToThorChainString(1),
'affiliate': _affiliateName,
'affiliate_bps': _affiliateBps
};
final responseJSON = await _getSwapQuote(params);
final minAmountIn = responseJSON['recommended_min_amount_in'] as String? ?? '0.0';
return Limits(min: _thorChainAmountToDouble(minAmountIn));
}
@override
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
String formattedToAddress = request.toAddress.startsWith('bitcoincash:')
? request.toAddress.replaceFirst('bitcoincash:', '')
: request.toAddress;
final formattedFromAmount = double.parse(request.fromAmount);
final params = {
'from_asset': _normalizeCurrency(request.fromCurrency),
'to_asset': _normalizeCurrency(request.toCurrency),
'amount': _doubleToThorChainString(formattedFromAmount),
'destination': formattedToAddress,
'affiliate': _affiliateName,
'affiliate_bps': _affiliateBps,
'refund_address':
isRefundAddressSupported.contains(request.fromCurrency) ? request.refundAddress : '',
};
final responseJSON = await _getSwapQuote(params);
final inputAddress = responseJSON['inbound_address'] as String?;
final memo = responseJSON['memo'] as String?;
return Trade(
id: '',
from: request.fromCurrency,
to: request.toCurrency,
provider: description,
inputAddress: inputAddress,
createdAt: DateTime.now(),
amount: request.fromAmount,
state: TradeState.notFound,
payoutAddress: request.toAddress,
memo: memo,
isSendAll: isSendAll);
}
@override
Future<Trade> findTradeById({required String id}) async {
if (id.isEmpty) throw Exception('Trade id is empty');
final formattedId = id.startsWith('0x') ? id.substring(2) : id;
final uri = Uri.https(_baseURL, '$_txInfoPath$formattedId');
final response = await http.get(uri);
if (response.statusCode == 404) {
throw Exception('Trade not found for id: $formattedId');
} else if (response.statusCode != 200) {
throw Exception('Unexpected HTTP status: ${response.statusCode}');
}
final responseJSON = json.decode(response.body);
final Map<String, dynamic> stagesJson = responseJSON['stages'] as Map<String, dynamic>;
final inboundObservedStarted = stagesJson['inbound_observed']?['started'] as bool? ?? true;
if (!inboundObservedStarted) {
throw Exception('Trade has not started for id: $formattedId');
}
final currentState = _updateStateBasedOnStages(stagesJson) ?? TradeState.notFound;
final tx = responseJSON['tx'];
final String fromAddress = tx['from_address'] as String? ?? '';
final String toAddress = tx['to_address'] as String? ?? '';
final List<dynamic> coins = tx['coins'] as List<dynamic>;
final String? memo = tx['memo'] as String?;
final parts = memo?.split(':') ?? [];
final String toChain = parts.length > 1 ? parts[1].split('.')[0] : '';
final String toAsset = parts.length > 1 && parts[1].split('.').length > 1
? parts[1].split('.')[1].split('-')[0]
: '';
final formattedToChain = CryptoCurrency.fromString(toChain);
final toAssetWithChain = CryptoCurrency.fromString(toAsset, walletCurrency: formattedToChain);
final plannedOutTxs = responseJSON['planned_out_txs'] as List<dynamic>?;
final isRefund = plannedOutTxs?.any((tx) => tx['refund'] == true) ?? false;
return Trade(
id: id,
from: CryptoCurrency.fromString(tx['chain'] as String? ?? ''),
to: toAssetWithChain,
provider: description,
inputAddress: fromAddress,
payoutAddress: toAddress,
amount: coins.first['amount'] as String? ?? '0.0',
state: currentState,
memo: memo,
isRefund: isRefund,
);
}
Future<Map<String, dynamic>> _getSwapQuote(Map<String, String> params) async {
Uri uri = Uri.https(_baseURL, _quotePath, params);
final response = await http.get(uri);
if (response.statusCode != 200) {
throw Exception('Unexpected HTTP status: ${response.statusCode}');
}
if (response.body.contains('error')) {
throw Exception('Unexpected response: ${response.body}');
}
return json.decode(response.body) as Map<String, dynamic>;
}
String _normalizeCurrency(CryptoCurrency currency) {
final networkTitle = currency.tag == 'ETH' ? 'ETH' : currency.title;
return '$networkTitle.${currency.title}';
}
String _doubleToThorChainString(double amount) => (amount * 1e8).toInt().toString();
double _thorChainAmountToDouble(String amount) => double.parse(amount) / 1e8;
TradeState? _updateStateBasedOnStages(Map<String, dynamic> stages) {
TradeState? currentState;
if (stages['inbound_observed']['completed'] as bool? ?? false) {
currentState = TradeState.confirmation;
}
if (stages['inbound_confirmation_counted']['completed'] as bool? ?? false) {
currentState = TradeState.confirmed;
}
if (stages['inbound_finalised']['completed'] as bool? ?? false) {
currentState = TradeState.processing;
}
if (stages['swap_finalised']['completed'] as bool? ?? false) {
currentState = TradeState.traded;
}
if (stages['outbound_signed']['completed'] as bool? ?? false) {
currentState = TradeState.success;
}
return currentState;
}
}

View file

@ -13,7 +13,8 @@ import 'package:http/http.dart';
class TrocadorExchangeProvider extends ExchangeProvider { class TrocadorExchangeProvider extends ExchangeProvider {
TrocadorExchangeProvider({this.useTorOnly = false, this.providerStates = const {}}) TrocadorExchangeProvider({this.useTorOnly = false, this.providerStates = const {}})
: _lastUsedRateId = '', _provider = [], : _lastUsedRateId = '',
_provider = [],
super(pairList: supportedPairs(_notSupported)); super(pairList: supportedPairs(_notSupported));
bool useTorOnly; bool useTorOnly;
@ -23,7 +24,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
'Swapter', 'Swapter',
'StealthEx', 'StealthEx',
'Simpleswap', 'Simpleswap',
'Swapuz' 'Swapuz',
'ChangeNow', 'ChangeNow',
'Changehero', 'Changehero',
'FixedFloat', 'FixedFloat',
@ -31,7 +32,17 @@ class TrocadorExchangeProvider extends ExchangeProvider {
'Exolix', 'Exolix',
'Godex', 'Godex',
'Exch', 'Exch',
'CoinCraddle' 'CoinCraddle',
'Alfacash',
'LocalMonero',
'XChange',
'NeroSwap',
'Changee',
'BitcoinVN',
'EasyBit',
'WizardSwap',
'Quantex',
'SwapSpace',
]; ];
static const List<CryptoCurrency> _notSupported = [ static const List<CryptoCurrency> _notSupported = [
@ -144,8 +155,11 @@ class TrocadorExchangeProvider extends ExchangeProvider {
} }
@override @override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async { Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
final params = { final params = {
'api_key': apiKey, 'api_key': apiKey,
'ticker_from': _normalizeCurrency(request.fromCurrency), 'ticker_from': _normalizeCurrency(request.fromCurrency),
@ -172,7 +186,6 @@ class TrocadorExchangeProvider extends ExchangeProvider {
params['id'] = _lastUsedRateId; params['id'] = _lastUsedRateId;
} }
String firstAvailableProvider = ''; String firstAvailableProvider = '';
for (var provider in _provider) { for (var provider in _provider) {
@ -225,7 +238,8 @@ class TrocadorExchangeProvider extends ExchangeProvider {
providerName: providerName, providerName: providerName,
createdAt: DateTime.tryParse(date)?.toLocal(), createdAt: DateTime.tryParse(date)?.toLocal(),
amount: responseJSON['amount_from']?.toString() ?? request.fromAmount, amount: responseJSON['amount_from']?.toString() ?? request.fromAmount,
payoutAddress: payoutAddress); payoutAddress: payoutAddress,
isSendAll: isSendAll);
} }
@override @override

View file

@ -27,7 +27,11 @@ class Trade extends HiveObject {
this.password, this.password,
this.providerId, this.providerId,
this.providerName, this.providerName,
this.fromWalletAddress this.fromWalletAddress,
this.memo,
this.txId,
this.isRefund,
this.isSendAll,
}) { }) {
if (provider != null) providerRaw = provider.raw; if (provider != null) providerRaw = provider.raw;
@ -105,6 +109,18 @@ class Trade extends HiveObject {
@HiveField(17) @HiveField(17)
String? fromWalletAddress; String? fromWalletAddress;
@HiveField(18)
String? memo;
@HiveField(19)
String? txId;
@HiveField(20)
bool? isRefund;
@HiveField(21)
bool? isSendAll;
static Trade fromMap(Map<String, Object?> map) { static Trade fromMap(Map<String, Object?> map) {
return Trade( return Trade(
id: map['id'] as String, id: map['id'] as String,
@ -115,8 +131,11 @@ class Trade extends HiveObject {
map['date'] != null ? DateTime.fromMillisecondsSinceEpoch(map['date'] as int) : null, map['date'] != null ? DateTime.fromMillisecondsSinceEpoch(map['date'] as int) : null,
amount: map['amount'] as String, amount: map['amount'] as String,
walletId: map['wallet_id'] as String, walletId: map['wallet_id'] as String,
fromWalletAddress: map['from_wallet_address'] as String? fromWalletAddress: map['from_wallet_address'] as String?,
); memo: map['memo'] as String?,
txId: map['tx_id'] as String?,
isRefund: map['isRefund'] as bool?,
isSendAll: map['isSendAll'] as bool?);
} }
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
@ -128,7 +147,11 @@ class Trade extends HiveObject {
'date': createdAt != null ? createdAt!.millisecondsSinceEpoch : null, 'date': createdAt != null ? createdAt!.millisecondsSinceEpoch : null,
'amount': amount, 'amount': amount,
'wallet_id': walletId, 'wallet_id': walletId,
'from_wallet_address': fromWalletAddress 'from_wallet_address': fromWalletAddress,
'memo': memo,
'tx_id': txId,
'isRefund': isRefund,
'isSendAll': isSendAll,
}; };
} }

View file

@ -41,6 +41,8 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
static const success = TradeState(raw: 'success', title: 'Success'); static const success = TradeState(raw: 'success', title: 'Success');
static TradeState deserialize({required String raw}) { static TradeState deserialize({required String raw}) {
switch (raw) { switch (raw) {
case 'NOT_FOUND':
return notFound;
case 'pending': case 'pending':
return pending; return pending;
case 'confirming': case 'confirming':
@ -98,6 +100,7 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
case 'sending': case 'sending':
return sending; return sending;
case 'success': case 'success':
case 'done':
return success; return success;
default: default:
throw Exception('Unexpected token: $raw in TradeState deserialize'); throw Exception('Unexpected token: $raw in TradeState deserialize');

View file

@ -153,25 +153,26 @@ Future<void> initializeAppConfigs() async {
final unspentCoinsInfoSource = await CakeHive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName); final unspentCoinsInfoSource = await CakeHive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName);
await initialSetup( await initialSetup(
sharedPreferences: await SharedPreferences.getInstance(), sharedPreferences: await SharedPreferences.getInstance(),
nodes: nodes, nodes: nodes,
powNodes: powNodes, powNodes: powNodes,
walletInfoSource: walletInfoSource, walletInfoSource: walletInfoSource,
contactSource: contacts, contactSource: contacts,
tradesSource: trades, tradesSource: trades,
ordersSource: orders, ordersSource: orders,
unspentCoinsInfoSource: unspentCoinsInfoSource, unspentCoinsInfoSource: unspentCoinsInfoSource,
// fiatConvertationService: fiatConvertationService, // fiatConvertationService: fiatConvertationService,
templates: templates, templates: templates,
exchangeTemplates: exchangeTemplates, exchangeTemplates: exchangeTemplates,
transactionDescriptions: transactionDescriptions, transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage, secureStorage: secureStorage,
anonpayInvoiceInfo: anonpayInvoiceInfo, anonpayInvoiceInfo: anonpayInvoiceInfo,
initialMigrationVersion: 31); initialMigrationVersion: 32,
);
} }
Future<void> initialSetup( Future<void> initialSetup(
{required SharedPreferences sharedPreferences, {required SharedPreferences sharedPreferences,
required Box<Node> nodes, required Box<Node> nodes,
required Box<Node> powNodes, required Box<Node> powNodes,
required Box<WalletInfo> walletInfoSource, required Box<WalletInfo> walletInfoSource,

View file

@ -179,6 +179,16 @@ class CWNano extends Nano {
String getRepresentative(Object wallet) { String getRepresentative(Object wallet) {
return (wallet as NanoWallet).representative; return (wallet as NanoWallet).representative;
} }
@override
Future<List<N2Node>> getN2Reps(Object wallet) async {
return (wallet as NanoWallet).getN2Reps();
}
@override
bool isRepOk(Object wallet) {
return (wallet as NanoWallet).isRepOk;
}
} }
class CWNanoUtil extends NanoUtil { class CWNanoUtil extends NanoUtil {

View file

@ -129,7 +129,7 @@ class CWPolygon extends Polygon {
@override @override
Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) async { Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) async {
final polygonWallet = wallet as PolygonWallet; final polygonWallet = wallet as PolygonWallet;
return await polygonWallet.getErc20Token(contractAddress); return await polygonWallet.getErc20Token(contractAddress, 'polygon');
} }
@override @override

View file

@ -16,6 +16,7 @@ bool isWalletConnectCompatibleChain(WalletType walletType) {
switch (walletType) { switch (walletType) {
case WalletType.polygon: case WalletType.polygon:
case WalletType.ethereum: case WalletType.ethereum:
case WalletType.solana:
return true; return true;
default: default:
return false; return false;

View file

@ -54,6 +54,7 @@ import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart
import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/support/support_page.dart';
import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart';
import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart'; import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
@ -253,6 +254,12 @@ Route<dynamic> createRoute(RouteSettings settings) {
builder: (_) => builder: (_) =>
getIt.get<TransactionDetailsPage>(param1: settings.arguments as TransactionInfo)); getIt.get<TransactionDetailsPage>(param1: settings.arguments as TransactionInfo));
case Routes.bumpFeePage:
return CupertinoPageRoute<void>(
fullscreenDialog: true,
builder: (_) =>
getIt.get<RBFDetailsPage>(param1: settings.arguments as TransactionInfo));
case Routes.newSubaddress: case Routes.newSubaddress:
return CupertinoPageRoute<void>( return CupertinoPageRoute<void>(
builder: (_) => getIt.get<AddressEditOrCreatePage>(param1: settings.arguments)); builder: (_) => getIt.get<AddressEditOrCreatePage>(param1: settings.arguments));

View file

@ -12,6 +12,7 @@ class Routes {
static const dashboard = '/dashboard'; static const dashboard = '/dashboard';
static const send = '/send'; static const send = '/send';
static const transactionDetails = '/transaction_info'; static const transactionDetails = '/transaction_info';
static const bumpFeePage = '/bump_fee_page';
static const receive = '/receive'; static const receive = '/receive';
static const newSubaddress = '/new_subaddress'; static const newSubaddress = '/new_subaddress';
static const walletEdit = '/walletEdit'; static const walletEdit = '/walletEdit';

View file

@ -74,8 +74,23 @@ class CWSolana extends Solana {
} }
@override @override
Future<void> addSPLToken(WalletBase wallet, CryptoCurrency token) async => Future<void> addSPLToken(
await (wallet as SolanaWallet).addSPLToken(token as SPLToken); WalletBase wallet,
CryptoCurrency token,
String contractAddress,
) async {
final splToken = SPLToken(
name: token.name,
symbol: token.title,
mintAddress: contractAddress,
decimal: token.decimals,
mint: token.name.toUpperCase(),
enabled: token.enabled,
iconPath: token.iconPath,
);
await (wallet as SolanaWallet).addSPLToken(splToken);
}
@override @override
Future<void> deleteSPLToken(WalletBase wallet, CryptoCurrency token) async => Future<void> deleteSPLToken(WalletBase wallet, CryptoCurrency token) async =>
@ -115,4 +130,9 @@ class CWSolana extends Solana {
return null; return null;
} }
@override
double? getEstimateFees(WalletBase wallet) {
return (wallet as SolanaWallet).estimatedFee;
}
} }

View file

@ -1,6 +1,7 @@
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/option_tile.dart'; import 'package:cake_wallet/src/widgets/option_tile.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
@ -25,45 +26,46 @@ class BuySellOptionsPage extends BasePage {
? dashboardViewModel.availableBuyProviders ? dashboardViewModel.availableBuyProviders
: dashboardViewModel.availableSellProviders; : dashboardViewModel.availableSellProviders;
return Container( return ScrollableWithBottomSection(
child: Center( content: Container(
child: ConstrainedBox( child: Center(
constraints: BoxConstraints(maxWidth: 330), child: ConstrainedBox(
child: Column( constraints: BoxConstraints(maxWidth: 330),
children: [ child: Column(
...availableProviders.map((provider) { children: [
final icon = Image.asset( ...availableProviders.map((provider) {
isLightMode ? provider.lightIcon : provider.darkIcon, final icon = Image.asset(
height: 40, isLightMode ? provider.lightIcon : provider.darkIcon,
width: 40, height: 40,
); width: 40,
);
return Padding( return Padding(
padding: EdgeInsets.only(top: 24), padding: EdgeInsets.only(top: 24),
child: OptionTile( child: OptionTile(
image: icon, image: icon,
title: provider.toString(), title: provider.toString(),
description: provider.providerDescription, description: provider.providerDescription,
onPressed: () => provider.launchProvider(context, isBuyAction), onPressed: () => provider.launchProvider(context, isBuyAction),
), ),
); );
}).toList(), }).toList(),
Spacer(), ],
Padding( ),
padding: EdgeInsets.fromLTRB(24, 24, 24, 32), ),
child: Text( ),
isBuyAction ),
? S.of(context).select_buy_provider_notice bottomSection: Padding(
: S.of(context).select_sell_provider_notice, padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
textAlign: TextAlign.center, child: Text(
style: TextStyle( isBuyAction
fontSize: 14, ? S.of(context).select_buy_provider_notice
fontWeight: FontWeight.normal, : S.of(context).select_sell_provider_notice,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor, textAlign: TextAlign.center,
), style: TextStyle(
), fontSize: 14,
), fontWeight: FontWeight.normal,
], color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
), ),
), ),
), ),

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