Merge branch 'main' of https://github.com/cake-tech/cake_wallet into bitcoin-derivations
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 |
4
.github/workflows/pr_test_build.yml
vendored
|
@ -139,11 +139,13 @@ 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
|
||||
echo "const robinhoodCIdApiSecret = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
|
||||
echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
|
||||
echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart
|
||||
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
|
||||
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/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
|
||||
|
||||
|
|
12
SECURITY.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you need to report a vulnerability, please either:
|
||||
|
||||
* Open a security advisory: https://github.com/cake-tech/cake_wallet/security/advisories/new
|
||||
* Send an email to `dev@cakewallet.com` with details on the vulnerability
|
||||
|
||||
## Supported Versions
|
||||
|
||||
As we don't maintain prevoius versions of the app, only the latest release for each platform is supported and any updates will bump the version number.
|
BIN
assets/images/thorchain.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
|
@ -3,4 +3,26 @@
|
|||
useSSL: true
|
||||
is_default: true
|
||||
-
|
||||
uri: node.perish.co:9076
|
||||
uri: node.nautilus.io
|
||||
path: /api
|
||||
useSSL: true
|
||||
-
|
||||
uri: app.natrium.io
|
||||
path: /api
|
||||
useSSL: true
|
||||
-
|
||||
uri: rainstorm.city
|
||||
path: /api
|
||||
useSSL: true
|
||||
-
|
||||
uri: node.somenano.com
|
||||
path: /proxy
|
||||
useSSL: true
|
||||
-
|
||||
uri: nanoslo.0x.no
|
||||
path: /proxy
|
||||
useSSL: true
|
||||
-
|
||||
uri: www.bitrequest.app
|
||||
port: 8020
|
||||
useSSL: true
|
|
@ -1,4 +1,2 @@
|
|||
Monero enhancements
|
||||
In-App live status page for the app services
|
||||
Add Exolix exchange provider
|
||||
Bug fixes and enhancements
|
||||
UI enhancements
|
||||
Bug fixes
|
|
@ -1,5 +1,7 @@
|
|||
Monero enhancements
|
||||
Bitcoin support different address types (Taproot, Segwit P2WPKH/P2WSH, Legacy)
|
||||
In-App live status page for the app services
|
||||
Add Exolix exchange provider
|
||||
Bug fixes and enhancements
|
||||
Add Replace-By-Fee to boost pending Bitcoin transactions
|
||||
Enable WalletConnect for Solana
|
||||
WalletConnect Enhancements
|
||||
Enhancements for ERC-20 tokens and Solana tokens
|
||||
Enhancements for Nano wallet
|
||||
UI enhancements
|
||||
Bug fixes
|
|
@ -3,6 +3,9 @@ import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin;
|
|||
|
||||
List<int> addressToOutputScript(String address, bitcoin.BasedUtxoNetwork network) {
|
||||
try {
|
||||
if (network == bitcoin.BitcoinCashNetwork.mainnet) {
|
||||
return bitcoin.BitcoinCashAddress(address).baseAddress.toScriptPubKey().toBytes();
|
||||
}
|
||||
return bitcoin.addressToOutputScript(address: address, network: network);
|
||||
} catch (err) {
|
||||
print(err);
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:convert';
|
||||
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/script_hash.dart' as sh;
|
||||
|
@ -20,10 +19,9 @@ class BitcoinAddressRecord {
|
|||
_balance = balance,
|
||||
_name = name,
|
||||
_isUsed = isUsed,
|
||||
scriptHash =
|
||||
scriptHash ?? (network != null ? sh.scriptHash(address, network: network) : null);
|
||||
scriptHash = scriptHash ?? sh.scriptHash(address, network: network);
|
||||
|
||||
factory BitcoinAddressRecord.fromJSON(String jsonSource, BasedUtxoNetwork? network) {
|
||||
factory BitcoinAddressRecord.fromJSON(String jsonSource, BasedUtxoNetwork network) {
|
||||
final decoded = json.decode(jsonSource) as Map;
|
||||
|
||||
return BitcoinAddressRecord(
|
||||
|
@ -39,9 +37,7 @@ class BitcoinAddressRecord {
|
|||
.firstWhere((type) => type.toString() == decoded['type'] as String)
|
||||
: SegwitAddresType.p2wpkh,
|
||||
scriptHash: decoded['scriptHash'] as String?,
|
||||
network: (decoded['network'] as String?) == null
|
||||
? network
|
||||
: BasedUtxoNetwork.fromName(decoded['network'] as String),
|
||||
network: network,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -56,7 +52,7 @@ class BitcoinAddressRecord {
|
|||
String _name;
|
||||
bool _isUsed;
|
||||
String? scriptHash;
|
||||
BasedUtxoNetwork? network;
|
||||
BasedUtxoNetwork network;
|
||||
|
||||
int get txCount => _txCount;
|
||||
|
||||
|
@ -76,8 +72,6 @@ class BitcoinAddressRecord {
|
|||
@override
|
||||
int get hashCode => address.hashCode;
|
||||
|
||||
String get cashAddr => bitbox.Address.toCashAddress(address);
|
||||
|
||||
BitcoinAddressType type;
|
||||
|
||||
String updateScriptHash(BasedUtxoNetwork network) {
|
||||
|
@ -95,6 +89,5 @@ class BitcoinAddressRecord {
|
|||
'balance': balance,
|
||||
'type': type.toString(),
|
||||
'scriptHash': scriptHash,
|
||||
'network': network?.value,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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.';
|
||||
}
|
|
@ -115,8 +115,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required String password,
|
||||
}) async {
|
||||
final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password,
|
||||
walletInfo.network != null ? BasedUtxoNetwork.fromName(walletInfo.network!) : null);
|
||||
final network = walletInfo.network != null
|
||||
? BasedUtxoNetwork.fromName(walletInfo.network!)
|
||||
: BitcoinNetwork.mainnet;
|
||||
final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network);
|
||||
|
||||
walletInfo.derivationInfo ??= DerivationInfo(
|
||||
derivationType: snp.derivationType ?? DerivationType.electrum2,
|
||||
|
@ -149,7 +151,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
initialRegularAddressIndex: snp.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp.changeAddressIndex,
|
||||
addressPageType: snp.addressPageType,
|
||||
networkParam: snp.network,
|
||||
networkParam: network,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -75,11 +76,7 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
: {}),
|
||||
this.unspentCoinsInfo = unspentCoinsInfo,
|
||||
this.network = networkType == bitcoin.bitcoin
|
||||
? BitcoinNetwork.mainnet
|
||||
: networkType == litecoinNetwork
|
||||
? LitecoinNetwork.mainnet
|
||||
: BitcoinNetwork.testnet,
|
||||
this.network = _getNetwork(networkType, currency),
|
||||
this.isTestnet = networkType == bitcoin.testnet,
|
||||
super(walletInfo) {
|
||||
this.electrumClient = electrumClient ?? ElectrumClient();
|
||||
|
@ -192,27 +189,27 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
Future<EstimatedTxResult> _estimateTxFeeAndInputsToUse(
|
||||
int credentialsAmount,
|
||||
bool sendAll,
|
||||
List<BitcoinBaseAddress> outputAddresses,
|
||||
List<BitcoinOutput> outputs,
|
||||
BitcoinTransactionCredentials transactionCredentials,
|
||||
{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 address = addressTypeFromStr(utx.address, network);
|
||||
final privkey = generateECPrivate(
|
||||
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
|
||||
index: utx.bitcoinAddressRecord.index,
|
||||
|
@ -228,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,114 +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);
|
||||
|
||||
final fee = transactionCredentials.feeRate != null
|
||||
? feeAmountWithFeeRate(transactionCredentials.feeRate!, 0, 0, size: estimatedSize)
|
||||
: feeAmountForPriority(transactionCredentials.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, transactionCredentials,
|
||||
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);
|
||||
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
|
||||
|
@ -362,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();
|
||||
});
|
||||
|
@ -391,7 +592,6 @@ abstract class ElectrumWalletBase
|
|||
? SegwitAddresType.p2wpkh.toString()
|
||||
: walletInfo.addressPageType.toString(),
|
||||
'balance': balance[currency]?.toJSON(),
|
||||
'network_type': network == BitcoinNetwork.testnet ? 'testnet' : 'mainnet',
|
||||
});
|
||||
|
||||
int feeRate(TransactionPriority priority) {
|
||||
|
@ -406,7 +606,7 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount,
|
||||
int feeAmountForPriority(TransactionPriority priority, int inputsCount, int outputsCount,
|
||||
{int? size}) =>
|
||||
feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount));
|
||||
|
||||
|
@ -595,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;
|
||||
|
@ -627,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(
|
||||
|
@ -638,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) {
|
||||
|
@ -852,6 +1228,22 @@ abstract class ElectrumWalletBase
|
|||
final HD = index == null ? hd : hd.derive(index);
|
||||
return base64Encode(HD.signMessage(message));
|
||||
}
|
||||
|
||||
static BasedUtxoNetwork _getNetwork(bitcoin.NetworkType networkType, CryptoCurrency? currency) {
|
||||
if (networkType == bitcoin.bitcoin && currency == CryptoCurrency.bch) {
|
||||
return BitcoinCashNetwork.mainnet;
|
||||
}
|
||||
|
||||
if (networkType == litecoinNetwork) {
|
||||
return LitecoinNetwork.mainnet;
|
||||
}
|
||||
|
||||
if (networkType == bitcoin.testnet) {
|
||||
return BitcoinNetwork.testnet;
|
||||
}
|
||||
|
||||
return BitcoinNetwork.mainnet;
|
||||
}
|
||||
}
|
||||
|
||||
class EstimateTxParams {
|
||||
|
@ -870,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) {
|
||||
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)) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/electrum.dart';
|
||||
import 'package:cw_core/wallet_addresses.dart';
|
||||
|
@ -30,6 +29,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex,
|
||||
BitcoinAddressType? initialAddressPageType,
|
||||
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
|
||||
addressesByReceiveType =
|
||||
ObservableList<BitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
|
||||
|
@ -41,9 +41,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
.toSet()),
|
||||
currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {},
|
||||
currentChangeAddressIndexByType = initialChangeAddressIndex ?? {},
|
||||
_addressPageType = walletInfo.addressPageType != null
|
||||
? BitcoinAddressType.fromValue(walletInfo.addressPageType!)
|
||||
: SegwitAddresType.p2wpkh,
|
||||
_addressPageType = initialAddressPageType ??
|
||||
(walletInfo.addressPageType != null
|
||||
? BitcoinAddressType.fromValue(walletInfo.addressPageType!)
|
||||
: SegwitAddresType.p2wpkh),
|
||||
super(walletInfo) {
|
||||
updateAddressesByMatch();
|
||||
}
|
||||
|
@ -52,10 +53,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
static const defaultChangeAddressesCount = 17;
|
||||
static const gap = 20;
|
||||
|
||||
static String toCashAddr(String address) => bitbox.Address.toCashAddress(address);
|
||||
|
||||
static String toLegacy(String address) => bitbox.Address.toLegacyAddress(address);
|
||||
|
||||
final ObservableList<BitcoinAddressRecord> _addresses;
|
||||
// Matched by addressPageType
|
||||
late ObservableList<BitcoinAddressRecord> addressesByReceiveType;
|
||||
|
@ -67,7 +64,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
final bitcoin.HDWallet sideHd;
|
||||
|
||||
@observable
|
||||
BitcoinAddressType _addressPageType = SegwitAddresType.p2wpkh;
|
||||
late BitcoinAddressType _addressPageType;
|
||||
|
||||
@computed
|
||||
BitcoinAddressType get addressPageType => _addressPageType;
|
||||
|
@ -80,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) {
|
||||
|
@ -97,7 +95,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
}
|
||||
}
|
||||
|
||||
return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(receiveAddress) : receiveAddress;
|
||||
return receiveAddress;
|
||||
}
|
||||
|
||||
@observable
|
||||
|
@ -105,9 +103,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
@override
|
||||
set address(String addr) {
|
||||
if (addr.startsWith('bitcoincash:')) {
|
||||
addr = toLegacy(addr);
|
||||
}
|
||||
final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr);
|
||||
|
||||
previousAddressRecord = addressRecord;
|
||||
|
@ -155,11 +150,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
await _generateInitialAddresses();
|
||||
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
|
||||
await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
|
||||
await _generateInitialAddresses(type: SegwitAddresType.p2tr);
|
||||
await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
|
||||
if (walletInfo.type == WalletType.bitcoinCash) {
|
||||
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
|
||||
} else if (walletInfo.type == WalletType.litecoin) {
|
||||
await _generateInitialAddresses();
|
||||
} else if (walletInfo.type == WalletType.bitcoin) {
|
||||
await _generateInitialAddresses();
|
||||
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
|
||||
await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
|
||||
await _generateInitialAddresses(type: SegwitAddresType.p2tr);
|
||||
await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
|
||||
}
|
||||
updateAddressesByMatch();
|
||||
updateReceiveAddresses();
|
||||
updateChangeAddresses();
|
||||
|
@ -221,6 +222,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
try {
|
||||
addressesMap.clear();
|
||||
addressesMap[address] = '';
|
||||
|
||||
allAddressesMap.clear();
|
||||
_addresses.forEach((addressRecord) {
|
||||
allAddressesMap[addressRecord.address] = addressRecord.name;
|
||||
});
|
||||
await saveAddressesInBox();
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
|
@ -229,15 +235,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
@action
|
||||
void updateAddress(String address, String label) {
|
||||
if (address.startsWith('bitcoincash:')) {
|
||||
address = toLegacy(address);
|
||||
}
|
||||
final addressRecord =
|
||||
_addresses.firstWhere((addressRecord) => addressRecord.address == address);
|
||||
addressRecord.setNewName(label);
|
||||
final index = _addresses.indexOf(addressRecord);
|
||||
_addresses.remove(addressRecord);
|
||||
_addresses.insert(index, addressRecord);
|
||||
|
||||
updateAddressesByMatch();
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -261,7 +266,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
addressRecord.isHidden &&
|
||||
!addressRecord.isUsed &&
|
||||
// TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type
|
||||
addressRecord.type == SegwitAddresType.p2wpkh);
|
||||
(walletInfo.type != WalletType.bitcoin || addressRecord.type == SegwitAddresType.p2wpkh));
|
||||
changeAddresses.addAll(newAddresses);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ class ElectrumWalletSnapshot {
|
|||
required this.regularAddressIndex,
|
||||
required this.changeAddressIndex,
|
||||
required this.addressPageType,
|
||||
required this.network,
|
||||
this.derivationType,
|
||||
this.derivationPath,
|
||||
});
|
||||
|
@ -26,8 +25,7 @@ class ElectrumWalletSnapshot {
|
|||
final String name;
|
||||
final String password;
|
||||
final WalletType type;
|
||||
final String addressPageType;
|
||||
final BasedUtxoNetwork network;
|
||||
final String? addressPageType;
|
||||
|
||||
String mnemonic;
|
||||
List<BitcoinAddressRecord> addresses;
|
||||
|
@ -38,7 +36,7 @@ class ElectrumWalletSnapshot {
|
|||
String? derivationPath;
|
||||
|
||||
static Future<ElectrumWalletSnapshot> load(
|
||||
String name, WalletType type, String password, BasedUtxoNetwork? network) async {
|
||||
String name, WalletType type, String password, BasedUtxoNetwork network) async {
|
||||
final path = await pathForWallet(name: name, type: type);
|
||||
final jsonSource = await read(path: path, password: password);
|
||||
final data = json.decode(jsonSource) as Map;
|
||||
|
@ -80,8 +78,7 @@ class ElectrumWalletSnapshot {
|
|||
balance: balance,
|
||||
regularAddressIndex: regularAddressIndexByType,
|
||||
changeAddressIndex: changeAddressIndexByType,
|
||||
addressPageType: data['address_page_type'] as String? ?? SegwitAddresType.p2wpkh.toString(),
|
||||
network: data['network_type'] == 'testnet' ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet,
|
||||
addressPageType: data['address_page_type'] as String?,
|
||||
derivationType: derivationType,
|
||||
derivationPath: derivationPath,
|
||||
);
|
||||
|
|
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()));
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:cw_bitcoin/address_to_output_script.dart';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin;
|
||||
|
||||
String scriptHash(String address, {required BasedUtxoNetwork network}) {
|
||||
final outputScript = addressToOutputScript(address: address, network: network);
|
||||
String scriptHash(String address, {required bitcoin.BasedUtxoNetwork network}) {
|
||||
final outputScript = addressToOutputScript(address, network);
|
||||
final parts = sha256.convert(outputScript).toString().split('');
|
||||
var res = '';
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
@ -79,11 +79,11 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: cake-update-v1
|
||||
resolved-ref: "9611e9db77e92a8434e918cdfb620068f6fcb1aa"
|
||||
ref: cake-update-v2
|
||||
resolved-ref: "3fd81d238b990bb767fc7a4fdd5053a22a142e2e"
|
||||
url: "https://github.com/cake-tech/bitcoin_base.git"
|
||||
source: git
|
||||
version: "4.0.0"
|
||||
version: "4.2.0"
|
||||
bitcoin_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -97,10 +97,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: blockchain_utils
|
||||
sha256: "9701dfaa74caad4daae1785f1ec4445cf7fb94e45620bc3a4aca1b9b281dc6c9"
|
||||
sha256: "38ef5f4a22441ac4370aed9071dc71c460acffc37c79b344533f67d15f24c13c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
version: "2.1.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -26,15 +26,15 @@ 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
|
||||
bitcoin_base:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitcoin_base.git
|
||||
ref: cake-update-v1
|
||||
blockchain_utils: ^1.6.0
|
||||
ref: cake-update-v2
|
||||
blockchain_utils: ^2.1.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
@ -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';
|
||||
|
@ -34,7 +29,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required Uint8List seedBytes,
|
||||
String? addressPageType,
|
||||
BitcoinAddressType? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
|
@ -58,6 +53,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
mainHd: hd,
|
||||
sideHd: bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/1"),
|
||||
network: network,
|
||||
initialAddressPageType: addressPageType,
|
||||
);
|
||||
autorun((_) {
|
||||
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
|
||||
|
@ -84,7 +80,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
seedBytes: await Mnemonic.toSeed(mnemonic),
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
addressPageType: addressPageType,
|
||||
addressPageType: P2pkhAddressType.p2pkh,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -101,193 +97,37 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
initialAddresses: snp.addresses,
|
||||
initialAddresses: snp.addresses.map((addr) {
|
||||
try {
|
||||
BitcoinCashAddress(addr.address);
|
||||
return BitcoinAddressRecord(
|
||||
addr.address,
|
||||
index: addr.index,
|
||||
isHidden: addr.isHidden,
|
||||
type: P2pkhAddressType.p2pkh,
|
||||
network: BitcoinCashNetwork.mainnet,
|
||||
);
|
||||
} catch (_) {
|
||||
return BitcoinAddressRecord(
|
||||
AddressUtils.getCashAddrFormat(addr.address),
|
||||
index: addr.index,
|
||||
isHidden: addr.isHidden,
|
||||
type: P2pkhAddressType.p2pkh,
|
||||
network: BitcoinCashNetwork.mainnet,
|
||||
);
|
||||
}
|
||||
}).toList(),
|
||||
initialBalance: snp.balance,
|
||||
seedBytes: await Mnemonic.toSeed(snp.mnemonic),
|
||||
initialRegularAddressIndex: snp.regularAddressIndex,
|
||||
initialChangeAddressIndex: snp.changeAddressIndex,
|
||||
addressPageType: snp.addressPageType,
|
||||
addressPageType: P2pkhAddressType.p2pkh,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingBitcoinCashTransaction> createTransaction(Object credentials) async {
|
||||
const minAmount = 546;
|
||||
final transactionCredentials = credentials as BitcoinTransactionCredentials;
|
||||
final inputs = <BitcoinUnspent>[];
|
||||
final outputs = transactionCredentials.outputs;
|
||||
final hasMultiDestination = outputs.length > 1;
|
||||
|
||||
var allInputsAmount = 0;
|
||||
|
||||
if (unspentCoins.isEmpty) await updateUnspent();
|
||||
|
||||
for (final utx in unspentCoins) {
|
||||
if (utx.isSending) {
|
||||
allInputsAmount += utx.value;
|
||||
inputs.add(utx);
|
||||
}
|
||||
}
|
||||
|
||||
if (inputs.isEmpty) throw BitcoinTransactionNoInputsException();
|
||||
|
||||
final allAmountFee = transactionCredentials.feeRate != null
|
||||
? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length)
|
||||
: feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length);
|
||||
|
||||
final allAmount = allInputsAmount - allAmountFee;
|
||||
|
||||
var credentialsAmount = 0;
|
||||
var amount = 0;
|
||||
var fee = 0;
|
||||
|
||||
if (hasMultiDestination) {
|
||||
if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!);
|
||||
|
||||
if (allAmount - credentialsAmount < minAmount) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
amount = credentialsAmount;
|
||||
|
||||
if (transactionCredentials.feeRate != null) {
|
||||
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount,
|
||||
outputsCount: outputs.length + 1);
|
||||
} else {
|
||||
fee = calculateEstimatedFee(transactionCredentials.priority, amount,
|
||||
outputsCount: outputs.length + 1);
|
||||
}
|
||||
} else {
|
||||
final output = outputs.first;
|
||||
credentialsAmount = !output.sendAll ? output.formattedCryptoAmount! : 0;
|
||||
|
||||
if (credentialsAmount > allAmount) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
amount = output.sendAll || allAmount - credentialsAmount < minAmount
|
||||
? allAmount
|
||||
: credentialsAmount;
|
||||
|
||||
if (output.sendAll || amount == allAmount) {
|
||||
fee = allAmountFee;
|
||||
} else if (transactionCredentials.feeRate != null) {
|
||||
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount);
|
||||
} else {
|
||||
fee = calculateEstimatedFee(transactionCredentials.priority, amount);
|
||||
}
|
||||
}
|
||||
|
||||
if (fee == 0) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
final totalAmount = amount + fee;
|
||||
|
||||
if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
final txb = bitbox.Bitbox.transactionBuilder(testnet: false);
|
||||
|
||||
final changeAddress = await walletAddresses.getChangeAddress();
|
||||
var leftAmount = totalAmount;
|
||||
var totalInputAmount = 0;
|
||||
|
||||
inputs.clear();
|
||||
|
||||
for (final utx in unspentCoins) {
|
||||
if (utx.isSending) {
|
||||
leftAmount = leftAmount - utx.value;
|
||||
totalInputAmount += utx.value;
|
||||
inputs.add(utx);
|
||||
|
||||
if (leftAmount <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inputs.isEmpty) throw BitcoinTransactionNoInputsException();
|
||||
|
||||
if (amount <= 0 || totalInputAmount < totalAmount) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
inputs.forEach((input) {
|
||||
txb.addInput(input.hash, input.vout);
|
||||
});
|
||||
|
||||
final String bchPrefix = "bitcoincash:";
|
||||
|
||||
outputs.forEach((item) {
|
||||
final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount;
|
||||
String outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address;
|
||||
|
||||
if (!outputAddress.startsWith(bchPrefix)) {
|
||||
outputAddress = "$bchPrefix$outputAddress";
|
||||
}
|
||||
|
||||
bool isP2sh = outputAddress.startsWith("p", bchPrefix.length);
|
||||
|
||||
if (isP2sh) {
|
||||
final p2sh = P2shAddress.fromAddress(
|
||||
address: outputAddress,
|
||||
network: BitcoinCashNetwork.mainnet,
|
||||
);
|
||||
|
||||
txb.addOutput(Uint8List.fromList(p2sh.toScriptPubKey().toBytes()), outputAmount!);
|
||||
return;
|
||||
}
|
||||
|
||||
txb.addOutput(outputAddress, outputAmount!);
|
||||
});
|
||||
|
||||
final estimatedSize = bitbox.BitcoinCash.getByteCount(inputs.length, outputs.length + 1);
|
||||
|
||||
var feeAmount = 0;
|
||||
|
||||
if (transactionCredentials.feeRate != null) {
|
||||
feeAmount = transactionCredentials.feeRate! * estimatedSize;
|
||||
} else {
|
||||
feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize;
|
||||
}
|
||||
|
||||
final changeValue = totalInputAmount - amount - feeAmount;
|
||||
|
||||
if (changeValue > minAmount) {
|
||||
txb.addOutput(changeAddress, changeValue);
|
||||
}
|
||||
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
final input = inputs[i];
|
||||
final keyPair = generateKeyPair(
|
||||
hd: input.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
|
||||
index: input.bitcoinAddressRecord.index);
|
||||
txb.sign(i, keyPair, input.value);
|
||||
}
|
||||
|
||||
// Build the transaction
|
||||
final tx = txb.build();
|
||||
|
||||
return PendingBitcoinCashTransaction(tx, type,
|
||||
electrumClient: electrumClient, amount: amount, fee: fee);
|
||||
}
|
||||
|
||||
bitbox.ECPair generateKeyPair({required bitcoin.HDWallet hd, required int index}) =>
|
||||
bitbox.ECPair.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;
|
||||
|
|
|
@ -19,6 +19,7 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi
|
|||
super.initialAddresses,
|
||||
super.initialRegularAddressIndex,
|
||||
super.initialChangeAddressIndex,
|
||||
super.initialAddressPageType,
|
||||
}) : super(walletInfo);
|
||||
|
||||
@override
|
||||
|
|
|
@ -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,11 +28,11 @@ 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
|
||||
ref: cake-update-v1
|
||||
ref: cake-update-v2
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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 {}
|
31
cw_core/lib/n2_node.dart
Normal file
|
@ -0,0 +1,31 @@
|
|||
class N2Node {
|
||||
N2Node({
|
||||
this.weight,
|
||||
this.uptime,
|
||||
this.score,
|
||||
this.account,
|
||||
this.alias,
|
||||
});
|
||||
|
||||
String? uptime;
|
||||
double? weight;
|
||||
int? score;
|
||||
String? account;
|
||||
String? alias;
|
||||
|
||||
factory N2Node.fromJson(Map<String, dynamic> json) => N2Node(
|
||||
weight: double.tryParse((json['weight'] as num?).toString()),
|
||||
uptime: json['uptime'] as String?,
|
||||
score: json['score'] as int?,
|
||||
account: json['rep_address'] as String?,
|
||||
alias: json['alias'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||
'uptime': uptime,
|
||||
'weight': weight,
|
||||
'score': score,
|
||||
'rep_address': account,
|
||||
'alias': alias,
|
||||
};
|
||||
}
|
|
@ -21,6 +21,7 @@ class Node extends HiveObject with Keyable {
|
|||
this.trusted = false,
|
||||
this.socksProxyAddress,
|
||||
String? uri,
|
||||
String? path,
|
||||
WalletType? type,
|
||||
}) {
|
||||
if (uri != null) {
|
||||
|
@ -29,10 +30,14 @@ class Node extends HiveObject with Keyable {
|
|||
if (type != null) {
|
||||
this.type = type;
|
||||
}
|
||||
if (path != null) {
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
|
||||
Node.fromMap(Map<String, Object?> map)
|
||||
: uriRaw = map['uri'] as String? ?? '',
|
||||
path = map['path'] as String? ?? '',
|
||||
login = map['login'] as String?,
|
||||
password = map['password'] as String?,
|
||||
useSSL = map['useSSL'] as bool?,
|
||||
|
@ -63,6 +68,9 @@ class Node extends HiveObject with Keyable {
|
|||
@HiveField(6)
|
||||
String? socksProxyAddress;
|
||||
|
||||
@HiveField(7, defaultValue: '')
|
||||
String? path;
|
||||
|
||||
bool get isSSL => useSSL ?? false;
|
||||
|
||||
bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty;
|
||||
|
@ -79,9 +87,9 @@ class Node extends HiveObject with Keyable {
|
|||
case WalletType.nano:
|
||||
case WalletType.banano:
|
||||
if (isSSL) {
|
||||
return Uri.https(uriRaw, '');
|
||||
return Uri.https(uriRaw, path ?? '');
|
||||
} else {
|
||||
return Uri.http(uriRaw, '');
|
||||
return Uri.http(uriRaw, path ?? '');
|
||||
}
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
|
@ -103,7 +111,8 @@ class Node extends HiveObject with Keyable {
|
|||
other.typeRaw == typeRaw &&
|
||||
other.useSSL == useSSL &&
|
||||
other.trusted == trusted &&
|
||||
other.socksProxyAddress == socksProxyAddress);
|
||||
other.socksProxyAddress == socksProxyAddress &&
|
||||
other.path == path);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
|
@ -113,7 +122,8 @@ class Node extends HiveObject with Keyable {
|
|||
typeRaw.hashCode ^
|
||||
useSSL.hashCode ^
|
||||
trusted.hashCode ^
|
||||
socksProxyAddress.hashCode;
|
||||
socksProxyAddress.hashCode ^
|
||||
path.hashCode;
|
||||
|
||||
@override
|
||||
dynamic get keyIndex {
|
||||
|
|
|
@ -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,6 @@ abstract class WalletAddresses {
|
|||
}
|
||||
}
|
||||
|
||||
bool containsAddress(String address) => addressesMap.containsKey(address);
|
||||
bool containsAddress(String address) =>
|
||||
addressesMap.containsKey(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)}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:cw_core/nano_account_info_response.dart';
|
||||
import 'package:cw_core/n2_node.dart';
|
||||
import 'package:cw_nano/nano_balance.dart';
|
||||
import 'package:cw_nano/nano_transaction_model.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
@ -16,6 +17,8 @@ class NanoClient {
|
|||
"nano-app": "cake-wallet"
|
||||
};
|
||||
|
||||
static const String N2_REPS_ENDPOINT = "https://rpc.nano.to";
|
||||
|
||||
NanoClient() {
|
||||
SharedPreferences.getInstance().then((value) => prefs = value);
|
||||
}
|
||||
|
@ -418,7 +421,7 @@ class NanoClient {
|
|||
body: jsonEncode({
|
||||
"action": "account_history",
|
||||
"account": address,
|
||||
"count": "250", // TODO: pick a number
|
||||
"count": "100",
|
||||
// "raw": true,
|
||||
}));
|
||||
final data = await jsonDecode(response.body);
|
||||
|
@ -434,4 +437,37 @@ class NanoClient {
|
|||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<N2Node>> getN2Reps() async {
|
||||
final response = await http.post(
|
||||
Uri.parse(N2_REPS_ENDPOINT),
|
||||
headers: CAKE_HEADERS,
|
||||
body: jsonEncode({"action": "reps"}),
|
||||
);
|
||||
try {
|
||||
final List<N2Node> nodes = (json.decode(response.body) as List<dynamic>)
|
||||
.map((dynamic e) => N2Node.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
return nodes;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> getRepScore(String rep) async {
|
||||
final response = await http.post(
|
||||
Uri.parse(N2_REPS_ENDPOINT),
|
||||
headers: CAKE_HEADERS,
|
||||
body: jsonEncode({
|
||||
"action": "rep_info",
|
||||
"account": rep,
|
||||
}),
|
||||
);
|
||||
try {
|
||||
final N2Node node = N2Node.fromJson(json.decode(response.body) as Map<String, dynamic>);
|
||||
return node.score ?? 100;
|
||||
} catch (error) {
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'package:cw_core/transaction_priority.dart';
|
|||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_nano/file.dart';
|
||||
import 'package:cw_core/nano_account.dart';
|
||||
import 'package:cw_core/n2_node.dart';
|
||||
import 'package:cw_nano/nano_balance.dart';
|
||||
import 'package:cw_nano/nano_client.dart';
|
||||
import 'package:cw_nano/nano_transaction_credentials.dart';
|
||||
|
@ -65,9 +66,11 @@ abstract class NanoWalletBase
|
|||
String? _privateKey;
|
||||
String? _publicAddress;
|
||||
String? _hexSeed;
|
||||
Timer? _receiveTimer;
|
||||
|
||||
String? _representativeAddress;
|
||||
Timer? _receiveTimer;
|
||||
int repScore = 100;
|
||||
bool get isRepOk => repScore >= 90;
|
||||
|
||||
late final NanoClient _client;
|
||||
bool _isTransactionUpdating;
|
||||
|
@ -375,7 +378,7 @@ abstract class NanoWalletBase
|
|||
|
||||
final data = json.decode(jsonSource) as Map;
|
||||
final mnemonic = data['mnemonic'] as String;
|
||||
|
||||
|
||||
final balance = NanoBalance.fromRawString(
|
||||
currentBalance: data['currentBalance'] as String? ?? "0",
|
||||
receivableBalance: data['receivableBalance'] as String? ?? "0",
|
||||
|
@ -432,6 +435,8 @@ abstract class NanoWalletBase
|
|||
_representativeAddress = await _client.getRepFromPrefs();
|
||||
throw Exception("Failed to get representative address $e");
|
||||
}
|
||||
|
||||
repScore = await _client.getRepScore(_representativeAddress!);
|
||||
}
|
||||
|
||||
Future<void> regenerateAddress() async {
|
||||
|
@ -468,6 +473,10 @@ abstract class NanoWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
Future<List<N2Node>> getN2Reps() async {
|
||||
return _client.getN2Reps();
|
||||
}
|
||||
|
||||
Future<void>? updateBalance() async => await _updateBalance();
|
||||
|
||||
@override
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'package:cw_core/cake_hive.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
@ -30,7 +29,6 @@ import 'package:mobx/mobx.dart';
|
|||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solana/metaplex.dart' as metaplex;
|
||||
import 'package:solana/solana.dart';
|
||||
import 'package:web3dart/crypto.dart';
|
||||
|
||||
part 'solana_wallet.g.dart';
|
||||
|
||||
|
@ -77,6 +75,9 @@ abstract class SolanaWalletBase
|
|||
|
||||
late SolanaWalletClient _client;
|
||||
|
||||
@observable
|
||||
double? estimatedFee;
|
||||
|
||||
Timer? _transactionsUpdateTimer;
|
||||
|
||||
late final Box<SPLToken> splTokensBox;
|
||||
|
@ -134,7 +135,7 @@ abstract class SolanaWalletBase
|
|||
assert(mnemonic != null || privateKey != null);
|
||||
|
||||
if (privateKey != null) {
|
||||
final privateKeyBytes = hexToBytes(privateKey);
|
||||
final privateKeyBytes = HEX.decode(privateKey);
|
||||
return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes);
|
||||
}
|
||||
|
||||
|
@ -173,6 +174,14 @@ abstract class SolanaWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _getEstimatedFees() async {
|
||||
try {
|
||||
estimatedFee = await _client.getEstimatedFee(_walletKeyPair!);
|
||||
} catch (e) {
|
||||
estimatedFee = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
final solCredentials = credentials as SolanaTransactionCredentials;
|
||||
|
@ -190,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);
|
||||
|
@ -206,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);
|
||||
|
@ -230,6 +247,7 @@ abstract class SolanaWalletBase
|
|||
destinationAddress: solCredentials.outputs.first.isParsedAddress
|
||||
? solCredentials.outputs.first.extractedAddress!
|
||||
: solCredentials.outputs.first.address,
|
||||
isSendAll: isSendAll,
|
||||
);
|
||||
|
||||
return pendingSolanaTransaction;
|
||||
|
@ -271,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,
|
||||
|
@ -328,6 +349,7 @@ abstract class SolanaWalletBase
|
|||
_updateBalance(),
|
||||
_updateNativeSOLTransactions(),
|
||||
_updateSPLTokenTransactions(),
|
||||
_getEstimatedFees(),
|
||||
]);
|
||||
|
||||
syncStatus = SyncedSyncStatus();
|
||||
|
@ -435,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
|
||||
|
@ -477,9 +509,9 @@ abstract class SolanaWalletBase
|
|||
}
|
||||
|
||||
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
|
||||
_updateSPLTokenTransactions();
|
||||
_updateNativeSOLTransactions();
|
||||
_updateBalance();
|
||||
_updateNativeSOLTransactions();
|
||||
_updateSPLTokenTransactions();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -491,7 +523,7 @@ abstract class SolanaWalletBase
|
|||
final signature = await _walletKeyPair!.sign(messageBytes);
|
||||
|
||||
// Convert the signature to a hexadecimal string
|
||||
final hex = bytesToHex(signature.bytes);
|
||||
final hex = HEX.encode(signature.bytes);
|
||||
|
||||
return hex;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ dependencies:
|
|||
bip39: ^1.0.6
|
||||
mobx: ^2.3.0+1
|
||||
shared_preferences: ^2.0.15
|
||||
web3dart: ^2.7.1
|
||||
bip32: ^2.0.0
|
||||
hex: ^0.2.0
|
||||
|
||||
|
@ -34,4 +33,4 @@ dev_dependencies:
|
|||
flutter:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -277,7 +277,7 @@ SPEC CHECKSUMS:
|
|||
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
|
||||
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
|
||||
fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c
|
||||
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
||||
local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
|
@ -300,6 +300,6 @@ SPEC CHECKSUMS:
|
|||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
||||
PODFILE CHECKSUM: fcb1b8418441a35b438585c9dd8374e722e6c6ca
|
||||
PODFILE CHECKSUM: a2fe518be61cdbdc5b0e2da085ab543d556af2d3
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.15.2
|
||||
|
|
|
@ -83,21 +83,25 @@ 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,13 +126,46 @@ class CWBitcoin extends Bitcoin {
|
|||
.map((BitcoinAddressRecord addr) => ElectrumSubAddress(
|
||||
id: addr.index,
|
||||
name: addr.name,
|
||||
address: electrumWallet.type == WalletType.bitcoinCash ? addr.cashAddr : addr.address,
|
||||
address: addr.address,
|
||||
txCount: addr.txCount,
|
||||
balance: addr.balance,
|
||||
isChange: addr.isHidden))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority) async {
|
||||
try {
|
||||
final sk = ECPrivate.random();
|
||||
final electrumWallet = wallet as ElectrumWallet;
|
||||
|
||||
if (wallet.type == WalletType.bitcoinCash) {
|
||||
final p2pkhAddr = sk.getPublic().toP2pkhAddress();
|
||||
final estimatedTx = await electrumWallet.estimateSendAllTx(
|
||||
[BitcoinOutput(address: p2pkhAddr, value: BigInt.zero)],
|
||||
getFeeRate(wallet, priority as BitcoinCashTransactionPriority),
|
||||
);
|
||||
|
||||
return estimatedTx.amount;
|
||||
}
|
||||
|
||||
final p2shAddr = sk.getPublic().toP2pkhInP2sh();
|
||||
final estimatedTx = await electrumWallet.estimateSendAllTx(
|
||||
[BitcoinOutput(address: p2shAddr, value: BigInt.zero)],
|
||||
getFeeRate(
|
||||
wallet,
|
||||
wallet.type == WalletType.litecoin
|
||||
? priority as LitecoinTransactionPriority
|
||||
: priority as BitcoinTransactionPriority,
|
||||
),
|
||||
);
|
||||
|
||||
return estimatedTx.amount;
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String getAddress(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
|
@ -147,8 +184,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) {
|
||||
|
@ -174,6 +212,9 @@ class CWBitcoin extends Bitcoin {
|
|||
@override
|
||||
TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium;
|
||||
|
||||
@override
|
||||
TransactionPriority getBitcoinTransactionPriorityCustom() => BitcoinTransactionPriority.custom;
|
||||
|
||||
@override
|
||||
TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium;
|
||||
|
||||
|
@ -312,4 +353,48 @@ class CWBitcoin extends Bitcoin {
|
|||
|
||||
return list;
|
||||
}
|
||||
|
||||
@override
|
||||
bool hasTaprootInput(PendingTransaction pendingTransaction) {
|
||||
return (pendingTransaction as PendingBitcoinTransaction).hasTaprootInputs;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingBitcoinTransaction> replaceByFee(
|
||||
Object wallet, String transactionHash, String fee) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return await bitcoinWallet.replaceByFee(transactionHash, int.parse(fee));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> canReplaceByFee(Object wallet, String transactionHash) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.canReplaceByFee(transactionHash);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isChangeSufficientForFee(Object wallet, String txId, String newFee) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.isChangeSufficientForFee(txId, int.parse(newFee));
|
||||
}
|
||||
|
||||
@override
|
||||
int getFeeAmountForPriority(
|
||||
Object wallet, TransactionPriority priority, int inputsCount, int outputsCount,
|
||||
{int? size}) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.feeAmountForPriority(
|
||||
priority as BitcoinTransactionPriority, inputsCount, outputsCount);
|
||||
}
|
||||
|
||||
@override
|
||||
int getFeeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount,
|
||||
{int? size}) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
return bitcoinWallet.feeAmountWithFeeRate(
|
||||
feeRate,
|
||||
inputsCount,
|
||||
outputsCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:cake_wallet/buy/buy_amount.dart';
|
||||
import 'package:cake_wallet/buy/buy_exception.dart';
|
||||
import 'package:cake_wallet/buy/buy_provider.dart';
|
||||
import 'package:cake_wallet/buy/buy_provider_description.dart';
|
||||
import 'package:cake_wallet/buy/order.dart';
|
||||
import 'package:cake_wallet/exchange/trade_state.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/palette.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
|
@ -6,34 +15,31 @@ import 'package:cake_wallet/store/settings_store.dart';
|
|||
import 'package:cake_wallet/themes/theme_base.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:cake_wallet/buy/buy_exception.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:cake_wallet/buy/buy_amount.dart';
|
||||
import 'package:cake_wallet/buy/buy_provider.dart';
|
||||
import 'package:cake_wallet/buy/buy_provider_description.dart';
|
||||
import 'package:cake_wallet/buy/order.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cake_wallet/exchange/trade_state.dart';
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
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 =>
|
||||
|
@ -60,146 +66,121 @@ class MoonPaySellProvider extends BuyProvider {
|
|||
|
||||
static String get _apiKey => secrets.moonPayApiKey;
|
||||
|
||||
static String get _secretKey => secrets.moonPaySecretKey;
|
||||
final String baseUrl;
|
||||
final String baseBuyUrl;
|
||||
final String baseSellUrl;
|
||||
|
||||
Future<Uri> requestMoonPayUrl({
|
||||
String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
|
||||
|
||||
String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
|
||||
|
||||
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
|
||||
|
||||
Future<String> getMoonpaySignature(String query) async {
|
||||
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
|
||||
|
||||
final response = await post(
|
||||
uri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': _exchangeHelperApiKey,
|
||||
},
|
||||
body: json.encode({'query': query}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
|
||||
} else {
|
||||
throw Exception(
|
||||
'Provider currently unavailable. Status: ${response.statusCode} ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uri> requestSellMoonPayUrl({
|
||||
required CryptoCurrency currency,
|
||||
required 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': currency.toString().toLowerCase(),
|
||||
'refundWalletAddress': refundWalletAddress,
|
||||
}..addAll(customParams),
|
||||
);
|
||||
if (_apiKey.isNotEmpty) {
|
||||
params['apiKey'] = _apiKey;
|
||||
}
|
||||
|
||||
final messageBytes = utf8.encode('?${originalUri.query}');
|
||||
final key = utf8.encode(_secretKey);
|
||||
final hmac = Hmac(sha256, key);
|
||||
final digest = hmac.convert(messageBytes);
|
||||
final signature = base64.encode(digest.bytes);
|
||||
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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -274,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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,11 +32,12 @@ class RobinhoodBuyProvider extends BuyProvider {
|
|||
|
||||
String get _applicationId => secrets.robinhoodApplicationId;
|
||||
|
||||
String get _apiSecret => secrets.robinhoodCIdApiSecret;
|
||||
String get _apiSecret => secrets.exchangeHelperApiKey;
|
||||
|
||||
String getSignature(String message) {
|
||||
switch (wallet.type) {
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
return wallet.signMessage(message);
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoin:
|
||||
|
|
|
@ -274,7 +274,7 @@ class AddressValidator extends TextValidator {
|
|||
'|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type
|
||||
'|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type
|
||||
'|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type
|
||||
'|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)'; //P2trAddress type
|
||||
'|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)'; //P2trAddress type
|
||||
case CryptoCurrency.ltc:
|
||||
return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
|
||||
'|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)'
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cake_wallet/core/secure_storage.dart';
|
||||
import 'package:cake_wallet/core/totp_request_details.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
|
||||
|
@ -64,7 +66,7 @@ class AuthService with Store {
|
|||
|
||||
Future<bool> authenticate(String pin) async {
|
||||
final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
|
||||
final encodedPin = await secureStorage.read(key: key);
|
||||
final encodedPin = await readSecureStorage(secureStorage, key);
|
||||
final decodedPin = decodedPinCode(pin: encodedPin!);
|
||||
|
||||
return decodedPin == pin;
|
||||
|
@ -76,7 +78,8 @@ class AuthService with Store {
|
|||
}
|
||||
|
||||
Future<bool> requireAuth() async {
|
||||
final timestamp = int.tryParse(await secureStorage.read(key: SecureKey.lastAuthTimeMilliseconds) ?? '0');
|
||||
final timestamp =
|
||||
int.tryParse(await secureStorage.read(key: SecureKey.lastAuthTimeMilliseconds) ?? '0');
|
||||
final duration = _durationToRequireAuth(timestamp ?? 0);
|
||||
final requiredPinInterval = settingsStore.pinTimeOutDuration;
|
||||
|
||||
|
|
|
@ -14,4 +14,13 @@ class FailureState extends ExecutionState {
|
|||
FailureState(this.error);
|
||||
|
||||
final String error;
|
||||
}
|
||||
|
||||
class AwaitingConfirmationState extends ExecutionState {
|
||||
AwaitingConfirmationState({this.title, this.message, this.onConfirm, this.onCancel});
|
||||
|
||||
final String? title;
|
||||
final String? message;
|
||||
final Function()? onConfirm;
|
||||
final Function()? onCancel;
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:cake_wallet/core/secure_storage.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:cake_wallet/entities/secret_store_key.dart';
|
||||
import 'package:cake_wallet/entities/encrypt.dart';
|
||||
|
@ -10,7 +11,7 @@ class KeyService {
|
|||
Future<String> getWalletPassword({required String walletName}) async {
|
||||
final key = generateStoreKeyFor(
|
||||
key: SecretStoreKey.moneroWalletPassword, walletName: walletName);
|
||||
final encodedPassword = await _secureStorage.read(key: key);
|
||||
final encodedPassword = await readSecureStorage(_secureStorage, key);
|
||||
return decodeWalletPassword(password: encodedPassword!);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,3 +8,8 @@ class NodeAddressValidator extends TextValidator {
|
|||
pattern:
|
||||
'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\$|^[0-9a-zA-Z.\-]+\$');
|
||||
}
|
||||
|
||||
class NodePathValidator extends TextValidator {
|
||||
NodePathValidator()
|
||||
: super(errorMessage: S.current.error_text_node_address, pattern: '^([/0-9a-zA-Z.\-]+)?\$');
|
||||
}
|
||||
|
|
27
lib/core/secure_storage.dart
Normal file
|
@ -0,0 +1,27 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
// For now, we can create a utility function to handle this.
|
||||
//
|
||||
// However, we could look into abstracting the entire FlutterSecureStorage package
|
||||
// so the app doesn't depend on the package directly but an absraction.
|
||||
// It'll make these kind of modifications to read/write come from a single point.
|
||||
|
||||
Future<String?> readSecureStorage(FlutterSecureStorage secureStorage, String key) async {
|
||||
String? result;
|
||||
const maxWait = Duration(seconds: 3);
|
||||
const checkInterval = Duration(milliseconds: 200);
|
||||
|
||||
DateTime start = DateTime.now();
|
||||
|
||||
while (result == null && DateTime.now().difference(start) < maxWait) {
|
||||
result = await secureStorage.read(key: key);
|
||||
|
||||
if (result != null) {
|
||||
break;
|
||||
}
|
||||
|
||||
await Future.delayed(checkInterval);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
|
||||
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart';
|
||||
import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
|
||||
import 'package:cake_wallet/entities/preferences_key.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/core/wallet_connect/models/auth_request_model.dart';
|
||||
import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
|
||||
|
@ -19,6 +21,7 @@ import 'package:cw_core/wallet_type.dart';
|
|||
import 'package:eth_sig_util/eth_sig_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
|
||||
|
||||
import 'chain_service/solana/solana_chain_id.dart';
|
||||
|
@ -32,6 +35,7 @@ class Web3WalletService = Web3WalletServiceBase with _$Web3WalletService;
|
|||
|
||||
abstract class Web3WalletServiceBase with Store {
|
||||
final AppStore appStore;
|
||||
final SharedPreferences sharedPreferences;
|
||||
final BottomSheetService _bottomSheetHandler;
|
||||
final WalletConnectKeyService walletKeyService;
|
||||
|
||||
|
@ -52,7 +56,8 @@ abstract class Web3WalletServiceBase with Store {
|
|||
@observable
|
||||
ObservableList<StoredCacao> auth;
|
||||
|
||||
Web3WalletServiceBase(this._bottomSheetHandler, this.walletKeyService, this.appStore)
|
||||
Web3WalletServiceBase(
|
||||
this._bottomSheetHandler, this.walletKeyService, this.appStore, this.sharedPreferences)
|
||||
: pairings = ObservableList<PairingInfo>(),
|
||||
sessions = ObservableList<SessionData>(),
|
||||
auth = ObservableList<StoredCacao>(),
|
||||
|
@ -133,13 +138,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,
|
||||
|
@ -177,13 +196,6 @@ abstract class Web3WalletServiceBase with Store {
|
|||
_refreshPairings();
|
||||
}
|
||||
|
||||
@action
|
||||
void _refreshPairings() {
|
||||
pairings.clear();
|
||||
final allPairings = _web3Wallet.pairings.getAll();
|
||||
pairings.addAll(allPairings);
|
||||
}
|
||||
|
||||
Future<void> _onSessionProposalError(SessionProposalErrorEvent? args) async {
|
||||
log(args.toString());
|
||||
}
|
||||
|
@ -246,14 +258,37 @@ abstract class Web3WalletServiceBase with Store {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void _refreshPairings() {
|
||||
print('Refreshing pairings');
|
||||
pairings.clear();
|
||||
|
||||
final allPairings = _web3Wallet.pairings.getAll();
|
||||
|
||||
final keyForWallet = getKeyForStoringTopicsForWallet();
|
||||
|
||||
final currentTopicsForWallet = getPairingTopicsForWallet(keyForWallet);
|
||||
|
||||
final filteredPairings =
|
||||
allPairings.where((pairing) => currentTopicsForWallet.contains(pairing.topic)).toList();
|
||||
|
||||
pairings.addAll(filteredPairings);
|
||||
}
|
||||
|
||||
void _onPairingCreate(PairingEvent? args) {
|
||||
log('Pairing Create Event: $args');
|
||||
}
|
||||
|
||||
@action
|
||||
void _onSessionConnect(SessionConnect? args) {
|
||||
Future<void> _onSessionConnect(SessionConnect? args) async {
|
||||
if (args != null) {
|
||||
log('Session Connected $args');
|
||||
|
||||
await savePairingTopicToLocalStorage(args.session.pairingTopic);
|
||||
|
||||
sessions.add(args.session);
|
||||
|
||||
_refreshPairings();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -321,4 +356,53 @@ abstract class Web3WalletServiceBase with Store {
|
|||
List<SessionData> getSessionsForPairingInfo(PairingInfo pairing) {
|
||||
return sessions.where((element) => element.pairingTopic == pairing.topic).toList();
|
||||
}
|
||||
|
||||
String getKeyForStoringTopicsForWallet() {
|
||||
List<ChainKeyModel> chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
|
||||
|
||||
final keyForPairingTopic =
|
||||
PreferencesKey.walletConnectPairingTopicsListForWallet(chainKeys.first.publicKey);
|
||||
|
||||
return keyForPairingTopic;
|
||||
}
|
||||
|
||||
List<String> getPairingTopicsForWallet(String key) {
|
||||
// Get the JSON-encoded string from shared preferences
|
||||
final jsonString = sharedPreferences.getString(key);
|
||||
|
||||
// If the string is null, return an empty list
|
||||
if (jsonString == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Decode the JSON string to a list of strings
|
||||
final List<dynamic> jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
|
||||
// Cast each item to a string
|
||||
return jsonList.map((item) => item as String).toList();
|
||||
}
|
||||
|
||||
Future<void> savePairingTopicToLocalStorage(String pairingTopic) async {
|
||||
// Get key specific to the current wallet
|
||||
final key = getKeyForStoringTopicsForWallet();
|
||||
|
||||
// Get all pairing topics attached to this key
|
||||
final pairingTopicsForWallet = getPairingTopicsForWallet(key);
|
||||
|
||||
print(pairingTopicsForWallet);
|
||||
|
||||
bool isPairingTopicAlreadySaved = pairingTopicsForWallet.contains(pairingTopic);
|
||||
print('Is Pairing Topic Saved: $isPairingTopicAlreadySaved');
|
||||
|
||||
if (!isPairingTopicAlreadySaved) {
|
||||
// Update the list with the most recent pairing topic
|
||||
pairingTopicsForWallet.add(pairingTopic);
|
||||
|
||||
// Convert the list of updated pairing topics to a JSON-encoded string
|
||||
final jsonString = jsonEncode(pairingTopicsForWallet);
|
||||
|
||||
// Save the encoded string to shared preferences
|
||||
await sharedPreferences.setString(key, jsonString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
18
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';
|
||||
|
@ -491,6 +493,7 @@ Future<void> setup({
|
|||
getIt.get<BottomSheetService>(),
|
||||
getIt.get<WalletConnectKeyService>(),
|
||||
appStore,
|
||||
getIt.get<SharedPreferences>()
|
||||
);
|
||||
web3WalletService.create();
|
||||
return web3WalletService;
|
||||
|
@ -806,8 +809,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 +916,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 +1140,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,10 +212,17 @@ Future<void> defaultSettingsMigration(
|
|||
await changeDefaultBitcoinNode(nodes, sharedPreferences);
|
||||
break;
|
||||
|
||||
case 31:
|
||||
await updateBtcNanoWalletInfos(walletInfoSource);
|
||||
case 30:
|
||||
await disableServiceStatusFiatDisabled(sharedPreferences);
|
||||
break;
|
||||
|
||||
case 31:
|
||||
await updateNanoNodeList(nodes: nodes);
|
||||
break;
|
||||
|
||||
case 32:
|
||||
await updateBtcNanoWalletInfos(walletInfoSource);
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -229,6 +237,44 @@ Future<void> defaultSettingsMigration(
|
|||
await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version);
|
||||
}
|
||||
|
||||
Future<void> updateNanoNodeList({required Box<Node> nodes}) async {
|
||||
final nodeList = await loadDefaultNanoNodes();
|
||||
var listOfNewEndpoints = <String>[
|
||||
"app.natrium.io",
|
||||
"rainstorm.city",
|
||||
"node.somenano.com",
|
||||
"nanoslo.0x.no",
|
||||
"www.bitrequest.app",
|
||||
];
|
||||
// add new nodes:
|
||||
for (final node in nodeList) {
|
||||
if (listOfNewEndpoints.contains(node.uriRaw)) {
|
||||
await nodes.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
// update the nautilus node:
|
||||
final nautilusNode =
|
||||
nodes.values.firstWhereOrNull((element) => element.uriRaw == "node.perish.co");
|
||||
if (nautilusNode != null) {
|
||||
nautilusNode.uriRaw = "node.nautilus.io";
|
||||
nautilusNode.path = "/api";
|
||||
nautilusNode.useSSL = true;
|
||||
await nautilusNode.save();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disableServiceStatusFiatDisabled(SharedPreferences sharedPreferences) async {
|
||||
final currentFiat = await sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey) ?? -1;
|
||||
if (currentFiat == -1 || currentFiat == FiatApiMode.enabled.raw) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentFiat == FiatApiMode.disabled.raw || currentFiat == FiatApiMode.torOnly.raw) {
|
||||
await sharedPreferences.setBool(PreferencesKey.disableBulletinKey, true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateMoneroPriority(SharedPreferences sharedPreferences) async {
|
||||
final currentPriority =
|
||||
await sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority) ??
|
||||
|
|
|
@ -20,6 +20,7 @@ class PreferencesKey {
|
|||
static const isAppSecureKey = 'is_app_secure';
|
||||
static const disableBuyKey = 'disable_buy';
|
||||
static const disableSellKey = 'disable_sell';
|
||||
static const disableBulletinKey = 'disable_bulletin';
|
||||
static const defaultBuyProvider = 'default_buy_provider';
|
||||
static const walletListOrder = 'wallet_list_order';
|
||||
static const walletListAscending = 'wallet_list_ascending';
|
||||
|
@ -41,8 +42,10 @@ 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 shouldShowRepWarning = 'should_show_rep_warning';
|
||||
static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1';
|
||||
static const syncModeKey = 'sync_mode';
|
||||
static const syncAllKey = 'sync_all';
|
||||
|
@ -73,4 +76,7 @@ class PreferencesKey {
|
|||
static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard';
|
||||
static const isNewInstall = 'is_new_install';
|
||||
static const serviceStatusShaKey = 'service_status_sha_key';
|
||||
static const walletConnectPairingTopicsList = 'wallet_connect_pairing_topics_list';
|
||||
static String walletConnectPairingTopicsListForWallet(String publicKey) =>
|
||||
'${PreferencesKey.walletConnectPairingTopicsList}_${publicKey}';
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
@ -55,18 +55,18 @@ class ProvidersHelper {
|
|||
case WalletType.monero:
|
||||
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.polygon:
|
||||
case WalletType.ethereum:
|
||||
return [
|
||||
ProviderType.askEachTime,
|
||||
ProviderType.onramper,
|
||||
ProviderType.dfx,
|
||||
ProviderType.robinhood,
|
||||
ProviderType.moonpay,
|
||||
];
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoinCash:
|
||||
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
|
||||
case WalletType.polygon:
|
||||
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
|
||||
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay];
|
||||
case WalletType.solana:
|
||||
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
|
||||
case WalletType.none:
|
||||
|
@ -79,23 +79,22 @@ class ProvidersHelper {
|
|||
switch (walletType) {
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
return [
|
||||
ProviderType.askEachTime,
|
||||
ProviderType.onramper,
|
||||
ProviderType.moonpaySell,
|
||||
ProviderType.moonpay,
|
||||
ProviderType.dfx,
|
||||
];
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoinCash:
|
||||
return [ProviderType.askEachTime, ProviderType.moonpaySell];
|
||||
case WalletType.polygon:
|
||||
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
|
||||
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:
|
||||
|
@ -114,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',
|
||||
|
@ -31,7 +32,17 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
|||
'Exolix',
|
||||
'Godex',
|
||||
'Exch',
|
||||
'CoinCraddle'
|
||||
'CoinCraddle',
|
||||
'Alfacash',
|
||||
'LocalMonero',
|
||||
'XChange',
|
||||
'NeroSwap',
|
||||
'Changee',
|
||||
'BitcoinVN',
|
||||
'EasyBit',
|
||||
'WizardSwap',
|
||||
'Quantex',
|
||||
'SwapSpace',
|
||||
];
|
||||
|
||||
static const List<CryptoCurrency> _notSupported = [
|
||||
|
@ -144,8 +155,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 +186,6 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
|||
params['id'] = _lastUsedRateId;
|
||||
}
|
||||
|
||||
|
||||
String firstAvailableProvider = '';
|
||||
|
||||
for (var provider in _provider) {
|
||||
|
@ -225,7 +238,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');
|
||||
|
|
|
@ -153,25 +153,26 @@ 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: 31);
|
||||
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: 32,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> initialSetup(
|
||||
{required SharedPreferences sharedPreferences,
|
||||
{required SharedPreferences sharedPreferences,
|
||||
required Box<Node> nodes,
|
||||
required Box<Node> powNodes,
|
||||
required Box<WalletInfo> walletInfoSource,
|
||||
|
|
|
@ -179,6 +179,16 @@ class CWNano extends Nano {
|
|||
String getRepresentative(Object wallet) {
|
||||
return (wallet as NanoWallet).representative;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<N2Node>> getN2Reps(Object wallet) async {
|
||||
return (wallet as NanoWallet).getN2Reps();
|
||||
}
|
||||
|
||||
@override
|
||||
bool isRepOk(Object wallet) {
|
||||
return (wallet as NanoWallet).isRepOk;
|
||||
}
|
||||
}
|
||||
|
||||
class CWNanoUtil extends NanoUtil {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|