rewrite extension around new UX
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
parent
66bd2077c7
commit
d3274222f9
@ -11,14 +11,14 @@
|
|||||||
"chainType": "Live",
|
"chainType": "Live",
|
||||||
"forkBlocks": null,
|
"forkBlocks": null,
|
||||||
"genesis": {
|
"genesis": {
|
||||||
"stateRootHash": "0xfcb09113f5c18bb681bd0376bdb01a1d672030cf021ff24f017f9c90d6340669"
|
"stateRootHash": "0xb4784a3c8f2f29ba3f191d563f1166f08f4e0337e114679c0f24ef77d06e10e4"
|
||||||
},
|
},
|
||||||
"id": "casper_staging_testnet",
|
"id": "casper_staging_testnet",
|
||||||
"lightSyncState": {
|
"lightSyncState": {
|
||||||
"babeEpochChanges": "0x0486e94817613148af08d25b61d10de83b314aad94478565e9d5070ad5bf1384de181c00000164dc951100000000c4e59511000000000468c4943cf7c7ced2abbe900614a80a5e8a3029eca80186350ff93745379523007825000001c4e595110000000024ef951100000000047460d5a9745e7ac79c62165f29bfc35ca816684a91e0afe30464781ec663ffd3d82e00000124ef95110000000084f895110000000000000c68c4943cf7c7ced2abbe900614a80a5e8a3029eca80186350ff937453795230078250000010500000000000000c4e5951100000000600900000000000010daaaaab6a6e574099e24ae9bb75b543610edef9d374fa85a378edb573b47615f010000000000000074fa7381a7a74b316afb6793a00387eed9d95d46a69866cbb316b5d9c918af0e01000000000000006c4dd88b43e2011cf9a6a73d53446336ac9e04cdd4ca23587df63187ac455e4901000000000000003c944c704cae203619b9e7a5a4b6742736da6a8e76c762291bebdc7652cfec2f01000000000000000e662d4ad2b3bf5eb96ec0fade8662d2225d4717319580d1c4eb932e8bce917d01000000000000000400000000000000027460d5a9745e7ac79c62165f29bfc35ca816684a91e0afe30464781ec663ffd3d82e000001060000000000000024ef951100000000600900000000000028426ab855ba3d0eaade067c62f197989e13c28641948d018d657c13c139da9e450100000000000000daaaaab6a6e574099e24ae9bb75b543610edef9d374fa85a378edb573b47615f010000000000000074fa7381a7a74b316afb6793a00387eed9d95d46a69866cbb316b5d9c918af0e0100000000000000dee45a9f88b2829e831f26908a7769db5fd38e7c0fc0cfec3fd83cede451fc120100000000000000c66631a4b6220beb0ad83498ddd36b2cb53fc0e345edd50e6d40dcfc74cc6b7b01000000000000006c4dd88b43e2011cf9a6a73d53446336ac9e04cdd4ca23587df63187ac455e4901000000000000003c944c704cae203619b9e7a5a4b6742736da6a8e76c762291bebdc7652cfec2f01000000000000002c4d7c0e011f53f81f31a071f3d4c42f1e85ad60e50918bc841a5001b9763f4f01000000000000003a9afa87f78c79af14753c30851f66414604987dc5ba85dd568403e0963c927d01000000000000001e06092d5210b61cadd1118e3142ecafc4d53de4267c1d7ad8205d50e64c5d4601000000000000003584ab6c4870be3cb13b2b69b7569fd593d8354b202917d61a6f139f70187b24010000000000000004000000000000000286e94817613148af08d25b61d10de83b314aad94478565e9d5070ad5bf1384de181c000001040000000000000064dc951100000000600900000000000010daaaaab6a6e574099e24ae9bb75b543610edef9d374fa85a378edb573b47615f010000000000000074fa7381a7a74b316afb6793a00387eed9d95d46a69866cbb316b5d9c918af0e01000000000000006c4dd88b43e2011cf9a6a73d53446336ac9e04cdd4ca23587df63187ac455e4901000000000000003c944c704cae203619b9e7a5a4b6742736da6a8e76c762291bebdc7652cfec2f010000000000000005cfbdbd5ccbf25f2bc82b8488ef493b8ee1205d2d1e0b2a52689d8ed252c1470100000000000000040000000000000002",
|
"babeEpochChanges": "0x042c315b655c7144df4bfdc41837eeacb84d80aee06eaba51f6941a0c7a26cb1bf5d090000015e9de408000000000ea2e40800000000049b2e88b8ce43a677c1ca90f62134800b7e510c0c353dad719cad82faf6136cfb0d0e0000010ea2e40800000000bea6e408000000000000082c315b655c7144df4bfdc41837eeacb84d80aee06eaba51f6941a0c7a26cb1bf5d0900000103000000000000005e9de40800000000b00400000000000014daaaaab6a6e574099e24ae9bb75b543610edef9d374fa85a378edb573b47615f010000000000000074fa7381a7a74b316afb6793a00387eed9d95d46a69866cbb316b5d9c918af0e0100000000000000e0f0a776ecc9fa5e1f22e2fa001fe3fba5aea52b9444bc894b45589d4213247501000000000000006c4dd88b43e2011cf9a6a73d53446336ac9e04cdd4ca23587df63187ac455e4901000000000000003c944c704cae203619b9e7a5a4b6742736da6a8e76c762291bebdc7652cfec2f01000000000000002ff7c765405e9c11a59525a08173bb1fa59e5e281090f8e4d1824469f513d0cc01000000000000000400000000000000029b2e88b8ce43a677c1ca90f62134800b7e510c0c353dad719cad82faf6136cfb0d0e00000104000000000000000ea2e40800000000b00400000000000014daaaaab6a6e574099e24ae9bb75b543610edef9d374fa85a378edb573b47615f010000000000000074fa7381a7a74b316afb6793a00387eed9d95d46a69866cbb316b5d9c918af0e0100000000000000e0f0a776ecc9fa5e1f22e2fa001fe3fba5aea52b9444bc894b45589d4213247501000000000000006c4dd88b43e2011cf9a6a73d53446336ac9e04cdd4ca23587df63187ac455e4901000000000000003c944c704cae203619b9e7a5a4b6742736da6a8e76c762291bebdc7652cfec2f0100000000000000dea465ff8bf16938cc9ead89bf9fdb5d9c495fad61fd952b2067ebdc51c4e1c10100000000000000040000000000000002",
|
||||||
"babeFinalizedBlockWeight": 3115,
|
"babeFinalizedBlockWeight": 1039,
|
||||||
"finalizedBlockHeader": "0x40ab762007d6e2d1dddb18a10073e637712b425bac09fe2d87e50373afa1fcb111bd1cd1b45c2547cc5123ca78564a1fe0bcaced95cf3e0c7cf305b7cb443e2c037d6a40040ac0406b97a19a74625c9a717e9a0a922e41d733a88e8ce860fce8e278080642414245b501030100000030e69511000000006279297ea7ba3278c5a8fcbd16a7fd02d14fa771983443ff8d1fc4d536c8684a00ac542d13f1b03c54f5c4e51f473d326bad1feae09b95a632aa87da9c491c0cae7d5981d364a093b341db9a0028af5540957f6a01b952d3c0ef18b52c2f490d054241424501016e1a35643f1fe6e1f08e8ef0b2165368d2ae2a576a0bb10584c58a27005e2c5b5ef4d86c1f2dd98ac498c8cb3b314def9e72aaaf1b446c36f345339091a76487",
|
"finalizedBlockHeader": "0x6ed6c2debc2e5fd4e7db0d14426d1b904e6c101f2c62a71b8e891c217ba79079f9433280c0257a860f793833bd6bed6b9f86094400aa8e5ea24a81752f0f046ff5ed560f7231760c622ae35d3968fbab033ed34d950b57a7937ae45c411d1d384748080642414245b50103000000004fa0e40800000000a2c7d71b4e1f94c0208c9730ab93955b08c8d7ac86d2fc6ec5f62cc3272a5f1ec0e28c798461372109ee3e12f03ff66ea5ad9cac5b1c37d7280d2333d0226f0865ea77a801bad79785a8ca83c87f0dbf1e36936a6f65cd7b125a41c52f38f80105424142450101caaa6fe5f9df57930e2ac08aef43b75d5477d410c73c3a8e6dea9d5503b9fd25eae1d12b6604194389e22f3f4265d581eb162d1dc5a9034247fe33765dddbe8c",
|
||||||
"grandpaAuthoritySet": "0x1055446f9a7aa99ced06b317c80ce90d56b84e56526775683af2525969e8da0b640100000000000000236d2fa03f4ed8cb65de7e514d7540159b328f1c170dd402b094ad7fbf54721801000000000000008f9ea20bf4a807a8e710f7559dece86e94672b5b361de157bdaa5c1f37849f8d01000000000000000aa3a88f6b777c95c3dfe7e997b76798413f16aa325f34824cae0c9102b281d5010000000000000000000000000000000001442f00000000"
|
"grandpaAuthoritySet": "0x1455446f9a7aa99ced06b317c80ce90d56b84e56526775683af2525969e8da0b640100000000000000236d2fa03f4ed8cb65de7e514d7540159b328f1c170dd402b094ad7fbf5472180100000000000000b63c5a0cf342b9b04931bc8ed74d7d0165ab99ab5f8a4514797d4b299a4501fe01000000000000008f9ea20bf4a807a8e710f7559dece86e94672b5b361de157bdaa5c1f37849f8d01000000000000000aa3a88f6b777c95c3dfe7e997b76798413f16aa325f34824cae0c9102b281d5010000000000000000000000000000000001fe1000000000"
|
||||||
},
|
},
|
||||||
"name": "Casper Staging Testnet",
|
"name": "Casper Staging Testnet",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
32
assets/logos/casper.svg
Normal file
32
assets/logos/casper.svg
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="uuid-29cf08a4-4a65-4410-8aa5-350a28213b48" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe {
|
||||||
|
fill: #345b87;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uuid-b9fad165-95df-4ba5-b7d8-2290481eaf04 {
|
||||||
|
fill: #f2e370;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g id="uuid-f073e362-1a02-4167-a269-10b1f33990ea" data-name="uuid-2a2ed4dd-1266-4b44-b1c2-f456e6ec6f49">
|
||||||
|
<g id="uuid-744a1c1d-d715-4b76-a1d0-fb54d03f9094" data-name="uuid-fb84f985-acc6-4529-8860-a7d17e43f533">
|
||||||
|
<g>
|
||||||
|
<circle class="uuid-b9fad165-95df-4ba5-b7d8-2290481eaf04" cx="125" cy="125" r="122.5"/>
|
||||||
|
<path class="uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe" d="m125,5c32.05,0,62.19,12.48,84.85,35.15s35.15,52.8,35.15,84.85-12.48,62.19-35.15,84.85c-22.67,22.67-52.8,35.15-84.85,35.15s-62.19-12.48-84.85-35.15C17.48,187.19,5,157.05,5,125s12.48-62.19,35.15-84.85C62.81,17.48,92.95,5,125,5m0-5C55.96,0,0,55.96,0,125s55.96,125,125,125,125-55.96,125-125S194.04,0,125,0h0Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="uuid-4871e70f-254a-45a2-84fe-1d7c3034da86" data-name="uuid-1a93fc72-ded1-41af-8e7c-010273c35667">
|
||||||
|
<path class="uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe" d="m135.65,68.54c.11,0,.22,0,.33.02l.22.04,4.48,1.05,1.43.42c.34.1.67.2,1,.3.65.19,1.29.38,1.93.59l2.86,1.04c4.22,1.67,7.84,3.5,11.14,5.63,7.13,4.65,13.1,10.9,17.26,18.08,4.11,7.04,6.75,15.28,7.63,23.83.45,4.44.44,8.69,0,12.65-.47,4.09-1.28,7.61-2.49,10.77-.41,1.07-1.25,1.92-2.32,2.34s-2.27.38-3.3-.12c-1.95-.95-2.82-3.25-1.99-5.25,1.03-2.47,1.8-5.41,2.27-8.73.49-3.4.6-7.1.32-11-.56-7.54-2.66-14.85-6.09-21.13-3.46-6.4-8.55-12.04-14.71-16.32-2.89-1.99-6.12-3.74-9.87-5.34l-2.67-1.06c-.5-.19-1.07-.37-1.64-.55-.34-.11-.69-.22-1.03-.34l-1.32-.43-4.31-1.14c-1.1-.35-2.07-1.52-1.79-3.1.26-1.47,1.5-2.26,2.65-2.26h.01Z"/>
|
||||||
|
<path class="uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe" d="m83.09,153.6c1.37,0,2.69.69,3.46,1.92,1.64,2.63,3.72,5.09,6.17,7.29,2.78,2.53,5.95,4.78,9.42,6.71,6.84,3.8,14.53,6.08,22.23,6.58,3.91.25,7.82.04,11.62-.64,3.78-.68,7.52-1.86,11.12-3.49,3.45-1.57,6.85-3.6,10.13-6.06,2.93-2.27,5.8-4.94,8.74-8.14l.19-.18c.87-.76,2.38-.97,3.57.11,1.15,1.04,1.14,2.52.54,3.45l-.2.26c-3.06,3.52-6.08,6.49-9.24,9.08-3.57,2.84-7.29,5.2-11.09,7.05-4.02,1.96-8.23,3.41-12.51,4.31-4.31.89-8.75,1.26-13.22,1.09-8.75-.34-17.55-2.68-25.45-6.78-4.05-2.1-7.78-4.61-11.08-7.44-3.07-2.62-5.71-5.57-7.83-8.76-.63-.95-.84-2.14-.57-3.25s1-2.06,2-2.61c.63-.35,1.31-.51,1.98-.51h.02Z"/>
|
||||||
|
<path class="uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe" d="m113.94,67.47c.76,0,1.51.21,2.16.61.95.59,1.63,1.55,1.86,2.64.45,2.16-.92,4.32-3.05,4.82-6.07,1.42-12.11,4.08-17.49,7.68-5.92,3.97-11.02,9.05-14.75,14.69-3.86,5.81-6.41,12.52-7.38,19.41-.96,6.47-.53,13.57,1.27,21.12l.04.18c.18,1.15-.43,2.55-1.95,3-1.48.43-2.76-.39-3.25-1.43l-.09-.19-.05-.2c-2.23-8.2-2.93-16.03-2.09-23.27.86-7.84,3.53-15.57,7.72-22.32,4.04-6.55,9.66-12.5,16.23-17.2,6.1-4.36,12.75-7.52,19.76-9.4.35-.09.71-.14,1.06-.14Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="uuid-0cc46f90-c0cd-4f64-85d6-dec8a4b73fc6" data-name="uuid-977abf79-b623-4053-ab7b-a617d834d6cf">
|
||||||
|
<path class="uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe" d="m178.97,83.02c1.57,3.93,4.57,7.01,8.46,8.68,2.89,1.24,5.95,1.55,8.84,1.06,3.25,6.6,5.62,13.81,6.98,21.39,2.3,12.74,1.62,25.68-1.97,37.4-1.85,6.05-4.48,11.82-7.82,17.13-3.3,5.27-7.37,10.18-12.08,14.61-4.45,4.19-9.63,8.08-15.34,11.52-5.3,3.08-11.08,5.82-17.68,8.38l-.52.2-.43.35c-1.4,1.12-1.93,3.01-1.32,4.71.71,1.95,2.75,3.08,4.74,2.61l.38-.11c7.11-2.5,13.39-5.24,19.25-8.39,6.44-3.6,12.28-7.71,17.37-12.19,5.48-4.82,10.26-10.24,14.2-16.11,4.02-5.96,7.25-12.47,9.59-19.35,4.54-13.3,5.8-28.07,3.64-42.72-1.38-9.42-4.14-18.45-8.11-26.71.41-.66.77-1.35,1.08-2.08,3.45-8.03-.28-17.36-8.31-20.81-1.88-.81-3.82-1.22-5.75-1.28-6.32-.19-12.42,3.43-15.06,9.58-1.67,3.89-1.73,8.19-.16,12.12h.02Z"/>
|
||||||
|
<path class="uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe" d="m54.73,98.51c8.74.08,15.91-6.96,15.99-15.7.03-3.12-.86-6.05-2.4-8.52,3.95-4.62,8.41-8.85,13.25-12.51,8.91-6.76,19.25-11.58,29.93-13.93,10.7-2.38,22.16-2.24,33.16.39,10.09,2.34,20.49,7.06,30.91,14.02l.09.06c1.82,1.14,4.24.63,5.51-1.15,1.29-1.81.94-4.28-.78-5.61l-.1-.07c-11.02-7.96-22.22-13.52-33.26-16.51-7.2-2.01-14.65-3.05-22.05-3.12-5.27-.05-10.51.4-15.63,1.34-12.34,2.25-24.43,7.35-34.94,14.74-6.17,4.32-11.87,9.43-16.9,15.1-.81-.14-1.64-.21-2.49-.22-8.74-.08-15.91,6.96-15.99,15.7s6.96,15.91,15.7,15.99Z"/>
|
||||||
|
<path class="uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe" d="m129.64,191.95c-1.28-.35-2.57-.54-3.87-.57-2.83-.06-5.64.64-8.17,2.08-2.59,1.47-4.65,3.6-6.04,6.15-6.41-1.32-12.79-3.45-18.92-6.36-10.76-5.15-20.35-12.54-27.73-21.36-7.56-8.99-12.97-19.82-15.67-31.34-2.59-10.69-2.85-22.71-.77-35.71l.04-.46c.06-2.05-1.43-3.83-3.46-4.16-.2-.03-.39-.05-.58-.05-1.62-.04-3.11.85-3.83,2.33l-.21.43-.09.47c-2.75,14.06-2.9,27.26-.44,39.21,2.59,13.11,8.31,25.6,16.56,36.12,8.05,10.33,18.68,19.12,30.75,25.42,7.48,3.87,15.37,6.72,23.35,8.48.3.83.67,1.65,1.12,2.44,2.1,3.68,5.5,6.32,9.58,7.44s8.35.58,12.03-1.51c3.68-2.1,6.32-5.5,7.44-9.58s.58-8.35-1.51-12.03c-2.1-3.68-5.5-6.32-9.58-7.44h0Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.1 KiB |
33
assets/logos/ghost.svg
Normal file
33
assets/logos/ghost.svg
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="uuid-9f9d14c3-06d2-4ba3-918a-61d473502c89" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.uuid-ce8b5d66-0551-460b-9b55-639040331854 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uuid-f651383e-729c-4543-82ed-5cea5662307d {
|
||||||
|
fill: #345b87;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g id="uuid-57f748d9-5b3e-4f39-b3f4-87f670a07a49" data-name="uuid-2a2ed4dd-1266-4b44-b1c2-f456e6ec6f49">
|
||||||
|
<g id="uuid-42f1c85f-22d4-41d1-813e-1cfb00eb9c2c" data-name="uuid-fb84f985-acc6-4529-8860-a7d17e43f533">
|
||||||
|
<g>
|
||||||
|
<circle class="uuid-ce8b5d66-0551-460b-9b55-639040331854" cx="125" cy="125" r="122.5"/>
|
||||||
|
<path class="uuid-f651383e-729c-4543-82ed-5cea5662307d" d="m125,5c32.05,0,62.19,12.48,84.85,35.15s35.15,52.8,35.15,84.85-12.48,62.19-35.15,84.85c-22.67,22.67-52.8,35.15-84.85,35.15s-62.19-12.48-84.85-35.15C17.48,187.19,5,157.05,5,125s12.48-62.19,35.15-84.85C62.81,17.48,92.95,5,125,5m0-5C55.96,0,0,55.96,0,125s55.96,125,125,125,125-55.96,125-125S194.04,0,125,0h0Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="uuid-9e11c10f-2d67-4caf-a85c-64f8a8abdac4" data-name="uuid-1a93fc72-ded1-41af-8e7c-010273c35667">
|
||||||
|
<path class="uuid-f651383e-729c-4543-82ed-5cea5662307d" d="m135.65,68.54c.11,0,.22,0,.33.02l.22.04,4.48,1.05,1.43.42c.34.1.67.2,1,.3.65.19,1.29.38,1.93.59l2.86,1.04c4.22,1.67,7.84,3.5,11.14,5.63,7.13,4.65,13.1,10.9,17.26,18.08,4.11,7.04,6.75,15.28,7.63,23.83.45,4.44.44,8.69,0,12.65-.47,4.09-1.28,7.61-2.49,10.77-.41,1.07-1.25,1.92-2.32,2.34s-2.27.38-3.3-.12c-1.95-.95-2.82-3.25-1.99-5.25,1.03-2.47,1.8-5.41,2.27-8.73.49-3.4.6-7.1.32-11-.56-7.54-2.66-14.85-6.09-21.13-3.46-6.4-8.55-12.04-14.71-16.32-2.89-1.99-6.12-3.74-9.87-5.34l-2.67-1.06c-.5-.19-1.07-.37-1.64-.55-.34-.11-.69-.22-1.03-.34l-1.32-.43-4.31-1.14c-1.1-.35-2.07-1.52-1.79-3.1.26-1.47,1.5-2.26,2.65-2.26h.01Z"/>
|
||||||
|
<path class="uuid-f651383e-729c-4543-82ed-5cea5662307d" d="m83.09,153.6c1.37,0,2.69.69,3.46,1.92,1.64,2.63,3.72,5.09,6.17,7.29,2.78,2.53,5.95,4.78,9.42,6.71,6.84,3.8,14.53,6.08,22.23,6.58,3.91.25,7.82.04,11.62-.64,3.78-.68,7.52-1.86,11.12-3.49,3.45-1.57,6.85-3.6,10.13-6.06,2.93-2.27,5.8-4.94,8.74-8.14l.19-.18c.87-.76,2.38-.97,3.57.11,1.15,1.04,1.14,2.52.54,3.45l-.2.26c-3.06,3.52-6.08,6.49-9.24,9.08-3.57,2.84-7.29,5.2-11.09,7.05-4.02,1.96-8.23,3.41-12.51,4.31-4.31.89-8.75,1.26-13.22,1.09-8.75-.34-17.55-2.68-25.45-6.78-4.05-2.1-7.78-4.61-11.08-7.44-3.07-2.62-5.71-5.57-7.83-8.76-.63-.95-.84-2.14-.57-3.25s1-2.06,2-2.61c.63-.35,1.31-.51,1.98-.51h.02Z"/>
|
||||||
|
<path class="uuid-f651383e-729c-4543-82ed-5cea5662307d" d="m113.94,67.47c.76,0,1.51.21,2.16.61.95.59,1.63,1.55,1.86,2.64.45,2.16-.92,4.32-3.05,4.82-6.07,1.42-12.11,4.08-17.49,7.68-5.92,3.97-11.02,9.05-14.75,14.69-3.86,5.81-6.41,12.52-7.38,19.41-.96,6.47-.53,13.57,1.27,21.12l.04.18c.18,1.15-.43,2.55-1.95,3-1.48.43-2.76-.39-3.25-1.43l-.09-.19-.05-.2c-2.23-8.2-2.93-16.03-2.09-23.27.86-7.84,3.53-15.57,7.72-22.32,4.04-6.55,9.66-12.5,16.23-17.2,6.1-4.36,12.75-7.52,19.76-9.4.35-.09.71-.14,1.06-.14Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="uuid-2b145d23-c492-471a-b246-f2e75af15020" data-name="uuid-977abf79-b623-4053-ab7b-a617d834d6cf">
|
||||||
|
<path class="uuid-f651383e-729c-4543-82ed-5cea5662307d" d="m178.97,83.02c1.57,3.93,4.57,7.01,8.46,8.68,2.89,1.24,5.95,1.55,8.84,1.06,3.25,6.6,5.62,13.81,6.98,21.39,2.3,12.74,1.62,25.68-1.97,37.4-1.85,6.05-4.48,11.82-7.82,17.13-3.3,5.27-7.37,10.18-12.08,14.61-4.45,4.19-9.63,8.08-15.34,11.52-5.3,3.08-11.08,5.82-17.68,8.38l-.52.2-.43.35c-1.4,1.12-1.93,3.01-1.32,4.71.71,1.95,2.75,3.08,4.74,2.61l.38-.11c7.11-2.5,13.39-5.24,19.25-8.39,6.44-3.6,12.28-7.71,17.37-12.19,5.48-4.82,10.26-10.24,14.2-16.11,4.02-5.96,7.25-12.47,9.59-19.35,4.54-13.3,5.8-28.07,3.64-42.72-1.38-9.42-4.14-18.45-8.11-26.71.41-.66.77-1.35,1.08-2.08,3.45-8.03-.28-17.36-8.31-20.81-1.88-.81-3.82-1.22-5.75-1.28-6.32-.19-12.42,3.43-15.06,9.58-1.67,3.89-1.73,8.19-.16,12.12h.02Z"/>
|
||||||
|
<path class="uuid-f651383e-729c-4543-82ed-5cea5662307d" d="m54.73,98.51c8.74.08,15.91-6.96,15.99-15.7.03-3.12-.86-6.05-2.4-8.52,3.95-4.62,8.41-8.85,13.25-12.51,8.91-6.76,19.25-11.58,29.93-13.93,10.7-2.38,22.16-2.24,33.16.39,10.09,2.34,20.49,7.06,30.91,14.02l.09.06c1.82,1.14,4.24.63,5.51-1.15,1.29-1.81.94-4.28-.78-5.61l-.1-.07c-11.02-7.96-22.22-13.52-33.26-16.51-7.2-2.01-14.65-3.05-22.05-3.12-5.27-.05-10.51.4-15.63,1.34-12.34,2.25-24.43,7.35-34.94,14.74-6.17,4.32-11.87,9.43-16.9,15.1-.81-.14-1.64-.21-2.49-.22-8.74-.08-15.91,6.96-15.99,15.7s6.96,15.91,15.7,15.99Z"/>
|
||||||
|
<path class="uuid-f651383e-729c-4543-82ed-5cea5662307d" d="m129.64,191.95c-1.28-.35-2.57-.54-3.87-.57-2.83-.06-5.64.64-8.17,2.08-2.59,1.47-4.65,3.6-6.04,6.15-6.41-1.32-12.79-3.45-18.92-6.36-10.76-5.15-20.35-12.54-27.73-21.36-7.56-8.99-12.97-19.82-15.67-31.34-2.59-10.69-2.85-22.71-.77-35.71l.04-.46c.06-2.05-1.43-3.83-3.46-4.16-.2-.03-.39-.05-.58-.05-1.62-.04-3.11.85-3.83,2.33l-.21.43-.09.47c-2.75,14.06-2.9,27.26-.44,39.21,2.59,13.11,8.31,25.6,16.56,36.12,8.05,10.33,18.68,19.12,30.75,25.42,7.48,3.87,15.37,6.72,23.35,8.48.3.83.67,1.65,1.12,2.44,2.1,3.68,5.5,6.32,9.58,7.44s8.35.58,12.03-1.51c3.68-2.1,6.32-5.5,7.44-9.58s.58-8.35-1.51-12.03c-2.1-3.68-5.5-6.32-9.58-7.44h0Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.1 KiB |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghost-wallet",
|
"name": "ghost-wallet",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"description": "Browser extension to manage ghost blockchain light clients.",
|
"description": "Browser extension to manage ghost blockchain light clients.",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"author": "Uncle f4ts0 <f4ts0@ghostchain.io>",
|
"author": "Uncle f4ts0 <f4ts0@ghostchain.io>",
|
||||||
@ -140,11 +140,13 @@
|
|||||||
"@zag-js/clipboard": "^0.47.0",
|
"@zag-js/clipboard": "^0.47.0",
|
||||||
"@zag-js/react": "^0.47.0",
|
"@zag-js/react": "^0.47.0",
|
||||||
"@zag-js/tabs": "^0.74.2",
|
"@zag-js/tabs": "^0.74.2",
|
||||||
|
"@zip.js/zip.js": "^2.18.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.4",
|
"cmdk": "^1.0.4",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "^8.5.1",
|
"embla-carousel-react": "^8.5.1",
|
||||||
|
"fuse.js": "^7.5.0",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
|
|||||||
@ -170,6 +170,9 @@ importers:
|
|||||||
'@zag-js/tabs':
|
'@zag-js/tabs':
|
||||||
specifier: ^0.74.2
|
specifier: ^0.74.2
|
||||||
version: 0.74.2
|
version: 0.74.2
|
||||||
|
'@zip.js/zip.js':
|
||||||
|
specifier: ^2.18.2
|
||||||
|
version: 2.18.2
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.0
|
specifier: ^0.7.0
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@ -185,6 +188,9 @@ importers:
|
|||||||
embla-carousel-react:
|
embla-carousel-react:
|
||||||
specifier: ^8.5.1
|
specifier: ^8.5.1
|
||||||
version: 8.6.0(react@18.3.1)
|
version: 8.6.0(react@18.3.1)
|
||||||
|
fuse.js:
|
||||||
|
specifier: ^7.5.0
|
||||||
|
version: 7.5.0
|
||||||
input-otp:
|
input-otp:
|
||||||
specifier: ^1.2.4
|
specifier: ^1.2.4
|
||||||
version: 1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
@ -3536,6 +3542,10 @@ packages:
|
|||||||
'@zag-js/utils@0.74.2':
|
'@zag-js/utils@0.74.2':
|
||||||
resolution: {integrity: sha512-WtIsNyDvnslCjtIIP/bRzx3bJMaT0cIgI3f+TgiFWhtQMlUZMpBkwkKVfvUwI5qcZ+ZOMeoonAWFqFECCb3h3g==}
|
resolution: {integrity: sha512-WtIsNyDvnslCjtIIP/bRzx3bJMaT0cIgI3f+TgiFWhtQMlUZMpBkwkKVfvUwI5qcZ+ZOMeoonAWFqFECCb3h3g==}
|
||||||
|
|
||||||
|
'@zip.js/zip.js@2.18.2':
|
||||||
|
resolution: {integrity: sha512-J1jwg0Kv5Aab50VlVZ6hp2rzGL/J0YLjBnkF6oJoIjmpxx9+tHgjRELGKryKnDZEcGXKjv7ibwxAziFiWHdR2A==}
|
||||||
|
engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'}
|
||||||
|
|
||||||
abort-controller@3.0.0:
|
abort-controller@3.0.0:
|
||||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||||
engines: {node: '>=6.5'}
|
engines: {node: '>=6.5'}
|
||||||
@ -4805,6 +4815,10 @@ packages:
|
|||||||
functions-have-names@1.2.3:
|
functions-have-names@1.2.3:
|
||||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||||
|
|
||||||
|
fuse.js@7.5.0:
|
||||||
|
resolution: {integrity: sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
fx-runner@1.4.0:
|
fx-runner@1.4.0:
|
||||||
resolution: {integrity: sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg==}
|
resolution: {integrity: sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -11255,6 +11269,8 @@ snapshots:
|
|||||||
|
|
||||||
'@zag-js/utils@0.74.2': {}
|
'@zag-js/utils@0.74.2': {}
|
||||||
|
|
||||||
|
'@zip.js/zip.js@2.18.2': {}
|
||||||
|
|
||||||
abort-controller@3.0.0:
|
abort-controller@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
event-target-shim: 5.0.1
|
event-target-shim: 5.0.1
|
||||||
@ -12866,6 +12882,8 @@ snapshots:
|
|||||||
|
|
||||||
functions-have-names@1.2.3: {}
|
functions-have-names@1.2.3: {}
|
||||||
|
|
||||||
|
fuse.js@7.5.0: {}
|
||||||
|
|
||||||
fx-runner@1.4.0:
|
fx-runner@1.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
commander: 2.9.0
|
commander: 2.9.0
|
||||||
|
|||||||
@ -26,342 +26,455 @@ import {
|
|||||||
wellKnownDecimalsByGenesisHash
|
wellKnownDecimalsByGenesisHash
|
||||||
} from "../constants"
|
} from "../constants"
|
||||||
|
|
||||||
import {
|
import { listChainSpecsHandler } from "./rpc/chainspec"
|
||||||
addChainSpecHandler,
|
|
||||||
listChainSpecsHandler,
|
|
||||||
removeChainSpecHandler,
|
|
||||||
} from "./rpc/chainspec"
|
|
||||||
|
|
||||||
const isUserSignedExtensionName = (s: string): s is UserSignedExtensionName => {
|
const isUserSignedExtensionName = (s: string): s is UserSignedExtensionName => {
|
||||||
return (
|
return (
|
||||||
s === "CheckMortality" ||
|
s === "CheckMortality" ||
|
||||||
s === "ChargeTransactionPayment" ||
|
s === "ChargeTransactionPayment" ||
|
||||||
s === "ChargeAssetTxPayment"
|
s === "ChargeAssetTxPayment"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyring = createKeyring()
|
const keyring = createKeyring()
|
||||||
|
const requestCooldowns = new Map<string, number>()
|
||||||
|
const COOLDOWN_MS = 3000
|
||||||
let nextSignRequestId = 0
|
let nextSignRequestId = 0
|
||||||
|
|
||||||
export const createBackgroundRpc = (
|
export const createBackgroundRpc = (
|
||||||
sendMessage: (message: RpcMessage) => void
|
sendMessage: (message: RpcMessage) => void
|
||||||
) => {
|
) => {
|
||||||
const getAccounts: RpcMethodHandlers<
|
const getAccounts: RpcMethodHandlers<
|
||||||
BackgroundRpcSpec,
|
BackgroundRpcSpec,
|
||||||
Context
|
Context
|
||||||
>["getAccounts"] = async ([chainId], { lightClientPageHelper }) => {
|
>["getAccounts"] = async ([chainId], { lightClientPageHelper }) => {
|
||||||
const chains = await lightClientPageHelper.getChains()
|
const chains = await lightClientPageHelper.getChains()
|
||||||
const chain = chains.find(({ genesisHash }) => genesisHash === chainId)
|
const chain = chains.find(({ genesisHash }) => genesisHash === chainId)
|
||||||
if (!chain) throw new Error("unknown chain")
|
if (!chain) return []
|
||||||
return (await keyring.getAccounts(chain.genesisHash)).map(
|
|
||||||
({ publicKey }) => ({
|
|
||||||
address: ss58Address(
|
|
||||||
publicKey,
|
|
||||||
wellKnownPrefixByGenesisHash[chain.genesisHash]
|
|
||||||
),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const notifyOnAccountsChanged = async (context: Context) =>
|
const accounts = await keyring.getAccounts(chainId)
|
||||||
context.notifyOnAccountsChanged(
|
const prefix = wellKnownPrefixByGenesisHash[chain.genesisHash]
|
||||||
(
|
|
||||||
await Promise.all(
|
|
||||||
(await context.lightClientPageHelper.getChains()).map(
|
|
||||||
({ genesisHash }) => getAccounts([genesisHash], context)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
).flatMap((accounts) => accounts),
|
|
||||||
)
|
|
||||||
|
|
||||||
const handlers: RpcMethodHandlers<BackgroundRpcSpec, Context> = {
|
return accounts.map(acc => ({
|
||||||
getAccounts,
|
...acc,
|
||||||
async createTx(
|
address: ss58Address(acc.publicKey, prefix)
|
||||||
[chainId, from, callData],
|
}))
|
||||||
{ lightClientPageHelper, signRequests, port },
|
}
|
||||||
) {
|
|
||||||
const url = port.sender?.url
|
|
||||||
if (!url) throw new Error("unknown url")
|
|
||||||
const chains = await lightClientPageHelper.getChains()
|
|
||||||
const chain = chains.find(({ genesisHash }) => genesisHash === chainId)
|
|
||||||
if (!chain) throw new Error("unknown chain")
|
|
||||||
|
|
||||||
const id = nextSignRequestId++
|
const getActiveAccount: RpcMethodHandlers<
|
||||||
const client = getObservableClient(createClient(chain.provider))
|
BackgroundRpcSpec,
|
||||||
const chainHead$ = client.chainHead$()
|
Context
|
||||||
|
>["getActiveAccount"] = async ([chainId], { lightClientPageHelper }) => {
|
||||||
|
const chains = await lightClientPageHelper.getChains()
|
||||||
|
const chain = chains.find(({ genesisHash }) => genesisHash === chainId)
|
||||||
|
if (!chain) return undefined
|
||||||
|
|
||||||
const userSignedExtensionNames = await firstValueFrom(
|
return await keyring.getActiveAccount(chainId)
|
||||||
chainHead$.best$.pipe(
|
}
|
||||||
mergeMap((blockInfo) =>
|
|
||||||
chainHead$.getRuntimeContext$(blockInfo.hash).pipe(
|
const getAccountDerivations: RpcMethodHandlers<
|
||||||
take(1),
|
BackgroundRpcSpec,
|
||||||
map(({ lookup: { metadata } }) =>
|
Context
|
||||||
metadata.extrinsic.signedExtensions
|
>["getAccountDerivations"] = async ([metadata], { lightClientPageHelper }) => {
|
||||||
.map(({ identifier }) => identifier)
|
if (!metadata) return undefined
|
||||||
.filter(isUserSignedExtensionName)
|
const chains = await lightClientPageHelper.getChains()
|
||||||
),
|
const chain = chains.find(({ genesisHash }) => genesisHash === metadata.chainId)
|
||||||
|
if (!chain) return undefined
|
||||||
|
|
||||||
|
const derivations = await keyring.getAccountDerivations(metadata)
|
||||||
|
const prefix = wellKnownPrefixByGenesisHash[chain.genesisHash]
|
||||||
|
|
||||||
|
return {
|
||||||
|
...metadata,
|
||||||
|
address: ss58Address(metadata.publicKey, prefix),
|
||||||
|
derivations: derivations.map(derivation => ({
|
||||||
|
...derivation,
|
||||||
|
address: ss58Address(derivation.publicKey, prefix),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAccount: RpcMethodHandlers<
|
||||||
|
BackgroundRpcSpec,
|
||||||
|
Context
|
||||||
|
>["getAccount"] = async ([chainId], { lightClientPageHelper }) => {
|
||||||
|
const chains = await lightClientPageHelper.getChains()
|
||||||
|
const chain = chains.find(({ genesisHash }) => genesisHash === chainId)
|
||||||
|
if (!chain) return undefined
|
||||||
|
|
||||||
|
const activeAccount = await keyring.getActiveAccount(chainId)
|
||||||
|
if (!activeAccount || !activeAccount.publicKey) return undefined
|
||||||
|
|
||||||
|
const derivations = await keyring.getAccountDerivations({
|
||||||
|
// name and scheme not used for derivation extraction
|
||||||
|
publicKey: activeAccount.publicKey, chainId, name: "", scheme: "Sr25519"
|
||||||
|
})
|
||||||
|
const derivation = derivations.find(derivation => derivation.path == activeAccount.path)
|
||||||
|
const publicKey = derivation ? derivation.publicKey : activeAccount.publicKey
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey: publicKey,
|
||||||
|
address: ss58Address(
|
||||||
|
publicKey,
|
||||||
|
wellKnownPrefixByGenesisHash[chainId]
|
||||||
),
|
),
|
||||||
),
|
}
|
||||||
filter(Boolean)
|
}
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
const notifyOnAccountsChanged = async (context: Context) =>
|
||||||
const signRequest = new Promise<
|
context.notifyOnAccountsChanged(
|
||||||
Parameters<InternalSignRequest["resolve"]>[0]
|
(
|
||||||
>(
|
await Promise.all(
|
||||||
(resolve, reject) =>
|
(await context.lightClientPageHelper.getChains()).map(
|
||||||
(signRequests[id] = {
|
({ genesisHash }) => getAccounts([genesisHash], context)
|
||||||
resolve,
|
),
|
||||||
reject,
|
)
|
||||||
chainId,
|
).flatMap((accounts) => accounts),
|
||||||
url,
|
)
|
||||||
address: ss58Address(
|
|
||||||
from,
|
const handlers: RpcMethodHandlers<BackgroundRpcSpec, Context> = {
|
||||||
wellKnownPrefixByGenesisHash[chain.genesisHash]
|
getAccountDerivations,
|
||||||
),
|
getAccounts,
|
||||||
callData,
|
getAccount,
|
||||||
userSignedExtensions: {
|
getActiveAccount,
|
||||||
type: "names",
|
async createTx(
|
||||||
names: userSignedExtensionNames,
|
[chainId, from, callData],
|
||||||
},
|
{ lightClientPageHelper, signRequests, port },
|
||||||
|
) {
|
||||||
|
const url = port.sender?.url
|
||||||
|
if (!url) throw new Error("unknown url provided")
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const lastApprovedRequestTime = requestCooldowns.get(url) || 0
|
||||||
|
|
||||||
|
if (now - lastApprovedRequestTime < COOLDOWN_MS) {
|
||||||
|
throw new Error("wallet interaction is rate-limited")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
requestCooldowns.set(url, now)
|
||||||
|
|
||||||
|
const chains = await lightClientPageHelper.getChains()
|
||||||
|
const chain = chains.find(({ genesisHash }) => genesisHash === chainId)
|
||||||
|
if (!chain) throw new Error("unknown chain provided")
|
||||||
|
|
||||||
|
const id = nextSignRequestId++
|
||||||
|
|
||||||
|
const client = getObservableClient(createClient(chain.provider))
|
||||||
|
const chainHead$ = client.chainHead$()
|
||||||
|
|
||||||
|
const userSignedExtensionNames = await firstValueFrom(
|
||||||
|
chainHead$.best$.pipe(
|
||||||
|
mergeMap((blockInfo: { hash: string; [key: string]: any }) =>
|
||||||
|
chainHead$.getRuntimeContext$(blockInfo.hash).pipe(
|
||||||
|
take(1),
|
||||||
|
map(({ lookup: { metadata } }: { lookup: { metadata: any } }) =>
|
||||||
|
metadata.extrinsic.signedExtensions
|
||||||
|
.map(({ identifier }: { identifier: string }) => identifier)
|
||||||
|
.filter(isUserSignedExtensionName)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
filter(Boolean)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.finally(() => {
|
||||||
|
chainHead$.unfollow()
|
||||||
|
client.destroy()
|
||||||
})
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const window = await chrome.windows.create({
|
const currentRequestId = id
|
||||||
focused: true,
|
|
||||||
height: 640,
|
|
||||||
width: 530,
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
type: "popup",
|
|
||||||
url: chrome.runtime.getURL(
|
|
||||||
`ui/assets/wallet-popup.html#/sign-request/${id}`
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
const removeWindow = () => chrome.windows.remove(window.id!)
|
try {
|
||||||
port.onDisconnect.addListener(removeWindow)
|
const signRequest = new Promise<{
|
||||||
const onWindowsRemoved = (windowId: number) => {
|
userSignedExtensions: any;
|
||||||
if (windowId !== window.id) return
|
password?: string;
|
||||||
const signRequest = signRequests[id]
|
}>((resolve, reject) => {
|
||||||
if (!signRequest) return
|
signRequests[id] = {
|
||||||
signRequest.reject()
|
resolve,
|
||||||
|
reject,
|
||||||
|
chainId,
|
||||||
|
url,
|
||||||
|
address: ss58Address(from, wellKnownPrefixByGenesisHash[chain.genesisHash]),
|
||||||
|
callData,
|
||||||
|
userSignedExtensions: {
|
||||||
|
type: "names",
|
||||||
|
names: userSignedExtensionNames,
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
})
|
||||||
|
|
||||||
|
let windowId: number | undefined = undefined
|
||||||
|
|
||||||
|
const createdWindow = await chrome.windows.create({
|
||||||
|
focused: true,
|
||||||
|
height: 680,
|
||||||
|
width: 460,
|
||||||
|
type: "popup",
|
||||||
|
url: chrome.runtime.getURL(`ui/assets/wallet-popup.html#/sign-request/${id}`),
|
||||||
|
})
|
||||||
|
|
||||||
|
windowId = createdWindow.id
|
||||||
|
const targetWindowId = createdWindow.id
|
||||||
|
|
||||||
|
const removeWindow = () => {
|
||||||
|
if (windowId && windowId === targetWindowId) {
|
||||||
|
chrome.windows.remove(windowId).catch(() => {})
|
||||||
|
windowId = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onWindowsRemoved = (closedWindowId: number) => {
|
||||||
|
if (closedWindowId === windowId) {
|
||||||
|
if (signRequests[currentRequestId]) {
|
||||||
|
signRequests[currentRequestId].reject(new Error("user closed connection window"))
|
||||||
|
}
|
||||||
|
cleanUpWindowListeners(port)
|
||||||
|
windowId = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanUpWindowListeners = (port: any) => {
|
||||||
|
port.onDisconnect.removeListener(removeWindow)
|
||||||
|
chrome.windows.onRemoved.removeListener(onWindowsRemoved)
|
||||||
|
}
|
||||||
|
|
||||||
|
port.onDisconnect.addListener(removeWindow)
|
||||||
|
chrome.windows.onRemoved.addListener(onWindowsRemoved)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { userSignedExtensions, password } = await signRequest
|
||||||
|
cleanUpWindowListeners(port)
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
throw new Error("password verification is strictly required for this action")
|
||||||
|
}
|
||||||
|
|
||||||
|
let [keypair, scheme] = await keyring.getKeypairWithPassword(chainId, password)
|
||||||
|
let signer = getPolkadotSigner(keypair.publicKey, scheme, keypair.sign)
|
||||||
|
|
||||||
|
const mortality = userSignedExtensions.CheckMortality ?? { mortal: true, period: 64 }
|
||||||
|
const asset = userSignedExtensions.ChargeAssetTxPayment?.asset
|
||||||
|
const tip = (asset
|
||||||
|
? userSignedExtensions.ChargeAssetTxPayment?.tip
|
||||||
|
: userSignedExtensions.ChargeTransactionPayment
|
||||||
|
) ?? 0n
|
||||||
|
|
||||||
|
const tx = await createTx(chain.provider)({
|
||||||
|
signer,
|
||||||
|
callData: fromHex(callData),
|
||||||
|
hinted: { mortality, asset, tip },
|
||||||
|
})
|
||||||
|
|
||||||
|
;(keypair as any) = null;
|
||||||
|
;(signer as any) = null;
|
||||||
|
|
||||||
|
return toHex(tx)
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
cleanUpWindowListeners(port)
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
delete signRequests[id]
|
||||||
|
if (windowId) {
|
||||||
|
chrome.windows.remove(windowId).catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const msgError = error instanceof Error ? error.message : String(error)
|
||||||
|
throw new Error(`Transaction signing context failed: ${msgError}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async pjsSignPayload(
|
||||||
|
[payload],
|
||||||
|
{ port, lightClientPageHelper, signRequests }
|
||||||
|
) {
|
||||||
|
const url = port.sender?.url
|
||||||
|
if (!url) throw new Error("unknown url")
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const lastApprovedRequestTime = requestCooldowns.get(url) || 0
|
||||||
|
|
||||||
|
if (now - lastApprovedRequestTime < COOLDOWN_MS) {
|
||||||
|
throw new Error("wallet interaction is rate-limited. Please wait 3 seconds.")
|
||||||
|
}
|
||||||
|
|
||||||
|
requestCooldowns.set(url, now)
|
||||||
|
|
||||||
|
const chains = await lightClientPageHelper.getChains()
|
||||||
|
const chain = chains.find(
|
||||||
|
({ genesisHash }) => genesisHash === payload.genesisHash
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!chain) throw new Error("unknown chain")
|
||||||
|
|
||||||
|
const id = nextSignRequestId++
|
||||||
|
const currentRequestId = id
|
||||||
|
|
||||||
|
const signRequest = new Promise<
|
||||||
|
Parameters<InternalSignRequest["resolve"]>[0]
|
||||||
|
>((resolve, reject) =>
|
||||||
|
(signRequests[id] = {
|
||||||
|
resolve,
|
||||||
|
reject,
|
||||||
|
chainId: payload.genesisHash,
|
||||||
|
url,
|
||||||
|
address: payload.address,
|
||||||
|
callData: payload.method,
|
||||||
|
userSignedExtensions: {
|
||||||
|
type: "values",
|
||||||
|
values: pjs.getUserSignedExtensions(payload),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
let windowId: number | undefined = undefined
|
||||||
|
|
||||||
|
const createdWindow = await chrome.windows.create({
|
||||||
|
focused: true,
|
||||||
|
height: 680,
|
||||||
|
width: 460,
|
||||||
|
type: "popup",
|
||||||
|
url: chrome.runtime.getURL(`ui/assets/wallet-popup.html#/sign-request/${id}`),
|
||||||
|
})
|
||||||
|
|
||||||
|
windowId = createdWindow.id
|
||||||
|
const targetWindowId = createdWindow.id
|
||||||
|
|
||||||
|
const removeWindow = () => {
|
||||||
|
if (windowId && windowId === targetWindowId) {
|
||||||
|
chrome.windows.remove(windowId).catch(() => {})
|
||||||
|
windowId = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onWindowsRemoved = (closedWindowId: number) => {
|
||||||
|
if (closedWindowId === windowId) {
|
||||||
|
if (signRequests[currentRequestId]) {
|
||||||
|
signRequests[currentRequestId].reject(new Error("user closed connection window"))
|
||||||
|
}
|
||||||
|
cleanUpWindowListeners(port)
|
||||||
|
windowId = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanUpWindowListeners = (port: any) => {
|
||||||
|
port.onDisconnect.removeListener(removeWindow)
|
||||||
|
chrome.windows.onRemoved.removeListener(onWindowsRemoved)
|
||||||
|
}
|
||||||
|
|
||||||
|
port.onDisconnect.addListener(removeWindow)
|
||||||
|
chrome.windows.onRemoved.addListener(onWindowsRemoved)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { password } = await signRequest
|
||||||
|
cleanUpWindowListeners(port)
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
throw new Error("password verification is strictly required for this action")
|
||||||
|
}
|
||||||
|
|
||||||
|
const signaturePayload = await pjs.getSignaturePayload(chain.provider, payload)
|
||||||
|
const multiSignatureEncoder = Variant({
|
||||||
|
Ed25519: Bytes(64),
|
||||||
|
Sr25519: Bytes(64),
|
||||||
|
Ecdsa: Bytes(65),
|
||||||
|
}).enc
|
||||||
|
|
||||||
|
let [keypair, scheme] = await keyring.getKeypairWithPassword(payload.genesisHash, password)
|
||||||
|
|
||||||
|
const result = multiSignatureEncoder({
|
||||||
|
type: scheme,
|
||||||
|
value: keypair.sign(signaturePayload),
|
||||||
|
})
|
||||||
|
|
||||||
|
;(keypair as any) = null;
|
||||||
|
|
||||||
|
return toHex(result)
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
cleanUpWindowListeners(port)
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
delete signRequests[id]
|
||||||
|
if (windowId) {
|
||||||
|
chrome.windows.remove(windowId).catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getSignRequests(_, { signRequests }) {
|
||||||
|
return signRequests
|
||||||
|
},
|
||||||
|
async approveSignRequest([id, userSignedExtensions, password], { signRequests }) {
|
||||||
|
signRequests[id]?.resolve({
|
||||||
|
userSignedExtensions,
|
||||||
|
password
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async cancelSignRequest([id], { signRequests }) {
|
||||||
|
signRequests[id]?.reject()
|
||||||
|
},
|
||||||
|
async changePassword([args]) {
|
||||||
|
return keyring.changePassword(args)
|
||||||
|
},
|
||||||
|
async insertAccount([args], context) {
|
||||||
|
await keyring.insertAccount(args)
|
||||||
|
await notifyOnAccountsChanged(context)
|
||||||
|
},
|
||||||
|
async deriveAccount([args], context) {
|
||||||
|
await keyring.deriveAccount(args)
|
||||||
|
await notifyOnAccountsChanged(context)
|
||||||
|
},
|
||||||
|
async revealSecretSeed([args]) {
|
||||||
|
return await keyring.revealSecretSeed(args)
|
||||||
|
},
|
||||||
|
async prepareBackup([args]) {
|
||||||
|
return await keyring.prepareBackup(args)
|
||||||
|
},
|
||||||
|
async extendKeystore([args]) {
|
||||||
|
await keyring.extendKeystore(args)
|
||||||
|
},
|
||||||
|
async renameAccount([args]) {
|
||||||
|
await keyring.renameAccount(args)
|
||||||
|
},
|
||||||
|
async removeAccount([args]) {
|
||||||
|
await keyring.removeAccount(args)
|
||||||
|
},
|
||||||
|
async setActiveAccount([args], context) {
|
||||||
|
await keyring.setActiveAccount(args)
|
||||||
|
await notifyOnAccountsChanged(context)
|
||||||
|
},
|
||||||
|
getChainSpecs: listChainSpecsHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Method = keyof BackgroundRpcSpec
|
||||||
|
|
||||||
|
const ALLOWED_WEB_METHODS: Method[] = [
|
||||||
|
"createTx",
|
||||||
|
"getAccount",
|
||||||
|
"pjsSignPayload",
|
||||||
|
]
|
||||||
|
|
||||||
|
const allowedMethodsMiddleware: RpcMethodMiddleware<Context> = async (
|
||||||
|
next,
|
||||||
|
request,
|
||||||
|
context
|
||||||
|
) => {
|
||||||
|
const { port } = context
|
||||||
|
|
||||||
|
const isWebChannel = port.name === CHANNEL_ID
|
||||||
|
const isInternalSource = port.sender?.url?.startsWith(chrome.runtime.getURL(""))
|
||||||
|
|
||||||
|
if (isWebChannel) {
|
||||||
|
if (!ALLOWED_WEB_METHODS.includes(request.method as Method)) {
|
||||||
|
throw new RpcError("Method not found or access denied", -32601)
|
||||||
|
}
|
||||||
|
} else if (!isInternalSource) {
|
||||||
|
throw new RpcError("Unauthorized port origin access detected", -32601)
|
||||||
}
|
}
|
||||||
chrome.windows.onRemoved.addListener(onWindowsRemoved)
|
|
||||||
|
|
||||||
try {
|
return next(request, context)
|
||||||
const { userSignedExtensions } = await signRequest
|
}
|
||||||
const [keypair, scheme] = await keyring.getKeypair(chainId, from)
|
|
||||||
const signer = getPolkadotSigner(
|
|
||||||
keypair.publicKey,
|
|
||||||
scheme,
|
|
||||||
keypair.sign
|
|
||||||
)
|
|
||||||
|
|
||||||
const mortality = userSignedExtensions.CheckMortality ?? {
|
return createRpc(sendMessage, handlers, [allowedMethodsMiddleware]).withClient<InPageRpcSpec>()
|
||||||
mortal: true,
|
|
||||||
period: 64,
|
|
||||||
}
|
|
||||||
const decimals = wellKnownDecimalsByGenesisHash[chainId] ?? 0
|
|
||||||
const asset = userSignedExtensions.ChargeAssetTxPayment?.asset
|
|
||||||
const tip =
|
|
||||||
(asset
|
|
||||||
? userSignedExtensions.ChargeAssetTxPayment?.tip
|
|
||||||
: userSignedExtensions.ChargeTransactionPayment) ?? 0n
|
|
||||||
|
|
||||||
const tipWithDecimals = BigInt(tip * Math.pow(10, decimals))
|
|
||||||
|
|
||||||
const tx = await createTx(chain.provider)({
|
|
||||||
signer,
|
|
||||||
callData: fromHex(callData),
|
|
||||||
hinted: {
|
|
||||||
mortality,
|
|
||||||
asset,
|
|
||||||
tipWithDecimals,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return toHex(tx)
|
|
||||||
} finally {
|
|
||||||
delete signRequests[id]
|
|
||||||
chrome.windows.remove(window.id!)
|
|
||||||
port.onDisconnect.removeListener(removeWindow)
|
|
||||||
chrome.windows.onRemoved.removeListener(onWindowsRemoved)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
chainHead$.unfollow()
|
|
||||||
client.destroy()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async pjsSignPayload(
|
|
||||||
[payload],
|
|
||||||
{ port, lightClientPageHelper, signRequests }
|
|
||||||
) {
|
|
||||||
const url = port.sender?.url
|
|
||||||
if (!url) throw new Error("unknown url")
|
|
||||||
const chains = await lightClientPageHelper.getChains()
|
|
||||||
const chain = chains.find(
|
|
||||||
({ genesisHash }) => genesisHash === payload.genesisHash
|
|
||||||
)
|
|
||||||
if (!chain) throw new Error("unknown chain")
|
|
||||||
const id = nextSignRequestId++
|
|
||||||
const signRequest = new Promise<
|
|
||||||
Parameters<InternalSignRequest["resolve"]>[0]
|
|
||||||
>(
|
|
||||||
(resolve, reject) =>
|
|
||||||
(signRequests[id] = {
|
|
||||||
resolve,
|
|
||||||
reject,
|
|
||||||
chainId: payload.genesisHash,
|
|
||||||
url,
|
|
||||||
address: payload.address,
|
|
||||||
callData: payload.method,
|
|
||||||
userSignedExtensions: {
|
|
||||||
type: "values",
|
|
||||||
values: pjs.getUserSignedExtensions(payload),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const window = await chrome.windows.create({
|
|
||||||
focused: true,
|
|
||||||
height: 640,
|
|
||||||
width: 530,
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
type: "popup",
|
|
||||||
url: chrome.runtime.getURL(
|
|
||||||
`ui/assets/wallet-popup.html#/sign-request/${id}`
|
|
||||||
),
|
|
||||||
})
|
|
||||||
const removeWindow = () => chrome.windows.remove(window.id!)
|
|
||||||
port.onDisconnect.addListener(removeWindow)
|
|
||||||
const onWindowsRemoved = (windowId: number) => {
|
|
||||||
if (windowId !== window.id) return
|
|
||||||
const signRequest = signRequests[id]
|
|
||||||
if (!signRequest) return
|
|
||||||
signRequest.reject()
|
|
||||||
}
|
|
||||||
chrome.windows.onRemoved.addListener(onWindowsRemoved)
|
|
||||||
try {
|
|
||||||
await signRequest
|
|
||||||
} finally {
|
|
||||||
delete signRequests[id]
|
|
||||||
chrome.windows.remove(window.id!)
|
|
||||||
port.onDisconnect.removeListener(removeWindow)
|
|
||||||
chrome.windows.onRemoved.removeListener(onWindowsRemoved)
|
|
||||||
}
|
|
||||||
const signaturePayload = await pjs.getSignaturePayload(
|
|
||||||
chain.provider,
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
const multiSignatureEncoder = Variant({
|
|
||||||
Ed25519: Bytes(64),
|
|
||||||
Sr25519: Bytes(64),
|
|
||||||
Ecdsa: Bytes(65),
|
|
||||||
}).enc
|
|
||||||
const [keypair, scheme] = await keyring.getKeypair(
|
|
||||||
payload.genesisHash,
|
|
||||||
toHex(ss58Decode(payload.address)[0])
|
|
||||||
)
|
|
||||||
return toHex(
|
|
||||||
multiSignatureEncoder({
|
|
||||||
type: scheme,
|
|
||||||
value: keypair.sign(signaturePayload),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
},
|
|
||||||
async getSignRequests(_, { signRequests }) {
|
|
||||||
return signRequests
|
|
||||||
},
|
|
||||||
async approveSignRequest([id, userSignedExtensions], { signRequests }) {
|
|
||||||
signRequests[id]?.resolve({
|
|
||||||
userSignedExtensions,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async cancelSignRequest([id], { signRequests }) {
|
|
||||||
signRequests[id]?.reject()
|
|
||||||
},
|
|
||||||
async lockKeyring() {
|
|
||||||
return keyring.lock()
|
|
||||||
},
|
|
||||||
async resetKeyring([password]) {
|
|
||||||
return keyring.reset(password)
|
|
||||||
},
|
|
||||||
async unlockKeyring([password]) {
|
|
||||||
return keyring.unlock(password)
|
|
||||||
},
|
|
||||||
async changePassword([currentPassword, newPassword]) {
|
|
||||||
return keyring.changePassword(currentPassword, newPassword)
|
|
||||||
},
|
|
||||||
async createPassword([password]) {
|
|
||||||
return keyring.setup(password)
|
|
||||||
},
|
|
||||||
async insertCryptoKey([args], context) {
|
|
||||||
const existingKey = await keyring.getCryptoKey(args.name)
|
|
||||||
|
|
||||||
if (existingKey)
|
|
||||||
throw new Error(`crypto key "${args.name}" already exists`)
|
|
||||||
await keyring.insertCryptoKey(args)
|
|
||||||
notifyOnAccountsChanged(context)
|
|
||||||
},
|
|
||||||
async updateCryptoKey([args], context) {
|
|
||||||
const existingKey = await keyring.getCryptoKey(args.name)
|
|
||||||
|
|
||||||
if (!existingKey)
|
|
||||||
throw new Error(`crypto key "${args.name}" does not exist`)
|
|
||||||
await keyring.updateCryptoKey(args)
|
|
||||||
notifyOnAccountsChanged(context)
|
|
||||||
},
|
|
||||||
async getCryptoKey([name]) {
|
|
||||||
return keyring.getCryptoKey(name)
|
|
||||||
},
|
|
||||||
async getCryptoKeys() {
|
|
||||||
return keyring.getCryptoKeys()
|
|
||||||
},
|
|
||||||
async revealCryptoKey([name, index]) {
|
|
||||||
return keyring.revealCryptoKey(name, index)
|
|
||||||
},
|
|
||||||
async clearCryptoKeys([name], context) {
|
|
||||||
await keyring.clearCryptoKeys(name)
|
|
||||||
notifyOnAccountsChanged(context)
|
|
||||||
},
|
|
||||||
async clearCryptoKey([name, index], context) {
|
|
||||||
await keyring.clearCryptoKey(name, index)
|
|
||||||
notifyOnAccountsChanged(context)
|
|
||||||
},
|
|
||||||
async getKeyringState() {
|
|
||||||
return {
|
|
||||||
isLocked: await keyring.isLocked(),
|
|
||||||
hasPassword: await keyring.hasPassword(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getChainSpecs: listChainSpecsHandler,
|
|
||||||
addChainSpec: addChainSpecHandler,
|
|
||||||
removeChainSpec: removeChainSpecHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Method = keyof BackgroundRpcSpec
|
|
||||||
const ALLOWED_WEB_METHODS: Method[] = [
|
|
||||||
"createTx",
|
|
||||||
"getAccounts",
|
|
||||||
"pjsSignPayload",
|
|
||||||
]
|
|
||||||
const allowedMethodsMiddleware: RpcMethodMiddleware<Context> = async (
|
|
||||||
next,
|
|
||||||
request,
|
|
||||||
context
|
|
||||||
) => {
|
|
||||||
const { port } = context
|
|
||||||
if (
|
|
||||||
port.name === CHANNEL_ID &&
|
|
||||||
!ALLOWED_WEB_METHODS.includes(request.method as Method)
|
|
||||||
)
|
|
||||||
throw new RpcError("Method not found", -32601)
|
|
||||||
return next(request, context)
|
|
||||||
}
|
|
||||||
return createRpc(sendMessage, handlers, [
|
|
||||||
allowedMethodsMiddleware,
|
|
||||||
]).withClient<InPageRpcSpec>()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import type { Account } from "./types"
|
|||||||
import { startHeartbeat } from "./heartbeat"
|
import { startHeartbeat } from "./heartbeat"
|
||||||
import { CHANNEL_ID } from "../constants"
|
import { CHANNEL_ID } from "../constants"
|
||||||
|
|
||||||
const { lightClientPageHelper, addOnAddChainByUserListener } = register({
|
const { lightClientPageHelper } = register({
|
||||||
smoldotClient: start({ maxLogLevel: 4 }),
|
smoldotClient: start({ maxLogLevel: 4 }),
|
||||||
getWellKnownChainSpecs: () =>
|
getWellKnownChainSpecs: () =>
|
||||||
// Note that this list doesn't necessarily always have to match the list of well-known
|
// Note that this list doesn't necessarily always have to match the list of well-known
|
||||||
@ -32,17 +32,23 @@ const signRequests = {}
|
|||||||
type BackgroundRpc = ReturnType<typeof createBackgroundRpc>
|
type BackgroundRpc = ReturnType<typeof createBackgroundRpc>
|
||||||
const connectedRpcs: BackgroundRpc[] = []
|
const connectedRpcs: BackgroundRpc[] = []
|
||||||
const notifyOnAccountsChanged = (accounts: Account[]) =>
|
const notifyOnAccountsChanged = (accounts: Account[]) =>
|
||||||
connectedRpcs.forEach((rpc) => rpc.notify("onAccountsChanged", [accounts]))
|
connectedRpcs.forEach((rpc) => rpc.notify("onAccountsChanged", [accounts]))
|
||||||
|
|
||||||
const subscribeOnAccountsChanged = (rpc: BackgroundRpc) => {
|
const subscribeOnAccountsChanged = (rpc: BackgroundRpc) => {
|
||||||
connectedRpcs.push(rpc)
|
connectedRpcs.push(rpc)
|
||||||
return () => {
|
return () => {
|
||||||
connectedRpcs.splice(connectedRpcs.indexOf(rpc), 1)
|
const index = connectedRpcs.indexOf(rpc)
|
||||||
|
if (index !== -1) {
|
||||||
|
connectedRpcs.splice(index, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.runtime.onConnect.addListener((port) => {
|
chrome.runtime.onConnect.addListener((port) => {
|
||||||
if (!port.name.startsWith(CHANNEL_ID)) return
|
if (!port.name.startsWith(CHANNEL_ID)) return
|
||||||
|
|
||||||
const rpc = createBackgroundRpc((msg) => port.postMessage(msg))
|
const rpc = createBackgroundRpc((msg) => port.postMessage(msg))
|
||||||
|
|
||||||
port.onMessage.addListener((msg) =>
|
port.onMessage.addListener((msg) =>
|
||||||
rpc.handle(msg, {
|
rpc.handle(msg, {
|
||||||
lightClientPageHelper,
|
lightClientPageHelper,
|
||||||
@ -73,46 +79,4 @@ chrome.runtime.onInstalled.addListener(async ({ reason }) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
addOnAddChainByUserListener(async (inputChain) => {
|
|
||||||
const isRelayChain = !inputChain.relayChainGenesisHash
|
|
||||||
const existingChain = await lightClientPageHelper.getChain(
|
|
||||||
inputChain.genesisHash,
|
|
||||||
)
|
|
||||||
if (isRelayChain && !existingChain) {
|
|
||||||
await waitForAddChainApproval(inputChain)
|
|
||||||
|
|
||||||
const persistedChain = await lightClientPageHelper.getChain(
|
|
||||||
inputChain.genesisHash,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!persistedChain) {
|
|
||||||
throw new Error("User rejected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const waitForAddChainApproval = async (inputChain: InputChain) => {
|
|
||||||
const window = await chrome.windows.create({
|
|
||||||
focused: true,
|
|
||||||
width: 400,
|
|
||||||
height: 600,
|
|
||||||
left: 150,
|
|
||||||
top: 150,
|
|
||||||
type: "popup",
|
|
||||||
url: chrome.runtime.getURL(
|
|
||||||
`ui/assets/wallet-popup.html#/add-chain-by-user?params=${encodeURIComponent(JSON.stringify(inputChain))}`,
|
|
||||||
),
|
|
||||||
})
|
|
||||||
const onWindowsRemoved = (windowId: number) => {
|
|
||||||
if (windowId !== window.id) return
|
|
||||||
resolveWindowClosed()
|
|
||||||
}
|
|
||||||
chrome.windows.onRemoved.addListener(onWindowsRemoved)
|
|
||||||
|
|
||||||
const { promise: windowClosedPromise, resolve: resolveWindowClosed } =
|
|
||||||
Promise.withResolvers<void>()
|
|
||||||
|
|
||||||
await windowClosedPromise
|
|
||||||
}
|
|
||||||
|
|
||||||
startHeartbeat()
|
startHeartbeat()
|
||||||
|
|||||||
@ -11,16 +11,29 @@ import {
|
|||||||
KeyPair,
|
KeyPair,
|
||||||
Curve,
|
Curve,
|
||||||
} from "@polkadot-labs/hdkd-helpers"
|
} from "@polkadot-labs/hdkd-helpers"
|
||||||
import { KeystoreMeta, keystoreV4, type KeystoreV4WithMeta } from "./keystore"
|
import {
|
||||||
|
KeyringStorage,
|
||||||
|
KeystoreMeta,
|
||||||
|
FullKeystoreMeta,
|
||||||
|
Derivation,
|
||||||
|
ActiveAccount,
|
||||||
|
KeystoreV4WithMeta,
|
||||||
|
FullKeystoreMetaWithVault,
|
||||||
|
keystoreV4,
|
||||||
|
} from "./keystore"
|
||||||
|
import {
|
||||||
|
InsertAccountArgs,
|
||||||
|
RemoveAccountArgs,
|
||||||
|
RenameAccountArgs,
|
||||||
|
ChangePasswordArgs,
|
||||||
|
RevealSecretSeedArgs,
|
||||||
|
ExtendKeystoreArgs,
|
||||||
|
DeriveAccountArgs,
|
||||||
|
ActiveAccountArgs,
|
||||||
|
} from "./types"
|
||||||
|
|
||||||
import { assert } from "./utils"
|
import { assert } from "./utils"
|
||||||
import * as storage from "./storage"
|
import * as storage from "./storage"
|
||||||
import {
|
|
||||||
InsertCryptoKeyArgs,
|
|
||||||
UpdateCryptoKeyArgs,
|
|
||||||
RemoveCryptoKeyArgs,
|
|
||||||
KeystoreAccount,
|
|
||||||
RevealCryptoKeyArgs,
|
|
||||||
} from "./types"
|
|
||||||
import { toHex, fromHex } from "@polkadot-api/utils"
|
import { toHex, fromHex } from "@polkadot-api/utils"
|
||||||
import { wellKnownChainIdByGenesisHash } from "../constants"
|
import { wellKnownChainIdByGenesisHash } from "../constants"
|
||||||
|
|
||||||
@ -36,321 +49,378 @@ const curveFnMap: Record<string, Curve> = {
|
|||||||
Ecdsa: ecdsa,
|
Ecdsa: ecdsa,
|
||||||
}
|
}
|
||||||
|
|
||||||
const createKeyPair = (privateKey: string, scheme: string): KeyPair => {
|
const createKeyPair = (miniSecret: string | Uint8Array, scheme: string, path: string): KeyPair => {
|
||||||
const curve = curveFnMap[scheme]
|
const miniSecretBytes = typeof miniSecret === "string" ? fromHex(miniSecret) : miniSecret
|
||||||
if (!curve) throw new Error("unsupported signature scheme")
|
const createDeriveFn = createDeriveFnMap[scheme]
|
||||||
|
const derive = createDeriveFn(miniSecretBytes)
|
||||||
|
const keyPair = derive(path)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
publicKey: curve.getPublicKey(privateKey),
|
publicKey: keyPair.publicKey,
|
||||||
sign(message) {
|
sign(message) {
|
||||||
return curve.sign(message, privateKey)
|
return keyPair.sign(message)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ACCOUNT_DERIVATION_PREFIX = "derivation_storage"
|
||||||
|
const KEYRING_STORAGE_PREFIX = "keyring_storage"
|
||||||
|
const ACTIVE_ACCOUNT_PREFIX = "active_account"
|
||||||
|
|
||||||
export const createKeyring = () => {
|
export const createKeyring = () => {
|
||||||
const getKeystore = () => storage.get("keystore")
|
const getKeystoreStorage = async (): Promise<KeyringStorage> => {
|
||||||
const setKeystore = (keystore: KeystoreV4WithMeta) =>
|
let keyring = await storage.get(KEYRING_STORAGE_PREFIX)
|
||||||
storage.set("keystore", keystore)
|
if (!keyring || !keyring.keystores) {
|
||||||
const removeKeystore = () => storage.remove("keystore")
|
keyring = { keystores: [] }
|
||||||
|
await setKeystoreStorage(keyring)
|
||||||
const getKeystoreAccounts = (
|
|
||||||
keystoreMeta: KeystoreMeta,
|
|
||||||
): KeystoreAccount[] => {
|
|
||||||
switch (keystoreMeta.type) {
|
|
||||||
case "KeysetKeystore":
|
|
||||||
return keystoreMeta.derivationPaths.map((d) => ({
|
|
||||||
...d,
|
|
||||||
type: "Keyset",
|
|
||||||
}))
|
|
||||||
case "KeypairKeystore":
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
type: "Keypair",
|
|
||||||
publicKey: keystoreMeta.publicKey,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
return keyring
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCryptoKeys = async () => {
|
const setKeystoreStorage = async (keyringData: KeyringStorage): Promise<void> => {
|
||||||
const keys = (await getKeystore())?.meta ?? []
|
await storage.set(KEYRING_STORAGE_PREFIX, keyringData)
|
||||||
|
|
||||||
return keys.map((meta) => ({
|
|
||||||
name: meta.name,
|
|
||||||
scheme: meta.scheme,
|
|
||||||
accounts: getKeystoreAccounts(meta),
|
|
||||||
createdAt: meta.createdAt,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const decodeSecrets = (secrets: Uint8Array) =>
|
const getAccountDerivations = async (args: KeystoreMeta): Promise<Derivation[]> => {
|
||||||
JSON.parse(new TextDecoder().decode(secrets)) as string[]
|
const derivations = await storage.get(`${ACCOUNT_DERIVATION_PREFIX}_${args.chainId}_${args.publicKey}`)
|
||||||
const encodeSecrets = (secrets: string[]) =>
|
return derivations ?? []
|
||||||
new TextEncoder().encode(JSON.stringify(secrets))
|
|
||||||
|
|
||||||
const getAccounts = async (chainId: string) => {
|
|
||||||
const keystore = await getKeystore()
|
|
||||||
if (!keystore) return []
|
|
||||||
|
|
||||||
return keystore.meta
|
|
||||||
.flatMap(getKeystoreAccounts)
|
|
||||||
.filter(
|
|
||||||
(account) =>
|
|
||||||
(account.type === "Keyset" && account.chainId === chainId) ||
|
|
||||||
account.type !== "Keyset",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertCryptoKey = async (args: InsertCryptoKeyArgs) => {
|
const setAccountDerivations = async (chainId: string, publicKey: string, derivations: Derivation[]): Promise<void> => {
|
||||||
const keystore = await getKeystore()
|
await storage.set(`${ACCOUNT_DERIVATION_PREFIX}_${chainId}_${publicKey}`, derivations)
|
||||||
assert(keystore, "keyring must be setup")
|
}
|
||||||
assert(currentPassword, "keyring must be unlocked")
|
|
||||||
|
const getActiveAccount = async (chainId: string): Promise<ActiveAccount | undefined> => {
|
||||||
|
const activeAccount = await storage.get(`${ACTIVE_ACCOUNT_PREFIX}_${chainId}`)
|
||||||
|
return activeAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
const setActiveAccount = async (args: ActiveAccountArgs): Promise<void> => {
|
||||||
|
await storage.set(`${ACTIVE_ACCOUNT_PREFIX}_${args.chainId}`, args.activeAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAccounts = async (chainId: string): Promise<KeystoreMeta[]> => {
|
||||||
|
const keystoreStorage = await getKeystoreStorage()
|
||||||
|
|
||||||
|
const accounts = keystoreStorage.keystores
|
||||||
|
.map((keystore: KeystoreV4WithMeta) => keystore.metadata)
|
||||||
|
.filter((account: KeystoreMeta) => account.chainId === chainId)
|
||||||
|
|
||||||
|
return accounts
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodeSecret = (secret: Uint8Array) => JSON.parse(new TextDecoder().decode(secret)) as string
|
||||||
|
const encodeSecret = (secret: string) => new TextEncoder().encode(JSON.stringify(secret))
|
||||||
|
|
||||||
|
const insertAccount = async (args: InsertAccountArgs): Promise<void> => {
|
||||||
|
assert(args.password, "password is required to modify the keystore")
|
||||||
assert(
|
assert(
|
||||||
["Sr25519", "Ed25519", "Ecdsa"].includes(args.scheme),
|
["Sr25519", "Ed25519", "Ecdsa"].includes(args.scheme),
|
||||||
"invalid signature scheme",
|
"invalid signature scheme",
|
||||||
)
|
)
|
||||||
|
|
||||||
const secrets = decodeSecrets(keystoreV4.decrypt(keystore, currentPassword))
|
const keystoreStorage = await getKeystoreStorage()
|
||||||
const secret = args.type === "Keyset" ? args.miniSecret : args.privatekey
|
const keystore = keystoreStorage.keystores.find(
|
||||||
const newKeystore = keystoreV4.create(
|
(keystore: KeystoreV4WithMeta) => keystore.metadata.name === args.name
|
||||||
currentPassword,
|
|
||||||
encodeSecrets([...secrets, secret]),
|
|
||||||
)
|
)
|
||||||
|
assert(!keystore, "account with this name already exists")
|
||||||
|
|
||||||
const newCryptoKey =
|
const publicKeyHex = toHex(createKeyPair(args.miniSecret, args.scheme, "").publicKey)
|
||||||
args.type === "Keyset"
|
|
||||||
? {
|
|
||||||
type: "KeysetKeystore" as const,
|
|
||||||
derivationPaths: args.derivationPaths,
|
|
||||||
}
|
|
||||||
: args.type === "Keypair"
|
|
||||||
? {
|
|
||||||
type: "KeypairKeystore" as const,
|
|
||||||
publicKey: toHex(
|
|
||||||
createKeyPair(args.privatekey, args.scheme).publicKey,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
if (!newCryptoKey) throw new Error("invalid keystore type")
|
|
||||||
|
|
||||||
setKeystore({
|
await setActiveAccount({
|
||||||
...newKeystore,
|
chainId: args.chainId,
|
||||||
meta: [
|
activeAccount: { publicKey: publicKeyHex }
|
||||||
...keystore.meta,
|
})
|
||||||
{
|
|
||||||
name: args.name,
|
const newVault = keystoreV4.create(args.password, encodeSecret(args.miniSecret))
|
||||||
scheme: args.scheme,
|
const newKeystoreItem: KeystoreV4WithMeta = {
|
||||||
createdAt: args.createdAt,
|
metadata: {
|
||||||
...newCryptoKey,
|
name: args.name,
|
||||||
},
|
scheme: args.scheme,
|
||||||
],
|
chainId: args.chainId,
|
||||||
|
publicKey: publicKeyHex,
|
||||||
|
},
|
||||||
|
vault: newVault,
|
||||||
|
}
|
||||||
|
|
||||||
|
await setKeystoreStorage({
|
||||||
|
keystores: [...(keystoreStorage.keystores ?? []), newKeystoreItem],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCryptoKey = async (args: UpdateCryptoKeyArgs) => {
|
const deriveAccount = async (args: DeriveAccountArgs): Promise<void> => {
|
||||||
const keystore = await getKeystore()
|
assert(args.password, "password is required to modify the keystore")
|
||||||
assert(keystore, "keyring must be setup")
|
|
||||||
assert(currentPassword, "keyring must be unlocked")
|
|
||||||
const cryptoKeyIndex = keystore.meta.findIndex((k) => k.name === args.name)
|
|
||||||
assert(cryptoKeyIndex != -1, "keyring name not found")
|
|
||||||
|
|
||||||
const uniqueNetworks = new Map()
|
const keystoreStorage = await getKeystoreStorage()
|
||||||
keystore.meta[cryptoKeyIndex].derivationPaths.forEach((obj: any) =>
|
const keystore = keystoreStorage.keystores.find(
|
||||||
uniqueNetworks.set(wellKnownChainIdByGenesisHash[obj.chainId], {
|
(keystore: KeystoreV4WithMeta) => keystore.metadata.publicKey === args.publicKey
|
||||||
network: wellKnownChainIdByGenesisHash[obj.chainId],
|
)
|
||||||
chainId: obj.chainId,
|
assert(keystore, "keystore with this public key does not exist")
|
||||||
})
|
|
||||||
|
const derivations = await getAccountDerivations(keystore.metadata)
|
||||||
|
const derivationIndex = derivations.findIndex(
|
||||||
|
(d) => d.name === args.name || d.path === args.path
|
||||||
|
)
|
||||||
|
assert(derivationIndex === -1, "derivation path or name already occupied")
|
||||||
|
|
||||||
|
assert(
|
||||||
|
keystoreV4.verifyPassword(keystore.vault, args.password),
|
||||||
|
"invalid password for keystore provided"
|
||||||
)
|
)
|
||||||
|
|
||||||
const secrets = decodeSecrets(keystoreV4.decrypt(keystore, currentPassword))
|
const createDeriveFn = createDeriveFnMap[keystore.metadata.scheme]
|
||||||
const derive = createDeriveFnMap[keystore.meta[cryptoKeyIndex].scheme](secrets[cryptoKeyIndex])
|
const decryptedBytes = keystoreV4.decrypt(keystore.vault, args.password)
|
||||||
const newDerivationPaths = Array.from(uniqueNetworks.values())
|
|
||||||
.map(({ network, chainId }) => {
|
try {
|
||||||
return {
|
const secret = decodeSecret(decryptedBytes)
|
||||||
chainId,
|
const derive = createDeriveFn(secret)
|
||||||
path: `//${network}/${args.deviation}`,
|
|
||||||
publicKey: toHex(derive(`//${network}/${args.deviation}`).publicKey),
|
const derivedKeyPair = derive(args.path)
|
||||||
}
|
const publicKeyHex = toHex(derivedKeyPair.publicKey)
|
||||||
|
|
||||||
|
const updatedDerivations = [
|
||||||
|
...derivations,
|
||||||
|
{ name: args.name, path: args.path, publicKey: publicKeyHex }
|
||||||
|
]
|
||||||
|
|
||||||
|
await setAccountDerivations(keystore.metadata.chainId, keystore.metadata.publicKey, updatedDerivations)
|
||||||
|
await setActiveAccount({
|
||||||
|
chainId: keystore.metadata.chainId,
|
||||||
|
activeAccount: { publicKey: args.publicKey, path: args.path }
|
||||||
})
|
})
|
||||||
|
|
||||||
keystore.meta[cryptoKeyIndex].derivationPaths.push(...newDerivationPaths)
|
} finally {
|
||||||
setKeystore(keystore)
|
decryptedBytes.fill(0)
|
||||||
}
|
|
||||||
|
|
||||||
const clearCryptoKey = async (name: string, index: number) => {
|
|
||||||
const keystore = await getKeystore()
|
|
||||||
assert(keystore, "keyring must be setup")
|
|
||||||
assert(currentPassword, "keyring must be unlocked")
|
|
||||||
const cryptoKeyIndex = keystore.meta.findIndex(obj => obj.name === name)
|
|
||||||
assert(cryptoKeyIndex != -1, "keyring name not found")
|
|
||||||
const secrets = decodeSecrets(keystoreV4.decrypt(keystore, currentPassword))
|
|
||||||
switch (keystore.meta.at(cryptoKeyIndex)?.type) {
|
|
||||||
case "KeysetKeystore":
|
|
||||||
keystore.meta.at(cryptoKeyIndex)?.derivationPaths.splice(index, 1)
|
|
||||||
setKeystore(keystore)
|
|
||||||
break
|
|
||||||
case "KeypairKeystore":
|
|
||||||
secrets.splice(cryptoKeyIndex, 1)
|
|
||||||
keystore.meta.splice(cryptoKeyIndex, 1)
|
|
||||||
const newKeystore = keystoreV4.create(
|
|
||||||
currentPassword,
|
|
||||||
encodeSecrets(secrets),
|
|
||||||
)
|
|
||||||
setKeystore({
|
|
||||||
...newKeystore,
|
|
||||||
meta: keystore.meta,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
throw new Error("invalid keystore type")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearCryptoKeys = async (args: RemoveCryptoKeyArgs) => {
|
const renameAccount = async (args: RenameAccountArgs) => {
|
||||||
const keystore = await getKeystore()
|
assert(args.newName?.trim(), "new name cannot be empty")
|
||||||
assert(keystore, "keyring must be setup")
|
|
||||||
assert(currentPassword, "keyring must be unlocked")
|
const keystoreStorage = await getKeystoreStorage()
|
||||||
const cryptoKeyIndex = keystore.meta.findIndex(obj => obj.name === args.name)
|
const keystore = keystoreStorage.keystores.find(
|
||||||
assert(cryptoKeyIndex != -1, "keyring name not found")
|
(keystore: KeystoreV4WithMeta) => keystore.metadata.publicKey === args.publicKey
|
||||||
const secrets = decodeSecrets(keystoreV4.decrypt(keystore, currentPassword))
|
|
||||||
secrets.splice(cryptoKeyIndex, 1)
|
|
||||||
const newKeystore = keystoreV4.create(
|
|
||||||
currentPassword,
|
|
||||||
encodeSecrets(secrets),
|
|
||||||
)
|
)
|
||||||
keystore.meta.splice(cryptoKeyIndex, 1)
|
|
||||||
setKeystore({
|
assert(keystore, "keystore with this public key does not exist")
|
||||||
...newKeystore,
|
if (args.path) {
|
||||||
meta: keystore.meta,
|
const metadata = keystore.metadata
|
||||||
})
|
const derivations = await getAccountDerivations(metadata)
|
||||||
|
const derivation = derivations.find(d => d.path === args.path)
|
||||||
|
|
||||||
|
assert(derivation, "derivation not found by derivation path")
|
||||||
|
derivation.name = args.newName
|
||||||
|
await setAccountDerivations(metadata.chainId, metadata.publicKey, derivations)
|
||||||
|
} else {
|
||||||
|
keystore.metadata.name = args.newName
|
||||||
|
await setKeystoreStorage({ keystores: keystoreStorage.keystores })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const revealCryptoKey = async (name: string, index: number) => {
|
const removeAccount = async (args: RemoveAccountArgs) => {
|
||||||
const keystore = await getKeystore()
|
assert(args.password, "password is required for this operation")
|
||||||
assert(keystore, "keyring must be setup")
|
|
||||||
assert(currentPassword, "keyring must be unlocked")
|
const keystoreStorage = await getKeystoreStorage()
|
||||||
const cryptoKeyIndex = keystore.meta.findIndex((k) => k.name === name)
|
const keystore = keystoreStorage.keystores.find(
|
||||||
assert(cryptoKeyIndex != -1, "keyring name not found")
|
(keystore: KeystoreV4WithMeta) => keystore.metadata.publicKey === args.publicKey
|
||||||
const secret = decodeSecrets(
|
)
|
||||||
keystoreV4.decrypt(keystore, currentPassword)
|
assert(keystore, "specific crypto vault not found based on account name")
|
||||||
).at(cryptoKeyIndex)
|
assert(keystoreV4.verifyPassword(keystore.vault, args.password), "invalid password for keystore provided")
|
||||||
assert(secret, "secret key should exists")
|
|
||||||
// TODO: need to get secret from secret and derivation path
|
if (args.path) {
|
||||||
return secret
|
const metadata = keystore.metadata
|
||||||
|
const derivations = await getAccountDerivations(metadata)
|
||||||
|
const updatedDerivations = derivations.filter(d => d.path !== args.path)
|
||||||
|
|
||||||
|
await setAccountDerivations(metadata.chainId, metadata.publicKey, updatedDerivations)
|
||||||
|
await setActiveAccount({
|
||||||
|
chainId: metadata.chainId,
|
||||||
|
activeAccount: { publicKey: metadata.publicKey }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const updatedKeystores = keystoreStorage.keystores.filter(
|
||||||
|
(keystore: KeystoreV4WithMeta) => keystore.metadata.publicKey !== args.publicKey
|
||||||
|
)
|
||||||
|
const nextActiveAccount: ActiveAccount = { publicKey: updatedKeystores.at(0)?.metadata.publicKey }
|
||||||
|
|
||||||
|
await setKeystoreStorage({ keystores: updatedKeystores })
|
||||||
|
await setActiveAccount({
|
||||||
|
chainId: keystore.metadata.chainId,
|
||||||
|
activeAccount: nextActiveAccount
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentPassword: string | undefined
|
const prepareBackup = async (publicKeys: string[]): Promise<FullKeystoreMetaWithVault[]> => {
|
||||||
|
const keystoreStorage = await getKeystoreStorage()
|
||||||
|
return Promise.all(
|
||||||
|
keystoreStorage.keystores
|
||||||
|
.filter((keystore: KeystoreV4WithMeta) => publicKeys.includes(keystore.metadata.publicKey))
|
||||||
|
.map(async (keystore: KeystoreV4WithMeta) => {
|
||||||
|
const derivations = await getAccountDerivations(keystore.metadata)
|
||||||
|
return { ...keystore, derivations }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
const extendKeystore = async (args: ExtendKeystoreArgs): Promise<void> => {
|
||||||
async unlock(password: string) {
|
const { includeDerivations, overwriteExisting, keystores } = args
|
||||||
const keystore = await getKeystore()
|
const currentStorage = await getKeystoreStorage()
|
||||||
assert(keystore, "keyring must be setup")
|
|
||||||
if (!keystoreV4.verifyPassword(keystore, password))
|
|
||||||
throw new Error("invalid password")
|
|
||||||
currentPassword = password
|
|
||||||
},
|
|
||||||
async lock() {
|
|
||||||
assert(await getKeystore(), "keyring must be setup")
|
|
||||||
assert(currentPassword, "keyring must be unlocked")
|
|
||||||
currentPassword = undefined
|
|
||||||
},
|
|
||||||
async isLocked() {
|
|
||||||
return !currentPassword || !(await getKeystore())
|
|
||||||
},
|
|
||||||
async reset(password: string) {
|
|
||||||
const keystore = await getKeystore()
|
|
||||||
assert(keystore, "keyring must be setup")
|
|
||||||
assert(currentPassword, "keyring must be unlocked")
|
|
||||||
if (!keystoreV4.verifyPassword(keystore, password))
|
|
||||||
throw new Error("invalid password")
|
|
||||||
currentPassword = undefined
|
|
||||||
await removeKeystore()
|
|
||||||
},
|
|
||||||
async changePassword(password: string, newPassword: string) {
|
|
||||||
const keystore = await getKeystore()
|
|
||||||
assert(keystore, "keyring must be setup")
|
|
||||||
assert(currentPassword, "keyring must be unlocked")
|
|
||||||
if (!keystoreV4.verifyPassword(keystore, password))
|
|
||||||
throw new Error("invalid password")
|
|
||||||
currentPassword = newPassword
|
|
||||||
await setKeystore({
|
|
||||||
...keystoreV4.create(
|
|
||||||
newPassword,
|
|
||||||
keystoreV4.decrypt(keystore, password),
|
|
||||||
),
|
|
||||||
meta: keystore.meta,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async setup(password: string) {
|
|
||||||
assert(!(await getKeystore()), "keyring is already setup")
|
|
||||||
await setKeystore({
|
|
||||||
...keystoreV4.create(
|
|
||||||
password,
|
|
||||||
new TextEncoder().encode(JSON.stringify([])),
|
|
||||||
),
|
|
||||||
meta: [],
|
|
||||||
})
|
|
||||||
currentPassword = password
|
|
||||||
},
|
|
||||||
async hasPassword() {
|
|
||||||
return !!(await getKeystore())
|
|
||||||
},
|
|
||||||
getAccounts,
|
|
||||||
insertCryptoKey,
|
|
||||||
updateCryptoKey,
|
|
||||||
getCryptoKeys,
|
|
||||||
async getCryptoKey(name: string) {
|
|
||||||
return (await getCryptoKeys())?.find((m) => m.name === name)
|
|
||||||
},
|
|
||||||
revealCryptoKey,
|
|
||||||
clearCryptoKey,
|
|
||||||
clearCryptoKeys,
|
|
||||||
async getKeypair(chainId: string, publicKey: string) {
|
|
||||||
assert(currentPassword, "keyring must be unlocked")
|
|
||||||
const keystore = await getKeystore()
|
|
||||||
assert(keystore, "keyring must be setup")
|
|
||||||
|
|
||||||
const keysetIndex = keystore.meta.findIndex((keyset) => {
|
const namesSet = new Set((currentStorage?.keystores || [])
|
||||||
switch (keyset.type) {
|
.map((acc: KeystoreV4WithMeta) => acc.metadata.name)
|
||||||
case "KeysetKeystore":
|
)
|
||||||
return keyset.derivationPaths.some(
|
|
||||||
(d) => d.chainId === chainId && d.publicKey === publicKey,
|
|
||||||
)
|
|
||||||
case "KeypairKeystore":
|
|
||||||
return keyset.publicKey === publicKey
|
|
||||||
default:
|
|
||||||
throw new Error("invalid keystore type")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (keysetIndex === -1) {
|
const publicKeysMap = new Map((currentStorage?.keystores || [])
|
||||||
throw new Error("unknown account")
|
.map((acc: KeystoreV4WithMeta) => [acc.metadata.publicKey, acc])
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const newAcc of keystores) {
|
||||||
|
const publicKey = newAcc.metadata.publicKey
|
||||||
|
if (!overwriteExisting && publicKeysMap.has(publicKey)) continue
|
||||||
|
|
||||||
|
let finalName = newAcc.metadata.name
|
||||||
|
while (namesSet.has(finalName)) {
|
||||||
|
const postfix = Math.random().toString(16).substring(2, 6)
|
||||||
|
finalName = `${finalName}_${postfix}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = decodeSecrets(
|
namesSet.add(finalName)
|
||||||
keystoreV4.decrypt(keystore, currentPassword),
|
|
||||||
)[keysetIndex]
|
|
||||||
|
|
||||||
const cryptoKey = keystore.meta[keysetIndex]
|
const newKeystoreItem: KeystoreMeta = { ...newAcc.metadata, name: finalName }
|
||||||
switch (cryptoKey.type) {
|
publicKeysMap.set(publicKey, { metadata: newKeystoreItem, vault: newAcc.vault })
|
||||||
case "KeysetKeystore": {
|
|
||||||
const { derivationPaths, scheme } = cryptoKey
|
|
||||||
const derivationPath = derivationPaths.find(
|
|
||||||
(d) => d.publicKey === publicKey,
|
|
||||||
)!
|
|
||||||
const createDeriveFn = createDeriveFnMap[scheme]
|
|
||||||
if (!createDeriveFn) throw new Error("invalid signature scheme")
|
|
||||||
return [
|
|
||||||
createDeriveFn(secret)(derivationPath.path),
|
|
||||||
scheme as "Sr25519" | "Ed25519" | "Ecdsa",
|
|
||||||
] as const
|
|
||||||
}
|
|
||||||
case "KeypairKeystore": {
|
|
||||||
let keypair = createKeyPair(secret, cryptoKey.scheme)
|
|
||||||
|
|
||||||
return [keypair, cryptoKey.scheme] as const
|
if (includeDerivations) {
|
||||||
|
const currentDerivations = await getAccountDerivations(newKeystoreItem)
|
||||||
|
|
||||||
|
const derivationNameSet = new Set(currentDerivations.map(der => der.name))
|
||||||
|
const derivationMap = new Map(
|
||||||
|
(currentDerivations || []).map(der => [der.path, der])
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const der of newAcc.derivations) {
|
||||||
|
const path = der.path
|
||||||
|
if (!overwriteExisting && derivationMap.has(path)) continue
|
||||||
|
|
||||||
|
const derPublicKey = derivationMap.get(path)?.publicKey
|
||||||
|
if (derPublicKey && der.publicKey !== derPublicKey) continue
|
||||||
|
|
||||||
|
let derFinalName = der.name
|
||||||
|
if (derivationNameSet.has(derFinalName)) {
|
||||||
|
const postfix = Math.random().toString(16).substring(2, 6)
|
||||||
|
derFinalName = `${derFinalName}_${postfix}`
|
||||||
|
}
|
||||||
|
|
||||||
|
derivationNameSet.add(derFinalName)
|
||||||
|
derivationMap.set(der.path, { ...der, name: derFinalName })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const finalDerivations = Array.from(derivationMap.values())
|
||||||
|
await setAccountDerivations(newKeystoreItem.chainId, newKeystoreItem.publicKey, finalDerivations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalKeystores = Array.from<KeystoreV4WithMeta>(publicKeysMap.values())
|
||||||
|
await setKeystoreStorage({ keystores: finalKeystores })
|
||||||
|
|
||||||
|
if (currentStorage.keystores.length === 0 && finalKeystores.length > 0) {
|
||||||
|
const first = finalKeystores[0].metadata
|
||||||
|
await setActiveAccount({
|
||||||
|
chainId: first.chainId,
|
||||||
|
activeAccount: { publicKey: first.publicKey }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const revealSecretSeed = async (args: RevealSecretSeedArgs): Promise<string> => {
|
||||||
|
assert(args.password, "password is required for this operation")
|
||||||
|
|
||||||
|
const keystoreStorage = await getKeystoreStorage()
|
||||||
|
const keystore = keystoreStorage.keystores.find(
|
||||||
|
(keystore: KeystoreV4WithMeta) => keystore.metadata.publicKey === args.publicKey
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(keystore, "specific crypto vault not found based on account name")
|
||||||
|
assert(keystoreV4.verifyPassword(keystore.vault, args.password), "invalid password for keystore provided")
|
||||||
|
|
||||||
|
const decryptedBytes = keystoreV4.decrypt(keystore.vault, args.password)
|
||||||
|
let finalSecret = ""
|
||||||
|
|
||||||
|
try {
|
||||||
|
const masterSecret = decodeSecret(decryptedBytes)
|
||||||
|
finalSecret = masterSecret
|
||||||
|
} finally {
|
||||||
|
decryptedBytes.fill(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
async changePassword(args: ChangePasswordArgs) {
|
||||||
|
assert(args.password, "password is required for this operation")
|
||||||
|
assert(args.newPassword, "new password is required for this operation")
|
||||||
|
assert(args.password !== args.newPassword, "new password must be different")
|
||||||
|
|
||||||
|
const keystoreStorage = await getKeystoreStorage()
|
||||||
|
const keystoreIndex = keystoreStorage.keystores.findIndex(
|
||||||
|
(keystore: KeystoreV4WithMeta) => keystore.metadata.publicKey === args.publicKey
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(keystoreIndex !== -1, "specific crypto vault not found based on account name")
|
||||||
|
const keystore = keystoreStorage.keystores[keystoreIndex]
|
||||||
|
const decryptedBytes = keystoreV4.decrypt(keystore.vault, args.password)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const secret = decodeSecret(decryptedBytes)
|
||||||
|
const newEncryptedVault = keystoreV4.create(args.newPassword, encodeSecret(secret))
|
||||||
|
|
||||||
|
keystoreStorage.keystores[keystoreIndex] = {
|
||||||
|
...keystore,
|
||||||
|
vault: newEncryptedVault
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
decryptedBytes.fill(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
await setKeystoreStorage({ keystores: keystoreStorage.keystores })
|
||||||
|
},
|
||||||
|
setActiveAccount,
|
||||||
|
getActiveAccount,
|
||||||
|
getAccounts,
|
||||||
|
getAccountDerivations,
|
||||||
|
|
||||||
|
insertAccount,
|
||||||
|
deriveAccount,
|
||||||
|
|
||||||
|
renameAccount,
|
||||||
|
revealSecretSeed,
|
||||||
|
removeAccount,
|
||||||
|
prepareBackup,
|
||||||
|
extendKeystore,
|
||||||
|
|
||||||
|
async getKeypairWithPassword(chainId: string, password: string) {
|
||||||
|
const activeAccount = await getActiveAccount(chainId)
|
||||||
|
assert(activeAccount, "no active account found")
|
||||||
|
|
||||||
|
const keystoreStorage = await getKeystoreStorage()
|
||||||
|
const keystore = keystoreStorage.keystores.find((k: KeystoreV4WithMeta) =>
|
||||||
|
k.metadata.chainId === chainId && k.metadata.publicKey === activeAccount.publicKey
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(keystore, "unknown account targets mapped")
|
||||||
|
assert(keystoreV4.verifyPassword(keystore.vault, password), "invalid password for keystore provided")
|
||||||
|
|
||||||
|
const decryptedBytes = keystoreV4.decrypt(keystore.vault, password)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const masterSecret = decodeSecret(decryptedBytes)
|
||||||
|
const { scheme } = keystore.metadata
|
||||||
|
|
||||||
|
const keypair = createKeyPair(masterSecret, scheme, activeAccount.path ?? "")
|
||||||
|
return [keypair, scheme as "Sr25519" | "Ed25519" | "Ecdsa"] as const
|
||||||
|
} finally {
|
||||||
|
decryptedBytes.fill(0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,57 @@
|
|||||||
import { KeystoreV4 } from "./keystoreV4"
|
import { KeystoreV4 } from "./keystoreV4"
|
||||||
|
|
||||||
export type DerivationPath = {
|
export type Derivation = {
|
||||||
chainId: string
|
name: string
|
||||||
path: string
|
path: string
|
||||||
publicKey: string
|
publicKey: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DerivationWithAddress = Derivation & {
|
||||||
|
address: string
|
||||||
|
}
|
||||||
|
|
||||||
export type BaseKeystore = {
|
export type BaseKeystore = {
|
||||||
name: string
|
name: string
|
||||||
scheme: "Sr25519" | "Ed25519" | "Ecdsa"
|
scheme: "Sr25519" | "Ed25519" | "Ecdsa"
|
||||||
createdAt: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type KeysetKeystore = BaseKeystore & {
|
export type KeystoreMeta = BaseKeystore & {
|
||||||
type: "KeysetKeystore"
|
|
||||||
derivationPaths: DerivationPath[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type KeypairKeystore = BaseKeystore & {
|
|
||||||
type: "KeypairKeystore"
|
|
||||||
publicKey: string
|
publicKey: string
|
||||||
|
chainId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type KeystoreMeta = KeysetKeystore | KeypairKeystore
|
export type KeystoreMetaWithAddress = KeystoreMeta & {
|
||||||
|
address: string
|
||||||
export type KeystoreV4WithMeta = KeystoreV4 & {
|
}
|
||||||
meta: KeystoreMeta[]
|
|
||||||
|
export type ExposedAccount = {
|
||||||
|
publicKey: string,
|
||||||
|
address: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AccountTree = KeystoreMetaWithAddress & {
|
||||||
|
derivations: DerivationWithAddress[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KeystoreV4WithMeta = {
|
||||||
|
metadata: KeystoreMeta
|
||||||
|
vault: KeystoreV4
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KeyringStorage = {
|
||||||
|
keystores: KeystoreV4WithMeta[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FullKeystoreMeta = {
|
||||||
|
metadata: KeystoreMeta
|
||||||
|
derivations: Derivation[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FullKeystoreMetaWithVault = FullKeystoreMeta & {
|
||||||
|
vault: KeystoreV4
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ActiveAccount = {
|
||||||
|
publicKey?: string
|
||||||
|
path?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
|
|||||||
import {
|
import {
|
||||||
Bytes,
|
Bytes,
|
||||||
Struct,
|
Struct,
|
||||||
|
decAnyMetadata,
|
||||||
|
unifyMetadata,
|
||||||
_void,
|
_void,
|
||||||
compact,
|
compact,
|
||||||
u32,
|
u32,
|
||||||
@ -21,15 +23,13 @@ export const getSignaturePayload = async (
|
|||||||
provider: JsonRpcProvider,
|
provider: JsonRpcProvider,
|
||||||
payload: Pjs.SignerPayloadJSON,
|
payload: Pjs.SignerPayloadJSON,
|
||||||
) => {
|
) => {
|
||||||
const { metadata$, unfollow } = getObservableClient(
|
const { metadata$, unfollow } = getObservableClient(createClient(provider)).chainHead$()
|
||||||
createClient(provider),
|
const rawMetadata = await firstValueFrom(metadata$.pipe(filter(Boolean))).finally(unfollow)
|
||||||
).chainHead$()
|
|
||||||
const metadata = await firstValueFrom(
|
const metadata = unifyMetadata(rawMetadata as any)
|
||||||
metadata$.pipe(filter(Boolean)),
|
|
||||||
).finally(unfollow)
|
|
||||||
const dynamicBuilder = getDynamicBuilder(getLookupFn(metadata))
|
const dynamicBuilder = getDynamicBuilder(getLookupFn(metadata))
|
||||||
const [extra, additionalSigned] = metadata.extrinsic.signedExtensions.reduce<
|
const [extra, additionalSigned] = metadata.extrinsic.signedExtensions.reduce<
|
||||||
[extra: Uint8Array[], additionalSigned: Uint8Array[]]
|
[extra: Uint8Array[], additionalSigned: Uint8Array[]]
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
[extra, additionalSigned],
|
[extra, additionalSigned],
|
||||||
@ -93,8 +93,8 @@ export const getSignaturePayload = async (
|
|||||||
...additionalSigned,
|
...additionalSigned,
|
||||||
)
|
)
|
||||||
return signaturePayload.length > 256
|
return signaturePayload.length > 256
|
||||||
? blake2b256(signaturePayload)
|
? blake2b256(signaturePayload)
|
||||||
: signaturePayload
|
: signaturePayload
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUserSignedExtensions = (payload: Pjs.SignerPayloadJSON) => {
|
export const getUserSignedExtensions = (payload: Pjs.SignerPayloadJSON) => {
|
||||||
@ -103,15 +103,16 @@ export const getUserSignedExtensions = (payload: Pjs.SignerPayloadJSON) => {
|
|||||||
userSignedExtensions.CheckMortality =
|
userSignedExtensions.CheckMortality =
|
||||||
// Ser mortality encoding https://spec.polkadot.network/id-extrinsics#sect-mortality-encoding
|
// Ser mortality encoding https://spec.polkadot.network/id-extrinsics#sect-mortality-encoding
|
||||||
mortality.length === 1
|
mortality.length === 1
|
||||||
? { mortal: false }
|
? { mortal: false }
|
||||||
: { mortal: true, period: 2 << u16.dec(mortality) % (1 << 4) }
|
: { mortal: true, period: 2 << u16.dec(mortality) % (1 << 4) }
|
||||||
|
|
||||||
if (payload.signedExtensions.includes("ChargeTransactionPayment"))
|
if (payload.signedExtensions.includes("ChargeTransactionPayment"))
|
||||||
userSignedExtensions.ChargeTransactionPayment = Number(payload.tip)
|
userSignedExtensions.ChargeTransactionPayment = BigInt(payload.tip || 0)
|
||||||
else if (payload.signedExtensions.includes("ChargeAssetTxPayment"))
|
else if (payload.signedExtensions.includes("ChargeAssetTxPayment"))
|
||||||
userSignedExtensions.ChargeAssetTxPayment = {
|
userSignedExtensions.ChargeAssetTxPayment = {
|
||||||
// @ts-expect-error FIXME: bigint needs to be serialized
|
// @ts-expect-error FIXME: bigint needs to be serialized
|
||||||
tip: Number(payload.tip),
|
tip: Number(payload.tip),
|
||||||
}
|
}
|
||||||
|
|
||||||
return userSignedExtensions
|
return userSignedExtensions
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,41 +28,3 @@ export const listChainSpecsHandler: RpcMethodHandlers<
|
|||||||
|
|
||||||
return chainSpecs
|
return chainSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addChainSpecHandler: RpcMethodHandlers<
|
|
||||||
BackgroundRpcSpec,
|
|
||||||
Context
|
|
||||||
>["addChainSpec"] = async ([chainSpec], { lightClientPageHelper }) => {
|
|
||||||
const chainSpecParsed = chainSpecSchema.parse(JSON.parse(chainSpec))
|
|
||||||
|
|
||||||
const relayChainChainSpec = await lightClientPageHelper
|
|
||||||
.getChains()
|
|
||||||
.then((chains) =>
|
|
||||||
chains.map((chain) => ({
|
|
||||||
...chainSpecSchema.parse(JSON.parse(chain.chainSpec)),
|
|
||||||
genesisHash: chain.genesisHash,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.then((chains) =>
|
|
||||||
chains.find((chain) => chain.id === chainSpecParsed.relay_chain),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (chainSpecParsed.relay_chain && !relayChainChainSpec) {
|
|
||||||
throw new Error("relay chain not found")
|
|
||||||
}
|
|
||||||
if (relayChainChainSpec?.relay_chain) {
|
|
||||||
throw new Error("relay chain cannot be a parachain")
|
|
||||||
}
|
|
||||||
|
|
||||||
await lightClientPageHelper.persistChain(
|
|
||||||
chainSpec,
|
|
||||||
relayChainChainSpec?.genesisHash,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const removeChainSpecHandler: RpcMethodHandlers<
|
|
||||||
BackgroundRpcSpec,
|
|
||||||
Context
|
|
||||||
>["removeChainSpec"] = async ([genesisHash], { lightClientPageHelper }) => {
|
|
||||||
await lightClientPageHelper.deleteChain(genesisHash)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { Account, SignRequest } from "../types"
|
|||||||
|
|
||||||
export type SignResponse = {
|
export type SignResponse = {
|
||||||
userSignedExtensions: Partial<UserSignedExtensions>
|
userSignedExtensions: Partial<UserSignedExtensions>
|
||||||
|
password: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InternalSignRequest = {
|
export type InternalSignRequest = {
|
||||||
|
|||||||
@ -2,28 +2,27 @@ import type { KeystoreV4WithMeta } from "./keystore"
|
|||||||
|
|
||||||
const STORAGE_PREFIX = "ghost-extension/"
|
const STORAGE_PREFIX = "ghost-extension/"
|
||||||
|
|
||||||
type StorageConfig = {
|
export const remove = <Config extends Record<string, any>>(
|
||||||
keystore: KeystoreV4WithMeta
|
keyOrKeys: keyof Config | Array<keyof Config>
|
||||||
}
|
): Promise<void> => {
|
||||||
|
const getKey = (key: keyof Config) => `${STORAGE_PREFIX}${String(key)}`
|
||||||
type StorageKey = keyof StorageConfig
|
return chrome.storage.local.remove(
|
||||||
|
|
||||||
const getKey = (key: StorageKey) => `${STORAGE_PREFIX}${key}`
|
|
||||||
|
|
||||||
export const remove = (keyOrKeys: StorageKey | StorageKey[]): Promise<void> =>
|
|
||||||
chrome.storage.local.remove(
|
|
||||||
Array.isArray(keyOrKeys) ? keyOrKeys.map(getKey) : getKey(keyOrKeys),
|
Array.isArray(keyOrKeys) ? keyOrKeys.map(getKey) : getKey(keyOrKeys),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const get = async <K extends StorageKey>(
|
export const get = async <Config extends Record<string, any>, K extends keyof Config>(
|
||||||
key: K,
|
key: K
|
||||||
): Promise<StorageConfig[K] | undefined> => {
|
): Promise<Config[K] | undefined> => {
|
||||||
const key_ = getKey(key)
|
const fullKey = `${STORAGE_PREFIX}${String(key)}`
|
||||||
const { [key_]: value } = await chrome.storage.local.get([key_])
|
const { [fullKey]: value } = await chrome.storage.local.get([fullKey])
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
export const set = <K extends StorageKey>(
|
export const set = <Config extends Record<string, any>, K extends keyof Config>(
|
||||||
key: K,
|
key: K,
|
||||||
value: StorageConfig[K],
|
value: Config[K],
|
||||||
): Promise<void> => chrome.storage.local.set({ [getKey(key)]: value })
|
): Promise<void> => {
|
||||||
|
const fullKey = `${STORAGE_PREFIX}${String(key)}`
|
||||||
|
return chrome.storage.local.set({ [fullKey]: value })
|
||||||
|
}
|
||||||
|
|||||||
@ -4,6 +4,17 @@ import {
|
|||||||
UserSignedExtensions,
|
UserSignedExtensions,
|
||||||
} from "../types/UserSignedExtension"
|
} from "../types/UserSignedExtension"
|
||||||
|
|
||||||
|
import {
|
||||||
|
FullKeystoreMeta,
|
||||||
|
FullKeystoreMetaWithVault,
|
||||||
|
KeystoreMeta,
|
||||||
|
ActiveAccount,
|
||||||
|
AccountTree,
|
||||||
|
ExposedAccount,
|
||||||
|
KeystoreV4WithMeta,
|
||||||
|
KeystoreMetaWithAddress,
|
||||||
|
} from "./keystore/types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1:1 representation of chain spec JSON format with addition fields
|
* 1:1 representation of chain spec JSON format with addition fields
|
||||||
*/
|
*/
|
||||||
@ -20,21 +31,22 @@ export type Account = {
|
|||||||
address: string
|
address: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type KeystoreAccount =
|
export type KeystoreAccount = {
|
||||||
| ({ type: "Keyset" } & DerivationPath)
|
publicKey: string,
|
||||||
| { type: "Keypair"; publicKey: string }
|
name: string,
|
||||||
|
chainId: string,
|
||||||
|
derivations: Derivation[]
|
||||||
|
}
|
||||||
|
|
||||||
export type DerivationPath = {
|
export type Derivation = {
|
||||||
chainId: string
|
name: string
|
||||||
path: string
|
path: string
|
||||||
publicKey: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CryptoKey = {
|
export type CryptoKey = {
|
||||||
name: string
|
name: string
|
||||||
scheme: "Sr25519" | "Ed25519" | "Ecdsa"
|
scheme: "Sr25519" | "Ed25519" | "Ecdsa"
|
||||||
accounts: KeystoreAccount[]
|
accounts: KeystoreAccount[]
|
||||||
createdAt: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignRequest = {
|
export type SignRequest = {
|
||||||
@ -47,39 +59,64 @@ export type SignRequest = {
|
|||||||
| { type: "values"; values: Partial<UserSignedExtensions> }
|
| { type: "values"; values: Partial<UserSignedExtensions> }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InsertCryptoKeyArgs = {
|
export type InsertAccountArgs = {
|
||||||
name: string
|
name: string
|
||||||
scheme: "Sr25519" | "Ed25519" | "Ecdsa"
|
scheme: "Sr25519" | "Ed25519" | "Ecdsa"
|
||||||
createdAt: number
|
password: string
|
||||||
} & (
|
chainId: string
|
||||||
| {
|
miniSecret: string
|
||||||
type: "Keyset"
|
|
||||||
miniSecret: string
|
|
||||||
derivationPaths: DerivationPath[]
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "Keypair"
|
|
||||||
privatekey: string
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type KeyringState = {
|
|
||||||
isLocked: boolean
|
|
||||||
hasPassword: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateCryptoKeyArgs = {
|
export type DeriveAccountArgs = {
|
||||||
name: string,
|
publicKey: string
|
||||||
deviation: number,
|
password: string
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RemoveCryptoKeyArgs = {
|
export type ValidatePasswordArgs = {
|
||||||
name: string,
|
name: string
|
||||||
|
publicKey: string
|
||||||
|
password: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RevealCryptoKeyArgs = {
|
export type RevealSecretSeedArgs = {
|
||||||
meta: string,
|
publicKey: string
|
||||||
index: number,
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RemoveAccountArgs = {
|
||||||
|
publicKey: string
|
||||||
|
password: string
|
||||||
|
path?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RenameAccountArgs = {
|
||||||
|
path?: string
|
||||||
|
newName: string
|
||||||
|
publicKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChangePasswordArgs = {
|
||||||
|
publicKey: string
|
||||||
|
password: string
|
||||||
|
newPassword: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AccountTreeArgs = {
|
||||||
|
chainId: string
|
||||||
|
publicKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExtendKeystoreArgs = {
|
||||||
|
includeDerivations: boolean,
|
||||||
|
overwriteExisting: boolean,
|
||||||
|
keystores: FullKeystoreMetaWithVault[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ActiveAccountArgs = {
|
||||||
|
chainId: string,
|
||||||
|
activeAccount?: ActiveAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace Pjs {
|
export namespace Pjs {
|
||||||
@ -92,31 +129,32 @@ export namespace Pjs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type BackgroundRpcSpec = {
|
export type BackgroundRpcSpec = {
|
||||||
getAccounts(chainId: string): Promise<Account[]>
|
getAccount(chainId: string): Promise<ExposedAccount | undefined>
|
||||||
createTx(chainId: string, from: string, callData: string): Promise<string>
|
|
||||||
pjsSignPayload(payload: Pjs.SignerPayloadJSON): Promise<string>
|
pjsSignPayload(payload: Pjs.SignerPayloadJSON): Promise<string>
|
||||||
|
createTx(chainId: string, from: string, callData: string): Promise<string>
|
||||||
// private methods
|
// private methods
|
||||||
getSignRequests(): Promise<Record<string, SignRequest>>
|
getSignRequests(): Promise<Record<string, SignRequest>>
|
||||||
approveSignRequest(
|
approveSignRequest(
|
||||||
id: string,
|
id: string,
|
||||||
userSignedExtensions: Partial<UserSignedExtensions>,
|
userSignedExtensions: Partial<UserSignedExtensions>,
|
||||||
|
password: string
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
cancelSignRequest(id: string): Promise<void>
|
cancelSignRequest(id: string): Promise<void>
|
||||||
lockKeyring(): Promise<void>
|
changePassword(args: ChangePasswordArgs): Promise<void>
|
||||||
resetKeyring(password: string): Promise<void>
|
revealSecretSeed(args: RevealSecretSeedArgs): Promise<string>
|
||||||
unlockKeyring(password: string): Promise<void>
|
|
||||||
changePassword(currentPassword: string, newPassword: string): Promise<void>
|
|
||||||
createPassword(password: string): Promise<void>
|
|
||||||
getCryptoKeys(): Promise<CryptoKey[]>
|
|
||||||
insertCryptoKey(args: InsertCryptoKeyArgs): Promise<void>
|
|
||||||
updateCryptoKey(args: UpdateCryptoKeyArgs): Promise<void>
|
|
||||||
getCryptoKey(name: string): Promise<CryptoKey | undefined>
|
|
||||||
revealCryptoKey(name: string, index: number): Promise<string>
|
|
||||||
clearCryptoKey(name: string, index: number): Promise<void>
|
|
||||||
clearCryptoKeys(args: RemoveCryptoKeyArgs): Promise<void>
|
|
||||||
getKeyringState(): Promise<KeyringState>
|
|
||||||
|
|
||||||
|
getAccounts(chainId: string): Promise<KeystoreMetaWithAddress[]>
|
||||||
|
renameAccount(args: RenameAccountArgs): Promise<void>
|
||||||
|
setActiveAccount(args: ActiveAccountArgs): Promise<void>
|
||||||
getChainSpecs(): Promise<ChainSpec[]>
|
getChainSpecs(): Promise<ChainSpec[]>
|
||||||
addChainSpec(chainSpec: string): Promise<void>
|
|
||||||
removeChainSpec(genesisHash: string): Promise<void>
|
// new ones
|
||||||
|
getAccountDerivations(metadata: KeystoreMeta | undefined): Promise<AccountTree | undefined>
|
||||||
|
getActiveAccount(chainId: string): Promise<ActiveAccount | undefined>
|
||||||
|
prepareBackup(publicKeys: string[]): Promise<FullKeystoreMeta[]>
|
||||||
|
|
||||||
|
insertAccount(args: InsertAccountArgs): Promise<void>
|
||||||
|
deriveAccount(args: DeriveAccountArgs): Promise<void>
|
||||||
|
removeAccount(args: RemoveAccountArgs): Promise<void>
|
||||||
|
extendKeystore(args: ExtendKeystoreArgs): Promise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -146,7 +146,7 @@ export const Bootnodes = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="container">
|
<section className="container">
|
||||||
<h2 className="pb-2 space-y-2 text-3xl font-semibold md:pb-4">
|
<h2 className="pb-2 space-y-2 text-3xl font-semibold md:pb-4 text-white">
|
||||||
Networks
|
Networks
|
||||||
</h2>
|
</h2>
|
||||||
<div className="p-4 bg-muted rounded">
|
<div className="p-4 bg-muted rounded">
|
||||||
@ -159,10 +159,10 @@ export const Bootnodes = () => {
|
|||||||
name={selectedChain}
|
name={selectedChain}
|
||||||
value={selectedChain}
|
value={selectedChain}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="sm:w-[250px] w-[100%]" data-testid="scheme-select">
|
<SelectTrigger className="sm:w-[250px] w-[100%] text-white" data-testid="scheme-select">
|
||||||
<SelectValue placeholder="Select Network" />
|
<SelectValue placeholder="Select Network" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="sm:w-[250px] w-[100%]">
|
<SelectContent className="sm:w-[250px] w-[100%] text-white">
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{networks.map(network => (
|
{networks.map(network => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
@ -177,16 +177,15 @@ export const Bootnodes = () => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Title className="mt-8">Bootnodes</Title>
|
<Title className="mt-8 text-white">Bootnodes</Title>
|
||||||
<Title titleType="small">Default</Title>
|
<Title titleType="small" className="text-white">Default</Title>
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
{selectedChainDefaultBn?.map((bn) => (
|
{selectedChainDefaultBn?.map((bn) => (
|
||||||
<div className="flex items-center mb-2 leading-4 wrap">
|
<div className="flex items-center mb-2 leading-4 wrap">
|
||||||
<div className="sm:w-11/12 w-1/2 overflow-hidden text-ellipsis whitespace-nowrap">
|
<div className="sm:w-11/12 w-1/2 overflow-hidden text-ellipsis whitespace-nowrap text-white">
|
||||||
{bn}
|
{bn}
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
className="w-1/2 sm:w-1/12"
|
|
||||||
bootnode={bn}
|
bootnode={bn}
|
||||||
alterBootnodes={alterBootnodes}
|
alterBootnodes={alterBootnodes}
|
||||||
defaultBootnode={true}
|
defaultBootnode={true}
|
||||||
@ -195,11 +194,11 @@ export const Bootnodes = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Title titleType="small" className="text-accent">Custom</Title>
|
<Title titleType="small" className="text-white">Custom</Title>
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
{customBn.map((c) => (
|
{customBn.map((c) => (
|
||||||
<div className="flex items-center mb-2 leading-4">
|
<div className="flex items-center mb-2 leading-4">
|
||||||
<div className="sm:w-11/12 w-1/2 overflow-hidden text-ellipsis whitespace-nowrap">
|
<div className="sm:w-11/12 w-1/2 overflow-hidden text-ellipsis whitespace-nowrap text-white">
|
||||||
{c.bootnode}
|
{c.bootnode}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-1/2 sm:w-1/12 ml-8">
|
<div className="flex w-1/2 sm:w-1/12 ml-8">
|
||||||
@ -226,13 +225,13 @@ export const Bootnodes = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Title>Add custom Bootnode</Title>
|
<Title className="text-white">Add custom Bootnode</Title>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex sm:flex-row flex-col gap-6 justify-between mb-4">
|
<div className="flex sm:flex-row flex-col gap-6 justify-between mb-4">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter bootnode address"
|
placeholder="Enter bootnode address"
|
||||||
className="sm:w-5/6 w-full"
|
className="sm:w-5/6 w-full text-white"
|
||||||
value={customBnInput}
|
value={customBnInput}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
addMessage && setAddMessage(undefined)
|
addMessage && setAddMessage(undefined)
|
||||||
@ -240,7 +239,7 @@ export const Bootnodes = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="primary"
|
||||||
size="full"
|
size="full"
|
||||||
className="sm:w-1/6 w-full"
|
className="sm:w-1/6 w-full"
|
||||||
disabled={!customBnInput}
|
disabled={!customBnInput}
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export const BraveModal = ({ show, isOptions }: Props) => {
|
|||||||
}" flex pt-2 pb-4 px-4 space-x-2 border-gray-200 dark:border-gray-600"`}
|
}" flex pt-2 pb-4 px-4 space-x-2 border-gray-200 dark:border-gray-600"`}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -91,7 +91,7 @@ export const BraveModal = ({ show, isOptions }: Props) => {
|
|||||||
Dismiss
|
Dismiss
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => openInNewTab("https://github.com/brave/brave-browser/issues/19990")}
|
onClick={() => openInNewTab("https://github.com/brave/brave-browser/issues/19990")}
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -1,9 +1,79 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { ChevronDown } from 'lucide-react'
|
||||||
|
|
||||||
|
import { NetworkLogo } from "../containers/WalletPopup/components"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
innerClassName?: string
|
||||||
|
className?: string
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
|
accountName?: string | undefined
|
||||||
|
chainValue?: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Layout: React.FC<Props> = ({ children }) => {
|
export const Layout: React.FC<Props> = ({ children, className, innerClassName, accountName, chainValue }) => {
|
||||||
return <div className="bg-muted mx-auto px-6 py-8">{children}</div>
|
return (
|
||||||
}
|
<main
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center",
|
||||||
|
"min-h-screen",
|
||||||
|
"font-sans",
|
||||||
|
"bg-slate-950",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"bg-muted",
|
||||||
|
"w-[400px] h-[600px]",
|
||||||
|
"flex flex-col",
|
||||||
|
"shadow-sm",
|
||||||
|
"rounded-none lg:rounded",
|
||||||
|
"overflow-hidden",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<header className="flex flex-row items-center gap-2 px-6 pt-3 pb-3 bg-background flex-shrink-0 w-full">
|
||||||
|
<Link
|
||||||
|
to="/switch-network"
|
||||||
|
className={cn(
|
||||||
|
"w-[52px] h-9 rounded-lg bg-input text-white flex items-center justify-between p-1",
|
||||||
|
"hover:bg-input/90 active:scale-[0.98] transition-all border border-white shadow-sm"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NetworkLogo value={chainValue} />
|
||||||
|
<ChevronDown className="w-3 h-3 ml-0.5 text-white" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/switch-account"
|
||||||
|
className={cn(
|
||||||
|
"flex-grow h-9 rounded-lg bg-input text-white px-3 flex items-center justify-between",
|
||||||
|
"hover:bg-input/90 active:scale-[0.98] transition-all shadow-sm border border-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-xs font-semibold tracking-wide text-white/90 truncate max-w-[230px] text-center mx-auto">
|
||||||
|
{accountName ?? ""}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className="w-3.5 h-3.5 text-white flex-shrink-0" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="w-full h-[1px] bg-white/20 flex-shrink-0" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"bg-muted",
|
||||||
|
"flex-grow flex flex-col min-h-0 w-full",
|
||||||
|
innerClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
import Logo from "./Logo";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@ -17,22 +18,42 @@ export type Props = {
|
|||||||
<main
|
<main
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-center",
|
"flex items-center justify-center",
|
||||||
"min-h-screen",
|
"w-full h-full min-h-screen",
|
||||||
"font-sans",
|
"font-sans",
|
||||||
|
"bg-slate-950",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background",
|
"bg-muted",
|
||||||
"w-[400px] h-[600px]",
|
"min-w-[400px] min-h-[600px]",
|
||||||
|
"w-full h-screen",
|
||||||
"flex flex-col",
|
"flex flex-col",
|
||||||
"shadow-sm",
|
"shadow-none",
|
||||||
"rounded-none lg:rounded",
|
"rounded-none",
|
||||||
innerClassName
|
"overflow-hidden",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
<header className="text-center flex-shrink-0 pt-2 pb-2 bg-muted">
|
||||||
|
<div className="w-full flex justify-center items-center py-[5px] px-[30px]">
|
||||||
|
<div className="w-full max-w-[140px] h-auto flex items-center justify-center">
|
||||||
|
<Logo cName="fill-white w-full h-auto" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="w-full h-[1px] bg-white/20 flex-shrink-0" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"bg-muted",
|
||||||
|
"flex-grow flex flex-col min-h-0 w-full",
|
||||||
|
innerClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
const HeaderIcon: React.FC<IconProps> = (props) => (
|
const HeaderIcon = () => (
|
||||||
<svg id="uuid-32ee7813-8d87-40da-a035-c52caac6bd18" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 277.87">
|
<svg id="uuid-32ee7813-8d87-40da-a035-c52caac6bd18" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 277.87">
|
||||||
<g id="uuid-9027531e-a307-4e67-9c22-6677086ef914" data-name="ghostConnect - vertical">
|
<g id="uuid-9027531e-a307-4e67-9c22-6677086ef914" data-name="ghostConnect - vertical">
|
||||||
<g>
|
<g>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ interface TitleProps {
|
|||||||
children: ReactNode
|
children: ReactNode
|
||||||
titleType?: "small" | "normal" | "large"
|
titleType?: "small" | "normal" | "large"
|
||||||
showOptions?: boolean
|
showOptions?: boolean
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Title = ({
|
const Title = ({
|
||||||
|
|||||||
@ -5,29 +5,28 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-foreground hover:text-background active:text-primary-foreground",
|
primary: "bg-primary text-primary-foreground hover:bg-muted-foreground transition-colors duration-200",
|
||||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary-foreground hover:text-secondary active:text-secondary-foreground",
|
secondary: "bg-transparent text-white border-2 border-solid border-white/40 hover:bg-primary hover:text-primary-foreground transition-colors duration-200",
|
||||||
ghost: "text-primary hover:bg-accent hover:text-background active:text-primary",
|
ghost: "bg-transparent text-white border-2 border-solid border-white/40 hover:bg-white/10 transition-colors duration-200",
|
||||||
|
|
||||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 active:text-destructive-foreground",
|
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 active:text-destructive-foreground",
|
||||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
link: "text-primary underline-offset-4 hover:underline active:text-primary",
|
link: "text-primary underline-offset-4 hover:underline active:text-primary",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 px-4 py-2",
|
primary: "h-10 px-4 py-2",
|
||||||
sm: "h-9 rounded-md px-3",
|
sm: "h-9 px-3",
|
||||||
lg: "h-11 rounded-md px-8",
|
lg: "h-11 px-8",
|
||||||
full: "h-10 w-full",
|
full: "h-10 w-full",
|
||||||
icon: "h-10 w-10",
|
icon: "h-10 w-10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "primary",
|
||||||
size: "default",
|
size: "primary",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -11,16 +11,16 @@ const Checkbox = React.forwardRef<
|
|||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background",
|
"peer h-4 w-4 shrink-0 rounded-sm border border-white ring-offset-background",
|
||||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
"data-[state=checked]:bg-accent data-[state=checked]:border-secondary data-[state=checked]:text-secondary",
|
"data-[state=checked]:bg-primary data-[state=checked]:border-primary",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator
|
<CheckboxPrimitive.Indicator
|
||||||
className={cn("flex items-center justify-center text-current")}
|
className={cn("flex items-center justify-center text-input")}
|
||||||
>
|
>
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
|
|||||||
@ -65,7 +65,7 @@ const PaginationPrevious = ({
|
|||||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
aria-label="Go to previous page"
|
aria-label="Go to previous page"
|
||||||
size="default"
|
size="primary"
|
||||||
className={cn("gap-1 pl-2.5", className)}
|
className={cn("gap-1 pl-2.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@ -81,7 +81,7 @@ const PaginationNext = ({
|
|||||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
aria-label="Go to next page"
|
aria-label="Go to next page"
|
||||||
size="default"
|
size="primary"
|
||||||
className={cn("gap-1 pr-2.5", className)}
|
className={cn("gap-1 pr-2.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -21,7 +21,7 @@ RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
|||||||
const RadioGroupItem = React.forwardRef<
|
const RadioGroupItem = React.forwardRef<
|
||||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||||
>(({ className, label, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<RadioGroupPrimitive.Item
|
<RadioGroupPrimitive.Item
|
||||||
@ -38,9 +38,6 @@ const RadioGroupItem = React.forwardRef<
|
|||||||
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||||
</RadioGroupPrimitive.Indicator>
|
</RadioGroupPrimitive.Indicator>
|
||||||
</RadioGroupPrimitive.Item>
|
</RadioGroupPrimitive.Item>
|
||||||
<label className="ml-2" htmlFor={props.id}>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,43 +1,41 @@
|
|||||||
type Network = {
|
export type NetworkType = {
|
||||||
chainId: string
|
chainId: string
|
||||||
label: string
|
label: string
|
||||||
value: string
|
value: string
|
||||||
logo: string
|
|
||||||
prefix: number
|
prefix: number
|
||||||
decimals: number
|
decimals: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const networks: Network[] = [
|
export const networks: NetworkType[] = [
|
||||||
{
|
{
|
||||||
chainId: "0x475e48fab52f3d0587b6b03101d224560c549e984d1dee197b7d8b55830e7da3",
|
chainId: "0xcaf9c5ddccc7ba6f96149a6b8a81c26a3641742795cd5e7e0aee79bf3f4e77b7",
|
||||||
value: "casper_staging_testnet",
|
value: "casper_staging_testnet",
|
||||||
label: "CASPER",
|
label: "CASPER Testnet",
|
||||||
logo: "https://cryptologos.cc/logos/ghostchain-logo.svg",
|
|
||||||
prefix: 1996,
|
prefix: 1996,
|
||||||
decimals: 18,
|
decimals: 18,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export const wellKnownGenesisHashByChainId: Record<string, string> =
|
export const wellKnownGenesisHashByChainId: Record<string, string> =
|
||||||
networks.reduce((wellKnownChainIds, { chainId, value }) => {
|
networks.reduce<Record<string, string>>((wellKnownChainIds, { chainId, value }) => {
|
||||||
wellKnownChainIds[value] = chainId
|
wellKnownChainIds[value] = chainId
|
||||||
return wellKnownChainIds
|
return wellKnownChainIds
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
export const wellKnownChainIdByGenesisHash: Record<string, string> =
|
export const wellKnownChainIdByGenesisHash: Record<string, string> =
|
||||||
networks.reduce((wellKnownChainName, { chainId, value }) => {
|
networks.reduce<Record<string, string>>((wellKnownChainName, { chainId, value }) => {
|
||||||
wellKnownChainName[chainId] = value
|
wellKnownChainName[chainId] = value
|
||||||
return wellKnownChainName
|
return wellKnownChainName
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
export const wellKnownPrefixByGenesisHash: Record<string, number> =
|
export const wellKnownPrefixByGenesisHash: Record<string, number> =
|
||||||
networks.reduce((wellKnownPrefixes, { chainId, prefix }) => {
|
networks.reduce<Record<string, number>>((wellKnownPrefixes, { chainId, prefix }) => {
|
||||||
wellKnownPrefixes[chainId] = prefix
|
wellKnownPrefixes[chainId] = prefix
|
||||||
return wellKnownPrefixes
|
return wellKnownPrefixes
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
export const wellKnownDecimalsByGenesisHash: Record<string, number> =
|
export const wellKnownDecimalsByGenesisHash: Record<string, number> =
|
||||||
networks.reduce((wellKnownDecimals, { decimals, chainId }) => {
|
networks.reduce<Record<string, number>>((wellKnownDecimals, { decimals, chainId }) => {
|
||||||
wellKnownDecimals[chainId] = decimals
|
wellKnownDecimals[chainId] = decimals
|
||||||
return wellKnownDecimals
|
return wellKnownDecimals
|
||||||
}, {})
|
}, {})
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const item = [
|
|||||||
"flex",
|
"flex",
|
||||||
"items-center",
|
"items-center",
|
||||||
"text-base",
|
"text-base",
|
||||||
"text-primary",
|
"text-white",
|
||||||
"py-4",
|
"py-4",
|
||||||
"px-6",
|
"px-6",
|
||||||
"h-12",
|
"h-12",
|
||||||
@ -40,7 +40,7 @@ const itemActive = [
|
|||||||
"cursor-default",
|
"cursor-default",
|
||||||
]
|
]
|
||||||
|
|
||||||
const title = ["ml-4 font-inter font-medium text-primary"]
|
const title = ["ml-4 font-inter font-medium text-white"]
|
||||||
const titleInactive = ["group-hover:text-accent"]
|
const titleInactive = ["group-hover:text-accent"]
|
||||||
const titleActive = ["text-accent", "cursor-default"]
|
const titleActive = ["text-accent", "cursor-default"]
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ const icon = [
|
|||||||
"h-6",
|
"h-6",
|
||||||
"md:w-4",
|
"md:w-4",
|
||||||
"md:h-4",
|
"md:h-4",
|
||||||
"text-primary",
|
"text-white",
|
||||||
]
|
]
|
||||||
const iconInactive = ["group-hover:text-accent"]
|
const iconInactive = ["group-hover:text-accent"]
|
||||||
const iconActive = ["cursor-default"]
|
const iconActive = ["cursor-default"]
|
||||||
@ -107,7 +107,7 @@ const Networks: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="container">
|
<section className="container">
|
||||||
<h2 className="pb-2 space-y-2 text-3xl font-semibold md:pb-4">
|
<h2 className="pb-2 space-y-2 text-3xl font-semibold md:pb-4 text-white">
|
||||||
Networks
|
Networks
|
||||||
</h2>
|
</h2>
|
||||||
{networks.length ? (
|
{networks.length ? (
|
||||||
@ -123,7 +123,7 @@ const Networks: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
) : (
|
) : (
|
||||||
<div>The extension isn't connected to any network.</div>
|
<div className="text-white">The extension isn't connected to any network.</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
@ -149,8 +149,8 @@ export const Options: FunctionComponent = () => {
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="flex flex-col gap-2 justify-center items-center">
|
<div className="flex flex-col gap-2 justify-center items-center">
|
||||||
<Logo cName="cursor-pointer fill-primary hover:fill-accent md:w-full w-[50%]" />
|
<Logo cName="cursor-pointer fill-white hover:fill-accent md:w-full w-[50%]" />
|
||||||
<div className="text-base text-xs">Version {pckg.version}</div>
|
<div className="text-white text-xs">Version {pckg.version}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -187,7 +187,7 @@ export const Options: FunctionComponent = () => {
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
to="https://git.ghostchain.io/ghostchain/ghost-extension-wallet"
|
to="https://git.ghostchain.io/ghostchain/ghost-extension-wallet"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-base hover:text-accent"
|
className="text-base text-white hover:text-primary"
|
||||||
>
|
>
|
||||||
<FaGithub className="w-6 h-6" />
|
<FaGithub className="w-6 h-6" />
|
||||||
</Link>
|
</Link>
|
||||||
@ -195,7 +195,7 @@ export const Options: FunctionComponent = () => {
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
to="https://x.com/realGhostChain"
|
to="https://x.com/realGhostChain"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-base hover:text-accent"
|
className="text-base text-white hover:text-primary"
|
||||||
>
|
>
|
||||||
<FaXTwitter className="w-6 h-6" />
|
<FaXTwitter className="w-6 h-6" />
|
||||||
</Link>
|
</Link>
|
||||||
@ -203,7 +203,7 @@ export const Options: FunctionComponent = () => {
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
to="https://discord.com/invite/CvYP7vrqN3"
|
to="https://discord.com/invite/CvYP7vrqN3"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-base hover:text-accent"
|
className="text-base text-white hover:text-primary"
|
||||||
>
|
>
|
||||||
<FaDiscord className="w-6 h-6" />
|
<FaDiscord className="w-6 h-6" />
|
||||||
</Link>
|
</Link>
|
||||||
@ -211,7 +211,7 @@ export const Options: FunctionComponent = () => {
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
to="https://t.me/realGhostChain"
|
to="https://t.me/realGhostChain"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-base hover:text-accent"
|
className="text-base text-white hover:text-primary"
|
||||||
>
|
>
|
||||||
<FaTelegramPlane className="w-6 h-6" />
|
<FaTelegramPlane className="w-6 h-6" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -1,54 +1,49 @@
|
|||||||
import { HashRouter, Routes, Route } from "react-router-dom"
|
import { HashRouter, Routes, Route } from "react-router-dom"
|
||||||
import { lazy, Suspense } from "react"
|
import { lazy, Suspense } from "react"
|
||||||
|
|
||||||
import { ProtectedRoute } from "./WalletPopup/components"
|
import { ChainProvider, AccountsProvider } from "./WalletPopup/hooks"
|
||||||
import { KeyringProvider } from "./WalletPopup/hooks"
|
|
||||||
import { EmptyPage } from "./EmptyPage"
|
import { EmptyPage } from "./EmptyPage"
|
||||||
|
|
||||||
// Dynamic Imports for code splitting
|
// Dynamic Imports for code splitting
|
||||||
const UnlockKeyring = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.UnlockKeyring })))
|
|
||||||
const SignRequest = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.SignRequest })))
|
const SignRequest = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.SignRequest })))
|
||||||
const Settings = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.Settings })))
|
const Settings = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.Settings })))
|
||||||
const ChangePassword = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.ChangePassword })))
|
|
||||||
const Welcome = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.Welcome })))
|
const Welcome = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.Welcome })))
|
||||||
const AddAccount = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.AddAccount })))
|
const AddAccount = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.AddAccount })))
|
||||||
|
const DeriveAccount = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.DeriveAccount })))
|
||||||
|
const DeleteAccount = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.DeleteAccount })))
|
||||||
const SwitchAccount = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.SwitchAccount })))
|
const SwitchAccount = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.SwitchAccount })))
|
||||||
|
const SwitchNetwork = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.SwitchNetwork })))
|
||||||
const ImportAccounts = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.ImportAccounts })))
|
const ImportAccounts = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.ImportAccounts })))
|
||||||
|
const ExportAccounts = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.ExportAccounts })))
|
||||||
const AccountDetails = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.AccountDetails })))
|
const AccountDetails = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.AccountDetails })))
|
||||||
const AddChainByUser = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.AddChainByUser })))
|
|
||||||
const Accounts = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.Accounts })))
|
const Accounts = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.Accounts })))
|
||||||
const DeleteWallet = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.DeleteWallet })))
|
|
||||||
const Options = lazy(() => import("./Options").then(module => ({ default: module.Options })))
|
const Options = lazy(() => import("./Options").then(module => ({ default: module.Options })))
|
||||||
const CreatePassword = lazy(() => import("./WalletPopup/pages/CreatePassword").then(module => ({ default: module.CreatePassword })))
|
|
||||||
const Networks = lazy(() => import("./WalletPopup/pages/Networks").then(module => ({ default: module.Networks })))
|
|
||||||
|
|
||||||
export const WalletPopup = () => (
|
export const WalletPopup = () => (
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<KeyringProvider>
|
<ChainProvider>
|
||||||
<Suspense fallback={<EmptyPage />}>
|
<AccountsProvider>
|
||||||
<Routes>
|
<Suspense fallback={<EmptyPage />}>
|
||||||
<Route element={<ProtectedRoute />}>
|
<Routes>
|
||||||
<Route path="/" element={<Accounts />} />
|
<Route path="/" element={<Accounts />} />
|
||||||
|
<Route path="/welcome" element={<Welcome />} />
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
<Route path="/change-password" element={<ChangePassword />} />
|
<Route path="/options" element={<Options />} />
|
||||||
<Route path="/delete-wallet" element={<DeleteWallet />} />
|
|
||||||
|
|
||||||
<Route path="/accounts/:accountId" element={<AccountDetails />} />
|
|
||||||
<Route path="/accounts" element={<Accounts />} />
|
<Route path="/accounts" element={<Accounts />} />
|
||||||
<Route path="/accounts/add" element={<AddAccount />} />
|
<Route path="/add-account" element={<AddAccount />} />
|
||||||
<Route path="/accounts/switch" element={<SwitchAccount />} />
|
<Route path="/derive-account" element={<DeriveAccount />} />
|
||||||
<Route path="/accounts/import" element={<ImportAccounts />} />
|
<Route path="/switch-account" element={<SwitchAccount />} />
|
||||||
|
<Route path="/import-account" element={<ImportAccounts />} />
|
||||||
|
<Route path="/export-accounts" element={<ExportAccounts />} />
|
||||||
|
<Route path="/switch-network" element={<SwitchNetwork />} />
|
||||||
|
|
||||||
|
<Route path="/account-details/:derivation?" element={<AccountDetails />} />
|
||||||
|
<Route path="/delete-account/:derivation?" element={<DeleteAccount />} />
|
||||||
<Route path="/sign-request/:signRequestId" element={<SignRequest />} />
|
<Route path="/sign-request/:signRequestId" element={<SignRequest />} />
|
||||||
<Route path="/add-chain-by-user" element={<AddChainByUser />} />
|
</Routes>
|
||||||
</Route>
|
</Suspense>
|
||||||
<Route path="/options" element={<Options />} />
|
</AccountsProvider>
|
||||||
<Route path="/welcome" element={<Welcome />} />
|
</ChainProvider>
|
||||||
<Route path="/networks" element={<Networks />} />
|
|
||||||
<Route path="/create-password" element={<CreatePassword />} />
|
|
||||||
<Route path="/unlock-keyring" element={<UnlockKeyring />} />
|
|
||||||
</Routes>
|
|
||||||
</Suspense>
|
|
||||||
</KeyringProvider>
|
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -12,15 +12,16 @@ export const decodeCallData = async (chainId: string, callData: string) => {
|
|||||||
if (!chain) throw new Error("unknown chain")
|
if (!chain) throw new Error("unknown chain")
|
||||||
const client = createClient(chain.provider)
|
const client = createClient(chain.provider)
|
||||||
try {
|
try {
|
||||||
const metadata = await new Promise((resolve, reject) =>
|
const metadata = await new Promise<string>((resolve, reject) =>
|
||||||
client._request("state_getMetadata", [], {
|
client._request("state_getMetadata", [], {
|
||||||
onSuccess: resolve,
|
onSuccess: (result) => resolve(result as string),
|
||||||
onError: reject,
|
onError: reject,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then(r => r)
|
|
||||||
.catch(_ => undefined)
|
.catch(_ => undefined)
|
||||||
|
|
||||||
|
if (!metadata) return undefined
|
||||||
|
|
||||||
const convertedMetadata = unifyMetadata(decAnyMetadata(metadata))
|
const convertedMetadata = unifyMetadata(decAnyMetadata(metadata))
|
||||||
return getViewBuilder(getLookupFn(convertedMetadata)).callDecoder(callData)
|
return getViewBuilder(getLookupFn(convertedMetadata)).callDecoder(callData)
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -12,13 +12,14 @@ const navItems = [
|
|||||||
{
|
{
|
||||||
name: "networks",
|
name: "networks",
|
||||||
icon: Globe,
|
icon: Globe,
|
||||||
to: "/networks",
|
to: "ui/assets/wallet-popup.html#/options",
|
||||||
|
isExternal: true
|
||||||
},
|
},
|
||||||
{ name: "add", icon: Plus, to: "/accounts/add" },
|
{ name: "add", icon: Plus, to: "/add-account" },
|
||||||
{
|
{
|
||||||
name: "import",
|
name: "import",
|
||||||
icon: Download,
|
icon: Download,
|
||||||
to: "/accounts/import",
|
to: "/import-account",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
@ -29,7 +30,7 @@ const navItems = [
|
|||||||
|
|
||||||
export type BottomNavBarProps = {
|
export type BottomNavBarProps = {
|
||||||
className?: string
|
className?: string
|
||||||
currentItem: NavItemName
|
currentItem?: NavItemName
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BottomNavBar: React.FC<BottomNavBarProps> = ({
|
export const BottomNavBar: React.FC<BottomNavBarProps> = ({
|
||||||
@ -38,23 +39,45 @@ export const BottomNavBar: React.FC<BottomNavBarProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleNavigation = (item: typeof navItems[number]) => {
|
||||||
|
if ('isExternal' in item && item.isExternal) {
|
||||||
|
const fullUrl = chrome.runtime.getURL(item.to)
|
||||||
|
window.open(fullUrl, "_blank", "noopener,noreferrer")
|
||||||
|
} else {
|
||||||
|
navigate(item.to)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={cn("p-4 bg-background", className)}>
|
<nav className={cn("p-2 bg-background border-t border-white/5", className)}>
|
||||||
<div className="flex justify-around">
|
<div className="flex w-full justify-between items-center">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => {
|
||||||
<Button
|
const isSelected = currentItem === item.name
|
||||||
key={item.name}
|
|
||||||
variant="ghost"
|
return (
|
||||||
onClick={() => navigate(item.to)}
|
<div
|
||||||
className={cn(
|
key={item.name}
|
||||||
"flex flex-col items-center py-2 hover:bg-accent hover:text-background",
|
className="flex flex-1 flex-col items-center justify-center cursor-pointer space-y-1"
|
||||||
currentItem === item.name ? "text-accent" : "text-primary",
|
onClick={() => handleNavigation(item)}
|
||||||
)}
|
>
|
||||||
>
|
<Button
|
||||||
<item.icon className="w-6 h-6 min-h-6" />
|
variant="secondary"
|
||||||
<span className="text-xs font-medium capitalize">{item.name}</span>
|
size="icon"
|
||||||
</Button>
|
className={cn(
|
||||||
))}
|
"w-16 h-14 rounded-xl border-none p-0 flex flex-col items-center justify-center transition-all duration-200",
|
||||||
|
isSelected
|
||||||
|
? "cursor-default bg-card text-white shadow-sm scale-105 hover:bg-card hover:text-white"
|
||||||
|
: "bg-transparent text-white/85 hover:bg-white/5 hover:text-white",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<item.icon className="w-5 h-5" />
|
||||||
|
<span className="text-[10px] font-semibold tracking-wide capitalize">
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -24,6 +24,7 @@ type Props = {
|
|||||||
export const DecodedCallData = ({ chainId, callData }: Props) => {
|
export const DecodedCallData = ({ chainId, callData }: Props) => {
|
||||||
const {
|
const {
|
||||||
data: decodedCallData,
|
data: decodedCallData,
|
||||||
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
} = useSWR([chainId, callData], ([chainId, callData]) =>
|
} = useSWR([chainId, callData], ([chainId, callData]) =>
|
||||||
decodeCallData(chainId, callData),
|
decodeCallData(chainId, callData),
|
||||||
@ -31,24 +32,40 @@ export const DecodedCallData = ({ chainId, callData }: Props) => {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<div className="w-full h-[160px] min-h-[160px] max-h-[160px] rounded-md bg-input px-4 py-3 text-white text-xs font-mono flex items-center justify-center animate-pulse border border-white/5 shadow-inner">
|
||||||
readOnly
|
<div className="flex items-center gap-2">
|
||||||
aria-label="Call Data Loading"
|
<span className="inline-block w-2 h-2 rounded-full bg-primary animate-ping"></span>
|
||||||
type="text"
|
Trying to decode calldata, wait for it...
|
||||||
className="pr-10"
|
</div>
|
||||||
placeholder={isLoading ? "trying to decode..." : "decoding failed"}
|
</div>
|
||||||
/>
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !decodedCallData) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-[160px] min-h-[160px] max-h-[160px] rounded-md border border-destructive/30 bg-destructive/10 p-4 text-xs text-destructive overflow-y-auto custom-vertical-scrollbar break-all shadow-inner">
|
||||||
|
<span className="font-bold block mb-1 uppercase tracking-wide text-red-400">Decoding failed</span>
|
||||||
|
<p className="text-white/80 leading-relaxed">
|
||||||
|
{error?.message || "The call data could not be parsed with the current chain metadata."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-input bg-background px-3 py-2 text-muted-foreground">
|
<div
|
||||||
|
className="w-full h-[160px] min-h-[160px] max-h-[160px] rounded-md bg-input px-4 py-3 text-white overflow-y-auto overflow-x-auto custom-vertical-scrollbar custom-horizontal-scrollbar shadow-inner"
|
||||||
|
style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)' }}
|
||||||
|
>
|
||||||
<div className="text-xs font-semibold">Pallet Call</div>
|
<div className="text-xs font-semibold">Pallet Call</div>
|
||||||
<div className="px-2">
|
<div className="px-2 whitespace-nowrap">
|
||||||
{decodedCallData!.pallet.value.name} {decodedCallData!.call.value.name}
|
{decodedCallData?.pallet?.value?.name} :: {decodedCallData?.call?.value?.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs font-semibold">Args</div>
|
<div className="text-xs font-semibold">Args</div>
|
||||||
<DecodedValue value={decodedCallData!.args.value} />
|
<div className="min-w-0 w-full text-xs leading-relaxed">
|
||||||
|
<DecodedValue value={decodedCallData.args.value} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -78,16 +95,16 @@ const DecodedPrimitiveValue = ({ value }: DecodedPrimitiveProps) => {
|
|||||||
case "char":
|
case "char":
|
||||||
case "str":
|
case "str":
|
||||||
case "Bytes": {
|
case "Bytes": {
|
||||||
return <div>{value.value}</div>
|
return <div className="whitespace-nowrap text-xs">{value.value}</div>
|
||||||
}
|
}
|
||||||
case "AccountId": {
|
case "AccountId": {
|
||||||
return <div>{value.value.address}</div>
|
return <div className="whitespace-nowrap text-xs">{value.value.address}</div>
|
||||||
}
|
}
|
||||||
case "bitSequence": {
|
case "bitSequence": {
|
||||||
// FIXME: improve bitsequence UI
|
// FIXME: improve bitsequence UI
|
||||||
// for example: n1,n2,...,n
|
// for example: n1,n2,...,n
|
||||||
return (
|
return (
|
||||||
<div>BitSequence: {toHex(value.value.bytes)}({value.value.bitsLen})</div>
|
<div className="whitespace-nowrap text-xs">BitSequence: {toHex(value.value.bytes)}({value.value.bitsLen})</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case "compactBn":
|
case "compactBn":
|
||||||
@ -116,7 +133,7 @@ type DecodedComplexProps = {
|
|||||||
const DecodedComplexValue = ({ value }: DecodedComplexProps) => {
|
const DecodedComplexValue = ({ value }: DecodedComplexProps) => {
|
||||||
switch (value.codec) {
|
switch (value.codec) {
|
||||||
case "BytesArray": {
|
case "BytesArray": {
|
||||||
return <div>{value.value}</div>
|
return <div className="whitespace-nowrap text-xs">{value.value}</div>
|
||||||
}
|
}
|
||||||
case "Tuple":
|
case "Tuple":
|
||||||
case "Sequence":
|
case "Sequence":
|
||||||
@ -126,7 +143,7 @@ const DecodedComplexValue = ({ value }: DecodedComplexProps) => {
|
|||||||
case "Enum": {
|
case "Enum": {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="text-xs">{value.value.type}</div>
|
<div className="whitespace-nowrap text-xs">{value.value.type}</div>
|
||||||
<DecodedValue value={value.value.value} />
|
<DecodedValue value={value.value.value} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@ -141,7 +158,7 @@ const DecodedComplexValue = ({ value }: DecodedComplexProps) => {
|
|||||||
case "Result": {
|
case "Result": {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="text-xs">{value.value.success ? "Ok" : "Error"}</div>
|
<div className="whitespace-nowrap text-xs">{value.value.success ? "Ok" : "Error"}</div>
|
||||||
<DecodedValue value={value.value.value} />
|
<DecodedValue value={value.value.value} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@ -149,13 +166,13 @@ const DecodedComplexValue = ({ value }: DecodedComplexProps) => {
|
|||||||
case "Struct": {
|
case "Struct": {
|
||||||
return Object.entries(value.value).map(([key, value]) => (
|
return Object.entries(value.value).map(([key, value]) => (
|
||||||
<>
|
<>
|
||||||
<div className="text-xs">{key}</div>
|
<div className="whitespace-nowrap text-xs">{key}</div>
|
||||||
<DecodedValue value={value} />
|
<DecodedValue value={value} />
|
||||||
</>
|
</>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return <pre className="text-xs">{jsonStringify(value)}</pre>
|
return <pre className="whitespace-nowrap text-xs">{jsonStringify(value)}</pre>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
302
src/containers/WalletPopup/components/JohnMcAffee.tsx
Normal file
302
src/containers/WalletPopup/components/JohnMcAffee.tsx
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
import { SVGProps, useEffect, useRef } from "react"
|
||||||
|
|
||||||
|
interface McAffeeProps extends SVGProps<SVGSVGElement> {
|
||||||
|
leftEyeRef: React.RefObject<SVGGElement | null>
|
||||||
|
rightEyeRef: React.RefObject<SVGGElement | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const JohnMcAffeeIcon = (props: SVGProps<SVGSVGElement>) => {
|
||||||
|
const leftEyeRef = useRef<SVGGElement>(null)
|
||||||
|
const rightEyeRef = useRef<SVGGElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const maxDistance = 10
|
||||||
|
|
||||||
|
const targetMouse = { x: 0, y: 0 }
|
||||||
|
const currentPos = { x: 0, y: 0 }
|
||||||
|
|
||||||
|
let isResetting = false
|
||||||
|
let isAwaitingMovement = false
|
||||||
|
let resetTimer: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
|
if (isResetting || isAwaitingMovement) {
|
||||||
|
isResetting = false
|
||||||
|
isAwaitingMovement = false
|
||||||
|
}
|
||||||
|
|
||||||
|
targetMouse.x = event.clientX
|
||||||
|
targetMouse.y = event.clientY
|
||||||
|
|
||||||
|
if (resetTimer) clearTimeout(resetTimer)
|
||||||
|
resetTimer = setTimeout(() => {
|
||||||
|
triggerEyeReset()
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerEyeReset = () => {
|
||||||
|
if (!isAwaitingMovement) {
|
||||||
|
isResetting = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let animationFrameId: number
|
||||||
|
|
||||||
|
const updateAnimation = () => {
|
||||||
|
const eyeRefs = [leftEyeRef, rightEyeRef]
|
||||||
|
|
||||||
|
eyeRefs.forEach((ref) => {
|
||||||
|
const eyeElement = ref.current
|
||||||
|
if (!eyeElement) return
|
||||||
|
|
||||||
|
const rect = eyeElement.getBoundingClientRect()
|
||||||
|
const centerX = rect.left + rect.width / 2
|
||||||
|
const centerY = rect.top + rect.height / 2
|
||||||
|
|
||||||
|
let targetX = 0
|
||||||
|
let targetY = 0
|
||||||
|
|
||||||
|
if (isResetting || isAwaitingMovement) {
|
||||||
|
targetX = 0
|
||||||
|
targetY = 0
|
||||||
|
} else {
|
||||||
|
const dx = targetMouse.x - centerX
|
||||||
|
const dy = targetMouse.y - centerY
|
||||||
|
const angle = Math.atan2(dy, dx)
|
||||||
|
const distance = Math.hypot(dx, dy)
|
||||||
|
|
||||||
|
const limitedDistance = Math.min(distance * 0.2, maxDistance)
|
||||||
|
targetX = Math.cos(angle) * limitedDistance
|
||||||
|
targetY = Math.sin(angle) * limitedDistance
|
||||||
|
}
|
||||||
|
|
||||||
|
const speed = 0.12
|
||||||
|
|
||||||
|
currentPos.x += (targetX - currentPos.x) * speed
|
||||||
|
currentPos.y += (targetY - currentPos.y) * speed
|
||||||
|
|
||||||
|
eyeElement.setAttribute("transform", `translate(${currentPos.x}, ${currentPos.y})`)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isResetting) {
|
||||||
|
const distanceToResetTarget = Math.hypot(currentPos.x - 0, currentPos.y - 0)
|
||||||
|
if (distanceToResetTarget < 0.1) {
|
||||||
|
isResetting = false
|
||||||
|
isAwaitingMovement = true
|
||||||
|
if (resetTimer) clearTimeout(resetTimer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(updateAnimation)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", handleMouseMove)
|
||||||
|
animationFrameId = requestAnimationFrame(updateAnimation)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove)
|
||||||
|
cancelAnimationFrame(animationFrameId)
|
||||||
|
if (resetTimer) clearTimeout(resetTimer)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg id="uuid-4e9813cc-0f70-4e29-b165-d3f414ddd744" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" viewBox="0 0 560.66 697.07">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="uuid-356277a2-684a-423f-8fda-deeb094c4c1d" x1="442.98" y1="293.02" x2="536.89" y2="293.02" gradientTransform="translate(-280 -3.23)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".5" stop-color="#d7e1eb"/>
|
||||||
|
<stop offset=".5" stop-color="#6284a6"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="uuid-36a2373a-4596-4657-92d9-89a8320b8f0f" x1="581.69" y1="296.15" x2="675.59" y2="296.15" gradientTransform="translate(-280 -6.36)" xlinkHref="#uuid-356277a2-684a-423f-8fda-deeb094c4c1d"/>
|
||||||
|
</defs>
|
||||||
|
<g id="uuid-7ad84f25-d803-44fa-8b79-cad3d6e81c47" data-name="Layer 1">
|
||||||
|
<g id="uuid-12a87b54-2a87-45c5-9183-606d7f6eea97" ref={leftEyeRef} data-name="Layer 4">
|
||||||
|
<rect x="162.98" y="259.38" width="93.9" height="60.82" transform="translate(419.87 579.58) rotate(180)" fill="url(#uuid-356277a2-684a-423f-8fda-deeb094c4c1d)"/>
|
||||||
|
<g id="uuid-3b47bebe-f720-4485-b629-4c3126954722" data-name="Left Pupil">
|
||||||
|
<path d="m209.64,298.48c-6.03,0-10.93-4.9-10.93-10.93s4.9-10.93,10.93-10.93,10.93,4.9,10.93,10.93-4.9,10.93-10.93,10.93Z" fill="#fff"/>
|
||||||
|
<path d="m209.64,279.12c4.66,0,8.43,3.77,8.43,8.43s-3.77,8.43-8.43,8.43-8.43-3.77-8.43-8.43,3.77-8.43,8.43-8.43m0-5c-7.41,0-13.43,6.02-13.43,13.43s6.02,13.43,13.43,13.43,13.43-6.02,13.43-13.43-6.02-13.43-13.43-13.43h0Z" fill="#2c5377"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="uuid-08be0013-800d-4526-a368-aef079169a72" ref={rightEyeRef} data-name="Layer 3">
|
||||||
|
<rect x="301.69" y="259.38" width="93.9" height="60.82" fill="url(#uuid-36a2373a-4596-4657-92d9-89a8320b8f0f)"/>
|
||||||
|
<g id="uuid-cc5b3724-49ef-4db0-8a5d-cd61a0e51137" data-name="Right Pupil">
|
||||||
|
<path d="m349.81,298.04c-6.03,0-10.93-4.9-10.93-10.93s4.9-10.93,10.93-10.93,10.93,4.9,10.93,10.93-4.9,10.93-10.93,10.93Z" fill="#fff"/>
|
||||||
|
<path d="m349.81,278.68c4.66,0,8.43,3.77,8.43,8.43s-3.77,8.43-8.43,8.43-8.43-3.77-8.43-8.43,3.77-8.43,8.43-8.43m0-5c-7.41,0-13.43,6.02-13.43,13.43s6.02,13.43,13.43,13.43,13.43-6.02,13.43-13.43-6.02-13.43-13.43-13.43h0Z" fill="#2c5377"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="m406.95,497.02c.16.28.22.57.19.89-1.03,13.75-2.06,27.49-3.1,41.24-1.82,1.09-4.13,1.78-5.38,3.33-5.05,6.26-9.67,12.86-14.64,19.19-18.08,23.02-36.32,45.92-54.27,69.04-2.98,3.84-6.12,4.78-10.65,4.19-7.23-.93-14.55-1.22-21.81-1.96-8-.82-15.99-1.74-23.96-2.8-12.65-1.68-25.37-4.4-37.02,3.77-2.52,1.77-4.47,1.07-6.32-1.36-4.93-6.49-10.05-12.82-15.08-19.24-16.72-21.32-33.32-42.72-50.23-63.88-2.95-3.69-7.19-6.35-10.83-9.48-.58-2.01-1.59-3.99-1.67-6.02-.49-11.67-.77-23.35-1.11-35.03-.33-12.94-.66-25.88-.99-38.83-.67-1.33-1.34-2.67-2.02-4,0,0,.02.04.02.04-1.34-8.99-2.61-17.99-4.07-26.96-.83-5.1-1.99-10.15-3.01-15.23-.97-7.93-1.94-15.86-2.91-23.79l-1.97-11.89c-.03-1.04-.06-2.09-.09-3.13-1.32-10.31-2.63-20.62-3.95-30.93.33-3.38.65-6.76.98-10.14,1.84,3.6,3.68,7.21,4.99,9.78,4.37-.26,7.97-.47,11.56-.68,0,0,.42.22.42.22l.42-.23c0,.63.02,1.26.03,1.89.49.67.98,1.35,1.47,2.02-.37.91-1.17,1.88-1.05,2.73.94,6.97,2,13.93,3.12,20.88,1.09,6.75,2.31,13.48,3.48,20.23.37,1.96.74,3.93,1.11,5.89-.04.65-.21,1.32-.09,1.94,4.68,25.18,9.44,50.35,14.06,75.55,1.58,8.59,2.84,17.24,4.25,25.87-1.69-1.92-3.64-3.68-5.04-5.8-5.26-7.97-10.34-16.06-15.48-24.1-.38.19-.76.39-1.13.58.62,1.39,1.17,2.81,1.87,4.15,3.61,6.93,7.66,13.67,10.82,20.8,8.05,18.18,19.18,34.51,30.18,50.89,4.42,6.58,7.82,14.24,16.52,17.21,8.46,2.9,16.65,6.66,24.81,10.35,13.13,5.94,26.41,10.65,41.25,8.92,5.09-.59,10.32.07,15.47-.16,2.41-.11,4.94-.64,7.15-1.58,14.97-6.39,29.89-12.89,44.75-19.53,1.81-.81,3.54-2.42,4.63-4.1,4.11-6.35,7.77-13,11.91-19.33,5.41-8.26,11.61-16.04,16.58-24.55,5.77-9.88,10.66-20.28,15.74-30.55,2.63-5.32,4.84-10.86,7.23-16.3h0c.34-.66.68-1.34,1.02-2.01,0,0-.03.03-.03.03.34-.34.68-.68,1.02-1.02h-.02c.17,4.67.4,9.33.51,13.99.16,6.33.23,12.66.35,18.99Z" fill="#2c5377"/>
|
||||||
|
<path d="m153.85,539.95c3.64,3.14,7.88,5.79,10.83,9.48,16.91,21.16,33.51,42.57,50.23,63.88,5.03,6.41,10.15,12.75,15.08,19.24,1.85,2.43,3.8,3.13,6.32,1.36,11.65-8.18,24.36-5.46,37.02-3.77,7.97,1.06,15.96,1.99,23.96,2.8,7.26.74,14.57,1.03,21.81,1.96,4.53.58,7.67-.35,10.65-4.19,17.95-23.12,36.19-46.02,54.27-69.04,4.97-6.33,9.59-12.93,14.64-19.19,1.25-1.55,3.56-2.24,5.38-3.33-.95,11.02-1.77,22.05-2.88,33.05-1.43,14.21-2.98,28.41-4.66,42.59-2.07,17.45-4.34,34.88-6.52,52.31-1.27-.33-2.65-.45-3.79-1.03-18.41-9.29-36.85-18.53-55.13-28.08-4.03-2.1-6.31-1.88-8.99,2-5.94,8.61-12.45,16.82-18.58,25.3-7.58,10.48-15,21.07-22.5,31.61-1.3.05-2.61.11-3.91.16-.79-1.61-1.35-3.39-2.4-4.81-13.02-17.56-26.26-34.96-39.07-52.66-2.64-3.65-4.57-3.61-8.11-1.77-11.48,5.97-23.14,11.59-34.68,17.43-8.28,4.19-16.48,8.51-24.72,12.78-.37-.07-.75-.15-1.12-.22-.05-2.32.08-4.66-.16-6.96-4.3-40.31-8.64-80.61-12.97-120.91Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m133.07,334.05c-.33,3.38-.65,6.76-.98,10.14-2.16-3.57-4.32-7.14-7.03-11.6-1.54,6.18-2.81,11.3-4.08,16.41-.34.06-.68.13-1.02.19,0,0-.02,0-.02,0,.07-6.98.37-13.97.15-20.95-.21-7.02-.35-14.12-1.58-21-1.6-8.94-4.22-17.7-6.49-26.51-.36-1.4-.87-3.21-1.93-3.9-4.25-2.8-8.73-5.24-13.13-7.82.4-5.76.63-11.54,1.25-17.27.91-8.28-3.27-15.52-4.45-23.37-1.4-9.33-4.09-18.57-4.37-27.91-.19-6.23,2.57-12.64,4.5-18.81,3.8-12.19,8.6-24.1,11.72-36.45,2.75-10.89,3.9-22.21,5.49-33.37.64-4.49,1.76-8.34,5.14-11.78,13.84-14.11,27.43-28.47,41.01-42.83,1.25-1.32,1.8-3.3,2.68-4.97,1.1.29,2.25.44,3.28.89,10.76,4.66,21.51,9.34,32.23,14.1,2.28,1.01,4.4,2.38,6.6,3.58-14.97,1.45-29.93,2.91-44.9,4.36-.03.46-.05.93-.08,1.39,12.07,8.13,24.14,16.26,36.21,24.4-4.32,2.65-8.53,5.5-12.97,7.91-20.37,11.07-40.82,22.01-61.21,33.07-.89.48-1.41,1.63-2.11,2.47v7.78c-1.31-.93-3.01-2.14-4.46-3.17-4.74,5.99.68,9.88,2.51,14.4-.33.32-.66.64-.99.96-3.4-1.31-6.8-2.61-10.6-4.07-.7,3.69-.25,8.18-2.28,10.5-4.1,4.7-5.57,9.44-4.31,14.41-4.88,2.63-4.11,7.28-6.29,11.69,2.6-.9,4.15-1.29,5.59-1.95,12.38-5.65,24.69-11.48,37.12-17,10.92-4.85,21.4-9.99,24.84-22.9.16-.61.75-1.09,1.07-1.67,4.28-7.82,8.55-15.65,12.82-23.47,1.34-.6,2.64-1.57,4.03-1.75,9.03-1.15,18.1-2.03,27.13-3.19,6.99-.9,13.93-2.08,20.9-3.13,2.61,1.08,5.22,2.16,7.83,3.24l.12-.04c.35.63.69,1.25,1.04,1.88.02.39.04.79.06,1.18-4.65,1.37-9.31,2.75-13.96,4.12-5.33,2.64-10.66,5.28-15.99,7.93-.63.13-1.35.12-1.89.42-15.91,8.99-32.45,17-46.49,28.95-2.75,2.35-4.78,5.55-7.14,8.36-.51.4-1.19.69-1.51,1.21-5.43,8.75-10.93,17.46-16.19,26.31-6.11,10.27-14.15,19.88-12.24,33.15.04.3-.27.63-.27.95-.02,4.35-.01,8.71-.01,14.25,4.47-4.85,8.02-8.73,11.6-12.58,4.73-5.08,9.49-10.13,14.24-15.2-1.51,5.94-3.1,11.86-4.51,17.82-2.39,10.15-5.88,20.22-6.72,30.51-1.29,15.82-.86,31.79-.85,47.7,0,4,1.22,8.01,1.87,12.01Z" fill="#183b5c"/>
|
||||||
|
<path d="m201.01,30.89c-1.5-1.81-2.82-3.82-4.53-5.4-2.24-2.06-4.78-3.77-8.34-6.51,12.16-.69,22.89-1.41,33.64-1.87,11.7-.5,23.4-.73,35.11-1.12,1.14-.04,2.27-.35,3.4-.54-3.49,16.87-6.98,33.74-10.66,51.52,1.56-.86,2.04-1.01,2.38-1.32,10.93-9.91,21.87-19.81,32.73-29.8,2.31-2.12,4.28-4.62,6.4-6.94.66-.02,1.32-.04,1.98-.06,1.04,1.93,1.72,4.22,3.19,5.72,7.15,7.29,7.32,7.76,2.59,16.84-1.31,2.51-2.72,4.96-4.07,7.44-4.24,7.83-8.47,15.66-13.37,24.73,2.59-1.16,3.88-1.68,5.13-2.3,12.37-6.15,24.7-12.38,37.13-18.41,2.32-1.12,5.04-1.42,7.57-2.1,14.58,5.16,29.17,10.31,44.8,15.84-15.78,6.68-30.36,12.85-45.67,19.32,17.93,7.04,34.76,13.64,51.59,20.25-.28,1.68-1.28,3.65-.74,4.99,6.81,16.66,13.84,33.23,20.81,49.82-7.33-5.38-14.65-10.76-21.98-16.14-4.84-2.82-9.61-5.78-14.54-8.43-9.3-4.99-18.69-9.8-28.05-14.68-4.2-1.59-8.32-3.45-12.61-4.7-3.54-1.02-7.29-1.37-10.96-1.93-8.96-1.35-17.93-2.65-26.9-3.97-1.97-.45-3.95-.91-5.92-1.36.02-.28,0-.56-.07-.83-.04-.49.08-1.09-.15-1.46-9.93-15.78-19.85-31.56-29.91-47.25-.75-1.16-2.54-1.65-3.85-2.45-1.39-1.79-2.52-3.91-4.22-5.32-10.57-8.72-21.2-17.39-31.99-25.84-2.98-2.34-6.59-3.87-9.92-5.77Z" fill="#183b5c"/>
|
||||||
|
<path d="m153.85,539.95c4.33,40.3,8.68,80.61,12.97,120.91.25,2.3.12,4.64.16,6.96-4.66-1.21-9.31-2.42-13.97-3.63h12.2c-16.91-35.11-33.74-70.06-51.16-106.24-10.71,28.57-20.88,55.56-30.86,82.62-.54,1.46.55,3.51.87,5.29-9.01-4.12-18.02-8.25-27.04-12.36-1.27-.58-2.6-1.01-3.9-1.52l.02.02-4.04-2,.02.02c-12.01-5.95-24.03-11.9-36.04-17.84-.3-.07-.61-.12-.92-.16-.36-.29-.71-.57-1.07-.86-.3-.08-.61-.13-.91-.15-.36-.28-.72-.56-1.08-.85-.31-.06-.62-.11-.93-.15-.36-.28-.71-.57-1.07-.85-.31-.07-.62-.12-.93-.15-2.06-1.66-4.12-3.31-6.19-4.97.37-.29.73-.58,1.1-.87,5.68-2.05,11.44-3.91,17.03-6.19,30.98-12.65,61.93-25.38,92.83-38.21,1.82-.76,3.59-2.42,4.64-4.13,9.26-15.15,18.33-30.41,27.49-45.62.83-1.38,1.83-2.66,2.75-3.99.17-.29.27-.6.29-.94.31-.36.62-.72.94-1.07l.02-.04c1.33-1.37,2.66-2.73,3.98-4.1.35,11.68.62,23.36,1.11,35.03.09,2.03,1.09,4.02,1.67,6.02Z" fill="#183b5c"/>
|
||||||
|
<path d="m456.78,236.1c.04.64.07,1.29.11,1.93-4.42,3.09-3.14,7.05-1.71,11.04.82,6.63,1.65,13.27,2.47,19.9-7.05,3.04-13.91,6.23-15.12,15.14-.6,4.42-1.04,8.9-2.11,13.21-3.85,15.49-3.53,31.24-3.25,47,.02,1.28.63,2.54.96,3.81l-1,1.07c-1.7-5.62-3.41-11.24-5.31-17.52-2.21,3.27-3.95,5.84-5.69,8.4-.48-2.78-.95-5.56-1.43-8.34.06-3.58.11-7.17.17-10.76-.03-2.27-.06-4.54-.1-6.81-.26-.71-.52-1.43-.78-2.14-.01-16.23,1.16-32.49-3.02-48.51-3.05-11.69-5.41-23.56-8.07-35.35,1.14.77,2.48,1.36,3.4,2.34,6.76,7.2,13.44,14.48,20.8,22.44,0-9.72.09-18.46-.08-27.19-.03-1.5-.97-3.11-1.82-4.46-6.49-10.4-12.96-20.82-19.66-31.08-3.2-4.9-6.91-9.46-10.39-14.18-.03-.99-.06-1.98-.09-2.98,1.01-1.22,2.69-2.32,2.92-3.67,1.22-7.12,2.08-14.3,3.06-21.46.47-12.22.95-24.43,1.42-36.65,1.31.83,2.81,1.47,3.89,2.54,6.96,6.82,13.76,13.78,20.74,20.57,3.15,3.06,6.57,5.83,9.87,8.73,6.03,17.64,12.25,35.23,17.92,52.99.97,3.03-.24,6.84-.74,10.25-1.24,8.56-2.6,17.11-4.06,25.63-.14.8-1.29,1.42-1.97,2.12-.9-.14-1.79-.27-2.69-.41.45.79.9,1.58,1.34,2.37Z" fill="#183b5c"/>
|
||||||
|
<path d="m84.08,645.87c-.33-1.78-1.41-3.83-.87-5.29,9.98-27.06,20.14-54.05,30.86-82.62,17.42,36.17,34.25,71.12,51.16,106.24h-12.2c-19.21-5.1-38.41-10.2-57.61-15.3-3.77-1-7.55-2.02-11.32-3.03Z" fill="#2c5377"/>
|
||||||
|
<path d="m406.04,663.83c-4.13.26-8.26.52-13.68.85,17.22-35.58,34.1-70.46,51.54-106.47,1.86,4.7,3.39,8.39,4.79,12.14,8.17,21.78,16.25,43.59,24.51,65.34,1.61,4.25,3.02,8.09-2.1,10.88-21.69,5.76-43.38,11.51-65.06,17.26Z" fill="#2c5377"/>
|
||||||
|
<path d="m331.29,60.8c-2.53.68-5.25.98-7.57,2.1-12.43,6.02-24.76,12.26-37.13,18.41-1.25.62-2.54,1.14-5.13,2.3,4.91-9.08,9.14-16.91,13.37-24.73,1.35-2.49,2.76-4.94,4.07-7.44,4.73-9.09,4.56-9.55-2.59-16.84-1.47-1.5-2.15-3.79-3.19-5.72,13.64-7.66,27.28-15.31,42.28-23.72-1.86,6.9-3.36,12.48-5.03,18.69,18.66,2.55,36.98,5.06,55.76,7.63-5.09,3.45-9.73,6.6-14.36,9.75-2.21.44-4.61.5-6.61,1.4-7.52,3.41-14.98,6.96-22.33,10.73-3.7,1.9-7.1,4.38-10.64,6.6-.3.29-.59.57-.89.86Z" fill="#6284a6"/>
|
||||||
|
<path d="m168.1,668.04c8.24-4.26,16.45-8.59,24.72-12.78,11.55-5.84,23.2-11.46,34.68-17.43,3.54-1.84,5.47-1.88,8.11,1.77,12.82,17.7,26.05,35.11,39.07,52.66,1.05,1.42,1.61,3.2,2.4,4.81-10.45-2.56-20.94-4.97-31.34-7.7-19.08-4.99-38.12-10.12-57.16-15.29-6.87-1.87-13.66-4.02-20.48-6.04Z" fill="#b6cadf"/>
|
||||||
|
<path d="m389.98,667.1c.08.28.13.56.14.85-11.26,3.16-22.5,6.41-33.79,9.46-23.64,6.39-47.3,12.7-70.96,18.99-1.4.37-2.92.34-4.38.5,7.49-10.54,14.92-21.13,22.5-31.61,6.13-8.48,12.64-16.7,18.58-25.3,2.68-3.88,4.96-4.11,8.99-2,18.28,9.55,36.72,18.78,55.13,28.08,1.15.58,2.52.7,3.79,1.03Z" fill="#b6cadf"/>
|
||||||
|
<path d="m424.7,331.73c.48,2.78.95,5.56,1.43,8.34-.75,4.39-1.5,8.79-2.24,13.18-1.26,8.26-2.52,16.51-3.78,24.77-.33,4.06-.66,8.11-.99,12.17-.64,8-1.28,16-1.91,24-1.02,8.47-2.41,16.91-2.95,25.41-.46,7.18-1.65,13.95-5.58,20.07-.86,1.46-1.72,2.93-2.58,4.39,0,0,.02-.01.02-.01-.34.34-.68.68-1.02,1.02,0,0,.03-.03.03-.03-.34.67-.68,1.35-1.02,2.02,0,0,0-.01,0-.01-2.71,4.09-5.34,8.23-8.15,12.26-4.85,6.97-9.81,13.88-14.72,20.81-.02-1-.2-2.02-.04-2.98,4.16-24.81,8.4-49.61,12.53-74.42,1.43-8.62,2.61-17.27,3.91-25.91.4-1.85.8-3.7,1.2-5.55,6.08-14.7,12.08-29.45,18.3-44.09,2.24-5.26,5.02-10.29,7.56-15.42Z" fill="#2c5377"/>
|
||||||
|
<path d="m201.01,30.89c3.32,1.9,6.94,3.43,9.92,5.77,10.79,8.45,21.42,17.12,31.99,25.84,1.71,1.41,2.83,3.53,4.22,5.32-6.44.43-12.88.85-19.32,1.28-8.59.58-17.18,1.16-25.77,1.74-2.2-1.2-4.32-2.57-6.6-3.58-10.72-4.76-21.47-9.44-32.23-14.1-1.03-.45-2.18-.6-3.28-.89,0,0,.05.02.05.02-.29-.13-.57-.26-.86-.4l-9.62-3.74c-.06-.4-.13-.79-.19-1.19,17.23-5.36,34.46-10.72,51.68-16.07Z" fill="#2c5377"/>
|
||||||
|
<path d="m446.98,143.13c-3.3-2.9-6.73-5.67-9.87-8.73-6.98-6.79-13.79-13.76-20.74-20.57-1.08-1.06-2.58-1.7-3.89-2.54-1.26-1.71-2.3-3.65-3.81-5.09-9.51-9.07-19.12-18.02-28.7-27.01.12-.11.24-.23.35-.36,10.9,3.4,21.79,6.83,32.71,10.18,3.67,1.13,7.41,2.03,11.12,3.03.38,0,.75,0,1.13,0,4.14,3.69,8.29,7.37,12.4,11.09.71.64,1.6,1.42,1.78,2.27,2.62,12.07,5.13,24.16,7.64,36.25.1.47-.07.99-.11,1.48Z" fill="#6284a6"/>
|
||||||
|
<path d="m291.14,28.93c-2.13,2.32-4.1,4.81-6.4,6.94-10.86,9.99-21.8,19.88-32.73,29.8-.34.31-.82.46-2.38,1.32,3.68-17.78,7.17-34.65,10.66-51.52,1.5-4.74,3-9.49,4.88-15.46,8.86,9.87,17.41,19.4,25.97,28.93Z" fill="#2c5377"/>
|
||||||
|
<path d="m332.18,59.94c3.54-2.22,6.94-4.7,10.64-6.6,7.35-3.77,14.81-7.32,22.33-10.73,2-.91,4.4-.96,6.61-1.4,9.83,3.59,19.7,7.08,29.44,10.89,1.58.62,2.89,2.47,3.84,4.04,5.03,8.34,9.91,16.77,14.84,25.17l.23-.22s-.25.2-.25.2c1.64,3.1,3.28,6.19,4.92,9.29-.32.25-.64.51-.96.76-13.25-10.58-26.43-21.27-39.85-31.63-2.11-1.63-5.4-2.61-8.08-2.49-14.58.66-29.15,1.77-43.72,2.72Z" fill="#2c5377"/>
|
||||||
|
<path d="m96.97,269.03c4.4,2.57,8.88,5.01,13.13,7.82,1.05.69,1.56,2.5,1.93,3.9,2.28,8.81,4.89,17.57,6.49,26.51,1.23,6.88,1.36,13.98,1.58,21,.21,6.97-.08,13.96-.15,20.94-5.29-4.38-10.57-8.75-15.86-13.13.62-5.81,1.24-11.63,1.86-17.44,3-10.2,5.07-20.28-3.93-28.86-.34-.33-.39-.95-.73-1.29-3.51-3.54-7.05-7.05-10.58-10.56.34-1.5.24-3.33,1.1-4.42,1.4-1.76,3.41-3.02,5.17-4.48Z" fill="#2c5377"/>
|
||||||
|
<path d="m438.14,348.12c-.33-1.27-.94-2.53-.96-3.81-.28-15.76-.61-31.51,3.25-47,1.07-4.31,1.51-8.79,2.11-13.21,1.21-8.91,8.07-12.1,15.12-15.14,2.06,1.42,4.13,2.85,6.19,4.27,0,0,.01-.06.01-.06.41.32.81.63,1.22.95.33.31.67.62,1,.92-.18.87-.36,1.73-.54,2.6-.13.48-.26.95-.4,1.42-.12.63-.24,1.25-.36,1.88-.43.1-.87.21-1.3.31-.11-.12-.26-.36-.34-.34-4.16,1.21-16.08,17.2-15.96,21.39,0,.16.01.33.05.49,1.27,5.07,2.54,10.13,3.82,15.19.37,5.7.74,11.4,1.1,17.1-2.7,2.64-5.39,5.27-8.09,7.91-1.98,1.7-3.95,3.4-5.93,5.11Z" fill="#2c5377"/>
|
||||||
|
<path d="m104.08,336.08c5.29,4.38,10.57,8.75,15.86,13.13,0,0,.02,0,.03,0-1.17,10.42,1.33,19.15,11.22,24.5.29.15.6.26.9.39l-.02-.02c.34.34.69.68,1.03,1.03l-.03-.04c.35.39.7.79,1.06,1.18.64-.37,1.27-.75,1.91-1.12.03,1.04.06,2.09.09,3.13-4.97.3-9.94.59-14.91.89-1.31-1.89-2.5-3.89-3.97-5.66-2.65-3.19-5.47-6.24-8.22-9.34-5.97-7.76-4.65-17.4-6.77-26.15.61-.64,1.21-1.27,1.82-1.91Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m109.03,364.14c2.75,3.11,5.57,6.15,8.22,9.34,1.47,1.76,2.65,3.76,3.97,5.66,1.11,1.99,1.73,4.91,3.41,5.8,4.23,2.22,8.94,3.53,13.46,5.2.97,7.93,1.94,15.86,2.91,23.79-.26.1-.54.14-.82.12-5.16-3.25-10.55-6.21-15.37-9.91-1.85-1.42-2.6-4.47-3.52-6.9-4.15-11.01-8.19-22.07-12.26-33.11Z" fill="#6284a6"/>
|
||||||
|
<path d="m417.21,414.19c.64-8,1.28-16,1.91-24,3.07-1.24,6.05-2.88,9.24-3.61,4.47-1.01,7.5-3.19,8.76-7.65.69-1.27,1.38-2.54,2.07-3.81,3.04-3.69,6.08-7.38,9.12-11.07-4.55,12.49-8.33,25.34-14.03,37.29-2.19,4.59-8.7,7.14-13.29,10.56-1.18.88-2.52,1.53-3.79,2.29Z" fill="#6284a6"/>
|
||||||
|
<path d="m90.7,277.93c3.53,3.52,7.07,7.02,10.58,10.56.34.34.39.96.73,1.29,9,8.58,6.93,18.65,3.93,28.85-.9-1.75-2.04-3.42-2.67-5.26-3.16-9.28-6.14-18.63-9.32-27.91-.55-1.61-1.73-3.01-2.62-4.51-.21-1.01-.42-2.01-.63-3.02Z" fill="#183b5c"/>
|
||||||
|
<path d="m91.32,280.95c.89,1.5,2.07,2.9,2.62,4.51,3.17,9.28,6.15,18.62,9.32,27.91.63,1.84,1.77,3.51,2.67,5.26-.62,5.82-1.24,11.63-1.86,17.44-.61.64-1.21,1.27-1.82,1.91-3.65-19.01-7.29-38.02-10.94-57.03Z" fill="#6284a6"/>
|
||||||
|
<path d="m438.14,348.12c1.98-1.7,3.95-3.4,5.93-5.11,3.72,4.06,8.91,7.39,7.07,14.22,0,.31-.05.61-.17.9-.28.35-.57.7-.85,1.05.01.32-.03.63-.14.94-.29.33-.57.66-.86,1,0,0,.07-.04.07-.04-3.11-.8-6.21-1.61-8.99-2.33-1.02,1.43-2.05,2.86-3.07,4.29-.02-4.3-.05-8.61-.07-12.91.04-.31.07-.63.08-.95l1-1.07Z" fill="#9db7d1"/>
|
||||||
|
<path d="m437.12,363.05c1.02-1.43,2.04-2.86,3.07-4.29,2.78.72,5.88,1.53,8.99,2.33-.29.99-.58,1.97-.87,2.96-3.04,3.69-6.08,7.38-9.12,11.07-1.08-1.05-2.27-2-3.21-3.17-1.09-1.35-1.95-2.87-2.91-4.32,1.35-1.53,2.7-3.06,4.06-4.59Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m451.13,357.24c1.84-6.83-3.35-10.16-7.07-14.22,2.7-2.64,5.39-5.27,8.09-7.91.59,2.64,1.18,5.28,1.77,7.91.09.34.19.69.28,1.03-.16.6-.33,1.2-.49,1.81-.86,3.8-1.72,7.59-2.58,11.38Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m140.18,414.05c.28.02.56-.02.82-.12,1.01,5.07,2.17,10.12,3.01,15.23,1.46,8.97,2.72,17.97,4.07,26.96-4.34-2.5-4-7.26-4.7-11.1-1.27-7-1.46-14.19-2.14-21.3-.31-3.23-.7-6.44-1.06-9.66Z" fill="#183b5c"/>
|
||||||
|
<path d="m406.09,464.06c.86-1.46,1.72-2.92,2.58-4.39-.57,12.45-1.15,24.9-1.72,37.35-.11-6.33-.19-12.66-.35-18.99-.12-4.66-.34-9.32-.51-13.97Z" fill="#183b5c"/>
|
||||||
|
<path d="m455.18,249.07c-1.43-3.99-2.71-7.96,1.71-11.04-.57,3.68-1.14,7.36-1.71,11.04Z" fill="#2c5377"/>
|
||||||
|
<path d="m456.78,236.1c-.45-.79-.9-1.58-1.34-2.37.9.14,1.79.27,2.69.41-.45.65-.9,1.31-1.34,1.96Z" fill="#2c5377"/>
|
||||||
|
<path d="m148.05,456.08c.67,1.33,1.34,2.67,2.02,4-.67-1.33-1.34-2.67-2.02-4Z" fill="#183b5c"/>
|
||||||
|
<path d="m449.11,361.13c.29-.33.57-.66.86-1-.29.33-.57.66-.86,1Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m425.26,92.04c-.38,0-.75,0-1.13,0-.16-.21-.26-.44-.32-.7.32-.25.64-.51.96-.76.16.49.33.97.49,1.46Z" fill="#2c5377"/>
|
||||||
|
<path d="m159.14,51.89c.29.13.57.26.86.4-.32-.05-.61-.19-.86-.4Z" fill="#183b5c"/>
|
||||||
|
<path d="m465.14,279.09c.13-.47.27-.94.4-1.42-.13.47-.27.94-.4,1.42Z" fill="#183b5c"/>
|
||||||
|
<path d="m450.11,359.2c.28-.35.57-.7.85-1.05-.28.35-.57.7-.85,1.05Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m381.24,500.12c4.91-6.93,9.87-13.83,14.72-20.81,2.8-4.03,5.44-8.17,8.15-12.26-2.4,5.44-4.6,10.98-7.23,16.3-5.08,10.27-9.97,20.67-15.74,30.55-4.97,8.51-11.17,16.29-16.58,24.55-4.15,6.33-7.8,12.98-11.91,19.33-1.09,1.68-2.82,3.29-4.63,4.1-14.86,6.64-29.78,13.14-44.75,19.53-2.21.95-4.74,1.47-7.15,1.58-5.15.24-10.38-.43-15.47.16-14.83,1.73-28.11-2.98-41.25-8.92-8.17-3.69-16.35-7.46-24.81-10.35-8.7-2.98-12.1-10.63-16.52-17.21-10.99-16.39-22.13-32.71-30.18-50.89-3.16-7.13-7.21-13.86-10.82-20.8-.7-1.34-1.25-2.77-1.87-4.15.38-.19.76-.39,1.13-.58,5.15,8.04,10.22,16.13,15.48,24.1,1.4,2.12,3.34,3.88,5.04,5.8,2.41,4.04,4.81,8.07,7.55,12.66,1.36-2.25,2.1-3.46,2.83-4.68,5.3-2.96,10.6-5.92,15.9-8.88,4.67-3.05,9.35-6.11,14.02-9.16.89.62,1.79,1.24,2.68,1.86-.55.54-1.45,1-1.58,1.63-3.05,14.09-6.05,28.19-8.93,42.31-.23,1.13.23,2.64.86,3.68,6.04,9.96,17.22,13.73,25.25,22.21,1.42-4.07,2.45-7.01,3.48-9.95,5.09,7.15,10.18,14.3,15.46,21.73,7.04-5.02,13.53-9.79,20.2-14.32,1.47-1,3.98-2.1,5.25-1.52,7.83,3.6,15.58,7.44,23.08,11.69,3.74,2.12,5.44,1.5,7.26-2.24,2.68-5.5,5.9-10.72,8.9-16.06,1.02,3.08,2.05,6.16,3.26,9.8,8.97-6.71,17.39-12.75,25.42-19.27,1.28-1.04,1.33-4.3,1.02-6.4-.51-3.39-1.91-6.64-2.72-9.99-2.59-10.73-5.09-21.48-7.62-32.22.08-.99.15-1.98.25-3.2,5.8,3.29,11.15,6.33,16.49,9.36,5.28,3.31,10.56,6.62,15.84,9.93.76,1.07,1.52,2.13,2.87,4.04,2.81-4.96,5.1-8.99,7.39-13.03Z" fill="#183b5c"/>
|
||||||
|
<path d="m147.67,177.93c2.36-2.81,4.39-6.01,7.14-8.36,14.04-11.95,30.58-19.96,46.49-28.95.54-.3,1.25-.29,1.89-.42-.68,1.09-1.17,2.37-2.06,3.25-9.24,9.22-18.65,18.28-27.78,27.62-2.79,2.85-5.94,6.08-6.98,9.7-1.18,4.13-.35,8.89-.12,13.37.9,17.33,3.31,34.76-4.07,51.43-.19.43-.08.98-.12,1.48-1.65,5.71-3.3,11.41-4.95,17.12,0,0,.04-.1.04-.1-.35.29-.7.58-1.05.87-2.26,8.9-4.52,17.8-6.85,26.97.87-.18,1.4-.15,1.75-.39,7.56-5.06,14.94-10.4,22.7-15.12,3.41-2.08,7.57-2.92,11.39-4.32l-.05-.05c-.81.97-1.65,1.92-2.43,2.91-6.01,7.6-12,15.22-18,22.83l1.34,1.7c5.26-1.6,10.53-3.2,15.79-4.8.82,8.65.78,8.76,9.03,11.15,8.72,2.53,17.51,4.8,26.27,7.19-7.93,1.28-15.87,2.56-23.8,3.85-6.01-4.09-11.94-8.31-18.09-12.17-1.09-.68-3.21-.25-4.65.24-5.97,2.02-11.77,4.59-17.82,6.31-4.5,1.28-4.73,3.63-4.3,7.69.84,8.04.86,16.16,1.22,24.24-3.59.21-7.19.42-11.56.68-1.31-2.57-3.15-6.18-4.99-9.78-.66-4-1.87-8.01-1.87-12.01-.02-15.91-.44-31.88.85-47.7.84-10.28,4.32-20.36,6.72-30.51,1.41-5.96,3-11.88,4.51-17.82.36-3.4.77-6.8,1.08-10.21,1.13-12.62,2.22-25.25,3.32-37.88Z" fill="#6284a6"/>
|
||||||
|
<path d="m149.62,343.16c-.36-8.09-.38-16.21-1.22-24.24-.43-4.07-.2-6.42,4.3-7.69,6.05-1.72,11.85-4.29,17.82-6.31,1.45-.49,3.56-.92,4.65-.24,6.15,3.86,12.08,8.08,18.09,12.17-6.07,1.17-12.14,2.33-18.21,3.5-.05.42-.1.84-.15,1.26,7.07,1.96,13.64,6.24,21.48,4.71,9.59-1.87,19.09-4.22,28.63-6.36,0,0,.19.19.19.19-1.99,5.61-3.99,11.22-5.98,16.83-1.14,2.57-2.18,5.19-3.43,7.71-5.33,10.68-10.62,21.37-16.13,31.95-2.23,4.28-4.97,8.28-7.48,12.41-6.39-6.72-12.77-13.46-19.18-20.16-6.99-7.29-14.01-14.55-21.01-21.83-.49-.67-.98-1.35-1.47-2.02,11.95-2.02,23.91-4.04,36.86-6.23-8.52-6.79-16.18-12.89-24.37-19.42-1.26,2.73-2.03,4.69-3.03,6.54-3.12,5.76-6.32,11.48-9.48,17.22l-.42.23-.42-.22Z" fill="#9db7d1"/>
|
||||||
|
<path d="m187.23,508.13c-.73,1.21-1.47,2.43-2.83,4.68-2.74-4.59-5.14-8.63-7.55-12.66-1.41-8.62-2.67-17.27-4.25-25.87-4.63-25.19-9.38-50.36-14.06-75.55-.12-.62.06-1.3.09-1.94.65.72,1.51,1.33,1.91,2.16,5.59,11.62,11.04,23.32,16.71,34.91,2.37,4.85,5.19,9.49,7.81,14.22,3.36,11.68,6.72,23.35,10.08,35.03-2.63,8.34-5.27,16.69-7.9,25.03Z" fill="#6284a6"/>
|
||||||
|
<path d="m185.05,448.08c-2.62-4.73-5.43-9.37-7.81-14.22-5.67-11.59-11.12-23.28-16.71-34.91-.4-.83-1.26-1.45-1.91-2.16-.37-1.96-.74-3.93-1.11-5.89,11.53.05,23.06.11,34.58.16.26.72.82,1.48.74,2.17-1.54,12.54-3.08,25.08-4.8,37.59-.79,5.78-1.98,11.51-2.99,17.27Z" fill="#9db7d1"/>
|
||||||
|
<path d="m192.1,391.06c-11.53-.05-23.05-.11-34.58-.16-1.17-6.74-2.39-13.48-3.48-20.23-1.13-6.95-2.18-13.91-3.12-20.88-.11-.85.68-1.82,1.05-2.73,7.01,7.27,14.03,14.53,21.01,21.83,6.42,6.7,12.79,13.44,19.18,20.16-.02.67-.04,1.34-.06,2.01Z" fill="#6284a6"/>
|
||||||
|
<path d="m150.46,343.15c3.17-5.74,6.37-11.46,9.48-17.22,1-1.85,1.78-3.81,3.03-6.54,8.2,6.53,15.85,12.63,24.37,19.42-12.96,2.19-24.91,4.21-36.86,6.23,0-.63-.02-1.26-.03-1.89Z" fill="#d7e1eb"/>
|
||||||
|
<path d="m136.03,375.12c-.64.37-1.27.75-1.91,1.12-.35-.39-.7-.79-1.06-1.18,0,0,.03.04.03.04-.34-.34-.69-.68-1.03-1.03,0,0,.02.02.02.02-.59-1.84-.99-3.77-1.79-5.51-3.03-6.56-6.2-13.06-9.31-19.58,1.27-5.12,2.54-10.23,4.08-16.41,2.7,4.46,4.87,8.03,7.03,11.6,1.32,10.31,2.63,20.62,3.95,30.93Z" fill="#6284a6"/>
|
||||||
|
<path d="m138.09,390.13c-4.52-1.67-9.23-2.98-13.46-5.2-1.69-.89-2.31-3.8-3.41-5.8,4.97-.3,9.94-.59,14.91-.89.66,3.96,1.31,7.93,1.97,11.89Z" fill="#183b5c"/>
|
||||||
|
<path d="m404.1,467.06c.34-.67.68-1.35,1.02-2.02-.34.67-.68,1.35-1.02,2.02Z" fill="#183b5c"/>
|
||||||
|
<path d="m405.09,465.07c.34-.34.68-.68,1.02-1.02-.34.34-.68.68-1.02,1.02Z" fill="#183b5c"/>
|
||||||
|
<path d="m162.07,247.04c.03-.5-.07-1.05.12-1.48,7.38-16.67,4.97-34.11,4.07-51.43-.23-4.47-1.07-9.24.12-13.37,1.04-3.62,4.19-6.85,6.98-9.7,9.13-9.33,18.53-18.39,27.78-27.62.89-.88,1.38-2.16,2.06-3.25,5.33-2.64,10.66-5.28,15.99-7.93.19,2.46.77,4.96.51,7.38-1.03,9.5-2.32,18.97-3.52,28.46-.9,4.71-1.88,9.41-2.68,14.14-1.85,10.93-3.61,21.88-5.41,32.81-10.14,6.98-20.29,13.94-30.4,20.96-5.23,3.63-10.4,7.35-15.6,11.03Z" fill="#9db7d1"/>
|
||||||
|
<path d="m202.05,70.84c8.59-.58,17.18-1.16,25.77-1.74,6.44-.43,12.88-.85,19.32-1.28,1.31.8,3.1,1.29,3.85,2.45,10.06,15.69,19.98,31.47,29.91,47.25.23.37.11.97.15,1.46-1.64-.96-3.3-1.89-4.91-2.9-23.02-14.36-46.02-28.73-69.05-43.07-.74-.46-1.68-.61-3.3-1.18.74,1.65,1.06,2.54,1.51,3.36,8.34,15.16,16.72,30.29,25.03,45.47.75,1.37,1.07,2.96,1.58,4.46-2.61-1.08-5.22-2.16-7.83-3.24-1.22-1.08-2.34-2.32-3.69-3.21-9.01-5.94-18.07-11.8-27.11-17.69-12.07-8.13-24.14-16.26-36.21-24.4.03-.46.05-.93.08-1.39,14.97-1.45,29.93-2.91,44.9-4.36Z" fill="#9db7d1"/>
|
||||||
|
<path d="m172.03,129.95c-4.27,7.83-8.54,15.65-12.82,23.47-.32.58-.91,1.06-1.07,1.67-3.44,12.91-13.92,18.05-24.84,22.9-12.44,5.52-24.74,11.35-37.12,17-1.44.66-2.99,1.05-5.59,1.95,2.18-4.41,1.41-9.07,6.29-11.69-1.26-4.97.21-9.71,4.31-14.41,2.03-2.32,1.58-6.81,2.28-10.5,3.8,1.46,7.2,2.76,10.6,4.07.33-.32.66-.64.99-.96-1.83-4.52-7.25-8.41-2.51-14.4,1.45,1.03,3.15,2.23,4.46,3.17,0-2.5,0-5.14,0-7.78,7.45-2.19,14.85-4.58,22.36-6.53,10.85-2.82,21.77-5.32,32.66-7.95Z" fill="#2c5377"/>
|
||||||
|
<path d="m231.92,125.13c-.52-1.49-.84-3.09-1.58-4.46-8.31-15.18-16.68-30.31-25.03-45.47-.45-.82-.78-1.72-1.51-3.36,1.62.57,2.56.72,3.3,1.18,23.03,14.34,46.03,28.72,69.05,43.07,1.61,1.01,3.27,1.93,4.91,2.9.07.27.09.55.07.83-.67.47-1.33.94-2,1.41-1.14.11-2.29.17-3.42.34-14.19,2.18-28.38,4.38-42.57,6.58-.02-.39-.04-.79-.06-1.18-.35-.63-.69-1.25-1.04-1.88l-.12.04Z" fill="#2c5377"/>
|
||||||
|
<path d="m172.03,129.95c-10.89,2.64-21.82,5.14-32.66,7.95-7.51,1.95-14.91,4.34-22.36,6.53.69-.84,1.22-1.98,2.11-2.47,20.38-11.06,40.83-21.99,61.21-33.07,4.44-2.42,8.65-5.26,12.97-7.91,9.04,5.89,18.1,11.75,27.11,17.69,1.35.89,2.47,2.13,3.69,3.21-6.97,1.05-13.91,2.24-20.9,3.13-9.03,1.16-18.1,2.04-27.13,3.19-1.4.18-2.69,1.15-4.03,1.75Z" fill="#6284a6"/>
|
||||||
|
<path d="m233.14,128.15c14.19-2.2,28.38-4.4,42.57-6.58,1.13-.17,2.28-.23,3.42-.34-.83,1.17-1.45,2.63-2.54,3.47-3.79,2.93-7.73,5.65-11.63,8.44-16.27,11.66-32.53,23.31-48.8,34.97,1.2-9.48,2.49-18.96,3.52-28.46.26-2.41-.31-4.92-.51-7.38,4.65-1.37,9.31-2.75,13.96-4.12Z" fill="#b6cadf"/>
|
||||||
|
<path d="m147.67,177.93c-1.1,12.63-2.19,25.25-3.32,37.88-.3,3.41-.72,6.81-1.08,10.21-4.75,5.06-9.5,10.12-14.23,15.19-3.58,3.85-7.12,7.72-11.6,12.58,0-5.55,0-9.9.01-14.25,0-.32.31-.65.27-.95-1.91-13.26,6.13-22.87,12.24-33.15,5.26-8.85,10.76-17.56,16.19-26.31.32-.52,1-.81,1.51-1.21Z" fill="#2c5377"/>
|
||||||
|
<path d="m120.98,349.01c3.12,6.52,6.28,13.02,9.31,19.58.8,1.74,1.21,3.67,1.79,5.51-.3-.13-.61-.23-.9-.39-9.89-5.35-12.4-14.08-11.22-24.5.34-.06.68-.13,1.02-.19Z" fill="#2c5377"/>
|
||||||
|
<path d="m232.04,125.09c.35.63.69,1.25,1.04,1.88-.35-.63-.69-1.25-1.04-1.88Z" fill="#9db7d1"/>
|
||||||
|
<path d="m216.17,168.1c16.27-11.66,32.54-23.31,48.8-34.97,3.89-2.79,7.84-5.51,11.63-8.44,1.09-.84,1.7-2.3,2.54-3.47.67-.47,1.33-.94,2-1.41,1.97.45,3.95.91,5.92,1.36.16,1.25-.16,3.02.55,3.66,7.36,6.63,14.93,13.03,22.35,19.6,5.67,5.01,11.08,10.32,16.83,15.23,3.56,3.04,7.54,5.6,11.33,8.38,2.98,15,5.97,30,8.95,45-5.71,2.2-11.48,4.23-17.11,6.63-10.31,4.39-20.5,9.08-30.81,13.46-6.53,2.78-13.15,5.37-19.85,7.74-1.66.59-3.94.48-5.6-.16-14.98-5.81-29.87-11.84-44.81-17.73-6.91-2.72-13.87-5.29-20.81-7.93,1.8-10.94,3.56-21.88,5.41-32.81.8-4.73,1.78-9.43,2.68-14.14Z" fill="#d7e1eb"/>
|
||||||
|
<path d="m423.81,91.34c.05.26.16.49.32.7-3.71-1-7.45-1.91-11.12-3.03-10.92-3.35-21.81-6.78-32.71-10.18-.24-.1-.48-.21-.72-.31.12.22.25.44.37.66,9.58,8.99,19.19,17.95,28.7,27.01,1.51,1.44,2.55,3.38,3.81,5.09-.47,12.22-.95,24.43-1.42,36.65-4.71-6.95-9.9-13.63-14-20.93-3.49-6.22-8.42-9.44-15.03-10.81-16.83-6.6-33.66-13.21-51.59-20.25,15.3-6.47,29.89-12.64,45.67-19.32-15.63-5.53-30.22-10.68-44.8-15.84.3-.29.59-.57.89-.86,14.57-.95,29.13-2.06,43.72-2.72,2.68-.12,5.96.86,8.08,2.49,13.42,10.36,26.59,21.05,39.85,31.63Z" fill="#9db7d1"/>
|
||||||
|
<path d="m405.06,173.07c.03.99.06,1.98.09,2.98,2.33,16.68,4.66,33.36,6.99,50.04.3.68.6,1.35.91,2.03l-.14.06c2.67,11.79,5.03,23.66,8.07,35.35,4.18,16.02,3.01,32.28,3.02,48.51-7.46.91-4.19-7.06-7.04-9.02-3.01,1.12-5.5,2.05-8,2.98-.31-3.2-.61-6.4-.92-9.6-.39-1.31-.78-2.62-1.18-3.93-2.59-9.06-5.19-18.12-7.78-27.18-.35-.4-.7-.79-1.05-1.19l.03.13c-1.97-6.73-3.95-13.45-5.92-20.18-.08-1.65-.02-3.32-.25-4.95-2.24-15.94-4.53-31.87-6.81-47.81-.17-1.79-.27-3.58-.52-5.36-1.46-10.35-2.97-20.69-4.45-31.04,7.33,5.38,14.65,10.76,21.98,16.14.99.68,1.98,1.37,2.97,2.05Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m338.11,168.05c-3.79-2.78-7.76-5.34-11.33-8.38-5.75-4.91-11.16-10.22-16.83-15.23-7.42-6.57-14.99-12.96-22.35-19.6-.71-.64-.39-2.41-.55-3.66,8.97,1.32,17.93,2.61,26.9,3.97,3.67.55,7.42.9,10.96,1.93,4.3,1.24,8.42,3.1,12.61,4.7-.11.43-.21.85-.32,1.28-3.75,1.58-2,4.74-1.81,7.19.75,9.28,1.78,18.54,2.71,27.81Z" fill="#b6cadf"/>
|
||||||
|
<path d="m337.2,133.05c.1-.43.21-.85.32-1.28,9.36,4.88,18.75,9.69,28.05,14.68,4.93,2.65,9.7,5.61,14.54,8.43,1.49,10.35,2.99,20.69,4.45,31.04.25,1.77.35,3.57.52,5.35-9.56-11.57-19.14-23.13-28.68-34.72-6.43-7.81-12.8-15.67-19.19-23.5Z" fill="#6284a6"/>
|
||||||
|
<path d="m405.06,173.07c-.99-.68-1.98-1.37-2.97-2.05-6.97-16.59-14-33.16-20.81-49.82-.55-1.34.45-3.31.74-4.99,6.62,1.37,11.55,4.59,15.03,10.81,4.09,7.3,9.28,13.98,14,20.93-.98,7.16-1.84,14.34-3.06,21.46-.23,1.35-1.91,2.45-2.92,3.67Z" fill="#2c5377"/>
|
||||||
|
<g>
|
||||||
|
<path d="m558.08,604.08c-3.09-1.3-6.18-2.59-9.27-3.89-.29.1-.53.03-.71-.22-7.03-2.9-14.06-5.8-21.08-8.7-.97-.43-1.94-.86-2.92-1.29-3.74-1.61-7.48-3.22-11.23-4.82-.64-.2-1.27-.39-1.91-.59-10.85-4.45-21.7-8.9-32.55-13.36-1.48-.61-2.93-1.29-4.4-1.93-.66-.36-1.32-.71-1.97-1.07-4.99-2.05-9.98-4.09-14.97-6.14-4.81-2.32-11.2-3.42-14.11-7.2-7.11-9.24-12.58-19.75-18.76-29.71-5.65-9.11-11.37-18.17-17.06-27.25-1.03,13.75-2.06,27.49-3.1,41.24-.95,11.02-1.77,22.05-2.88,33.05-1.43,14.21-2.98,28.41-4.66,42.59-2.07,17.45-4.34,34.88-6.52,52.31.08.28.13.56.14.85,5.31-1.37,10.61-2.75,15.92-4.12-4.13.26-8.26.52-13.68.85,17.22-35.58,34.1-70.46,51.54-106.47,1.86,4.7,3.39,8.39,4.79,12.14,8.17,21.78,16.25,43.59,24.51,65.34,1.61,4.25,3.02,8.09-2.1,10.88,3.93-1.54,7.94-2.93,11.79-4.66,9.24-4.16,18.47-8.36,27.59-12.78,12.21-5.92,24.32-12.08,36.47-18.13.38.02.76.05,1.13.07,0,0,.04-.05.04-.05-.01-.24-.02-.48-.03-.72,3.25-1.49,6.49-2.98,9.74-4.47.08-.59.16-1.17.23-1.76h0Z" fill="#183b5c"/>
|
||||||
|
<path d="m560.66,605.44l-.02-.84c-.85-.18-1.7-.35-2.56-.53-.08.59-.16,1.17-.23,1.76,0,0,0,0,0,0,0,0,0,.01,0,.02h0s0,0,0,0c.94-.14,1.88-.28,2.81-.41Z" fill="#183b5c"/>
|
||||||
|
</g>
|
||||||
|
<path d="m12.18,612.03c-.36-.29-.71-.57-1.07-.86.36.29.71.57,1.07.86Z" fill="#f7f8f3"/>
|
||||||
|
<path d="m10.2,611.02c-.36-.28-.72-.56-1.08-.85.36.28.72.56,1.08.85Z" fill="#f7f8f3"/>
|
||||||
|
<path d="m8.18,610.03c-.36-.28-.71-.57-1.07-.85.36.28.71.57,1.07.85Z" fill="#f7f8f3"/>
|
||||||
|
<path d="m146.12,504.12c.31-.36.62-.72.94-1.07-.31.36-.62.72-.94,1.07Z" fill="#f7f8f3"/>
|
||||||
|
<path d="m408.95,305.98c2.5-.93,4.99-1.86,8-2.97,2.85,1.96-.42,9.93,7.04,9.02.26.71.52,1.43.78,2.14-2.7,2.31-2.77,4.58.09,6.81-.05,3.59-.11,7.17-.17,10.76-2.54,5.13-5.33,10.16-7.56,15.42-6.22,14.65-12.22,29.39-18.3,44.09-9.74-.39-19.48-.75-29.21-1.21-1.23-.06-2.44-.64-3.66-.98,12.38-14.67,24.75-29.35,37.13-44.02.61-.51,1.21-1.01,1.82-1.52.13-.04.26-.08.39-.12-.13.03-.26.05-.4.08-.1-.44-.19-.87-.29-1.31,1.14-8.21,2.25-16.42,3.44-24.63.43-2.98-.21-4.76-3.48-5.97-6.69-2.48-13.12-5.69-19.72-8.43-1.16-.48-3.08-.71-3.92-.08-4.11,3.08-7.96,6.5-11.91,9.8-2.62.75-5.26,2.21-7.86,2.13-7.38-.24-14.74-1.11-22.11-1.73,0,0,.18-.3.18-.3,8.65-2.14,17.32-4.22,25.95-6.43,8.86-2.27,10.18-3.96,10.85-13.31,1.07-.37,2.13-.74,3.2-1.11,2.79,3.06,5.59,6.12,8.38,9.17,6.81,3.41,13.61,6.82,21.32,10.68v-5.96Z" fill="#6284a6"/>
|
||||||
|
<path d="m412.14,226.09c-2.33-16.68-4.66-33.36-6.99-50.04,3.48,4.71,7.19,9.28,10.39,14.18,6.71,10.26,13.17,20.68,19.66,31.08.85,1.35,1.79,2.96,1.82,4.46.17,8.73.08,17.47.08,27.19-7.36-7.96-14.04-15.24-20.8-22.44-.92-.98-2.26-1.57-3.4-2.34,0,0,.14-.06.14-.06-.3-.68-.6-1.35-.91-2.03Z" fill="#2c5377"/>
|
||||||
|
<path d="m437.13,349.19c-.01.32-.04.63-.08.95-.58.03-1.16.07-1.75.1-.8-.13-1.79,0-2.37-.43-4.88-3.62-7.8-2.34-9.05,3.45.75-4.39,1.5-8.79,2.24-13.18,1.74-2.57,3.47-5.13,5.69-8.4,1.91,6.28,3.61,11.9,5.31,17.52Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m424.87,320.97c-2.87-2.23-2.8-4.5-.09-6.81.03,2.27.06,4.54.09,6.81Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m381.24,500.12c-2.29,4.04-4.58,8.08-7.39,13.03-1.35-1.9-2.11-2.97-2.87-4.04-2.92-8.73-5.84-17.45-8.77-26.18.61-.61,1.57-1.12,1.78-1.85,1.42-5.07,2.76-10.16,3.97-15.29,1.48-6.22,2.81-12.47,4.2-18.71,1.03-1.17,2.4-2.17,3.03-3.53,6.31-13.67,12.46-27.41,18.77-41.08.93-2.03,2.44-3.79,3.68-5.68-1.29,8.64-2.47,17.3-3.91,25.91-4.13,24.82-8.37,49.61-12.53,74.42-.16.96.02,1.99.04,2.98Z" fill="#6284a6"/>
|
||||||
|
<path d="m397.64,396.8c-1.24,1.89-2.74,3.65-3.68,5.68-6.3,13.67-12.45,27.41-18.77,41.08-.62,1.35-2,2.36-3.03,3.52-.33-.55-.9-1.08-.96-1.66-1.73-15.42-3.47-30.84-5.06-46.27-.34-3.34-.13-6.73-.18-10.1,1.22.34,2.43.93,3.66.98,9.73.46,19.47.82,29.21,1.21-.4,1.85-.8,3.7-1.2,5.55Z" fill="#9db7d1"/>
|
||||||
|
<path d="m423.89,353.25c1.25-5.78,4.17-7.07,9.05-3.45.58.43,1.57.3,2.37.43-.95,2.41-1.78,4.88-2.86,7.23-2.71,5.91-5.54,11.76-8.32,17.64,0,0,.02-.05.02-.05-.35.51-.7,1.02-1.05,1.53.02.46.04.92.06,1.37-1.02.02-2.03.04-3.05.06,1.26-8.26,2.52-16.51,3.78-24.77Z" fill="#6284a6"/>
|
||||||
|
<path d="m420.11,378.02c1.02-.02,2.03-.04,3.05-.06,4.65.32,9.31.65,13.96.97-1.26,4.46-4.29,6.63-8.76,7.65-3.19.72-6.17,2.37-9.24,3.61.33-4.05.66-8.11.99-12.17Z" fill="#183b5c"/>
|
||||||
|
<path d="m463.48,281.27c-4.14,12.25-8.29,24.49-12.43,36.73-1.27-5.06-2.55-10.13-3.82-15.19-.04-.16-.05-.33-.05-.49-.11-4.19,11.81-20.18,15.96-21.39.07-.02.22.22.34.34Z" fill="#183b5c"/>
|
||||||
|
<path d="m132.07,374.07c.34.34.69.68,1.03,1.03-.34-.34-.69-.68-1.03-1.03Z" fill="#2c5377"/>
|
||||||
|
<path d="m133.07,375.05c.35.39.7.79,1.06,1.18-.35-.39-.7-.79-1.06-1.18Z" fill="#2c5377"/>
|
||||||
|
<path d="m437.12,378.93c-4.65-.32-9.31-.65-13.96-.97-.02-.46-.04-.92-.06-1.37.35-.51.7-1.02,1.05-1.53,0,0-.02.05-.02.05,2.98-2.49,5.96-4.98,8.94-7.47.96,1.45,1.82,2.97,2.91,4.32.94,1.16,2.13,2.12,3.21,3.17-.69,1.27-1.38,2.54-2.06,3.81Z" fill="#9db7d1"/>
|
||||||
|
<g>
|
||||||
|
<path d="m463.48,281.27c-4.14,12.25-8.29,24.49-12.43,36.73.37,5.7.74,11.4,1.1,17.1.59,2.64,1.18,5.28,1.77,7.91.29-.71.58-1.41.87-2.12.13-.62.26-1.23.39-1.85.2-1.8.3-3.62.6-5.41,2.98-17.57,6-35.13,9-52.69-.43.1-.87.21-1.3.31Z" fill="#6284a6"/>
|
||||||
|
<path d="m454.2,344.05c.2-1.05.39-2.1.59-3.14-.29.71-.58,1.41-.87,2.12.09.34.19.69.28,1.03Z" fill="#6284a6"/>
|
||||||
|
</g>
|
||||||
|
<path d="m433.07,367.64c-2.98,2.49-5.96,4.98-8.94,7.47,2.78-5.88,5.61-11.73,8.32-17.64,1.08-2.35,1.91-4.82,2.86-7.23.58-.03,1.17-.07,1.75-.1.02,4.3.05,8.61.07,12.91-1.35,1.53-2.7,3.06-4.06,4.59Z" fill="#2c5377"/>
|
||||||
|
<path d="m338.4,493.02c2.53,10.74,5.03,21.49,7.62,32.22.81,3.36,2.21,6.6,2.72,9.99.31,2.1.26,5.37-1.02,6.4-8.03,6.52-16.44,12.56-25.42,19.27-1.21-3.64-2.23-6.72-3.26-9.8-4.13-13.26-8.26-26.52-12.38-39.78-.38.09-.77.18-1.15.26-1.29,6.72-2.59,13.44-4,20.82-3.98-2.2-7.32-4.01-10.62-5.87-6.27-3.54-8.98-3.03-13.56,2.64-1.99,2.46-3.94,4.96-6.14,7.72-2.73-6.32-5.19-12.02-7.77-17.99-3.87,2.53-7.24,4.74-11.46,7.5v-14.28l-1.03-.16c-4.01,13.29-8.02,26.58-12.04,39.86-1.03,2.94-2.05,5.88-3.48,9.95-8.03-8.48-19.21-12.26-25.25-22.21-.63-1.04-1.09-2.55-.86-3.68,2.88-14.12,5.88-28.22,8.93-42.31.14-.63,1.04-1.09,1.58-1.63,3.03,10.27,6.07,20.54,9.23,31.23,7.88-5.17,15.48-10.15,23.08-15.13,5.24-1.98,10.83-3.34,15.65-6.07,8.2-4.64,15.74-4,23.83.09,4.45,2.25,9.51,3.29,14.29,4.87.11.48.05,1.21.35,1.41,7.32,4.98,14.67,9.91,22.39,15.09.6-1.9,1.03-3.29,1.48-4.68,2.75-8.58,5.51-17.16,8.27-25.74Z" fill="#6284a6"/>
|
||||||
|
<path d="m238.9,551.83c4.01-13.29,8.02-26.58,12.04-39.86l1.03.16v14.28c4.22-2.76,7.59-4.97,11.46-7.5,2.58,5.97,5.04,11.67,7.77,17.99,2.2-2.76,4.15-5.26,6.14-7.72,4.58-5.67,7.29-6.17,13.56-2.64,3.3,1.86,6.64,3.67,10.62,5.87,1.42-7.38,2.71-14.1,4-20.82.38-.09.77-.18,1.15-.26,4.13,13.26,8.26,26.52,12.38,39.78-2.99,5.34-6.22,10.57-8.9,16.06-1.82,3.73-3.52,4.35-7.26,2.24-7.5-4.25-15.25-8.09-23.08-11.69-1.28-.59-3.78.52-5.25,1.52-6.66,4.53-13.16,9.3-20.2,14.32-5.29-7.43-10.37-14.58-15.46-21.73Z" fill="#2c5377"/>
|
||||||
|
<path d="m338.4,493.02c-2.76,8.58-5.51,17.16-8.27,25.74-.44,1.39-.88,2.78-1.48,4.68-7.72-5.19-15.07-10.11-22.39-15.09-.3-.2-.24-.93-.35-1.41.76-3.29,1.52-6.58,2.28-9.87,1.73-5.3,3.45-10.61,5.18-15.92,5.37-2.88,10.95-5.44,16.06-8.73,4.94-3.18,9.63-5.51,15.34-2.22.86.94,1.71,1.88,2.57,2.82,2.6,8.72,5.2,17.44,7.8,26.17-5.35-3.04-10.69-6.07-16.49-9.36-.1,1.22-.17,2.21-.25,3.2Z" fill="#b6cadf"/>
|
||||||
|
<path d="m252.14,508.05c-7.6,4.98-15.2,9.96-23.08,15.13-3.16-10.68-6.19-20.95-9.23-31.23-.89-.62-1.79-1.24-2.68-1.86.7-2.53,1.41-5.06,2.11-7.58,2.35,1.44,4.82,2.7,7.02,4.33,7.24,5.37,13,13.52,23.66,12.34.23.58.46,1.17.69,1.75.51,2.37,1.01,4.74,1.52,7.12Z" fill="#d7e1eb"/>
|
||||||
|
<path d="m355.14,499.18c-2.6-8.72-5.2-17.44-7.8-26.17,4.31,1.67,8.62,3.35,12.92,5.02.65,1.63,1.3,3.27,1.95,4.9,2.92,8.73,5.84,17.45,8.77,26.18-5.28-3.31-10.56-6.62-15.84-9.93Z" fill="#2c5377"/>
|
||||||
|
<path d="m187.23,508.13c2.63-8.34,5.27-16.69,7.9-25.03.79-1.7,1.58-3.41,2.37-5.11,4.49-1.99,8.99-3.98,13.48-5.98-.58,1.89-1.18,3.77-1.73,5.67-2.05,7.18-4.08,14.37-6.12,21.56-5.3,2.96-10.6,5.92-15.9,8.88Z" fill="#2c5377"/>
|
||||||
|
<path d="m203.13,499.25c2.04-7.19,4.07-14.38,6.12-21.56.54-1.9,1.15-3.78,1.73-5.67.34-.16.67-.32,1.01-.48,2.43,3.66,4.85,7.32,7.28,10.98-.7,2.53-1.41,5.06-2.11,7.58-4.67,3.05-9.35,6.11-14.02,9.16Z" fill="#b6cadf"/>
|
||||||
|
<path d="m392.14,244.03c1.97,6.73,3.95,13.45,5.92,20.18-1.92-1.42-3.84-2.84-5.76-4.27-4.72-3.6-9.4-7.27-14.21-10.76-.96-.69-2.48-1.1-3.64-.93-9.15,1.29-18.28,2.75-27.41,4.15-.91-.35-1.8-.76-2.73-1.02-7.83-2.19-15.67-4.35-23.51-6.52-.29-.05-.58-.02-.86.08-1.11-.29-2.22-.83-3.34-.83-10.8-.06-21.6-.04-32.41.01-.37,0-.74.61-1.11.94.62,1.83,1.09,3.72,1.89,5.46,1.65,3.56,3.47,7.03,5.22,10.54-6.53-.25-13.06-.49-19.48-.74-1.16-5.47-2.12-9.99-3.36-15.89-1.6,3.7-2.77,6.03-3.62,8.47-1.39,3.94-3.68,4.97-7.64,3.37-3.48-1.41-7.21-2.21-10.82-3.28-.2-.1-.4-.1-.61,0-.8-.52-1.61-1.5-2.41-1.49-7.71.08-15.43.32-23.14.52-1.78-.22-3.56-.56-5.34-.64-6.27-.28-12.66.21-18.78-.87-9.02-1.59-17.21-1.71-24.26,5.32-2.67,2.66-5.95,4.71-8.96,7.03-1.55.43-3.09.87-4.64,1.3,1.65-5.71,3.3-11.41,4.95-17.12,5.2-3.68,10.37-7.4,15.6-11.03,10.11-7.01,20.27-13.97,30.4-20.96,6.94,2.64,13.91,5.2,20.81,7.93,14.94,5.89,29.84,11.93,44.81,17.73,1.66.64,3.94.75,5.6.16,6.69-2.37,13.31-4.96,19.85-7.74,10.31-4.39,20.5-9.07,30.81-13.46,5.62-2.39,11.4-4.43,17.11-6.63,8.01,5.5,16.02,11,24.03,16.5,7.02,4.82,14.03,9.65,21.05,14.48Z" fill="#b6cadf"/>
|
||||||
|
<path d="m219.11,252.02c7.71-.2,15.43-.44,23.14-.52.8,0,1.6.97,2.41,1.49-2.43,3.39-4.92,6.73-7.18,10.23-.23.35.87,2.21,1.63,2.44,6.02,1.81,12.12,3.39,18.19,5.04.23.18.47.33.73.45,2.29,3.31,4.58,6.61,6.87,9.92-.07.64-.14,1.28-.21,1.92-3.91,3.48-7.82,6.96-11.74,10.44-.95-.11-1.9-.22-2.85-.33-1.36-1.51-2.46-3.52-4.14-4.43-5.53-3-11.18-5.82-16.96-8.31-4.81-2.07-9.87-3.58-14.82-5.35-11.77-4.27-25.68-1.56-32.42,6.32-5.78,6.76-6.57,8.52-4.97,11.36,10.71-9.19,21.67-17.75,37.19-14.74,10.27,2.86,18.11,9.92,26.79,15.53.37.24.51.84.76,1.28h-11.94c-.07-.4-.14-.81-.2-1.21.76-.68,1.52-1.35,2.51-2.25-3.34-2.51-6.46-4.89-9.63-7.21-.32-.23-10.34-2.35-14.79-3.44-4.92,1.12-10.03,2.67-14.88,4.13-2.53,3.28-4.51,5.86-6.49,8.44-.77.2-1.53.4-2.3.6-.69.28-1.37.56-2.06.84-5.26,1.6-10.53,3.2-15.79,4.8l-1.34-1.7c6-7.61,11.99-15.22,18-22.83.78-.99,1.62-1.94,2.43-2.91.37-.23.73-.46,1.1-.69-.35.25-.7.5-1.05.74-3.82,1.4-7.98,2.25-11.39,4.32-7.76,4.73-15.14,10.06-22.7,15.12-.35.24-.89.21-1.75.39,2.33-9.17,4.59-18.07,6.85-26.97.35-.29.7-.58,1.05-.87,0,0-.04.1-.04.1,1.55-.43,3.09-.87,4.64-1.3,2.15.05,4.37.48,6.44.08,8.81-1.67,17.38-1.26,25.83,1.9,1.94.73,4.83,1.7,6.16.84,6.51-4.23,12.64-9.05,18.91-13.66Z" fill="#183b5c"/>
|
||||||
|
<path id="uuid-9216a0a0-ef51-4c31-a4e4-80b5b536d7eb" data-name="Left EYE" d="m229.59,294.76c3.98,0,7.96,0,11.94,0,2.86-.55,5.72-1.1,8.58-1.66.95.11,1.9.22,2.85.33-.19,3.6-.38,7.19-.57,10.79-2.77,4.27-5.54,8.54-8.31,12.82-2.18,1.69-4.39,3.33-6.53,5.06-6.12,4.94-12.22,9.91-18.32,14.87,1.99-5.61,3.99-11.22,5.98-16.83.11-.17.22-.34.33-.51-.17.11-.34.22-.51.33-9.54,2.14-19.04,4.49-28.63,6.36-7.85,1.53-14.42-2.74-21.48-4.71.05-.42.1-.84.15-1.26,6.07-1.17,12.14-2.33,18.21-3.5,7.93-1.28,15.87-2.56,23.8-3.85,7.27-3.91,14.53-7.82,21.8-11.73-.18-.56-.37-1.13-.55-1.69-4.74,0-9.49-.3-14.2.07-6.57.51-13.32,2.93-19.59,1.96-7.13-1.11-13.81-5.07-20.69-7.79.77-.2,1.53-.4,2.3-.6,3.71.52,7.42,1.05,11.13,1.57,3.64.13,7.28.26,10.92.39,9.88-.42,20.54-.42,21.42-.42Z" fill="#d7e1eb"/>
|
||||||
|
<path d="m183.82,293.82c6.88,2.72,13.56,6.68,20.69,7.79,6.27.98,13.02-1.44,19.59-1.96,4.71-.37,9.47-.07,14.2-.07.18.56.37,1.13.55,1.69-7.27,3.91-14.53,7.82-21.8,11.73-8.76-2.38-17.55-4.66-26.27-7.19-8.24-2.39-8.21-2.51-9.03-11.15.68-.28,1.37-.56,2.06-.85Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m157.17,264.07c-.35.29-.7.58-1.05.87.35-.29.7-.58,1.05-.87Z" fill="#b6cadf"/>
|
||||||
|
<path d="m219.21,336.97c6.11-4.96,12.2-9.93,18.32-14.87,2.14-1.73,4.35-3.38,6.53-5.06-1.84,13.42-3.83,26.83-5.46,40.28-.6,4.94-1.58,9.75-5.48,12.89-14.3,11.51-20.05,27.63-24.75,44.41-.31,1.12-.91,2.18-1.05,3.31-.08.65.47,1.38.73,2.08.11.5.21,1.01.32,1.51l-.22-.15.05-.27c-.36.36-.72.72-1.08,1.09,0,.24-.02.49-.05.73-.35.37-.71.74-1.06,1.11v-.17c-4.36,7.39-8.74,14.75-13.02,22.17-2.34,4.05-2.66,8.31.26,12.14,3.88,5.09,4.43,10.82,4.12,16.86-.05.99.09,1.99.14,2.98-.79,1.7-1.58,3.41-2.37,5.11-3.36-11.68-6.72-23.35-10.08-35.03,1.01-5.75,2.19-11.48,2.99-17.27,1.72-12.51,3.26-25.05,4.8-37.59.08-.68-.48-1.44-.74-2.17.02-.67.04-1.34.06-2.01,2.51-4.13,5.26-8.14,7.49-12.41,5.51-10.58,10.81-21.28,16.13-31.95,1.25-2.51,2.3-5.14,3.43-7.71Z" fill="#b6cadf"/>
|
||||||
|
<path d="m392.14,244.03c-7.02-4.83-14.03-9.65-21.05-14.48-8.01-5.5-16.02-11-24.03-16.5-2.98-15-5.97-30-8.95-45-.93-9.27-1.96-18.53-2.71-27.81-.2-2.45-1.95-5.61,1.81-7.19,6.4,7.84,12.77,15.69,19.19,23.5,9.54,11.59,19.12,23.14,28.68,34.72,2.28,15.94,4.57,31.87,6.81,47.81.23,1.63.17,3.3.25,4.95Z" fill="#9db7d1"/>
|
||||||
|
<path d="m379.96,79.19c-.12-.22-.25-.44-.37-.66.24.1.48.21.72.31-.11.12-.23.24-.35.35Z" fill="#6284a6"/>
|
||||||
|
<path d="m392.3,259.94c1.92,1.42,3.84,2.84,5.76,4.27,0,0-.03-.13-.03-.13.35.4.7.79,1.06,1.19,2.59,9.06,5.19,18.12,7.78,27.18-8.92-6.55-17.84-13.1-26.77-19.66-.34.36-.68.72-1.02,1.09,1.09,2.38,2.19,4.76,3.28,7.14-.51.97-1.02,1.95-1.53,2.92-3.83-6.32-10.08-8.87-16.75-10.78-.31-.19-.63-.2-.95-.02-.66-.06-1.31-.12-1.97-.18-3.35.08-6.7.17-10.04.25-4.17-.38-8.34-.85-12.51-1.11-8.22-.5-12.65,4.81-16.5,10.95-4.68,2.91-9.36,5.83-14.04,8.74-.59.13-1.18.25-1.77.38-3.55-2.41-7.11-4.82-10.66-7.23-.86-.62-1.71-1.24-2.57-1.87.05-.62.11-1.24.17-1.86,1.69-3.04,3.38-6.07,5.08-9.11,8.86-3.15,17.72-6.31,27.12-9.65-1.61-6.15-3.12-11.87-4.62-17.59,7.84,2.17,15.68,4.33,23.51,6.52.93.26,1.82.68,2.73,1.02,5.02,3.16,9.83,8.49,15.11,9.02,9.9.98,20.08-.83,30.15-1.48Z" fill="#183b5c"/>
|
||||||
|
<path d="m382.35,281.01c-1.09-2.38-2.19-4.76-3.28-7.14.34-.36.68-.72,1.02-1.09,8.92,6.55,17.84,13.1,26.77,19.66.39,1.31.79,2.63,1.18,3.93-5.02.83-10.03,1.65-15.05,2.48-3.55-5.95-7.09-11.9-10.64-17.85Z" fill="#6284a6"/>
|
||||||
|
<path d="m392.99,298.86c5.02-.83,10.03-1.66,15.05-2.48.31,3.2.61,6.41.92,9.61,0,1.46,0,2.92,0,5.96-7.7-3.86-14.51-7.27-21.31-10.68.52-.96,1.03-1.92,1.55-2.88,1.27.16,2.54.31,3.8.47Z" fill="#b6cadf"/>
|
||||||
|
<path d="m412.14,226.09c.3.68.6,1.35.91,2.03-.3-.68-.6-1.35-.91-2.03Z" fill="#183b5c"/>
|
||||||
|
<path d="m399.08,265.26c-.35-.4-.7-.79-1.06-1.19.35.4.7.79,1.06,1.19Z" fill="#b6cadf"/>
|
||||||
|
<path d="m306.29,292.16c.59-.13,1.18-.25,1.77-.38,1.19.77,2.39,1.53,3.58,2.3.5.6,1,1.2,1.5,1.8,1.84.96,3.68,1.91,5.53,2.87,6.86,4.73,13.73,9.46,20.59,14.19,0,0-.18.3-.18.3,1.3,1.54,2.29,3.78,3.94,4.51,17.48,7.67,35.09,15.05,52.6,22.65,2.66,1.16,5,3.07,7.49,4.64-12.38,14.67-24.75,29.35-37.13,44.02.04,3.37-.17,6.77.18,10.1,1.59,15.43,3.33,30.85,5.06,46.27.07.58.63,1.11.96,1.66-1.39,6.24-2.73,12.49-4.2,18.71-1.22,5.12-2.55,10.22-3.97,15.29-.2.73-1.16,1.24-1.78,1.85-.65-1.63-1.3-3.27-1.95-4.9-.51-6.1-.9-12.38,2.3-17.79,4.33-7.33,2.87-13.88-1.14-20.56-3.5-5.83-6.89-11.73-10.33-17.6-.41-.31-.81-.62-1.22-.93,0,0-.03.23-.03.23.1-.47.2-.93.29-1.39-.03-.32-.06-.63-.09-.95,0,0,.05.07.05.07-1.38-4.9-3.06-9.73-4.09-14.7-3.45-16.69-14.22-28.2-26.75-38.52-.47-1.18-.94-2.36-1.42-3.54-.57-9.23-1.08-18.46-1.74-27.68-.74-10.36-2.42-20.43-9.74-28.55-.42-.7-.85-1.41-1.27-2.11l.03.07c-1.61-4.17-2.43-8.25,1.17-11.91Z" fill="#b6cadf"/>
|
||||||
|
<path d="m403.1,345.04c-2.49-1.57-4.82-3.48-7.49-4.64-17.51-7.61-35.12-14.98-52.6-22.65-1.65-.73-2.64-2.97-3.94-4.51,7.37.62,14.73,1.49,22.11,1.73,2.59.08,5.24-1.37,7.86-2.13,1.66.25,3.39.93,4.98.67,5.5-.88,10.95-2.08,16.8-3.23,3.49,8.89,6.8,17.55,10.31,26.13.84,2.05,2.32,3.84,3.51,5.74.1.44.19.87.29,1.31,0,0,0,.05,0,.05-.61.51-1.21,1.01-1.82,1.52Z" fill="#d7e1eb"/>
|
||||||
|
<path d="m404.63,342.17c-1.19-1.91-2.67-3.69-3.51-5.74-3.51-8.58-6.82-17.24-10.31-26.13-5.85,1.15-11.3,2.35-16.8,3.23-1.58.25-3.31-.42-4.98-.67,3.94-3.3,7.8-6.71,11.91-9.8.84-.63,2.76-.4,3.92.08,6.6,2.74,13.02,5.95,19.72,8.43,3.28,1.21,3.91,2.99,3.48,5.97-1.19,8.2-2.3,16.42-3.44,24.63Z" fill="#b6cadf"/>
|
||||||
|
<path d="m339.25,312.94c-6.86-4.73-13.73-9.46-20.58-14.19,5.12.35,10.43-.03,15.33,1.19,13.9,3.47,26.83,1.82,39.05-5.62.05-.23.1-.45.17-.67.95-.15,1.9-.29,2.85-.44-.67,9.35-1.99,11.04-10.85,13.31-8.63,2.21-17.3,4.29-25.95,6.43Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m379.26,292.09c-7.06-4.37-14.11-8.74-21.17-13.11,1.02-2.01,2.05-4.02,3.07-6.03.66.06,1.31.12,1.97.18.32,0,.63.01.95.02,6.66,1.91,12.92,4.46,16.75,10.78,1.25,5.71,2.26,11.56,8.36,14.46-.51.96-1.03,1.92-1.55,2.88-2.8-3.06-5.59-6.11-8.38-9.17Z" fill="#9db7d1"/>
|
||||||
|
<path d="m404.92,343.52s0-.05,0-.05c.13-.02.26-.05.39-.08-.13.04-.26.08-.39.12Z" fill="#b6cadf"/>
|
||||||
|
<path d="m424.15,375.06c-.35.51-.7,1.02-1.05,1.53.35-.51.7-1.02,1.05-1.53Z" fill="#2c5377"/>
|
||||||
|
<path d="m252.14,508.05c-.51-2.37-1.01-4.74-1.52-7.12.86-.37,2.15-.5,2.5-1.15,4.34-7.96,8.55-16,12.79-24.02,7.76-2.02,15.45-1.56,23.09.67,5.91,6.79,11.81,13.6,17.76,20.36.25.29.95.19,1.43.28-.76,3.29-1.52,6.58-2.28,9.87-4.78-1.59-9.84-2.63-14.29-4.87-8.1-4.09-15.64-4.73-23.83-.09-4.82,2.73-10.41,4.09-15.65,6.07Z" fill="#183b5c"/>
|
||||||
|
<path d="m351.09,422.09c3.44,5.87,6.83,11.77,10.33,17.6,4.01,6.67,5.47,13.23,1.14,20.56-3.2,5.41-2.81,11.68-2.3,17.79-4.31-1.67-8.62-3.35-12.92-5.02-.86-.94-1.71-1.88-2.57-2.82-3.5-3.96-6.89-8.02-10.57-11.8-.8-.82-2.64-.63-4-.9-5.54-.79-11.07-2.06-16.63-2.24-6.48-.2-12.99.56-19.49.9-9-.02-17.99-.04-26.99-.07-.89-.34-1.76-.82-2.68-.98-12-2.14-23.72.04-35.43,2.35-1.94.39-4.53.13-5.71,1.3-4.03,3.99-7.57,8.48-11.3,12.78-.34.16-.67.32-1.01.48-4.49,1.99-8.99,3.98-13.48,5.98-.05-.99-.19-1.99-.14-2.98.32-6.03-.24-11.76-4.12-16.86-2.92-3.83-2.6-8.09-.26-12.14,4.29-7.42,8.67-14.78,13.01-22.17-.39,1.7-.75,3.41-1.19,5.1-.83,3.16-1.71,6.31-2.57,9.47l1.17.66c5.85-4.65,11.71-9.29,17.56-13.94,2.38-1.7,4.76-3.4,7.14-5.1-1,8.64-1.99,17.28-2.99,25.93,4.41-6.74,7.63-13.82,11.51-20.52,1.98-3.43,4.77-6.64,7.86-9.11,4.2-3.36,9.03-5.93,14.15-9.19-1.88-1.7-3.19-2.88-4.51-4.07-3.16-2.05-6.3-4.14-9.5-6.14-1.89-1.18-4.48-8.9-3.09-9.33,3-.92,7.51-1.96,9.44-.45,5.83,4.56,10.72,10.33,15.99,15.61.78.78,1.56,1.98,2.48,2.13,9.46,1.47,19.21,3.95,26.16-5.88.67-.94,1.75-1.6,2.35-2.57,5.38-8.56,12.74-12.61,22.97-10.31-1,2.61-2,5.22-2.99,7.82-4.62,2.7-9.24,5.4-13.86,8.1.33,1.49.2,3.39,1.08,4.4,2.82,3.26,5.86,6.38,9.12,9.2,3.06,2.64,4.26,5.66,3.85,9.63-.51,4.93-.51,9.92-1.16,14.83-.42,3.19-1.62,6.27-2.61,9.91,2.12-.5,4.72-1.25,7.36-1.69,4.32-.73,4.8-3.05,3.61-6.84-1.37-4.39-2.2-8.95-3.26-13.44h.01c3.66,4.26,7.31,8.53,10.97,12.8.42-.24.83-.48,1.25-.72-1.38-5.49-2.76-10.99-4.15-16.48,7.37,5.08,14.73,10.16,22.74,15.69-1.75-6.63-3.28-12.45-4.81-18.27.58.1,1.16.2,1.73.31,0,0,.03-.23.03-.23.41.31.81.62,1.22.93Zm-55.35-11.37c-2.49,1.76-4.09,2.91-5.72,4.02-2.13,1.45-4.35,4.15-6.4,4.04-4.73-.27-5.85,2.18-6.99,5.82-.92,2.97-2.63,5.69-4.03,8.5-1.6,3.23-3.25,6.44-5.01,9.92,6.49,2.97,12.09,6.73,18.94,2.08,1.21-.82,3.67-.17,5.44.27,3.77.93,7.39,1.29,10.49,0-1.51-2.53-4.03-4.88-3.59-6.33,2.96-9.79-1.17-18.49-3.13-28.32Zm-35.31.32c-.83,1.34-1.55,2.76-2.5,4-5.02,6.53-3.93,14.14-3.73,21.53.04,1.47,1.45,2.91,2.22,4.36,1.26-1.23,3.18-2.24,3.68-3.72,1.79-5.34,2.69-10.99,4.71-16.22,1.78-4.61,1.07-7.6-4.38-9.94Z" fill="#183b5c"/>
|
||||||
|
<path d="m211.98,471.53c3.73-4.3,7.27-8.79,11.3-12.78,1.18-1.17,3.77-.9,5.71-1.3.42.67.68,1.69,1.28,1.97,5.6,2.63,11.25,5.13,16.89,7.66,6.38,3.47,13.07,5.4,20.47,4.89,5.13-.35,10.31-.21,15.46,0,11.16.47,21.76-1.49,31.71-6.66,5.14-2.61,10.27-5.22,15.41-7.83,1.36.27,3.2.08,4,.9,3.68,3.78,7.07,7.84,10.57,11.8-5.71-3.28-10.41-.96-15.34,2.22-5.11,3.29-10.69,5.85-16.06,8.73-8.13-1.57-16.25-3.15-24.38-4.72-7.64-2.23-15.33-2.7-23.08-.67-6.55,1.57-13.11,3.14-20.19,4.83,1.46,6.46,2.84,12.53,4.21,18.59-10.65,1.18-16.41-6.97-23.66-12.34-2.2-1.63-4.67-2.89-7.02-4.33-2.42-3.67-4.85-7.33-7.28-10.98Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m289,476.43c8.13,1.57,16.25,3.15,24.38,4.72-1.73,5.31-3.45,10.61-5.18,15.92-.49-.08-1.18.01-1.43-.28-5.95-6.76-11.85-13.56-17.76-20.36Z" fill="#2c5377"/>
|
||||||
|
<path d="m249.93,499.18c-1.37-6.06-2.75-12.13-4.21-18.59,7.08-1.69,13.64-3.26,20.19-4.83-4.24,8.02-8.45,16.06-12.79,24.02-.35.65-1.64.78-2.5,1.15-.23-.58-.46-1.17-.69-1.75Z" fill="#2c5377"/>
|
||||||
|
<path d="m298.29,272.09c-1.69,3.04-3.38,6.07-5.08,9.11-1.21-.72-2.43-2.06-3.63-2.05-7.88.09-15.75.47-23.63.75,1.24-6.14,2.47-12.28,3.92-19.51-4.44,4.02-8.15,7.39-11.87,10.75-.26-.13-.5-.28-.73-.45-4.01-5.9-8.02-11.81-12.03-17.71,3.62,1.07,7.34,1.88,10.82,3.28,3.96,1.6,6.26.58,7.64-3.37.86-2.44,2.03-4.77,3.62-8.47,1.25,5.89,2.2,10.41,3.36,15.89,6.42.24,12.95.49,19.48.74,2.7,3.68,5.4,7.36,8.09,11.04Z" fill="#9db7d1"/>
|
||||||
|
<path d="m219.11,252.02c-6.27,4.61-12.4,9.43-18.91,13.66-1.33.86-4.21-.11-6.16-.84-8.46-3.16-17.02-3.57-25.83-1.9-2.07.39-4.29-.03-6.44-.08,3-2.33,6.29-4.37,8.96-7.03,7.04-7.03,15.24-6.91,24.26-5.32,6.12,1.08,12.51.59,18.78.87,1.79.08,3.56.42,5.34.64Z" fill="#2c5377"/>
|
||||||
|
<path d="m298.29,272.09c-2.7-3.68-5.4-7.36-8.09-11.04-1.75-3.51-3.58-6.99-5.22-10.54-.8-1.74-1.27-3.64-1.89-5.46,7.34,5.65,14.68,11.29,22.55,17.34,4.91-5.98,9.61-11.72,14.31-17.45.28-.1.57-.12.86-.08,1.5,5.72,3,11.44,4.62,17.59-9.4,3.35-18.26,6.5-27.12,9.65Z" fill="#6284a6"/>
|
||||||
|
<path d="m392.3,259.94c-10.06.65-20.24,2.45-30.15,1.48-5.28-.52-10.09-5.85-15.11-9.02,9.13-1.41,18.26-2.87,27.41-4.15,1.16-.16,2.69.24,3.64.93,4.81,3.49,9.48,7.15,14.21,10.76Z" fill="#2c5377"/>
|
||||||
|
<path d="m319.94,244.93c-4.7,5.73-9.4,11.47-14.31,17.45-7.86-6.05-15.21-11.69-22.55-17.34.37-.33.74-.94,1.11-.94,10.8-.05,21.6-.07,32.41-.01,1.11,0,2.23.54,3.34.83Z" fill="#9db7d1"/>
|
||||||
|
<path d="m245.26,252.99c4.01,5.9,8.02,11.81,12.03,17.71-6.07-1.65-12.16-3.24-18.19-5.04-.76-.23-1.85-2.09-1.63-2.44,2.25-3.5,4.75-6.84,7.18-10.23.2-.1.41-.1.61,0Z" fill="#6284a6"/>
|
||||||
|
<path d="m265.96,279.9c7.88-.28,15.75-.66,23.63-.75,1.2-.01,2.42,1.33,3.63,2.05-.06.62-.11,1.24-.17,1.86-.37,4.33-.73,8.65-1.1,12.98-.28.76-.72,1.5-.81,2.28-1.03,9.55-2.23,19.09-2.93,28.66-.67,9.03-2.9,18.16.84,27.07-.14,3.03-.57,6.08-.35,9.09.56,7.79,1.41,15.55,2.19,23.91-9.31-1.04-18.05-2.02-26.8-3,2.51-4.66,5.08-9.29,7.5-14,.65-1.27,1.28-2.84,1.15-4.19-1.5-15.92-3.07-31.83-4.82-47.72-.27-2.46-1.55-4.81-2.36-7.21-.28-8.67-.56-17.35-.83-26.02-.02-.64-.04-1.29-.06-1.93.07-.64.14-1.28.21-1.92.36-.39.71-.78,1.07-1.17Z" fill="#d7e1eb"/>
|
||||||
|
<path d="m213.98,277.96c-15.52-3.01-26.49,5.55-37.19,14.74-1.59-2.84-.81-4.6,4.97-11.36,6.74-7.88,20.65-10.59,32.42-6.32-.07.98-.13,1.96-.2,2.94Z" fill="#6284a6"/>
|
||||||
|
<path d="m213.98,277.96c.07-.98.13-1.96.2-2.94,4.95,1.76,10,3.27,14.82,5.34,5.78,2.49,11.43,5.3,16.96,8.31,1.69.92,2.78,2.93,4.14,4.43-2.86.55-5.72,1.1-8.58,1.66-.25-.43-.38-1.04-.76-1.28-8.68-5.6-16.52-12.67-26.79-15.53Z" fill="#2c5377"/>
|
||||||
|
<path d="m265.96,279.9c-.36.39-.71.78-1.07,1.17-2.29-3.31-4.58-6.61-6.87-9.92,3.71-3.37,7.43-6.73,11.87-10.75-1.45,7.22-2.69,13.36-3.92,19.51Z" fill="#6284a6"/>
|
||||||
|
<path d="m264.69,282.99c.02.64.04,1.29.06,1.93-1.57,3.02-3.14,6.03-4.71,9.05-2.55,3.42-5.1,6.84-7.65,10.26.19-3.6.38-7.19.57-10.79,3.91-3.48,7.82-6.96,11.74-10.44Z" fill="#2c5377"/>
|
||||||
|
<path d="m185.12,272.08c.35-.25.7-.5,1.05-.74-.37.23-.73.46-1.1.69,0,0,.05.05.05.05Z" fill="#6284a6"/>
|
||||||
|
<path d="m252.38,304.22c2.55-3.42,5.1-6.84,7.65-10.26-.17,4.8.28,9.73-.64,14.38-2.26,11.46-3.81,22.61,4.53,32.5-.96,5.85-1.68,11.75-2.98,17.52-.75,3.31-2.47,6.41-3.75,9.6-2.59-3.45-5.19-6.91-8.65-11.52-3.9,7.39-7.1,13.45-10.3,19.51-.75-.3-1.5-.6-2.25-.9-.39.31-.9.54-1.14.93-8.94,14.66-17.86,29.34-26.78,44.01-.27-.7-.82-1.43-.73-2.08.15-1.13.74-2.19,1.05-3.31,4.7-16.78,10.45-32.91,24.75-44.41,3.89-3.13,4.88-7.95,5.48-12.89,1.64-13.45,3.62-26.86,5.46-40.28,2.77-4.27,5.54-8.54,8.31-12.82Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m225.01,319.95c.17-.11.34-.22.51-.33-.11.17-.22.34-.33.51,0,0-.19-.19-.19-.19Z" fill="#9db7d1"/>
|
||||||
|
<path d="m208.06,420.01c8.92-14.67,17.84-29.35,26.78-44.01.24-.4.76-.63,1.14-.93.62,10.58,1.23,21.16,1.85,31.74-1.23,1.1-2.47,2.21-3.7,3.31-3.34,1.53-6.69,3.06-10.03,4.59-5.24,2.27-10.48,4.54-15.72,6.82-.11-.5-.21-1.01-.32-1.51Z" fill="#d7e1eb"/>
|
||||||
|
<path d="m208.38,421.52c5.24-2.27,10.48-4.54,15.72-6.82-1.05,3.47-2.09,6.95-3.14,10.42-5.85,4.65-11.71,9.29-17.56,13.94l-1.17-.66c.86-3.15,1.74-6.3,2.57-9.47.44-1.69.8-3.4,1.19-5.1,0,0,.01.17.01.17.35-.37.71-.74,1.06-1.11.03-.24.05-.48.05-.73.36-.36.72-.72,1.08-1.09l-.05.27.22.15Z" fill="#2c5377"/>
|
||||||
|
<path d="m208.2,421.1c-.36.36-.72.72-1.08,1.09.36-.36.72-.72,1.08-1.09Z" fill="#d7e1eb"/>
|
||||||
|
<path d="m207.08,422.92c-.35.37-.71.74-1.06,1.11.35-.37.71-.74,1.06-1.11Z" fill="#183b5c"/>
|
||||||
|
<path d="m291.95,296.04c.37-4.33.73-8.65,1.1-12.98.86.62,1.71,1.24,2.57,1.87,1.49,4.37,2.99,8.75,4.48,13.12-.32,1.89-.74,3.77-.96,5.67-.91,7.82-1.76,15.64-2.64,23.46-.58,0-1.15,0-1.73-.01-.6-6.84-1.19-13.69-1.81-20.53-.32-3.53-.68-7.07-1.02-10.6Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m340.91,278.93c-4.67,2.63-9.34,5.27-14,7.91-1.58.82-3.14,1.68-4.75,2.44-3.49,1.63-7.01,3.21-10.51,4.81-1.19-.77-2.39-1.53-3.58-2.3,4.68-2.91,9.36-5.83,14.04-8.74,5.09-1.73,10.15-3.57,15.29-5.1.99-.29,2.34.62,3.52.98Z" fill="#6284a6"/>
|
||||||
|
<path d="m340.91,278.93c-1.18-.36-2.53-1.28-3.52-.98-5.15,1.53-10.21,3.37-15.29,5.1,3.85-6.14,8.29-11.45,16.5-10.95,4.18.26,8.34.73,12.51,1.11-2.33,1.26-4.66,2.51-6.98,3.77-1.07.65-2.15,1.3-3.22,1.96Z" fill="#2c5377"/>
|
||||||
|
<path d="m389.19,298.38c-6.11-2.9-7.11-8.75-8.36-14.46.51-.97,1.02-1.94,1.53-2.92,3.55,5.95,7.09,11.9,10.64,17.85-1.27-.16-2.54-.31-3.8-.47Z" fill="#2c5377"/>
|
||||||
|
<path d="m300.1,298.06c-1.49-4.37-2.99-8.75-4.48-13.12,3.56,2.41,7.11,4.82,10.67,7.23-3.6,3.66-2.78,7.74-1.17,11.91-1.67-2.01-3.35-4.01-5.02-6.02Z" fill="#2c5377"/>
|
||||||
|
<path d="m344.13,276.97c2.33-1.26,4.66-2.51,6.98-3.77,3.35-.08,6.7-.17,10.04-.25-1.02,2.01-2.05,4.02-3.07,6.03-4.65-.67-9.31-1.34-13.96-2.01Z" fill="#b6cadf"/>
|
||||||
|
<path d="m364.07,273.15c-.32,0-.63-.01-.95-.02.32-.18.64-.17.95.02Z" fill="#b6cadf"/>
|
||||||
|
<path d="m300.1,298.06c1.67,2.01,3.35,4.01,5.02,6.02,0,0-.03-.07-.03-.07.42.7.85,1.41,1.27,2.11,7.32,8.13,9,18.2,9.74,28.55.66,9.22,1.17,18.45,1.74,27.67-1.29-.44-2.59-.89-3.89-1.33-3.38-1.22-6.76-2.44-10.32-3.73-.51,9.17-.99,17.9-1.47,26.63-1.03.03-2.06.06-3.09.1-3.34-9.98-6.68-19.96-10.02-29.94-3.74-8.91-1.5-18.05-.84-27.07.71-9.57,1.9-19.11,2.93-28.66.08-.78.53-1.52.81-2.28.34,3.53.7,7.06,1.02,10.6.61,6.84,1.21,13.69,1.81,20.53.58,0,1.16,0,1.73.01.87-7.82,1.73-15.65,2.64-23.46.22-1.9.63-3.78.96-5.67Z" fill="#9db7d1"/>
|
||||||
|
<path d="m313.95,361.01c1.3.44,2.59.89,3.89,1.33.47,1.18.94,2.36,1.41,3.55.63,2.76,1.26,5.51,1.89,8.27l-.2,11.97c-10.23-2.31-17.59,1.75-22.97,10.31-.61.97-1.69,1.63-2.35,2.57-6.95,9.83-16.71,7.34-26.16,5.88-.92-.14-1.7-1.35-2.48-2.13-5.27-5.28-10.16-11.05-15.99-15.61-1.93-1.51-6.44-.48-9.44.45-1.4.43,1.2,8.15,3.09,9.33,3.2,2,6.34,4.09,9.5,6.14-5.43,1.25-10.87,2.5-16.3,3.74-.62-10.58-1.23-21.16-1.85-31.74.75.3,1.5.6,2.25.9,6.26,2.61,12.52,5.21,18.78,7.82,2.02,1.44,4.05,2.87,6.07,4.31,3.75,7.24,10.69,7.23,17.47,7.89,5.33.52,9.62.05,12.26-5.67,1.16-2.53,4.12-4.23,6.26-6.3,1.03-.03,2.06-.06,3.09-.09,7.09-.29,12.27-4.65,17.64-7.52-2.11-5.56-3.98-10.47-5.84-15.38Z" fill="#6284a6"/>
|
||||||
|
<path d="m320.95,386.14c.07-3.99.13-7.98.2-11.98.58.79,1.21,1.55,1.74,2.37,9.08,14.19,18.15,28.38,27.21,42.58,0,0-.05-.07-.05-.07,0,.32.02.64.09.95-.1.46-.2.93-.29,1.39-.58-.1-1.16-.2-1.74-.3-7.68-2.31-15.37-4.62-23.06-6.93-2.68-2.35-5.37-4.7-8.05-7.05.32-4.38.63-8.76.95-13.14,1-2.61,2-5.22,2.99-7.82Z" fill="#d7e1eb"/>
|
||||||
|
<path d="m350.1,419.11c-9.07-14.19-18.14-28.39-27.21-42.58-.53-.82-1.16-1.58-1.74-2.37-.63-2.76-1.26-5.51-1.89-8.27,12.54,10.32,23.31,21.83,26.76,38.52,1.03,4.97,2.71,9.8,4.09,14.7Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m328.21,289.13c-.48,1.43-.96,2.86-1.63,4.86-3.53.49-8.48,1.19-13.44,1.88-.5-.6-1-1.2-1.5-1.8,3.5-1.6,7.02-3.18,10.51-4.81,1.61-.75,3.17-1.62,4.75-2.44.43.76.87,1.53,1.3,2.3Z" fill="#2c5377"/>
|
||||||
|
<path d="m306.36,306.12c-.42-.7-.85-1.41-1.27-2.11.42.7.85,1.41,1.27,2.11Z" fill="#2c5377"/>
|
||||||
|
<path d="m350.14,419.99c-.06-.31-.09-.63-.09-.95.03.32.06.63.09.95Z" fill="#7e9cbe"/>
|
||||||
|
<path d="m351.09,422.09c-.41-.31-.81-.62-1.22-.93.41.31.81.62,1.22.93Z" fill="#d7e1eb"/>
|
||||||
|
<path d="m314.79,465.33c-9.95,5.16-20.55,7.13-31.71,6.66-5.15-.22-10.33-.35-15.46,0-7.4.5-14.08-1.43-20.47-4.89,6.65-3.66,13.29-7.33,19.94-10.99,9,.02,17.99.04,26.99.07,6.9,3.06,13.8,6.11,20.7,9.17Z" fill="#d7e1eb"/>
|
||||||
|
<path d="m295.74,410.71c1.96,9.84,6.09,18.53,3.13,28.32-.44,1.45,2.08,3.79,3.59,6.33-3.1,1.29-6.72.93-10.49,0-1.77-.44-4.23-1.09-5.44-.27-6.84,4.65-12.45.89-18.94-2.08,1.76-3.48,3.41-6.69,5.01-9.92,1.4-2.81,3.11-5.54,4.03-8.5,1.13-3.64,2.26-6.09,6.99-5.82,2.05.12,4.27-2.59,6.4-4.04,1.63-1.11,3.23-2.27,5.72-4.02Z" fill="#2c5377"/>
|
||||||
|
<path d="m317.01,407.1c2.68,2.35,5.37,4.7,8.05,7.05,1.71,3.17,3.41,6.34,5.12,9.51,1.38,5.49,2.76,10.99,4.15,16.48-.42.24-.83.48-1.25.72-3.66-4.27-7.31-8.54-10.97-12.81-.14-.07-.28-.14-.42-.22.14.08.27.15.41.23,1.06,4.49,1.89,9.05,3.26,13.44,1.19,3.79.71,6.11-3.61,6.84-2.65.44-5.24,1.2-7.36,1.69.99-3.64,2.19-6.72,2.61-9.91.65-4.91.65-9.9,1.16-14.83.41-3.97-.79-6.99-3.85-9.63-3.26-2.82-6.3-5.94-9.12-9.2-.88-1.02-.75-2.91-1.08-4.4,4.3,1.68,8.61,3.36,12.91,5.05Z" fill="#2c5377"/>
|
||||||
|
<path d="m237.84,406.81c5.43-1.25,10.87-2.5,16.3-3.74,1.31,1.18,2.62,2.37,4.51,4.07-5.12,3.26-9.95,5.83-14.15,9.19-3.09,2.48-5.88,5.69-7.86,9.11-3.88,6.7-7.1,13.79-11.51,20.52,1-8.64,1.99-17.28,2.99-25.93,2.01-3.3,4.02-6.61,6.02-9.91,1.23-1.1,2.47-2.21,3.7-3.31Z" fill="#2c5377"/>
|
||||||
|
<path d="m267.1,456.09c-6.65,3.66-13.29,7.33-19.94,10.99-5.64-2.54-11.29-5.04-16.89-7.66-.6-.28-.86-1.3-1.28-1.97,11.71-2.31,23.43-4.49,35.43-2.34.92.16,1.79.65,2.68.98Z" fill="#6284a6"/>
|
||||||
|
<path d="m330.18,423.66c-1.71-3.17-3.41-6.34-5.12-9.51,7.69,2.31,15.37,4.62,23.06,6.93,1.53,5.82,3.06,11.64,4.81,18.27-8.01-5.53-15.38-10.61-22.74-15.69Z" fill="#6284a6"/>
|
||||||
|
<path d="m260.44,411.03c5.45,2.35,6.16,5.34,4.38,9.94-2.02,5.23-2.92,10.88-4.71,16.22-.5,1.48-2.41,2.49-3.68,3.72-.78-1.45-2.18-2.89-2.22-4.36-.2-7.39-1.29-15,3.73-21.53.95-1.24,1.67-2.65,2.5-4Z" fill="#2c5377"/>
|
||||||
|
<path d="m314.79,465.33c-6.9-3.06-13.8-6.11-20.7-9.17,6.5-.34,13.01-1.11,19.49-.9,5.56.17,11.08,1.45,16.63,2.24-5.14,2.61-10.27,5.22-15.41,7.84Z" fill="#6284a6"/>
|
||||||
|
<path d="m317.01,407.1c-4.3-1.68-8.61-3.36-12.91-5.05,4.62-2.7,9.24-5.4,13.86-8.1-.32,4.38-.63,8.76-.95,13.14Z" fill="#6284a6"/>
|
||||||
|
<path d="m234.13,410.12c-2.01,3.3-4.02,6.61-6.02,9.91-2.38,1.7-4.76,3.4-7.14,5.1,1.04-3.47,2.09-6.95,3.14-10.42,3.34-1.53,6.69-3.06,10.03-4.59Z" fill="#6284a6"/>
|
||||||
|
<path d="m263.08,388.09c-2.02-1.44-4.05-2.87-6.07-4.31.06-5.27.12-10.54.17-15.81,1.28-3.19,3-6.29,3.75-9.6,1.31-5.77,2.02-11.68,2.98-17.52.55-9.97,1.11-19.94,1.66-29.91.81,2.4,2.09,4.75,2.36,7.21,1.75,15.89,3.32,31.8,4.82,47.72.13,1.36-.5,2.92-1.15,4.19-2.41,4.71-4.99,9.34-7.5,14-.34,1.34-.68,2.69-1.02,4.03Z" fill="#9db7d1"/>
|
||||||
|
<path d="m263.08,388.09c.34-1.34.68-2.69,1.02-4.03,8.74.98,17.49,1.96,26.8,3-.79-8.36-1.63-16.13-2.19-23.91-.22-3.01.22-6.06.35-9.09,3.34,9.98,6.68,19.96,10.02,29.94-2.14,2.07-5.1,3.78-6.26,6.3-2.64,5.72-6.93,6.19-12.26,5.67-6.78-.66-13.72-.65-17.47-7.89Z" fill="#b6cadf"/>
|
||||||
|
<path d="m265.58,310.94c-.55,9.97-1.1,19.94-1.66,29.91-8.34-9.9-6.79-21.05-4.53-32.5.92-4.66.47-9.58.64-14.38,1.57-3.02,3.14-6.03,4.71-9.05.28,8.67.56,17.35.83,26.02Z" fill="#6284a6"/>
|
||||||
|
<path d="m257.19,367.97c-.06,5.27-.12,10.54-.17,15.81-6.26-2.61-12.52-5.21-18.78-7.82,3.2-6.06,6.4-12.12,10.3-19.51,3.46,4.61,6.05,8.06,8.65,11.52Z" fill="#b6cadf"/>
|
||||||
|
<path d="m313.95,361.01c1.87,4.91,3.73,9.82,5.84,15.38-5.36,2.87-10.55,7.22-17.64,7.52.48-8.73.96-17.46,1.47-26.63,3.56,1.29,6.94,2.51,10.32,3.73Z" fill="#b6cadf"/>
|
||||||
|
<path d="m322.09,428.06c-.14-.08-.27-.15-.41-.23.14.07.28.14.42.22,0,0-.01.01-.01.01Z" fill="#183b5c"/>
|
||||||
|
<path d="m376.05,293.2c-.95.15-1.9.29-2.85.44-2.85-2.34-5.69-4.68-9.18-7.55-2.61-1.74-7.06-4.44-7.06-4.44-3.89-.59-12.56-1.85-12.56-1.85,0,0-5.11,2.85-7.07,4.04-2.9,1.66-6.24,3.63-9.14,5.29-.43-.77-.87-1.53-1.3-2.3,4.67-2.63,9.34-5.27,14-7.9,1.07-.65,2.15-1.3,3.22-1.96,4.65.67,9.31,1.34,13.96,2.01,7.06,4.37,14.11,8.74,21.17,13.11-1.07.37-2.13.74-3.2,1.11Z" fill="#183b5c"/>
|
||||||
|
<path id="uuid-e2a2569a-911d-4e4c-be55-07c6b21a2263" data-name="Right Bottom Eye" d="m344.18,294.84c5.57-.04,11.14-.09,16.72-.13,4.05-.13,8.09-.27,12.14-.4-12.21,7.44-25.14,9.09-39.05,5.62-4.9-1.22-10.2-.84-15.33-1.19-1.85-.96-3.69-1.92-5.53-2.88,4.95-.69,9.91-1.39,13.44-1.88,1.78-.63,16.95.67,17.61.85Z" fill="#d7e1eb"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
88
src/containers/WalletPopup/components/NetworkLogo.tsx
Normal file
88
src/containers/WalletPopup/components/NetworkLogo.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
const ghostLogo = () => {
|
||||||
|
return (
|
||||||
|
<svg id="uuid-29cf08a4-4a65-4410-8aa5-350a28213b48" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
|
||||||
|
<defs>
|
||||||
|
<style>{`
|
||||||
|
.uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe {
|
||||||
|
fill: #345b87;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uuid-b9fad165-95df-4ba5-b7d8-2290481eaf04 {
|
||||||
|
fill: #f2e370;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g id="uuid-f073e362-1a02-4167-a269-10b1f33990ea" data-name="uuid-2a2ed4dd-1266-4b44-b1c2-f456e6ec6f49">
|
||||||
|
<g id="uuid-744a1c1d-d715-4b76-a1d0-fb54d03f9094" data-name="uuid-fb84f985-acc6-4529-8860-a7d17e43f533">
|
||||||
|
<g>
|
||||||
|
<circle className="uuid-b9fad165-95df-4ba5-b7d8-2290481eaf04" cx="125" cy="125" r="122.5"/>
|
||||||
|
<path className="uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe" d="m125,5c32.05,0,62.19,12.48,84.85,35.15s35.15,52.8,35.15,84.85-12.48,62.19-35.15,84.85c-22.67,22.67-52.8,35.15-84.85,35.15s-62.19-12.48-84.85-35.15C17.48,187.19,5,157.05,5,125s12.48-62.19,35.15-84.85C62.81,17.48,92.95,5,125,5m0-5C55.96,0,0,55.96,0,125s55.96,125,125,125,125-55.96,125-125S194.04,0,125,0h0Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="uuid-4871e70f-254a-45a2-84fe-1d7c3034da86" data-name="uuid-1a93fc72-ded1-41af-8e7c-010273c35667">
|
||||||
|
<path className="uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe" d="m135.65,68.54c.11,0,.22,0,.33.02l.22.04,4.48,1.05,1.43.42c.34.1.67.2,1,.3.65.19,1.29.38,1.93.59l2.86,1.04c4.22,1.67,7.84,3.5,11.14,5.63,7.13,4.65,13.1,10.9,17.26,18.08,4.11,7.04,6.75,15.28,7.63,23.83.45,4.44.44,8.69,0,12.65-.47,4.09-1.28,7.61-2.49,10.77-.41,1.07-1.25,1.92-2.32,2.34s-2.27.38-3.3-.12c-1.95-.95-2.82-3.25-1.99-5.25,1.03-2.47,1.8-5.41,2.27-8.73.49-3.4.6-7.1.32-11-.56-7.54-2.66-14.85-6.09-21.13-3.46-6.4-8.55-12.04-14.71-16.32-2.89-1.99-6.12-3.74-9.87-5.34l-2.67-1.06c-.5-.19-1.07-.37-1.64-.55-.34-.11-.69-.22-1.03-.34l-1.32-.43-4.31-1.14c-1.1-.35-2.07-1.52-1.79-3.1.26-1.47,1.5-2.26,2.65-2.26h.01Z"/>
|
||||||
|
<path className="uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe" d="m83.09,153.6c1.37,0,2.69.69,3.46,1.92,1.64,2.63,3.72,5.09,6.17,7.29,2.78,2.53,5.95,4.78,9.42,6.71,6.84,3.8,14.53,6.08,22.23,6.58,3.91.25,7.82.04,11.62-.64,3.78-.68,7.52-1.86,11.12-3.49,3.45-1.57,6.85-3.6,10.13-6.06,2.93-2.27,5.8-4.94,8.74-8.14l.19-.18c.87-.76,2.38-.97,3.57.11,1.15,1.04,1.14,2.52.54,3.45l-.2.26c-3.06,3.52-6.08,6.49-9.24,9.08-3.57,2.84-7.29,5.2-11.09,7.05-4.02,1.96-8.23,3.41-12.51,4.31-4.31.89-8.75,1.26-13.22,1.09-8.75-.34-17.55-2.68-25.45-6.78-4.05-2.1-7.78-4.61-11.08-7.44-3.07-2.62-5.71-5.57-7.83-8.76-.63-.95-.84-2.14-.57-3.25s1-2.06,2-2.61c.63-.35,1.31-.51,1.98-.51h.02Z"/>
|
||||||
|
<path className="uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe" d="m113.94,67.47c.76,0,1.51.21,2.16.61.95.59,1.63,1.55,1.86,2.64.45,2.16-.92,4.32-3.05,4.82-6.07,1.42-12.11,4.08-17.49,7.68-5.92,3.97-11.02,9.05-14.75,14.69-3.86,5.81-6.41,12.52-7.38,19.41-.96,6.47-.53,13.57,1.27,21.12l.04.18c.18,1.15-.43,2.55-1.95,3-1.48.43-2.76-.39-3.25-1.43l-.09-.19-.05-.2c-2.23-8.2-2.93-16.03-2.09-23.27.86-7.84,3.53-15.57,7.72-22.32,4.04-6.55,9.66-12.5,16.23-17.2,6.1-4.36,12.75-7.52,19.76-9.4.35-.09.71-.14,1.06-.14Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="uuid-0cc46f90-c0cd-4f64-85d6-dec8a4b73fc6" data-name="uuid-977abf79-b623-4053-ab7b-a617d834d6cf">
|
||||||
|
<path className="uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe" d="m178.97,83.02c1.57,3.93,4.57,7.01,8.46,8.68,2.89,1.24,5.95,1.55,8.84,1.06,3.25,6.6,5.62,13.81,6.98,21.39,2.3,12.74,1.62,25.68-1.97,37.4-1.85,6.05-4.48,11.82-7.82,17.13-3.3,5.27-7.37,10.18-12.08,14.61-4.45,4.19-9.63,8.08-15.34,11.52-5.3,3.08-11.08,5.82-17.68,8.38l-.52.2-.43.35c-1.4,1.12-1.93,3.01-1.32,4.71.71,1.95,2.75,3.08,4.74,2.61l.38-.11c7.11-2.5,13.39-5.24,19.25-8.39,6.44-3.6,12.28-7.71,17.37-12.19,5.48-4.82,10.26-10.24,14.2-16.11,4.02-5.96,7.25-12.47,9.59-19.35,4.54-13.3,5.8-28.07,3.64-42.72-1.38-9.42-4.14-18.45-8.11-26.71.41-.66.77-1.35,1.08-2.08,3.45-8.03-.28-17.36-8.31-20.81-1.88-.81-3.82-1.22-5.75-1.28-6.32-.19-12.42,3.43-15.06,9.58-1.67,3.89-1.73,8.19-.16,12.12h.02Z"/>
|
||||||
|
<path className="uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe" d="m54.73,98.51c8.74.08,15.91-6.96,15.99-15.7.03-3.12-.86-6.05-2.4-8.52,3.95-4.62,8.41-8.85,13.25-12.51,8.91-6.76,19.25-11.58,29.93-13.93,10.7-2.38,22.16-2.24,33.16.39,10.09,2.34,20.49,7.06,30.91,14.02l.09.06c1.82,1.14,4.24.63,5.51-1.15,1.29-1.81.94-4.28-.78-5.61l-.1-.07c-11.02-7.96-22.22-13.52-33.26-16.51-7.2-2.01-14.65-3.05-22.05-3.12-5.27-.05-10.51.4-15.63,1.34-12.34,2.25-24.43,7.35-34.94,14.74-6.17,4.32-11.87,9.43-16.9,15.1-.81-.14-1.64-.21-2.49-.22-8.74-.08-15.91,6.96-15.99,15.7s6.96,15.91,15.7,15.99Z"/>
|
||||||
|
<path className="uuid-fa5cc4a0-ee16-495b-b1f3-38cfa5384fbe" d="m129.64,191.95c-1.28-.35-2.57-.54-3.87-.57-2.83-.06-5.64.64-8.17,2.08-2.59,1.47-4.65,3.6-6.04,6.15-6.41-1.32-12.79-3.45-18.92-6.36-10.76-5.15-20.35-12.54-27.73-21.36-7.56-8.99-12.97-19.82-15.67-31.34-2.59-10.69-2.85-22.71-.77-35.71l.04-.46c.06-2.05-1.43-3.83-3.46-4.16-.2-.03-.39-.05-.58-.05-1.62-.04-3.11.85-3.83,2.33l-.21.43-.09.47c-2.75,14.06-2.9,27.26-.44,39.21,2.59,13.11,8.31,25.6,16.56,36.12,8.05,10.33,18.68,19.12,30.75,25.42,7.48,3.87,15.37,6.72,23.35,8.48.3.83.67,1.65,1.12,2.44,2.1,3.68,5.5,6.32,9.58,7.44s8.35.58,12.03-1.51c3.68-2.1,6.32-5.5,7.44-9.58s.58-8.35-1.51-12.03c-2.1-3.68-5.5-6.32-9.58-7.44h0Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const casperLogo = () => {
|
||||||
|
return (
|
||||||
|
<svg id="uuid-9f9d14c3-06d2-4ba3-918a-61d473502c89" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
|
||||||
|
<defs>
|
||||||
|
<style>{`
|
||||||
|
.uuid-ce8b5d66-0551-460b-9b55-639040331854 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uuid-f651383e-729c-4543-82ed-5cea5662307d {
|
||||||
|
fill: #345b87;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g id="uuid-57f748d9-5b3e-4f39-b3f4-87f670a07a49" data-name="uuid-2a2ed4dd-1266-4b44-b1c2-f456e6ec6f49">
|
||||||
|
<g id="uuid-42f1c85f-22d4-41d1-813e-1cfb00eb9c2c" data-name="uuid-fb84f985-acc6-4529-8860-a7d17e43f533">
|
||||||
|
<g>
|
||||||
|
<circle className="uuid-ce8b5d66-0551-460b-9b55-639040331854" cx="125" cy="125" r="122.5"/>
|
||||||
|
<path className="uuid-f651383e-729c-4543-82ed-5cea5662307d" d="m125,5c32.05,0,62.19,12.48,84.85,35.15s35.15,52.8,35.15,84.85-12.48,62.19-35.15,84.85c-22.67,22.67-52.8,35.15-84.85,35.15s-62.19-12.48-84.85-35.15C17.48,187.19,5,157.05,5,125s12.48-62.19,35.15-84.85C62.81,17.48,92.95,5,125,5m0-5C55.96,0,0,55.96,0,125s55.96,125,125,125,125-55.96,125-125S194.04,0,125,0h0Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="uuid-9e11c10f-2d67-4caf-a85c-64f8a8abdac4" data-name="uuid-1a93fc72-ded1-41af-8e7c-010273c35667">
|
||||||
|
<path className="uuid-f651383e-729c-4543-82ed-5cea5662307d" d="m135.65,68.54c.11,0,.22,0,.33.02l.22.04,4.48,1.05,1.43.42c.34.1.67.2,1,.3.65.19,1.29.38,1.93.59l2.86,1.04c4.22,1.67,7.84,3.5,11.14,5.63,7.13,4.65,13.1,10.9,17.26,18.08,4.11,7.04,6.75,15.28,7.63,23.83.45,4.44.44,8.69,0,12.65-.47,4.09-1.28,7.61-2.49,10.77-.41,1.07-1.25,1.92-2.32,2.34s-2.27.38-3.3-.12c-1.95-.95-2.82-3.25-1.99-5.25,1.03-2.47,1.8-5.41,2.27-8.73.49-3.4.6-7.1.32-11-.56-7.54-2.66-14.85-6.09-21.13-3.46-6.4-8.55-12.04-14.71-16.32-2.89-1.99-6.12-3.74-9.87-5.34l-2.67-1.06c-.5-.19-1.07-.37-1.64-.55-.34-.11-.69-.22-1.03-.34l-1.32-.43-4.31-1.14c-1.1-.35-2.07-1.52-1.79-3.1.26-1.47,1.5-2.26,2.65-2.26h.01Z"/>
|
||||||
|
<path className="uuid-f651383e-729c-4543-82ed-5cea5662307d" d="m83.09,153.6c1.37,0,2.69.69,3.46,1.92,1.64,2.63,3.72,5.09,6.17,7.29,2.78,2.53,5.95,4.78,9.42,6.71,6.84,3.8,14.53,6.08,22.23,6.58,3.91.25,7.82.04,11.62-.64,3.78-.68,7.52-1.86,11.12-3.49,3.45-1.57,6.85-3.6,10.13-6.06,2.93-2.27,5.8-4.94,8.74-8.14l.19-.18c.87-.76,2.38-.97,3.57.11,1.15,1.04,1.14,2.52.54,3.45l-.2.26c-3.06,3.52-6.08,6.49-9.24,9.08-3.57,2.84-7.29,5.2-11.09,7.05-4.02,1.96-8.23,3.41-12.51,4.31-4.31.89-8.75,1.26-13.22,1.09-8.75-.34-17.55-2.68-25.45-6.78-4.05-2.1-7.78-4.61-11.08-7.44-3.07-2.62-5.71-5.57-7.83-8.76-.63-.95-.84-2.14-.57-3.25s1-2.06,2-2.61c.63-.35,1.31-.51,1.98-.51h.02Z"/>
|
||||||
|
<path className="uuid-f651383e-729c-4543-82ed-5cea5662307d" d="m113.94,67.47c.76,0,1.51.21,2.16.61.95.59,1.63,1.55,1.86,2.64.45,2.16-.92,4.32-3.05,4.82-6.07,1.42-12.11,4.08-17.49,7.68-5.92,3.97-11.02,9.05-14.75,14.69-3.86,5.81-6.41,12.52-7.38,19.41-.96,6.47-.53,13.57,1.27,21.12l.04.18c.18,1.15-.43,2.55-1.95,3-1.48.43-2.76-.39-3.25-1.43l-.09-.19-.05-.2c-2.23-8.2-2.93-16.03-2.09-23.27.86-7.84,3.53-15.57,7.72-22.32,4.04-6.55,9.66-12.5,16.23-17.2,6.1-4.36,12.75-7.52,19.76-9.4.35-.09.71-.14,1.06-.14Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="uuid-2b145d23-c492-471a-b246-f2e75af15020" data-name="uuid-977abf79-b623-4053-ab7b-a617d834d6cf">
|
||||||
|
<path className="uuid-f651383e-729c-4543-82ed-5cea5662307d" d="m178.97,83.02c1.57,3.93,4.57,7.01,8.46,8.68,2.89,1.24,5.95,1.55,8.84,1.06,3.25,6.6,5.62,13.81,6.98,21.39,2.3,12.74,1.62,25.68-1.97,37.4-1.85,6.05-4.48,11.82-7.82,17.13-3.3,5.27-7.37,10.18-12.08,14.61-4.45,4.19-9.63,8.08-15.34,11.52-5.3,3.08-11.08,5.82-17.68,8.38l-.52.2-.43.35c-1.4,1.12-1.93,3.01-1.32,4.71.71,1.95,2.75,3.08,4.74,2.61l.38-.11c7.11-2.5,13.39-5.24,19.25-8.39,6.44-3.6,12.28-7.71,17.37-12.19,5.48-4.82,10.26-10.24,14.2-16.11,4.02-5.96,7.25-12.47,9.59-19.35,4.54-13.3,5.8-28.07,3.64-42.72-1.38-9.42-4.14-18.45-8.11-26.71.41-.66.77-1.35,1.08-2.08,3.45-8.03-.28-17.36-8.31-20.81-1.88-.81-3.82-1.22-5.75-1.28-6.32-.19-12.42,3.43-15.06,9.58-1.67,3.89-1.73,8.19-.16,12.12h.02Z"/>
|
||||||
|
<path className="uuid-f651383e-729c-4543-82ed-5cea5662307d" d="m54.73,98.51c8.74.08,15.91-6.96,15.99-15.7.03-3.12-.86-6.05-2.4-8.52,3.95-4.62,8.41-8.85,13.25-12.51,8.91-6.76,19.25-11.58,29.93-13.93,10.7-2.38,22.16-2.24,33.16.39,10.09,2.34,20.49,7.06,30.91,14.02l.09.06c1.82,1.14,4.24.63,5.51-1.15,1.29-1.81.94-4.28-.78-5.61l-.1-.07c-11.02-7.96-22.22-13.52-33.26-16.51-7.2-2.01-14.65-3.05-22.05-3.12-5.27-.05-10.51.4-15.63,1.34-12.34,2.25-24.43,7.35-34.94,14.74-6.17,4.32-11.87,9.43-16.9,15.1-.81-.14-1.64-.21-2.49-.22-8.74-.08-15.91,6.96-15.99,15.7s6.96,15.91,15.7,15.99Z"/>
|
||||||
|
<path className="uuid-f651383e-729c-4543-82ed-5cea5662307d" d="m129.64,191.95c-1.28-.35-2.57-.54-3.87-.57-2.83-.06-5.64.64-8.17,2.08-2.59,1.47-4.65,3.6-6.04,6.15-6.41-1.32-12.79-3.45-18.92-6.36-10.76-5.15-20.35-12.54-27.73-21.36-7.56-8.99-12.97-19.82-15.67-31.34-2.59-10.69-2.85-22.71-.77-35.71l.04-.46c.06-2.05-1.43-3.83-3.46-4.16-.2-.03-.39-.05-.58-.05-1.62-.04-3.11.85-3.83,2.33l-.21.43-.09.47c-2.75,14.06-2.9,27.26-.44,39.21,2.59,13.11,8.31,25.6,16.56,36.12,8.05,10.33,18.68,19.12,30.75,25.42,7.48,3.87,15.37,6.72,23.35,8.48.3.83.67,1.65,1.12,2.44,2.1,3.68,5.5,6.32,9.58,7.44s8.35.58,12.03-1.51c3.68-2.1,6.32-5.5,7.44-9.58s.58-8.35-1.51-12.03c-2.1-3.68-5.5-6.32-9.58-7.44h0Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const logoMap: Record<string, React.ComponentType<React.SVGProps<SVGSVGElement>>> = {
|
||||||
|
casper_staging_testnet: casperLogo,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NetworkLogo = ({ value, className }: { value: string | undefined; className?: string }) => {
|
||||||
|
const Logo = logoMap[value ?? ""];
|
||||||
|
if (!Logo) return <div className="placeholder-logo" />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center text-sm border border-white/10">
|
||||||
|
<Logo className={className} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import { ReactNode, useEffect } from "react"
|
|
||||||
import { Navigate, Outlet, useLocation } from "react-router-dom"
|
|
||||||
import { useKeyring } from "../hooks"
|
|
||||||
|
|
||||||
export const ProtectedRoute = ({ children }: { children?: ReactNode }) => {
|
|
||||||
const location = useLocation()
|
|
||||||
const {
|
|
||||||
lock,
|
|
||||||
keyring: { isLocked, hasPassword },
|
|
||||||
} = useKeyring()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLocked && hasPassword && location.key === "default") {
|
|
||||||
lock()
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return !hasPassword ? (
|
|
||||||
<Navigate to="/welcome" replace />
|
|
||||||
) : isLocked ? (
|
|
||||||
<Navigate
|
|
||||||
to="/unlock-keyring"
|
|
||||||
replace
|
|
||||||
state={{
|
|
||||||
from: location,
|
|
||||||
pathname: location.pathname,
|
|
||||||
search: location.search,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
(children ?? <Outlet />)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -12,8 +12,8 @@ import {
|
|||||||
SelectContent,
|
SelectContent,
|
||||||
SelectGroup,
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
} from "../../../components/ui/select"
|
} from "@/components/ui/select"
|
||||||
import { Input } from "../../../components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
|
|
||||||
const reducer = (
|
const reducer = (
|
||||||
state: Partial<UserSignedExtensions>,
|
state: Partial<UserSignedExtensions>,
|
||||||
@ -73,20 +73,26 @@ export const UserSignedExtensionInputs = ({
|
|||||||
|
|
||||||
type TipInputProps = {
|
type TipInputProps = {
|
||||||
onChange(value: UserSignedExtensions["ChargeTransactionPayment"]): void
|
onChange(value: UserSignedExtensions["ChargeTransactionPayment"]): void
|
||||||
defaultValue?: number
|
defaultValue?: bigint
|
||||||
}
|
}
|
||||||
const TipInput = ({ onChange, defaultValue = 0 }: TipInputProps) => {
|
const TipInput = ({ onChange, defaultValue = 0n }: TipInputProps) => {
|
||||||
// @ts-expect-error TODO: improve bigint types
|
|
||||||
useEffect(() => onChange(defaultValue), [])
|
useEffect(() => onChange(defaultValue), [])
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2 text-white">
|
||||||
<div className="text-xs font-semibold mt-4">Tip Amount</div>
|
<div className="text-xs font-semibold mt-4">Tip Amount</div>
|
||||||
<Input
|
<Input
|
||||||
onChange={e => onChange(+e.target.value)}
|
onChange={e => {
|
||||||
|
const val = e.target.value.trim()
|
||||||
|
try {
|
||||||
|
onChange(val ? BigInt(val) : 0n)
|
||||||
|
} catch {
|
||||||
|
onChange(0n)
|
||||||
|
}
|
||||||
|
}}
|
||||||
aria-label="Request Origin"
|
aria-label="Request Origin"
|
||||||
type="number"
|
type="number"
|
||||||
className="pr-10 text-muted-foreground"
|
className="bg-input border-none text-white placeholder:text-white/40 h-11 px-4 rounded-lg focus-visible:ring-1 focus-visible:ring-white/20"
|
||||||
value={Number(defaultValue)}
|
value={defaultValue.toString()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -107,10 +113,10 @@ const MortalityInput = ({
|
|||||||
}: MortalityInputProps) => {
|
}: MortalityInputProps) => {
|
||||||
useEffect(() => onChange(defaultValue), [])
|
useEffect(() => onChange(defaultValue), [])
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2 text-white">
|
||||||
<div className="text-xs font-semibold mt-4">Transaction Mortality</div>
|
<div className="text-xs font-semibold mt-4">Transaction Mortality</div>
|
||||||
<Select onValueChange={(value) => onChange({ mortal: true, period: +value })}>
|
<Select onValueChange={(value: string) => onChange({ mortal: true, period: Number(value) })}>
|
||||||
<SelectTrigger className="w-full" data-testid="scheme-select">
|
<SelectTrigger className="w-full bg-input" data-testid="scheme-select">
|
||||||
<SelectValue placeholder="Blocks in Queue" />
|
<SelectValue placeholder="Blocks in Queue" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { type UserSignedExtensions as UserSignedExtensionsTy } from "../../../types/UserSignedExtension"
|
import { type UserSignedExtensions as UserSignedExtensionsTy } from "../../../types/UserSignedExtension"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
userSignedExtensions: Partial<UserSignedExtensionsTy>
|
userSignedExtensions: Partial<UserSignedExtensionsTy>
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
export * from "./DecodedCallData"
|
export * from "./DecodedCallData"
|
||||||
export * from "./UserSignedExtensionInputs"
|
export * from "./UserSignedExtensionInputs"
|
||||||
export * from "./UserSignedExtensions"
|
export * from "./UserSignedExtensions"
|
||||||
export * from "./ProtectedRoute"
|
|
||||||
export * from "./Header"
|
export * from "./Header"
|
||||||
export * from "./BottomNavBar"
|
export * from "./BottomNavBar"
|
||||||
export * from "./CopyButton"
|
export * from "./CopyButton"
|
||||||
|
export * from "./JohnMcAffee"
|
||||||
|
export * from "./NetworkLogo"
|
||||||
export { default as NetworkTab } from "./NetworkTab"
|
export { default as NetworkTab } from "./NetworkTab"
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export * from "./useKeyring"
|
export * from "./useChain"
|
||||||
|
export * from "./useAccounts"
|
||||||
|
|||||||
271
src/containers/WalletPopup/hooks/useAccounts.tsx
Normal file
271
src/containers/WalletPopup/hooks/useAccounts.tsx
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import { ReactNode, createContext, useContext, useState, useEffect, useMemo } from "react"
|
||||||
|
import useSWR, { KeyedMutator } from "swr"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { rpc } from "../api"
|
||||||
|
import { useChain } from "./"
|
||||||
|
|
||||||
|
import {
|
||||||
|
KeystoreMetaWithAddress,
|
||||||
|
ActiveAccount,
|
||||||
|
AccountTree,
|
||||||
|
} from "@/background/keystore/types"
|
||||||
|
|
||||||
|
import { RenameAccountArgs } from "@/background/types"
|
||||||
|
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/components/ui/card"
|
||||||
|
import { Form, FormField, FormItem, FormControl } from "@/components/ui/form"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Layout2 } from "@/components/Layout2"
|
||||||
|
import { JohnMcAffeeIcon } from "../components/JohnMcAffee"
|
||||||
|
|
||||||
|
const passwordSchema = z.object({
|
||||||
|
password: z.string().min(1, "password is required"),
|
||||||
|
})
|
||||||
|
|
||||||
|
type AccountsContextType = {
|
||||||
|
accounts: KeystoreMetaWithAddress[] | undefined
|
||||||
|
activeAccount: ActiveAccount | undefined
|
||||||
|
accountTree: AccountTree | undefined
|
||||||
|
|
||||||
|
activeAccountName: string
|
||||||
|
isFetchingAccounts: boolean
|
||||||
|
isFetchingActiveAccounts: boolean
|
||||||
|
isFetchingAccountTree: boolean
|
||||||
|
|
||||||
|
renameAccount(args: RenameAccountArgs): Promise<void>
|
||||||
|
selectAccount(activeAccount: ActiveAccount): Promise<void>
|
||||||
|
accountsStorageMutate(): Promise<void>
|
||||||
|
requestPassword: <R>(fn: (password: string) => Promise<R>) => Promise<R>
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccountsContext = createContext({} as AccountsContextType)
|
||||||
|
|
||||||
|
export const AccountsProvider = ({ children }: { children?: ReactNode }) => {
|
||||||
|
const { currentChain } = useChain()
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [password, setPassword] = useState<string>("")
|
||||||
|
const [rpcError, setRpcError] = useState<string | undefined>(undefined)
|
||||||
|
const [isPasswordOpen, setIsPasswordOpen] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const [deferred, setDeferred] = useState<{
|
||||||
|
execute: (password: string) => Promise<any>
|
||||||
|
resolve: (value: any) => void
|
||||||
|
reject: (err: Error) => void
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof passwordSchema>>({
|
||||||
|
resolver: zodResolver(passwordSchema),
|
||||||
|
defaultValues: { password: "" },
|
||||||
|
})
|
||||||
|
|
||||||
|
const { formState: passwordFormState, watch } = form
|
||||||
|
const passwordValue = watch("password")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (rpcError) setRpcError(undefined)
|
||||||
|
}, [passwordValue])
|
||||||
|
|
||||||
|
const requestPassword = <R,>(fn: (password: string) => Promise<R>): Promise<R> => {
|
||||||
|
return new Promise<R>((resolve, reject) => {
|
||||||
|
setRpcError(undefined)
|
||||||
|
setIsPasswordOpen(true)
|
||||||
|
setDeferred({
|
||||||
|
execute: async (password: string): Promise<any> => {
|
||||||
|
return await fn(password)
|
||||||
|
},
|
||||||
|
resolve,
|
||||||
|
reject
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: accounts, mutate: accountsMutate, isLoading: isFetchingAccounts } = useSWR(
|
||||||
|
currentChain?.chainId ? ["rpc.getAccounts", currentChain.chainId] : null,
|
||||||
|
([_, id]) => rpc.client.getAccounts(id),
|
||||||
|
{
|
||||||
|
revalidateOnFocus: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: activeAccount, mutate: activeAccountMutate, isLoading: isFetchingActiveAccounts } = useSWR(
|
||||||
|
currentChain?.chainId ? ["rpc.getActiveAccount", currentChain.chainId] : null,
|
||||||
|
([_, id]) => rpc.client.getActiveAccount(id),
|
||||||
|
{
|
||||||
|
revalidateOnFocus: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentAccountMetadata = useMemo(() => {
|
||||||
|
if (!activeAccount || !accounts) return undefined
|
||||||
|
return accounts.find(acc => acc.publicKey === activeAccount.publicKey)
|
||||||
|
}, [accounts, activeAccount])
|
||||||
|
|
||||||
|
const { data: accountTree, mutate: accountTreeMutate, isLoading: isFetchingAccountTree } = useSWR(
|
||||||
|
currentAccountMetadata ? ["rpc.getAccountDerivations", currentAccountMetadata] : null,
|
||||||
|
([_, acc]) => rpc.client.getAccountDerivations(acc),
|
||||||
|
{
|
||||||
|
revalidateOnFocus: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectAccount = async (newActiveAccount: ActiveAccount): Promise<void> => {
|
||||||
|
if (!currentChain) throw "current chain not found"
|
||||||
|
await rpc.client.setActiveAccount({
|
||||||
|
chainId: currentChain.chainId,
|
||||||
|
activeAccount: newActiveAccount,
|
||||||
|
})
|
||||||
|
await activeAccountMutate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const renameAccount = async (args: RenameAccountArgs): Promise<void> => {
|
||||||
|
await rpc.client.renameAccount(args)
|
||||||
|
await accountsMutate()
|
||||||
|
await activeAccountMutate()
|
||||||
|
await accountTreeMutate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountsStorageMutate = async (): Promise<void> => {
|
||||||
|
await activeAccountMutate()
|
||||||
|
await accountsMutate()
|
||||||
|
await accountTreeMutate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeAccountName = useMemo(() => {
|
||||||
|
if (!accountTree || !activeAccount) return ""
|
||||||
|
|
||||||
|
if (activeAccount.path) {
|
||||||
|
const activeDer = accountTree.derivations.find(
|
||||||
|
(der) => der.path === activeAccount.path
|
||||||
|
)
|
||||||
|
return `${accountTree.name} ● ${activeDer?.path ?? ""}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountTree.name
|
||||||
|
}, [accountTree, activeAccount])
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (deferred) deferred.reject(new Error("password input cancelled"))
|
||||||
|
setDeferred(null)
|
||||||
|
setRpcError(undefined)
|
||||||
|
setIsPasswordOpen(false)
|
||||||
|
form.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPasswordSubmit = async (data: z.infer<typeof passwordSchema>) => {
|
||||||
|
if (!deferred) return
|
||||||
|
setIsSubmitting(true)
|
||||||
|
setRpcError(undefined)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await deferred.execute(data.password)
|
||||||
|
deferred.resolve(result)
|
||||||
|
|
||||||
|
setDeferred(null)
|
||||||
|
setIsPasswordOpen(false)
|
||||||
|
|
||||||
|
form.reset()
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMsg = error?.message || "incorrect password given";
|
||||||
|
setRpcError(errorMsg)
|
||||||
|
deferred.reject(new Error(errorMsg))
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: AccountsContextType = {
|
||||||
|
accounts,
|
||||||
|
activeAccountName,
|
||||||
|
activeAccount,
|
||||||
|
accountTree,
|
||||||
|
|
||||||
|
isFetchingAccounts,
|
||||||
|
isFetchingActiveAccounts,
|
||||||
|
isFetchingAccountTree,
|
||||||
|
|
||||||
|
accountsStorageMutate,
|
||||||
|
selectAccount,
|
||||||
|
renameAccount,
|
||||||
|
requestPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccountsContext.Provider value={value}>
|
||||||
|
<div className={isPasswordOpen ? "hidden" : "w-full h-full"}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{isPasswordOpen && (
|
||||||
|
<Layout2>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onPasswordSubmit)}
|
||||||
|
className="flex flex-col w-full h-full max-h-full bg-muted justify-between"
|
||||||
|
>
|
||||||
|
<Card className="flex flex-col w-full h-full max-h-full shadow-none border-none rounded-none overflow-hidden bg-muted">
|
||||||
|
<CardContent className="flex flex-col items-center justify-start flex-grow w-full px-6 pt-8 pb-4 min-h-0 text-center">
|
||||||
|
<div className="w-[150px] h-[170px] drop-shadow-xl animate-fade-in flex items-center justify-center mb-6">
|
||||||
|
<JohnMcAffeeIcon className="w-full h-full object-contain" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-extrabold tracking-tight text-white mb-6">
|
||||||
|
Submit Password for {currentAccountMetadata?.name ?? ""}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{rpcError && (<div className="mb-6 w-full bg-error text-white p-3 rounded text-xs text-center font-medium leading-normal shadow-sm">
|
||||||
|
{rpcError}
|
||||||
|
</div>)}
|
||||||
|
|
||||||
|
<div className="w-full max-w-sm space-y-4 text-left">
|
||||||
|
<FormField
|
||||||
|
name="password"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter password"
|
||||||
|
className="bg-input border-none text-white placeholder:text-white/40 h-11 pl-4 pr-10 rounded-lg w-full focus-visible:ring-1 focus-visible:ring-white/20"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<CardFooter className="flex flex-col space-y-3 justify-end pb-8 px-6 flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="full"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Submitting..." : "Submit Password"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="full"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</Layout2>
|
||||||
|
)}
|
||||||
|
</AccountsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAccounts = () => useContext(AccountsContext)
|
||||||
69
src/containers/WalletPopup/hooks/useChain.tsx
Normal file
69
src/containers/WalletPopup/hooks/useChain.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { ReactNode, createContext, useContext, useState, useEffect, useMemo } from "react"
|
||||||
|
import useSWR from "swr"
|
||||||
|
import { rpc } from "../api"
|
||||||
|
|
||||||
|
import { networks, NetworkType } from "@/constants"
|
||||||
|
import * as storage from "@/background/storage"
|
||||||
|
|
||||||
|
type ChainContextType = {
|
||||||
|
currentChain: NetworkType | undefined
|
||||||
|
allChains: NetworkType[]
|
||||||
|
setCurrentChainId(id: string): Promise<void>
|
||||||
|
isLoadingChain: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChainContext = createContext({} as ChainContextType)
|
||||||
|
|
||||||
|
const STORAGE_CHAIN_KEY = "current_selected_chain_id"
|
||||||
|
|
||||||
|
export const ChainProvider = ({ children }: { children?: ReactNode }) => {
|
||||||
|
const [isLoadingChain, setIsLoadingChain] = useState<boolean>(true)
|
||||||
|
const [currentChainId, setCurrentChainIdState] = useState<string>("")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadSavedChain = async () => {
|
||||||
|
const stored = await storage.get(STORAGE_CHAIN_KEY)
|
||||||
|
const savedId = stored?.[STORAGE_CHAIN_KEY]
|
||||||
|
|
||||||
|
if (savedId && networks.some(n => n.chainId === savedId)) {
|
||||||
|
setCurrentChainIdState(savedId)
|
||||||
|
} else {
|
||||||
|
const defaultChainId = networks[0].chainId
|
||||||
|
setCurrentChainIdState(defaultChainId)
|
||||||
|
await storage.set(STORAGE_CHAIN_KEY, defaultChainId)
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoadingChain(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSavedChain()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const currentChain = useMemo(() => {
|
||||||
|
return networks.find(n => n.chainId === currentChainId)
|
||||||
|
}, [currentChainId])
|
||||||
|
|
||||||
|
const setCurrentChainId = async (id: string) => {
|
||||||
|
const chainExists = networks.some(n => n.chainId === id)
|
||||||
|
if (!chainExists) return
|
||||||
|
|
||||||
|
setCurrentChainIdState(id)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await storage.set(STORAGE_CHAIN_KEY, id)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save chain change:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: ChainContextType = {
|
||||||
|
currentChain,
|
||||||
|
allChains: networks,
|
||||||
|
setCurrentChainId,
|
||||||
|
isLoadingChain
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ChainContext.Provider value={value}>{children}</ChainContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useChain = () => useContext(ChainContext)
|
||||||
@ -1,75 +0,0 @@
|
|||||||
import { ReactNode, createContext, useContext, useEffect } from "react"
|
|
||||||
import { useLocation, useNavigate } from "react-router-dom"
|
|
||||||
import useSWR from "swr"
|
|
||||||
import { rpc } from "../api"
|
|
||||||
import { z } from "zod"
|
|
||||||
|
|
||||||
type Context = {
|
|
||||||
keyring: {
|
|
||||||
isLocked: boolean
|
|
||||||
hasPassword: boolean
|
|
||||||
}
|
|
||||||
unlock(password: string): Promise<void>
|
|
||||||
reset(password: string): Promise<void>
|
|
||||||
lock(): Promise<void>
|
|
||||||
refresh(): Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
const LocationStateSchema = z.object({
|
|
||||||
pathname: z.string(),
|
|
||||||
search: z.string().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const LockContext = createContext({} as Context)
|
|
||||||
|
|
||||||
export const KeyringProvider = ({ children }: { children?: ReactNode }) => {
|
|
||||||
const {
|
|
||||||
data: keyring,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
mutate,
|
|
||||||
} = useSWR("rpc.getKeyringState", () => rpc.client.getKeyringState())
|
|
||||||
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
if (isLoading || error) return null
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
await mutate()
|
|
||||||
}
|
|
||||||
const unlock = async (password: string) => {
|
|
||||||
await rpc.client.unlockKeyring(password)
|
|
||||||
await refresh()
|
|
||||||
|
|
||||||
const locationResult = LocationStateSchema.safeParse(location.state)
|
|
||||||
if (locationResult.success) {
|
|
||||||
navigate({
|
|
||||||
pathname: locationResult.data.pathname,
|
|
||||||
search: locationResult.data.search,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
navigate("/accounts")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const lock = async () => {
|
|
||||||
await rpc.client.lockKeyring()
|
|
||||||
await refresh()
|
|
||||||
navigate("/unlock-keyring")
|
|
||||||
}
|
|
||||||
const reset = async (password: string) => {
|
|
||||||
await rpc.client.resetKeyring(password)
|
|
||||||
await refresh()
|
|
||||||
navigate("/welcome")
|
|
||||||
}
|
|
||||||
const value = {
|
|
||||||
keyring: keyring!,
|
|
||||||
unlock,
|
|
||||||
lock,
|
|
||||||
reset,
|
|
||||||
refresh,
|
|
||||||
}
|
|
||||||
return <LockContext.Provider value={value}>{children}</LockContext.Provider>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useKeyring = () => useContext(LockContext)
|
|
||||||
@ -1,9 +1,11 @@
|
|||||||
import { ArrowLeft, Trash2, Eye } from "lucide-react"
|
import { ArrowLeft, Trash2, Eye } from "lucide-react"
|
||||||
import * as clipboard from "@zag-js/clipboard"
|
import * as clipboard from "@zag-js/clipboard"
|
||||||
import { useMachine, normalizeProps } from "@zag-js/react"
|
import { useMachine, normalizeProps } from "@zag-js/react"
|
||||||
import React, { useId, useState } from "react"
|
import React, { useId, useState, useMemo } from "react"
|
||||||
import { useLocation, useParams, useNavigate } from "react-router-dom"
|
import { useLocation, useParams, useNavigate } from "react-router-dom"
|
||||||
import { Layout2 } from "@/components/Layout2"
|
import { Copy, Check } from 'lucide-react'
|
||||||
|
import { Layout } from "@/components/Layout"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
@ -16,154 +18,218 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { rpc } from "@/containers/WalletPopup/api"
|
import { rpc } from "@/containers/WalletPopup/api"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
import { BottomNavBar, Header } from "../../components"
|
import { BottomNavBar, Header } from "../../components"
|
||||||
|
import { useAccounts, useChain } from "../../hooks"
|
||||||
|
|
||||||
export const AccountDetails: React.FC = () => {
|
export const AccountDetails: React.FC = () => {
|
||||||
const defaultSecretKey = "0xg00db0ybutwh4tareyou7ry1ngtofindhere1t1ss3cr3tinf0g0aw4yr1gh7n0w"
|
const defaultSecretKey = "0xg00db0ybutwh4tareyou7ry1ngtofindhere1t1ss3cr3tinf0g0aw4yr1gh7n0w"
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
const [secretKey, setSecretKey] = useState<string>(defaultSecretKey)
|
const { currentChain } = useChain()
|
||||||
const [error, setError] = useState<string>()
|
const {
|
||||||
|
activeAccountName,
|
||||||
|
accountTree,
|
||||||
|
renameAccount,
|
||||||
|
requestPassword,
|
||||||
|
} = useAccounts()
|
||||||
|
|
||||||
|
const [secretSeed, setSecretSeed] = useState<string>(defaultSecretKey)
|
||||||
|
const [newName, setNewName] = useState<string>()
|
||||||
const [isRevealed, setIsRevealed] = useState<boolean>()
|
const [isRevealed, setIsRevealed] = useState<boolean>()
|
||||||
const [isPending, setIsPending] = useState<boolean>()
|
|
||||||
|
|
||||||
const query = new URLSearchParams(location.search)
|
const { derivation } = useParams<{ derivation?: string }>()
|
||||||
const cryptoName = query.get("meta")
|
const derivationPath = useMemo(() => {
|
||||||
const cryptoIndex = query.get("index")
|
return derivation ? decodeURIComponent(derivation) : undefined;
|
||||||
|
}, [derivation])
|
||||||
|
|
||||||
|
const currentName = useMemo(() => {
|
||||||
|
if (!accountTree) return ""
|
||||||
|
|
||||||
|
if (!derivationPath) {
|
||||||
|
return accountTree.name
|
||||||
|
} else {
|
||||||
|
const der = accountTree.derivations.find(d => d.path == derivationPath)
|
||||||
|
return der ? der.name : ""
|
||||||
|
}
|
||||||
|
}, [derivationPath, accountTree])
|
||||||
|
|
||||||
|
const currentAddress = useMemo(() => {
|
||||||
|
if (!accountTree) return ""
|
||||||
|
|
||||||
|
if (!derivationPath) {
|
||||||
|
return accountTree.address
|
||||||
|
} else {
|
||||||
|
const der = accountTree.derivations.find(d => d.path == derivationPath)
|
||||||
|
return der ? der.address : ""
|
||||||
|
}
|
||||||
|
}, [derivationPath, accountTree])
|
||||||
|
|
||||||
const { accountId } = useParams<{ accountId: string }>()
|
|
||||||
const [state, send] = useMachine(
|
const [state, send] = useMachine(
|
||||||
clipboard.machine({
|
clipboard.machine({
|
||||||
id: useId(),
|
id: useId(),
|
||||||
value: accountId,
|
value: currentAddress ?? "",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const api = clipboard.connect(state, send, normalizeProps)
|
const api = clipboard.connect(state, send, normalizeProps)
|
||||||
|
|
||||||
if (!accountId) {
|
const handleBlur = async () => {
|
||||||
return null
|
if (!newName) {
|
||||||
}
|
setNewName(currentName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const pendingWrapper = (func) => {
|
const trimmedName = newName.trim()
|
||||||
return async (...args) => {
|
|
||||||
try {
|
if (trimmedName === "") {
|
||||||
setError("")
|
setNewName(currentName)
|
||||||
setIsPending(true)
|
return
|
||||||
await func(...args)
|
}
|
||||||
} catch (e) {
|
|
||||||
setError(e.message)
|
if (trimmedName === currentName || !accountTree) return
|
||||||
} finally {
|
|
||||||
setIsPending(false)
|
try {
|
||||||
setSecretKey(defaultSecretKey)
|
await renameAccount({
|
||||||
}
|
newName: trimmedName,
|
||||||
|
publicKey: accountTree.publicKey,
|
||||||
|
path: derivationPath
|
||||||
|
})
|
||||||
|
setNewName(trimmedName)
|
||||||
|
} catch {
|
||||||
|
setNewName(currentName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestSecretKey = async () => {
|
const requestSecretSeed = async () => {
|
||||||
const key = await rpc.client.revealCryptoKey(cryptoName, cryptoIndex)
|
if (isRevealed || !accountTree) return
|
||||||
setSecretKey(key)
|
|
||||||
|
const seed = await requestPassword(async (password) => {
|
||||||
|
return await rpc.client.revealSecretSeed({
|
||||||
|
publicKey: accountTree.publicKey,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
setSecretSeed(`0x${seed}`)
|
||||||
setIsRevealed(true)
|
setIsRevealed(true)
|
||||||
await new Promise((resolve) => {
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsRevealed(false)
|
setIsRevealed(false)
|
||||||
|
setSecretSeed(defaultSecretKey)
|
||||||
resolve()
|
resolve()
|
||||||
}, 5000)
|
}, 5000)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCryptoKey = async () => {
|
|
||||||
await rpc.client.clearCryptoKey(cryptoName, cryptoIndex)
|
|
||||||
navigate(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrappedRequestSecretKey = pendingWrapper(requestSecretKey)
|
|
||||||
const wrappedDeleteCryptoKey = pendingWrapper(deleteCryptoKey)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout2 innerClassName="bg-muted">
|
<Layout accountName={activeAccountName} chainValue={currentChain?.value} innerClassName="bg-muted">
|
||||||
<Header />
|
<Card className="border-none bg-muted h-full flex flex-col flex-grow w-full max-w-md shadow-none px-2">
|
||||||
<Card className="bg-muted grow">
|
<CardHeader className="space-y-1 pt-6 pb-2 px-6">
|
||||||
<CardHeader>
|
<CardTitle className="text-xl font-bold tracking-normal text-white">
|
||||||
<CardTitle>Address Details</CardTitle>
|
{derivationPath ? "Derivation" : "Account"} Details
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 flex flex-col">
|
<CardContent className="flex-grow px-6 py-2 space-y-4">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="text-sm text-primary">
|
<div className="space-y-1 animate-fade-in text-left">
|
||||||
Public address, {api.isCopied
|
<Label className="text-sm text-white/90">{derivationPath ? "Derivation" : "Account"} Name</Label>
|
||||||
? <span className="text-accent">copied to clipboard</span>
|
<Input
|
||||||
: <span>click on it to copy</span>
|
value={newName ? newName : currentName}
|
||||||
}
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
</div>
|
onBlur={handleBlur}
|
||||||
<Textarea
|
id="cryptoKeyName"
|
||||||
{...api.triggerProps}
|
type="text"
|
||||||
aria-label="Account Address"
|
placeholder="Enter account name"
|
||||||
id="account"
|
className={cn(
|
||||||
rows={2}
|
"bg-input border-none text-white placeholder:text-white/40 h-11 px-4 rounded-lg",
|
||||||
className="min-h-0 h-[60px] text-justify resize-none text-muted-foreground cursor-pointer"
|
"focus-visible:ring-1 focus-visible:ring-white/20"
|
||||||
placeholder={accountId}
|
)}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="text-sm text-primary">
|
|
||||||
Secret key, press reveal to make it visible
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<Textarea
|
|
||||||
readOnly
|
|
||||||
aria-label="Secret key"
|
|
||||||
id="secret"
|
|
||||||
rows={2}
|
|
||||||
className={`min-h-0 h-[60px] text-justify text-muted-foreground resize-none cursor-pointer`}
|
|
||||||
value={secretKey}
|
|
||||||
/>
|
/>
|
||||||
{!isRevealed && <div className="absolute w-full h-full z-10 inset-0 rounded-md bg-background/30 backdrop-blur-[4px]"></div>}
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 animate-fade-in text-left w-full">
|
||||||
|
<Label className="text-sm text-white/90">Public Address</Label>
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Textarea
|
||||||
|
onClick={() => api.copy()}
|
||||||
|
readOnly
|
||||||
|
aria-label="Public Address"
|
||||||
|
id="address"
|
||||||
|
rows={2}
|
||||||
|
className={cn(
|
||||||
|
"bg-input border-none laceholder:text-white/40 min-h-none h-[46px] cursor-pointer",
|
||||||
|
"py-2 pl-4 pr-12 rounded-lg focus-visible:ring-1 focus-visible:ring-white/20",
|
||||||
|
"w-full resize-none text-xs font-mono break-all leading-tight scrollbar-none",
|
||||||
|
api.isCopied ? "text-primary" : "text-white"
|
||||||
|
)}
|
||||||
|
value={currentAddress ?? ""}
|
||||||
|
placeholder="sf..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="absolute right-4 top-1/2 -translate-y-1/2 z-10">
|
||||||
|
<Button
|
||||||
|
onClick={() => api.copy()}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
"h-6 w-6 border-none ounded-md hover:bg-transparent",
|
||||||
|
api.isCopied ? "text-primary" : "text-white/85"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{api.isCopied ? (
|
||||||
|
<Check className="w-3.5 h-3.5" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1 animate-fade-in text-left">
|
||||||
|
<Label className="text-sm text-white/90">Secret seed, press reveal to make it visible</Label>
|
||||||
|
<div className="relative rounded-lg overflow-hidden">
|
||||||
|
<Textarea
|
||||||
|
onClick={() => requestSecretSeed()}
|
||||||
|
readOnly
|
||||||
|
aria-label="Secret seed"
|
||||||
|
id="secret"
|
||||||
|
rows={2}
|
||||||
|
className={cn(
|
||||||
|
"bg-input border-none text-white placeholder:text-white/40 h-11 px-4",
|
||||||
|
"rounded-lg focus-visible:ring-1 focus-visible:ring-white/20 w-full resize-none",
|
||||||
|
isRevealed ? "cursor-default" : "cursor-pointer select-none"
|
||||||
|
)}
|
||||||
|
value={secretSeed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isRevealed && (
|
||||||
|
<div className="absolute inset-0 z-10 bg-background/30 backdrop-blur-[4px] flex items-center justify-center pointer-events-none" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
|
||||||
<div className="text-sm font-medium text-destructive">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter className="flex justify-center pt-6 flex flex-col gap-2">
|
<CardFooter className="flex justify-center pt-6 flex flex-col gap-2">
|
||||||
<div className="w-full flex flex-row gap-2">
|
|
||||||
<Button
|
|
||||||
disabled={isPending || isRevealed}
|
|
||||||
onClick={wrappedRequestSecretKey}
|
|
||||||
variant="secondary"
|
|
||||||
size="full"
|
|
||||||
aria-label="Reveal key"
|
|
||||||
>
|
|
||||||
<Eye className="w-5 h-5 mr-2" />
|
|
||||||
Reveal Private
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled={isPending || isRevealed}
|
|
||||||
onClick={wrappedDeleteCryptoKey}
|
|
||||||
variant="secondary"
|
|
||||||
size="full"
|
|
||||||
aria-label="Delete Address"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-5 h-5 mr-2" />
|
|
||||||
Delete Address
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
disabled={isPending}
|
disabled={isRevealed}
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(`/delete-account/${derivation ? encodeURIComponent(derivation) : ""}`)}
|
||||||
variant="ghost"
|
variant="primary"
|
||||||
className="flex items-center mt-2 ml-0 w-full"
|
size="full"
|
||||||
aria-label="Go Back"
|
aria-label="Delete"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-5 h-5 mr-2" />
|
<Trash2 className="w-5 h-5 mr-2" />
|
||||||
Back
|
Delete {`${derivationPath ? "Derivation" : "Account"}`}
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<BottomNavBar currentItem="home" />
|
<BottomNavBar />
|
||||||
</Layout2>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,11 +11,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
import { ArrowRight, Trash2, KeyRound } from "lucide-react"
|
import { Input } from "@/components/ui/input"
|
||||||
import { useEffect, useState } from "react"
|
import { ArrowRight, Trash2, KeyRound, GitFork } from "lucide-react"
|
||||||
import { Layout2 } from "@/components/Layout2"
|
import { useMemo, useState, useEffect } from "react"
|
||||||
import { Link } from "react-router-dom"
|
import { Layout } from "@/components/Layout"
|
||||||
|
import { useNavigate, Link } from "react-router-dom"
|
||||||
import useSWR, { mutate } from "swr"
|
import useSWR, { mutate } from "swr"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import Fuse from "fuse.js"
|
||||||
import { rpc } from "@/containers/WalletPopup/api"
|
import { rpc } from "@/containers/WalletPopup/api"
|
||||||
import { ss58Address } from "@polkadot-labs/hdkd-helpers"
|
import { ss58Address } from "@polkadot-labs/hdkd-helpers"
|
||||||
import {
|
import {
|
||||||
@ -26,10 +29,24 @@ import {
|
|||||||
} from "@polkadot-labs/hdkd"
|
} from "@polkadot-labs/hdkd"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Header, BottomNavBar } from "../../components"
|
import { Header, BottomNavBar } from "../../components"
|
||||||
|
import { useAccounts, useChain } from "../../hooks"
|
||||||
import { networks } from "@/constants"
|
import { networks } from "@/constants"
|
||||||
|
import { Pencil, Copy, Check } from 'lucide-react'
|
||||||
|
import { AccountTree, ActiveAccount } from "@/background/keystore/types"
|
||||||
|
|
||||||
type Scheme = "Sr25519" | "Ed25519" | "Ecdsa"
|
type Scheme = "Sr25519" | "Ed25519" | "Ecdsa"
|
||||||
|
|
||||||
|
function truncate(address: string, startChars = 10, endChars = 10): string {
|
||||||
|
if (!address || address.length <= (startChars + endChars)) {
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = address.slice(0, startChars)
|
||||||
|
const end = address.slice(-endChars)
|
||||||
|
|
||||||
|
return `${start}...${end}`
|
||||||
|
}
|
||||||
|
|
||||||
const createDeriveFnMap: Record<Scheme, CreateDeriveFn> = {
|
const createDeriveFnMap: Record<Scheme, CreateDeriveFn> = {
|
||||||
Sr25519: sr25519CreateDerive,
|
Sr25519: sr25519CreateDerive,
|
||||||
Ed25519: ed25519CreateDerive,
|
Ed25519: ed25519CreateDerive,
|
||||||
@ -37,40 +54,163 @@ const createDeriveFnMap: Record<Scheme, CreateDeriveFn> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AccountCardProps = React.ComponentProps<typeof Card> & {
|
type AccountCardProps = React.ComponentProps<typeof Card> & {
|
||||||
name: string
|
accountTree?: AccountTree | undefined
|
||||||
ss58Address: string
|
path?: string | undefined
|
||||||
index: number
|
selectAccount(activeAccount: ActiveAccount): Promise<void>
|
||||||
meta: string | undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountCard: React.FC<AccountCardProps> = (props) => {
|
const AccountCard: React.FC<AccountCardProps> = ({ accountTree, selectAccount, path }) => {
|
||||||
if (!props.meta) return null
|
const activePublicKey = useMemo(() => {
|
||||||
|
if (!path) return accountTree?.publicKey
|
||||||
|
const derivation = accountTree?.derivations.find(der => der.path === path)
|
||||||
|
return derivation?.publicKey
|
||||||
|
}, [accountTree, path])
|
||||||
|
|
||||||
|
const [copied, setCopied] = useState<string>("")
|
||||||
|
|
||||||
|
const handleCopy = async (e: React.MouseEvent, text: string | undefined) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (!text || copied !== "") return
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
setCopied(text)
|
||||||
|
setTimeout(() => setCopied(""), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card data-testid={props.name} key={props.name} className="p-2 border-none rounded bg-background">
|
<div className="flex flex-col space-y-2 w-full">
|
||||||
<div className="flex items-center justify-between">
|
<Card
|
||||||
<div>
|
data-testid={accountTree?.name ?? ""}
|
||||||
<CardTitle className="font-semibold text-base mb-0.5">
|
onClick={() => accountTree ? selectAccount({ publicKey: accountTree.publicKey }) : {}}
|
||||||
{props.name ? props.name : "Root Address" }
|
className={`p-3 pb-1 border-none rounded-lg text-card-foreground transition-all shadow-md hover:bg-input ${
|
||||||
</CardTitle>
|
activePublicKey === accountTree?.publicKey ? "bg-input ring-2 ring-white ring-inset" : "bg-card cursor-pointer"
|
||||||
<CardDescription className="text-xs truncate max-w-[240px]">
|
}`}
|
||||||
{props.ss58Address}
|
>
|
||||||
</CardDescription>
|
<div className="flex items-center justify-between">
|
||||||
</div>
|
<div className="space-y-0">
|
||||||
<Button variant="ghost" asChild>
|
<CardTitle className="font-bold text-sm tracking-wide text-white">
|
||||||
<Link
|
{accountTree?.name ?? ""}
|
||||||
data-testid={`${props.name}-expand`}
|
</CardTitle>
|
||||||
to={`/accounts/${props.ss58Address}?meta=${props.meta}&index=${props.index}`}
|
<CardDescription
|
||||||
|
className={cn(
|
||||||
|
"h-[20px] text-[11px] flex items-center gap-1 font-mono",
|
||||||
|
copied === accountTree?.address
|
||||||
|
? "text-primary cursor-default"
|
||||||
|
: "text-white/60 cursor-pointer"
|
||||||
|
)}
|
||||||
|
onClick={(e) => handleCopy(e, accountTree?.address)}
|
||||||
|
>
|
||||||
|
<span className="w-full">{copied === accountTree?.address
|
||||||
|
? "Address copied to clipboard"
|
||||||
|
: truncate(accountTree?.address ?? "")
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
"border-none hover:bg-transparent h-6 w-6 shrink-0",
|
||||||
|
copied === accountTree?.address ? "text-primary" : "text-white/60"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{copied === accountTree?.address ? (
|
||||||
|
<Check className="w-3 h-3" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 rounded-md"
|
||||||
>
|
>
|
||||||
<ArrowRight className="w-4 h-4" />
|
<Link to="/account-details">
|
||||||
</Link>
|
<Pencil className="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Link>
|
||||||
</div>
|
</Button>
|
||||||
</Card>
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{accountTree && accountTree.derivations && (
|
||||||
|
<div className="relative pl-6 flex flex-col space-y-2">
|
||||||
|
<div className="absolute left-[10px] top-0 bottom-4 w-[1px] bg-white/20" />
|
||||||
|
|
||||||
|
{accountTree.derivations.map((der, j) => {
|
||||||
|
const derivationLabel = der.path && der.name
|
||||||
|
? `${der.path} • ${der.name}`
|
||||||
|
: der.path || `Derivation #${j}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={der.address || ""}
|
||||||
|
onClick={() => selectAccount({ publicKey: accountTree.publicKey, path: der.path })}
|
||||||
|
className={`p-3 pb-1 border-none rounded-lg text-card-foreground transition-all relative shadow-sm hover:bg-input ${
|
||||||
|
activePublicKey === der.publicKey ? "bg-input ring-2 ring-white ring-inset" : "bg-card cursor-pointer"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0">
|
||||||
|
<CardTitle className="font-semibold text-sm tracking-wide text-white/90">
|
||||||
|
{derivationLabel}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription
|
||||||
|
className={cn(
|
||||||
|
"h-[20px] text-[11px] flex items-center gap-1 font-mono",
|
||||||
|
copied === der?.address
|
||||||
|
? "text-primary cursor-default"
|
||||||
|
: "text-white/60 cursor-pointer"
|
||||||
|
)}
|
||||||
|
onClick={(e) => handleCopy(e, der?.address)}
|
||||||
|
>
|
||||||
|
<span className="w-full">{copied === der?.address
|
||||||
|
? "Address copied to clipboard"
|
||||||
|
: truncate(der?.address ?? "")
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
"border-none hover:bg-transparent h-6 w-6 shrink-0",
|
||||||
|
copied === der?.address ? "text-primary" : "text-white/60"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{copied === der?.address ? (
|
||||||
|
<Check className="w-3 h-3" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 rounded-md"
|
||||||
|
>
|
||||||
|
<Link to={`/account-details/${encodeURIComponent(der.path)}`}>
|
||||||
|
<Pencil className="w-3.5 h-3.5" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountsSkeleton: React.FC = () => {
|
const AccountsSkeleton: React.FC = () => {
|
||||||
return new Array(10).fill(null).map((_, i) => (
|
return new Array(4).fill(null).map((_, i) => (
|
||||||
<Card
|
<Card
|
||||||
key={`account-skeleton-card-${i}`}
|
key={`account-skeleton-card-${i}`}
|
||||||
className="p-2 mb-2 transition duration-300 border-none rounded bg-background"
|
className="p-2 mb-2 transition duration-300 border-none rounded bg-background"
|
||||||
@ -89,171 +229,90 @@ const AccountsSkeleton: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Accounts = () => {
|
export const Accounts = () => {
|
||||||
const { data: cryptoKeys, isLoading: isFetchingCryptoKeys } = useSWR(
|
const navigate = useNavigate()
|
||||||
"rpc.getCryptoKeys",
|
const { currentChain } = useChain()
|
||||||
() => rpc.client.getCryptoKeys(),
|
const {
|
||||||
{
|
activeAccount,
|
||||||
revalidateOnFocus: true,
|
activeAccountName,
|
||||||
},
|
accountTree,
|
||||||
)
|
selectAccount,
|
||||||
|
isFetchingAccountTree,
|
||||||
|
isFetchingAccounts,
|
||||||
|
isFetchingActiveAccounts,
|
||||||
|
} = useAccounts()
|
||||||
|
|
||||||
const [selectedCryptoKeyName, setSelectedCryptoKeyName] = useState<string | undefined>()
|
const [query, setQuery] = useState<string>("")
|
||||||
const [isPending, setIsPending] = useState<boolean>(false)
|
|
||||||
const [error, setError] = useState<string>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
const isFetching = useMemo(() => {
|
||||||
const storedCryptoKey = window.localStorage.getItem("selectedCryptoKeyName")
|
return isFetchingAccountTree || isFetchingAccounts || isFetchingActiveAccounts
|
||||||
if (!storedCryptoKey) return
|
}, [isFetchingAccountTree, isFetchingAccounts, isFetchingActiveAccounts])
|
||||||
|
|
||||||
setSelectedCryptoKeyName(storedCryptoKey)
|
const fuse = useMemo(() => {
|
||||||
}, [])
|
return new Fuse(accountTree?.derivations ?? [], {
|
||||||
|
keys: [
|
||||||
useEffect(() => {
|
{ name: "name", weight: 0.7 },
|
||||||
if (!selectedCryptoKeyName) window.localStorage.removeItem("selectedCryptoKeyName")
|
{ name: "path", weight: 0.5 },
|
||||||
else window.localStorage.setItem("selectedCryptoKeyName", selectedCryptoKeyName)
|
{ name: "address", weight: 0.4 },
|
||||||
}, [selectedCryptoKeyName])
|
],
|
||||||
|
threshold: 0.3,
|
||||||
const keygroup = cryptoKeys?.find((k) => k.name === selectedCryptoKeyName) ?? cryptoKeys?.[0]
|
distance: 100,
|
||||||
const accounts = keygroup?.accounts ?? []
|
ignoreLocation: true
|
||||||
|
|
||||||
const getChainPrefix = (chainId: string) => {
|
|
||||||
const foundPrefix = networks.find(network => network.chainId === chainId)?.prefix
|
|
||||||
return foundPrefix !== undefined ? foundPrefix : 1996
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingWrapper = (func: any) => {
|
|
||||||
return async (...args: any[]) => {
|
|
||||||
try {
|
|
||||||
setError("")
|
|
||||||
setIsPending(true)
|
|
||||||
await func(...args)
|
|
||||||
await mutate("rpc.getCryptoKeys")
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) setError(e.message)
|
|
||||||
} finally {
|
|
||||||
setIsPending(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deriveCryptoAccount = async (derivationPath: string) => {
|
|
||||||
let number = 0
|
|
||||||
const match = derivationPath.match(/\d+$/)
|
|
||||||
if (match) {
|
|
||||||
number = parseInt(match[0], 10) + 1
|
|
||||||
}
|
|
||||||
await rpc.client.updateCryptoKey({
|
|
||||||
name: selectedCryptoKeyName ? selectedCryptoKeyName : "",
|
|
||||||
deviation: number,
|
|
||||||
})
|
})
|
||||||
}
|
}, [accountTree])
|
||||||
|
|
||||||
const clearCryptoKeys = async (name: string) => {
|
const filteredAccountTree = useMemo(() => {
|
||||||
await rpc.client.clearCryptoKeys({ name })
|
if (!accountTree || accountTree.derivations.length <= 5) return accountTree
|
||||||
}
|
if (!query || query.trim() === "") return accountTree
|
||||||
|
|
||||||
const wrappedDeriveCryptoAccount = pendingWrapper(deriveCryptoAccount)
|
const searchedDerviations = fuse.search(query).map(obj => obj.item)
|
||||||
const wrappedClearCryptoKeys = pendingWrapper(clearCryptoKeys)
|
return { ...accountTree, derivations: searchedDerviations }
|
||||||
|
}, [query, accountTree, fuse])
|
||||||
|
|
||||||
const keysetAccounts = accounts
|
useEffect(() => {
|
||||||
.filter((account) => account.type === "Keyset")
|
if (isFetching) return
|
||||||
.map(({ path, publicKey, chainId }) => [path, ss58Address(publicKey, getChainPrefix(chainId))] as const)
|
if (!activeAccount && !accountTree) navigate("/welcome")
|
||||||
const keypairAccounts = accounts
|
}, [activeAccount, accountTree, isFetching])
|
||||||
.filter((account) => account.type === "Keypair")
|
|
||||||
.map(({ publicKey, chainId }) => ss58Address(publicKey, getChainPrefix(chainId)))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout2 innerClassName="bg-muted">
|
<Layout accountName={activeAccountName} chainValue={currentChain?.value} innerClassName="bg-muted">
|
||||||
<Header />
|
{accountTree && accountTree.derivations.length > 5 && (<div className="px-6 mt-4 mb-2">
|
||||||
|
<Input
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
id="query"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search your derviations"
|
||||||
|
className="bg-input border-none text-white placeholder:text-white/40 h-11 px-4 rounded-lg focus-visible:ring-1 focus-visible:ring-white/20"
|
||||||
|
/>
|
||||||
|
</div>)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between px-6 my-4 sm:px-8">
|
<div className="flex items-center justify-between px-6 my-4 sm:px-8">
|
||||||
<h2 className="text-xl font-semibold">Accounts</h2>
|
<h2 className="text-xl font-semibold text-white">Account Tree</h2>
|
||||||
<Select
|
<Button
|
||||||
disabled={!cryptoKeys || cryptoKeys.length === 0}
|
variant="secondary"
|
||||||
value={selectedCryptoKeyName}
|
size="icon"
|
||||||
onValueChange={(value) => setSelectedCryptoKeyName(value)}
|
onClick={() => navigate("/derive-account")}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[235px]" data-testid="accounts-select">
|
<GitFork className="w-4 h-4 transform rotate-180" />
|
||||||
<SelectValue placeholder="Select Crypto Key" />
|
</Button>
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>Crypto Keys</SelectLabel>
|
|
||||||
{(cryptoKeys ?? []).map((key) => (
|
|
||||||
<SelectItem
|
|
||||||
data-testid={`accounts-select-${key.name}`}
|
|
||||||
key={key.name}
|
|
||||||
value={key.name}
|
|
||||||
>
|
|
||||||
{`${key.name} <${key.scheme.toLowerCase()}>`}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScrollArea className="px-6 mb-4 grow sm:px-8">
|
<ScrollArea className="px-6 mb-4 grow sm:px-8">
|
||||||
<section>
|
<section>
|
||||||
{isFetchingCryptoKeys && <AccountsSkeleton />}
|
{isFetching && <AccountsSkeleton />}
|
||||||
{!isFetchingCryptoKeys && (
|
{!isFetching && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{keysetAccounts.map(([derivationPath, ss58Address], i) => (
|
<AccountCard
|
||||||
<AccountCard
|
accountTree={filteredAccountTree}
|
||||||
index={i}
|
selectAccount={selectAccount}
|
||||||
meta={selectedCryptoKeyName}
|
path={activeAccount?.path}
|
||||||
name={derivationPath}
|
/>
|
||||||
ss58Address={ss58Address}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{keypairAccounts.map((ss58Address, i) => (
|
|
||||||
<AccountCard
|
|
||||||
index={i}
|
|
||||||
meta={selectedCryptoKeyName}
|
|
||||||
name={`Account ${i}`}
|
|
||||||
ss58Address={ss58Address}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{error && (
|
|
||||||
<div className="text-sm font-medium text-destructive">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
{!isFetchingCryptoKeys && (
|
|
||||||
<div className="pt-5 flex flex-row justify-between gap-2">
|
|
||||||
{keysetAccounts.length > 0 && (
|
|
||||||
<Button
|
|
||||||
disabled={isPending || isFetchingCryptoKeys}
|
|
||||||
variant="secondary"
|
|
||||||
size="full"
|
|
||||||
className="flex items-center"
|
|
||||||
onClick={() => wrappedDeriveCryptoAccount(keysetAccounts[keysetAccounts.length - 1][0])}
|
|
||||||
>
|
|
||||||
<KeyRound className="w-5 h-5 mr-2" />
|
|
||||||
Create Address
|
|
||||||
</Button>)
|
|
||||||
}
|
|
||||||
{(keysetAccounts.length > 0 || keypairAccounts.length > 0) && (
|
|
||||||
<Button
|
|
||||||
disabled={isPending || isFetchingCryptoKeys}
|
|
||||||
variant="secondary"
|
|
||||||
size="full"
|
|
||||||
className="flex items-center"
|
|
||||||
onClick={() => wrappedClearCryptoKeys(selectedCryptoKeyName).then((_) => {
|
|
||||||
setSelectedCryptoKeyName(cryptoKeys?.at(-2)?.name)
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-5 h-5 mr-2" />
|
|
||||||
Delete Account
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
<BottomNavBar currentItem="home" />
|
<BottomNavBar currentItem="home" />
|
||||||
</Layout2>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ArrowLeftIcon, ArrowRightIcon, CheckCircleIcon } from "lucide-react"
|
import { ArrowLeftIcon, ArrowRightIcon, CheckCircleIcon, Eye, EyeOff } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useState, useEffect, useMemo } from "react"
|
||||||
import {
|
import {
|
||||||
entropyToMiniSecret,
|
entropyToMiniSecret,
|
||||||
generateMnemonic,
|
generateMnemonic,
|
||||||
@ -8,8 +8,8 @@ import {
|
|||||||
import { networks } from "@/constants"
|
import { networks } from "@/constants"
|
||||||
import { SubmitHandler, useForm } from "react-hook-form"
|
import { SubmitHandler, useForm } from "react-hook-form"
|
||||||
import { toHex } from "@polkadot-api/utils"
|
import { toHex } from "@polkadot-api/utils"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate, useLocation } from "react-router-dom"
|
||||||
import { rpc } from "../../api"
|
import { rpc } from "@/containers/WalletPopup/api"
|
||||||
import { sr25519CreateDerive } from "@polkadot-labs/hdkd"
|
import { sr25519CreateDerive } from "@polkadot-labs/hdkd"
|
||||||
import { bytesToHex } from "@noble/ciphers/utils"
|
import { bytesToHex } from "@noble/ciphers/utils"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
@ -46,251 +46,309 @@ import { zodResolver } from "@hookform/resolvers/zod"
|
|||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
import { useChain, useAccounts } from "../../hooks"
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
cryptoKeyName: z
|
seedPhraseConfirmed: z.boolean().refine((val) => val === true, {
|
||||||
.string({ required_error: "This field is required." })
|
message: "You must confirm that you saved your seed phrase",
|
||||||
.min(1, "This field cannot be empty.")
|
|
||||||
.refine(
|
|
||||||
async (name) => {
|
|
||||||
const existingKeys = (await rpc.client.getCryptoKeys()).map(
|
|
||||||
(key) => key.name,
|
|
||||||
)
|
|
||||||
return !existingKeys.includes(name)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "Name already exists",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
scheme: z.enum(["Sr25519", "Ed25519", "Ecdsa"]),
|
|
||||||
seedPhraseConfirmed: z.boolean().refine((value) => value, {
|
|
||||||
message: "Please confirm that you have written down the mnemonic.",
|
|
||||||
}),
|
}),
|
||||||
|
cryptoKeyName: z
|
||||||
|
.string({ required_error: "Account name is required" })
|
||||||
|
.min(1, "Account name is required"),
|
||||||
|
password: z.string().min(1, "Password must be provided"),
|
||||||
|
confirmPassword: z.string().min(1, "Please confirm your password"),
|
||||||
|
}).superRefine((data, ctx) => {
|
||||||
|
if (data.password && data.confirmPassword && data.password !== data.confirmPassword) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Passwords do not match",
|
||||||
|
path: ["confirmPassword"],
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const AddAccount = () => {
|
export const AddAccount = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
const { currentChain } = useChain()
|
||||||
|
const { accountsStorageMutate } = useAccounts();
|
||||||
|
|
||||||
const [mnemonic, _] = useState(generateMnemonic(256).split(" "))
|
const [mnemonic, _] = useState(generateMnemonic(256).split(" "))
|
||||||
|
const [showPassword, setShowPassword] = useState<boolean>(false)
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState<boolean>(false)
|
||||||
|
const [customError, setCustomError] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
|
const isMiniSecretExternal = useMemo(() => {
|
||||||
|
if (location.state?.miniSecret) return true
|
||||||
|
return false
|
||||||
|
}, [location])
|
||||||
|
|
||||||
|
const miniSecretHex = useMemo(() => {
|
||||||
|
if (isMiniSecretExternal) {
|
||||||
|
return bytesToHex(location.state.miniSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
const entropy = mnemonicToEntropy(mnemonic.join(" "))
|
||||||
|
return bytesToHex(entropyToMiniSecret(entropy))
|
||||||
|
}, [mnemonic, isMiniSecretExternal])
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof schema>>({
|
const form = useForm<z.infer<typeof schema>>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
seedPhraseConfirmed: isMiniSecretExternal,
|
||||||
cryptoKeyName: "",
|
cryptoKeyName: "",
|
||||||
scheme: "Sr25519",
|
password: "",
|
||||||
seedPhraseConfirmed: false,
|
confirmPassword: "",
|
||||||
},
|
},
|
||||||
|
mode: "onSubmit",
|
||||||
|
shouldUnregister: false,
|
||||||
})
|
})
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
trigger,
|
trigger,
|
||||||
formState: { isSubmitting },
|
watch,
|
||||||
|
setError,
|
||||||
|
formState: { isSubmitting, errors },
|
||||||
} = form
|
} = form
|
||||||
|
|
||||||
|
const isSeedConfirmed = watch("seedPhraseConfirmed")
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<z.infer<typeof schema>> = async (data) => {
|
const onSubmit: SubmitHandler<z.infer<typeof schema>> = async (data) => {
|
||||||
|
if (!currentChain) throw "current chain not found"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const entropy = mnemonicToEntropy(mnemonic.join(" "))
|
await rpc.client.insertAccount({
|
||||||
const miniSecret = entropyToMiniSecret(entropy)
|
|
||||||
const derive = sr25519CreateDerive(miniSecret)
|
|
||||||
|
|
||||||
const derivationPaths = networks.map((network) => {
|
|
||||||
const path = `//${network.value}/0`
|
|
||||||
return {
|
|
||||||
chainId: network.chainId,
|
|
||||||
path,
|
|
||||||
publicKey: toHex(derive(`//${network.value}/0`).publicKey),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await rpc.client.insertCryptoKey({
|
|
||||||
type: "Keyset",
|
|
||||||
name: data.cryptoKeyName,
|
name: data.cryptoKeyName,
|
||||||
scheme: data.scheme,
|
scheme: "Sr25519",
|
||||||
createdAt: Date.now(),
|
password: data.password,
|
||||||
miniSecret: bytesToHex(miniSecret),
|
chainId: currentChain.chainId,
|
||||||
derivationPaths,
|
miniSecret: miniSecretHex,
|
||||||
})
|
})
|
||||||
|
|
||||||
window.localStorage.setItem("selectedCryptoKeyName", data.cryptoKeyName)
|
await accountsStorageMutate()
|
||||||
} finally {
|
|
||||||
navigate("/accounts")
|
navigate("/accounts", { replace: true, state: {} })
|
||||||
|
} catch (error) {
|
||||||
|
setCustomError((error as any)?.message || "An unknown error occurred")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = watch((value, { name, type }) => {
|
||||||
|
if (['cryptoKeyName', 'password', 'confirmPassword'].includes(name || '')) {
|
||||||
|
setCustomError(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => subscription.unsubscribe()
|
||||||
|
}, [watch, setCustomError])
|
||||||
|
|
||||||
const [step, setStep] = useState(1)
|
const [step, setStep] = useState(1)
|
||||||
|
const currentStep = useMemo(() => {
|
||||||
|
if (isMiniSecretExternal) return 2
|
||||||
|
return step
|
||||||
|
}, [step, isMiniSecretExternal])
|
||||||
|
|
||||||
const handleNext = async () => {
|
const handleNext = async () => {
|
||||||
switch (step) {
|
const isChecked = await trigger("seedPhraseConfirmed")
|
||||||
case 1:
|
if (isChecked) setStep(2)
|
||||||
if (!(await trigger("cryptoKeyName"))) return
|
};
|
||||||
break
|
|
||||||
case 2:
|
|
||||||
if (!(await trigger("scheme"))) return
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
setStep((prevStep) => Math.min(prevStep + 1, 3))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
setStep((prevStep) => Math.max(prevStep - 1, 1))
|
if (isMiniSecretExternal) navigate("/import-accounts", { replace: true, state: {} })
|
||||||
}
|
if (step === 1) navigate(-1)
|
||||||
|
setStep(1)
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout2 innerClassName="bg-muted">
|
<Layout2>
|
||||||
<Header />
|
<Form {...form}>
|
||||||
<ScrollArea className="grow">
|
<form className="h-full flex flex-col" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Form {...form}>
|
<Card className="bg-muted border-none h-full flex flex-col flex-grow w-full max-w-md shadow-none">
|
||||||
<form className="relative grow" onSubmit={handleSubmit(onSubmit)}>
|
<CardHeader className="space-y-1 pt-6 pb-2 px-6">
|
||||||
<Card className="border-none bg-muted h-full flex flex-col flex-grow w-full max-w-md shadow-none">
|
<CardTitle className="text-xl font-bold tracking-normal text-white">
|
||||||
<CardHeader className="space-y-0 pt-4">
|
{currentStep === 1 ? "Save Mnemonic Seed" : "Create Account"}
|
||||||
<CardTitle className="text-xl font-semibold tracking-normal">
|
</CardTitle>
|
||||||
{step === 1
|
<CardDescription className="text-primary font-medium text-sm">
|
||||||
? "Choose Name"
|
Step {currentStep} of 2
|
||||||
: step === 2
|
</CardDescription>
|
||||||
? "Select Crypto Scheme"
|
</CardHeader>
|
||||||
: "Review Seed Phrase"}
|
|
||||||
</CardTitle>
|
<CardContent className="flex-grow px-6 py-2 space-y-4">
|
||||||
<CardDescription className="text-accent">
|
{currentStep === 2 && (errors.cryptoKeyName || errors.password || errors.confirmPassword || customError) && (
|
||||||
Step {step} of 3
|
<div className="bg-error text-white p-3 rounded text-xs text-center font-medium leading-normal shadow-sm">
|
||||||
</CardDescription>
|
{errors.cryptoKeyName?.message ||
|
||||||
</CardHeader>
|
errors.password?.message ||
|
||||||
<CardContent>
|
errors.confirmPassword?.message ||
|
||||||
{step === 1 && (
|
customError}
|
||||||
<div className="step-1">
|
</div>
|
||||||
<FormField
|
)}
|
||||||
name="cryptoKeyName"
|
|
||||||
control={control}
|
{currentStep === 2 && !(errors.cryptoKeyName || errors.password || errors.confirmPassword || customError) && (
|
||||||
render={({ field }) => (
|
<div className="bg-success text-white p-3 rounded text-xs text-center font-medium leading-normal shadow-sm animate-fade-in">
|
||||||
<FormItem>
|
All fields are valid. Ready to generate account!
|
||||||
<FormDescription className="mb-4 text-primary text-xs">
|
</div>
|
||||||
Set a name for your crypto key. This name will be
|
)}
|
||||||
used to identify your crypto key within the
|
|
||||||
extension.
|
{currentStep === 1 && (
|
||||||
</FormDescription>
|
<div className="space-y-4">
|
||||||
|
<div className="bg-input rounded-lg p-4 min-h-[110px] flex flex-col justify-start border border-white/5 shadow-inner">
|
||||||
|
<p className={cn(
|
||||||
|
"text-sm font-medium tracking-wide leading-relaxed text-justify cursor-pointer select-all select-none transition-all duration-300",
|
||||||
|
"text-white/90 blur-[3px] hover:blur-none"
|
||||||
|
)}>
|
||||||
|
{mnemonic.join(" ")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name="seedPhraseConfirmed"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className={cn(
|
||||||
|
"flex flex-row items-center space-x-3 space-y-0",
|
||||||
|
"rounded-md border border-white/10 px-4 py-2 bg-input"
|
||||||
|
)}>
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel className="text-sm font-medium text-white cursor-pointer">
|
||||||
|
I have written down my mnemonic seed
|
||||||
|
</FormLabel>
|
||||||
|
<p className="text-[11px] text-white/40">
|
||||||
|
Mnemonic seed is the only way to restore your account. Never share it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 2 && (
|
||||||
|
<div className="space-y-4 animate-fade-in text-left">
|
||||||
|
<FormField
|
||||||
|
name="cryptoKeyName"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="space-y-1.5">
|
||||||
|
<Label className="text-sm text-white/90">Account Name</Label>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="cryptoKeyName"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter account name"
|
||||||
|
className="bg-input border-none text-white placeholder:text-white/40 h-11 px-4 rounded-lg focus-visible:ring-1 focus-visible:ring-white/20"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name="password"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="space-y-1.5">
|
||||||
|
<Label className="text-sm text-white/90">Password</Label>
|
||||||
|
<div className="relative flex items-center">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
id="cryptoKeyName"
|
id="password"
|
||||||
type="text"
|
type={showPassword ? "text" : "password"}
|
||||||
placeholder="Set the key name"
|
placeholder="Enter new password"
|
||||||
|
className="bg-input border-none text-white placeholder:text-white/40 h-11 pl-4 pr-10 rounded-lg w-full focus-visible:ring-1 focus-visible:ring-white/20"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<button
|
||||||
</FormItem>
|
type="button"
|
||||||
)}
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
/>
|
className="absolute right-3 text-white/40 hover:text-white/70 transition-colors"
|
||||||
</div>
|
>
|
||||||
)}
|
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
{step === 2 && (
|
</button>
|
||||||
<div className="step-2">
|
</div>
|
||||||
<FormField
|
</FormItem>
|
||||||
name="scheme"
|
)}
|
||||||
control={control}
|
/>
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
<FormField
|
||||||
<FormDescription className="mb-4 text-primary text-xs">
|
name="confirmPassword"
|
||||||
Choose the cryptographic signature scheme that meets your
|
control={control}
|
||||||
requirements (default: Sr25519)
|
render={({ field }) => (
|
||||||
</FormDescription>
|
<FormItem className="space-y-1.5">
|
||||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
<Label className="text-sm text-white/90">Confirm Password</Label>
|
||||||
<FormControl>
|
<div className="relative flex items-center">
|
||||||
<RadioGroup
|
<FormControl>
|
||||||
onValueChange={(scheme) => field.onChange(scheme)}
|
<Input
|
||||||
value={field.value}
|
{...field}
|
||||||
>
|
id="confirmPassword"
|
||||||
<RadioGroupItem value="Sr25519" id="Sr25519" label="Sr25519" />
|
type={showConfirmPassword ? "text" : "password"}
|
||||||
<RadioGroupItem value="Ed25519" id="Ed25519" label="Ed25519" />
|
placeholder="Confirm new password"
|
||||||
<RadioGroupItem value="Ecdsa" id="ECDSA" label="ECDSA" />
|
className="bg-input border-none text-white placeholder:text-white/40 h-11 pl-4 pr-10 rounded-lg w-full focus-visible:ring-1 focus-visible:ring-white/20"
|
||||||
</RadioGroup>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
<button
|
||||||
<FormMessage />
|
type="button"
|
||||||
</FormItem>
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
)}
|
className="absolute right-3 text-white/40 hover:text-white/70 transition-colors"
|
||||||
/>
|
>
|
||||||
</div>
|
{showConfirmPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
)}
|
</button>
|
||||||
{step === 3 && (
|
</div>
|
||||||
<div className="step-3">
|
</FormItem>
|
||||||
<FormField
|
)}
|
||||||
name="seedPhraseConfirmed"
|
/>
|
||||||
control={control}
|
</div>
|
||||||
render={({ field }) => (
|
)}
|
||||||
<FormItem className="space-y-0 red">
|
</CardContent>
|
||||||
<FormDescription className="mb-4 text-primary text-xs">
|
|
||||||
Your seed phrase is a unique set of words that you
|
<CardFooter className="flex w-full gap-x-4 mt-6 px-6 pb-6">
|
||||||
need to back up and recover your wallet. Do not
|
<Button
|
||||||
share this with anyone.
|
type="button"
|
||||||
</FormDescription>
|
disabled={isSubmitting}
|
||||||
<Textarea
|
onClick={handleBack}
|
||||||
readOnly
|
className="flex-1 flex items-center justify-center space-x-2"
|
||||||
disabled={isSubmitting}
|
variant="secondary"
|
||||||
aria-label="Created mnemonic phrase"
|
>
|
||||||
id="text"
|
<ArrowLeftIcon className="w-4 h-4" />
|
||||||
rows={4}
|
<span>Back</span>
|
||||||
className={cn(
|
</Button>
|
||||||
"min-h-auto h-auto text-justify resize-none cursor-pointer",
|
|
||||||
"blur-[2px] hover:blur-none"
|
{currentStep === 1 ? (
|
||||||
)}
|
<Button
|
||||||
placeholder={mnemonic.join(" ")}
|
type="button"
|
||||||
/>
|
disabled={isSubmitting || !isSeedConfirmed}
|
||||||
<div className="flex pt-4 pb-2">
|
onClick={handleNext}
|
||||||
<FormControl className="mr-2">
|
className="flex-1 flex items-center justify-center space-x-2"
|
||||||
<Checkbox
|
variant="secondary"
|
||||||
aria-label="I have written down my seed phrase."
|
>
|
||||||
checked={field.value}
|
<span>Next</span>
|
||||||
onCheckedChange={field.onChange}
|
<ArrowRightIcon className="w-4 h-4" />
|
||||||
/>
|
</Button>
|
||||||
</FormControl>
|
) : (
|
||||||
<Label className="block text-primary">
|
<Button
|
||||||
I have written down my seed phrase.
|
type="submit"
|
||||||
</Label>
|
disabled={isSubmitting}
|
||||||
</div>
|
className="flex-1 flex items-center justify-center space-x-2"
|
||||||
<FormMessage className="text-xs" />
|
variant="primary"
|
||||||
</FormItem>
|
>
|
||||||
)}
|
<CheckCircleIcon className="w-4 h-4" />
|
||||||
/>
|
<span>Create</span>
|
||||||
</div>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardFooter>
|
||||||
<CardFooter className="flex justify-between mt-6">
|
</Card>
|
||||||
{step > 1 && (
|
</form>
|
||||||
<Button
|
</Form>
|
||||||
disabled={isSubmitting}
|
|
||||||
onClick={handleBack}
|
|
||||||
className="flex items-center space-x-2"
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon className="w-4 h-4" />
|
|
||||||
<span>Back</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{step < 3 ? (
|
|
||||||
<Button
|
|
||||||
disabled={isSubmitting}
|
|
||||||
onClick={handleNext}
|
|
||||||
className="flex items-center ml-auto space-x-2"
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
<span>Next</span>
|
|
||||||
<ArrowRightIcon className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="flex items-center ml-auto space-x-2"
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
<CheckCircleIcon className="w-4 h-4" />
|
|
||||||
<span>Finish</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</ScrollArea>
|
|
||||||
<BottomNavBar currentItem="add" />
|
<BottomNavBar currentItem="add" />
|
||||||
</Layout2>
|
</Layout2>
|
||||||
)
|
)
|
||||||
|
|||||||
254
src/containers/WalletPopup/pages/Accounts/DeleteAccount.tsx
Normal file
254
src/containers/WalletPopup/pages/Accounts/DeleteAccount.tsx
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
import { Card, CardDescription, CardTitle } from "@/components/ui/card"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import { ArrowRight, Trash2, KeyRound, GitFork } from "lucide-react"
|
||||||
|
import { useMemo, useState, useEffect } from "react"
|
||||||
|
import { Layout } from "@/components/Layout"
|
||||||
|
import { useParams, useNavigate, Link } from "react-router-dom"
|
||||||
|
import useSWR, { mutate } from "swr"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { rpc } from "@/containers/WalletPopup/api"
|
||||||
|
import { ss58Address } from "@polkadot-labs/hdkd-helpers"
|
||||||
|
import {
|
||||||
|
CreateDeriveFn,
|
||||||
|
ecdsaCreateDerive,
|
||||||
|
ed25519CreateDerive,
|
||||||
|
sr25519CreateDerive,
|
||||||
|
} from "@polkadot-labs/hdkd"
|
||||||
|
import React from "react"
|
||||||
|
import { Header, BottomNavBar } from "../../components"
|
||||||
|
import { useAccounts, useChain } from "../../hooks"
|
||||||
|
import { networks } from "@/constants"
|
||||||
|
import { Pencil, Copy, Check } from 'lucide-react'
|
||||||
|
|
||||||
|
|
||||||
|
import { AccountTree, DerivationWithAddress } from "@/background/keystore/types"
|
||||||
|
|
||||||
|
type Scheme = "Sr25519" | "Ed25519" | "Ecdsa"
|
||||||
|
|
||||||
|
function truncate(address: string, startChars = 10, endChars = 10): string {
|
||||||
|
if (!address || address.length <= (startChars + endChars)) {
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = address.slice(0, startChars)
|
||||||
|
const end = address.slice(-endChars)
|
||||||
|
|
||||||
|
return `${start}...${end}`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DerivationCardProps = React.ComponentProps<typeof Card> & {
|
||||||
|
derivation?: DerivationWithAddress | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountCardProps = React.ComponentProps<typeof Card> & {
|
||||||
|
accountTree?: AccountTree | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const DerivationCard: React.FC<DerivationCardProps> = ({ derivation }) => {
|
||||||
|
const [copied, setCopied] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const handleCopy = async (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (copied || !derivation) return
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(derivation.address)
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={derivation?.address || ""}
|
||||||
|
className="p-3 pb-1 border-none rounded-lg text-card-foreground transition-all relative shadow-sm bg-card cursor-pointer"
|
||||||
|
>
|
||||||
|
<CardTitle className="font-semibold text-sm tracking-wide text-white/90">
|
||||||
|
{`${derivation?.path ?? ""} ● ${derivation?.name ?? ""}`}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription
|
||||||
|
className={cn(
|
||||||
|
"h-[20px] text-[11px] flex items-center justify-start gap-2 font-mono",
|
||||||
|
copied
|
||||||
|
? "text-primary cursor-default"
|
||||||
|
: "text-white/60 cursor-pointer"
|
||||||
|
)}
|
||||||
|
onClick={(e) => handleCopy(e)}
|
||||||
|
>
|
||||||
|
<span>{copied
|
||||||
|
? "Address copied to clipboard"
|
||||||
|
: truncate(derivation?.address ?? "")
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
"border-none hover:bg-transparent h-6 w-6 shrink-0",
|
||||||
|
copied ? "text-primary" : "text-white/60"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="w-3 h-3" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardDescription>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccountCard: React.FC<AccountCardProps> = ({ accountTree }) => {
|
||||||
|
const [copied, setCopied] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const handleCopy = async (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (copied || !accountTree) return
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(accountTree.address)
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-2 w-full">
|
||||||
|
<Card
|
||||||
|
data-testid={accountTree?.name ?? ""}
|
||||||
|
className="p-3 pb-1 border-none rounded-lg text-card-foreground transition-all shadow-md bg-card cursor-pointer"
|
||||||
|
>
|
||||||
|
<CardTitle className="font-bold text-sm tracking-wide text-white">
|
||||||
|
{accountTree?.name ?? ""}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription
|
||||||
|
className={cn(
|
||||||
|
"h-[20px] text-[11px] flex items-center justify-start gap-2 font-mono",
|
||||||
|
copied
|
||||||
|
? "text-primary cursor-default"
|
||||||
|
: "text-white/60 cursor-pointer"
|
||||||
|
)}
|
||||||
|
onClick={(e) => handleCopy(e)}
|
||||||
|
>
|
||||||
|
<span>{copied
|
||||||
|
? "Address copied to clipboard"
|
||||||
|
: truncate(accountTree?.address ?? "")
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
"border-none hover:bg-transparent h-6 w-6 shrink-0",
|
||||||
|
copied ? "text-primary" : "text-white/60"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="w-3 h-3" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardDescription>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{accountTree && accountTree.derivations && (
|
||||||
|
<div className="relative pl-6 flex flex-col space-y-2">
|
||||||
|
<div className="absolute left-[10px] top-0 bottom-4 w-[1px] bg-white/20" />
|
||||||
|
{accountTree.derivations.map((derivation, j) => {
|
||||||
|
return <DerivationCard key={j} derivation={derivation} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccountsSkeleton: React.FC = () => {
|
||||||
|
return new Array(4).fill(null).map((_, i) => (
|
||||||
|
<Card
|
||||||
|
key={`account-skeleton-card-${i}`}
|
||||||
|
className="p-2 mb-2 transition duration-300 border-none rounded bg-background"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="w-full">
|
||||||
|
<Skeleton className="w-5/12 h-4 mb-2" />
|
||||||
|
<Skeleton className="w-11/12 h-3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteAccount = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { currentChain } = useChain()
|
||||||
|
const {
|
||||||
|
activeAccountName,
|
||||||
|
accountTree,
|
||||||
|
requestPassword,
|
||||||
|
accountsStorageMutate,
|
||||||
|
isFetchingAccountTree,
|
||||||
|
isFetchingAccounts,
|
||||||
|
isFetchingActiveAccounts,
|
||||||
|
} = useAccounts()
|
||||||
|
|
||||||
|
const { derivation } = useParams<{ derivation?: string }>()
|
||||||
|
const derivationPath = useMemo(() => {
|
||||||
|
return derivation ? decodeURIComponent(derivation) : undefined;
|
||||||
|
}, [derivation])
|
||||||
|
|
||||||
|
const isFetching = useMemo(() => {
|
||||||
|
return isFetchingAccountTree || isFetchingAccounts || isFetchingActiveAccounts
|
||||||
|
}, [isFetchingAccountTree, isFetchingAccounts, isFetchingActiveAccounts])
|
||||||
|
|
||||||
|
const deleteAccount = async () => {
|
||||||
|
await requestPassword(async (password) => {
|
||||||
|
await rpc.client.removeAccount({
|
||||||
|
publicKey: accountTree?.publicKey ?? "",
|
||||||
|
path: derivationPath,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await accountsStorageMutate()
|
||||||
|
navigate("/accounts")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout accountName={activeAccountName} chainValue={currentChain?.value} innerClassName="bg-muted">
|
||||||
|
<div className="flex items-center justify-between px-6 my-4 sm:px-8">
|
||||||
|
<h2 className="text-xl font-semibold text-white">Delete {derivationPath ? "Derivation" : "Account Tree"}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="px-6 mb-4 grow sm:px-8">
|
||||||
|
<section>
|
||||||
|
{isFetching && <AccountsSkeleton />}
|
||||||
|
{!isFetching && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{derivationPath
|
||||||
|
? <DerivationCard derivation={accountTree?.derivations.find(d => d.path === derivationPath)} />
|
||||||
|
: <AccountCard accountTree={accountTree} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<div className="flex w-full gap-x-4 mt-6 px-6 pb-6">
|
||||||
|
<Button onClick={() => navigate(-1)} size="full" variant="secondary">Back</Button>
|
||||||
|
<Button onClick={() => deleteAccount()} size="full" variant="primary">Delete Now</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BottomNavBar />
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
179
src/containers/WalletPopup/pages/Accounts/DeriveAccount.tsx
Normal file
179
src/containers/WalletPopup/pages/Accounts/DeriveAccount.tsx
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import { ArrowLeftIcon, ArrowRightIcon, CheckCircleIcon, Eye, EyeOff } from "lucide-react"
|
||||||
|
import { useState, useEffect, useMemo } from "react"
|
||||||
|
import {
|
||||||
|
entropyToMiniSecret,
|
||||||
|
generateMnemonic,
|
||||||
|
mnemonicToEntropy,
|
||||||
|
} from "@polkadot-labs/hdkd-helpers"
|
||||||
|
import { networks } from "@/constants"
|
||||||
|
import { SubmitHandler, useForm } from "react-hook-form"
|
||||||
|
import { toHex } from "@polkadot-api/utils"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import useSWR from "swr"
|
||||||
|
import { rpc } from "@/containers/WalletPopup/api"
|
||||||
|
import { sr25519CreateDerive } from "@polkadot-labs/hdkd"
|
||||||
|
import { bytesToHex } from "@noble/ciphers/utils"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { Layout2 } from "@/components/Layout2"
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form"
|
||||||
|
import {
|
||||||
|
RadioGroup,
|
||||||
|
RadioGroupItem,
|
||||||
|
} from "@/components/ui/radio-group"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
import { Header, BottomNavBar } from "../../components"
|
||||||
|
import { useAccounts } from "../../hooks"
|
||||||
|
|
||||||
|
const derivationRegex = /^(\/\/|\/)[a-zA-Z0-9_\-]+((\/\/|\/)[a-zA-Z0-9_\-]+)*$/
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
derivationPath: z
|
||||||
|
.string()
|
||||||
|
.regex(derivationRegex, "Invalid derivation path format. Use format (e.g., //0 or //hard/soft)")
|
||||||
|
.refine(
|
||||||
|
(val) => !val.includes(" "),
|
||||||
|
"Derivation path cannot contain spaces"
|
||||||
|
),
|
||||||
|
derivationName: z
|
||||||
|
.string({ required_error: "Derivation name is required" })
|
||||||
|
.min(1, "Derivation name is required"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const DeriveAccount = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const {
|
||||||
|
activeAccount,
|
||||||
|
accountTree,
|
||||||
|
accountsStorageMutate,
|
||||||
|
requestPassword
|
||||||
|
} = useAccounts()
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof schema>>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
derivationName: "",
|
||||||
|
derivationPath: "//",
|
||||||
|
},
|
||||||
|
mode: "onSubmit",
|
||||||
|
shouldUnregister: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
trigger,
|
||||||
|
formState: { isValid, isDirty }
|
||||||
|
} = form
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<z.infer<typeof schema>> = async (data) => {
|
||||||
|
await requestPassword(async (password) => {
|
||||||
|
await rpc.client.deriveAccount({
|
||||||
|
publicKey: activeAccount?.publicKey ?? "",
|
||||||
|
name: data.derivationName,
|
||||||
|
path: data.derivationPath,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await accountsStorageMutate()
|
||||||
|
navigate("/accounts")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout2>
|
||||||
|
<ScrollArea className="h-full w-full">
|
||||||
|
<Form {...form}>
|
||||||
|
<form className="h-full flex flex-col">
|
||||||
|
<Card className="border-none bg-muted h-full flex flex-col flex-grow w-full max-w-md shadow-none px-2">
|
||||||
|
<CardHeader className="space-y-1 pt-6 pb-2 px-6">
|
||||||
|
<CardTitle className="text-xl font-bold tracking-normal text-white">
|
||||||
|
Derive {accountTree?.name ?? ""}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="flex-grow px-6 py-2 space-y-4">
|
||||||
|
<div className="space-y-4 animate-fade-in text-left">
|
||||||
|
<FormField
|
||||||
|
name="derivationName"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="space-y-1.5">
|
||||||
|
<Label className="text-sm text-white/90">Derivation Name</Label>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="derivationName"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter account name"
|
||||||
|
className="bg-input border-none text-white placeholder:text-white/40 h-11 px-4 rounded-lg focus-visible:ring-1 focus-visible:ring-white/20"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name="derivationPath"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="space-y-1.5">
|
||||||
|
<Label className="text-sm text-white/90">Derivation Path</Label>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="derivationPath"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter account name"
|
||||||
|
className="bg-input border-none text-white placeholder:text-white/40 h-11 px-4 rounded-lg focus-visible:ring-1 focus-visible:ring-white/20"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<CardFooter className="flex flex-row gap-2 pt-8 px-6 flex-shrink-0">
|
||||||
|
<Button variant="secondary" size="full" onClick={() => navigate("/accounts")}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button disabled={!isDirty || !isValid} variant="primary" size="full" type="button" onClick={handleSubmit(onSubmit)}>
|
||||||
|
Derive
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</ScrollArea>
|
||||||
|
<BottomNavBar />
|
||||||
|
</Layout2>
|
||||||
|
)
|
||||||
|
}
|
||||||
213
src/containers/WalletPopup/pages/Accounts/ExportAccounts.tsx
Normal file
213
src/containers/WalletPopup/pages/Accounts/ExportAccounts.tsx
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import { Card, CardDescription, CardTitle } from "@/components/ui/card"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import { ArrowRight, Trash2, KeyRound, GitFork, Eye, EyeOff } from "lucide-react"
|
||||||
|
import { useMemo, useRef, useState } from "react"
|
||||||
|
import { Layout2 } from "@/components/Layout2"
|
||||||
|
import { useNavigate, Link } from "react-router-dom"
|
||||||
|
import useSWR, { mutate } from "swr"
|
||||||
|
import { BlobWriter, ZipWriter, TextReader } from "@zip.js/zip.js"
|
||||||
|
import Fuse from "fuse.js"
|
||||||
|
import { rpc } from "@/containers/WalletPopup/api"
|
||||||
|
import { ss58Address } from "@polkadot-labs/hdkd-helpers"
|
||||||
|
import {
|
||||||
|
CreateDeriveFn,
|
||||||
|
ecdsaCreateDerive,
|
||||||
|
ed25519CreateDerive,
|
||||||
|
sr25519CreateDerive,
|
||||||
|
} from "@polkadot-labs/hdkd"
|
||||||
|
import React from "react"
|
||||||
|
import { Header, BottomNavBar } from "../../components"
|
||||||
|
import { useAccounts } from "../../hooks"
|
||||||
|
import { networks } from "@/constants"
|
||||||
|
import { Circle, CircleCheck } from 'lucide-react'
|
||||||
|
|
||||||
|
function truncate(address: string, startChars = 10, endChars = 10): string {
|
||||||
|
if (!address || address.length <= (startChars + endChars)) {
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = address.slice(0, startChars)
|
||||||
|
const end = address.slice(-endChars)
|
||||||
|
|
||||||
|
return `${start}...${end}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccountsSkeleton: React.FC = () => {
|
||||||
|
return new Array(3).fill(null).map((_, i) => (
|
||||||
|
<Card
|
||||||
|
key={`account-skeleton-card-${i}`}
|
||||||
|
className="p-2 mb-2 transition duration-300 border-none rounded bg-background"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="w-full">
|
||||||
|
<Skeleton className="w-5/12 h-4 mb-2" />
|
||||||
|
<Skeleton className="w-11/12 h-3" />
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" disabled>
|
||||||
|
<Skeleton className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExportAccounts = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { accounts, isFetchingAccounts } = useAccounts()
|
||||||
|
|
||||||
|
const excludedKeys = useRef<Set<string>>(new Set())
|
||||||
|
const [excludedNumber, setExcludedNumber] = useState<Number>(0)
|
||||||
|
const [password, setPassword] = useState<string>("")
|
||||||
|
const [showPassword, setShowPassword] = useState<boolean>(false)
|
||||||
|
const [isPending, setIsPending] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const finalAccounts = useMemo(() => {
|
||||||
|
return (accounts ?? []).map(acc => ({
|
||||||
|
...acc,
|
||||||
|
excluded: excludedKeys.current.has(acc.publicKey)
|
||||||
|
}))
|
||||||
|
}, [accounts, excludedNumber])
|
||||||
|
|
||||||
|
const toggleExcludedKeys = (publicKey: string) => {
|
||||||
|
if (isPending) return
|
||||||
|
|
||||||
|
const set = excludedKeys.current
|
||||||
|
if (set.has(publicKey)) {
|
||||||
|
set.delete(publicKey)
|
||||||
|
setExcludedNumber(prev => Number(prev) - 1)
|
||||||
|
} else {
|
||||||
|
set.add(publicKey)
|
||||||
|
setExcludedNumber(prev => Number(prev) + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupKeystore = async () => {
|
||||||
|
if (isPending) return
|
||||||
|
|
||||||
|
setIsPending(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const publicKeys = finalAccounts.filter(acc => !acc.excluded).map(acc => acc.publicKey)
|
||||||
|
const exportData = await rpc.client.prepareBackup(publicKeys)
|
||||||
|
if (!exportData || exportData.length === 0) return
|
||||||
|
|
||||||
|
const zipBlobWriter = new BlobWriter("application/zip")
|
||||||
|
const zipWriter = new ZipWriter(zipBlobWriter, { password: password, zipCrypto: false })
|
||||||
|
|
||||||
|
const jsonString = JSON.stringify(exportData, null, 0)
|
||||||
|
await zipWriter.add("full-keystore.json", new TextReader(jsonString))
|
||||||
|
const zipBlob = await zipWriter.close()
|
||||||
|
|
||||||
|
const downloadUrl = URL.createObjectURL(zipBlob)
|
||||||
|
const link = document.createElement("a")
|
||||||
|
link.href = downloadUrl
|
||||||
|
link.download = "ghost-connect-backup.zip"
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(downloadUrl)
|
||||||
|
navigate("/settings")
|
||||||
|
} finally {
|
||||||
|
setIsPending(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout2>
|
||||||
|
<div className="flex items-center justify-between px-6 mt-4 mb-6">
|
||||||
|
<div className="relative flex items-center w-full">
|
||||||
|
<Input
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
id="archieve-password"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
placeholder="Enter password for the backup archieve"
|
||||||
|
className="bg-input border-none text-white placeholder:text-white/40 h-11 px-4 rounded-lg focus-visible:ring-1 focus-visible:ring-white/20"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 text-white/40 hover:text-white/70 transition-colors"
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between px-6 my-4">
|
||||||
|
<h2 className="text-xl font-semibold text-white">
|
||||||
|
Export {!accounts || Number(excludedNumber) === 0
|
||||||
|
? "All"
|
||||||
|
: `${accounts.length - Number(excludedNumber)} of ${accounts.length}`
|
||||||
|
} Accounts
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="px-6 mb-4 grow sm:px-8">
|
||||||
|
<section>
|
||||||
|
{isFetchingAccounts && <AccountsSkeleton />}
|
||||||
|
{!isFetchingAccounts && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{finalAccounts.map(acc => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={acc.publicKey}
|
||||||
|
data-testid={acc.publicKey}
|
||||||
|
className="p-3 pb-2 border-none rounded-lg text-card-foreground transition-all shadow-md bg-input"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0">
|
||||||
|
<CardTitle className="font-bold text-sm tracking-wide text-white">
|
||||||
|
{acc.name}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-[11px] text-white/60 flex items-center gap-1 font-mono">
|
||||||
|
<span className="w-full">{truncate(acc?.address ?? "")}</span>
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => toggleExcludedKeys(acc.publicKey)}
|
||||||
|
className={`text-white h-8 w-8 border-none`}
|
||||||
|
>
|
||||||
|
{!acc.excluded
|
||||||
|
? <CircleCheck className="w-3.5 h-3.5" />
|
||||||
|
: <Circle className="w-3.5 h-3.5" />
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<div className="w-full mt-6 px-6 pb-6">
|
||||||
|
<Button
|
||||||
|
size="full"
|
||||||
|
variant="primary"
|
||||||
|
onClick={backupKeystore}
|
||||||
|
disabled={isPending || excludedNumber === accounts?.length}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BottomNavBar/>
|
||||||
|
</Layout2>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Key, NotepadText, ChevronDown, Sprout } from "lucide-react"
|
import { FileArchive, NotepadText, ChevronDown, Sprout } from "lucide-react"
|
||||||
import { rpc } from "../../api"
|
import { rpc } from "../../api"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
@ -14,6 +14,7 @@ import {
|
|||||||
} from "@polkadot-labs/hdkd-helpers"
|
} from "@polkadot-labs/hdkd-helpers"
|
||||||
import { fromHex, toHex } from "@polkadot-api/utils"
|
import { fromHex, toHex } from "@polkadot-api/utils"
|
||||||
import { bytesToHex } from "@noble/ciphers/utils"
|
import { bytesToHex } from "@noble/ciphers/utils"
|
||||||
|
import { BlobReader, ZipReader, TextWriter, FileEntry } from "@zip.js/zip.js";
|
||||||
import {
|
import {
|
||||||
CreateDeriveFn,
|
CreateDeriveFn,
|
||||||
ecdsaCreateDerive,
|
ecdsaCreateDerive,
|
||||||
@ -29,6 +30,8 @@ import { Input } from "@/components/ui/input"
|
|||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
|
import { useAccounts } from "../../hooks"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -56,82 +59,43 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
|
|
||||||
const createDeriveFnMap: Record<Scheme, CreateDeriveFn> = {
|
const importAccountScheme = z.object({
|
||||||
Sr25519: sr25519CreateDerive,
|
tab: z.discriminatedUnion("_type", [
|
||||||
Ed25519: ed25519CreateDerive,
|
z.object({
|
||||||
Ecdsa: ecdsaCreateDerive,
|
_type: z.literal("mnemonic"),
|
||||||
}
|
mnemonic: z.string().trim().min(1, "mnemonic is required"),
|
||||||
|
}),
|
||||||
const privateKeyIsValid = (value: string | undefined, scheme: string) => {
|
z.object({
|
||||||
if (!value) return false
|
_type: z.literal("seed"),
|
||||||
|
seed: z.string().trim().min(1, "seed is required"),
|
||||||
const bytes = fromHex(value)
|
}),
|
||||||
try {
|
z.object({
|
||||||
switch (scheme) {
|
_type: z.literal("jsonBackup"),
|
||||||
case "Ecdsa":
|
jsonBackup: z.instanceof(File, { message: "backup archive is required" }).nullable().optional(),
|
||||||
ecdsa.getPublicKey(bytes)
|
archivePassword: z.string(),
|
||||||
break
|
includeDerivations: z.boolean().default(true),
|
||||||
case "Ed25519":
|
overwriteExisting: z.boolean().default(false),
|
||||||
ed25519.getPublicKey(bytes)
|
}),
|
||||||
break
|
]),
|
||||||
case "Sr25519":
|
})
|
||||||
sr25519.getPublicKey(value)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (_) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mnemonicIsValid = (value: string | undefined) => {
|
|
||||||
if (!value) return false
|
|
||||||
|
|
||||||
try {
|
|
||||||
mnemonicToEntropy(value)
|
|
||||||
return true
|
|
||||||
} catch (_) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const importAccountScheme = z
|
|
||||||
.object({
|
|
||||||
tab: z.union([
|
|
||||||
z.object({
|
|
||||||
_type: z.enum(["mnemonic", "privateKey", "seed"]),
|
|
||||||
mnemonic: z.string(),
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
_type: z.enum(["mnemonic", "privateKey", "seed"]),
|
|
||||||
privateKey: z.string(),
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
_type: z.enum(["mnemonic", "privateKey", "seed"]),
|
|
||||||
seed: z.string(),
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
scheme: z.string(),
|
|
||||||
keyname: z.string().min(1, "Key name cannot be empty"),
|
|
||||||
})
|
|
||||||
|
|
||||||
type FormFields = z.infer<typeof importAccountScheme>
|
type FormFields = z.infer<typeof importAccountScheme>
|
||||||
|
|
||||||
export function ImportAccounts() {
|
export function ImportAccounts() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { accountsStorageMutate } = useAccounts()
|
||||||
|
|
||||||
const form = useForm<FormFields>({
|
const [isPending, setIsPending] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const form = useForm<FormFields>({
|
||||||
resolver: zodResolver(importAccountScheme),
|
resolver: zodResolver(importAccountScheme),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
tab: {
|
tab: {
|
||||||
_type: "mnemonic",
|
_type: "mnemonic",
|
||||||
mnemonic: "",
|
mnemonic: "",
|
||||||
},
|
},
|
||||||
scheme: "",
|
},
|
||||||
keyname: "",
|
mode: "onSubmit",
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -139,7 +103,7 @@ export function ImportAccounts() {
|
|||||||
watch,
|
watch,
|
||||||
getValues,
|
getValues,
|
||||||
getFieldState,
|
getFieldState,
|
||||||
formState: { isSubmitting, isValid, isReady },
|
formState: { isSubmitting },
|
||||||
control,
|
control,
|
||||||
setValue,
|
setValue,
|
||||||
setError,
|
setError,
|
||||||
@ -148,199 +112,260 @@ export function ImportAccounts() {
|
|||||||
|
|
||||||
const activeTab = watch("tab._type")
|
const activeTab = watch("tab._type")
|
||||||
|
|
||||||
const onActiveTabChanged = (tab: Tab["_type"]) => {
|
const onActiveTabChanged = (tab: FormFields["tab"]["_type"]) => {
|
||||||
clearErrors()
|
clearErrors()
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case "mnemonic":
|
case "mnemonic": {
|
||||||
setValue("tab", { _type: "mnemonic", mnemonic: "" })
|
setValue("tab", { _type: "mnemonic", mnemonic: "" })
|
||||||
break;
|
break
|
||||||
case "privateKey":
|
}
|
||||||
setValue("tab", { _type: "privateKey", privateKey: "" })
|
|
||||||
break;
|
case "jsonBackup": {
|
||||||
case "seed":
|
setValue("tab", {
|
||||||
|
_type: "jsonBackup",
|
||||||
|
jsonBackup: null as any,
|
||||||
|
archivePassword: "",
|
||||||
|
includeDerivations: true,
|
||||||
|
overwriteExisting: false,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "seed": {
|
||||||
setValue("tab", { _type: "seed", seed: "" })
|
setValue("tab", { _type: "seed", seed: "" })
|
||||||
break;
|
break
|
||||||
default:
|
}
|
||||||
setValue("tab", { _type: "privateKey", privateKey: "" })
|
|
||||||
break;
|
default: {
|
||||||
|
setValue("tab", { _type: "mnemonic", mnemonic: "" })
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setValue("scheme", "")
|
|
||||||
setValue("keyname", "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<FormFields> = async (data) => {
|
const onSubmit: SubmitHandler<FormFields> = async (data) => {
|
||||||
let errorOccured = null
|
setIsPending(true)
|
||||||
|
let miniSecret: Uint8Array | null = null;
|
||||||
|
|
||||||
const keynameExists = await rpc.client.getCryptoKey(data.keyname)
|
try {
|
||||||
if (keynameExists) {
|
|
||||||
errorOccured = {
|
|
||||||
field: "keyname",
|
|
||||||
message: "Account with this name already exists",
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (data.tab._type) {
|
switch (data.tab._type) {
|
||||||
case "privateKey": {
|
case "mnemonic":
|
||||||
try {
|
const cleanMnemonic = data.tab.mnemonic.trim()
|
||||||
if (!privateKeyIsValid(data.tab.privateKey, data.scheme)) {
|
const entropy = mnemonicToEntropy(cleanMnemonic)
|
||||||
throw new Error("a")
|
miniSecret = entropyToMiniSecret(entropy)
|
||||||
}
|
navigate("/add-account", { state: { miniSecret }})
|
||||||
await rpc.client.insertCryptoKey({
|
|
||||||
type: "Keypair",
|
|
||||||
name: data.keyname,
|
|
||||||
scheme: data.scheme,
|
|
||||||
privatekey: data.tab.privateKey,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
errorOccured = {
|
|
||||||
field: "tab.privateKey",
|
|
||||||
message: "Could not insert private key at selected scheme",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
|
||||||
case "seed": {
|
|
||||||
try {
|
|
||||||
const miniSecret = fromHex(data.tab.seed)
|
|
||||||
const derive = createDeriveFnMap[data.scheme](miniSecret)
|
|
||||||
const derivationPaths = networks
|
|
||||||
.map((network) => {
|
|
||||||
return {
|
|
||||||
chainId: network.chainId,
|
|
||||||
path: "",
|
|
||||||
publicKey: toHex(derive("").publicKey),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await rpc.client.insertCryptoKey({
|
case "seed":
|
||||||
type: "Keyset",
|
const cleanSeed = data.tab.seed
|
||||||
name: data.keyname,
|
miniSecret = fromHex(cleanSeed)
|
||||||
scheme: data.scheme,
|
navigate("/add-account", { state: { miniSecret }})
|
||||||
miniSecret: data.tab.seed,
|
|
||||||
derivationPaths,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
errorOccured = {
|
|
||||||
field: "tab.seed",
|
|
||||||
message: "Incorrect secret seed at selected scheme",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
|
||||||
case "mnemonic": {
|
|
||||||
try {
|
|
||||||
if (!mnemonicIsValid(data.tab.mnemonic)) {
|
|
||||||
throw new Error("a")
|
|
||||||
}
|
|
||||||
|
|
||||||
const entropy = mnemonicToEntropy(data.tab.mnemonic)
|
case "jsonBackup":
|
||||||
const miniSecret = entropyToMiniSecret(entropy)
|
const file = data.tab.jsonBackup
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
const derive = createDeriveFnMap[data.scheme](miniSecret)
|
const archivePassword = data.tab.archivePassword
|
||||||
const derivationPaths = networks
|
const includeDerivations = data.tab.includeDerivations
|
||||||
.map((network) => {
|
const overwriteExisting = data.tab.overwriteExisting
|
||||||
const path = `//${network.value}/0`
|
|
||||||
return {
|
|
||||||
chainId: network.chainId,
|
|
||||||
path,
|
|
||||||
publicKey: toHex(derive(`//${network.value}/0`).publicKey),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await rpc.client.insertCryptoKey({
|
const zipBlobReader = new BlobReader(file)
|
||||||
type: "Keyset",
|
const zipReader = new ZipReader(zipBlobReader, { password: archivePassword })
|
||||||
name: data.keyname,
|
const entries = await zipReader.getEntries()
|
||||||
scheme: data.scheme,
|
|
||||||
miniSecret: bytesToHex(miniSecret),
|
const entry = entries.find(e => e.filename === "full-keystore.json")
|
||||||
derivationPaths,
|
if (!entry) {
|
||||||
createdAt: Date.now(),
|
await zipReader.close();
|
||||||
})
|
throw new Error("Invalid backup archive: JSON data not found inside.")
|
||||||
} catch {
|
|
||||||
errorOccured = {
|
|
||||||
field: "tab.mnemonic",
|
|
||||||
message: "Could not use provided mnemonic phrase",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const backupEntry = entry as FileEntry
|
||||||
|
const jsonText = await backupEntry.getData!(new TextWriter())
|
||||||
|
const importedKeystore = JSON.parse(jsonText)
|
||||||
|
await zipReader.close()
|
||||||
|
|
||||||
|
await rpc.client.extendKeystore({
|
||||||
|
includeDerivations,
|
||||||
|
overwriteExisting,
|
||||||
|
keystores: importedKeystore,
|
||||||
|
})
|
||||||
|
|
||||||
|
await accountsStorageMutate()
|
||||||
|
|
||||||
|
navigate("/accounts")
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
} finally {
|
||||||
if (errorOccured) {
|
setIsPending(true)
|
||||||
setError(errorOccured.field, {
|
|
||||||
type: "server",
|
|
||||||
message: errorOccured.message,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
window.localStorage.setItem("selectedCryptoKeyName", data.keyname)
|
|
||||||
navigate("/accounts")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout2 innerClassName="bg-muted">
|
<Layout2 innerClassName="bg-muted">
|
||||||
<Header />
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form className="relative grow" onSubmit={handleSubmit(onSubmit)}>
|
<form className="h-full flex flex-col" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Card className="bg-muted border-none h-full flex flex-col flex-grow w-full max-w-md shadow-none">
|
<Card className="bg-muted border-none h-full flex flex-col flex-grow w-full max-w-md shadow-none">
|
||||||
<CardHeader className="space-y-0 pt-4 gap-4">
|
<CardHeader className="space-y-0 pt-4 gap-4">
|
||||||
<CardTitle className="text-xl font-semibold tracking-normal">
|
<CardTitle className="text-xl font-semibold tracking-normal text-white">
|
||||||
Import Crypto Key
|
Import Account
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-accent flex justify-between">
|
<CardDescription className="text-accent flex gap-1 justify-between">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="secondary"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || isPending}
|
||||||
onClick={() => onActiveTabChanged("mnemonic")}
|
onClick={() => onActiveTabChanged("mnemonic")}
|
||||||
className={`text-xs p-2 ${activeTab === "mnemonic" ? "font-semibold underline" : ""}`}
|
className={`text-xs p-2 ${activeTab === "mnemonic" ? "font-semibold underline" : ""}`}
|
||||||
>
|
>
|
||||||
<NotepadText className="inline-block mr-2" />
|
<NotepadText className="inline-block mr-1 w-4 h-4" />
|
||||||
Mnemonic
|
Mnemonic
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="secondary"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || isPending}
|
||||||
onClick={() => onActiveTabChanged("seed")}
|
onClick={() => onActiveTabChanged("seed")}
|
||||||
className={`text-xs p-2 ${activeTab === "seed" ? "font-semibold underline" : ""}`}
|
className={`text-xs p-2 ${activeTab === "seed" ? "font-semibold underline" : ""}`}
|
||||||
>
|
>
|
||||||
<Sprout className="inline-block mr-2" />
|
<Sprout className="inline-block mr-1 w-4 h-4" />
|
||||||
Secret Seed
|
Secret Seed
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="secondary"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || isPending}
|
||||||
onClick={() => onActiveTabChanged("privateKey")}
|
onClick={() => onActiveTabChanged("jsonBackup")}
|
||||||
className={`text-xs p-2 ${activeTab === "privateKey" ? "font-semibold underline" : ""}`}
|
className={`text-xs p-2 ${activeTab === "jsonBackup" ? "font-semibold underline" : ""}`}
|
||||||
>
|
>
|
||||||
<Key className="inline-block mr-2" />
|
<FileArchive className="inline-block mr-1 w-4 h-4" />
|
||||||
Private Key
|
JSON Backup
|
||||||
</Button>
|
</Button>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="content-center flex flex-col flex-grow justify-start">
|
<CardContent className="content-center flex flex-col flex-grow justify-start">
|
||||||
{activeTab === "privateKey" && (
|
{activeTab === "jsonBackup" && (
|
||||||
<FormField
|
<div className="space-y-4">
|
||||||
control={control}
|
<FormField
|
||||||
name="tab.privateKey"
|
control={control}
|
||||||
render={({ field }) => (
|
name="tab.jsonBackup"
|
||||||
<FormItem className="mb-4">
|
render={({ field }) => (
|
||||||
<FormControl>
|
<FormItem className="mb-4">
|
||||||
<Input
|
<FormControl>
|
||||||
{...field}
|
<div className="relative">
|
||||||
disabled={isSubmitting}
|
<input
|
||||||
aria-label="Enter private key"
|
type="file"
|
||||||
type="text"
|
accept=".zip"
|
||||||
className="pr-10"
|
id="zip-file-input"
|
||||||
placeholder="Enter private key"
|
disabled={isSubmitting || isPending}
|
||||||
/>
|
aria-label="Click to upload the backup archieve here"
|
||||||
</FormControl>
|
className="sr-only"
|
||||||
<FormMessage />
|
onChange={(e) => {
|
||||||
</FormItem>
|
const file = e.target.files?.[0]
|
||||||
)}
|
if (file) field.onChange(file)
|
||||||
/>)}
|
}}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="zip-file-input"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-between h-11 px-4 rounded-lg cursor-pointer transition-all",
|
||||||
|
"bg-input border-none text-white focus-within:ring-1 focus-within:ring-white/20",
|
||||||
|
isSubmitting ? "opacity-50 pointer-events-none" : "hover:bg-input/80"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-medium text-white">
|
||||||
|
Choose backup file
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="text-sm text-white/40 truncate max-w-[200px] font-mono">
|
||||||
|
{field.value instanceof File ? field.value.name : "No file chosen"}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="tab.archivePassword"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="mb-4">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
disabled={isSubmitting || isPending}
|
||||||
|
aria-label="Enter archive password"
|
||||||
|
type="password"
|
||||||
|
className={cn(
|
||||||
|
"bg-input border-none text-white placeholder:text-white/40 h-11 px-4",
|
||||||
|
"rounded-lg focus-visible:ring-1 focus-visible:ring-white/20"
|
||||||
|
)}
|
||||||
|
placeholder="Enter archive password"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="tab.includeDerivations"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className={cn(
|
||||||
|
"flex flex-row items-center space-x-3 space-y-0",
|
||||||
|
"rounded-md border border-white/10 px-4 py-2 bg-input"
|
||||||
|
)}>
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
disabled={isSubmitting || isPending}
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel className="text-sm font-medium text-white cursor-pointer">
|
||||||
|
Include derived accounts
|
||||||
|
</FormLabel>
|
||||||
|
<p className="text-[11px] text-white/40">
|
||||||
|
Import all child keys derived from this account root.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name="tab.overwriteExisting"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className={cn(
|
||||||
|
"flex flex-row items-center space-x-3 space-y-0",
|
||||||
|
"rounded-md border border-white/10 px-4 py-2 bg-input"
|
||||||
|
)}>
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
disabled={isSubmitting || isPending}
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel className="text-sm font-medium text-white cursor-pointer">
|
||||||
|
Overwrite existing accounts
|
||||||
|
</FormLabel>
|
||||||
|
<p className="text-[11px] text-white/40">
|
||||||
|
If unchecked, existing accounts will be skipped safely.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>)}
|
||||||
{activeTab === "mnemonic" && (
|
{activeTab === "mnemonic" && (
|
||||||
<FormField
|
<FormField
|
||||||
control={control}
|
control={control}
|
||||||
@ -350,15 +375,18 @@ export function ImportAccounts() {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
{...field}
|
{...field}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || isPending}
|
||||||
aria-label="Enter mnemonic phrase"
|
aria-label="Enter mnemonic phrase"
|
||||||
id="text"
|
id="text"
|
||||||
rows={4}
|
rows={4}
|
||||||
className="min-h-auto h-auto text-justify resize-none"
|
|
||||||
placeholder="Enter mnemonic phrase"
|
placeholder="Enter mnemonic phrase"
|
||||||
|
className={cn(
|
||||||
|
"bg-input border-none placeholder:text-white/40 cursor-text text-sm text-white",
|
||||||
|
"min-h-[110px] p-2 rounded-lg focus-visible:ring-1 focus-visible:ring-white/20 text-justify",
|
||||||
|
"w-full resize-none text-sm font-medium break-words leading-relaxed scrollbar-none",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>)}
|
/>)}
|
||||||
@ -371,74 +399,38 @@ export function ImportAccounts() {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || isPending}
|
||||||
aria-label="Enter secret seed"
|
aria-label="Enter secret seed"
|
||||||
type="text"
|
type="text"
|
||||||
className="pr-10"
|
className={cn(
|
||||||
|
"bg-input border-none text-white placeholder:text-white/40 h-11 px-4",
|
||||||
|
"rounded-lg focus-visible:ring-1 focus-visible:ring-white/20"
|
||||||
|
)}
|
||||||
placeholder="Enter secret seed"
|
placeholder="Enter secret seed"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>)}
|
/>)}
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name="keyname"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mb-4">
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
aria-label="Enter crypto key name"
|
|
||||||
type="text"
|
|
||||||
className="pr-10"
|
|
||||||
placeholder="Enter crypto key name"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name="scheme"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mb-4">
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
onValueChange={(scheme) => field.onChange(scheme)}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
className={`${field.value ? "text-primary" : "text-muted-foreground"} w-full`}
|
|
||||||
data-testid="scheme-select"
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Select Key Type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>Crypto scheme</SelectLabel>
|
|
||||||
<SelectItem data-testid="scheme-sr25519" value="Sr25519">Sr25519</SelectItem>
|
|
||||||
<SelectItem data-testid="scheme-ed25519" value="Ed25519">Ed25519</SelectItem>
|
|
||||||
<SelectItem data-testid="scheme-ecdsa" value="Ecdsa">ECDSA</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex justify-between mt-4">
|
<CardFooter className="flex flex-row gap-2 justify-between mt-4">
|
||||||
<Button
|
<Button
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || isPending}
|
||||||
size="full"
|
size="full"
|
||||||
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
Import Key
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={isSubmitting || isPending}
|
||||||
|
type="submit"
|
||||||
|
size="full"
|
||||||
|
variant={activeTab === "jsonBackup" ? "primary" : "secondary"}
|
||||||
|
>
|
||||||
|
{activeTab === "jsonBackup" ? "Import" : "Next"}
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -1,92 +1,167 @@
|
|||||||
import { User } from "lucide-react"
|
import { Card, CardDescription, CardTitle } from "@/components/ui/card"
|
||||||
import React, { useEffect, useState } from "react"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import { rpc } from "../../api"
|
import { Button } from "@/components/ui/button"
|
||||||
import { RadioGroup } from "@headlessui/react"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
import { SubmitHandler, useForm } from "react-hook-form"
|
import { Input } from "@/components/ui/input"
|
||||||
import { useNavigate } from "react-router-dom"
|
import {
|
||||||
import useSWR from "swr"
|
Select,
|
||||||
import { Layout } from "../../../../components/Layout"
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import { ArrowRight, Trash2, KeyRound, GitFork } from "lucide-react"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { Layout2 } from "@/components/Layout2"
|
||||||
|
import { useNavigate, Link } from "react-router-dom"
|
||||||
|
import useSWR, { mutate } from "swr"
|
||||||
|
import Fuse from "fuse.js"
|
||||||
|
import { rpc } from "@/containers/WalletPopup/api"
|
||||||
|
import { ss58Address } from "@polkadot-labs/hdkd-helpers"
|
||||||
|
import {
|
||||||
|
CreateDeriveFn,
|
||||||
|
ecdsaCreateDerive,
|
||||||
|
ed25519CreateDerive,
|
||||||
|
sr25519CreateDerive,
|
||||||
|
} from "@polkadot-labs/hdkd"
|
||||||
|
import React from "react"
|
||||||
|
import { Header, BottomNavBar } from "../../components"
|
||||||
|
import { useAccounts } from "../../hooks"
|
||||||
|
import { networks } from "@/constants"
|
||||||
|
import { Circle, CircleCheck } from 'lucide-react'
|
||||||
|
|
||||||
export const SwitchAccount: React.FC = () => {
|
function truncate(address: string, startChars = 10, endChars = 10): string {
|
||||||
const navigate = useNavigate()
|
if (!address || address.length <= (startChars + endChars)) {
|
||||||
|
return address
|
||||||
const { data: cryptoKeys } = useSWR(
|
|
||||||
"rpc.getCryptoKeys",
|
|
||||||
() => rpc.client.getCryptoKeys(),
|
|
||||||
{
|
|
||||||
revalidateOnFocus: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const [selectedCryptoKeyName, setSelectedCryptoKeyName] = useState<string>("")
|
|
||||||
const { handleSubmit } = useForm()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (cryptoKeys && selectedCryptoKeyName === "") {
|
|
||||||
setSelectedCryptoKeyName(
|
|
||||||
() =>
|
|
||||||
window.localStorage.getItem("selectedCryptoKeyName") ??
|
|
||||||
cryptoKeys[0].name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [selectedCryptoKeyName, cryptoKeys])
|
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<Record<string, any>> = async () => {
|
|
||||||
try {
|
|
||||||
if (!cryptoKeys) return
|
|
||||||
window.localStorage.setItem(
|
|
||||||
"selectedCryptoKeyName",
|
|
||||||
selectedCryptoKeyName,
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
navigate("/accounts")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cryptoKeyNames = cryptoKeys?.map(({ name }) => name) ?? []
|
const start = address.slice(0, startChars)
|
||||||
return (
|
const end = address.slice(-endChars)
|
||||||
<Layout>
|
|
||||||
<form className="max-w-xl p-6 mx-auto" onSubmit={handleSubmit(onSubmit)}>
|
return `${start}...${end}`
|
||||||
<h1 className="mb-4 text-2xl font-bold">Switch Account</h1>
|
}
|
||||||
<RadioGroup
|
|
||||||
value={selectedCryptoKeyName}
|
const AccountsSkeleton: React.FC = () => {
|
||||||
onChange={setSelectedCryptoKeyName}
|
return new Array(3).fill(null).map((_, i) => (
|
||||||
>
|
<Card
|
||||||
<RadioGroup.Label className="sr-only">Crypto Key</RadioGroup.Label>
|
key={`account-skeleton-card-${i}`}
|
||||||
<div className="space-y-2">
|
className="p-2 mb-2 transition duration-300 border-none rounded bg-background"
|
||||||
{cryptoKeyNames.map((name) => (
|
>
|
||||||
<RadioGroup.Option
|
<div className="flex items-center justify-between">
|
||||||
key={name}
|
<div className="w-full">
|
||||||
value={name}
|
<Skeleton className="w-5/12 h-4 mb-2" />
|
||||||
className={({ active, checked }) =>
|
<Skeleton className="w-11/12 h-3" />
|
||||||
`${
|
|
||||||
active
|
|
||||||
? "ring-2 ring-white/60 ring-offset-2 ring-offset-black"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${checked ? "bg-black/75 text-white" : "bg-white"}
|
|
||||||
relative flex cursor-pointer rounded-lg px-5 py-4 shadow-md focus:outline-none`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center mr-3">
|
|
||||||
<User className="text-gray-600" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<span className="text-lg">{name}</span>
|
|
||||||
</div>
|
|
||||||
</RadioGroup.Option>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
<div className="mt-6">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-teal-500 text-white py-2 rounded-lg hover:bg-teal-600 transition-colors"
|
|
||||||
>
|
|
||||||
Switch
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<Button variant="ghost" disabled>
|
||||||
</Layout>
|
<Skeleton className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SwitchAccount = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [query, setQuery] = useState<string>("")
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectAccount,
|
||||||
|
accounts,
|
||||||
|
activeAccount,
|
||||||
|
isFetchingActiveAccounts,
|
||||||
|
isFetchingAccounts
|
||||||
|
} = useAccounts()
|
||||||
|
|
||||||
|
const fuse = useMemo(() => {
|
||||||
|
return new Fuse(accounts ?? [], {
|
||||||
|
keys: [
|
||||||
|
{ name: "name", weight: 0.7 },
|
||||||
|
{ name: "address", weight: 0.4 },
|
||||||
|
],
|
||||||
|
threshold: 0.3,
|
||||||
|
distance: 100,
|
||||||
|
ignoreLocation: true
|
||||||
|
})
|
||||||
|
}, [accounts])
|
||||||
|
|
||||||
|
const filteredAccounts = useMemo(() => {
|
||||||
|
if (!accounts || accounts.length <= 5) return accounts
|
||||||
|
if (!query.trim()) return accounts
|
||||||
|
return fuse.search(query).map(res => res.item)
|
||||||
|
}, [query, accounts, fuse])
|
||||||
|
|
||||||
|
const isAccountSelected = (publicKey: string): boolean => {
|
||||||
|
return publicKey === activeAccount?.publicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = async (publicKey: string): Promise<void> => {
|
||||||
|
await selectAccount({ publicKey })
|
||||||
|
navigate("/accounts")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout2>
|
||||||
|
{accounts && accounts.length > 5 && (<div className="flex items-center justify-between px-6 mt-4 mb-6">
|
||||||
|
<Input
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
id="query"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search your accounts"
|
||||||
|
className="bg-input border-none text-white placeholder:text-white/40 h-11 px-4 rounded-lg focus-visible:ring-1 focus-visible:ring-white/20"
|
||||||
|
/>
|
||||||
|
</div>)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between px-6 my-4">
|
||||||
|
<h2 className="text-xl font-semibold text-white">All Accounts</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="px-6 mb-4 grow sm:px-8">
|
||||||
|
<section>
|
||||||
|
{isFetchingAccounts && <AccountsSkeleton />}
|
||||||
|
{!isFetchingAccounts && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{filteredAccounts && filteredAccounts.map(acc => {
|
||||||
|
const isSelected = isAccountSelected(acc.publicKey)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={acc.publicKey}
|
||||||
|
data-testid={acc.publicKey}
|
||||||
|
onClick={() => handleClick(acc.publicKey)}
|
||||||
|
className={`p-3 pb-2 border-none rounded-lg text-card-foreground transition-all shadow-md hover:bg-input ${
|
||||||
|
isSelected ? "bg-input ring-2 ring-white ring-inset" : "bg-card cursor-pointer"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0">
|
||||||
|
<CardTitle className="font-bold text-sm tracking-wide text-white">
|
||||||
|
{acc.name}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-[11px] text-white/60 flex items-center gap-1 font-mono">
|
||||||
|
<span className="w-full">{truncate(acc?.address ?? "")}</span>
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={`text-white h-8 w-8 border-none hover:bg-input`}
|
||||||
|
>
|
||||||
|
{isSelected ? <CircleCheck className="w-3.5 h-3.5" /> : <Circle className="w-3.5 h-3.5" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<BottomNavBar/>
|
||||||
|
</Layout2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,3 +3,6 @@ export * from "./SwitchAccount"
|
|||||||
export * from "./ImportAccounts"
|
export * from "./ImportAccounts"
|
||||||
export * from "./AddAccount"
|
export * from "./AddAccount"
|
||||||
export * from "./AccountDetails"
|
export * from "./AccountDetails"
|
||||||
|
export * from "./DeriveAccount"
|
||||||
|
export * from "./DeleteAccount"
|
||||||
|
export * from "./ExportAccounts"
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export const ChangePassword = () => {
|
|||||||
newPassword,
|
newPassword,
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
await rpc.client.changePassword(currentPassword, newPassword)
|
// await rpc.client.changePassword(currentPassword, newPassword)
|
||||||
reset()
|
reset()
|
||||||
navigate("/settings")
|
navigate("/settings")
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -27,7 +27,6 @@ import {
|
|||||||
import { Navigate } from "react-router-dom"
|
import { Navigate } from "react-router-dom"
|
||||||
|
|
||||||
import { rpc } from "../api"
|
import { rpc } from "../api"
|
||||||
import { useKeyring } from "../hooks"
|
|
||||||
|
|
||||||
const FormSchema = z
|
const FormSchema = z
|
||||||
.object({
|
.object({
|
||||||
@ -48,7 +47,6 @@ const FormSchema = z
|
|||||||
|
|
||||||
export const CreatePassword = () => {
|
export const CreatePassword = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const keyring = useKeyring()
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof FormSchema>>({
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
resolver: zodResolver(FormSchema),
|
resolver: zodResolver(FormSchema),
|
||||||
@ -63,56 +61,66 @@ export const CreatePassword = () => {
|
|||||||
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async ({
|
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async ({
|
||||||
password,
|
password,
|
||||||
}) => {
|
}) => {
|
||||||
await rpc.client.createPassword(password)
|
// await rpc.client.createPassword(password)
|
||||||
keyring.refresh()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout2>
|
<Layout2 innerClassName="bg-muted">
|
||||||
{isSubmitSuccessful && <Navigate to="/accounts" replace={true} />}
|
{isSubmitSuccessful && <Navigate to="/accounts" replace={true} />}
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form className="relative grow" onSubmit={handleSubmit(onSubmit)}>
|
<form className="flex flex-col h-full w-full min-h-0" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Card className="absolute flex flex-col flex-grow w-full max-w-md min-h-full shadow-none">
|
<Card className="flex flex-col flex-grow w-full h-full max-h-full border-none bg-transparent shadow-none rounded-none overflow-hidden">
|
||||||
<CardHeader className="text-center">
|
<div className="text-center px-6 pt-6 flex-shrink-0">
|
||||||
<CardTitle className="mt-6 mb-6 text-2xl font-extrabold">
|
<h2 className="text-xl font-extrabold text-white tracking-tight">
|
||||||
Create Password
|
Create GHOST Connect Password
|
||||||
<br />
|
</h2>
|
||||||
</CardTitle>
|
<CardDescription className="mt-3 text-xs text-foreground/70 leading-normal max-w-xs mx-auto">
|
||||||
<CardDescription className="mt-2 text-sm">
|
Set a name for your crypto key. This name will be used to identify your crypto within GHOST Connect.
|
||||||
Feel free to create any password you are comfortable with.
|
|
||||||
We do NOT impose any rules regarding your password; the only
|
|
||||||
requirement is to confirm it.
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</div>
|
||||||
<CardContent className="content-center flex flex-col flex-grow">
|
|
||||||
|
<CardContent className="flex flex-col justify-center flex-grow w-full px-6 py-2 min-h-0 gap-y-1">
|
||||||
<FormField
|
<FormField
|
||||||
control={control}
|
control={control}
|
||||||
name="password"
|
name="password"
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<FormItem className="grid w-full max-w-sm items-center gap-1.5 my-4">
|
<FormItem className="w-full max-w-sm mx-auto">
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel className="text-xs font-semibold">Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} disabled={isSubmitting} type="password" />
|
<Input
|
||||||
|
{...field}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter GHOST Connect password"
|
||||||
|
className="bg-secondary/40 border-border/40 focus-visible:ring-primary h-9"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{fieldState.invalid && fieldState.error?.message && (
|
{fieldState.invalid && fieldState.error?.message && (
|
||||||
<span className="text-xs text-red-500">
|
<span className="text-[11px] font-medium text-red-500 block mt-0.5 animate-fade-in">
|
||||||
{fieldState.error?.message}
|
{fieldState.error?.message}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={control}
|
control={control}
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<FormItem className="grid w-full max-w-sm items-center gap-1.5 my-4">
|
<FormItem className="w-full max-w-sm mx-auto mt-2">
|
||||||
<FormLabel>Confirm Password</FormLabel>
|
<FormLabel className="text-xs font-semibold">Confirm Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} disabled={isSubmitting} type="password" />
|
<Input
|
||||||
|
{...field}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm GHOST Connect password"
|
||||||
|
className="bg-secondary/40 border-border/40 focus-visible:ring-primary h-9"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{fieldState.invalid && fieldState.error?.message && (
|
{fieldState.invalid && fieldState.error?.message && (
|
||||||
<span className="text-xs text-red-500">
|
<span className="text-[11px] font-medium text-red-500 block mt-0.5 animate-fade-in">
|
||||||
{fieldState.error?.message}
|
{fieldState.error?.message}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -120,21 +128,20 @@ export const CreatePassword = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex flex-col space-y-4 justify-end">
|
|
||||||
<Button disabled={isSubmitting} type="submit" size="full" className={cn("text-lg py-3")} >
|
<CardFooter className="flex flex-col space-y-3 justify-end pb-6 pt-2 flex-shrink-0 w-full max-w-sm mx-auto">
|
||||||
Create Wallet
|
<Button disabled={isSubmitting} type="submit" variant="primary" size="full" className={cn("text-base py-2.5")} >
|
||||||
|
Create GHOST Connect Password
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
size="full"
|
size="sm"
|
||||||
className={cn("text-lg py-3")}
|
className="text-sm font-medium hover:bg-transparent text-foreground/70 hover:text-foreground mx-auto flex items-center"
|
||||||
onClick={() => navigate("/welcome")}
|
onClick={() => navigate("/welcome")}
|
||||||
>
|
>
|
||||||
<ArrowLeft
|
<ArrowLeft className="w-4 h-4 mr-1.5" aria-hidden="true" />
|
||||||
className="w-5 h-5 mr-2"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { useKeyring } from "../hooks"
|
|
||||||
import { Layout2 } from "@/components/Layout2"
|
import { Layout2 } from "@/components/Layout2"
|
||||||
|
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
@ -32,7 +31,6 @@ const FormSchema = z.object({
|
|||||||
|
|
||||||
export const DeleteWallet = () => {
|
export const DeleteWallet = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { reset } = useKeyring()
|
|
||||||
const [showPassword, setShowPassword] = useState<boolean>(false)
|
const [showPassword, setShowPassword] = useState<boolean>(false)
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof FormSchema>>({
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
@ -49,7 +47,7 @@ export const DeleteWallet = () => {
|
|||||||
|
|
||||||
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async ({ password }) => {
|
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async ({ password }) => {
|
||||||
try {
|
try {
|
||||||
await reset(password)
|
// await reset(password)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError("password", { message: "Incorrect password provided" })
|
setError("password", { message: "Incorrect password provided" })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,7 +97,6 @@ export const Networks = () => {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<BottomNavBar currentItem="networks" />
|
|
||||||
</Layout2>
|
</Layout2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,58 +1,92 @@
|
|||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import { useKeyring } from "../hooks"
|
|
||||||
import { Layout2 } from "@/components/Layout2"
|
import { Layout2 } from "@/components/Layout2"
|
||||||
import { BottomNavBar, Header } from "../components"
|
import { BottomNavBar, Header } from "../components"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { KeyIcon, LockIcon, Trash2, HeartPulse } from "lucide-react"
|
import { KeyIcon, LockIcon, Trash2, HeartPulse } from "lucide-react"
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
|
||||||
|
const latestReleaseUrl = "https://git.ghostchain.io/api/v1/repos/ghostchain/ghost-extension-wallet/releases/latest"
|
||||||
|
|
||||||
export const Settings: React.FC = () => {
|
export const Settings: React.FC = () => {
|
||||||
const { lock } = useKeyring()
|
const [higherVersion, setHigherVersion] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
|
const isNewerVersion = (current: string, latest: string): boolean => {
|
||||||
|
const cleanCurrent = current.replace(/^v/, '').trim()
|
||||||
|
const cleanLatest = latest.replace(/^v/, '').trim()
|
||||||
|
|
||||||
|
const currentParts = cleanCurrent.split('.').map(Number)
|
||||||
|
const latestParts = cleanLatest.split('.').map(Number)
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const latestNum = latestParts[i] || 0
|
||||||
|
const currentNum = currentParts[i] || 0
|
||||||
|
|
||||||
|
if (latestNum > currentNum) return true
|
||||||
|
if (latestNum < currentNum) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true
|
||||||
|
|
||||||
|
const runCheck = async () => {
|
||||||
|
const version = chrome.runtime.getManifest().version
|
||||||
|
|
||||||
|
const response = await fetch(latestReleaseUrl)
|
||||||
|
if (!response.ok) return
|
||||||
|
const data = await response.json()
|
||||||
|
if (!data || !isMounted) return
|
||||||
|
|
||||||
|
const releaseVersion = data.tag_name;
|
||||||
|
if (isNewerVersion(version, releaseVersion)) {
|
||||||
|
setHigherVersion(releaseVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runCheck()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout2 innerClassName="bg-muted">
|
<Layout2 innerClassName="bg-muted">
|
||||||
<Header />
|
|
||||||
<div className="flex items-center justify-between px-6 mt-4 mb-4 sm:px-8">
|
<div className="flex items-center justify-between px-6 mt-4 mb-4 sm:px-8">
|
||||||
<h2 className="text-xl font-semibold">Settings</h2>
|
<h2 className="text-xl font-semibold text-white">Settings</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScrollArea className="px-6 mb-4 grow sm:px-8">
|
<ScrollArea className="px-6 mb-4 grow sm:px-8">
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<Button className="w-[320]" variant="outline" onClick={() => lock()}>
|
<Button className="w-full" variant="secondary" asChild>
|
||||||
<div className="flex flex-row justify-between">
|
<Link to={"/export-accounts"}>
|
||||||
<LockIcon className="w-4 h-4 mr-2 text-primary hover:text-foreground" />
|
Export All Accounts
|
||||||
Lock Wallet
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
<Button className="w-[320]" variant="outline" asChild>
|
|
||||||
<Link to={"/change-password"}>
|
|
||||||
<div className="flex flex-row justify-between">
|
|
||||||
<KeyIcon className="w-4 h-4 mr-2 text-primary hover:text-foreground" />
|
|
||||||
Change Password
|
|
||||||
</div>
|
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="w-[320]" variant="outline" asChild>
|
<Button className="w-full" variant="secondary" asChild>
|
||||||
<Link
|
<Link rel="noreferrer" target="_blank" to="https://ghostchain.io/ecosystem/">
|
||||||
rel="noreferrer"
|
Ecosystem
|
||||||
target="_blank"
|
|
||||||
to={"https://lite.ghostchain.io"}
|
|
||||||
>
|
|
||||||
<div className="flex flex-row justify-between">
|
|
||||||
<HeartPulse className="w-4 h-4 mr-2 text-primary hover:text-foreground" />
|
|
||||||
GHOST Lite
|
|
||||||
</div>
|
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="w-[320]" variant="outline" asChild>
|
<Button className="w-full" variant="secondary" asChild>
|
||||||
<Link to={"/delete-wallet"}>
|
<Link rel="noreferrer" target="_blank" to="https://git.ghostchain.io/ghostchain/ghost-extension-wallet#overview">
|
||||||
<div className="flex flex-row justify-between">
|
About GHOST Connect
|
||||||
<Trash2 className="w-4 h-4 mr-2 text-primary hover:text-foreground" />
|
|
||||||
Delete Wallet
|
|
||||||
</div>
|
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
|
{higherVersion && (<div className="flex items-center justify-between px-6 mt-4 mb-4 sm:px-8">
|
||||||
|
<Button className="w-full" variant="primary" asChild>
|
||||||
|
<Link rel="noreferrer" target="_blank" to="https://ghostchain.io/ghost-connect/">
|
||||||
|
GHOST Connect Version {higherVersion} Available
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>)}
|
||||||
|
|
||||||
<BottomNavBar currentItem="settings" />
|
<BottomNavBar currentItem="settings" />
|
||||||
</Layout2>
|
</Layout2>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react"
|
import { useState, useMemo } from "react"
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
|
|
||||||
@ -6,7 +6,7 @@ import { rpc } from "../api"
|
|||||||
import { UserSignedExtensions as UserSignedExtensionsTy } from "@/types/UserSignedExtension"
|
import { UserSignedExtensions as UserSignedExtensionsTy } from "@/types/UserSignedExtension"
|
||||||
import { wellKnownChainIdByGenesisHash } from "@/constants"
|
import { wellKnownChainIdByGenesisHash } from "@/constants"
|
||||||
|
|
||||||
import { Layout } from "@/components/Layout"
|
import { Layout2 } from "@/components/Layout2"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
@ -17,8 +17,20 @@ import {
|
|||||||
UserSignedExtensions,
|
UserSignedExtensions,
|
||||||
} from "../components"
|
} from "../components"
|
||||||
|
|
||||||
|
import { useAccounts, useChain } from "../hooks"
|
||||||
|
|
||||||
export const SignRequest = () => {
|
export const SignRequest = () => {
|
||||||
|
const { currentChain, isLoadingChain } = useChain()
|
||||||
|
const {
|
||||||
|
activeAccount,
|
||||||
|
accountTree,
|
||||||
|
requestPassword,
|
||||||
|
isFetchingActiveAccounts,
|
||||||
|
isFetchingAccountTree,
|
||||||
|
} = useAccounts()
|
||||||
|
|
||||||
const { signRequestId } = useParams<{ signRequestId: string }>()
|
const { signRequestId } = useParams<{ signRequestId: string }>()
|
||||||
|
const [inAction, setInAction] = useState<boolean>(false)
|
||||||
const [userSignedExtensions, setUserSignedExtensions] = useState<Partial<UserSignedExtensionsTy>>({})
|
const [userSignedExtensions, setUserSignedExtensions] = useState<Partial<UserSignedExtensionsTy>>({})
|
||||||
const {
|
const {
|
||||||
data: signRequests,
|
data: signRequests,
|
||||||
@ -27,111 +39,157 @@ export const SignRequest = () => {
|
|||||||
} = useSWR(signRequestId ? ["getSignRequest"] : null, () =>
|
} = useSWR(signRequestId ? ["getSignRequest"] : null, () =>
|
||||||
rpc.client.getSignRequests(),
|
rpc.client.getSignRequests(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const chainId = useMemo(() => currentChain?.chainId, [currentChain])
|
||||||
|
|
||||||
|
const activeAddress = useMemo(() => {
|
||||||
|
if (!activeAccount || !accountTree) return undefined
|
||||||
|
|
||||||
|
let targetAddress = accountTree.address
|
||||||
|
|
||||||
|
if (activeAccount.path) {
|
||||||
|
const matchingDerivation = accountTree.derivations.find(
|
||||||
|
(d) => d.path === activeAccount.path
|
||||||
|
)
|
||||||
|
if (matchingDerivation) {
|
||||||
|
targetAddress = matchingDerivation.address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetAddress
|
||||||
|
}, [activeAccount, accountTree])
|
||||||
|
|
||||||
|
if (isLoading || isLoadingChain || isFetchingActiveAccounts || isFetchingAccountTree) {
|
||||||
|
return (
|
||||||
|
<Layout2>
|
||||||
|
<h1 className="text-3xl font-bold mb-6 px-6">Loading Sign Request #{signRequestId}</h1>
|
||||||
|
</Layout2>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!signRequestId) {
|
if (!signRequestId) {
|
||||||
window.close()
|
window.close()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<Layout>
|
|
||||||
<h1 className="text-3xl font-bold mb-6 px-6">Loading Sign Request #{signRequestId}</h1>
|
|
||||||
</Layout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const request = signRequests?.[signRequestId]
|
const request = signRequests?.[signRequestId]
|
||||||
if (error || !request) {
|
const isRequestNotFound = !request || error
|
||||||
return (
|
|
||||||
<Layout>
|
const isAddressMismatched = !!(request && activeAddress && request.address !== activeAddress)
|
||||||
<h1 className="text-3xl font-bold mb-6 px-6">Could not parse the sign request #{signRequestId}</h1>
|
const isChainMismatched = !!(request && chainId && request.chainId !== chainId)
|
||||||
<br />
|
const isExecutionBlocked = isRequestNotFound || isAddressMismatched || isChainMismatched
|
||||||
Error: {error?.message}
|
|
||||||
</Layout>
|
const handleSigning = async () => {
|
||||||
)
|
setInAction(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cleanExtensions = JSON.parse(
|
||||||
|
JSON.stringify(userSignedExtensions, (_, value) =>
|
||||||
|
typeof value === "bigint" ? value.toString() : value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await requestPassword(async (password) => {
|
||||||
|
await rpc.client.approveSignRequest(
|
||||||
|
signRequestId,
|
||||||
|
cleanExtensions,
|
||||||
|
password
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
setInAction(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(isExecutionBlocked, inAction)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout2>
|
||||||
<h1 className="text-3xl font-bold mb-6 px-6">Sign Request #{signRequestId}</h1>
|
<div className="flex flex-col justify-between">
|
||||||
<ScrollArea className="px-6 mb-4 grow sm:px-8">
|
<div className="">
|
||||||
<section>
|
<div className="py-2 px-6">
|
||||||
<div className="my-4">
|
{activeAddress && (
|
||||||
<div className="my-2">
|
<div className="mt-1 text-xs text-white flex items-center justify-center gap-1.5 font-mono">
|
||||||
<div className="text-xs font-semibold mb-2 mt-4">Origin</div>
|
<span className="inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||||
<Input
|
{`Active Account: ${activeAddress.slice(0, 8)}...${activeAddress.slice(-8)}`}
|
||||||
readOnly
|
</div>
|
||||||
aria-label="Request Origin"
|
)}
|
||||||
type="text"
|
|
||||||
className="pr-10"
|
{isExecutionBlocked && (
|
||||||
placeholder={request.url}
|
<div className="mt-4 p-3.5 rounded-xl border border-destructive/30 bg-destructive/15 text-destructive-foreground text-xs space-y-1">
|
||||||
/>
|
<span className="font-bold flex items-center gap-1 text-red-400">
|
||||||
</div>
|
Signing Request Blocked
|
||||||
<div className="my-2 overflow-hidden">
|
</span>
|
||||||
<div className="text-xs font-semibold mb-2 mt-4">Chain</div>
|
<p className="text-white/80 leading-relaxed">
|
||||||
<Input
|
{isRequestNotFound && "This transaction payload could not be recovered or found by the proxy client pipeline."}
|
||||||
readOnly
|
{isAddressMismatched && `Identity Mismatch! The DApp is requesting a signature for address but your wallet extension is currently does not expose it". Change your active profile inside the extension to proceed.`}
|
||||||
aria-label="Request Origin"
|
{isChainMismatched && `Network Mismatch! This transaction was generated for chain context "${wellKnownChainIdByGenesisHash[request.chainId] || 'Unknown'} ", but your extension is currently connected to "${wellKnownChainIdByGenesisHash[chainId] || 'Unknown'}".`}
|
||||||
type="text"
|
</p>
|
||||||
className="pr-10"
|
</div>
|
||||||
placeholder={wellKnownChainIdByGenesisHash[request.chainId]}
|
)}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="my-2">
|
|
||||||
<div className="text-xs font-semibold mb-2 mt-4">From Address</div>
|
|
||||||
<Input
|
|
||||||
readOnly
|
|
||||||
aria-label="From Address"
|
|
||||||
type="text"
|
|
||||||
className="pr-10"
|
|
||||||
placeholder={request.address}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="my-2 overflow-hidden">
|
|
||||||
<div className="text-xs font-semibold mb-2 mt-4">Call data</div>
|
|
||||||
<Textarea
|
|
||||||
readOnly
|
|
||||||
aria-label="Call data"
|
|
||||||
rows={6}
|
|
||||||
className="text-sm text-justify resize-none"
|
|
||||||
placeholder={request.callData}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="my-2">
|
|
||||||
<div className="text-xs font-semibold mb-2 mt-4">Decoded Call data</div>
|
|
||||||
<DecodedCallData chainId={request.chainId} callData={request.callData} />
|
|
||||||
</div>
|
|
||||||
<div className="my-2">
|
|
||||||
{request.userSignedExtensions.type === "names" ? (
|
|
||||||
<UserSignedExtensionInputs
|
|
||||||
userSignedExtensionNames={request.userSignedExtensions.names}
|
|
||||||
onChange={setUserSignedExtensions}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<UserSignedExtensions
|
|
||||||
userSignedExtensions={request.userSignedExtensions.values}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex gap-4 justify-center space-x-4 pt-6">
|
|
||||||
<Button
|
{!isRequestNotFound && (
|
||||||
variant="secondary"
|
<div className="w-full flex flex-col gap-1 pt-2 px-6">
|
||||||
size="full"
|
<div className="w-full flex flex-row justify-between items-center">
|
||||||
className="h-12"
|
<span className="text-xs font-semibold text-white">Origin</span>
|
||||||
onClick={() => rpc.client.approveSignRequest(signRequestId, userSignedExtensions)}
|
<span className="text-xs font-medium text-primary truncate max-w-[280px] select-all font-mono" title={request.url}>
|
||||||
>
|
{request.url}
|
||||||
Approve
|
</span>
|
||||||
</Button>
|
</div>
|
||||||
<Button
|
|
||||||
variant="secondary"
|
<div className="w-full flex flex-row justify-between items-center">
|
||||||
size="full"
|
<span className="text-xs font-semibold text-white">Chain</span>
|
||||||
className="h-12"
|
<span className="text-xs font-medium text-primary font-mono">
|
||||||
onClick={() => rpc.client.cancelSignRequest(signRequestId)}
|
{wellKnownChainIdByGenesisHash[request.chainId] || "Unknown Chain Spec"}
|
||||||
>
|
</span>
|
||||||
Cancel
|
</div>
|
||||||
</Button>
|
|
||||||
</div>
|
<div className="w-full flex flex-col min-w-0 mt-6">
|
||||||
</section>
|
<div className="text-xs font-semibold mb-2 text-white uppercase tracking-wider">
|
||||||
</ScrollArea>
|
Transaction Details
|
||||||
</Layout>
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full min-w-0">
|
||||||
|
<DecodedCallData chainId={request.chainId} callData={request.callData} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="my-2">
|
||||||
|
{request.userSignedExtensions.type === "names" ? (
|
||||||
|
<UserSignedExtensionInputs
|
||||||
|
userSignedExtensionNames={request.userSignedExtensions.names}
|
||||||
|
onChange={setUserSignedExtensions}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<UserSignedExtensions
|
||||||
|
userSignedExtensions={request.userSignedExtensions.values}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full flex gap-4 justify-center space-x-4 pt-6 px-6">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
disabled={inAction}
|
||||||
|
size="full"
|
||||||
|
onClick={() => rpc.client.cancelSignRequest(signRequestId)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="full"
|
||||||
|
disabled={isExecutionBlocked || inAction}
|
||||||
|
onClick={() => handleSigning()}
|
||||||
|
>
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
123
src/containers/WalletPopup/pages/SwitchNetwork.tsx
Normal file
123
src/containers/WalletPopup/pages/SwitchNetwork.tsx
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { Card, CardDescription, CardTitle } from "@/components/ui/card"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import { ArrowRight, Trash2, KeyRound, GitFork } from "lucide-react"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { Layout2 } from "@/components/Layout2"
|
||||||
|
import { useNavigate, Link } from "react-router-dom"
|
||||||
|
import useSWR, { mutate } from "swr"
|
||||||
|
import { rpc } from "@/containers/WalletPopup/api"
|
||||||
|
import { ss58Address } from "@polkadot-labs/hdkd-helpers"
|
||||||
|
import {
|
||||||
|
CreateDeriveFn,
|
||||||
|
ecdsaCreateDerive,
|
||||||
|
ed25519CreateDerive,
|
||||||
|
sr25519CreateDerive,
|
||||||
|
} from "@polkadot-labs/hdkd"
|
||||||
|
import React from "react"
|
||||||
|
import { BottomNavBar, NetworkLogo } from "../components"
|
||||||
|
import { useChain } from "../hooks"
|
||||||
|
import { networks } from "@/constants"
|
||||||
|
import { Circle, CircleCheck } from 'lucide-react'
|
||||||
|
|
||||||
|
const NetworksSkeleton: React.FC = () => {
|
||||||
|
return new Array(2).fill(null).map((_, i) => (
|
||||||
|
<Card
|
||||||
|
key={`network-skeleton-card-${i}`}
|
||||||
|
className="p-2 mb-2 transition duration-300 border-none rounded bg-background"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="w-full">
|
||||||
|
<Skeleton className="w-5/12 h-4 mb-2" />
|
||||||
|
<Skeleton className="w-11/12 h-3" />
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" disabled>
|
||||||
|
<Skeleton className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SwitchNetwork = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const {
|
||||||
|
allChains,
|
||||||
|
currentChain,
|
||||||
|
setCurrentChainId,
|
||||||
|
isLoadingChain,
|
||||||
|
} = useChain()
|
||||||
|
|
||||||
|
const isChainSelected = (chainId: string): boolean => {
|
||||||
|
return chainId === currentChain?.chainId
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = async (chainId: string): Promise<void> => {
|
||||||
|
await setCurrentChainId(chainId)
|
||||||
|
navigate("/accounts")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout2>
|
||||||
|
<div className="flex items-center justify-between px-6 my-4 sm:px-8">
|
||||||
|
<h2 className="text-xl font-semibold text-white">Select Network</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="px-6 mb-4 grow sm:px-8">
|
||||||
|
<section>
|
||||||
|
{isLoadingChain && <NetworksSkeleton />}
|
||||||
|
{!isLoadingChain && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{allChains.map(chain => {
|
||||||
|
const isSelected = isChainSelected(chain.chainId)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={chain.chainId}
|
||||||
|
data-testid={chain.chainId}
|
||||||
|
onClick={() => handleClick(chain.chainId)}
|
||||||
|
className={`p-3 pb-2 border-none rounded-lg text-card-foreground transition-all shadow-md hover:bg-input ${
|
||||||
|
isSelected ? "bg-input ring-2 ring-white ring-inset" : "bg-card cursor-pointer"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex flex-row justify-start gap-2">
|
||||||
|
<CardTitle className="font-bold text-sm tracking-wide text-white">
|
||||||
|
<NetworkLogo value={chain.value} />
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-sm text-white flex items-center gap-1 font-mono">
|
||||||
|
<span className="w-full">{chain.label}</span>
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={`text-white h-8 w-8 border-none hover:bg-input`}
|
||||||
|
>
|
||||||
|
{isSelected ? <CircleCheck className="w-3.5 h-3.5" /> : <Circle className="w-3.5 h-3.5" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<BottomNavBar/>
|
||||||
|
</Layout2>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,142 +0,0 @@
|
|||||||
import { useKeyring } from "../hooks"
|
|
||||||
import { Layout2 } from "@/components/Layout2"
|
|
||||||
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
} from "@/components/ui/card"
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
} from "@/components/ui/form"
|
|
||||||
import { EyeIcon, EyeOffIcon, LayersIcon } from "lucide-react"
|
|
||||||
import { useForm, SubmitHandler } from "react-hook-form"
|
|
||||||
import { useState } from "react"
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import { z } from "zod"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Link } from "react-router-dom"
|
|
||||||
|
|
||||||
import { Logo } from "../../../components"
|
|
||||||
|
|
||||||
const FormSchema = z.object({
|
|
||||||
password: z.string({ required_error: "You must specify a password" }),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const UnlockKeyring = () => {
|
|
||||||
const { unlock } = useKeyring()
|
|
||||||
const [showPassword, setShowPassword] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof FormSchema>>({
|
|
||||||
resolver: zodResolver(FormSchema),
|
|
||||||
})
|
|
||||||
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
getValues,
|
|
||||||
setError,
|
|
||||||
formState: { errors, isSubmitting, isSubmitSuccessful },
|
|
||||||
} = form
|
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async ({ password }) => {
|
|
||||||
try {
|
|
||||||
await unlock(password)
|
|
||||||
} catch (e) {
|
|
||||||
setError("password", { message: "Incorrect password provided" })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const togglePasswordVisibility = () => {
|
|
||||||
setShowPassword((prev) => !prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout2>
|
|
||||||
<Form {...form}>
|
|
||||||
<form className="relative grow" onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<Card className="absolute flex flex-col flex-grow w-full max-w-md min-h-full shadow-none">
|
|
||||||
<CardHeader className="text-center">
|
|
||||||
<CardTitle className="mt-6 mb-6 py-[5px] px-[30px] text-2xl leading-4">
|
|
||||||
<Logo cName="fill-primary" />
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="mt-2 text-sm">
|
|
||||||
Unfortunately, we are unable to assist you in restoring your password.
|
|
||||||
The only recommendation we can offer is to reinstall the extension;
|
|
||||||
however, please note that this will result in the loss of all account data.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="relative content-center flex flex-grow">
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name="password"
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<FormItem className="grid w-full max-w-sm items-center gap-1.5 my-4">
|
|
||||||
<div className="relative">
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
placeholder="Enter your password"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit(togglePasswordVisibility)}
|
|
||||||
className="absolute -translate-y-1/2 top-1/2 -translate-x-1/2 right-2"
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<EyeOffIcon className="w-5 h-5" />
|
|
||||||
) : (
|
|
||||||
<EyeIcon className="w-5 h-5" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{fieldState.invalid && fieldState.error?.message && (
|
|
||||||
<span className="absolute top-1/2 pt-4 text-xs text-red-500">
|
|
||||||
{fieldState.error?.message}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex flex-col space-y-4 justify-end">
|
|
||||||
<Button disabled={isSubmitting} type="submit" size="full" className={cn("text-lg py-3")}>
|
|
||||||
Unlock Wallet
|
|
||||||
</Button>
|
|
||||||
<Link
|
|
||||||
data-testid="btnGoToOptions"
|
|
||||||
to="/options"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
variant="secondary"
|
|
||||||
size="full"
|
|
||||||
className={cn("text-lg py-3")}
|
|
||||||
>
|
|
||||||
Options
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</Layout2>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -11,85 +11,60 @@ import { Signal, Router, RefreshCcw, GlobeLock } from "lucide-react"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Layout2 } from "@/components/Layout2"
|
import { Layout2 } from "@/components/Layout2"
|
||||||
import { useNavigate, Link } from "react-router-dom"
|
import { useNavigate, Link } from "react-router-dom"
|
||||||
import { useEffect } from "react"
|
import useSWR from "swr"
|
||||||
|
|
||||||
import { useKeyring } from "../hooks"
|
import { UserRoundPlus } from "lucide-react"
|
||||||
|
|
||||||
|
import { JohnMcAffeeIcon } from "../components/JohnMcAffee"
|
||||||
import { rpc } from "../api"
|
import { rpc } from "../api"
|
||||||
|
|
||||||
import { Logo } from "../../../components"
|
|
||||||
|
|
||||||
export const Welcome = () => {
|
export const Welcome = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { keyring } = useKeyring()
|
|
||||||
const { isLocked, hasPassword } = keyring
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
hasPassword && (isLocked
|
|
||||||
? navigate("/unlock-keyring")
|
|
||||||
: navigate("/accounts"))
|
|
||||||
}, [hasPassword, isLocked])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout2>
|
<Layout2>
|
||||||
<Card className="flex flex-col flex-grow w-full max-w-md min-h-full shadow-none">
|
<Card className="flex flex-col w-full h-full max-h-full shadow-none border-none rounded-none overflow-hidden bg-muted">
|
||||||
<CardHeader className="text-center">
|
<CardContent className="flex flex-col items-center justify-start flex-grow w-full px-6 pt-8 pb-4 min-h-0 text-center">
|
||||||
<CardTitle className="mt-6 mb-6 py-[5px] px-[30px] text-2xl leading-4">
|
<div className="w-[150px] h-[170px] drop-shadow-xl animate-fade-in flex items-center justify-center mb-6">
|
||||||
<Logo cName="fill-primary" />
|
<JohnMcAffeeIcon className="w-full h-full object-contain" />
|
||||||
</CardTitle>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="items-center content-center flex flex-grow">
|
<h2 className="text-2xl font-extrabold tracking-tight text-white mb-6">
|
||||||
<ul className="space-y-4">
|
Choose Your Destiny
|
||||||
<li className="flex items-center gap-3">
|
</h2>
|
||||||
<Signal className="w-8 h-8 text-primary" aria-hidden="true" />
|
|
||||||
<p className="ml-3 text-base">
|
<div className="w-[90%] max-w-sm space-y-6 text-left text-white/90">
|
||||||
Be connected to the GHOST and CASPER networks from your browser
|
<div className="flex items-start space-x-4">
|
||||||
|
<UserRoundPlus className="w-6 h-6 mt-1 flex-shrink-0 text-white" />
|
||||||
|
<p className="text-sm font-medium leading-tight">
|
||||||
|
Create a new account or import an existing one
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</div>
|
||||||
<li className="flex items-center gap-3">
|
|
||||||
<RefreshCcw className="w-8 h-8 text-primary" aria-hidden="true" />
|
<div className="flex items-start space-x-4">
|
||||||
<p className="ml-3 text-base">
|
<UserRoundPlus className="w-6 h-6 mt-1 flex-shrink-0 text-white" />
|
||||||
Syncs silently in a background without disturbing you
|
<p className="text-sm font-medium leading-tight">
|
||||||
|
Unlock full access to GHOST and its pure Web3 cross-chain infrastructure
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</div>
|
||||||
<li className="flex items-center gap-3">
|
</div>
|
||||||
<Router className="w-8 h-8 text-primary" aria-hidden="true" />
|
|
||||||
<p className="ml-3 text-base">
|
|
||||||
Become self-hosted endpoint for yourself for ghost projects
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-3">
|
|
||||||
<GlobeLock className="w-8 h-8 text-primary" aria-hidden="true" />
|
|
||||||
<p className="ml-3 text-base">
|
|
||||||
Make your browser be part of the GHOST and CASPER networks
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex flex-col flex-grow space-y-4 justify-end">
|
|
||||||
|
<CardFooter className="flex flex-col space-y-3 justify-end pb-8 px-6 flex-shrink-0">
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="primary"
|
||||||
size="full"
|
size="full"
|
||||||
className={cn("text-lg py-3")}
|
onClick={() => navigate("/add-account")}
|
||||||
onClick={() => navigate("/create-password")}
|
|
||||||
>
|
>
|
||||||
Create Wallet
|
Create Account
|
||||||
</Button>
|
</Button>
|
||||||
<Link
|
|
||||||
data-testid="btnGoToOptions"
|
<Button
|
||||||
to="/options"
|
variant="secondary"
|
||||||
target="_blank"
|
size="full"
|
||||||
rel="noopener noreferrer"
|
onClick={() => navigate("/import-account")}
|
||||||
className="w-full"
|
|
||||||
>
|
>
|
||||||
<Button
|
Import Account
|
||||||
type="button"
|
</Button>
|
||||||
variant="secondary"
|
|
||||||
size="full"
|
|
||||||
className={cn("text-lg py-3")}
|
|
||||||
>
|
|
||||||
Options
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</Layout2>
|
</Layout2>
|
||||||
|
|||||||
@ -5,5 +5,5 @@ export * from "./DeleteWallet"
|
|||||||
export * from "./Networks"
|
export * from "./Networks"
|
||||||
export * from "./Settings"
|
export * from "./Settings"
|
||||||
export * from "./SignRequest"
|
export * from "./SignRequest"
|
||||||
export * from "./UnlockKeyring"
|
|
||||||
export * from "./Welcome"
|
export * from "./Welcome"
|
||||||
|
export * from "./SwitchNetwork"
|
||||||
|
|||||||
@ -73,8 +73,12 @@ const lightClientProvider = getLightClientProvider(CHANNEL_ID)
|
|||||||
{
|
{
|
||||||
const provider = lightClientProvider.then((lightClientProvider) => ({
|
const provider = lightClientProvider.then((lightClientProvider) => ({
|
||||||
...lightClientProvider,
|
...lightClientProvider,
|
||||||
|
async getAccount(chainId: string) {
|
||||||
|
return rpc.client.getAccount(chainId)
|
||||||
|
},
|
||||||
async getAccounts(chainId: string) {
|
async getAccounts(chainId: string) {
|
||||||
return rpc.client.getAccounts(chainId)
|
const account = await rpc.client.getAccount(chainId)
|
||||||
|
return account ? [account] : []
|
||||||
},
|
},
|
||||||
async createTx(chainId: string, from: string, callData: string) {
|
async createTx(chainId: string, from: string, callData: string) {
|
||||||
return rpc.client.createTx(chainId, from, callData)
|
return rpc.client.createTx(chainId, from, callData)
|
||||||
|
|||||||
@ -4,58 +4,34 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 211 57% 28%;
|
--background: 212 33% 47%;
|
||||||
--foreground: 53 83% 69%;
|
--muted: 212 34% 48%;
|
||||||
|
|
||||||
--muted: 212 33% 47%;
|
--card: 211 57% 28%;
|
||||||
--muted-foreground: 53 100% 89%;
|
--card-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--popover: 150 40% 98.04%;
|
--primary: 51 87% 70%;
|
||||||
--popover-foreground: 221 39.3% 11%;
|
--primary-foreground: 212 34% 30%;
|
||||||
--card: 211 55.4% 25.5%;
|
|
||||||
--card-foreground: 180 0% 100%;
|
|
||||||
--border: 221 6% 91%;
|
|
||||||
--input: 221 6% 91%;
|
|
||||||
|
|
||||||
|
--foreground: 0 0% 100%;
|
||||||
|
--muted-foreground: 52 76% 67%;
|
||||||
|
|
||||||
--primary: 180 0% 100%;
|
--border: 0 0% 100%;
|
||||||
--primary-foreground: 0 0% 0%;
|
--input: 212 44% 37%;
|
||||||
|
--input-foreground: 209 24% 48%;
|
||||||
|
|
||||||
--secondary: 211 57% 28%;
|
--error: 0 91% 70%;
|
||||||
--secondary-foreground: 53 83% 69%;
|
--success: 117 47% 56%;
|
||||||
|
|
||||||
--accent: 53 83.3% 69.4%;
|
--ring: 52 76% 67%;
|
||||||
--accent-foreground: 221 7% 20%;
|
|
||||||
|
|
||||||
--destructive: 357 96% 58%;
|
|
||||||
--destructive-foreground: 16 98% 50%;
|
|
||||||
|
|
||||||
--ring: 221 69% 32%;
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--white: 0 0% 100%;
|
|
||||||
--black: 0 0% 0%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 150 40% 98.04%;
|
--background: 48 89% 50%;
|
||||||
--foreground: 221 39.3% 11%;
|
--foreground: 215 57% 23%;
|
||||||
--muted: 221 19% 87%;
|
--primary: 215 57% 23%;
|
||||||
--muted-foreground: 221 11% 26%;
|
--primary-foreground: 48 89% 50%;
|
||||||
--popover: 150 40% 98.04%;
|
|
||||||
--popover-foreground: 221 39.3% 11%;
|
|
||||||
--card: 150 40% 98.04%;
|
|
||||||
--card-foreground: 221 39.3% 11%;
|
|
||||||
--border: 221 6% 91%;
|
|
||||||
--input: 221 6% 91%;
|
|
||||||
--primary: 154.64 70% 47.06%;
|
|
||||||
--primary-foreground: 0 0% 100%;
|
|
||||||
--secondary: 152 1% 86%;
|
|
||||||
--secondary-foreground: 221 1% 26%;
|
|
||||||
--accent: 221 7% 80%;
|
|
||||||
--accent-foreground: 221 7% 20%;
|
|
||||||
--destructive: 16 98% 31%;
|
|
||||||
--destructive-foreground: 16 98% 50%;
|
|
||||||
--ring: 221 69% 32%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ module.exports = {
|
|||||||
prefix: "",
|
prefix: "",
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["Ubuntu-Regular", "sans-serif"],
|
sans: ["Ubuntu-Regular"],
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
center: true,
|
||||||
@ -30,6 +30,9 @@ module.exports = {
|
|||||||
ring: "hsl(var(--ring))",
|
ring: "hsl(var(--ring))",
|
||||||
background: "hsl(var(--background))",
|
background: "hsl(var(--background))",
|
||||||
foreground: "hsl(var(--foreground))",
|
foreground: "hsl(var(--foreground))",
|
||||||
|
error: "hsl(var(--error))",
|
||||||
|
success: "hsl(var(--success))",
|
||||||
|
"input-foreground": "hsl(var(--input-foreground))",
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: "hsl(var(--primary))",
|
DEFAULT: "hsl(var(--primary))",
|
||||||
foreground: "hsl(var(--primary-foreground))",
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
@ -46,14 +49,6 @@ module.exports = {
|
|||||||
DEFAULT: "hsl(var(--muted))",
|
DEFAULT: "hsl(var(--muted))",
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
},
|
},
|
||||||
accent: {
|
|
||||||
DEFAULT: "hsl(var(--accent))",
|
|
||||||
foreground: "hsl(var(--accent-foreground))",
|
|
||||||
},
|
|
||||||
popover: {
|
|
||||||
DEFAULT: "hsl(var(--popover))",
|
|
||||||
foreground: "hsl(var(--popover-foreground))",
|
|
||||||
},
|
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: "hsl(var(--card))",
|
DEFAULT: "hsl(var(--card))",
|
||||||
foreground: "hsl(var(--card-foreground))",
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user