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,11 +26,7 @@ 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 (
@ -41,7 +37,8 @@ const isUserSignedExtensionName = (s: string): s is UserSignedExtensionName => {
} }
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 = (
@ -53,15 +50,75 @@ export const createBackgroundRpc = (
>["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 }) => ({ const accounts = await keyring.getAccounts(chainId)
const prefix = wellKnownPrefixByGenesisHash[chain.genesisHash]
return accounts.map(acc => ({
...acc,
address: ss58Address(acc.publicKey, prefix)
}))
}
const getActiveAccount: RpcMethodHandlers<
BackgroundRpcSpec,
Context
>["getActiveAccount"] = async ([chainId], { lightClientPageHelper }) => {
const chains = await lightClientPageHelper.getChains()
const chain = chains.find(({ genesisHash }) => genesisHash === chainId)
if (!chain) return undefined
return await keyring.getActiveAccount(chainId)
}
const getAccountDerivations: RpcMethodHandlers<
BackgroundRpcSpec,
Context
>["getAccountDerivations"] = async ([metadata], { lightClientPageHelper }) => {
if (!metadata) return undefined
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( address: ss58Address(
publicKey, publicKey,
wellKnownPrefixByGenesisHash[chain.genesisHash] wellKnownPrefixByGenesisHash[chainId]
), ),
}) }
)
} }
const notifyOnAccountsChanged = async (context: Context) => const notifyOnAccountsChanged = async (context: Context) =>
@ -76,29 +133,44 @@ export const createBackgroundRpc = (
) )
const handlers: RpcMethodHandlers<BackgroundRpcSpec, Context> = { const handlers: RpcMethodHandlers<BackgroundRpcSpec, Context> = {
getAccountDerivations,
getAccounts, getAccounts,
getAccount,
getActiveAccount,
async createTx( async createTx(
[chainId, from, callData], [chainId, from, callData],
{ lightClientPageHelper, signRequests, port }, { lightClientPageHelper, signRequests, port },
) { ) {
const url = port.sender?.url const url = port.sender?.url
if (!url) throw new Error("unknown 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 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) throw new Error("unknown chain provided")
const id = nextSignRequestId++ const id = nextSignRequestId++
const client = getObservableClient(createClient(chain.provider)) const client = getObservableClient(createClient(chain.provider))
const chainHead$ = client.chainHead$() const chainHead$ = client.chainHead$()
const userSignedExtensionNames = await firstValueFrom( const userSignedExtensionNames = await firstValueFrom(
chainHead$.best$.pipe( chainHead$.best$.pipe(
mergeMap((blockInfo) => mergeMap((blockInfo: { hash: string; [key: string]: any }) =>
chainHead$.getRuntimeContext$(blockInfo.hash).pipe( chainHead$.getRuntimeContext$(blockInfo.hash).pipe(
take(1), take(1),
map(({ lookup: { metadata } }) => map(({ lookup: { metadata } }: { lookup: { metadata: any } }) =>
metadata.extrinsic.signedExtensions metadata.extrinsic.signedExtensions
.map(({ identifier }) => identifier) .map(({ identifier }: { identifier: string }) => identifier)
.filter(isUserSignedExtensionName) .filter(isUserSignedExtensionName)
), ),
), ),
@ -106,93 +178,111 @@ export const createBackgroundRpc = (
filter(Boolean) filter(Boolean)
) )
) )
.finally(() => {
chainHead$.unfollow()
client.destroy()
})
const currentRequestId = id
try { try {
const signRequest = new Promise< const signRequest = new Promise<{
Parameters<InternalSignRequest["resolve"]>[0] userSignedExtensions: any;
>( password?: string;
(resolve, reject) => }>((resolve, reject) => {
(signRequests[id] = { signRequests[id] = {
resolve, resolve,
reject, reject,
chainId, chainId,
url, url,
address: ss58Address( address: ss58Address(from, wellKnownPrefixByGenesisHash[chain.genesisHash]),
from,
wellKnownPrefixByGenesisHash[chain.genesisHash]
),
callData, callData,
userSignedExtensions: { userSignedExtensions: {
type: "names", type: "names",
names: userSignedExtensionNames, names: userSignedExtensionNames,
}, },
} as any
}) })
)
const window = await chrome.windows.create({ let windowId: number | undefined = undefined
const createdWindow = await chrome.windows.create({
focused: true, focused: true,
height: 640, height: 680,
width: 530, width: 460,
left: 0,
top: 0,
type: "popup", type: "popup",
url: chrome.runtime.getURL( url: chrome.runtime.getURL(`ui/assets/wallet-popup.html#/sign-request/${id}`),
`ui/assets/wallet-popup.html#/sign-request/${id}`
),
}) })
const removeWindow = () => chrome.windows.remove(window.id!) windowId = createdWindow.id
port.onDisconnect.addListener(removeWindow) const targetWindowId = createdWindow.id
const onWindowsRemoved = (windowId: number) => {
if (windowId !== window.id) return const removeWindow = () => {
const signRequest = signRequests[id] if (windowId && windowId === targetWindowId) {
if (!signRequest) return chrome.windows.remove(windowId).catch(() => {})
signRequest.reject() 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) chrome.windows.onRemoved.addListener(onWindowsRemoved)
try { try {
const { userSignedExtensions } = await signRequest const { userSignedExtensions, password } = await signRequest
const [keypair, scheme] = await keyring.getKeypair(chainId, from) cleanUpWindowListeners(port)
const signer = getPolkadotSigner(
keypair.publicKey,
scheme,
keypair.sign
)
const mortality = userSignedExtensions.CheckMortality ?? { if (!password) {
mortal: true, throw new Error("password verification is strictly required for this action")
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)) 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)({ const tx = await createTx(chain.provider)({
signer, signer,
callData: fromHex(callData), callData: fromHex(callData),
hinted: { hinted: { mortality, asset, tip },
mortality,
asset,
tipWithDecimals,
},
}) })
;(keypair as any) = null;
;(signer as any) = null;
return toHex(tx) return toHex(tx)
} catch (err) {
cleanUpWindowListeners(port)
throw err
} finally { } finally {
delete signRequests[id] delete signRequests[id]
chrome.windows.remove(window.id!) if (windowId) {
port.onDisconnect.removeListener(removeWindow) chrome.windows.remove(windowId).catch(() => {})
chrome.windows.onRemoved.removeListener(onWindowsRemoved)
} }
} finally { }
chainHead$.unfollow() } catch (error) {
client.destroy() const msgError = error instanceof Error ? error.message : String(error)
throw new Error(`Transaction signing context failed: ${msgError}`)
} }
}, },
async pjsSignPayload( async pjsSignPayload(
@ -201,16 +291,29 @@ export const createBackgroundRpc = (
) { ) {
const url = port.sender?.url const url = port.sender?.url
if (!url) throw new Error("unknown 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 chains = await lightClientPageHelper.getChains()
const chain = chains.find( const chain = chains.find(
({ genesisHash }) => genesisHash === payload.genesisHash ({ genesisHash }) => genesisHash === payload.genesisHash
) )
if (!chain) throw new Error("unknown chain") if (!chain) throw new Error("unknown chain")
const id = nextSignRequestId++ const id = nextSignRequestId++
const currentRequestId = id
const signRequest = new Promise< const signRequest = new Promise<
Parameters<InternalSignRequest["resolve"]>[0] Parameters<InternalSignRequest["resolve"]>[0]
>( >((resolve, reject) =>
(resolve, reject) =>
(signRequests[id] = { (signRequests[id] = {
resolve, resolve,
reject, reject,
@ -224,144 +327,154 @@ export const createBackgroundRpc = (
}, },
}) })
) )
const window = await chrome.windows.create({
let windowId: number | undefined = undefined
const createdWindow = await chrome.windows.create({
focused: true, focused: true,
height: 640, height: 680,
width: 530, width: 460,
left: 0,
top: 0,
type: "popup", type: "popup",
url: chrome.runtime.getURL( url: chrome.runtime.getURL(`ui/assets/wallet-popup.html#/sign-request/${id}`),
`ui/assets/wallet-popup.html#/sign-request/${id}`
),
}) })
const removeWindow = () => chrome.windows.remove(window.id!)
port.onDisconnect.addListener(removeWindow) windowId = createdWindow.id
const onWindowsRemoved = (windowId: number) => { const targetWindowId = createdWindow.id
if (windowId !== window.id) return
const signRequest = signRequests[id] const removeWindow = () => {
if (!signRequest) return if (windowId && windowId === targetWindowId) {
signRequest.reject() chrome.windows.remove(windowId).catch(() => {})
windowId = undefined
} }
chrome.windows.onRemoved.addListener(onWindowsRemoved) }
try {
await signRequest const onWindowsRemoved = (closedWindowId: number) => {
} finally { if (closedWindowId === windowId) {
delete signRequests[id] if (signRequests[currentRequestId]) {
chrome.windows.remove(window.id!) signRequests[currentRequestId].reject(new Error("user closed connection window"))
}
cleanUpWindowListeners(port)
windowId = undefined
}
}
const cleanUpWindowListeners = (port: any) => {
port.onDisconnect.removeListener(removeWindow) port.onDisconnect.removeListener(removeWindow)
chrome.windows.onRemoved.removeListener(onWindowsRemoved) chrome.windows.onRemoved.removeListener(onWindowsRemoved)
} }
const signaturePayload = await pjs.getSignaturePayload(
chain.provider, port.onDisconnect.addListener(removeWindow)
payload 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({ const multiSignatureEncoder = Variant({
Ed25519: Bytes(64), Ed25519: Bytes(64),
Sr25519: Bytes(64), Sr25519: Bytes(64),
Ecdsa: Bytes(65), Ecdsa: Bytes(65),
}).enc }).enc
const [keypair, scheme] = await keyring.getKeypair(
payload.genesisHash, let [keypair, scheme] = await keyring.getKeypairWithPassword(payload.genesisHash, password)
toHex(ss58Decode(payload.address)[0])
) const result = multiSignatureEncoder({
return toHex(
multiSignatureEncoder({
type: scheme, type: scheme,
value: keypair.sign(signaturePayload), 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 }) { async getSignRequests(_, { signRequests }) {
return signRequests return signRequests
}, },
async approveSignRequest([id, userSignedExtensions], { signRequests }) { async approveSignRequest([id, userSignedExtensions, password], { signRequests }) {
signRequests[id]?.resolve({ signRequests[id]?.resolve({
userSignedExtensions, userSignedExtensions,
password
}) })
}, },
async cancelSignRequest([id], { signRequests }) { async cancelSignRequest([id], { signRequests }) {
signRequests[id]?.reject() signRequests[id]?.reject()
}, },
async lockKeyring() { async changePassword([args]) {
return keyring.lock() return keyring.changePassword(args)
}, },
async resetKeyring([password]) { async insertAccount([args], context) {
return keyring.reset(password) await keyring.insertAccount(args)
await notifyOnAccountsChanged(context)
}, },
async unlockKeyring([password]) { async deriveAccount([args], context) {
return keyring.unlock(password) await keyring.deriveAccount(args)
await notifyOnAccountsChanged(context)
}, },
async changePassword([currentPassword, newPassword]) { async revealSecretSeed([args]) {
return keyring.changePassword(currentPassword, newPassword) return await keyring.revealSecretSeed(args)
}, },
async createPassword([password]) { async prepareBackup([args]) {
return keyring.setup(password) return await keyring.prepareBackup(args)
}, },
async insertCryptoKey([args], context) { async extendKeystore([args]) {
const existingKey = await keyring.getCryptoKey(args.name) await keyring.extendKeystore(args)
if (existingKey)
throw new Error(`crypto key "${args.name}" already exists`)
await keyring.insertCryptoKey(args)
notifyOnAccountsChanged(context)
}, },
async updateCryptoKey([args], context) { async renameAccount([args]) {
const existingKey = await keyring.getCryptoKey(args.name) await keyring.renameAccount(args)
if (!existingKey)
throw new Error(`crypto key "${args.name}" does not exist`)
await keyring.updateCryptoKey(args)
notifyOnAccountsChanged(context)
}, },
async getCryptoKey([name]) { async removeAccount([args]) {
return keyring.getCryptoKey(name) await keyring.removeAccount(args)
}, },
async getCryptoKeys() { async setActiveAccount([args], context) {
return keyring.getCryptoKeys() await keyring.setActiveAccount(args)
}, await notifyOnAccountsChanged(context)
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, getChainSpecs: listChainSpecsHandler,
addChainSpec: addChainSpecHandler,
removeChainSpec: removeChainSpecHandler,
} }
type Method = keyof BackgroundRpcSpec type Method = keyof BackgroundRpcSpec
const ALLOWED_WEB_METHODS: Method[] = [ const ALLOWED_WEB_METHODS: Method[] = [
"createTx", "createTx",
"getAccounts", "getAccount",
"pjsSignPayload", "pjsSignPayload",
] ]
const allowedMethodsMiddleware: RpcMethodMiddleware<Context> = async ( const allowedMethodsMiddleware: RpcMethodMiddleware<Context> = async (
next, next,
request, request,
context context
) => { ) => {
const { port } = context const { port } = context
if (
port.name === CHANNEL_ID && const isWebChannel = port.name === CHANNEL_ID
!ALLOWED_WEB_METHODS.includes(request.method as Method) const isInternalSource = port.sender?.url?.startsWith(chrome.runtime.getURL(""))
)
throw new RpcError("Method not found", -32601) 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)
}
return next(request, context) return next(request, context)
} }
return createRpc(sendMessage, handlers, [
allowedMethodsMiddleware, return createRpc(sendMessage, handlers, [allowedMethodsMiddleware]).withClient<InPageRpcSpec>()
]).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, })
{
const newVault = keystoreV4.create(args.password, encodeSecret(args.miniSecret))
const newKeystoreItem: KeystoreV4WithMeta = {
metadata: {
name: args.name, name: args.name,
scheme: args.scheme, scheme: args.scheme,
createdAt: args.createdAt, chainId: args.chainId,
...newCryptoKey, 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 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 === 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))
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, assert(keystore, "keystore with this public key does not exist")
meta: keystore.meta, if (args.path) {
}) const metadata = keystore.metadata
break const derivations = await getAccountDerivations(metadata)
default: const derivation = derivations.find(d => d.path === args.path)
throw new Error("invalid keystore type")
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 clearCryptoKeys = async (args: RemoveCryptoKeyArgs) => { 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(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) assert(keystore, "specific crypto vault not found based on account name")
setKeystore({ assert(keystoreV4.verifyPassword(keystore.vault, args.password), "invalid password for keystore provided")
...newKeystore,
meta: keystore.meta, if (args.path) {
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
}) })
} }
const revealCryptoKey = 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((k) => k.name === name)
assert(cryptoKeyIndex != -1, "keyring name not found")
const secret = decodeSecrets(
keystoreV4.decrypt(keystore, currentPassword)
).at(cryptoKeyIndex)
assert(secret, "secret key should exists")
// TODO: need to get secret from secret and derivation path
return secret
} }
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 }
})
)
}
const extendKeystore = async (args: ExtendKeystoreArgs): Promise<void> => {
const { includeDerivations, overwriteExisting, keystores } = args
const currentStorage = await getKeystoreStorage()
const namesSet = new Set((currentStorage?.keystores || [])
.map((acc: KeystoreV4WithMeta) => acc.metadata.name)
)
const publicKeysMap = new Map((currentStorage?.keystores || [])
.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}`
}
namesSet.add(finalName)
const newKeystoreItem: KeystoreMeta = { ...newAcc.metadata, name: finalName }
publicKeysMap.set(publicKey, { metadata: newKeystoreItem, vault: newAcc.vault })
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 { return {
async unlock(password: string) { async changePassword(args: ChangePasswordArgs) {
const keystore = await getKeystore() assert(args.password, "password is required for this operation")
assert(keystore, "keyring must be setup") assert(args.newPassword, "new password is required for this operation")
if (!keystoreV4.verifyPassword(keystore, password)) assert(args.password !== args.newPassword, "new password must be different")
throw new Error("invalid password")
currentPassword = password const keystoreStorage = await getKeystoreStorage()
}, const keystoreIndex = keystoreStorage.keystores.findIndex(
async lock() { (keystore: KeystoreV4WithMeta) => keystore.metadata.publicKey === args.publicKey
assert(await getKeystore(), "keyring must be setup") )
assert(currentPassword, "keyring must be unlocked")
currentPassword = undefined assert(keystoreIndex !== -1, "specific crypto vault not found based on account name")
}, const keystore = keystoreStorage.keystores[keystoreIndex]
async isLocked() { const decryptedBytes = keystoreV4.decrypt(keystore.vault, args.password)
return !currentPassword || !(await getKeystore())
}, try {
async reset(password: string) { const secret = decodeSecret(decryptedBytes)
const keystore = await getKeystore() const newEncryptedVault = keystoreV4.create(args.newPassword, encodeSecret(secret))
assert(keystore, "keyring must be setup")
assert(currentPassword, "keyring must be unlocked") keystoreStorage.keystores[keystoreIndex] = {
if (!keystoreV4.verifyPassword(keystore, password)) ...keystore,
throw new Error("invalid password") vault: newEncryptedVault
currentPassword = undefined }
await removeKeystore() } finally {
}, decryptedBytes.fill(0)
async changePassword(password: string, newPassword: string) { }
const keystore = await getKeystore()
assert(keystore, "keyring must be setup") await setKeystoreStorage({ keystores: keystoreStorage.keystores })
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())
}, },
setActiveAccount,
getActiveAccount,
getAccounts, getAccounts,
insertCryptoKey, getAccountDerivations,
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) => { insertAccount,
switch (keyset.type) { deriveAccount,
case "KeysetKeystore":
return keyset.derivationPaths.some( renameAccount,
(d) => d.chainId === chainId && d.publicKey === publicKey, 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
) )
case "KeypairKeystore":
return keyset.publicKey === publicKey
default:
throw new Error("invalid keystore type")
}
})
if (keysetIndex === -1) { assert(keystore, "unknown account targets mapped")
throw new Error("unknown account") assert(keystoreV4.verifyPassword(keystore.vault, password), "invalid password for keystore provided")
}
const secret = decodeSecrets( const decryptedBytes = keystoreV4.decrypt(keystore.vault, password)
keystoreV4.decrypt(keystore, currentPassword),
)[keysetIndex]
const cryptoKey = keystore.meta[keysetIndex] try {
switch (cryptoKey.type) { const masterSecret = decodeSecret(decryptedBytes)
case "KeysetKeystore": { const { scheme } = keystore.metadata
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 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,12 +23,10 @@ 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[]]
@ -105,8 +105,9 @@ export const getUserSignedExtensions = (payload: Pjs.SignerPayloadJSON) => {
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

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
| {
type: "Keyset"
miniSecret: string 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,23 +18,43 @@ 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",
"overflow-hidden",
)}
>
<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 innerClassName
)} )}
> >
{children} {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>
<AccountsProvider>
<Suspense fallback={<EmptyPage />}> <Suspense fallback={<EmptyPage />}>
<Routes> <Routes>
<Route element={<ProtectedRoute />}>
<Route path="/" element={<Accounts />} /> <Route path="/" element={<Accounts />} />
<Route path="/settings" element={<Settings />} />
<Route path="/change-password" element={<ChangePassword />} />
<Route path="/delete-wallet" element={<DeleteWallet />} />
<Route path="/accounts/:accountId" element={<AccountDetails />} />
<Route path="/accounts" element={<Accounts />} />
<Route path="/accounts/add" element={<AddAccount />} />
<Route path="/accounts/switch" element={<SwitchAccount />} />
<Route path="/accounts/import" element={<ImportAccounts />} />
<Route path="/sign-request/:signRequestId" element={<SignRequest />} />
<Route path="/add-chain-by-user" element={<AddChainByUser />} />
</Route>
<Route path="/options" element={<Options />} />
<Route path="/welcome" element={<Welcome />} /> <Route path="/welcome" element={<Welcome />} />
<Route path="/networks" element={<Networks />} /> <Route path="/settings" element={<Settings />} />
<Route path="/create-password" element={<CreatePassword />} /> <Route path="/options" element={<Options />} />
<Route path="/unlock-keyring" element={<UnlockKeyring />} />
<Route path="/accounts" element={<Accounts />} />
<Route path="/add-account" element={<AddAccount />} />
<Route path="/derive-account" element={<DeriveAccount />} />
<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 />} />
</Routes> </Routes>
</Suspense> </Suspense>
</KeyringProvider> </AccountsProvider>
</ChainProvider>
</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
return (
<div
key={item.name} key={item.name}
variant="ghost" className="flex flex-1 flex-col items-center justify-center cursor-pointer space-y-1"
onClick={() => navigate(item.to)} onClick={() => handleNavigation(item)}
>
<Button
variant="secondary"
size="icon"
className={cn( className={cn(
"flex flex-col items-center py-2 hover:bg-accent hover:text-background", "w-16 h-14 rounded-xl border-none p-0 flex flex-col items-center justify-center transition-all duration-200",
currentItem === item.name ? "text-accent" : "text-primary", 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-6 h-6 min-h-6" /> <item.icon className="w-5 h-5" />
<span className="text-xs font-medium capitalize">{item.name}</span> <span className="text-[10px] font-semibold tracking-wide capitalize">
{item.name}
</span>
</Button> </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) => {
if (trimmedName === "") {
setNewName(currentName)
return
}
if (trimmedName === currentName || !accountTree) return
try { try {
setError("") await renameAccount({
setIsPending(true) newName: trimmedName,
await func(...args) publicKey: accountTree.publicKey,
} catch (e) { path: derivationPath
setError(e.message) })
} finally { setNewName(trimmedName)
setIsPending(false) } catch {
setSecretKey(defaultSecretKey) 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>
<div className="flex flex-col gap-2">
<div className="text-sm text-primary"> <div className="space-y-2 animate-fade-in text-left w-full">
Secret key, press reveal to make it visible <Label className="text-sm text-white/90">Public Address</Label>
</div> <div className="relative w-full">
<div className="relative">
<Textarea <Textarea
onClick={() => api.copy()}
readOnly readOnly
aria-label="Secret key" 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" id="secret"
rows={2} rows={2}
className={`min-h-0 h-[60px] text-justify text-muted-foreground resize-none cursor-pointer`} className={cn(
value={secretKey} "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 && <div className="absolute w-full h-full z-10 inset-0 rounded-md bg-background/30 backdrop-blur-[4px]"></div>} isRevealed ? "cursor-default" : "cursor-pointer select-none"
</div>
</div>
{error && (
<div className="text-sm font-medium text-destructive">
{error}
</div>
)} )}
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>
</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 <Button
disabled={isPending || isRevealed} disabled={isRevealed}
onClick={wrappedRequestSecretKey} onClick={() => navigate(`/delete-account/${derivation ? encodeURIComponent(derivation) : ""}`)}
variant="secondary" variant="primary"
size="full" size="full"
aria-label="Reveal key" aria-label="Delete"
>
<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" /> <Trash2 className="w-5 h-5 mr-2" />
Delete Address Delete {`${derivationPath ? "Derivation" : "Account"}`}
</Button>
</div>
<Button
disabled={isPending}
onClick={() => navigate(-1)}
variant="ghost"
className="flex items-center mt-2 ml-0 w-full"
aria-label="Go Back"
>
<ArrowLeft className="w-5 h-5 mr-2" />
Back
</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">
<Card
data-testid={accountTree?.name ?? ""}
onClick={() => accountTree ? selectAccount({ publicKey: accountTree.publicKey }) : {}}
className={`p-3 pb-1 border-none rounded-lg text-card-foreground transition-all shadow-md hover:bg-input ${
activePublicKey === accountTree?.publicKey ? "bg-input ring-2 ring-white ring-inset" : "bg-card cursor-pointer"
}`}
>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div className="space-y-0">
<CardTitle className="font-semibold text-base mb-0.5"> <CardTitle className="font-bold text-sm tracking-wide text-white">
{props.name ? props.name : "Root Address" } {accountTree?.name ?? ""}
</CardTitle> </CardTitle>
<CardDescription className="text-xs truncate max-w-[240px]"> <CardDescription
{props.ss58Address} 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> </CardDescription>
</div> </div>
<Button variant="ghost" asChild>
<Link <Button
data-testid={`${props.name}-expand`} asChild
to={`/accounts/${props.ss58Address}?meta=${props.meta}&index=${props.index}`} variant="secondary"
size="icon"
className="h-8 w-8 rounded-md"
> >
<ArrowRight className="w-4 h-4" /> <Link to="/account-details">
<Pencil className="w-3.5 h-3.5" />
</Link> </Link>
</Button> </Button>
</div> </div>
</Card> </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
index={i} accountTree={filteredAccountTree}
meta={selectedCryptoKeyName} selectAccount={selectAccount}
name={derivationPath} path={activeAccount?.path}
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,230 +46,289 @@ 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 />
<ScrollArea className="grow">
<Form {...form}> <Form {...form}>
<form className="relative grow" onSubmit={handleSubmit(onSubmit)}> <form className="h-full flex flex-col" onSubmit={handleSubmit(onSubmit)}>
<Card className="border-none bg-muted 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"> <CardHeader className="space-y-1 pt-6 pb-2 px-6">
<CardTitle className="text-xl font-semibold tracking-normal"> <CardTitle className="text-xl font-bold tracking-normal text-white">
{step === 1 {currentStep === 1 ? "Save Mnemonic Seed" : "Create Account"}
? "Choose Name"
: step === 2
? "Select Crypto Scheme"
: "Review Seed Phrase"}
</CardTitle> </CardTitle>
<CardDescription className="text-accent"> <CardDescription className="text-primary font-medium text-sm">
Step {step} of 3 Step {currentStep} of 2
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent>
{step === 1 && ( <CardContent className="flex-grow px-6 py-2 space-y-4">
<div className="step-1"> {currentStep === 2 && (errors.cryptoKeyName || errors.password || errors.confirmPassword || customError) && (
<div className="bg-error text-white p-3 rounded text-xs text-center font-medium leading-normal shadow-sm">
{errors.cryptoKeyName?.message ||
errors.password?.message ||
errors.confirmPassword?.message ||
customError}
</div>
)}
{currentStep === 2 && !(errors.cryptoKeyName || errors.password || errors.confirmPassword || customError) && (
<div className="bg-success text-white p-3 rounded text-xs text-center font-medium leading-normal shadow-sm animate-fade-in">
All fields are valid. Ready to generate account!
</div>
)}
{currentStep === 1 && (
<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 <FormField
name="cryptoKeyName" name="cryptoKeyName"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem className="space-y-1.5">
<FormDescription className="mb-4 text-primary text-xs"> <Label className="text-sm text-white/90">Account Name</Label>
Set a name for your crypto key. This name will be
used to identify your crypto key within the
extension.
</FormDescription>
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
id="cryptoKeyName" id="cryptoKeyName"
type="text" type="text"
placeholder="Set the key name" 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> </FormControl>
<FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</div>
)}
{step === 2 && (
<div className="step-2">
<FormField <FormField
name="scheme" name="password"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem className="space-y-1.5">
<FormDescription className="mb-4 text-primary text-xs"> <Label className="text-sm text-white/90">Password</Label>
Choose the cryptographic signature scheme that meets your <div className="relative flex items-center">
requirements (default: Sr25519)
</FormDescription>
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl> <FormControl>
<RadioGroup <Input
onValueChange={(scheme) => field.onChange(scheme)} {...field}
value={field.value} id="password"
> type={showPassword ? "text" : "password"}
<RadioGroupItem value="Sr25519" id="Sr25519" label="Sr25519" /> placeholder="Enter new password"
<RadioGroupItem value="Ed25519" id="Ed25519" label="Ed25519" /> 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"
<RadioGroupItem value="Ecdsa" id="ECDSA" label="ECDSA" /> />
</RadioGroup>
</FormControl> </FormControl>
</FormItem> <button
<FormMessage /> 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>
</FormItem> </FormItem>
)} )}
/> />
</div>
)}
{step === 3 && (
<div className="step-3">
<FormField <FormField
name="seedPhraseConfirmed" name="confirmPassword"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<FormItem className="space-y-0 red"> <FormItem className="space-y-1.5">
<FormDescription className="mb-4 text-primary text-xs"> <Label className="text-sm text-white/90">Confirm Password</Label>
Your seed phrase is a unique set of words that you <div className="relative flex items-center">
need to back up and recover your wallet. Do not <FormControl>
share this with anyone. <Input
</FormDescription> {...field}
<Textarea id="confirmPassword"
readOnly type={showConfirmPassword ? "text" : "password"}
disabled={isSubmitting} placeholder="Confirm new password"
aria-label="Created mnemonic phrase" 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"
id="text"
rows={4}
className={cn(
"min-h-auto h-auto text-justify resize-none cursor-pointer",
"blur-[2px] hover:blur-none"
)}
placeholder={mnemonic.join(" ")}
/>
<div className="flex pt-4 pb-2">
<FormControl className="mr-2">
<Checkbox
aria-label="I have written down my seed phrase."
checked={field.value}
onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<Label className="block text-primary"> <button
I have written down my seed phrase. type="button"
</Label> onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 text-white/40 hover:text-white/70 transition-colors"
>
{showConfirmPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div> </div>
<FormMessage className="text-xs" />
</FormItem> </FormItem>
)} )}
/> />
</div> </div>
)} )}
</CardContent> </CardContent>
<CardFooter className="flex justify-between mt-6">
{step > 1 && ( <CardFooter className="flex w-full gap-x-4 mt-6 px-6 pb-6">
<Button <Button
type="button"
disabled={isSubmitting} disabled={isSubmitting}
onClick={handleBack} onClick={handleBack}
className="flex items-center space-x-2" className="flex-1 flex items-center justify-center space-x-2"
variant="secondary" variant="secondary"
> >
<ArrowLeftIcon className="w-4 h-4" /> <ArrowLeftIcon className="w-4 h-4" />
<span>Back</span> <span>Back</span>
</Button> </Button>
)}
{step < 3 ? ( {currentStep === 1 ? (
<Button <Button
disabled={isSubmitting} type="button"
disabled={isSubmitting || !isSeedConfirmed}
onClick={handleNext} onClick={handleNext}
className="flex items-center ml-auto space-x-2" className="flex-1 flex items-center justify-center space-x-2"
variant="secondary" variant="secondary"
> >
<span>Next</span> <span>Next</span>
@ -279,18 +338,17 @@ export const AddAccount = () => {
<Button <Button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
className="flex items-center ml-auto space-x-2" className="flex-1 flex items-center justify-center space-x-2"
variant="secondary" variant="primary"
> >
<CheckCircleIcon className="w-4 h-4" /> <CheckCircleIcon className="w-4 h-4" />
<span>Finish</span> <span>Create</span>
</Button> </Button>
)} )}
</CardFooter> </CardFooter>
</Card> </Card>
</form> </form>
</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,71 +59,33 @@ 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,
Ecdsa: ecdsaCreateDerive,
}
const privateKeyIsValid = (value: string | undefined, scheme: string) => {
if (!value) return false
const bytes = fromHex(value)
try {
switch (scheme) {
case "Ecdsa":
ecdsa.getPublicKey(bytes)
break
case "Ed25519":
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({ z.object({
_type: z.enum(["mnemonic", "privateKey", "seed"]), _type: z.literal("mnemonic"),
mnemonic: z.string(), mnemonic: z.string().trim().min(1, "mnemonic is required"),
}), }),
z.object({ z.object({
_type: z.enum(["mnemonic", "privateKey", "seed"]), _type: z.literal("seed"),
privateKey: z.string(), seed: z.string().trim().min(1, "seed is required"),
}), }),
z.object({ z.object({
_type: z.enum(["mnemonic", "privateKey", "seed"]), _type: z.literal("jsonBackup"),
seed: z.string(), jsonBackup: z.instanceof(File, { message: "backup archive is required" }).nullable().optional(),
}) archivePassword: z.string(),
includeDerivations: z.boolean().default(true),
overwriteExisting: z.boolean().default(false),
}),
]), ]),
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 [isPending, setIsPending] = useState<boolean>(false)
const form = useForm<FormFields>({ const form = useForm<FormFields>({
resolver: zodResolver(importAccountScheme), resolver: zodResolver(importAccountScheme),
@ -129,9 +94,8 @@ export function ImportAccounts() {
_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 "seed":
setValue("tab", { _type: "seed", seed: "" })
break;
default:
setValue("tab", { _type: "privateKey", privateKey: "" })
break;
}
setValue("scheme", "")
setValue("keyname", "")
} }
const onSubmit: SubmitHandler<FormFields> = async (data) => { case "jsonBackup": {
let errorOccured = null setValue("tab", {
_type: "jsonBackup",
const keynameExists = await rpc.client.getCryptoKey(data.keyname) jsonBackup: null as any,
if (keynameExists) { archivePassword: "",
errorOccured = { includeDerivations: true,
field: "keyname", overwriteExisting: false,
message: "Account with this name already exists",
}
} else {
switch (data.tab._type) {
case "privateKey": {
try {
if (!privateKeyIsValid(data.tab.privateKey, data.scheme)) {
throw new Error("a")
}
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": { case "seed": {
try { setValue("tab", { _type: "seed", seed: "" })
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({
type: "Keyset",
name: data.keyname,
scheme: data.scheme,
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) default: {
const miniSecret = entropyToMiniSecret(entropy) setValue("tab", { _type: "mnemonic", mnemonic: "" })
const derive = createDeriveFnMap[data.scheme](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.keyname,
scheme: data.scheme,
miniSecret: bytesToHex(miniSecret),
derivationPaths,
createdAt: Date.now(),
})
} catch {
errorOccured = {
field: "tab.mnemonic",
message: "Could not use provided mnemonic phrase",
}
}
break break
} }
} }
} }
if (errorOccured) {
setError(errorOccured.field, { const onSubmit: SubmitHandler<FormFields> = async (data) => {
type: "server", setIsPending(true)
message: errorOccured.message, let miniSecret: Uint8Array | null = null;
try {
switch (data.tab._type) {
case "mnemonic":
const cleanMnemonic = data.tab.mnemonic.trim()
const entropy = mnemonicToEntropy(cleanMnemonic)
miniSecret = entropyToMiniSecret(entropy)
navigate("/add-account", { state: { miniSecret }})
break
case "seed":
const cleanSeed = data.tab.seed
miniSecret = fromHex(cleanSeed)
navigate("/add-account", { state: { miniSecret }})
break
case "jsonBackup":
const file = data.tab.jsonBackup
if (!file) return
const archivePassword = data.tab.archivePassword
const includeDerivations = data.tab.includeDerivations
const overwriteExisting = data.tab.overwriteExisting
const zipBlobReader = new BlobReader(file)
const zipReader = new ZipReader(zipBlobReader, { password: archivePassword })
const entries = await zipReader.getEntries()
const entry = entries.find(e => e.filename === "full-keystore.json")
if (!entry) {
await zipReader.close();
throw new Error("Invalid backup archive: JSON data not found inside.")
}
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,
}) })
} else {
window.localStorage.setItem("selectedCryptoKeyName", data.keyname) await accountsStorageMutate()
navigate("/accounts") navigate("/accounts")
break
default:
break;
}
} finally {
setIsPending(true)
} }
} }
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" && (
<div className="space-y-4">
<FormField <FormField
control={control} control={control}
name="tab.privateKey" name="tab.jsonBackup"
render={({ field }) => (
<FormItem className="mb-4">
<FormControl>
<div className="relative">
<input
type="file"
accept=".zip"
id="zip-file-input"
disabled={isSubmitting || isPending}
aria-label="Click to upload the backup archieve here"
className="sr-only"
onChange={(e) => {
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 }) => ( render={({ field }) => (
<FormItem className="mb-4"> <FormItem className="mb-4">
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
disabled={isSubmitting} disabled={isSubmitting || isPending}
aria-label="Enter private key" aria-label="Enter archive password"
type="text" type="password"
className="pr-10" className={cn(
placeholder="Enter private key" "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> </FormControl>
<FormMessage />
</FormItem> </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 () => { const start = address.slice(0, startChars)
try { const end = address.slice(-endChars)
if (!cryptoKeys) return
window.localStorage.setItem( return `${start}...${end}`
"selectedCryptoKeyName", }
selectedCryptoKeyName,
) const AccountsSkeleton: React.FC = () => {
} finally { 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 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") navigate("/accounts")
} }
}
const cryptoKeyNames = cryptoKeys?.map(({ name }) => name) ?? []
return ( return (
<Layout> <Layout2>
<form className="max-w-xl p-6 mx-auto" onSubmit={handleSubmit(onSubmit)}> {accounts && accounts.length > 5 && (<div className="flex items-center justify-between px-6 mt-4 mb-6">
<h1 className="mb-4 text-2xl font-bold">Switch Account</h1> <Input
<RadioGroup value={query}
value={selectedCryptoKeyName} onChange={(e) => setQuery(e.target.value)}
onChange={setSelectedCryptoKeyName} id="query"
> type="text"
<RadioGroup.Label className="sr-only">Crypto Key</RadioGroup.Label> 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"> <div className="space-y-2">
{cryptoKeyNames.map((name) => ( {filteredAccounts && filteredAccounts.map(acc => {
<RadioGroup.Option const isSelected = isAccountSelected(acc.publicKey)
key={name}
value={name} return (
className={({ active, checked }) => <Card
`${ key={acc.publicKey}
active data-testid={acc.publicKey}
? "ring-2 ring-white/60 ring-offset-2 ring-offset-black" 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"
${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"> <div className="flex items-center justify-between">
<User className="text-gray-600" /> <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> </div>
<div className="flex items-center justify-center">
<span className="text-lg">{name}</span> <Button
</div> variant="ghost"
</RadioGroup.Option> size="icon"
))} className={`text-white h-8 w-8 border-none hover:bg-input`}
</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 {isSelected ? <CircleCheck className="w-3.5 h-3.5" /> : <Circle className="w-3.5 h-3.5" />}
</button> </Button>
</div> </div>
</form> </Card>
</Layout> )
})}
</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,78 +39,121 @@ 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
aria-label="Request Origin"
type="text"
className="pr-10"
placeholder={request.url}
/>
</div> </div>
<div className="my-2 overflow-hidden"> )}
<div className="text-xs font-semibold mb-2 mt-4">Chain</div>
<Input {isExecutionBlocked && (
readOnly <div className="mt-4 p-3.5 rounded-xl border border-destructive/30 bg-destructive/15 text-destructive-foreground text-xs space-y-1">
aria-label="Request Origin" <span className="font-bold flex items-center gap-1 text-red-400">
type="text" Signing Request Blocked
className="pr-10" </span>
placeholder={wellKnownChainIdByGenesisHash[request.chainId]} <p className="text-white/80 leading-relaxed">
/> {isRequestNotFound && "This transaction payload could not be recovered or found by the proxy client pipeline."}
{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.`}
{isChainMismatched && `Network Mismatch! This transaction was generated for chain context "${wellKnownChainIdByGenesisHash[request.chainId] || 'Unknown'} ", but your extension is currently connected to "${wellKnownChainIdByGenesisHash[chainId] || 'Unknown'}".`}
</p>
</div> </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>
<div className="my-2 overflow-hidden">
<div className="text-xs font-semibold mb-2 mt-4">Call data</div> {!isRequestNotFound && (
<Textarea <div className="w-full flex flex-col gap-1 pt-2 px-6">
readOnly <div className="w-full flex flex-row justify-between items-center">
aria-label="Call data" <span className="text-xs font-semibold text-white">Origin</span>
rows={6} <span className="text-xs font-medium text-primary truncate max-w-[280px] select-all font-mono" title={request.url}>
className="text-sm text-justify resize-none" {request.url}
placeholder={request.callData} </span>
/>
</div> </div>
<div className="my-2">
<div className="text-xs font-semibold mb-2 mt-4">Decoded Call data</div> <div className="w-full flex flex-row justify-between items-center">
<span className="text-xs font-semibold text-white">Chain</span>
<span className="text-xs font-medium text-primary font-mono">
{wellKnownChainIdByGenesisHash[request.chainId] || "Unknown Chain Spec"}
</span>
</div>
<div className="w-full flex flex-col min-w-0 mt-6">
<div className="text-xs font-semibold mb-2 text-white uppercase tracking-wider">
Transaction Details
</div>
<div className="w-full min-w-0">
<DecodedCallData chainId={request.chainId} callData={request.callData} /> <DecodedCallData chainId={request.chainId} callData={request.callData} />
</div> </div>
</div>
<div className="my-2"> <div className="my-2">
{request.userSignedExtensions.type === "names" ? ( {request.userSignedExtensions.type === "names" ? (
<UserSignedExtensionInputs <UserSignedExtensionInputs
@ -112,26 +167,29 @@ export const SignRequest = () => {
)} )}
</div> </div>
</div> </div>
<div className="w-full flex gap-4 justify-center space-x-4 pt-6"> )}
</div>
<div className="w-full flex gap-4 justify-center space-x-4 pt-6 px-6">
<Button <Button
variant="secondary" variant="secondary"
disabled={inAction}
size="full" size="full"
className="h-12"
onClick={() => rpc.client.approveSignRequest(signRequestId, userSignedExtensions)}
>
Approve
</Button>
<Button
variant="secondary"
size="full"
className="h-12"
onClick={() => rpc.client.cancelSignRequest(signRequestId)} onClick={() => rpc.client.cancelSignRequest(signRequestId)}
> >
Cancel Cancel
</Button> </Button>
<Button
variant="primary"
size="full"
disabled={isExecutionBlocked || inAction}
onClick={() => handleSigning()}
>
Approve
</Button>
</div> </div>
</section> </div>
</ScrollArea> </Layout2>
</Layout>
) )
} }

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"
to="/options"
target="_blank"
rel="noopener noreferrer"
className="w-full"
>
<Button <Button
type="button"
variant="secondary" variant="secondary"
size="full" size="full"
className={cn("text-lg py-3")} onClick={() => navigate("/import-account")}
> >
Options Import Account
</Button> </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))",