rewrite extension around new UX

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2026-10-08 18:25:56 +03:00
parent 66bd2077c7
commit d3274222f9
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
62 changed files with 4252 additions and 2313 deletions

View File

@ -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
View 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
View 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

View File

@ -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",

View File

@ -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

View File

@ -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>()
} }

View File

@ -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()

View File

@ -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)
} }
}, },
} }

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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)
}

View File

@ -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 = {

View File

@ -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 })
}

View File

@ -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>
} }

View File

@ -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}

View File

@ -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"

View File

@ -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>
)
}

View File

@ -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>
) )

View File

@ -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>

View File

@ -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 = ({

View File

@ -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",
}, },
}, },
) )

View File

@ -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>

View File

@ -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}
> >

View File

@ -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>
) )
}) })

View File

@ -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
}, {}) }, {})

View File

@ -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>

View File

@ -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>
) )

View File

@ -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 {

View File

@ -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>
) )

View File

@ -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>
} }
} }

View 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>
)
}

View 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>
)
};

View File

@ -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 />)
)
}

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -1 +1,2 @@
export * from "./useKeyring" export * from "./useChain"
export * from "./useAccounts"

View 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)

View 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)

View File

@ -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)

View File

@ -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>
) )
} }

View File

@ -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>
) )
} }

View File

@ -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>
) )

View 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>
)
}

View 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>
)
}

View 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>
)
}

View File

@ -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>

View File

@ -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>
) )
} }

View File

@ -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"

View File

@ -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) {

View File

@ -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>

View File

@ -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" })
} }

View File

@ -97,7 +97,6 @@ export const Networks = () => {
)} )}
</section> </section>
</ScrollArea> </ScrollArea>
<BottomNavBar currentItem="networks" />
</Layout2> </Layout2>
) )
} }

View File

@ -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>
) )

View File

@ -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>
) )
} }

View 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>
)
}

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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"

View File

@ -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)

View File

@ -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%;
} }
} }

View File

@ -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))",