Merge branch 'main' into autogen-address
BIN
.github/assets/Logo_CakeWallet.png
vendored
Normal file
After Width: | Height: | Size: 156 KiB |
48
.github/assets/NOTICE.txt
vendored
Normal 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
|
@ -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="<Group>">
|
||||
<g id="_Group_2" data-name="<Group>">
|
||||
<g id="_Group_3" data-name="<Group>">
|
||||
<path id="_Path_" data-name="<Path>" 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="<Path>" 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="<Group>">
|
||||
<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
After Width: | Height: | Size: 72 KiB |
BIN
.github/assets/f-droid-badge.png
vendored
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
.github/assets/google-play-badge.png
vendored
Normal file
After Width: | Height: | Size: 22 KiB |
1071
.github/assets/linux-badge.svg
vendored
Executable file
After Width: | Height: | Size: 67 KiB |
51
.github/assets/mac-store-badge.svg
vendored
Executable 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="<Group>">
|
||||
<g id="_Group_2" data-name="<Group>">
|
||||
<g id="_Group_3" data-name="<Group>">
|
||||
<g id="_Group_4" data-name="<Group>">
|
||||
<path id="_Path_" data-name="<Path>" 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="<Path>" 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="<Group>">
|
||||
<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 |
2
.github/workflows/pr_test_build.yml
vendored
|
@ -139,7 +139,9 @@ jobs:
|
|||
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 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 moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/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 robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart
|
||||
|
|
38
README.md
|
@ -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
|
||||
* App Store (iOS / MacOS): https://cakewallet.com/ios
|
||||
* Google Play: https://cakewallet.com/gp
|
||||
* F-Droid: https://fdroid.cakelabs.com
|
||||
* APK: https://github.com/cake-tech/cake_wallet/releases
|
||||
* Linux: https://github.com/cake-tech/cake_wallet/releases
|
||||
![devices](.github/assets/devices.png)
|
||||
|
||||
<div align="center">
|
||||
|
||||
[<img height="42" src=".github/assets/app-store-badge.svg">](https://apps.apple.com/us/app/cake-wallet/id1334702542?platform=iphone)
|
||||
[<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
|
||||
|
||||
|
|
BIN
assets/images/thorchain.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
|
@ -1,4 +1,2 @@
|
|||
Monero enhancements
|
||||
In-App live status page for the app services
|
||||
Add Exolix exchange provider
|
||||
Bug fixes and enhancements
|
||||
Exchange flow enhancements and fixes
|
||||
Generic enhancements and bug fixes
|
|
@ -1 +1,6 @@
|
|||
Bug fixes and enhancements
|
||||
Exchange flow enhancements and fixes
|
||||
Add MoonPay to Buy options
|
||||
Add THORChain to Exchange providers
|
||||
Improve Bitcoin fee calculations
|
||||
Fixes and enhancements for Solana
|
||||
Generic enhancements and bug fixes
|
|
@ -1,4 +1,8 @@
|
|||
class BitcoinCommitTransactionException implements Exception {
|
||||
String errorMessage;
|
||||
BitcoinCommitTransactionException(this.errorMessage);
|
||||
|
||||
@override
|
||||
String toString() => 'Transaction commit is failed.';
|
||||
String toString() => errorMessage;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
|||
import 'package:cw_core/output_info.dart';
|
||||
|
||||
class BitcoinTransactionCredentials {
|
||||
BitcoinTransactionCredentials(this.outputs, {required this.priority, this.feeRate});
|
||||
BitcoinTransactionCredentials(this.outputs,
|
||||
{required this.priority, this.feeRate});
|
||||
|
||||
final List<OutputInfo> outputs;
|
||||
final BitcoinTransactionPriority? priority;
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
class BitcoinTransactionNoInputsException implements Exception {
|
||||
@override
|
||||
String toString() => 'Not enough inputs available. Please select more under Coin Control';
|
||||
}
|
|
@ -4,13 +4,15 @@ class BitcoinTransactionPriority extends TransactionPriority {
|
|||
const BitcoinTransactionPriority({required String title, required int 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 =
|
||||
BitcoinTransactionPriority(title: 'Slow', raw: 0);
|
||||
static const BitcoinTransactionPriority medium =
|
||||
BitcoinTransactionPriority(title: 'Medium', raw: 1);
|
||||
static const BitcoinTransactionPriority fast =
|
||||
BitcoinTransactionPriority(title: 'Fast', raw: 2);
|
||||
static const BitcoinTransactionPriority custom =
|
||||
BitcoinTransactionPriority(title: 'Custom', raw: 3);
|
||||
|
||||
static BitcoinTransactionPriority deserialize({required int raw}) {
|
||||
switch (raw) {
|
||||
|
@ -20,6 +22,8 @@ class BitcoinTransactionPriority extends TransactionPriority {
|
|||
return medium;
|
||||
case 2:
|
||||
return fast;
|
||||
case 3:
|
||||
return custom;
|
||||
default:
|
||||
throw Exception('Unexpected token: $raw for BitcoinTransactionPriority deserialize');
|
||||
}
|
||||
|
@ -39,7 +43,10 @@ class BitcoinTransactionPriority extends TransactionPriority {
|
|||
label = 'Medium'; // S.current.transaction_priority_medium;
|
||||
break;
|
||||
case BitcoinTransactionPriority.fast:
|
||||
label = 'Fast'; // S.current.transaction_priority_fast;
|
||||
label = 'Fast';
|
||||
break; // S.current.transaction_priority_fast;
|
||||
case BitcoinTransactionPriority.custom:
|
||||
label = 'Custom';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -48,7 +55,10 @@ class BitcoinTransactionPriority extends TransactionPriority {
|
|||
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 {
|
||||
|
|
|
@ -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.';
|
||||
}
|
|
@ -7,10 +7,9 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart';
|
|||
import 'package:cw_bitcoin/script_hash.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
String jsonrpcparams(List<Object> params) {
|
||||
final _params = params?.map((val) => '"${val.toString()}"')?.join(',');
|
||||
final _params = params.map((val) => '"${val.toString()}"').join(',');
|
||||
return '[$_params]';
|
||||
}
|
||||
|
||||
|
@ -34,6 +33,7 @@ class ElectrumClient {
|
|||
: _id = 0,
|
||||
_isConnected = false,
|
||||
_tasks = {},
|
||||
_errors = {},
|
||||
unterminatedString = '';
|
||||
|
||||
static const connectionTimeout = Duration(seconds: 5);
|
||||
|
@ -44,6 +44,7 @@ class ElectrumClient {
|
|||
void Function(bool)? onConnectionStatusChange;
|
||||
int _id;
|
||||
final Map<String, SocketTask> _tasks;
|
||||
final Map<String, String> _errors;
|
||||
bool _isConnected;
|
||||
Timer? _aliveTimer;
|
||||
String unterminatedString;
|
||||
|
@ -243,30 +244,20 @@ class ElectrumClient {
|
|||
});
|
||||
|
||||
Future<String> broadcastTransaction(
|
||||
{required String transactionRaw, BasedUtxoNetwork? network}) async {
|
||||
if (network == BitcoinNetwork.testnet) {
|
||||
return http
|
||||
.post(Uri(scheme: 'https', host: 'blockstream.info', path: '/testnet/api/tx'),
|
||||
headers: <String, String>{'Content-Type': 'application/json; charset=utf-8'},
|
||||
body: transactionRaw)
|
||||
.then((http.Response response) {
|
||||
if (response.statusCode == 200) {
|
||||
return response.body;
|
||||
{required String transactionRaw,
|
||||
BasedUtxoNetwork? network,
|
||||
Function(int)? idCallback}) async =>
|
||||
call(
|
||||
method: 'blockchain.transaction.broadcast',
|
||||
params: [transactionRaw],
|
||||
idCallback: idCallback)
|
||||
.then((dynamic result) {
|
||||
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 =>
|
||||
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>();
|
||||
_id += 1;
|
||||
final id = _id;
|
||||
idCallback?.call(id);
|
||||
_registryTask(id, completer);
|
||||
socket!.write(jsonrpc(method: method, id: id, params: params));
|
||||
|
||||
|
@ -456,6 +449,23 @@ class ElectrumClient {
|
|||
final id = response['id'] as String?;
|
||||
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) {
|
||||
_methodHandler(method: method, request: response);
|
||||
return;
|
||||
|
@ -465,6 +475,8 @@ class ElectrumClient {
|
|||
_finish(id, result);
|
||||
}
|
||||
}
|
||||
|
||||
String getErrorMessage(int id) => _errors[id.toString()] ?? '';
|
||||
}
|
||||
|
||||
// FIXME: move me
|
||||
|
|
|
@ -11,12 +11,11 @@ import 'package:cw_core/wallet_type.dart';
|
|||
|
||||
class ElectrumTransactionBundle {
|
||||
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 List<BtcTransaction> ins;
|
||||
final int? time;
|
||||
final int confirmations;
|
||||
final int height;
|
||||
}
|
||||
|
||||
class ElectrumTransactionInfo extends TransactionInfo {
|
||||
|
@ -25,6 +24,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
required int height,
|
||||
required int amount,
|
||||
int? fee,
|
||||
List<String>? inputAddresses,
|
||||
List<String>? outputAddresses,
|
||||
required TransactionDirection direction,
|
||||
required bool isPending,
|
||||
required DateTime date,
|
||||
|
@ -32,6 +33,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
this.id = id;
|
||||
this.height = height;
|
||||
this.amount = amount;
|
||||
this.inputAddresses = inputAddresses;
|
||||
this.outputAddresses = outputAddresses;
|
||||
this.fee = fee;
|
||||
this.direction = direction;
|
||||
this.date = date;
|
||||
|
@ -100,6 +103,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
var amount = 0;
|
||||
var inputAmount = 0;
|
||||
var totalOutAmount = 0;
|
||||
List<String> inputAddresses = [];
|
||||
List<String> outputAddresses = [];
|
||||
|
||||
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
|
||||
final input = bundle.originalTransaction.inputs[i];
|
||||
|
@ -108,6 +113,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
inputAmount += outTransaction.amount.toInt();
|
||||
if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) {
|
||||
direction = TransactionDirection.outgoing;
|
||||
inputAddresses.add(addressFromOutputScript(outTransaction.scriptPubKey, network));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,6 +121,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
for (final out in bundle.originalTransaction.outputs) {
|
||||
totalOutAmount += out.amount.toInt();
|
||||
final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network));
|
||||
outputAddresses.add(addressFromOutputScript(out.scriptPubKey, network));
|
||||
|
||||
if (addressExists) {
|
||||
receivedAmounts.add(out.amount.toInt());
|
||||
|
@ -137,6 +144,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
id: bundle.originalTransaction.txId(),
|
||||
height: height,
|
||||
isPending: bundle.confirmations == 0,
|
||||
inputAddresses: inputAddresses,
|
||||
outputAddresses: outputAddresses,
|
||||
fee: fee,
|
||||
direction: direction,
|
||||
amount: amount,
|
||||
|
@ -187,6 +196,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
direction: parseTransactionDirectionFromInt(data['direction'] as int),
|
||||
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
|
||||
isPending: data['isPending'] as bool,
|
||||
inputAddresses: data['inputAddresses'] as List<String>,
|
||||
outputAddresses: data['outputAddresses'] as List<String>,
|
||||
confirmations: data['confirmations'] as int);
|
||||
}
|
||||
|
||||
|
@ -218,6 +229,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
direction: direction,
|
||||
date: date,
|
||||
isPending: isPending,
|
||||
inputAddresses: inputAddresses,
|
||||
outputAddresses: outputAddresses,
|
||||
confirmations: info.confirmations);
|
||||
}
|
||||
|
||||
|
@ -231,6 +244,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
m['isPending'] = isPending;
|
||||
m['confirmations'] = confirmations;
|
||||
m['fee'] = fee;
|
||||
m['inputAddresses'] = inputAddresses;
|
||||
m['outputAddresses'] = outputAddresses;
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@ import 'package:bitcoin_base/bitcoin_base.dart';
|
|||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin_base;
|
||||
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_amount_format.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_wrong_balance_exception.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_wallet_keys.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_info.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/pending_bitcoin_transaction.dart';
|
||||
import 'package:cw_bitcoin/script_hash.dart';
|
||||
|
@ -188,26 +189,25 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
Future<EstimatedTxResult> estimateTxFeeAndInputsToUse(
|
||||
int credentialsAmount,
|
||||
bool sendAll,
|
||||
List<BitcoinBaseAddress> outputAddresses,
|
||||
List<BitcoinOutput> outputs,
|
||||
int? feeRate,
|
||||
BitcoinTransactionPriority? priority,
|
||||
{int? inputsCount}) async {
|
||||
int get _dustAmount => 546;
|
||||
|
||||
bool _isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet;
|
||||
|
||||
Future<EstimatedTxResult> estimateSendAllTx(
|
||||
List<BitcoinOutput> outputs,
|
||||
int feeRate, {
|
||||
String? memo,
|
||||
int credentialsAmount = 0,
|
||||
}) async {
|
||||
final utxos = <UtxoWithAddress>[];
|
||||
List<ECPrivate> privateKeys = [];
|
||||
|
||||
var leftAmount = credentialsAmount;
|
||||
var allInputsAmount = 0;
|
||||
int allInputsAmount = 0;
|
||||
|
||||
for (int i = 0; i < unspentCoins.length; i++) {
|
||||
final utx = unspentCoins[i];
|
||||
|
||||
if (utx.isSending) {
|
||||
allInputsAmount += utx.value;
|
||||
leftAmount = leftAmount - utx.value;
|
||||
|
||||
final address = addressTypeFromStr(utx.address, network);
|
||||
final privkey = generateECPrivate(
|
||||
|
@ -225,15 +225,12 @@ abstract class ElectrumWalletBase
|
|||
vout: utx.vout,
|
||||
scriptType: _getScriptType(address),
|
||||
),
|
||||
ownerDetails:
|
||||
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address),
|
||||
ownerDetails: UtxoAddressDetails(
|
||||
publicKey: privkey.getPublic().toHex(),
|
||||
address: address,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
bool amountIsAcquired = !sendAll && leftAmount <= 0;
|
||||
if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,120 +238,314 @@ abstract class ElectrumWalletBase
|
|||
throw BitcoinTransactionNoInputsException();
|
||||
}
|
||||
|
||||
var changeValue = allInputsAmount - credentialsAmount;
|
||||
|
||||
if (!sendAll) {
|
||||
if (changeValue > 0) {
|
||||
final changeAddress = await walletAddresses.getChangeAddress();
|
||||
final address = addressTypeFromStr(changeAddress, network);
|
||||
outputAddresses.add(address);
|
||||
outputs.add(BitcoinOutput(address: address, value: BigInt.from(changeValue)));
|
||||
}
|
||||
int estimatedSize;
|
||||
if (network is BitcoinCashNetwork) {
|
||||
estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
|
||||
utxos: utxos,
|
||||
outputs: outputs,
|
||||
network: network as BitcoinCashNetwork,
|
||||
memo: memo,
|
||||
);
|
||||
} else {
|
||||
estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
|
||||
utxos: utxos,
|
||||
outputs: outputs,
|
||||
network: network,
|
||||
memo: memo,
|
||||
);
|
||||
}
|
||||
|
||||
final estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
|
||||
utxos: utxos, outputs: outputs, network: network);
|
||||
|
||||
int fee = feeRate != null
|
||||
? feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize)
|
||||
: feeAmountForPriority(priority!, 0, 0, size: estimatedSize);
|
||||
int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
|
||||
|
||||
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;
|
||||
if (!sendAll) {
|
||||
if (changeValue > fee) {
|
||||
// Here, lastOutput is change, deduct the fee from it
|
||||
outputs[outputs.length - 1] =
|
||||
BitcoinOutput(address: lastOutput.address, value: lastOutput.value - BigInt.from(fee));
|
||||
// Attempting to send less than the dust limit
|
||||
if (_isBelowDust(amount)) {
|
||||
throw BitcoinTransactionNoDustException();
|
||||
}
|
||||
|
||||
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 {
|
||||
// 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
|
||||
amount = allInputsAmount - fee;
|
||||
estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
|
||||
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] =
|
||||
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;
|
||||
|
||||
if (totalAmount > balance[currency]!.confirmed) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
throw BitcoinTransactionWrongBalanceException();
|
||||
}
|
||||
|
||||
if (totalAmount > allInputsAmount) {
|
||||
if (unspentCoins.where((utx) => utx.isSending).length == utxos.length) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
if (spendingAllCoins) {
|
||||
throw BitcoinTransactionWrongBalanceException();
|
||||
} else {
|
||||
if (changeValue > fee) {
|
||||
outputAddresses.removeLast();
|
||||
if (amountLeftForChangeAndFee > fee) {
|
||||
outputs.removeLast();
|
||||
}
|
||||
|
||||
return estimateTxFeeAndInputsToUse(
|
||||
credentialsAmount, sendAll, outputAddresses, outputs, feeRate, priority,
|
||||
inputsCount: utxos.length + 1);
|
||||
return estimateTxForAmount(
|
||||
credentialsAmount,
|
||||
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
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
try {
|
||||
final outputs = <BitcoinOutput>[];
|
||||
final outputAddresses = <BitcoinBaseAddress>[];
|
||||
final transactionCredentials = credentials as BitcoinTransactionCredentials;
|
||||
final hasMultiDestination = transactionCredentials.outputs.length > 1;
|
||||
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) {
|
||||
final outputAddress = out.isParsedAddress ? out.extractedAddress! : out.address;
|
||||
final address = addressTypeFromStr(outputAddress, network);
|
||||
final outputAmount = out.formattedCryptoAmount!;
|
||||
|
||||
outputAddresses.add(address);
|
||||
if (!sendAll && _isBelowDust(outputAmount)) {
|
||||
throw BitcoinTransactionNoDustException();
|
||||
}
|
||||
|
||||
if (hasMultiDestination) {
|
||||
if (out.sendAll || out.formattedCryptoAmount! <= 0) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
if (out.sendAll) {
|
||||
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 {
|
||||
if (!sendAll) {
|
||||
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)));
|
||||
}
|
||||
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
|
||||
}
|
||||
}
|
||||
|
||||
final estimatedTx = await estimateTxFeeAndInputsToUse(
|
||||
credentialsAmount,
|
||||
sendAll,
|
||||
outputAddresses,
|
||||
outputs,
|
||||
transactionCredentials.feeRate,
|
||||
transactionCredentials.priority,
|
||||
);
|
||||
final feeRateInt = transactionCredentials.feeRate != null
|
||||
? 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,
|
||||
outputs: outputs,
|
||||
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 key = estimatedTx.privateKeys
|
||||
|
@ -365,18 +556,25 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
if (utxo.utxo.isP2tr()) {
|
||||
hasTaprootInputs = true;
|
||||
return key.signTapRoot(txDigest, sighash: sighash);
|
||||
} else {
|
||||
return key.signInput(txDigest, sigHash: sighash);
|
||||
}
|
||||
});
|
||||
|
||||
return PendingBitcoinTransaction(transaction, type,
|
||||
electrumClient: electrumClient,
|
||||
amount: estimatedTx.amount,
|
||||
fee: estimatedTx.fee,
|
||||
network: network)
|
||||
..addListener((transaction) async {
|
||||
return PendingBitcoinTransaction(
|
||||
transaction,
|
||||
type,
|
||||
electrumClient: electrumClient,
|
||||
amount: estimatedTx.amount,
|
||||
fee: estimatedTx.fee,
|
||||
feeRate: feeRateInt.toString(),
|
||||
network: network,
|
||||
hasChange: estimatedTx.hasChange,
|
||||
isSendAll: estimatedTx.isSendAll,
|
||||
hasTaprootInputs: hasTaprootInputs,
|
||||
)..addListener((transaction) async {
|
||||
transactionHistory.addOne(transaction);
|
||||
await updateBalance();
|
||||
});
|
||||
|
@ -408,7 +606,7 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount,
|
||||
int feeAmountForPriority(TransactionPriority priority, int inputsCount, int outputsCount,
|
||||
{int? size}) =>
|
||||
feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount));
|
||||
|
||||
|
@ -597,8 +795,180 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
Future<ElectrumTransactionBundle> getTransactionExpanded(
|
||||
{required String hash, required int height}) async {
|
||||
Future<bool> canReplaceByFee(String hash) 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;
|
||||
int? time;
|
||||
int confirmations = 0;
|
||||
|
@ -629,8 +999,12 @@ abstract class ElectrumWalletBase
|
|||
ins.add(tx);
|
||||
}
|
||||
|
||||
return ElectrumTransactionBundle(original,
|
||||
ins: ins, time: time, confirmations: confirmations, height: height);
|
||||
return ElectrumTransactionBundle(
|
||||
original,
|
||||
ins: ins,
|
||||
time: time,
|
||||
confirmations: confirmations,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ElectrumTransactionInfo?> fetchTransactionInfo(
|
||||
|
@ -640,7 +1014,7 @@ abstract class ElectrumWalletBase
|
|||
bool? retryOnFailure}) async {
|
||||
try {
|
||||
return ElectrumTransactionInfo.fromElectrumBundle(
|
||||
await getTransactionExpanded(hash: hash, height: height), walletInfo.type, network,
|
||||
await getTransactionExpanded(hash: hash), walletInfo.type, network,
|
||||
addresses: myAddresses, height: height);
|
||||
} catch (e) {
|
||||
if (e is FormatException && retryOnFailure == true) {
|
||||
|
@ -888,16 +1262,35 @@ class EstimateTxParams {
|
|||
}
|
||||
|
||||
class EstimatedTxResult {
|
||||
EstimatedTxResult(
|
||||
{required this.utxos, required this.privateKeys, required this.fee, required this.amount});
|
||||
EstimatedTxResult({
|
||||
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<ECPrivate> privateKeys;
|
||||
final int fee;
|
||||
final int amount;
|
||||
final bool hasChange;
|
||||
final bool isSendAll;
|
||||
final String? memo;
|
||||
}
|
||||
|
||||
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)) {
|
||||
return P2pkhAddress.fromAddress(address: address, network: network);
|
||||
} else if (P2shAddress.regex.hasMatch(address)) {
|
||||
|
|
|
@ -77,7 +77,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
String get address {
|
||||
String receiveAddress;
|
||||
|
||||
final typeMatchingReceiveAddresses = receiveAddresses.where(_isAddressPageTypeMatch);
|
||||
final typeMatchingReceiveAddresses =
|
||||
receiveAddresses.where(_isAddressPageTypeMatch).where((addr) => !addr.isUsed);
|
||||
|
||||
if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) ||
|
||||
typeMatchingReceiveAddresses.isEmpty) {
|
||||
|
@ -220,8 +221,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
Future<void> updateAddressesInBox() async {
|
||||
try {
|
||||
addressesMap.clear();
|
||||
addressesMap[address] = '';
|
||||
|
||||
allAddressesMap.clear();
|
||||
_addresses.forEach((addressRecord) {
|
||||
addressesMap[addressRecord.address] = addressRecord.name;
|
||||
allAddressesMap[addressRecord.address] = addressRecord.name;
|
||||
});
|
||||
await saveAddressesInBox();
|
||||
} catch (e) {
|
||||
|
|
27
cw_bitcoin/lib/exceptions.dart
Normal 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 {}
|
|
@ -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:cw_core/pending_transaction.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';
|
||||
|
||||
class PendingBitcoinTransaction with PendingTransaction {
|
||||
PendingBitcoinTransaction(this._tx, this.type,
|
||||
{required this.electrumClient, required this.amount, required this.fee, this.network})
|
||||
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
|
||||
PendingBitcoinTransaction(
|
||||
this._tx,
|
||||
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 BtcTransaction _tx;
|
||||
final ElectrumClient electrumClient;
|
||||
final int amount;
|
||||
final int fee;
|
||||
final String feeRate;
|
||||
final BasedUtxoNetwork? network;
|
||||
final bool hasChange;
|
||||
final bool isSendAll;
|
||||
final bool hasTaprootInputs;
|
||||
|
||||
@override
|
||||
String get id => _tx.txId();
|
||||
|
@ -31,14 +44,37 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
@override
|
||||
String get feeFormatted => bitcoinAmountToString(amount: fee);
|
||||
|
||||
@override
|
||||
int? get outputCount => _tx.outputs.length;
|
||||
|
||||
final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
|
||||
|
||||
@override
|
||||
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) {
|
||||
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()));
|
||||
|
|
|
@ -70,8 +70,8 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: master
|
||||
resolved-ref: ea65073efbaf395a5557e8cd7bd72f195cd7eb11
|
||||
ref: Add-Support-For-OP-Return-data
|
||||
resolved-ref: "57b78afb85bd2c30d3cdb9f7884f3878a62be442"
|
||||
url: "https://github.com/cake-tech/bitbox-flutter.git"
|
||||
source: git
|
||||
version: "1.0.1"
|
||||
|
|
|
@ -26,7 +26,7 @@ dependencies:
|
|||
bitbox:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitbox-flutter.git
|
||||
ref: master
|
||||
ref: Add-Support-For-OP-Return-data
|
||||
rxdart: ^0.27.5
|
||||
unorm_dart: ^0.2.0
|
||||
cryptography: ^2.0.5
|
||||
|
|
|
@ -4,15 +4,10 @@ import 'package:bitbox/bitbox.dart' as bitbox;
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
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_wrong_balance_exception.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
||||
import 'package:cw_bitcoin/electrum_balance.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet.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/transaction_priority.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
|
@ -130,184 +125,9 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
);
|
||||
}
|
||||
|
||||
@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.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 inputsCount = 0;
|
||||
int totalValue = 0;
|
||||
|
|
|
@ -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:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_bitcoin/electrum.dart';
|
||||
|
@ -11,7 +11,9 @@ class PendingBitcoinCashTransaction with PendingTransaction {
|
|||
PendingBitcoinCashTransaction(this._tx, this.type,
|
||||
{required this.electrumClient,
|
||||
required this.amount,
|
||||
required this.fee})
|
||||
required this.fee,
|
||||
required this.hasChange,
|
||||
required this.isSendAll})
|
||||
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
|
||||
|
||||
final WalletType type;
|
||||
|
@ -19,6 +21,8 @@ class PendingBitcoinCashTransaction with PendingTransaction {
|
|||
final ElectrumClient electrumClient;
|
||||
final int amount;
|
||||
final int fee;
|
||||
final bool hasChange;
|
||||
final bool isSendAll;
|
||||
|
||||
@override
|
||||
String get id => _tx.getId();
|
||||
|
@ -36,18 +40,36 @@ class PendingBitcoinCashTransaction with PendingTransaction {
|
|||
|
||||
@override
|
||||
Future<void> commit() async {
|
||||
final result =
|
||||
await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex());
|
||||
int? callId;
|
||||
|
||||
final result = await electrumClient.broadcastTransaction(
|
||||
transactionRaw: hex, idCallback: (id) => callId = id);
|
||||
|
||||
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 Function(ElectrumTransactionInfo transaction) listener) =>
|
||||
void addListener(void Function(ElectrumTransactionInfo transaction) listener) =>
|
||||
_listeners.add(listener);
|
||||
|
||||
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,
|
||||
|
|
|
@ -28,7 +28,7 @@ dependencies:
|
|||
bitbox:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitbox-flutter.git
|
||||
ref: master
|
||||
ref: Add-Support-For-OP-Return-data
|
||||
bitcoin_base:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitcoin_base.git
|
||||
|
|
|
@ -38,6 +38,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
|||
CryptoCurrency.trx,
|
||||
CryptoCurrency.usdt,
|
||||
CryptoCurrency.usdterc20,
|
||||
CryptoCurrency.sol,
|
||||
CryptoCurrency.maticpoly,
|
||||
CryptoCurrency.xlm,
|
||||
CryptoCurrency.xrp,
|
||||
CryptoCurrency.xhv,
|
||||
|
@ -50,7 +52,6 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
|||
CryptoCurrency.usdttrc20,
|
||||
CryptoCurrency.hbar,
|
||||
CryptoCurrency.sc,
|
||||
CryptoCurrency.sol,
|
||||
CryptoCurrency.usdc,
|
||||
CryptoCurrency.usdcsol,
|
||||
CryptoCurrency.zaddr,
|
||||
|
@ -61,7 +62,6 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
|||
CryptoCurrency.dcr,
|
||||
CryptoCurrency.kmd,
|
||||
CryptoCurrency.mana,
|
||||
CryptoCurrency.maticpoly,
|
||||
CryptoCurrency.matic,
|
||||
CryptoCurrency.mkr,
|
||||
CryptoCurrency.near,
|
||||
|
|
30
cw_core/lib/exceptions.dart
Normal 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 {}
|
|
@ -7,7 +7,8 @@ class OutputInfo {
|
|||
this.formattedCryptoAmount,
|
||||
this.fiatAmount,
|
||||
this.note,
|
||||
this.extractedAddress,});
|
||||
this.extractedAddress,
|
||||
this.memo});
|
||||
|
||||
final String? fiatAmount;
|
||||
final String? cryptoAmount;
|
||||
|
@ -17,4 +18,5 @@ class OutputInfo {
|
|||
final bool sendAll;
|
||||
final bool isParsedAddress;
|
||||
final int? formattedCryptoAmount;
|
||||
final String? memo;
|
||||
}
|
|
@ -2,7 +2,9 @@ mixin PendingTransaction {
|
|||
String get id;
|
||||
String get amountFormatted;
|
||||
String get feeFormatted;
|
||||
String? feeRate;
|
||||
String get hex;
|
||||
int? get outputCount => null;
|
||||
|
||||
Future<void> commit();
|
||||
}
|
|
@ -16,6 +16,8 @@ abstract class TransactionInfo extends Object with Keyable {
|
|||
void changeFiatAmount(String amount);
|
||||
String? to;
|
||||
String? from;
|
||||
List<String>? inputAddresses;
|
||||
List<String>? outputAddresses;
|
||||
|
||||
@override
|
||||
dynamic get keyIndex => id;
|
||||
|
|
|
@ -3,8 +3,9 @@ import 'package:cw_core/wallet_info.dart';
|
|||
|
||||
abstract class WalletAddresses {
|
||||
WalletAddresses(this.walletInfo)
|
||||
: addressesMap = {},
|
||||
addressInfos = {};
|
||||
: addressesMap = {},
|
||||
allAddressesMap = {},
|
||||
addressInfos = {};
|
||||
|
||||
final WalletInfo walletInfo;
|
||||
|
||||
|
@ -15,6 +16,7 @@ abstract class WalletAddresses {
|
|||
set address(String address);
|
||||
|
||||
Map<String, String> addressesMap;
|
||||
Map<String, String> allAddressesMap;
|
||||
|
||||
Map<int, List<AddressInfo>> addressInfos;
|
||||
|
||||
|
@ -39,5 +41,5 @@ abstract class WalletAddresses {
|
|||
}
|
||||
}
|
||||
|
||||
bool containsAddress(String address) => addressesMap.containsKey(address);
|
||||
bool containsAddress(String address) => allAddressesMap.containsKey(address);
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
|||
|
||||
int calculateEstimatedFee(TransactionPriority priority, int? amount);
|
||||
|
||||
|
||||
// void fetchTransactionsAsync(
|
||||
// void Function(TransactionType transaction) onTransactionLoaded,
|
||||
// {void Function() onFinished});
|
||||
|
|
|
@ -41,4 +41,29 @@ class EthereumClient extends EVMChainClient {
|
|||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
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/pending_evm_chain_transaction.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:http/http.dart';
|
||||
import 'package:erc20/erc20.dart';
|
||||
import 'package:web3dart/web3dart.dart';
|
||||
import 'package:hex/hex.dart' as hex;
|
||||
|
||||
abstract class EVMChainClient {
|
||||
final httpClient = Client();
|
||||
|
@ -26,6 +29,8 @@ abstract class EVMChainClient {
|
|||
Future<List<EVMChainTransactionModel>> fetchTransactions(String address,
|
||||
{String? contractAddress});
|
||||
|
||||
Future<List<EVMChainTransactionModel>> fetchInternalTransactions(String address);
|
||||
|
||||
Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction);
|
||||
|
||||
//! Common methods across all child classes
|
||||
|
@ -79,12 +84,13 @@ abstract class EVMChainClient {
|
|||
Future<PendingEVMChainTransaction> signTransaction({
|
||||
required EthPrivateKey privateKey,
|
||||
required String toAddress,
|
||||
required String amount,
|
||||
required BigInt amount,
|
||||
required int gas,
|
||||
required EVMChainTransactionPriority priority,
|
||||
required CryptoCurrency currency,
|
||||
required int exponent,
|
||||
String? contractAddress,
|
||||
String? data,
|
||||
}) async {
|
||||
assert(currency == CryptoCurrency.eth ||
|
||||
currency == CryptoCurrency.maticpoly ||
|
||||
|
@ -99,7 +105,8 @@ abstract class EVMChainClient {
|
|||
from: privateKey.address,
|
||||
to: EthereumAddress.fromHex(toAddress),
|
||||
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 =
|
||||
|
@ -119,7 +126,7 @@ abstract class EVMChainClient {
|
|||
_sendTransaction = () async {
|
||||
await erc20.transfer(
|
||||
EthereumAddress.fromHex(toAddress),
|
||||
BigInt.parse(amount),
|
||||
amount,
|
||||
credentials: privateKey,
|
||||
transaction: transaction,
|
||||
);
|
||||
|
@ -128,7 +135,7 @@ abstract class EVMChainClient {
|
|||
|
||||
return PendingEVMChainTransaction(
|
||||
signedTransaction: signedTransaction,
|
||||
amount: amount,
|
||||
amount: amount.toString(),
|
||||
fee: BigInt.from(gas) * (await price).getInWei,
|
||||
sendTransaction: _sendTransaction,
|
||||
exponent: exponent,
|
||||
|
@ -140,12 +147,14 @@ abstract class EVMChainClient {
|
|||
required EthereumAddress to,
|
||||
required EtherAmount amount,
|
||||
EtherAmount? maxPriorityFeePerGas,
|
||||
Uint8List? data,
|
||||
}) {
|
||||
return Transaction(
|
||||
from: from,
|
||||
to: to,
|
||||
maxPriorityFeePerGas: maxPriorityFeePerGas,
|
||||
value: amount,
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -204,24 +213,63 @@ abstract class EVMChainClient {
|
|||
return EVMChainERC20Balance(balance, exponent: exponent);
|
||||
}
|
||||
|
||||
Future<Erc20Token?> getErc20Token(String contractAddress) async {
|
||||
Future<Erc20Token?> getErc20Token(String contractAddress, String chainName) async {
|
||||
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();
|
||||
final uri = Uri.https(
|
||||
'deep-index.moralis.io',
|
||||
'/api/v2.2/erc20/metadata',
|
||||
{
|
||||
"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(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
contractAddress: contractAddress,
|
||||
decimal: decimal.toInt(),
|
||||
decimal: int.tryParse(decimal) ?? 0,
|
||||
iconPath: iconPath,
|
||||
);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List hexToBytes(String hexString) {
|
||||
return Uint8List.fromList(
|
||||
hex.HEX.decode(hexString.startsWith('0x') ? hexString.substring(2) : hexString));
|
||||
}
|
||||
|
||||
void stop() {
|
||||
_client?.dispose();
|
||||
}
|
||||
|
|
|
@ -9,3 +9,14 @@ class EVMChainTransactionCreationException implements Exception {
|
|||
@override
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -32,15 +32,15 @@ class EVMChainTransactionModel {
|
|||
factory EVMChainTransactionModel.fromJson(Map<String, dynamic> json, String defaultSymbol) =>
|
||||
EVMChainTransactionModel(
|
||||
date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000),
|
||||
hash: json["hash"],
|
||||
from: json["from"],
|
||||
to: json["to"],
|
||||
amount: BigInt.parse(json["value"]),
|
||||
gasUsed: int.parse(json["gasUsed"]),
|
||||
gasPrice: BigInt.parse(json["gasPrice"]),
|
||||
contractAddress: json["contractAddress"],
|
||||
confirmations: int.parse(json["confirmations"]),
|
||||
blockNumber: int.parse(json["blockNumber"]),
|
||||
hash: json["hash"] ?? "",
|
||||
from: json["from"] ?? "",
|
||||
to: json["to"] ?? "",
|
||||
amount: BigInt.parse(json["value"] ?? "0"),
|
||||
gasUsed: int.parse(json["gasUsed"] ?? "0"),
|
||||
gasPrice: BigInt.parse(json["gasPrice"] ?? "0"),
|
||||
contractAddress: json["contractAddress"] ?? "",
|
||||
confirmations: int.parse(json["confirmations"] ?? "0"),
|
||||
blockNumber: int.parse(json["blockNumber"] ?? "0"),
|
||||
tokenSymbol: json["tokenSymbol"] ?? defaultSymbol,
|
||||
tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""),
|
||||
isError: json["isError"] == "1",
|
||||
|
|
|
@ -224,10 +224,17 @@ abstract class EVMChainWalletBase
|
|||
final outputs = _credentials.outputs;
|
||||
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 =
|
||||
balance.keys.firstWhere((element) => element.title == _credentials.currency.title);
|
||||
|
||||
final _erc20Balance = balance[transactionCurrency]!;
|
||||
final erc20Balance = balance[transactionCurrency]!;
|
||||
BigInt totalAmount = BigInt.zero;
|
||||
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
|
||||
num amountToEVMChainMultiplier = pow(10, exponent);
|
||||
|
@ -242,7 +249,7 @@ abstract class EVMChainWalletBase
|
|||
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
|
||||
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
|
||||
|
||||
if (_erc20Balance.balance < totalAmount) {
|
||||
if (erc20Balance.balance < totalAmount) {
|
||||
throw EVMChainTransactionCreationException(transactionCurrency);
|
||||
}
|
||||
} else {
|
||||
|
@ -251,18 +258,27 @@ abstract class EVMChainWalletBase
|
|||
// then no need to subtract the fees from the amount if send all
|
||||
final BigInt allAmount;
|
||||
if (transactionCurrency is Erc20Token) {
|
||||
allAmount = _erc20Balance.balance;
|
||||
allAmount = erc20Balance.balance;
|
||||
} else {
|
||||
allAmount = _erc20Balance.balance -
|
||||
BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
|
||||
}
|
||||
final totalOriginalAmount =
|
||||
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
|
||||
totalAmount = output.sendAll
|
||||
? allAmount
|
||||
: BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
|
||||
final estimatedFee = BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -272,13 +288,14 @@ abstract class EVMChainWalletBase
|
|||
toAddress: _credentials.outputs.first.isParsedAddress
|
||||
? _credentials.outputs.first.extractedAddress!
|
||||
: _credentials.outputs.first.address,
|
||||
amount: totalAmount.toString(),
|
||||
amount: totalAmount,
|
||||
gas: _estimatedGas!,
|
||||
priority: _credentials.priority!,
|
||||
currency: transactionCurrency,
|
||||
exponent: exponent,
|
||||
contractAddress:
|
||||
transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null,
|
||||
data: hexOpReturnMemo,
|
||||
);
|
||||
|
||||
return pendingEVMChainTransaction;
|
||||
|
@ -310,6 +327,7 @@ abstract class EVMChainWalletBase
|
|||
Future<Map<String, EVMChainTransactionInfo>> fetchTransactions() async {
|
||||
final address = _evmChainPrivateKey.address.hex;
|
||||
final transactions = await _client.fetchTransactions(address);
|
||||
final internalTransactions = await _client.fetchInternalTransactions(address);
|
||||
|
||||
final List<Future<List<EVMChainTransactionModel>>> erc20TokensTransactions = [];
|
||||
|
||||
|
@ -324,6 +342,7 @@ abstract class EVMChainWalletBase
|
|||
|
||||
final tokensTransaction = await Future.wait(erc20TokensTransactions);
|
||||
transactions.addAll(tokensTransaction.expand((element) => element));
|
||||
transactions.addAll(internalTransactions);
|
||||
|
||||
final Map<String, EVMChainTransactionInfo> result = {};
|
||||
|
||||
|
@ -420,11 +439,16 @@ abstract class EVMChainWalletBase
|
|||
|
||||
Future<void> addErc20Token(Erc20Token token) async {
|
||||
String? iconPath;
|
||||
try {
|
||||
iconPath = CryptoCurrency.all
|
||||
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
|
||||
.iconPath;
|
||||
} catch (_) {}
|
||||
|
||||
if (token.iconPath == null || token.iconPath!.isEmpty) {
|
||||
try {
|
||||
iconPath = CryptoCurrency.all
|
||||
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
|
||||
.iconPath;
|
||||
} catch (_) {}
|
||||
} else {
|
||||
iconPath = token.iconPath;
|
||||
}
|
||||
|
||||
final newToken = createNewErc20TokenObject(token, iconPath);
|
||||
|
||||
|
@ -447,8 +471,8 @@ abstract class EVMChainWalletBase
|
|||
_updateBalance();
|
||||
}
|
||||
|
||||
Future<Erc20Token?> getErc20Token(String contractAddress) async =>
|
||||
await _client.getErc20Token(contractAddress);
|
||||
Future<Erc20Token?> getErc20Token(String contractAddress, String chainName) async =>
|
||||
await _client.getErc20Token(contractAddress, chainName);
|
||||
|
||||
void _onNewTransaction() {
|
||||
_updateBalance();
|
||||
|
@ -484,7 +508,7 @@ abstract class EVMChainWalletBase
|
|||
_transactionsUpdateTimer!.cancel();
|
||||
}
|
||||
|
||||
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) {
|
||||
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 15), (_) {
|
||||
_updateTransactions();
|
||||
_updateBalance();
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:web3dart/crypto.dart';
|
||||
import 'package:hex/hex.dart' as Hex;
|
||||
|
||||
class PendingEVMChainTransaction with PendingTransaction {
|
||||
final Function sendTransaction;
|
||||
|
@ -38,5 +39,12 @@ class PendingEVMChainTransaction with PendingTransaction {
|
|||
String get hex => bytesToHex(signedTransaction, include0x: true);
|
||||
|
||||
@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)}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ class PolygonClient extends EVMChainClient {
|
|||
required EthereumAddress to,
|
||||
required EtherAmount amount,
|
||||
EtherAmount? maxPriorityFeePerGas,
|
||||
Uint8List? data,
|
||||
}) {
|
||||
return Transaction(
|
||||
from: from,
|
||||
|
@ -54,4 +55,28 @@ class PolygonClient extends EVMChainClient {
|
|||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,16 +96,30 @@ class SolanaWalletClient {
|
|||
return SolanaBalance(totalBalance);
|
||||
}
|
||||
|
||||
Future<double> getGasForMessage(String message) async {
|
||||
Future<double> getFeeForMessage(String message, Commitment commitment) async {
|
||||
try {
|
||||
final gasPrice = await _client!.rpcClient.getFeeForMessage(message) ?? 0;
|
||||
final fee = gasPrice / lamportsPerSol;
|
||||
final feeForMessage =
|
||||
await _client!.rpcClient.getFeeForMessage(message, commitment: commitment);
|
||||
final fee = (feeForMessage ?? 0.0) / lamportsPerSol;
|
||||
return fee;
|
||||
} 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
|
||||
Future<List<SolanaTransactionModel>> fetchTransactions(
|
||||
Ed25519HDPublicKey publicKey, {
|
||||
|
@ -257,24 +271,15 @@ class SolanaWalletClient {
|
|||
Future<PendingSolanaTransaction> signSolanaTransaction({
|
||||
required String tokenTitle,
|
||||
required int tokenDecimals,
|
||||
String? tokenMint,
|
||||
required double inputAmount,
|
||||
required String destinationAddress,
|
||||
required Ed25519HDKeyPair ownerKeypair,
|
||||
required bool isSendAll,
|
||||
String? tokenMint,
|
||||
List<String> references = const [],
|
||||
}) async {
|
||||
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) {
|
||||
final pendingNativeTokenTransaction = await _signNativeTokenTransaction(
|
||||
tokenTitle: tokenTitle,
|
||||
|
@ -282,8 +287,8 @@ class SolanaWalletClient {
|
|||
inputAmount: inputAmount,
|
||||
destinationAddress: destinationAddress,
|
||||
ownerKeypair: ownerKeypair,
|
||||
recentBlockhash: recentBlockhash,
|
||||
commitment: commitment,
|
||||
isSendAll: isSendAll,
|
||||
);
|
||||
return pendingNativeTokenTransaction;
|
||||
} else {
|
||||
|
@ -294,25 +299,29 @@ class SolanaWalletClient {
|
|||
inputAmount: inputAmount,
|
||||
destinationAddress: destinationAddress,
|
||||
ownerKeypair: ownerKeypair,
|
||||
recentBlockhash: recentBlockhash,
|
||||
commitment: commitment,
|
||||
);
|
||||
return pendingSPLTokenTransaction;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PendingSolanaTransaction> _signNativeTokenTransaction({
|
||||
required String tokenTitle,
|
||||
required int tokenDecimals,
|
||||
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();
|
||||
Future<RecentBlockhash> _getRecentBlockhash(Commitment commitment) async {
|
||||
final latestBlockhash =
|
||||
await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value;
|
||||
|
||||
final recentBlockhash = RecentBlockhash(
|
||||
blockhash: latestBlockhash.blockhash,
|
||||
feeCalculator: const FeeCalculator(lamportsPerSignature: 500),
|
||||
);
|
||||
|
||||
return recentBlockhash;
|
||||
}
|
||||
|
||||
Message _getMessageForNativeTransaction(
|
||||
Ed25519HDKeyPair ownerKeypair,
|
||||
String destinationAddress,
|
||||
int lamports,
|
||||
) {
|
||||
final instructions = [
|
||||
SystemInstruction.transfer(
|
||||
fundingAccount: ownerKeypair.publicKey,
|
||||
|
@ -322,21 +331,75 @@ class SolanaWalletClient {
|
|||
];
|
||||
|
||||
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 signedTx = await _signTransactionInternal(
|
||||
message: message,
|
||||
signers: signers,
|
||||
commitment: commitment,
|
||||
recentBlockhash: recentBlockhash,
|
||||
);
|
||||
RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment);
|
||||
|
||||
final fee = await _getFeeFromCompiledMessage(
|
||||
message,
|
||||
recentBlockhash,
|
||||
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(
|
||||
signedTransaction: signedTx,
|
||||
commitment: commitment,
|
||||
|
@ -360,7 +423,6 @@ class SolanaWalletClient {
|
|||
required double inputAmount,
|
||||
required String destinationAddress,
|
||||
required Ed25519HDKeyPair ownerKeypair,
|
||||
required RecentBlockhash recentBlockhash,
|
||||
required Commitment commitment,
|
||||
}) async {
|
||||
final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress);
|
||||
|
@ -408,8 +470,18 @@ class SolanaWalletClient {
|
|||
);
|
||||
|
||||
final message = Message(instructions: [instruction]);
|
||||
|
||||
final signers = [ownerKeypair];
|
||||
|
||||
RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment);
|
||||
|
||||
final fee = await _getFeeFromCompiledMessage(
|
||||
message,
|
||||
signers.first.publicKey,
|
||||
recentBlockhash,
|
||||
commitment,
|
||||
);
|
||||
|
||||
final signedTx = await _signTransactionInternal(
|
||||
message: message,
|
||||
signers: signers,
|
||||
|
@ -417,12 +489,6 @@ class SolanaWalletClient {
|
|||
recentBlockhash: recentBlockhash,
|
||||
);
|
||||
|
||||
final fee = await _getFeeFromCompiledMessage(
|
||||
message,
|
||||
recentBlockhash,
|
||||
signers.first.publicKey,
|
||||
);
|
||||
|
||||
sendTx() async => await sendTransaction(
|
||||
signedTransaction: signedTx,
|
||||
commitment: commitment,
|
||||
|
@ -438,19 +504,6 @@ class SolanaWalletClient {
|
|||
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({
|
||||
required Message message,
|
||||
required List<Ed25519HDKeyPair> signers,
|
||||
|
@ -466,13 +519,35 @@ class SolanaWalletClient {
|
|||
required SignedTx signedTransaction,
|
||||
required Commitment commitment,
|
||||
}) async {
|
||||
final signature = await _client!.rpcClient.sendTransaction(
|
||||
signedTransaction.encode(),
|
||||
preflightCommitment: commitment,
|
||||
);
|
||||
try {
|
||||
final signature = await _client!.rpcClient.sendTransaction(
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,6 +75,9 @@ abstract class SolanaWalletBase
|
|||
|
||||
late SolanaWalletClient _client;
|
||||
|
||||
@observable
|
||||
double? estimatedFee;
|
||||
|
||||
Timer? _transactionsUpdateTimer;
|
||||
|
||||
late final Box<SPLToken> splTokensBox;
|
||||
|
@ -171,6 +174,14 @@ abstract class SolanaWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _getEstimatedFees() async {
|
||||
try {
|
||||
estimatedFee = await _client.getEstimatedFee(_walletKeyPair!);
|
||||
} catch (e) {
|
||||
estimatedFee = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
final solCredentials = credentials as SolanaTransactionCredentials;
|
||||
|
@ -188,6 +199,8 @@ abstract class SolanaWalletBase
|
|||
|
||||
double totalAmount = 0.0;
|
||||
|
||||
bool isSendAll = false;
|
||||
|
||||
if (hasMultiDestination) {
|
||||
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
|
||||
throw SolanaTransactionWrongBalanceException(transactionCurrency);
|
||||
|
@ -204,9 +217,15 @@ abstract class SolanaWalletBase
|
|||
} else {
|
||||
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) {
|
||||
throw SolanaTransactionWrongBalanceException(transactionCurrency);
|
||||
|
@ -228,6 +247,7 @@ abstract class SolanaWalletBase
|
|||
destinationAddress: solCredentials.outputs.first.isParsedAddress
|
||||
? solCredentials.outputs.first.extractedAddress!
|
||||
: solCredentials.outputs.first.address,
|
||||
isSendAll: isSendAll,
|
||||
);
|
||||
|
||||
return pendingSolanaTransaction;
|
||||
|
@ -269,7 +289,10 @@ abstract class SolanaWalletBase
|
|||
Future<void> _updateSPLTokenTransactions() async {
|
||||
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) {
|
||||
final tokenTxs = await _client.getSPLTokenTransfers(
|
||||
token.mintAddress,
|
||||
|
@ -326,6 +349,7 @@ abstract class SolanaWalletBase
|
|||
_updateBalance(),
|
||||
_updateNativeSOLTransactions(),
|
||||
_updateSPLTokenTransactions(),
|
||||
_getEstimatedFees(),
|
||||
]);
|
||||
|
||||
syncStatus = SyncedSyncStatus();
|
||||
|
@ -433,18 +457,28 @@ abstract class SolanaWalletBase
|
|||
final mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress);
|
||||
|
||||
// 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 SPLToken.fromMetadata(
|
||||
name: token.name,
|
||||
mint: token.mint,
|
||||
symbol: token.symbol,
|
||||
mintAddress: mintAddress,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -475,9 +509,9 @@ abstract class SolanaWalletBase
|
|||
}
|
||||
|
||||
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
|
||||
_updateSPLTokenTransactions();
|
||||
_updateNativeSOLTransactions();
|
||||
_updateBalance();
|
||||
_updateNativeSOLTransactions();
|
||||
_updateSPLTokenTransactions();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
|
|||
|
||||
await wallet.init();
|
||||
wallet.addInitialTokens();
|
||||
await wallet.save();
|
||||
return wallet;
|
||||
}
|
||||
|
||||
|
@ -46,16 +47,31 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
|
|||
Future<SolanaWallet> openWallet(String name, String password) async {
|
||||
final walletInfo =
|
||||
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
|
||||
final wallet = await SolanaWalletBase.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
await wallet.save();
|
||||
try {
|
||||
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
|
||||
|
@ -110,6 +126,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
|
|||
password: password, name: currentName, walletInfo: currentWalletInfo);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
await saveBackup(newName);
|
||||
|
||||
final newWalletInfo = currentWalletInfo;
|
||||
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
|
|
|
@ -55,6 +55,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
|||
required String mint,
|
||||
required String symbol,
|
||||
required String mintAddress,
|
||||
String? iconPath
|
||||
}) {
|
||||
return SPLToken(
|
||||
name: name,
|
||||
|
@ -62,7 +63,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
|||
mintAddress: mintAddress,
|
||||
decimal: 0,
|
||||
mint: mint,
|
||||
iconPath: '',
|
||||
iconPath: iconPath,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -58,16 +58,16 @@ post_install do |installer|
|
|||
'PERMISSION_CONTACTS=0',
|
||||
|
||||
## dart: PermissionGroup.camera
|
||||
'PERMISSION_CAMERA=0',
|
||||
'PERMISSION_CAMERA=1',
|
||||
|
||||
## dart: PermissionGroup.microphone
|
||||
'PERMISSION_MICROPHONE=0',
|
||||
'PERMISSION_MICROPHONE=1',
|
||||
|
||||
## dart: PermissionGroup.speech
|
||||
'PERMISSION_SPEECH_RECOGNIZER=0',
|
||||
|
||||
## dart: PermissionGroup.photos
|
||||
'PERMISSION_PHOTOS=0',
|
||||
'PERMISSION_PHOTOS=1',
|
||||
|
||||
## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
|
||||
'PERMISSION_LOCATION=0',
|
||||
|
|
|
@ -300,6 +300,6 @@ SPEC CHECKSUMS:
|
|||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
||||
PODFILE CHECKSUM: fcb1b8418441a35b438585c9dd8374e722e6c6ca
|
||||
PODFILE CHECKSUM: a2fe518be61cdbdc5b0e2da085ab543d556af2d3
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
|
|
|
@ -74,21 +74,26 @@ class CWBitcoin extends Bitcoin {
|
|||
|
||||
@override
|
||||
Object createBitcoinTransactionCredentials(List<Output> outputs,
|
||||
{required TransactionPriority priority, int? feeRate}) =>
|
||||
BitcoinTransactionCredentials(
|
||||
outputs
|
||||
.map((out) => OutputInfo(
|
||||
fiatAmount: out.fiatAmount,
|
||||
cryptoAmount: out.cryptoAmount,
|
||||
address: out.address,
|
||||
note: out.note,
|
||||
sendAll: out.sendAll,
|
||||
extractedAddress: out.extractedAddress,
|
||||
isParsedAddress: out.isParsedAddress,
|
||||
formattedCryptoAmount: out.formattedCryptoAmount))
|
||||
.toList(),
|
||||
priority: priority as BitcoinTransactionPriority,
|
||||
feeRate: feeRate);
|
||||
{required TransactionPriority priority, int? feeRate}) {
|
||||
final bitcoinFeeRate =
|
||||
priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null;
|
||||
return BitcoinTransactionCredentials(
|
||||
outputs
|
||||
.map((out) => OutputInfo(
|
||||
fiatAmount: out.fiatAmount,
|
||||
cryptoAmount: out.cryptoAmount,
|
||||
address: out.address,
|
||||
note: out.note,
|
||||
sendAll: out.sendAll,
|
||||
extractedAddress: out.extractedAddress,
|
||||
isParsedAddress: out.isParsedAddress,
|
||||
formattedCryptoAmount: out.formattedCryptoAmount,
|
||||
memo: out.memo))
|
||||
.toList(),
|
||||
priority: priority as BitcoinTransactionPriority,
|
||||
feeRate: bitcoinFeeRate
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Object createBitcoinTransactionCredentialsRaw(List<OutputInfo> outputs,
|
||||
|
@ -122,23 +127,30 @@ class CWBitcoin extends Bitcoin {
|
|||
|
||||
@override
|
||||
Future<int> estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority) async {
|
||||
final electrumWallet = wallet as ElectrumWallet;
|
||||
final sk = ECPrivate.random();
|
||||
|
||||
final p2shAddr = sk.getPublic().toP2pkhInP2sh();
|
||||
final p2wpkhAddr = sk.getPublic().toP2wpkhAddress();
|
||||
try {
|
||||
final estimatedTx = await electrumWallet.estimateTxFeeAndInputsToUse(
|
||||
0,
|
||||
true,
|
||||
// Deposit address + change address
|
||||
[p2shAddr, p2wpkhAddr],
|
||||
[
|
||||
BitcoinOutput(address: p2shAddr, value: BigInt.zero),
|
||||
BitcoinOutput(address: p2wpkhAddr, value: BigInt.zero)
|
||||
],
|
||||
null,
|
||||
priority as BitcoinTransactionPriority);
|
||||
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 (_) {
|
||||
|
@ -164,8 +176,9 @@ class CWBitcoin extends Bitcoin {
|
|||
int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount);
|
||||
|
||||
@override
|
||||
String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate) =>
|
||||
(priority as BitcoinTransactionPriority).labelWithRate(rate);
|
||||
String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate,
|
||||
{int? customRate}) =>
|
||||
(priority as BitcoinTransactionPriority).labelWithRate(rate, customRate);
|
||||
|
||||
@override
|
||||
List<BitcoinUnspent> getUnspents(Object wallet) {
|
||||
|
@ -191,6 +204,9 @@ class CWBitcoin extends Bitcoin {
|
|||
@override
|
||||
TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium;
|
||||
|
||||
@override
|
||||
TransactionPriority getBitcoinTransactionPriorityCustom() => BitcoinTransactionPriority.custom;
|
||||
|
||||
@override
|
||||
TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium;
|
||||
|
||||
|
@ -231,4 +247,48 @@ class CWBitcoin extends Bitcoin {
|
|||
return SegwitAddresType.p2wpkh;
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,20 +22,24 @@ import 'package:flutter/material.dart';
|
|||
import 'package:http/http.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class MoonPaySellProvider extends BuyProvider {
|
||||
MoonPaySellProvider({
|
||||
class MoonPayProvider extends BuyProvider {
|
||||
MoonPayProvider({
|
||||
required SettingsStore settingsStore,
|
||||
required WalletBase wallet,
|
||||
bool isTestEnvironment = false,
|
||||
}) : baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl,
|
||||
}) : baseSellUrl = isTestEnvironment ? _baseSellTestUrl : _baseSellProductUrl,
|
||||
baseBuyUrl = isTestEnvironment ? _baseBuyTestUrl : _baseBuyProductUrl,
|
||||
this._settingsStore = settingsStore,
|
||||
super(wallet: wallet, isTestEnvironment: isTestEnvironment);
|
||||
|
||||
final SettingsStore _settingsStore;
|
||||
|
||||
static const _baseTestUrl = 'sell-sandbox.moonpay.com';
|
||||
static const _baseProductUrl = 'sell.moonpay.com';
|
||||
static const _baseSellTestUrl = 'sell-sandbox.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
|
||||
String get providerDescription =>
|
||||
|
@ -62,8 +66,14 @@ class MoonPaySellProvider extends BuyProvider {
|
|||
|
||||
static String get _apiKey => secrets.moonPayApiKey;
|
||||
|
||||
final String baseBuyUrl;
|
||||
final String baseSellUrl;
|
||||
|
||||
String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
|
||||
|
||||
String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
|
||||
|
||||
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
|
||||
final String baseUrl;
|
||||
|
||||
Future<String> getMoonpaySignature(String query) async {
|
||||
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
|
||||
|
@ -85,147 +95,92 @@ class MoonPaySellProvider extends BuyProvider {
|
|||
}
|
||||
}
|
||||
|
||||
Future<Uri> requestMoonPayUrl({
|
||||
Future<Uri> requestSellMoonPayUrl({
|
||||
required CryptoCurrency currency,
|
||||
required String refundWalletAddress,
|
||||
required SettingsStore settingsStore,
|
||||
}) async {
|
||||
final customParams = {
|
||||
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),
|
||||
'refundWalletAddress': refundWalletAddress,
|
||||
};
|
||||
|
||||
final originalUri = Uri.https(
|
||||
baseUrl,
|
||||
'',
|
||||
<String, dynamic>{
|
||||
'apiKey': _apiKey,
|
||||
'defaultBaseCurrencyCode': _normalizeCurrency(currency),
|
||||
'refundWalletAddress': refundWalletAddress,
|
||||
}..addAll(customParams),
|
||||
);
|
||||
if (_apiKey.isNotEmpty) {
|
||||
params['apiKey'] = _apiKey;
|
||||
}
|
||||
|
||||
final signature = await getMoonpaySignature('?${originalUri.query}');
|
||||
final originalUri = Uri.https(
|
||||
baseSellUrl,
|
||||
'',
|
||||
params,
|
||||
);
|
||||
|
||||
if (isTestEnvironment) {
|
||||
return originalUri;
|
||||
}
|
||||
|
||||
final signature = await getMoonpaySignature('?${originalUri.query}');
|
||||
|
||||
final query = Map<String, dynamic>.from(originalUri.queryParameters);
|
||||
query['signature'] = signature;
|
||||
final signedUri = originalUri.replace(queryParameters: query);
|
||||
return signedUri;
|
||||
}
|
||||
|
||||
@override
|
||||
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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _normalizeCurrency(CryptoCurrency currency) {
|
||||
if (currency == CryptoCurrency.maticpoly) {
|
||||
return "MATIC_POLYGON";
|
||||
}
|
||||
|
||||
return currency.toString().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
// BUY:
|
||||
static const _currenciesSuffix = '/v3/currencies';
|
||||
static const _quoteSuffix = '/buy_quote';
|
||||
static const _transactionsSuffix = '/v1/transactions';
|
||||
static const _ipAddressSuffix = '/v4/ip_address';
|
||||
static const _apiKey = secrets.moonPayApiKey;
|
||||
static const _secretKey = secrets.moonPaySecretKey;
|
||||
|
||||
@override
|
||||
String get title => 'MoonPay';
|
||||
Future<Uri> requestBuyMoonPayUrl({
|
||||
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
|
||||
String get providerDescription =>
|
||||
'MoonPay offers a fast and simple way to buy and sell cryptocurrencies';
|
||||
if (_apiKey.isNotEmpty) {
|
||||
params['apiKey'] = _apiKey;
|
||||
}
|
||||
|
||||
@override
|
||||
String get lightIcon => 'assets/images/moonpay_light.png';
|
||||
final originalUri = Uri.https(
|
||||
baseBuyUrl,
|
||||
'',
|
||||
params,
|
||||
);
|
||||
|
||||
@override
|
||||
String get darkIcon => 'assets/images/moonpay_dark.png';
|
||||
if (isTestEnvironment) {
|
||||
return originalUri;
|
||||
}
|
||||
|
||||
String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
|
||||
|
||||
String get trackUrl => baseUrl + '/transaction_receipt?transactionId=';
|
||||
|
||||
String baseUrl;
|
||||
|
||||
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 +
|
||||
'¤cyCode=' +
|
||||
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;
|
||||
final signature = await getMoonpaySignature('?${originalUri.query}');
|
||||
final query = Map<String, dynamic>.from(originalUri.queryParameters);
|
||||
query['signature'] = signature;
|
||||
final signedUri = originalUri.replace(queryParameters: query);
|
||||
return signedUri;
|
||||
}
|
||||
|
||||
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
|
||||
|
@ -300,6 +255,52 @@ class MoonPayBuyProvider extends BuyProvider {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> launchProvider(BuildContext context, bool? isBuyAction) =>
|
||||
throw UnimplementedError();
|
||||
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,10 @@ class AmountValidator extends TextValidator {
|
|||
late final DecimalAmountValidator decimalAmountValidator;
|
||||
|
||||
String? call(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return S.current.error_text_amount;
|
||||
}
|
||||
|
||||
//* Validate for Text(length, symbols, decimals etc)
|
||||
|
||||
final textValidation = symbolsAmountValidator(value) ?? decimalAmountValidator(value);
|
||||
|
|
|
@ -15,3 +15,12 @@ class FailureState extends ExecutionState {
|
|||
|
||||
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;
|
||||
}
|
|
@ -2,8 +2,8 @@ import 'solana_chain_service.dart';
|
|||
|
||||
enum SolanaChainId {
|
||||
mainnet,
|
||||
testnet,
|
||||
devnet,
|
||||
// testnet,
|
||||
// devnet,
|
||||
}
|
||||
|
||||
extension SolanaChainIdX on SolanaChainId {
|
||||
|
@ -13,13 +13,16 @@ extension SolanaChainIdX on SolanaChainId {
|
|||
switch (this) {
|
||||
case SolanaChainId.mainnet:
|
||||
name = '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ';
|
||||
// solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp
|
||||
break;
|
||||
case SolanaChainId.testnet:
|
||||
name = '8E9rvCKLFQia2Y35HXjjpWzj8weVo44K';
|
||||
break;
|
||||
case SolanaChainId.devnet:
|
||||
name = '';
|
||||
break;
|
||||
// case SolanaChainId.devnet:
|
||||
// name = '8E9rvCKLFQia2Y35HXjjpWzj8weVo44K';
|
||||
// // solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1
|
||||
// break;
|
||||
// case SolanaChainId.testnet:
|
||||
// name = '';
|
||||
// // solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z
|
||||
// break;
|
||||
}
|
||||
|
||||
return '${SolanaChainServiceImpl.namespace}:$name';
|
||||
|
|
|
@ -43,7 +43,7 @@ class SolanaChainServiceImpl implements ChainService {
|
|||
SolanaClient(
|
||||
rpcUrl: rpcUrl,
|
||||
websocketUrl: Uri.parse(webSocketUrl),
|
||||
timeout: const Duration(minutes: 2),
|
||||
timeout: const Duration(minutes: 5),
|
||||
) {
|
||||
for (final String event in getEvents()) {
|
||||
wallet.registerEventEmitter(chainId: getChainId(), event: event);
|
||||
|
@ -72,7 +72,7 @@ class SolanaChainServiceImpl implements ChainService {
|
|||
|
||||
@override
|
||||
List<String> getEvents() {
|
||||
return [''];
|
||||
return ['chainChanged', 'accountsChanged'];
|
||||
}
|
||||
|
||||
Future<String?> requestAuthorization(String? text) async {
|
||||
|
@ -100,8 +100,7 @@ class SolanaChainServiceImpl implements ChainService {
|
|||
Future<String> solanaSignTransaction(String topic, dynamic parameters) async {
|
||||
log('received solana sign transaction request $parameters');
|
||||
|
||||
final solanaSignTx =
|
||||
SolanaSignTransaction.fromJson(parameters as Map<String, dynamic>);
|
||||
final solanaSignTx = SolanaSignTransaction.fromJson(parameters as Map<String, dynamic>);
|
||||
|
||||
final String? authError = await requestAuthorization('Confirm request to sign transaction?');
|
||||
|
||||
|
@ -122,10 +121,13 @@ class SolanaChainServiceImpl implements ChainService {
|
|||
return '';
|
||||
}
|
||||
|
||||
String signature = sign.signatures.first.toBase58();
|
||||
String signature = await solanaClient.sendAndConfirmTransaction(
|
||||
message: message,
|
||||
signers: [ownerKeyPair!],
|
||||
commitment: Commitment.confirmed,
|
||||
);
|
||||
|
||||
print(signature);
|
||||
print(signature.runtimeType);
|
||||
|
||||
bottomSheetService.queueBottomSheet(
|
||||
isModalDismissible: true,
|
||||
|
|
|
@ -133,13 +133,27 @@ abstract class Web3WalletServiceBase with Store {
|
|||
if (appStore.wallet!.type == WalletType.solana) {
|
||||
for (final cId in SolanaChainId.values) {
|
||||
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(
|
||||
reference: cId,
|
||||
rpcUrl: rpcUri,
|
||||
webSocketUrl: webSocketUri,
|
||||
rpcUrl: isModifiedNodeUri ? rpcUri! : node.uri,
|
||||
webSocketUrl: webSocketUrl,
|
||||
wcKeyService: walletKeyService,
|
||||
bottomSheetService: _bottomSheetHandler,
|
||||
wallet: _web3Wallet,
|
||||
|
|
17
lib/di.dart
|
@ -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/exchange_api_mode.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:cake_wallet/ethereum/ethereum.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_seed_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:get_it/get_it.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
@ -806,8 +808,11 @@ Future<void> setup({
|
|||
getIt
|
||||
.registerFactory<DFXBuyProvider>(() => DFXBuyProvider(wallet: getIt.get<AppStore>().wallet!));
|
||||
|
||||
getIt.registerFactory<MoonPaySellProvider>(() => MoonPaySellProvider(
|
||||
settingsStore: getIt.get<AppStore>().settingsStore, wallet: getIt.get<AppStore>().wallet!));
|
||||
getIt.registerFactory<MoonPayProvider>(() => MoonPayProvider(
|
||||
settingsStore: getIt.get<AppStore>().settingsStore,
|
||||
wallet: getIt.get<AppStore>().wallet!,
|
||||
isTestEnvironment: kDebugMode,
|
||||
));
|
||||
|
||||
getIt.registerFactory<OnRamperBuyProvider>(() => OnRamperBuyProvider(
|
||||
getIt.get<AppStore>().settingsStore,
|
||||
|
@ -910,7 +915,8 @@ Future<void> setup({
|
|||
transactionInfo: transactionInfo,
|
||||
transactionDescriptionBox: _transactionDescriptionBox,
|
||||
wallet: wallet,
|
||||
settingsStore: getIt.get<SettingsStore>());
|
||||
settingsStore: getIt.get<SettingsStore>(),
|
||||
sendViewModel: getIt.get<SendViewModel>());
|
||||
});
|
||||
|
||||
getIt.registerFactoryParam<TransactionDetailsPage, TransactionInfo, void>(
|
||||
|
@ -1133,6 +1139,11 @@ Future<void> setup({
|
|||
|
||||
getIt.registerFactory(() => IoniaAccountCardsPage(getIt.get<IoniaAccountViewModel>()));
|
||||
|
||||
getIt.registerFactoryParam<RBFDetailsPage, TransactionInfo, void>(
|
||||
(TransactionInfo transactionInfo, _) => RBFDetailsPage(
|
||||
transactionDetailsViewModel:
|
||||
getIt.get<TransactionDetailsViewModel>(param1: transactionInfo)));
|
||||
|
||||
getIt.registerFactory(() => AnonPayApi(
|
||||
useTorOnly: getIt.get<SettingsStore>().exchangeStatus == ExchangeApiMode.torOnly,
|
||||
wallet: getIt.get<AppStore>().wallet!));
|
||||
|
|
|
@ -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/store/settings_store.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/wallet_list/wallet_list_item.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 bool syncAll = settingsStore.currentSyncAll;
|
||||
|
||||
if (syncMode.type == SyncType.disabled) {
|
||||
if (syncMode.type == SyncType.disabled || !FeatureFlag.isBackgroundSyncEnabled) {
|
||||
cancelSyncTask();
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ class BiometricAuth {
|
|||
return await _localAuth.authenticate(
|
||||
localizedReason: S.current.biometric_auth_reason,
|
||||
options: AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
useErrorDialogs: true,
|
||||
stickyAuth: false));
|
||||
} on PlatformException catch (e) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io' show Directory, File, Platform;
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.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:cake_wallet/entities/secret_store_key.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
@ -211,6 +212,10 @@ Future<void> defaultSettingsMigration(
|
|||
await changeDefaultBitcoinNode(nodes, sharedPreferences);
|
||||
break;
|
||||
|
||||
case 30:
|
||||
await disableServiceStatusFiatDisabled(sharedPreferences);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -225,6 +230,18 @@ Future<void> defaultSettingsMigration(
|
|||
await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version);
|
||||
}
|
||||
|
||||
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 {
|
||||
final currentPriority =
|
||||
await sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority) ??
|
||||
|
|
|
@ -42,6 +42,7 @@ class PreferencesKey {
|
|||
static const ethereumTransactionPriority = 'current_fee_priority_ethereum';
|
||||
static const polygonTransactionPriority = 'current_fee_priority_polygon';
|
||||
static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash';
|
||||
static const customBitcoinFeeRate = 'custom_electrum_fee_rate';
|
||||
static const shouldShowReceiveWarning = 'should_show_receive_warning';
|
||||
static const shouldShowYatPopup = 'should_show_yat_popup';
|
||||
static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1';
|
||||
|
|
|
@ -11,7 +11,7 @@ enum ProviderType {
|
|||
robinhood,
|
||||
dfx,
|
||||
onramper,
|
||||
moonpaySell,
|
||||
moonpay,
|
||||
}
|
||||
|
||||
extension ProviderTypeName on ProviderType {
|
||||
|
@ -25,7 +25,7 @@ extension ProviderTypeName on ProviderType {
|
|||
return 'DFX Connect';
|
||||
case ProviderType.onramper:
|
||||
return 'Onramper';
|
||||
case ProviderType.moonpaySell:
|
||||
case ProviderType.moonpay:
|
||||
return 'MoonPay';
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ extension ProviderTypeName on ProviderType {
|
|||
return 'dfx_connect_provider';
|
||||
case ProviderType.onramper:
|
||||
return 'onramper_provider';
|
||||
case ProviderType.moonpaySell:
|
||||
case ProviderType.moonpay:
|
||||
return 'moonpay_provider';
|
||||
}
|
||||
}
|
||||
|
@ -62,10 +62,11 @@ class ProvidersHelper {
|
|||
ProviderType.onramper,
|
||||
ProviderType.dfx,
|
||||
ProviderType.robinhood,
|
||||
ProviderType.moonpay,
|
||||
];
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoinCash:
|
||||
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
|
||||
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay];
|
||||
case WalletType.solana:
|
||||
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
|
||||
case WalletType.none:
|
||||
|
@ -82,18 +83,18 @@ class ProvidersHelper {
|
|||
return [
|
||||
ProviderType.askEachTime,
|
||||
ProviderType.onramper,
|
||||
ProviderType.moonpaySell,
|
||||
ProviderType.moonpay,
|
||||
ProviderType.dfx,
|
||||
];
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoinCash:
|
||||
return [ProviderType.askEachTime, ProviderType.moonpaySell];
|
||||
return [ProviderType.askEachTime, ProviderType.moonpay];
|
||||
case WalletType.solana:
|
||||
return [
|
||||
ProviderType.askEachTime,
|
||||
ProviderType.onramper,
|
||||
ProviderType.robinhood,
|
||||
ProviderType.moonpaySell,
|
||||
ProviderType.moonpay,
|
||||
];
|
||||
case WalletType.monero:
|
||||
case WalletType.nano:
|
||||
|
@ -112,10 +113,10 @@ class ProvidersHelper {
|
|||
return getIt.get<DFXBuyProvider>();
|
||||
case ProviderType.onramper:
|
||||
return getIt.get<OnRamperBuyProvider>();
|
||||
case ProviderType.moonpay:
|
||||
return getIt.get<MoonPayProvider>();
|
||||
case ProviderType.askEachTime:
|
||||
return null;
|
||||
case ProviderType.moonpaySell:
|
||||
return getIt.get<MoonPaySellProvider>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,7 +76,8 @@ class CWEthereum extends Ethereum {
|
|||
sendAll: out.sendAll,
|
||||
extractedAddress: out.extractedAddress,
|
||||
isParsedAddress: out.isParsedAddress,
|
||||
formattedCryptoAmount: out.formattedCryptoAmount))
|
||||
formattedCryptoAmount: out.formattedCryptoAmount,
|
||||
memo: out.memo))
|
||||
.toList(),
|
||||
priority: priority as EVMChainTransactionPriority,
|
||||
currency: currency,
|
||||
|
@ -130,7 +131,7 @@ class CWEthereum extends Ethereum {
|
|||
@override
|
||||
Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) async {
|
||||
final ethereumWallet = wallet as EthereumWallet;
|
||||
return await ethereumWallet.getErc20Token(contractAddress);
|
||||
return await ethereumWallet.getErc20Token(contractAddress, 'eth');
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -22,6 +22,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
|
|||
ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png');
|
||||
static const exolix =
|
||||
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: '');
|
||||
|
||||
|
@ -41,6 +43,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
|
|||
return trocador;
|
||||
case 6:
|
||||
return exolix;
|
||||
case 8:
|
||||
return thorChain;
|
||||
case 7:
|
||||
return all;
|
||||
default:
|
||||
|
|
|
@ -133,7 +133,11 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
|||
}
|
||||
|
||||
@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 formattedAppVersion = int.tryParse(_settingsStore.appVersion.replaceAll('.', '')) ?? 0;
|
||||
final payload = {
|
||||
|
@ -202,7 +206,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
|||
createdAt: DateTime.now(),
|
||||
amount: responseJSON['fromAmount']?.toString() ?? request.fromAmount,
|
||||
state: TradeState.created,
|
||||
payoutAddress: payoutAddress);
|
||||
payoutAddress: payoutAddress,
|
||||
isSendAll: isSendAll);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -28,7 +28,8 @@ abstract class ExchangeProvider {
|
|||
Future<Limits> fetchLimits(
|
||||
{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});
|
||||
|
||||
|
|
|
@ -130,7 +130,11 @@ class ExolixExchangeProvider extends ExchangeProvider {
|
|||
}
|
||||
|
||||
@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 body = {
|
||||
'coinFrom': _normalizeCurrency(request.fromCurrency),
|
||||
|
@ -180,7 +184,8 @@ class ExolixExchangeProvider extends ExchangeProvider {
|
|||
createdAt: DateTime.now(),
|
||||
amount: amount,
|
||||
state: TradeState.created,
|
||||
payoutAddress: payoutAddress);
|
||||
payoutAddress: payoutAddress,
|
||||
isSendAll: isSendAll);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -144,7 +144,11 @@ class SideShiftExchangeProvider extends ExchangeProvider {
|
|||
}
|
||||
|
||||
@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 = '';
|
||||
final body = {
|
||||
'affiliateId': affiliateId,
|
||||
|
@ -197,6 +201,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
|
|||
amount: depositAmount ?? request.fromAmount,
|
||||
payoutAddress: settleAddress,
|
||||
createdAt: DateTime.now(),
|
||||
isSendAll: isSendAll,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -117,7 +117,11 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
|
|||
}
|
||||
|
||||
@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 params = {'api_key': apiKey};
|
||||
final body = <String, dynamic>{
|
||||
|
@ -162,6 +166,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
|
|||
amount: request.fromAmount,
|
||||
payoutAddress: payoutAddress,
|
||||
createdAt: DateTime.now(),
|
||||
isSendAll: isSendAll,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
255
lib/exchange/provider/thorchain_exchange.provider.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -13,7 +13,8 @@ import 'package:http/http.dart';
|
|||
|
||||
class TrocadorExchangeProvider extends ExchangeProvider {
|
||||
TrocadorExchangeProvider({this.useTorOnly = false, this.providerStates = const {}})
|
||||
: _lastUsedRateId = '', _provider = [],
|
||||
: _lastUsedRateId = '',
|
||||
_provider = [],
|
||||
super(pairList: supportedPairs(_notSupported));
|
||||
|
||||
bool useTorOnly;
|
||||
|
@ -23,7 +24,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
|||
'Swapter',
|
||||
'StealthEx',
|
||||
'Simpleswap',
|
||||
'Swapuz'
|
||||
'Swapuz',
|
||||
'ChangeNow',
|
||||
'Changehero',
|
||||
'FixedFloat',
|
||||
|
@ -144,8 +145,11 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
|||
}
|
||||
|
||||
@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 = {
|
||||
'api_key': apiKey,
|
||||
'ticker_from': _normalizeCurrency(request.fromCurrency),
|
||||
|
@ -172,7 +176,6 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
|||
params['id'] = _lastUsedRateId;
|
||||
}
|
||||
|
||||
|
||||
String firstAvailableProvider = '';
|
||||
|
||||
for (var provider in _provider) {
|
||||
|
@ -225,7 +228,8 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
|||
providerName: providerName,
|
||||
createdAt: DateTime.tryParse(date)?.toLocal(),
|
||||
amount: responseJSON['amount_from']?.toString() ?? request.fromAmount,
|
||||
payoutAddress: payoutAddress);
|
||||
payoutAddress: payoutAddress,
|
||||
isSendAll: isSendAll);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -27,7 +27,11 @@ class Trade extends HiveObject {
|
|||
this.password,
|
||||
this.providerId,
|
||||
this.providerName,
|
||||
this.fromWalletAddress
|
||||
this.fromWalletAddress,
|
||||
this.memo,
|
||||
this.txId,
|
||||
this.isRefund,
|
||||
this.isSendAll,
|
||||
}) {
|
||||
if (provider != null) providerRaw = provider.raw;
|
||||
|
||||
|
@ -105,6 +109,18 @@ class Trade extends HiveObject {
|
|||
@HiveField(17)
|
||||
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) {
|
||||
return Trade(
|
||||
id: map['id'] as String,
|
||||
|
@ -115,8 +131,11 @@ class Trade extends HiveObject {
|
|||
map['date'] != null ? DateTime.fromMillisecondsSinceEpoch(map['date'] as int) : null,
|
||||
amount: map['amount'] 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() {
|
||||
|
@ -128,7 +147,11 @@ class Trade extends HiveObject {
|
|||
'date': createdAt != null ? createdAt!.millisecondsSinceEpoch : null,
|
||||
'amount': amount,
|
||||
'wallet_id': walletId,
|
||||
'from_wallet_address': fromWalletAddress
|
||||
'from_wallet_address': fromWalletAddress,
|
||||
'memo': memo,
|
||||
'tx_id': txId,
|
||||
'isRefund': isRefund,
|
||||
'isSendAll': isSendAll,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,8 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
|
|||
static const success = TradeState(raw: 'success', title: 'Success');
|
||||
static TradeState deserialize({required String raw}) {
|
||||
switch (raw) {
|
||||
case 'NOT_FOUND':
|
||||
return notFound;
|
||||
case 'pending':
|
||||
return pending;
|
||||
case 'confirming':
|
||||
|
@ -98,6 +100,7 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
|
|||
case 'sending':
|
||||
return sending;
|
||||
case 'success':
|
||||
case 'done':
|
||||
return success;
|
||||
default:
|
||||
throw Exception('Unexpected token: $raw in TradeState deserialize');
|
||||
|
|
|
@ -149,21 +149,22 @@ Future<void> initializeAppConfigs() async {
|
|||
final unspentCoinsInfoSource = await CakeHive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName);
|
||||
|
||||
await initialSetup(
|
||||
sharedPreferences: await SharedPreferences.getInstance(),
|
||||
nodes: nodes,
|
||||
powNodes: powNodes,
|
||||
walletInfoSource: walletInfoSource,
|
||||
contactSource: contacts,
|
||||
tradesSource: trades,
|
||||
ordersSource: orders,
|
||||
unspentCoinsInfoSource: unspentCoinsInfoSource,
|
||||
// fiatConvertationService: fiatConvertationService,
|
||||
templates: templates,
|
||||
exchangeTemplates: exchangeTemplates,
|
||||
transactionDescriptions: transactionDescriptions,
|
||||
secureStorage: secureStorage,
|
||||
anonpayInvoiceInfo: anonpayInvoiceInfo,
|
||||
initialMigrationVersion: 29);
|
||||
sharedPreferences: await SharedPreferences.getInstance(),
|
||||
nodes: nodes,
|
||||
powNodes: powNodes,
|
||||
walletInfoSource: walletInfoSource,
|
||||
contactSource: contacts,
|
||||
tradesSource: trades,
|
||||
ordersSource: orders,
|
||||
unspentCoinsInfoSource: unspentCoinsInfoSource,
|
||||
// fiatConvertationService: fiatConvertationService,
|
||||
templates: templates,
|
||||
exchangeTemplates: exchangeTemplates,
|
||||
transactionDescriptions: transactionDescriptions,
|
||||
secureStorage: secureStorage,
|
||||
anonpayInvoiceInfo: anonpayInvoiceInfo,
|
||||
initialMigrationVersion: 30,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> initialSetup(
|
||||
|
|
|
@ -129,7 +129,7 @@ class CWPolygon extends Polygon {
|
|||
@override
|
||||
Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) async {
|
||||
final polygonWallet = wallet as PolygonWallet;
|
||||
return await polygonWallet.getErc20Token(contractAddress);
|
||||
return await polygonWallet.getErc20Token(contractAddress, 'polygon');
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -16,6 +16,7 @@ bool isWalletConnectCompatibleChain(WalletType walletType) {
|
|||
switch (walletType) {
|
||||
case WalletType.polygon:
|
||||
case WalletType.ethereum:
|
||||
case WalletType.solana:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
|
|
@ -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_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/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_list_page.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
|
||||
|
@ -253,6 +254,12 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
|||
builder: (_) =>
|
||||
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:
|
||||
return CupertinoPageRoute<void>(
|
||||
builder: (_) => getIt.get<AddressEditOrCreatePage>(param1: settings.arguments));
|
||||
|
|
|
@ -12,6 +12,7 @@ class Routes {
|
|||
static const dashboard = '/dashboard';
|
||||
static const send = '/send';
|
||||
static const transactionDetails = '/transaction_info';
|
||||
static const bumpFeePage = '/bump_fee_page';
|
||||
static const receive = '/receive';
|
||||
static const newSubaddress = '/new_subaddress';
|
||||
static const walletEdit = '/walletEdit';
|
||||
|
|
|
@ -74,8 +74,23 @@ class CWSolana extends Solana {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> addSPLToken(WalletBase wallet, CryptoCurrency token) async =>
|
||||
await (wallet as SolanaWallet).addSPLToken(token as SPLToken);
|
||||
Future<void> addSPLToken(
|
||||
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
|
||||
Future<void> deleteSPLToken(WalletBase wallet, CryptoCurrency token) async =>
|
||||
|
@ -115,4 +130,9 @@ class CWSolana extends Solana {
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
double? getEstimateFees(WalletBase wallet) {
|
||||
return (wallet as SolanaWallet).estimatedFee;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:cake_wallet/generated/i18n.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/scollable_with_bottom_section.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/view_model/dashboard/dashboard_view_model.dart';
|
||||
|
@ -25,45 +26,46 @@ class BuySellOptionsPage extends BasePage {
|
|||
? dashboardViewModel.availableBuyProviders
|
||||
: dashboardViewModel.availableSellProviders;
|
||||
|
||||
return Container(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 330),
|
||||
child: Column(
|
||||
children: [
|
||||
...availableProviders.map((provider) {
|
||||
final icon = Image.asset(
|
||||
isLightMode ? provider.lightIcon : provider.darkIcon,
|
||||
height: 40,
|
||||
width: 40,
|
||||
);
|
||||
return ScrollableWithBottomSection(
|
||||
content: Container(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 330),
|
||||
child: Column(
|
||||
children: [
|
||||
...availableProviders.map((provider) {
|
||||
final icon = Image.asset(
|
||||
isLightMode ? provider.lightIcon : provider.darkIcon,
|
||||
height: 40,
|
||||
width: 40,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: OptionTile(
|
||||
image: icon,
|
||||
title: provider.toString(),
|
||||
description: provider.providerDescription,
|
||||
onPressed: () => provider.launchProvider(context, isBuyAction),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
Spacer(),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
|
||||
child: Text(
|
||||
isBuyAction
|
||||
? S.of(context).select_buy_provider_notice
|
||||
: S.of(context).select_sell_provider_notice,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: OptionTile(
|
||||
image: icon,
|
||||
title: provider.toString(),
|
||||
description: provider.providerDescription,
|
||||
onPressed: () => provider.launchProvider(context, isBuyAction),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomSection: Padding(
|
||||
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
|
||||
child: Text(
|
||||
isBuyAction
|
||||
? S.of(context).select_buy_provider_notice
|
||||
: S.of(context).select_sell_provider_notice,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -60,7 +60,7 @@ class BuyWebViewPageBodyState extends State<BuyWebViewPageBody> {
|
|||
_saveOrder(keyword: 'completed', splitSymbol: '/');
|
||||
}
|
||||
|
||||
if (widget.buyViewModel.selectedProvider is MoonPayBuyProvider) {
|
||||
if (widget.buyViewModel.selectedProvider is MoonPayProvider) {
|
||||
_saveOrder(keyword: 'transactionId', splitSymbol: '=');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
|||
final TextEditingController _tokenNameController = TextEditingController();
|
||||
final TextEditingController _tokenSymbolController = TextEditingController();
|
||||
final TextEditingController _tokenDecimalController = TextEditingController();
|
||||
final TextEditingController _tokenIconPathController = TextEditingController();
|
||||
|
||||
final FocusNode _contractAddressFocusNode = FocusNode();
|
||||
final FocusNode _tokenNameFocusNode = FocusNode();
|
||||
|
@ -83,6 +84,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
|||
_tokenNameController.text = widget.token!.name;
|
||||
_tokenSymbolController.text = widget.token!.title;
|
||||
_tokenDecimalController.text = widget.token!.decimals.toString();
|
||||
_tokenIconPathController.text = widget.token?.iconPath ?? '';
|
||||
}
|
||||
|
||||
if (widget.initialContractAddress != null) {
|
||||
|
@ -195,12 +197,15 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
|||
onPressed: () async {
|
||||
if (_formKey.currentState!.validate() &&
|
||||
(!_showDisclaimer || _disclaimerChecked)) {
|
||||
await widget.homeSettingsViewModel.addToken(Erc20Token(
|
||||
name: _tokenNameController.text,
|
||||
symbol: _tokenSymbolController.text,
|
||||
await widget.homeSettingsViewModel.addToken(
|
||||
token: CryptoCurrency(
|
||||
name: _tokenNameController.text,
|
||||
title: _tokenSymbolController.text.toUpperCase(),
|
||||
decimals: int.parse(_tokenDecimalController.text),
|
||||
iconPath: _tokenIconPathController.text,
|
||||
),
|
||||
contractAddress: _contractAddressController.text,
|
||||
decimal: int.parse(_tokenDecimalController.text),
|
||||
));
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
@ -226,6 +231,8 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
|||
if (token != null) {
|
||||
if (_tokenNameController.text.isEmpty) _tokenNameController.text = token.name;
|
||||
if (_tokenSymbolController.text.isEmpty) _tokenSymbolController.text = token.title;
|
||||
if (_tokenIconPathController.text.isEmpty)
|
||||
_tokenIconPathController.text = token.iconPath ?? '';
|
||||
if (_tokenDecimalController.text.isEmpty)
|
||||
_tokenDecimalController.text = token.decimals.toString();
|
||||
}
|
||||
|
@ -303,10 +310,15 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
|||
if (text?.isEmpty ?? true) {
|
||||
return S.of(context).field_required;
|
||||
}
|
||||
|
||||
if (int.tryParse(text!) == null) {
|
||||
return S.of(context).invalid_input;
|
||||
}
|
||||
|
||||
if (int.tryParse(text) == 0) {
|
||||
return S.current.decimals_cannot_be_zero;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
),
|
||||
|
|
|
@ -129,23 +129,27 @@ class HomeSettingsPage extends BasePage {
|
|||
'token': token,
|
||||
});
|
||||
},
|
||||
leading: CakeImageWidget(
|
||||
imageUrl: token.iconPath,
|
||||
height: 40,
|
||||
width: 40,
|
||||
displayOnError: Container(
|
||||
height: 30.0,
|
||||
width: 30.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
token.title.substring(0, min(token.title.length, 2)),
|
||||
style: TextStyle(fontSize: 11),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.grey.shade400,
|
||||
leading: Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: token.iconPath,
|
||||
height: 40,
|
||||
width: 40,
|
||||
displayOnError: Container(
|
||||
height: 30.0,
|
||||
width: 30.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
token.title.substring(0, min(token.title.length, 2)),
|
||||
style: TextStyle(fontSize: 11),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
|
|
|
@ -323,7 +323,7 @@ class BalanceRowWidget extends StatelessWidget {
|
|||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).extension<BalancePageTheme>()!.textColor,
|
||||
height: 1)),
|
||||
],
|
||||
|
@ -334,24 +334,28 @@ class BalanceRowWidget extends StatelessWidget {
|
|||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: currency.iconPath,
|
||||
height: 40,
|
||||
width: 40,
|
||||
displayOnError: Container(
|
||||
height: 30.0,
|
||||
width: 30.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
currency.title.substring(0, min(currency.title.length, 2)),
|
||||
style: TextStyle(fontSize: 11),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.grey.shade400,
|
||||
Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: currency.iconPath,
|
||||
height: 40,
|
||||
width: 40,
|
||||
displayOnError: Container(
|
||||
height: 30.0,
|
||||
width: 30.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
currency.title.substring(0, min(currency.title.length, 2)),
|
||||
style: TextStyle(fontSize: 11),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
|
@ -410,9 +414,7 @@ class BalanceRowWidget extends StatelessWidget {
|
|||
fontSize: 20,
|
||||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context)
|
||||
.extension<BalancePageTheme>()!
|
||||
.balanceAmountColor,
|
||||
color: Theme.of(context).extension<BalancePageTheme>()!.balanceAmountColor,
|
||||
height: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
|
|
|
@ -9,7 +9,7 @@ class FilterTile extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0),
|
||||
padding: EdgeInsets.symmetric(vertical: 6.0, horizontal: 24.0),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ class SyncIndicatorIcon extends StatelessWidget {
|
|||
static const String created = 'created';
|
||||
static const String fetching = 'fetching';
|
||||
static const String finished = 'finished';
|
||||
static const String success = 'success';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -45,6 +46,7 @@ class SyncIndicatorIcon extends StatelessWidget {
|
|||
indicatorColor = Colors.red;
|
||||
break;
|
||||
case finished:
|
||||
case success:
|
||||
indicatorColor = PaletteDark.brightGreen;
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -34,7 +34,9 @@ class TradeRow extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_getPoweredImage(provider)!,
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: Image.asset(provider.image, width: 36, height: 36)),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
|
@ -69,38 +71,4 @@ class TradeRow extends StatelessWidget {
|
|||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget? _getPoweredImage(ExchangeProviderDescription provider) {
|
||||
Widget? image;
|
||||
|
||||
switch (provider) {
|
||||
case ExchangeProviderDescription.xmrto:
|
||||
image = Image.asset('assets/images/xmrto.png', height: 36, width: 36);
|
||||
break;
|
||||
case ExchangeProviderDescription.changeNow:
|
||||
image = Image.asset('assets/images/changenow.png', height: 36, width: 36);
|
||||
break;
|
||||
case ExchangeProviderDescription.morphToken:
|
||||
image = Image.asset('assets/images/morph.png', height: 36, width: 36);
|
||||
break;
|
||||
case ExchangeProviderDescription.sideShift:
|
||||
image = Image.asset('assets/images/sideshift.png', width: 36, height: 36);
|
||||
break;
|
||||
case ExchangeProviderDescription.simpleSwap:
|
||||
image = Image.asset('assets/images/simpleSwap.png', width: 36, height: 36);
|
||||
break;
|
||||
case ExchangeProviderDescription.trocador:
|
||||
image = ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: Image.asset('assets/images/trocador.png', width: 36, height: 36));
|
||||
break;
|
||||
case ExchangeProviderDescription.exolix:
|
||||
image = Image.asset('assets/images/exolix.png', width: 36, height: 36);
|
||||
break;
|
||||
default:
|
||||
image = null;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
||||
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
|
||||
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
|
||||
import 'package:cake_wallet/core/auth_service.dart';
|
||||
|
@ -60,7 +62,7 @@ class ExchangePage extends BasePage {
|
|||
final _receiveAmountFocus = FocusNode();
|
||||
final _receiveAddressFocus = FocusNode();
|
||||
final _receiveAmountDebounce = Debounce(Duration(milliseconds: 500));
|
||||
final _depositAmountDebounce = Debounce(Duration(milliseconds: 500));
|
||||
Debounce _depositAmountDebounce = Debounce(Duration(milliseconds: 500));
|
||||
var _isReactionsSet = false;
|
||||
|
||||
final arrowBottomPurple = Image.asset(
|
||||
|
@ -184,7 +186,13 @@ class ExchangePage extends BasePage {
|
|||
StandardCheckbox(
|
||||
value: exchangeViewModel.isFixedRateMode,
|
||||
caption: S.of(context).fixed_rate,
|
||||
onChanged: (value) => exchangeViewModel.isFixedRateMode = value,
|
||||
onChanged: (value) {
|
||||
if (value) {
|
||||
exchangeViewModel.enableFixedRateMode();
|
||||
} else {
|
||||
exchangeViewModel.isFixedRateMode = false;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
)),
|
||||
|
@ -431,7 +439,9 @@ class ExchangePage extends BasePage {
|
|||
}
|
||||
if (state is TradeIsCreatedSuccessfully) {
|
||||
exchangeViewModel.reset();
|
||||
Navigator.of(context).pushNamed(Routes.exchangeConfirm);
|
||||
(exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.thorChain)
|
||||
? Navigator.of(context).pushReplacementNamed(Routes.exchangeTrade)
|
||||
: Navigator.of(context).pushReplacementNamed(Routes.exchangeConfirm);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -470,6 +480,13 @@ class ExchangePage extends BasePage {
|
|||
if (depositAmountController.text != exchangeViewModel.depositAmount &&
|
||||
depositAmountController.text != S.of(context).all) {
|
||||
exchangeViewModel.isSendAllEnabled = false;
|
||||
final isThorChain = exchangeViewModel.selectedProviders
|
||||
.any((provider) => provider is ThorChainExchangeProvider);
|
||||
|
||||
_depositAmountDebounce = isThorChain
|
||||
? Debounce(Duration(milliseconds: 1000))
|
||||
: Debounce(Duration(milliseconds: 500));
|
||||
|
||||
_depositAmountDebounce.run(() {
|
||||
exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
|
||||
exchangeViewModel.isReceiveAmountEntered = false;
|
||||
|
@ -517,7 +534,7 @@ class ExchangePage extends BasePage {
|
|||
|
||||
_receiveAmountFocus.addListener(() {
|
||||
if (_receiveAmountFocus.hasFocus) {
|
||||
exchangeViewModel.isFixedRateMode = true;
|
||||
exchangeViewModel.enableFixedRateMode();
|
||||
}
|
||||
// exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
|
||||
});
|
||||
|
|
|
@ -485,14 +485,14 @@ class ExchangeCardState extends State<ExchangeCard> {
|
|||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertWithTwoActions(
|
||||
alertTitle: S.of(context).overwrite_amount,
|
||||
alertContent: S.of(context).qr_payment_amount,
|
||||
rightButtonText: S.of(context).ok,
|
||||
leftButtonText: S.of(context).cancel,
|
||||
alertTitle: S.of(dialogContext).overwrite_amount,
|
||||
alertContent: S.of(dialogContext).qr_payment_amount,
|
||||
rightButtonText: S.of(dialogContext).ok,
|
||||
leftButtonText: S.of(dialogContext).cancel,
|
||||
actionRightButton: () {
|
||||
widget.amountFocusNode?.requestFocus();
|
||||
amountController.text = paymentRequest.amount;
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
actionLeftButton: () => Navigator.of(dialogContext).pop());
|
||||
});
|
||||
|
|
|
@ -262,6 +262,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
|||
fee: S.of(popupContext).send_fee,
|
||||
feeValue: widget.exchangeTradeViewModel.sendViewModel
|
||||
.pendingTransaction!.feeFormatted,
|
||||
feeRate: widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.feeRate,
|
||||
rightButtonText: S.of(popupContext).send,
|
||||
leftButtonText: S.of(popupContext).cancel,
|
||||
actionRightButton: () async {
|
||||
|
|
|
@ -100,7 +100,10 @@ class SendPage extends BasePage {
|
|||
AppBarStyle get appBarStyle => AppBarStyle.transparent;
|
||||
|
||||
double _sendCardHeight(BuildContext context) {
|
||||
final double initialHeight = sendViewModel.hasCoinControl ? 500 : 465;
|
||||
double initialHeight = 450;
|
||||
if (sendViewModel.hasCoinControl) {
|
||||
initialHeight += 35;
|
||||
}
|
||||
|
||||
if (!responsiveLayoutUtil.shouldRenderMobileUI) {
|
||||
return initialHeight - 66;
|
||||
|
@ -190,7 +193,7 @@ class SendPage extends BasePage {
|
|||
},
|
||||
)),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 10, left: 24, right: 24, bottom: 10),
|
||||
padding: EdgeInsets.only(left: 24, right: 24, bottom: 10),
|
||||
child: Container(
|
||||
height: 10,
|
||||
child: Observer(
|
||||
|
@ -426,6 +429,7 @@ class SendPage extends BasePage {
|
|||
fee: isEVMCompatibleChain(sendViewModel.walletType)
|
||||
? S.of(_dialogContext).send_estimated_fee
|
||||
: S.of(_dialogContext).send_fee,
|
||||
feeRate: sendViewModel.pendingTransaction!.feeRate,
|
||||
feeValue: sendViewModel.pendingTransaction!.feeFormatted,
|
||||
feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted,
|
||||
outputs: sendViewModel.outputs,
|
||||
|
@ -455,7 +459,7 @@ class SendPage extends BasePage {
|
|||
? '. ${S.of(_dialogContext).waitFewSecondForTxUpdate}' : '';
|
||||
|
||||
final newContactMessage = newContactAddress != null
|
||||
? '\n${S.of(context).add_contact_to_address_book}' : '';
|
||||
? '\n${S.of(_dialogContext).add_contact_to_address_book}' : '';
|
||||
|
||||
final alertContent =
|
||||
"$successMessage$waitMessage$newContactMessage";
|
||||
|
|
|
@ -16,6 +16,7 @@ class ConfirmSendingAlert extends BaseAlertDialog {
|
|||
required this.amountValue,
|
||||
required this.fiatAmountValue,
|
||||
required this.fee,
|
||||
this.feeRate,
|
||||
required this.feeValue,
|
||||
required this.feeFiatAmount,
|
||||
required this.outputs,
|
||||
|
@ -36,6 +37,7 @@ class ConfirmSendingAlert extends BaseAlertDialog {
|
|||
final String amountValue;
|
||||
final String fiatAmountValue;
|
||||
final String fee;
|
||||
final String? feeRate;
|
||||
final String feeValue;
|
||||
final String feeFiatAmount;
|
||||
final List<Output> outputs;
|
||||
|
@ -90,6 +92,7 @@ class ConfirmSendingAlert extends BaseAlertDialog {
|
|||
amountValue: amountValue,
|
||||
fiatAmountValue: fiatAmountValue,
|
||||
fee: fee,
|
||||
feeRate: feeRate,
|
||||
feeValue: feeValue,
|
||||
feeFiatAmount: feeFiatAmount,
|
||||
outputs: outputs);
|
||||
|
@ -103,6 +106,7 @@ class ConfirmSendingAlertContent extends StatefulWidget {
|
|||
required this.amountValue,
|
||||
required this.fiatAmountValue,
|
||||
required this.fee,
|
||||
this.feeRate,
|
||||
required this.feeValue,
|
||||
required this.feeFiatAmount,
|
||||
required this.outputs});
|
||||
|
@ -113,6 +117,7 @@ class ConfirmSendingAlertContent extends StatefulWidget {
|
|||
final String amountValue;
|
||||
final String fiatAmountValue;
|
||||
final String fee;
|
||||
final String? feeRate;
|
||||
final String feeValue;
|
||||
final String feeFiatAmount;
|
||||
final List<Output> outputs;
|
||||
|
@ -125,6 +130,7 @@ class ConfirmSendingAlertContent extends StatefulWidget {
|
|||
amountValue: amountValue,
|
||||
fiatAmountValue: fiatAmountValue,
|
||||
fee: fee,
|
||||
feeRate: feeRate,
|
||||
feeValue: feeValue,
|
||||
feeFiatAmount: feeFiatAmount,
|
||||
outputs: outputs);
|
||||
|
@ -138,6 +144,7 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
|
|||
required this.amountValue,
|
||||
required this.fiatAmountValue,
|
||||
required this.fee,
|
||||
this.feeRate,
|
||||
required this.feeValue,
|
||||
required this.feeFiatAmount,
|
||||
required this.outputs})
|
||||
|
@ -153,6 +160,7 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
|
|||
final String amountValue;
|
||||
final String fiatAmountValue;
|
||||
final String fee;
|
||||
final String? feeRate;
|
||||
final String feeValue;
|
||||
final String feeFiatAmount;
|
||||
final List<Output> outputs;
|
||||
|
@ -183,7 +191,7 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
|
|||
|
||||
return Stack(alignment: Alignment.center, clipBehavior: Clip.none, children: [
|
||||
Container(
|
||||
height: 200,
|
||||
height: feeRate != null ? 250 : 200,
|
||||
child: SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
|
@ -311,6 +319,36 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
|
|||
)
|
||||
],
|
||||
)),
|
||||
if (feeRate != null && feeRate!.isNotEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 16),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
S.current.send_estimated_fee,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontFamily: 'Lato',
|
||||
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"$feeRate sat/byte",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'Lato',
|
||||
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
)
|
||||
],
|
||||
)),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 16),
|
||||
child: Column(
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
|
||||
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
|
||||
import 'package:cake_wallet/src/widgets/picker.dart';
|
||||
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/utils/payment_request.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/picker.dart';
|
||||
import 'package:cake_wallet/view_model/send/output.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
@ -323,8 +324,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
|||
? sendViewModel.allAmountValidator
|
||||
: sendViewModel.amountValidator,
|
||||
),
|
||||
if (!sendViewModel.isBatchSending &&
|
||||
sendViewModel.shouldDisplaySendALL)
|
||||
if (!sendViewModel.isBatchSending)
|
||||
Positioned(
|
||||
top: 2,
|
||||
right: 0,
|
||||
|
@ -456,7 +456,9 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
|||
if (sendViewModel.hasFees)
|
||||
Observer(
|
||||
builder: (_) => GestureDetector(
|
||||
onTap: () => _setTransactionPriority(context),
|
||||
onTap: sendViewModel.hasFeesPriority
|
||||
? () => pickTransactionPriority(context)
|
||||
: () {},
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: Row(
|
||||
|
@ -668,22 +670,41 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
|||
_effectsInstalled = true;
|
||||
}
|
||||
|
||||
Future<void> _setTransactionPriority(BuildContext context) async {
|
||||
Future<void> pickTransactionPriority(BuildContext context) async {
|
||||
final items = priorityForWalletType(sendViewModel.walletType);
|
||||
final selectedItem = items.indexOf(sendViewModel.transactionPriority);
|
||||
final customItemIndex = sendViewModel.getCustomPriorityIndex(items);
|
||||
final isBitcoinWallet = sendViewModel.walletType == WalletType.bitcoin;
|
||||
double? customFeeRate = isBitcoinWallet ? sendViewModel.customBitcoinFeeRate.toDouble() : null;
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (_) => Picker(
|
||||
items: items,
|
||||
displayItem: sendViewModel.displayFeeRate,
|
||||
selectedAtIndex: selectedItem,
|
||||
title: S.of(context).please_select,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
onItemSelected: (TransactionPriority priority) =>
|
||||
sendViewModel.setTransactionPriority(priority),
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
int selectedIdx = selectedItem;
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return Picker(
|
||||
items: items,
|
||||
displayItem: (TransactionPriority priority) =>
|
||||
sendViewModel.displayFeeRate(priority, customFeeRate?.round()),
|
||||
selectedAtIndex: selectedIdx,
|
||||
customItemIndex: customItemIndex,
|
||||
title: S.of(context).please_select,
|
||||
headerEnabled: !isBitcoinWallet,
|
||||
closeOnItemSelected: !isBitcoinWallet,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
sliderValue: customFeeRate,
|
||||
onSliderChanged: (double newValue) => setState(() => customFeeRate = newValue),
|
||||
onItemSelected: (TransactionPriority priority) {
|
||||
sendViewModel.setTransactionPriority(priority);
|
||||
setState(() => selectedIdx = items.indexOf(priority));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
if (isBitcoinWallet) sendViewModel.customBitcoinFeeRate = customFeeRate!.round();
|
||||
}
|
||||
|
||||
void _presentPicker(BuildContext context) {
|
||||
|
|
|
@ -15,7 +15,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_list.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
|
@ -43,7 +42,7 @@ class ConnectionSyncPage extends BasePage {
|
|||
title: S.current.rescan,
|
||||
handler: (context) => Navigator.of(context).pushNamed(Routes.rescan),
|
||||
),
|
||||
if (DeviceInfo.instance.isMobile) ...[
|
||||
if (DeviceInfo.instance.isMobile && FeatureFlag.isBackgroundSyncEnabled) ...[
|
||||
Observer(builder: (context) {
|
||||
return SettingsPickerCell<SyncMode>(
|
||||
title: S.current.background_sync_mode,
|
||||
|
|
|
@ -2,11 +2,12 @@ import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
|
|||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/widgets/setting_priority_picker_cell.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/widgets/settings_version_cell.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_list.dart';
|
||||
import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
|
@ -27,13 +28,23 @@ class OtherSettingsPage extends BasePage {
|
|||
child: Column(
|
||||
children: [
|
||||
if (_otherSettingsViewModel.displayTransactionPriority)
|
||||
SettingsPickerCell(
|
||||
title: S.current.settings_fee_priority,
|
||||
items: priorityForWalletType(_otherSettingsViewModel.walletType),
|
||||
displayItem: _otherSettingsViewModel.getDisplayPriority,
|
||||
selectedItem: _otherSettingsViewModel.transactionPriority,
|
||||
onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected,
|
||||
),
|
||||
_otherSettingsViewModel.walletType == WalletType.bitcoin ?
|
||||
SettingsPriorityPickerCell(
|
||||
title: S.current.settings_fee_priority,
|
||||
items: priorityForWalletType(_otherSettingsViewModel.walletType),
|
||||
displayItem: _otherSettingsViewModel.getDisplayBitcoinPriority,
|
||||
selectedItem: _otherSettingsViewModel.transactionPriority,
|
||||
customItemIndex: _otherSettingsViewModel.customPriorityItemIndex,
|
||||
onItemSelected: _otherSettingsViewModel.onDisplayBitcoinPrioritySelected,
|
||||
customValue: _otherSettingsViewModel.customBitcoinFeeRate,
|
||||
) :
|
||||
SettingsPickerCell(
|
||||
title: S.current.settings_fee_priority,
|
||||
items: priorityForWalletType(_otherSettingsViewModel.walletType),
|
||||
displayItem: _otherSettingsViewModel.getDisplayPriority,
|
||||
selectedItem: _otherSettingsViewModel.transactionPriority,
|
||||
onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected,
|
||||
),
|
||||
if (_otherSettingsViewModel.changeRepresentativeEnabled)
|
||||
SettingsCellWithArrow(
|
||||
title: S.current.change_rep,
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cake_wallet/src/widgets/picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_list.dart';
|
||||
|
||||
class SettingsPriorityPickerCell<ItemType> extends StandardListRow {
|
||||
SettingsPriorityPickerCell(
|
||||
{required String title,
|
||||
required this.selectedItem,
|
||||
required this.items,
|
||||
this.displayItem,
|
||||
this.images,
|
||||
this.searchHintText,
|
||||
this.isGridView = false,
|
||||
this.matchingCriteria,
|
||||
this.customValue,
|
||||
this.customItemIndex,
|
||||
this.onItemSelected})
|
||||
: super(
|
||||
title: title,
|
||||
isSelected: false,
|
||||
onTap: (BuildContext context) async {
|
||||
var selectedAtIndex = items.indexOf(selectedItem);
|
||||
double sliderValue = customValue ?? 0.0;
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return Picker(
|
||||
items: items,
|
||||
displayItem: (ItemType item) => displayItem!(item, sliderValue.round()),
|
||||
selectedAtIndex: selectedAtIndex,
|
||||
customItemIndex: customItemIndex,
|
||||
headerEnabled: false,
|
||||
closeOnItemSelected: false,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
sliderValue: sliderValue,
|
||||
onSliderChanged: (double newValue) =>
|
||||
setState(() => sliderValue = newValue),
|
||||
onItemSelected: (ItemType priority) {
|
||||
setState(() => selectedAtIndex = items.indexOf(priority));
|
||||
onItemSelected?.call(priority, sliderValue);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
onItemSelected?.call(items[selectedAtIndex], sliderValue);
|
||||
});
|
||||
|
||||
final ItemType selectedItem;
|
||||
final List<ItemType> items;
|
||||
final void Function(ItemType item, double customValue)? onItemSelected;
|
||||
final String Function(ItemType item, int value)? displayItem;
|
||||
final List<Image>? images;
|
||||
final String? searchHintText;
|
||||
final bool isGridView;
|
||||
final bool Function(ItemType, String)? matchingCriteria;
|
||||
double? customValue;
|
||||
int? customItemIndex;
|
||||
|
||||
@override
|
||||
Widget buildTrailing(BuildContext context) {
|
||||
return Text(
|
||||
displayItem?.call(selectedItem,customValue?.round() ?? 0) ?? selectedItem.toString(),
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
|
||||
|
||||
class StandardPickerListItem<T> extends TransactionDetailsListItem {
|
||||
StandardPickerListItem(
|
||||
{required String title,
|
||||
required String value,
|
||||
required this.items,
|
||||
required this.displayItem,
|
||||
required this.onSliderChanged,
|
||||
required this.onItemSelected,
|
||||
required this.selectedIdx,
|
||||
required this.customItemIndex,
|
||||
required this.customValue})
|
||||
: super(title: title, value: value);
|
||||
|
||||
final List<T> items;
|
||||
final String Function(T item, double sliderValue) displayItem;
|
||||
final Function(double) onSliderChanged;
|
||||
final Function(T) onItemSelected;
|
||||
final int selectedIdx;
|
||||
final int customItemIndex;
|
||||
double customValue;
|
||||
}
|
199
lib/src/screens/transaction_details/rbf_details_page.dart
Normal file
|
@ -0,0 +1,199 @@
|
|||
import 'package:cake_wallet/core/execution_state.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/transaction_expandable_list_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:cake_wallet/src/widgets/list_row.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_expandable_list.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_list.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_picker_list.dart';
|
||||
import 'package:cake_wallet/utils/show_bar.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
|
||||
import 'package:cake_wallet/view_model/transaction_details_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class RBFDetailsPage extends BasePage {
|
||||
RBFDetailsPage({required this.transactionDetailsViewModel});
|
||||
|
||||
@override
|
||||
String get title => S.current.bump_fee;
|
||||
|
||||
final TransactionDetailsViewModel transactionDetailsViewModel;
|
||||
|
||||
bool _effectsInstalled = false;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
_setEffects(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SectionStandardList(
|
||||
sectionCount: 1,
|
||||
itemCounter: (int _) => transactionDetailsViewModel.RBFListItems.length,
|
||||
itemBuilder: (__, index) {
|
||||
final item = transactionDetailsViewModel.RBFListItems[index];
|
||||
|
||||
if (item is StandartListItem) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: item.value));
|
||||
showBar<void>(context, S.of(context).transaction_details_copied(item.title));
|
||||
},
|
||||
child: ListRow(title: '${item.title}:', value: item.value),
|
||||
);
|
||||
}
|
||||
|
||||
if (item is StandardExpandableListItem) {
|
||||
return StandardExpandableList(
|
||||
title: '${item.title}: ${item.expandableItems.length}',
|
||||
expandableItems: item.expandableItems,
|
||||
);
|
||||
}
|
||||
|
||||
if (item is StandardPickerListItem) {
|
||||
return StandardPickerList(
|
||||
title: item.title,
|
||||
value: item.value,
|
||||
items: item.items,
|
||||
displayItem: item.displayItem,
|
||||
onSliderChanged: item.onSliderChanged,
|
||||
onItemSelected: item.onItemSelected,
|
||||
selectedIdx: item.selectedIdx,
|
||||
customItemIndex: item.customItemIndex,
|
||||
customValue: item.customValue,
|
||||
);
|
||||
}
|
||||
|
||||
if (item is TextFieldListItem) {
|
||||
return TextFieldListRow(
|
||||
title: item.title,
|
||||
value: item.value,
|
||||
onSubmitted: item.onSubmitted,
|
||||
);
|
||||
}
|
||||
|
||||
return Container();
|
||||
}),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Observer(
|
||||
builder: (_) => LoadingPrimaryButton(
|
||||
onPressed: () async {
|
||||
transactionDetailsViewModel
|
||||
.replaceByFee(transactionDetailsViewModel.newFee.toString());
|
||||
},
|
||||
text: S.of(context).send,
|
||||
isLoading:
|
||||
transactionDetailsViewModel.sendViewModel.state is IsExecutingState,
|
||||
color: Theme.of(context).primaryColor,
|
||||
textColor: Colors.white,
|
||||
))),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _setEffects(BuildContext context) {
|
||||
if (_effectsInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
reaction((_) => transactionDetailsViewModel.sendViewModel.state, (ExecutionState state) {
|
||||
if (state is FailureState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext popupContext) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(popupContext).error,
|
||||
alertContent: state.error,
|
||||
buttonText: S.of(popupContext).ok,
|
||||
buttonAction: () => Navigator.of(popupContext).pop());
|
||||
});
|
||||
});
|
||||
}
|
||||
if (state is AwaitingConfirmationState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext popupContext) {
|
||||
return AlertWithTwoActions(
|
||||
alertTitle: state.title ?? '',
|
||||
alertContent: state.message ?? '',
|
||||
rightButtonText: S.of(context).ok,
|
||||
leftButtonText: S.of(context).cancel,
|
||||
actionRightButton: () {
|
||||
state.onConfirm?.call();
|
||||
Navigator.of(popupContext).pop();
|
||||
},
|
||||
actionLeftButton: () {
|
||||
state.onCancel?.call();
|
||||
Navigator.of(popupContext).pop();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (state is ExecutedSuccessfullyState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext popupContext) {
|
||||
return ConfirmSendingAlert(
|
||||
alertTitle: S.of(popupContext).confirm_sending,
|
||||
amount: S.of(popupContext).send_amount,
|
||||
amountValue: transactionDetailsViewModel
|
||||
.sendViewModel.pendingTransaction!.amountFormatted,
|
||||
fee: S.of(popupContext).send_fee,
|
||||
feeValue:
|
||||
transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted,
|
||||
rightButtonText: S.of(popupContext).send,
|
||||
leftButtonText: S.of(popupContext).cancel,
|
||||
actionRightButton: () async {
|
||||
Navigator.of(popupContext).pop();
|
||||
await transactionDetailsViewModel.sendViewModel.commitTransaction();
|
||||
// transactionStatePopup();
|
||||
},
|
||||
actionLeftButton: () => Navigator.of(popupContext).pop(),
|
||||
feeFiatAmount:
|
||||
transactionDetailsViewModel.pendingTransactionFeeFiatAmountFormatted,
|
||||
fiatAmountValue:
|
||||
transactionDetailsViewModel.pendingTransactionFiatAmountValueFormatted,
|
||||
outputs: transactionDetailsViewModel.sendViewModel.outputs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (state is TransactionCommitted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext popupContext) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(popupContext).sending,
|
||||
alertContent: S.of(popupContext).transaction_sent,
|
||||
buttonText: S.of(popupContext).ok,
|
||||
buttonAction: () => Navigator.of(popupContext).pop());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_effectsInstalled = true;
|
||||
}
|
||||
}
|
|
@ -1,15 +1,18 @@
|
|||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/blockexplorer_list_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart';
|
||||
import 'package:cake_wallet/src/widgets/list_row.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_list.dart';
|
||||
import 'package:cake_wallet/utils/show_bar.dart';
|
||||
import 'package:cake_wallet/view_model/transaction_details_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/list_row.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/blockexplorer_list_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class TransactionDetailsPage extends BasePage {
|
||||
TransactionDetailsPage({required this.transactionDetailsViewModel});
|
||||
|
@ -21,41 +24,62 @@ class TransactionDetailsPage extends BasePage {
|
|||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return SectionStandardList(
|
||||
sectionCount: 1,
|
||||
itemCounter: (int _) => transactionDetailsViewModel.items.length,
|
||||
itemBuilder: (__, index) {
|
||||
final item = transactionDetailsViewModel.items[index];
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SectionStandardList(
|
||||
sectionCount: 1,
|
||||
itemCounter: (int _) => transactionDetailsViewModel.items.length,
|
||||
itemBuilder: (__, index) {
|
||||
final item = transactionDetailsViewModel.items[index];
|
||||
|
||||
if (item is StandartListItem) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: item.value));
|
||||
showBar<void>(context,
|
||||
S.of(context).transaction_details_copied(item.title));
|
||||
},
|
||||
child:
|
||||
ListRow(title: '${item.title}:', value: item.value),
|
||||
);
|
||||
}
|
||||
if (item is StandartListItem) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: item.value));
|
||||
showBar<void>(context, S.of(context).transaction_details_copied(item.title));
|
||||
},
|
||||
child: ListRow(title: '${item.title}:', value: item.value),
|
||||
);
|
||||
}
|
||||
|
||||
if (item is BlockExplorerListItem) {
|
||||
return GestureDetector(
|
||||
onTap: item.onTap,
|
||||
child:
|
||||
ListRow(title: '${item.title}:', value: item.value),
|
||||
);
|
||||
}
|
||||
if (item is BlockExplorerListItem) {
|
||||
return GestureDetector(
|
||||
onTap: item.onTap,
|
||||
child: ListRow(title: '${item.title}:', value: item.value),
|
||||
);
|
||||
}
|
||||
|
||||
if (item is TextFieldListItem) {
|
||||
return TextFieldListRow(
|
||||
title: item.title,
|
||||
value: item.value,
|
||||
onSubmitted: item.onSubmitted,
|
||||
);
|
||||
}
|
||||
if (item is TextFieldListItem) {
|
||||
return TextFieldListRow(
|
||||
title: item.title,
|
||||
value: item.value,
|
||||
onSubmitted: item.onSubmitted,
|
||||
);
|
||||
}
|
||||
|
||||
return Container();
|
||||
});
|
||||
return Container();
|
||||
}),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
if (transactionDetailsViewModel.canReplaceByFee) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SelectButton(
|
||||
text: S.of(context).bump_fee,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pushNamed(Routes.bumpFeePage,
|
||||
arguments: transactionDetailsViewModel.transactionInfo);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
|
||||
|
||||
class StandardExpandableListItem<T> extends TransactionDetailsListItem {
|
||||
StandardExpandableListItem({required String title, required this.expandableItems})
|
||||
: super(title: title, value: '');
|
||||
final List<T> expandableItems;
|
||||
}
|