From 42be72a7d1c13e0c9978e27afb4d925927f763c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix?= Date: Thu, 14 Nov 2024 14:20:27 +0100 Subject: [PATCH 1/8] All tests pass + simpler SDK API --- .pnp.cjs | 139 ++--- Anchor.toml | 5 + sdk/solana/package.json | 1 + sdk/solana/tbrv3/idl/token_bridge_relayer.ts | 8 + .../token-bridge-cpi-accounts-builder.ts | 178 ++++++ sdk/solana/tbrv3/token-bridge-relayer.ts | 523 ++++++------------ solana/programs/token-bridge-relayer/build.rs | 26 +- .../token-bridge-relayer/network.json | 10 +- .../src/processor/inbound.rs | 6 +- .../src/processor/initialize.rs | 6 + .../src/processor/outbound.rs | 7 +- .../token-bridge-relayer/src/state/config.rs | 4 + .../token-bridge-relayer/src/utils.rs | 31 +- solana/tests/token-bridge-relayer-tests.ts | 187 +++++-- solana/tests/token_metadata.so | Bin 0 -> 681120 bytes solana/tests/utils/client-wrapper.ts | 145 +++-- solana/tests/utils/helpers.ts | 9 + solana/tests/utils/wormhole-pdas.ts | 49 ++ target/idl/token_bridge_relayer.json | 8 + target/types/token_bridge_relayer.ts | 8 + yarn.lock | 1 + 21 files changed, 764 insertions(+), 587 deletions(-) create mode 100644 sdk/solana/tbrv3/token-bridge-cpi-accounts-builder.ts create mode 100755 solana/tests/token_metadata.so create mode 100644 solana/tests/utils/wormhole-pdas.ts diff --git a/.pnp.cjs b/.pnp.cjs index 18e42153..7a022d03 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -1635,15 +1635,15 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:9d6a3c5919ce35c2c92492439514128d1b7daddea2f122cfafad95a4724f4e98114597fe65b64b51199291322e7acd458c94f96310c9ef6adfd94ddeeb1ec6b7#npm:2.0.0-rc.1", {\ - "packageLocation": "./.yarn/__virtual__/@solana-codecs-virtual-18f73b176d/0/cache/@solana-codecs-npm-2.0.0-rc.1-7d3ba53573-5f4a30b1fe.zip/node_modules/@solana/codecs/",\ - "packageDependencies": [\ - ["@solana/codecs", "virtual:9d6a3c5919ce35c2c92492439514128d1b7daddea2f122cfafad95a4724f4e98114597fe65b64b51199291322e7acd458c94f96310c9ef6adfd94ddeeb1ec6b7#npm:2.0.0-rc.1"],\ - ["@solana/codecs-core", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/codecs-data-structures", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/codecs-numbers", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/codecs-strings", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/options", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ + ["virtual:a0f2ab78f9051be943778c6e7a4f911223af163b18042860d722eb12158af0922f8639789c054b3864f0f7325be4ee9ea9a481756a93f2de27ca4219ba031daf#npm:2.0.0-preview.4", {\ + "packageLocation": "./.yarn/__virtual__/@solana-codecs-virtual-825f9426fd/0/cache/@solana-codecs-npm-2.0.0-preview.4-02d9a7993a-49a736614a.zip/node_modules/@solana/codecs/",\ + "packageDependencies": [\ + ["@solana/codecs", "virtual:a0f2ab78f9051be943778c6e7a4f911223af163b18042860d722eb12158af0922f8639789c054b3864f0f7325be4ee9ea9a481756a93f2de27ca4219ba031daf#npm:2.0.0-preview.4"],\ + ["@solana/codecs-core", "virtual:825f9426fd36f5d1a32160a52e20c89919f13a0c1f814b57d8bd2112e647b415eeb7a18edccd6c0b825e503570ca80304ce76fd737117c3ce2af1ef9dab2f458#npm:2.0.0-preview.4"],\ + ["@solana/codecs-data-structures", "virtual:825f9426fd36f5d1a32160a52e20c89919f13a0c1f814b57d8bd2112e647b415eeb7a18edccd6c0b825e503570ca80304ce76fd737117c3ce2af1ef9dab2f458#npm:2.0.0-preview.4"],\ + ["@solana/codecs-numbers", "virtual:825f9426fd36f5d1a32160a52e20c89919f13a0c1f814b57d8bd2112e647b415eeb7a18edccd6c0b825e503570ca80304ce76fd737117c3ce2af1ef9dab2f458#npm:2.0.0-preview.4"],\ + ["@solana/codecs-strings", "virtual:825f9426fd36f5d1a32160a52e20c89919f13a0c1f814b57d8bd2112e647b415eeb7a18edccd6c0b825e503570ca80304ce76fd737117c3ce2af1ef9dab2f458#npm:2.0.0-preview.4"],\ + ["@solana/options", "virtual:825f9426fd36f5d1a32160a52e20c89919f13a0c1f814b57d8bd2112e647b415eeb7a18edccd6c0b825e503570ca80304ce76fd737117c3ce2af1ef9dab2f458#npm:2.0.0-preview.4"],\ ["@types/typescript", null],\ ["typescript", null]\ ],\ @@ -1653,15 +1653,15 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ - ["virtual:a0f2ab78f9051be943778c6e7a4f911223af163b18042860d722eb12158af0922f8639789c054b3864f0f7325be4ee9ea9a481756a93f2de27ca4219ba031daf#npm:2.0.0-preview.4", {\ - "packageLocation": "./.yarn/__virtual__/@solana-codecs-virtual-825f9426fd/0/cache/@solana-codecs-npm-2.0.0-preview.4-02d9a7993a-49a736614a.zip/node_modules/@solana/codecs/",\ + ["virtual:b14bca88483878c548749297e7bfe0e011ded7a9ab26e0b54229225098369abc9ea2c9e35b498e3663a3a260ab745524301b4de015eb0842a54d9590a2a45d45#npm:2.0.0-rc.1", {\ + "packageLocation": "./.yarn/__virtual__/@solana-codecs-virtual-05603e46c8/0/cache/@solana-codecs-npm-2.0.0-rc.1-7d3ba53573-5f4a30b1fe.zip/node_modules/@solana/codecs/",\ "packageDependencies": [\ - ["@solana/codecs", "virtual:a0f2ab78f9051be943778c6e7a4f911223af163b18042860d722eb12158af0922f8639789c054b3864f0f7325be4ee9ea9a481756a93f2de27ca4219ba031daf#npm:2.0.0-preview.4"],\ - ["@solana/codecs-core", "virtual:825f9426fd36f5d1a32160a52e20c89919f13a0c1f814b57d8bd2112e647b415eeb7a18edccd6c0b825e503570ca80304ce76fd737117c3ce2af1ef9dab2f458#npm:2.0.0-preview.4"],\ - ["@solana/codecs-data-structures", "virtual:825f9426fd36f5d1a32160a52e20c89919f13a0c1f814b57d8bd2112e647b415eeb7a18edccd6c0b825e503570ca80304ce76fd737117c3ce2af1ef9dab2f458#npm:2.0.0-preview.4"],\ - ["@solana/codecs-numbers", "virtual:825f9426fd36f5d1a32160a52e20c89919f13a0c1f814b57d8bd2112e647b415eeb7a18edccd6c0b825e503570ca80304ce76fd737117c3ce2af1ef9dab2f458#npm:2.0.0-preview.4"],\ - ["@solana/codecs-strings", "virtual:825f9426fd36f5d1a32160a52e20c89919f13a0c1f814b57d8bd2112e647b415eeb7a18edccd6c0b825e503570ca80304ce76fd737117c3ce2af1ef9dab2f458#npm:2.0.0-preview.4"],\ - ["@solana/options", "virtual:825f9426fd36f5d1a32160a52e20c89919f13a0c1f814b57d8bd2112e647b415eeb7a18edccd6c0b825e503570ca80304ce76fd737117c3ce2af1ef9dab2f458#npm:2.0.0-preview.4"],\ + ["@solana/codecs", "virtual:b14bca88483878c548749297e7bfe0e011ded7a9ab26e0b54229225098369abc9ea2c9e35b498e3663a3a260ab745524301b4de015eb0842a54d9590a2a45d45#npm:2.0.0-rc.1"],\ + ["@solana/codecs-core", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/codecs-data-structures", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/codecs-numbers", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/codecs-strings", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/options", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ ["@types/typescript", null],\ ["typescript", null]\ ],\ @@ -1687,11 +1687,11 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1", {\ - "packageLocation": "./.yarn/__virtual__/@solana-codecs-core-virtual-e4ccfae9b9/0/cache/@solana-codecs-core-npm-2.0.0-rc.1-5076cbaceb-3b1fd09727.zip/node_modules/@solana/codecs-core/",\ + ["virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1", {\ + "packageLocation": "./.yarn/__virtual__/@solana-codecs-core-virtual-dcd7745c00/0/cache/@solana-codecs-core-npm-2.0.0-rc.1-5076cbaceb-3b1fd09727.zip/node_modules/@solana/codecs-core/",\ "packageDependencies": [\ - ["@solana/codecs-core", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/errors", "virtual:e4ccfae9b91a3c6885593193d776815b60300ea23ca6e62d3cc891a05f002d031cedc3e20dea14cc1183b605dad96eb181eeafda45f69259970e0ca96924a914#npm:2.0.0-rc.1"],\ + ["@solana/codecs-core", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/errors", "virtual:dcd7745c00883918b5458d95a78cb53a3b2486ed9d76c1413b8644cca8dd564331cf53c7bd281e9fb3834b00c3ba3897c01b67bfd2324dde29f6caf42f7ed56a#npm:2.0.0-rc.1"],\ ["@types/typescript", null],\ ["typescript", null]\ ],\ @@ -1731,13 +1731,13 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1", {\ - "packageLocation": "./.yarn/__virtual__/@solana-codecs-data-structures-virtual-3f1726bbda/0/cache/@solana-codecs-data-structures-npm-2.0.0-rc.1-239d9704c0-e22dd63699.zip/node_modules/@solana/codecs-data-structures/",\ + ["virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1", {\ + "packageLocation": "./.yarn/__virtual__/@solana-codecs-data-structures-virtual-535e7b03f7/0/cache/@solana-codecs-data-structures-npm-2.0.0-rc.1-239d9704c0-e22dd63699.zip/node_modules/@solana/codecs-data-structures/",\ "packageDependencies": [\ - ["@solana/codecs-data-structures", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/codecs-core", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/codecs-numbers", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/errors", "virtual:e4ccfae9b91a3c6885593193d776815b60300ea23ca6e62d3cc891a05f002d031cedc3e20dea14cc1183b605dad96eb181eeafda45f69259970e0ca96924a914#npm:2.0.0-rc.1"],\ + ["@solana/codecs-data-structures", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/codecs-core", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/codecs-numbers", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/errors", "virtual:dcd7745c00883918b5458d95a78cb53a3b2486ed9d76c1413b8644cca8dd564331cf53c7bd281e9fb3834b00c3ba3897c01b67bfd2324dde29f6caf42f7ed56a#npm:2.0.0-rc.1"],\ ["@types/typescript", null],\ ["typescript", null]\ ],\ @@ -1779,12 +1779,12 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1", {\ - "packageLocation": "./.yarn/__virtual__/@solana-codecs-numbers-virtual-4db98b24e3/0/cache/@solana-codecs-numbers-npm-2.0.0-rc.1-115b36782e-baf888bbd9.zip/node_modules/@solana/codecs-numbers/",\ + ["virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1", {\ + "packageLocation": "./.yarn/__virtual__/@solana-codecs-numbers-virtual-76849cbda4/0/cache/@solana-codecs-numbers-npm-2.0.0-rc.1-115b36782e-baf888bbd9.zip/node_modules/@solana/codecs-numbers/",\ "packageDependencies": [\ - ["@solana/codecs-numbers", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/codecs-core", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/errors", "virtual:e4ccfae9b91a3c6885593193d776815b60300ea23ca6e62d3cc891a05f002d031cedc3e20dea14cc1183b605dad96eb181eeafda45f69259970e0ca96924a914#npm:2.0.0-rc.1"],\ + ["@solana/codecs-numbers", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/codecs-core", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/errors", "virtual:dcd7745c00883918b5458d95a78cb53a3b2486ed9d76c1413b8644cca8dd564331cf53c7bd281e9fb3834b00c3ba3897c01b67bfd2324dde29f6caf42f7ed56a#npm:2.0.0-rc.1"],\ ["@types/typescript", null],\ ["typescript", null]\ ],\ @@ -1825,13 +1825,13 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1", {\ - "packageLocation": "./.yarn/__virtual__/@solana-codecs-strings-virtual-d578b7a051/0/cache/@solana-codecs-strings-npm-2.0.0-rc.1-182be3c4d7-7f3483407d.zip/node_modules/@solana/codecs-strings/",\ + ["virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1", {\ + "packageLocation": "./.yarn/__virtual__/@solana-codecs-strings-virtual-548fe23fd9/0/cache/@solana-codecs-strings-npm-2.0.0-rc.1-182be3c4d7-7f3483407d.zip/node_modules/@solana/codecs-strings/",\ "packageDependencies": [\ - ["@solana/codecs-strings", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/codecs-core", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/codecs-numbers", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/errors", "virtual:e4ccfae9b91a3c6885593193d776815b60300ea23ca6e62d3cc891a05f002d031cedc3e20dea14cc1183b605dad96eb181eeafda45f69259970e0ca96924a914#npm:2.0.0-rc.1"],\ + ["@solana/codecs-strings", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/codecs-core", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/codecs-numbers", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/errors", "virtual:dcd7745c00883918b5458d95a78cb53a3b2486ed9d76c1413b8644cca8dd564331cf53c7bd281e9fb3834b00c3ba3897c01b67bfd2324dde29f6caf42f7ed56a#npm:2.0.0-rc.1"],\ ["@types/fastestsmallesttextencoderdecoder", null],\ ["@types/typescript", null],\ ["fastestsmallesttextencoderdecoder", null],\ @@ -1896,10 +1896,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ - ["virtual:e4ccfae9b91a3c6885593193d776815b60300ea23ca6e62d3cc891a05f002d031cedc3e20dea14cc1183b605dad96eb181eeafda45f69259970e0ca96924a914#npm:2.0.0-rc.1", {\ - "packageLocation": "./.yarn/__virtual__/@solana-errors-virtual-49bc1359fc/0/cache/@solana-errors-npm-2.0.0-rc.1-99b9f45244-26b9edb43b.zip/node_modules/@solana/errors/",\ + ["virtual:dcd7745c00883918b5458d95a78cb53a3b2486ed9d76c1413b8644cca8dd564331cf53c7bd281e9fb3834b00c3ba3897c01b67bfd2324dde29f6caf42f7ed56a#npm:2.0.0-rc.1", {\ + "packageLocation": "./.yarn/__virtual__/@solana-errors-virtual-8e85b7f051/0/cache/@solana-errors-npm-2.0.0-rc.1-99b9f45244-26b9edb43b.zip/node_modules/@solana/errors/",\ "packageDependencies": [\ - ["@solana/errors", "virtual:e4ccfae9b91a3c6885593193d776815b60300ea23ca6e62d3cc891a05f002d031cedc3e20dea14cc1183b605dad96eb181eeafda45f69259970e0ca96924a914#npm:2.0.0-rc.1"],\ + ["@solana/errors", "virtual:dcd7745c00883918b5458d95a78cb53a3b2486ed9d76c1413b8644cca8dd564331cf53c7bd281e9fb3834b00c3ba3897c01b67bfd2324dde29f6caf42f7ed56a#npm:2.0.0-rc.1"],\ ["@types/typescript", null],\ ["chalk", "npm:5.3.0"],\ ["commander", "npm:12.1.0"],\ @@ -1927,15 +1927,15 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1", {\ - "packageLocation": "./.yarn/__virtual__/@solana-options-virtual-19430b7549/0/cache/@solana-options-npm-2.0.0-rc.1-fce3f9ae7e-967dc01c12.zip/node_modules/@solana/options/",\ + ["virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1", {\ + "packageLocation": "./.yarn/__virtual__/@solana-options-virtual-b0f4339131/0/cache/@solana-options-npm-2.0.0-rc.1-fce3f9ae7e-967dc01c12.zip/node_modules/@solana/options/",\ "packageDependencies": [\ - ["@solana/options", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/codecs-core", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/codecs-data-structures", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/codecs-numbers", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/codecs-strings", "virtual:18f73b176d08ea97bb5ba34f40a529862e63af922e9e127a4012776b0ae30483a91c1cfb88a4e48258f0b185ac307a5b5e1263577599e12c0884420d31d7301e#npm:2.0.0-rc.1"],\ - ["@solana/errors", "virtual:e4ccfae9b91a3c6885593193d776815b60300ea23ca6e62d3cc891a05f002d031cedc3e20dea14cc1183b605dad96eb181eeafda45f69259970e0ca96924a914#npm:2.0.0-rc.1"],\ + ["@solana/options", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/codecs-core", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/codecs-data-structures", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/codecs-numbers", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/codecs-strings", "virtual:05603e46c82529e3bac520bae9f619871887867f8c0752249cf872313d343c60e1459b6246bda950adc710f20c1b979c0b0566203a54ac37b6b754be3a997e68#npm:2.0.0-rc.1"],\ + ["@solana/errors", "virtual:dcd7745c00883918b5458d95a78cb53a3b2486ed9d76c1413b8644cca8dd564331cf53c7bd281e9fb3834b00c3ba3897c01b67bfd2324dde29f6caf42f7ed56a#npm:2.0.0-rc.1"],\ ["@types/typescript", null],\ ["typescript", null]\ ],\ @@ -1986,14 +1986,14 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:0d651797a2843a64f9d24ebdfb5ce5b0c9db186e0bfb087fd6b67500708c972daac5ffe588b86f37cbeeb4bf85347e25cd633d8c0eb04e887e07267bb68096fd#npm:0.4.9", {\ - "packageLocation": "./.yarn/__virtual__/@solana-spl-token-virtual-f163f75d9a/0/cache/@solana-spl-token-npm-0.4.9-7f1d70ccee-66f22a026f.zip/node_modules/@solana/spl-token/",\ + ["virtual:5449dbfbc8f715fdbd2c737c0a09a82e4cd2ffe644d99dd1bc0581b0889f04b69068ed991ce11b47be2adc46d31415f8c245c393d61c33529fcc22e453281945#npm:0.4.8", {\ + "packageLocation": "./.yarn/__virtual__/@solana-spl-token-virtual-585b5d4f83/0/cache/@solana-spl-token-npm-0.4.8-e1313fe791-6570a439d9.zip/node_modules/@solana/spl-token/",\ "packageDependencies": [\ - ["@solana/spl-token", "virtual:0d651797a2843a64f9d24ebdfb5ce5b0c9db186e0bfb087fd6b67500708c972daac5ffe588b86f37cbeeb4bf85347e25cd633d8c0eb04e887e07267bb68096fd#npm:0.4.9"],\ + ["@solana/spl-token", "virtual:5449dbfbc8f715fdbd2c737c0a09a82e4cd2ffe644d99dd1bc0581b0889f04b69068ed991ce11b47be2adc46d31415f8c245c393d61c33529fcc22e453281945#npm:0.4.8"],\ ["@solana/buffer-layout", "npm:4.0.1"],\ ["@solana/buffer-layout-utils", "npm:0.2.0"],\ - ["@solana/spl-token-group", "virtual:f163f75d9afd7480a70fbfa6f9aba48c297163c8e336036bb9756b57fe5c93298d6e637ec25ec7b6c4bc511c242f886077a0a0f5d0f99509a8e608d4c7a29ba1#npm:0.0.7"],\ - ["@solana/spl-token-metadata", "virtual:f163f75d9afd7480a70fbfa6f9aba48c297163c8e336036bb9756b57fe5c93298d6e637ec25ec7b6c4bc511c242f886077a0a0f5d0f99509a8e608d4c7a29ba1#npm:0.1.6"],\ + ["@solana/spl-token-group", "virtual:585b5d4f831a4ba2a36933077686307b3e808f154056c94faf113072d10e5d795d7119bbc98ba70e9296bad802bf9227b95ae9322e93b77c74e6966843f51c5b#npm:0.0.5"],\ + ["@solana/spl-token-metadata", "virtual:585b5d4f831a4ba2a36933077686307b3e808f154056c94faf113072d10e5d795d7119bbc98ba70e9296bad802bf9227b95ae9322e93b77c74e6966843f51c5b#npm:0.1.5"],\ ["@solana/web3.js", "npm:1.95.3"],\ ["@types/solana__web3.js", null],\ ["buffer", "npm:6.0.3"]\ @@ -2004,14 +2004,14 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ - ["virtual:5449dbfbc8f715fdbd2c737c0a09a82e4cd2ffe644d99dd1bc0581b0889f04b69068ed991ce11b47be2adc46d31415f8c245c393d61c33529fcc22e453281945#npm:0.4.8", {\ - "packageLocation": "./.yarn/__virtual__/@solana-spl-token-virtual-585b5d4f83/0/cache/@solana-spl-token-npm-0.4.8-e1313fe791-6570a439d9.zip/node_modules/@solana/spl-token/",\ + ["virtual:6f2483ac784c41cab463da71887863a83b7aa4862c2a18272cbafb46b02451cd5342c3a97418eaa9816a54d6aae60a957dfa730193d9f71fe528fcf5887818bb#npm:0.4.9", {\ + "packageLocation": "./.yarn/__virtual__/@solana-spl-token-virtual-c0acbfa4dc/0/cache/@solana-spl-token-npm-0.4.9-7f1d70ccee-66f22a026f.zip/node_modules/@solana/spl-token/",\ "packageDependencies": [\ - ["@solana/spl-token", "virtual:5449dbfbc8f715fdbd2c737c0a09a82e4cd2ffe644d99dd1bc0581b0889f04b69068ed991ce11b47be2adc46d31415f8c245c393d61c33529fcc22e453281945#npm:0.4.8"],\ + ["@solana/spl-token", "virtual:6f2483ac784c41cab463da71887863a83b7aa4862c2a18272cbafb46b02451cd5342c3a97418eaa9816a54d6aae60a957dfa730193d9f71fe528fcf5887818bb#npm:0.4.9"],\ ["@solana/buffer-layout", "npm:4.0.1"],\ ["@solana/buffer-layout-utils", "npm:0.2.0"],\ - ["@solana/spl-token-group", "virtual:585b5d4f831a4ba2a36933077686307b3e808f154056c94faf113072d10e5d795d7119bbc98ba70e9296bad802bf9227b95ae9322e93b77c74e6966843f51c5b#npm:0.0.5"],\ - ["@solana/spl-token-metadata", "virtual:585b5d4f831a4ba2a36933077686307b3e808f154056c94faf113072d10e5d795d7119bbc98ba70e9296bad802bf9227b95ae9322e93b77c74e6966843f51c5b#npm:0.1.5"],\ + ["@solana/spl-token-group", "virtual:c0acbfa4dcc40a059a626896dcbb510afb820e50c74ddc0e48b6a9720e163880ddbc866b9ae63bf9cf95817547ec8625b26d55669dc5da584e788691b969717f#npm:0.0.7"],\ + ["@solana/spl-token-metadata", "virtual:c0acbfa4dcc40a059a626896dcbb510afb820e50c74ddc0e48b6a9720e163880ddbc866b9ae63bf9cf95817547ec8625b26d55669dc5da584e788691b969717f#npm:0.1.6"],\ ["@solana/web3.js", "npm:1.95.3"],\ ["@types/solana__web3.js", null],\ ["buffer", "npm:6.0.3"]\ @@ -2069,11 +2069,11 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ - ["virtual:f163f75d9afd7480a70fbfa6f9aba48c297163c8e336036bb9756b57fe5c93298d6e637ec25ec7b6c4bc511c242f886077a0a0f5d0f99509a8e608d4c7a29ba1#npm:0.0.7", {\ - "packageLocation": "./.yarn/__virtual__/@solana-spl-token-group-virtual-21a1bcab74/0/cache/@solana-spl-token-group-npm-0.0.7-a93ce99255-e1ebeb30c4.zip/node_modules/@solana/spl-token-group/",\ + ["virtual:c0acbfa4dcc40a059a626896dcbb510afb820e50c74ddc0e48b6a9720e163880ddbc866b9ae63bf9cf95817547ec8625b26d55669dc5da584e788691b969717f#npm:0.0.7", {\ + "packageLocation": "./.yarn/__virtual__/@solana-spl-token-group-virtual-b14bca8848/0/cache/@solana-spl-token-group-npm-0.0.7-a93ce99255-e1ebeb30c4.zip/node_modules/@solana/spl-token-group/",\ "packageDependencies": [\ - ["@solana/spl-token-group", "virtual:f163f75d9afd7480a70fbfa6f9aba48c297163c8e336036bb9756b57fe5c93298d6e637ec25ec7b6c4bc511c242f886077a0a0f5d0f99509a8e608d4c7a29ba1#npm:0.0.7"],\ - ["@solana/codecs", "virtual:9d6a3c5919ce35c2c92492439514128d1b7daddea2f122cfafad95a4724f4e98114597fe65b64b51199291322e7acd458c94f96310c9ef6adfd94ddeeb1ec6b7#npm:2.0.0-rc.1"],\ + ["@solana/spl-token-group", "virtual:c0acbfa4dcc40a059a626896dcbb510afb820e50c74ddc0e48b6a9720e163880ddbc866b9ae63bf9cf95817547ec8625b26d55669dc5da584e788691b969717f#npm:0.0.7"],\ + ["@solana/codecs", "virtual:b14bca88483878c548749297e7bfe0e011ded7a9ab26e0b54229225098369abc9ea2c9e35b498e3663a3a260ab745524301b4de015eb0842a54d9590a2a45d45#npm:2.0.0-rc.1"],\ ["@solana/web3.js", "npm:1.95.3"],\ ["@types/solana__web3.js", null]\ ],\ @@ -2103,7 +2103,7 @@ const RAW_RUNTIME_STATE = "packageLocation": "./.yarn/__virtual__/@solana-spl-token-metadata-virtual-9d6a3c5919/0/cache/@solana-spl-token-metadata-npm-0.1.5-3e43b1c467-8ddad2a3f0.zip/node_modules/@solana/spl-token-metadata/",\ "packageDependencies": [\ ["@solana/spl-token-metadata", "virtual:585b5d4f831a4ba2a36933077686307b3e808f154056c94faf113072d10e5d795d7119bbc98ba70e9296bad802bf9227b95ae9322e93b77c74e6966843f51c5b#npm:0.1.5"],\ - ["@solana/codecs", "virtual:9d6a3c5919ce35c2c92492439514128d1b7daddea2f122cfafad95a4724f4e98114597fe65b64b51199291322e7acd458c94f96310c9ef6adfd94ddeeb1ec6b7#npm:2.0.0-rc.1"],\ + ["@solana/codecs", "virtual:b14bca88483878c548749297e7bfe0e011ded7a9ab26e0b54229225098369abc9ea2c9e35b498e3663a3a260ab745524301b4de015eb0842a54d9590a2a45d45#npm:2.0.0-rc.1"],\ ["@solana/spl-type-length-value", "npm:0.1.0"],\ ["@solana/web3.js", "npm:1.95.3"],\ ["@types/solana__web3.js", null]\ @@ -2114,11 +2114,11 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ - ["virtual:f163f75d9afd7480a70fbfa6f9aba48c297163c8e336036bb9756b57fe5c93298d6e637ec25ec7b6c4bc511c242f886077a0a0f5d0f99509a8e608d4c7a29ba1#npm:0.1.6", {\ - "packageLocation": "./.yarn/__virtual__/@solana-spl-token-metadata-virtual-4364697b72/0/cache/@solana-spl-token-metadata-npm-0.1.6-b685f79ab6-a2ea535ac2.zip/node_modules/@solana/spl-token-metadata/",\ + ["virtual:c0acbfa4dcc40a059a626896dcbb510afb820e50c74ddc0e48b6a9720e163880ddbc866b9ae63bf9cf95817547ec8625b26d55669dc5da584e788691b969717f#npm:0.1.6", {\ + "packageLocation": "./.yarn/__virtual__/@solana-spl-token-metadata-virtual-c71e347c90/0/cache/@solana-spl-token-metadata-npm-0.1.6-b685f79ab6-a2ea535ac2.zip/node_modules/@solana/spl-token-metadata/",\ "packageDependencies": [\ - ["@solana/spl-token-metadata", "virtual:f163f75d9afd7480a70fbfa6f9aba48c297163c8e336036bb9756b57fe5c93298d6e637ec25ec7b6c4bc511c242f886077a0a0f5d0f99509a8e608d4c7a29ba1#npm:0.1.6"],\ - ["@solana/codecs", "virtual:9d6a3c5919ce35c2c92492439514128d1b7daddea2f122cfafad95a4724f4e98114597fe65b64b51199291322e7acd458c94f96310c9ef6adfd94ddeeb1ec6b7#npm:2.0.0-rc.1"],\ + ["@solana/spl-token-metadata", "virtual:c0acbfa4dcc40a059a626896dcbb510afb820e50c74ddc0e48b6a9720e163880ddbc866b9ae63bf9cf95817547ec8625b26d55669dc5da584e788691b969717f#npm:0.1.6"],\ + ["@solana/codecs", "virtual:b14bca88483878c548749297e7bfe0e011ded7a9ab26e0b54229225098369abc9ea2c9e35b498e3663a3a260ab745524301b4de015eb0842a54d9590a2a45d45#npm:2.0.0-rc.1"],\ ["@solana/web3.js", "npm:1.95.3"],\ ["@types/solana__web3.js", null]\ ],\ @@ -2683,6 +2683,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@xlabs-xyz/solana-arbitrary-token-transfers", "workspace:sdk/solana"],\ ["@coral-xyz/anchor", "npm:0.30.1"],\ + ["@solana/spl-token", "virtual:6f2483ac784c41cab463da71887863a83b7aa4862c2a18272cbafb46b02451cd5342c3a97418eaa9816a54d6aae60a957dfa730193d9f71fe528fcf5887818bb#npm:0.4.9"],\ ["@solana/web3.js", "npm:1.95.3"],\ ["@types/node", "npm:20.17.5"],\ ["@wormhole-foundation/sdk-base", "npm:0.12.0"],\ @@ -5986,7 +5987,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["solana", "workspace:solana"],\ ["@coral-xyz/anchor", "npm:0.30.1"],\ - ["@solana/spl-token", "virtual:0d651797a2843a64f9d24ebdfb5ce5b0c9db186e0bfb087fd6b67500708c972daac5ffe588b86f37cbeeb4bf85347e25cd633d8c0eb04e887e07267bb68096fd#npm:0.4.9"],\ + ["@solana/spl-token", "virtual:6f2483ac784c41cab463da71887863a83b7aa4862c2a18272cbafb46b02451cd5342c3a97418eaa9816a54d6aae60a957dfa730193d9f71fe528fcf5887818bb#npm:0.4.9"],\ ["@solana/web3.js", "npm:1.95.3"],\ ["@types/chai", "npm:5.0.0"],\ ["@types/mocha", "npm:10.0.9"],\ diff --git a/Anchor.toml b/Anchor.toml index d5719fc6..32869301 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -31,6 +31,11 @@ name = "wormhole-bridge" address = "DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe" # Testnet in lib.rs program = "solana/tests/token_bridge.so" +[[test.genesis]] +name = "mpl-token-metadata" +address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" +program = "solana/tests/token_metadata.so" + [registry] url = "https://api.apr.dev" diff --git a/sdk/solana/package.json b/sdk/solana/package.json index 51bcf37a..2cd9e89d 100644 --- a/sdk/solana/package.json +++ b/sdk/solana/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@coral-xyz/anchor": "^0.30.1", + "@solana/spl-token": "^0.4.9", "@solana/web3.js": "^1.95.3", "@wormhole-foundation/sdk-base": "^0.12.0", "@wormhole-foundation/sdk-definitions": "^0.12.0", diff --git a/sdk/solana/tbrv3/idl/token_bridge_relayer.ts b/sdk/solana/tbrv3/idl/token_bridge_relayer.ts index 59103e75..d97be39f 100644 --- a/sdk/solana/tbrv3/idl/token_bridge_relayer.ts +++ b/sdk/solana/tbrv3/idl/token_bridge_relayer.ts @@ -1996,6 +1996,14 @@ export type TokenBridgeRelayer = { "name": "evmTransactionSize", "type": "u64" }, + { + "name": "mintAuthority", + "docs": [ + "The mint authority used by the Token Bridge. Used to check whether a transfer is native", + "or wrapped." + ], + "type": "pubkey" + }, { "name": "senderBump", "type": "u8" diff --git a/sdk/solana/tbrv3/token-bridge-cpi-accounts-builder.ts b/sdk/solana/tbrv3/token-bridge-cpi-accounts-builder.ts new file mode 100644 index 00000000..8527688c --- /dev/null +++ b/sdk/solana/tbrv3/token-bridge-cpi-accounts-builder.ts @@ -0,0 +1,178 @@ +import { PublicKey } from '@solana/web3.js'; +import * as tokenBridge from '@wormhole-foundation/sdk-solana-tokenbridge'; +import { VaaMessage, WormholeAddress } from './token-bridge-relayer.js'; +import { chainToChainId } from '@wormhole-foundation/sdk-base'; + +/** Helper allowing to generate the list of accounts needed during a CPI. */ +export class TokenBridgeCpiAccountsBuilder { + constructor( + public programId: PublicKey, + public tokenBridgeProgramId: PublicKey, + public wormholeProgramId: PublicKey, + ) {} + + transferNative(mint: PublicKey): { + tokenBridgeConfig: PublicKey; + tokenBridgeCustody: PublicKey; + tokenBridgeAuthoritySigner: PublicKey; + tokenBridgeCustodySigner: PublicKey; + tokenBridgeWrappedMeta: null; + wormholeBridge: PublicKey; + tokenBridgeEmitter: PublicKey; + tokenBridgeSequence: PublicKey; + mint: PublicKey; + wormholeFeeCollector: PublicKey; + } { + const { + tokenBridgeConfig, + tokenBridgeCustody, + tokenBridgeAuthoritySigner, + tokenBridgeCustodySigner, + wormholeBridge, + tokenBridgeEmitter, + tokenBridgeSequence, + wormholeFeeCollector, + } = tokenBridge.getTransferNativeWithPayloadCpiAccounts( + this.programId, + this.tokenBridgeProgramId, + this.wormholeProgramId, + PublicKey.default, // we don't need payer + PublicKey.default, // we don't need message + PublicKey.default, // we don't need fromTokenAccount + mint, + ); + + return { + tokenBridgeConfig, + tokenBridgeCustody, + tokenBridgeAuthoritySigner, + tokenBridgeCustodySigner, + tokenBridgeWrappedMeta: null, + wormholeBridge, + tokenBridgeEmitter, + tokenBridgeSequence, + mint, + wormholeFeeCollector, + }; + } + + transferWrapped({ chain, address }: WormholeAddress): { + tokenBridgeConfig: PublicKey; + tokenBridgeCustody: null; + tokenBridgeAuthoritySigner: PublicKey; + tokenBridgeCustodySigner: null; + tokenBridgeWrappedMeta: PublicKey; + wormholeBridge: PublicKey; + tokenBridgeEmitter: PublicKey; + tokenBridgeSequence: PublicKey; + mint: PublicKey; + wormholeFeeCollector: PublicKey; + } { + const { + tokenBridgeConfig, + tokenBridgeAuthoritySigner, + wormholeBridge, + tokenBridgeEmitter, + tokenBridgeSequence, + tokenBridgeWrappedMeta, + tokenBridgeWrappedMint, + wormholeFeeCollector, + } = tokenBridge.getTransferWrappedWithPayloadCpiAccounts( + this.programId, + this.tokenBridgeProgramId, + this.wormholeProgramId, + PublicKey.default, // we don't need payer + PublicKey.default, // we don't need message + PublicKey.default, // we don't need fromTokenAccount + chainToChainId(chain), + address.toUint8Array(), + ); + + return { + tokenBridgeConfig, + tokenBridgeCustody: null, + tokenBridgeAuthoritySigner, + tokenBridgeCustodySigner: null, + tokenBridgeWrappedMeta, + wormholeBridge, + tokenBridgeEmitter, + tokenBridgeSequence, + mint: tokenBridgeWrappedMint, + wormholeFeeCollector, + }; + } + + completeNative(vaa: VaaMessage): { + tokenBridgeConfig: PublicKey; + tokenBridgeClaim: PublicKey; + tokenBridgeForeignEndpoint: PublicKey; + tokenBridgeCustody: PublicKey; + tokenBridgeCustodySigner: PublicKey; + tokenBridgeMintAuthority: null; + tokenBridgeWrappedMeta: null; + mint: PublicKey; + } { + const { + tokenBridgeConfig, + tokenBridgeClaim, + tokenBridgeForeignEndpoint, + tokenBridgeCustody, + tokenBridgeCustodySigner, + mint, + } = tokenBridge.getCompleteTransferNativeWithPayloadCpiAccounts( + this.tokenBridgeProgramId, + this.wormholeProgramId, + PublicKey.default, + vaa, + PublicKey.default, + ); + + return { + tokenBridgeConfig, + tokenBridgeClaim, + tokenBridgeForeignEndpoint, + tokenBridgeCustody, + tokenBridgeCustodySigner, + tokenBridgeMintAuthority: null, + tokenBridgeWrappedMeta: null, + mint, + }; + } + + completeWrapped(vaa: VaaMessage): { + tokenBridgeConfig: PublicKey; + tokenBridgeClaim: PublicKey; + tokenBridgeForeignEndpoint: PublicKey; + tokenBridgeCustody: null; + tokenBridgeCustodySigner: null; + tokenBridgeMintAuthority: PublicKey; + tokenBridgeWrappedMeta: PublicKey; + mint: PublicKey; + } { + const { + tokenBridgeConfig, + tokenBridgeClaim, + tokenBridgeForeignEndpoint, + tokenBridgeMintAuthority, + tokenBridgeWrappedMeta, + tokenBridgeWrappedMint, + } = tokenBridge.getCompleteTransferWrappedWithPayloadCpiAccounts( + this.tokenBridgeProgramId, + this.wormholeProgramId, + PublicKey.default, + vaa, + PublicKey.default, + ); + + return { + tokenBridgeConfig, + tokenBridgeClaim, + tokenBridgeForeignEndpoint, + tokenBridgeCustody: null, + tokenBridgeCustodySigner: null, + tokenBridgeMintAuthority, + tokenBridgeWrappedMeta, + mint: tokenBridgeWrappedMint, + }; + } +} diff --git a/sdk/solana/tbrv3/token-bridge-relayer.ts b/sdk/solana/tbrv3/token-bridge-relayer.ts index 372dfc9e..e764688f 100644 --- a/sdk/solana/tbrv3/token-bridge-relayer.ts +++ b/sdk/solana/tbrv3/token-bridge-relayer.ts @@ -10,6 +10,7 @@ import { LAMPORTS_PER_SOL, Keypair, } from '@solana/web3.js'; +import * as spl from '@solana/spl-token'; import * as borsh from 'borsh'; import { Chain, @@ -18,14 +19,9 @@ import { contracts, Network, chainIdToChain, + layout, } from '@wormhole-foundation/sdk-base'; -import { UniversalAddress } from '@wormhole-foundation/sdk-definitions'; -import { - getTransferNativeWithPayloadCpiAccounts, - getTransferWrappedWithPayloadCpiAccounts, - getCompleteTransferNativeWithPayloadCpiAccounts, - getCompleteTransferWrappedWithPayloadCpiAccounts, -} from '@wormhole-foundation/sdk-solana-tokenbridge'; +import { layoutItems, UniversalAddress } from '@wormhole-foundation/sdk-definitions'; import { SolanaPriceOracle, bigintToBn, bnToBigint } from '@xlabs-xyz/solana-price-oracle-sdk'; import { deserializeTbrV3Message, VaaMessage, throwError } from 'common-arbitrary-token-transfer'; import { BpfLoaderUpgradeableProgram } from './bpf-loader-upgradeable.js'; @@ -34,6 +30,8 @@ import { TokenBridgeRelayer as IdlType } from './idl/token_bridge_relayer.js'; import IDL from '../../../target/idl/token_bridge_relayer.json' with { type: 'json' }; import networkConfig from '../../../solana/programs/token-bridge-relayer/network.json' with { type: 'json' }; import testProgramKeypair from '../../../solana/programs/token-bridge-relayer/test-program-keypair.json' with { type: 'json' }; +import { TOKEN_PROGRAM_ID } from '@coral-xyz/anchor/dist/cjs/utils/token.js'; +import { TokenBridgeCpiAccountsBuilder } from './token-bridge-cpi-accounts-builder.js'; // Export IDL export * from './idl/token_bridge_relayer.js'; @@ -46,26 +44,21 @@ export interface WormholeAddress { address: UniversalAddress; } -export interface TransferNativeParameters { - recipient: WormholeAddress; - mint: PublicKey; - tokenAccount: PublicKey; - transferredAmount: bigint; - /** The dropoff in µ-target-token. */ - gasDropoffAmount: number; - maxFeeLamports: bigint; - unwrapIntent: boolean; -} - -export interface TransferWrappedParameters { +/** + * @param recipient The address on another chain to transfer the tokens to. + * @param userTokenAccount The end user's account with the token to be transferred. + * @param transferredAmount The amount to be transferred to the other chain. + * @param gasDropoffAmount The dropoff in µ-target-token. + * @param maxFeeLamports The maximum fee the user is ready to pay (including the dropoff). + * @param unwrapIntent Only used when transferring SOL back to Solana. Unused otherwise. + */ +export interface TransferParameters { recipient: WormholeAddress; - token: WormholeAddress; userTokenAccount: PublicKey; transferredAmount: bigint; - /** The dropoff in µ-target-token. */ gasDropoffAmount: number; maxFeeLamports: bigint; - unwrapIntent: boolean; + unwrapIntent?: boolean; } export type TbrConfigAccount = anchor.IdlAccounts['tbrConfigState']; @@ -86,6 +79,7 @@ export class SolanaTokenBridgeRelayer { private readonly priceOracleClient: SolanaPriceOracle; private readonly wormholeProgramId: PublicKey; private readonly tokenBridgeProgramId: PublicKey; + private readonly tbAccBuilder: TokenBridgeCpiAccountsBuilder; public debug: boolean; @@ -104,8 +98,13 @@ export class SolanaTokenBridgeRelayer { this.program = new anchor.Program(patchAddress(IDL, programId), provider); this.priceOracleClient = priceOracle; - this.wormholeProgramId = new PublicKey(contracts.coreBridge(wormholeNetwork, 'Solana')); this.tokenBridgeProgramId = new PublicKey(contracts.tokenBridge(wormholeNetwork, 'Solana')); + this.wormholeProgramId = new PublicKey(contracts.coreBridge(wormholeNetwork, 'Solana')); + this.tbAccBuilder = new TokenBridgeCpiAccountsBuilder( + programId, + this.tokenBridgeProgramId, + this.wormholeProgramId, + ); this.debug = debug; } @@ -281,19 +280,31 @@ export class SolanaTokenBridgeRelayer { }; } - private async payerSequenceNumber(payer: PublicKey): Promise { - const impl = async (payer: PublicKey) => { - try { - const account = await this.account.signerSequence(payer).fetch(); - return bnToBigint(account.value); - } catch { - return 0n; - } - }; + /** Returns the end user's wallet and associated token account */ + getRecipientAccountsFromVaa(vaa: VaaMessage): { + wallet: PublicKey; + associatedTokenAccount: PublicKey; + } { + const native = vaa.payload.token.chain === 'Solana'; + let mint; - const sequenceNumber = await impl(payer); - this.logDebug({ payerSequenceNumber: sequenceNumber.toString() }); - return sequenceNumber; + if (native) { + mint = new PublicKey(vaa.payload.token.address.toUint8Array()); + } else { + [mint] = PublicKey.findProgramAddressSync( + [ + Buffer.from('wrapped'), + chainSeed(vaa.payload.token.chain), + vaa.payload.token.address.toUint8Array(), + ], + this.tokenBridgeProgramId, + ); + } + + const wallet = new PublicKey(deserializeTbrV3Message(vaa.payload.payload).recipient.address); + const associatedTokenAccount = getAssociatedTokenAccount(wallet, mint); + + return { wallet, associatedTokenAccount }; } /* Initialize */ @@ -552,89 +563,44 @@ export class SolanaTokenBridgeRelayer { /* Transfers */ /** - * Signer: typically the Token Bridge. + * Signer: the tokens owner. */ - async transferNativeTokens( + async transferTokens( signer: PublicKey, - params: TransferNativeParameters, + params: TransferParameters, ): Promise { const { recipient, - mint, - tokenAccount: userTokenAccount, + userTokenAccount, transferredAmount, gasDropoffAmount, maxFeeLamports, unwrapIntent, } = params; - const { feeRecipient } = await this.read.config(); - const payerSequenceNumber = await this.payerSequenceNumber(signer); - - const tokenBridgeAccounts = transferNativeTokenBridgeAccounts({ - programId: this.program.programId, - tokenBridgeProgramId: this.tokenBridgeProgramId, - wormholeProgramId: this.wormholeProgramId, - mint, - }); - const accounts = { - payer: signer, - tbrConfig: this.account.config().address, - chainConfig: this.account.chainConfig(recipient.chain).address, - mint, - userTokenAccount, - temporaryAccount: this.account.temporary(mint).address, - feeRecipient, - oracleConfig: this.priceOracleClient.account.config().address, - oracleEvmPrices: this.priceOracleClient.account.evmPrices(recipient.chain).address, - ...tokenBridgeAccounts, - wormholeMessage: this.account.wormholeMessage(signer, payerSequenceNumber).address, - payerSequence: this.account.signerSequence(signer).address, - tokenBridgeProgram: this.tokenBridgeProgramId, - wormholeProgram: this.wormholeProgramId, + let transferType = '?'; + + const getTokenBridgeAccounts = async () => { + const mint = await spl + .getAccount(this.connection, userTokenAccount) + .then(({ mint }) => spl.getMint(this.connection, mint)); + if (mint.mintAuthority?.equals(this.tokenBridgeMintAuthority)) { + transferType = 'Wrapped'; + return this.tbAccBuilder.transferWrapped( + await this.getWormholeAddressFromWrappedMint(mint.address), + ); + } else { + transferType = 'Native'; + return this.tbAccBuilder.transferNative(mint.address); + } }; - this.logDebug('transferNativeTokens:', objToString(params), objToString(accounts)); - - return this.program.methods - .transferTokens( - uaToArray(recipient.address), - bigintToBn(transferredAmount), - unwrapIntent, - gasDropoffAmount, - bigintToBn(maxFeeLamports), - ) - .accountsPartial(accounts) - .instruction(); - } - - /** - * Signer: typically the Token Bridge. - */ - async transferWrappedTokens( - signer: PublicKey, - params: TransferWrappedParameters, - ): Promise { - const { - recipient, - token, - userTokenAccount, - transferredAmount, - gasDropoffAmount, - maxFeeLamports, - unwrapIntent, - } = params; - - const { feeRecipient } = await this.read.config(); - const payerSequenceNumber = await this.payerSequenceNumber(signer); + const [{ feeRecipient }, payerSequenceNumber, tokenBridgeAccounts] = await Promise.all([ + this.read.config(), + this.payerSequenceNumber(signer), + getTokenBridgeAccounts(), + ]); - const tokenBridgeAccounts = transferWrappedTokenBridgeAccounts({ - programId: this.program.programId, - tokenBridgeProgramId: this.tokenBridgeProgramId, - wormholeProgramId: this.wormholeProgramId, - tokenChain: chainToChainId(token.chain), - tokenAddress: Buffer.from(token.address.toUint8Array()), - }); const accounts = { payer: signer, tbrConfig: this.account.config().address, @@ -651,13 +617,13 @@ export class SolanaTokenBridgeRelayer { wormholeProgram: this.wormholeProgramId, }; - this.logDebug('transferWrappedTokens:', objToString(params), objToString(accounts)); + this.logDebug('transferTokens:', transferType, objToString(params), objToString(accounts)); return this.program.methods .transferTokens( uaToArray(recipient.address), bigintToBn(transferredAmount), - unwrapIntent, + unwrapIntent ?? false, gasDropoffAmount, bigintToBn(maxFeeLamports), ) @@ -670,24 +636,19 @@ export class SolanaTokenBridgeRelayer { * * @param signer * @param vaa - * @param recipientTokenAccount The user's account receiving the SPL tokens. */ - async completeNativeTransfer( - signer: PublicKey, - vaa: VaaMessage, - recipientTokenAccount: PublicKey, - ): Promise { - const tokenBridgeAccounts = completeNativeTokenBridgeAccounts({ - tokenBridgeProgramId: this.tokenBridgeProgramId, - wormholeProgramId: this.wormholeProgramId, - vaa, - }); - const { recipient } = deserializeTbrV3Message(vaa.payload.payload); + async completeTransfer(signer: PublicKey, vaa: VaaMessage): Promise { + const { wallet, associatedTokenAccount } = this.getRecipientAccountsFromVaa(vaa); + const native = vaa.payload.token.chain === 'Solana'; + const tokenBridgeAccounts = native + ? this.tbAccBuilder.completeNative(vaa) + : this.tbAccBuilder.completeWrapped(vaa); + const accounts = { payer: signer, tbrConfig: this.account.config().address, - recipientTokenAccount, - recipient: new PublicKey(recipient.address), + recipientTokenAccount: associatedTokenAccount, + recipient: wallet, vaa: this.account.vaa(vaa.hash).address, temporaryAccount: this.account.temporary(tokenBridgeAccounts.mint).address, ...tokenBridgeAccounts, @@ -696,45 +657,19 @@ export class SolanaTokenBridgeRelayer { peer: this.account.peer(vaa.emitterChain, vaa.payload.from).address, }; - this.logDebug('completeNativeTransfer:', accounts); - - return this.program.methods.completeTransfer().accountsPartial(accounts).instruction(); - } + this.logDebug('completeTransfer:', native ? 'Native:' : 'Wrapped:', objToString(accounts)); - /** - * Signer: typically the Token Bridge. - * - * @param signer - * @param vaa - * @param recipientTokenAccount The user's account receiving the SPL tokens. - */ - async completeWrappedTransfer( - signer: PublicKey, - vaa: VaaMessage, - recipientTokenAccount: PublicKey, - ): Promise { - const tokenBridgeAccounts = completeWrappedTokenBridgeAccounts({ - tokenBridgeProgramId: this.tokenBridgeProgramId, - wormholeProgramId: this.wormholeProgramId, - vaa, + const createAtaIdempotentIx = await createAssociatedTokenAccountIdempotent({ + signer, + mint: tokenBridgeAccounts.mint, + wallet, }); - const { recipient } = deserializeTbrV3Message(vaa.payload.payload); - const accounts = { - payer: signer, - tbrConfig: this.account.config().address, - recipientTokenAccount, - recipient: new PublicKey(recipient.address), - vaa: this.account.vaa(vaa.hash).address, - temporaryAccount: this.account.temporary(tokenBridgeAccounts.mint).address, - ...tokenBridgeAccounts, - tokenBridgeProgram: this.tokenBridgeProgramId, - wormholeProgram: this.wormholeProgramId, - peer: this.account.peer(vaa.emitterChain, vaa.payload.from).address, - }; - - this.logDebug('completeWrappedTransfer:', accounts); + const completeTransferIx = await this.program.methods + .completeTransfer() + .accountsPartial(accounts) + .instruction(); - return this.program.methods.completeTransfer().accountsPartial(accounts).instruction(); + return [createAtaIdempotentIx, completeTransferIx]; } /* Queries */ @@ -746,7 +681,7 @@ export class SolanaTokenBridgeRelayer { * @returns The fee to pay for the transfer in SOL. */ async relayingFee(chain: Chain, dropoffAmount: number): Promise { - assertProvider(this.program.provider); + providerAssert(this.program.provider); const tx = await this.program.methods .relayingFee(dropoffAmount) @@ -766,6 +701,8 @@ export class SolanaTokenBridgeRelayer { return Number(result) / LAMPORTS_PER_SOL; } + /* HELPERS */ + private accountInfo>( account: anchor.AccountClient, seeds: Array, @@ -778,8 +715,50 @@ export class SolanaTokenBridgeRelayer { }; } - /* HELPERS */ - logDebug(message?: any, ...optionalParams: any[]) { + private async payerSequenceNumber(payer: PublicKey): Promise { + const impl = async (payer: PublicKey) => { + try { + const account = await this.account.signerSequence(payer).fetch(); + return bnToBigint(account.value); + } catch { + return 0n; + } + }; + + const sequenceNumber = await impl(payer); + this.logDebug({ payerSequenceNumber: sequenceNumber.toString() }); + return sequenceNumber; + } + + /** Get the info about the foreign address from a Wormhole mint */ + private async getWormholeAddressFromWrappedMint(mint: PublicKey): Promise { + const [metaAddress] = PublicKey.findProgramAddressSync( + [Buffer.from('meta'), mint.toBuffer()], + this.tokenBridgeProgramId, + ); + const { data } = + (await this.connection.getAccountInfo(metaAddress)) ?? + throwError( + 'Cannot find the meta info\nThe mint authority indicates that the token is a Wormhole one, but no meta information is associated with it.', + ); + + const { chain, address } = layout.deserializeLayout( + [ + { name: 'chain', ...layoutItems.chainItem(), endianness: 'little' }, + { name: 'address', ...layoutItems.universalAddressItem }, + { name: 'decimals', binary: 'uint', size: 1 }, + ], + data, + ); + + return { chain, address }; + } + + private get tokenBridgeMintAuthority(): PublicKey { + return findPda(this.tokenBridgeProgramId, [Buffer.from('mint_signer')]).address; + } + + private logDebug(message?: any, ...optionalParams: any[]) { conditionalDebug(this.debug, message, ...optionalParams); } } @@ -791,6 +770,7 @@ function conditionalDebug(debug: boolean, message?: any, ...optionalParams: any[ } const chainSeed = (chain: Chain) => encoding.bignum.toBytes(chainToChainId(chain), 2); + function findPda(programId: PublicKey, seeds: Array) { const [address, seed] = PublicKey.findProgramAddressSync(seeds, programId); return { @@ -799,7 +779,36 @@ function findPda(programId: PublicKey, seeds: Array) { }; } -function assertProvider(provider: anchor.Provider) { +function getAssociatedTokenAccount(wallet: PublicKey, mint: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [wallet.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], + spl.ASSOCIATED_TOKEN_PROGRAM_ID, + )[0]; +} + +/** Return both the address and an idempotent instruction to create it. */ +async function createAssociatedTokenAccountIdempotent({ + signer, + mint, + wallet, +}: { + signer: PublicKey; + mint: PublicKey; + wallet: PublicKey; +}): Promise { + const recipientTokenAccount = getAssociatedTokenAccount(wallet, mint); + + const createAtaIdempotentIx = spl.createAssociatedTokenAccountIdempotentInstruction( + signer, + recipientTokenAccount, + wallet, + mint, + ); + + return createAtaIdempotentIx; +} + +function providerAssert(provider: anchor.Provider) { if (provider.sendAndConfirm === undefined) { throw new Error('The client must be created with a full provider to use this method'); } @@ -820,196 +829,6 @@ export function returnedDataFromTransaction( return borsh.deserialize(schema, Buffer.from(data, 'base64')) as T; } -function transferNativeTokenBridgeAccounts(params: { - programId: PublicKey; - tokenBridgeProgramId: PublicKey; - wormholeProgramId: PublicKey; - mint: PublicKey; -}): { - tokenBridgeConfig: PublicKey; - tokenBridgeCustody: PublicKey; - tokenBridgeAuthoritySigner: PublicKey; - tokenBridgeCustodySigner: PublicKey; - tokenBridgeWrappedMeta: null; - wormholeBridge: PublicKey; - tokenBridgeEmitter: PublicKey; - tokenBridgeSequence: PublicKey; - wormholeFeeCollector: PublicKey; -} { - const { programId, tokenBridgeProgramId, wormholeProgramId, mint } = params; - - const { - tokenBridgeConfig, - tokenBridgeCustody, - tokenBridgeAuthoritySigner, - tokenBridgeCustodySigner, - wormholeBridge, - tokenBridgeEmitter, - tokenBridgeSequence, - wormholeFeeCollector, - } = getTransferNativeWithPayloadCpiAccounts( - programId, - tokenBridgeProgramId, - wormholeProgramId, - PublicKey.default, // we don't need payer - PublicKey.default, // we don't need message - PublicKey.default, // we don't need fromTokenAccount - mint, - ); - - return { - tokenBridgeConfig, - tokenBridgeCustody, - tokenBridgeAuthoritySigner, - tokenBridgeCustodySigner, - tokenBridgeWrappedMeta: null, - wormholeBridge, - tokenBridgeEmitter, - tokenBridgeSequence, - wormholeFeeCollector, - }; -} - -function transferWrappedTokenBridgeAccounts(params: { - programId: PublicKey; - tokenBridgeProgramId: PublicKey; - wormholeProgramId: PublicKey; - tokenChain: number; - tokenAddress: Buffer; -}): { - tokenBridgeConfig: PublicKey; - tokenBridgeCustody: null; - tokenBridgeAuthoritySigner: PublicKey; - tokenBridgeCustodySigner: null; - tokenBridgeWrappedMeta: PublicKey; - wormholeBridge: PublicKey; - tokenBridgeEmitter: PublicKey; - tokenBridgeSequence: PublicKey; - mint: PublicKey; - wormholeFeeCollector: PublicKey; -} { - const { programId, tokenBridgeProgramId, wormholeProgramId, tokenChain, tokenAddress } = params; - - const { - tokenBridgeConfig, - tokenBridgeAuthoritySigner, - wormholeBridge, - tokenBridgeEmitter, - tokenBridgeSequence, - tokenBridgeWrappedMeta, - tokenBridgeWrappedMint, - wormholeFeeCollector, - } = getTransferWrappedWithPayloadCpiAccounts( - programId, - tokenBridgeProgramId, - wormholeProgramId, - PublicKey.default, // we don't need payer - PublicKey.default, // we don't need message - PublicKey.default, // we don't need fromTokenAccount - tokenChain, - tokenAddress, - ); - - return { - tokenBridgeConfig, - tokenBridgeCustody: null, - tokenBridgeAuthoritySigner, - tokenBridgeCustodySigner: null, - tokenBridgeWrappedMeta, - wormholeBridge, - tokenBridgeEmitter, - tokenBridgeSequence, - mint: tokenBridgeWrappedMint, - wormholeFeeCollector, - }; -} - -function completeNativeTokenBridgeAccounts(params: { - tokenBridgeProgramId: PublicKey; - wormholeProgramId: PublicKey; - vaa: VaaMessage; -}): { - tokenBridgeConfig: PublicKey; - tokenBridgeClaim: PublicKey; - tokenBridgeForeignEndpoint: PublicKey; - tokenBridgeCustody: PublicKey; - tokenBridgeCustodySigner: PublicKey; - tokenBridgeMintAuthority: null; - tokenBridgeWrappedMeta: null; - mint: PublicKey; -} { - const { tokenBridgeProgramId, wormholeProgramId, vaa } = params; - - const { - tokenBridgeConfig, - tokenBridgeClaim, - tokenBridgeForeignEndpoint, - tokenBridgeCustody, - tokenBridgeCustodySigner, - mint, - } = getCompleteTransferNativeWithPayloadCpiAccounts( - tokenBridgeProgramId, - wormholeProgramId, - PublicKey.default, - vaa, - PublicKey.default, - ); - - return { - tokenBridgeConfig, - tokenBridgeClaim, - tokenBridgeForeignEndpoint, - tokenBridgeCustody, - tokenBridgeCustodySigner, - tokenBridgeMintAuthority: null, - tokenBridgeWrappedMeta: null, - mint, - }; -} - -function completeWrappedTokenBridgeAccounts(params: { - tokenBridgeProgramId: PublicKey; - wormholeProgramId: PublicKey; - vaa: VaaMessage; -}): { - tokenBridgeConfig: PublicKey; - tokenBridgeClaim: PublicKey; - tokenBridgeForeignEndpoint: PublicKey; - tokenBridgeCustody: null; - tokenBridgeCustodySigner: null; - tokenBridgeMintAuthority: PublicKey; - tokenBridgeWrappedMeta: PublicKey; - mint: PublicKey; -} { - const { tokenBridgeProgramId, wormholeProgramId, vaa } = params; - - const { - tokenBridgeConfig, - tokenBridgeClaim, - tokenBridgeForeignEndpoint, - tokenBridgeMintAuthority, - tokenBridgeWrappedMeta, - tokenBridgeWrappedMint, - } = getCompleteTransferWrappedWithPayloadCpiAccounts( - tokenBridgeProgramId, - wormholeProgramId, - PublicKey.default, - vaa, - PublicKey.default, - ); - - return { - tokenBridgeConfig, - tokenBridgeClaim, - tokenBridgeForeignEndpoint, - tokenBridgeCustody: null, - tokenBridgeCustodySigner: null, - tokenBridgeMintAuthority, - tokenBridgeWrappedMeta, - mint: tokenBridgeWrappedMint, - }; -} - /** * Detects the network from a Solana connection. */ @@ -1035,9 +854,9 @@ async function networkFromConnection(connection: Connection): Promise Result<()> { // Generate the id.rs file: let network = Network::deserialize("network.json")?; - let addresses = network.value()?; + let program_id = network.value()?; fs::write( "src/id.rs", - format!( - "anchor_lang::prelude::declare_id!({:?});\n\ - pub const WORMHOLE_MINT_AUTHORITY: anchor_lang::prelude::Pubkey =\n anchor_lang::pubkey!({:?});\n", - addresses.program_id, addresses.wormhole_mint_authority, - ), + format!("anchor_lang::prelude::declare_id!({:?});\n", program_id), ) .context("could not write the file: id.rs")?; @@ -34,19 +30,12 @@ fn main() -> Result<()> { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct Network { - #[cfg(any(feature = "mainnet", not(feature = "testnet")))] - mainnet: NetworkAddresses, + #[cfg(feature = "mainnet")] + mainnet: String, #[cfg(feature = "testnet")] testnet: NetworkAddresses, } -#[derive(Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -struct NetworkAddresses { - program_id: String, - wormhole_mint_authority: String, -} - impl Network { fn deserialize(path: &str) -> Result { let file = fs::read_to_string(path).context(format!( @@ -69,7 +58,7 @@ impl Network { } #[cfg(not(any(feature = "mainnet", feature = "testnet",)))] - fn value(&self) -> Result { + fn value(&self) -> Result { let file = fs::read_to_string(TEST_KEYPAIR_PATH).context(format!( "Failed to read the file with the test program keypair: {TEST_KEYPAIR_PATH}" ))?; @@ -80,9 +69,6 @@ impl Network { let keypair = ed25519_dalek::Keypair::from_bytes(&content) .context(format!("Invalid keypair in the file: {TEST_KEYPAIR_PATH}"))?; - Ok(NetworkAddresses { - program_id: bs58::encode(keypair.public.as_bytes()).into_string(), - ..self.mainnet.clone() - }) + Ok(bs58::encode(keypair.public.as_bytes()).into_string()) } } diff --git a/solana/programs/token-bridge-relayer/network.json b/solana/programs/token-bridge-relayer/network.json index cd7342b2..51d835c9 100644 --- a/solana/programs/token-bridge-relayer/network.json +++ b/solana/programs/token-bridge-relayer/network.json @@ -1,10 +1,4 @@ { - "mainnet": { - "programId": "AtTrxsPbTfBhC9uwwJGJbkFMux78t5EWTAXAbwUW8yC7", - "wormholeMintAuthority": "BCD75RNBHrJJpW4dXVagL5mPjzRLnVZq4YirJdjEYMV7" - }, - "testnet": { - "programId": "AttmP2dVtvNgHhMjMqKquMSnrNJC9HxH9rXrFqZ9EDHA", - "wormholeMintAuthority": "8P2wAnHr2t4pAVEyJftzz7k6wuCE7aP1VugNwehzCJJY" - } + "mainnet": "AtTrxsPbTfBhC9uwwJGJbkFMux78t5EWTAXAbwUW8yC7", + "testnet": "AttmP2dVtvNgHhMjMqKquMSnrNJC9HxH9rXrFqZ9EDHA" } diff --git a/solana/programs/token-bridge-relayer/src/processor/inbound.rs b/solana/programs/token-bridge-relayer/src/processor/inbound.rs index 7033dfc9..e1d6e24c 100644 --- a/solana/programs/token-bridge-relayer/src/processor/inbound.rs +++ b/solana/programs/token-bridge-relayer/src/processor/inbound.rs @@ -3,7 +3,6 @@ use crate::{ error::{TokenBridgeRelayerError, TokenBridgeRelayerResult}, message::{PostedRelayerMessage, RelayerMessage}, state::{PeerState, TbrConfigState}, - utils::create_native_check, }; use anchor_lang::{prelude::*, system_program}; use anchor_spl::token::{spl_token::native_mint, Mint, Token, TokenAccount}; @@ -177,7 +176,10 @@ pub fn complete_transfer(mut ctx: Context) -> Result<()> { } fn is_native(ctx: &Context) -> TokenBridgeRelayerResult { - let check_native = create_native_check(ctx.accounts.mint.mint_authority); + let check_native = ctx + .accounts + .tbr_config + .create_native_check(ctx.accounts.mint.mint_authority); match ( &ctx.accounts.token_bridge_mint_authority, diff --git a/solana/programs/token-bridge-relayer/src/processor/initialize.rs b/solana/programs/token-bridge-relayer/src/processor/initialize.rs index 9aae20eb..d222543a 100644 --- a/solana/programs/token-bridge-relayer/src/processor/initialize.rs +++ b/solana/programs/token-bridge-relayer/src/processor/initialize.rs @@ -86,12 +86,18 @@ pub fn initialize<'a, 'b, 'c, 'info>( )?; } + let (mint_authority, _) = Pubkey::find_program_address( + &[b"mint_signer"], + &wormhole_anchor_sdk::token_bridge::program::ID, + ); + ctx.accounts.tbr_config.set_inner(TbrConfigState { owner: ctx.accounts.owner.key(), pending_owner: None, fee_recipient, evm_transaction_size: 0, evm_transaction_gas: 0, + mint_authority, sender_bump: ctx.bumps.wormhole_sender, redeemer_bump: ctx.bumps.wormhole_redeemer, bump: ctx.bumps.tbr_config, diff --git a/solana/programs/token-bridge-relayer/src/processor/outbound.rs b/solana/programs/token-bridge-relayer/src/processor/outbound.rs index e0f3ce36..812fe4cd 100644 --- a/solana/programs/token-bridge-relayer/src/processor/outbound.rs +++ b/solana/programs/token-bridge-relayer/src/processor/outbound.rs @@ -3,7 +3,7 @@ use crate::{ error::{TokenBridgeRelayerError, TokenBridgeRelayerResult}, message::RelayerMessage, state::{ChainConfigState, SignerSequenceState, TbrConfigState}, - utils::{calculate_total_fee, create_native_check, normalize_token_amount}, + utils::{calculate_total_fee, normalize_token_amount}, }; use anchor_lang::prelude::*; use anchor_spl::token::{Mint, Token, TokenAccount}; @@ -286,7 +286,10 @@ pub fn transfer_tokens( } fn is_native(ctx: &Context) -> TokenBridgeRelayerResult { - let check_native = create_native_check(ctx.accounts.mint.mint_authority); + let check_native = ctx + .accounts + .tbr_config + .create_native_check(ctx.accounts.mint.mint_authority); match ( &ctx.accounts.token_bridge_wrapped_meta, diff --git a/solana/programs/token-bridge-relayer/src/state/config.rs b/solana/programs/token-bridge-relayer/src/state/config.rs index 085756bc..3b3d484f 100644 --- a/solana/programs/token-bridge-relayer/src/state/config.rs +++ b/solana/programs/token-bridge-relayer/src/state/config.rs @@ -14,6 +14,10 @@ pub struct TbrConfigState { pub evm_transaction_gas: u64, pub evm_transaction_size: u64, + /// The mint authority used by the Token Bridge. Used to check whether a transfer is native + /// or wrapped. + pub mint_authority: Pubkey, + pub sender_bump: u8, pub redeemer_bump: u8, pub bump: u8, diff --git a/solana/programs/token-bridge-relayer/src/utils.rs b/solana/programs/token-bridge-relayer/src/utils.rs index d7a29de0..4e6d8073 100644 --- a/solana/programs/token-bridge-relayer/src/utils.rs +++ b/solana/programs/token-bridge-relayer/src/utils.rs @@ -90,21 +90,24 @@ fn check_prices_are_set(evm_prices: &EvmPricesState) -> Result<()> { Ok(()) } -/// Creates a closure allowing to verify that a native/wrapped transfer is indeed -/// what it pretends to be. -pub fn create_native_check( - mint_authority: COption, -) -> impl Fn(bool) -> TokenBridgeRelayerResult { - let is_wormhole_mint = mint_authority == COption::Some(crate::id::WORMHOLE_MINT_AUTHORITY); +impl TbrConfigState { + /// Creates a closure allowing to verify that a native/wrapped transfer is indeed + /// what it pretends to be. + pub(crate) fn create_native_check( + &self, + mint_authority: COption, + ) -> impl Fn(bool) -> TokenBridgeRelayerResult { + let is_wormhole_mint = mint_authority == COption::Some(self.mint_authority); - return move |expected_native| { - // Valid values: either: - // - The transfer is native and the mint is not the Wormhole one; - // - Or the transfer is wrapped and the mint is the Wormhole one. - (expected_native != is_wormhole_mint) - .then_some(expected_native) - .ok_or(TokenBridgeRelayerError::WrongMintAuthority) - }; + return move |expected_native| { + // Valid values: either: + // - The transfer is native and the mint is not the Wormhole one; + // - Or the transfer is wrapped and the mint is the Wormhole one. + (expected_native != is_wormhole_mint) + .then_some(expected_native) + .ok_or(TokenBridgeRelayerError::WrongMintAuthority) + }; + } } /// The Token Bridge uses 8 decimals. If we want to transfer a token whose mint has more decimals, diff --git a/solana/tests/token-bridge-relayer-tests.ts b/solana/tests/token-bridge-relayer-tests.ts index cc3c11d6..45ec405f 100644 --- a/solana/tests/token-bridge-relayer-tests.ts +++ b/solana/tests/token-bridge-relayer-tests.ts @@ -1,4 +1,4 @@ -import { chainToChainId, encoding } from '@wormhole-foundation/sdk-base'; +import { chainToChainId } from '@wormhole-foundation/sdk-base'; import { UniversalAddress } from '@wormhole-foundation/sdk-definitions'; import { Keypair, PublicKey, SendTransactionError, Transaction } from '@solana/web3.js'; import { NATIVE_MINT } from '@solana/spl-token'; @@ -21,8 +21,6 @@ const authorityKeypair = './target/deploy/token_bridge_relayer-keypair.json'; const $ = new TestsHelper(); -//TODO put the setup in its own object. - describe('Token Bridge Relayer Program', () => { const oracleClient = new SolanaPriceOracle($.connection, $.pubkey.from(oracleKeypair)); const clients = (['owner', 'owner', 'admin', 'admin', 'admin', 'regular'] as const).map( @@ -47,16 +45,17 @@ describe('Token Bridge Relayer Program', () => { const evmTransactionGas = 321_000n; const evmTransactionSize = 654_000n; - const ethereumPeer1 = new UniversalAddress( + const ethereumTokenBridge = new UniversalAddress( Buffer.from('e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1', 'hex'), ); - const ethereumPeer2 = new UniversalAddress( - Buffer.from('e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2', 'hex'), - ); - const oasisPeer = new UniversalAddress( + const oasisTokenBridge = new UniversalAddress( Buffer.from('0A51533333333333333333333333333333333333333333333333333333333333', 'hex'), ); + const ethereumTbrPeer1 = new UniversalAddress('0x' + '00'.repeat(12) + 'ab'.repeat(20)); + const ethereumTbrPeer2 = new UniversalAddress('0x' + '00'.repeat(12) + 'ca'.repeat(20)); + const oasisTbrPeer = new UniversalAddress('0x' + '00'.repeat(12) + '05'.repeat(20)); + before(async () => { await $.airdrop([ wormholeCoreOwner, @@ -105,9 +104,8 @@ describe('Token Bridge Relayer Program', () => { // =================== await Promise.all([wormholeCoreClient.initialize(), tokenBridgeClient.initialize()]); await Promise.all([ - tokenBridgeClient.registerPeer(ETHEREUM, ethereumPeer1), - tokenBridgeClient.registerPeer(ETHEREUM, ethereumPeer2), - tokenBridgeClient.registerPeer(OASIS, oasisPeer), + tokenBridgeClient.registerPeer(ETHEREUM, ethereumTokenBridge), + tokenBridgeClient.registerPeer(OASIS, oasisTokenBridge), ]); }); @@ -244,59 +242,63 @@ describe('Token Bridge Relayer Program', () => { describe('Peers', () => { it('Registers peers', async () => { - await newOwnerClient.registerPeer(ETHEREUM, ethereumPeer1); + await newOwnerClient.registerPeer(ETHEREUM, ethereumTbrPeer1); assert.chainConfig(await unauthorizedClient.account.chainConfig(ETHEREUM).fetch()).equal({ chainId: ETHEREUM_ID, - canonicalPeer: uaToArray(ethereumPeer1), + canonicalPeer: uaToArray(ethereumTbrPeer1), maxGasDropoffMicroToken: 0, pausedOutboundTransfers: true, relayerFeeMicroUsd: 0, }); - expect(await unauthorizedClient.account.peer(ETHEREUM, ethereumPeer1).fetch()).deep.include({ + expect( + await unauthorizedClient.account.peer(ETHEREUM, ethereumTbrPeer1).fetch(), + ).deep.include({ chainId: ETHEREUM_ID, - address: uaToArray(ethereumPeer1), + address: uaToArray(ethereumTbrPeer1), }); - await adminClient1.registerPeer(ETHEREUM, ethereumPeer2); + await adminClient1.registerPeer(ETHEREUM, ethereumTbrPeer2); assert.chainConfig(await unauthorizedClient.account.chainConfig(ETHEREUM).fetch()).equal({ chainId: ETHEREUM_ID, - canonicalPeer: uaToArray(ethereumPeer1), + canonicalPeer: uaToArray(ethereumTbrPeer1), maxGasDropoffMicroToken: 0, pausedOutboundTransfers: true, relayerFeeMicroUsd: 0, }); - expect(await unauthorizedClient.account.peer(ETHEREUM, ethereumPeer2).fetch()).deep.include({ + expect( + await unauthorizedClient.account.peer(ETHEREUM, ethereumTbrPeer2).fetch(), + ).deep.include({ chainId: ETHEREUM_ID, - address: uaToArray(ethereumPeer2), + address: uaToArray(ethereumTbrPeer2), }); - await adminClient1.registerPeer(OASIS, oasisPeer); + await adminClient1.registerPeer(OASIS, oasisTbrPeer); assert.chainConfig(await unauthorizedClient.account.chainConfig(OASIS).fetch()).equal({ chainId: OASIS_ID, - canonicalPeer: uaToArray(oasisPeer), + canonicalPeer: uaToArray(oasisTbrPeer), maxGasDropoffMicroToken: 0, pausedOutboundTransfers: true, relayerFeeMicroUsd: 0, }); assert.chainConfig(await unauthorizedClient.account.chainConfig(ETHEREUM).fetch()).equal({ chainId: ETHEREUM_ID, - canonicalPeer: uaToArray(ethereumPeer1), + canonicalPeer: uaToArray(ethereumTbrPeer1), maxGasDropoffMicroToken: 0, pausedOutboundTransfers: true, relayerFeeMicroUsd: 0, }); - expect(await unauthorizedClient.account.peer(OASIS, oasisPeer).fetch()).deep.include({ + expect(await unauthorizedClient.account.peer(OASIS, oasisTbrPeer).fetch()).deep.include({ chainId: OASIS_ID, - address: uaToArray(oasisPeer), + address: uaToArray(oasisTbrPeer), }); }); it('Updates the canonical peer to another one', async () => { - await newOwnerClient.updateCanonicalPeer(ETHEREUM, ethereumPeer2); + await newOwnerClient.updateCanonicalPeer(ETHEREUM, ethereumTbrPeer2); assert.chainConfig(await unauthorizedClient.account.chainConfig(ETHEREUM).fetch()).equal({ chainId: ETHEREUM_ID, - canonicalPeer: uaToArray(ethereumPeer2), + canonicalPeer: uaToArray(ethereumTbrPeer2), maxGasDropoffMicroToken: 0, pausedOutboundTransfers: true, relayerFeeMicroUsd: 0, @@ -305,13 +307,13 @@ describe('Token Bridge Relayer Program', () => { it('Does not let update a peer from another chain as canonical', async () => { await assert - .promise(newOwnerClient.updateCanonicalPeer(ETHEREUM, oasisPeer)) + .promise(newOwnerClient.updateCanonicalPeer(ETHEREUM, oasisTbrPeer)) .fails(SendTransactionError); }); it('Cannot register an existing peer', async () => { await assert - .promise(newOwnerClient.updateCanonicalPeer(OASIS, oasisPeer)) + .promise(newOwnerClient.updateCanonicalPeer(OASIS, oasisTbrPeer)) .fails(SendTransactionError); }); @@ -328,7 +330,7 @@ describe('Token Bridge Relayer Program', () => { // Admin cannot make another peer canonical: await assert - .promise(adminClient1.updateCanonicalPeer(ETHEREUM, ethereumPeer1)) + .promise(adminClient1.updateCanonicalPeer(ETHEREUM, ethereumTbrPeer1)) .failsWith('Signature verification failed'); }); }); @@ -345,7 +347,7 @@ describe('Token Bridge Relayer Program', () => { assert.chainConfig(await unauthorizedClient.account.chainConfig(ETHEREUM).fetch()).equal({ chainId: ETHEREUM_ID, - canonicalPeer: uaToArray(ethereumPeer2), + canonicalPeer: uaToArray(ethereumTbrPeer2), maxGasDropoffMicroToken, pausedOutboundTransfers: false, relayerFeeMicroUsd, @@ -400,6 +402,21 @@ describe('Token Bridge Relayer Program', () => { }); describe('Running transfers', () => { + const ethereumTokenAddressFoo = new UniversalAddress('0x' + '00'.repeat(12) + '11'.repeat(20)); + const recipientForeignToken = $.provider.generate(); + let recipientTokenAccountForeignToken = PublicKey.default; // Will be initialized down the line + let clientForeignToken: TbrWrapper = null as any; + + before(async () => { + [clientForeignToken] = await Promise.all([ + TbrWrapper.create(recipientForeignToken, DEBUG), + tokenBridgeClient.attestToken(ethereumTokenBridge, ETHEREUM, ethereumTokenAddressFoo, { + decimals: 12, + }), + $.airdrop(recipientForeignToken), + ]); + }); + it('Transfers SOL to another chain', async () => { const tokenAccount = await $.wrapSol(unauthorizedClient.provider, 1_000_000); const gasDropoffAmount = 5; @@ -409,10 +426,9 @@ describe('Token Bridge Relayer Program', () => { const foreignAddress = new UniversalAddress(Keypair.generate().publicKey.toBuffer()); const canonicalEthereum = await unauthorizedClient.read.canonicalPeer(ETHEREUM); - await unauthorizedClient.transferNativeTokens({ + await unauthorizedClient.transferTokens({ recipient: { address: foreignAddress, chain: ETHEREUM }, - mint: NATIVE_MINT, - tokenAccount: tokenAccount.publicKey, + userTokenAccount: tokenAccount.publicKey, transferredAmount, gasDropoffAmount, maxFeeLamports: 100_000_000n, // 0.1SOL max @@ -436,7 +452,7 @@ describe('Token Bridge Relayer Program', () => { expect(vaa.payload.token.amount).equal(transferredAmount / 10n); expect(vaa.payload.payload.recipient).deep.equal(foreignAddress); - // We need to divide by 1 million because it's deserialize as the token, not µToken: + // We need to divide by 1 million because it's deserialized as the token, not µToken: expect(vaa.payload.payload.gasDropoff).equal(gasDropoffAmount / 1_000_000); expect(vaa.payload.payload.unwrapIntent).equal(unwrapIntent); }); @@ -452,10 +468,9 @@ describe('Token Bridge Relayer Program', () => { const foreignAddress = new UniversalAddress(Keypair.generate().publicKey.toBuffer()); const canonicalEthereum = await unauthorizedClient.read.canonicalPeer(ETHEREUM); - await unauthorizedClient.transferNativeTokens({ + await unauthorizedClient.transferTokens({ recipient: { address: foreignAddress, chain: ETHEREUM }, - mint: mint.address, - tokenAccount: tokenAccount.publicKey, + userTokenAccount: tokenAccount.publicKey, transferredAmount, gasDropoffAmount, maxFeeLamports: 100_000_000n, // 0.1SOL max @@ -485,21 +500,111 @@ describe('Token Bridge Relayer Program', () => { it('Gets wrapped SOL back from another chain', async () => { const [payer, recipient] = await $.airdrop([Keypair.generate(), $.provider.generate()]); - const associatedTokenAccount = await $.createAssociatedTokenAccount(recipient, NATIVE_MINT); + // Associated token account already existing (to test if it breaks the transfer completion): + const recipientTokenAccount = await $.createAssociatedTokenAccount(recipient, NATIVE_MINT); + const amount = 123n; const vaaAddress = await wormholeCoreClient.postVaa( payer, - { chain: ETHEREUM, address: ethereumPeer1 }, // The token originally comes from Solana's native mint - NATIVE_MINT, + { amount, chain: 'Solana', address: new UniversalAddress(NATIVE_MINT.toBuffer()) }, + { chain: ETHEREUM, tokenBridge: ethereumTokenBridge, tbrPeer: ethereumTbrPeer1 }, { recipient: new UniversalAddress(recipient.publicKey.toBuffer()), gasDropoff: 0, //TODO increase the dropoff + unwrapIntent: false, //TODO test with true }, ); const vaa = await wormholeCoreClient.parseVaa(vaaAddress); - await unauthorizedClient.completeNativeTransfer(vaa, associatedTokenAccount); + await unauthorizedClient.completeTransfer(vaa); + + const balance = + await unauthorizedClient.provider.connection.getTokenAccountBalance(recipientTokenAccount); + // The wrapped token has 8 decimals, but the native one has 9. We must multiply the amount by 10: + expect(balance.value.amount).equal((amount * 10n).toString()); + }); + + //TODO get the other token back + + it('Gets a foreign token from another chain', async () => { + const payer = await $.airdrop(Keypair.generate()); + // Do not create the associated token account, it should be done by the SDK. + + const amount = 654_000_000n; + + const vaaAddress = await wormholeCoreClient.postVaa( + payer, + // Token: + { amount, chain: ETHEREUM, address: ethereumTokenAddressFoo }, + // Source: + { chain: ETHEREUM, tokenBridge: ethereumTokenBridge, tbrPeer: ethereumTbrPeer1 }, + // TBRv3 message: + { + recipient: new UniversalAddress(recipientForeignToken.publicKey.toBuffer()), + gasDropoff: 0, //TODO increase the dropoff + unwrapIntent: false, + }, + ); + + const vaa = await wormholeCoreClient.parseVaa(vaaAddress); + + const { wallet, associatedTokenAccount } = + unauthorizedClient.client.getRecipientAccountsFromVaa(vaa); + recipientTokenAccountForeignToken = associatedTokenAccount; + expect(wallet).deep.equal(recipientForeignToken.publicKey); + + await unauthorizedClient.completeTransfer(vaa); + + const balance = + await unauthorizedClient.provider.connection.getTokenAccountBalance(associatedTokenAccount); + expect(balance.value.amount).equal(amount.toString()); + }); + + it('Sends a foreign token back to its original chain', async () => { + const gasDropoffAmount = 0; + const unwrapIntent = false; // Does not matter anyway + const transferredAmount = 301_000_000n; + + const foreignAddress = new UniversalAddress(Keypair.generate().publicKey.toBuffer()); + const canonicalEthereum = await unauthorizedClient.read.canonicalPeer(ETHEREUM); + + await clientForeignToken.transferTokens({ + recipient: { address: foreignAddress, chain: ETHEREUM }, + userTokenAccount: recipientTokenAccountForeignToken, + transferredAmount, + gasDropoffAmount, + maxFeeLamports: 100_000_000n, // 0.1SOL max + unwrapIntent, + }); + + const sequence = 0n; + const vaa = toVaaWithTbrV3Message( + await wormholeCoreClient.parseVaa( + unauthorizedClient.account.wormholeMessage(clientForeignToken.publicKey, sequence) + .address, + ), + ); + + //expect(vaa.sequence).equal(sequence); + expect(vaa.emitterChain).equal('Solana'); + expect(vaa.emitterAddress.address).deep.equal(tokenBridgeEmitter().toBytes()); + + expect(vaa.payload.to).deep.equal({ address: canonicalEthereum, chain: ETHEREUM }); + + console.log('vaa amount:', vaa.payload.token.amount); + expect(vaa.payload.token).deep.equal({ + // It is a wrapped token, so it has the expected number of decimals. No shenanigans, then: + amount: transferredAmount, + chain: ETHEREUM, + address: ethereumTokenAddressFoo, + }); + + expect(vaa.payload.payload.recipient).deep.equal(foreignAddress); + }); + + after(async () => { + await clientForeignToken.close(); }); }); }); diff --git a/solana/tests/token_metadata.so b/solana/tests/token_metadata.so new file mode 100755 index 0000000000000000000000000000000000000000..00467cb25321da305b1ba7ff2c0283fbc68a3814 GIT binary patch literal 681120 zcmdqK3xHNtwFkbxZ#V*)4T1-8l*UTf`r_SxTC&VS_v#X`Z^)8PK>0Bt`%;;b23uyE1Q`j>Mv+%z|U{vGTFab47G zRuuVk!E*3)%;s!f7JaT%v>|gO!d*8YFH6*GL=w%=Dy}{6^Au{#4Ri zp5U~jLlAYe9b|YQF&>!EKj<7tbRDFpRQxrUClwiAdXBVvmn5z_RsN_>&{Vp2BUcDN zN8KU$XATs+)w0%Gt?4b2-bUm<9aFzV1ouz%x&MJrbN|-g$QG(ZkMR$5w-9_L(TjG~ z(^2YNyHsGa)_f8r3Sw_%sS;{{G83cl(pE_M@?p2pzcZXit=P!_r0*7Tb;Dg70pCtW4@sufA0Pur<~;QbQy>*vkfLHkjo^;9B|o}}ai zzC`@bs4<-F(ka3Zn-AUWRg2WCo~kM4H$CXDGABx;zI1ep|~)d`5zZfAb$*agafDI!inw}PMh5GI6cWh6H0}XS#F?%NFbai(Z6H| z>Hp|Iax!56gO13ZK>tcPI1o88xF`o*krTxR4EiE>9Q`Zx;cp|?=wZ+sF%?oSdIKs2 z2K^C_LST#|;!z5Wni2gE5BkHnBIc677+=Jr5*R&u`Sjqwl2tMfp=SnFeI#RWJwT9g?Da0--9gbjY7j`(Nh#45n(eEvw%eXL<)b3GFW& z@3|x(Z~uo=2lONr!Iut~d6HBFU#j%?cYMo<&lo~TPfz3f?lVVre7_m4zQBRBxcDNr zOmT6FBlp7d@q=gRq#$?)@x7JO$cf>{d|f`dwj3%OxmVM_(*AQndF0OZ`QNL_7exoY zKi1?+z>56mPpR!^6k&?oIUe8A)3fr(z0~KwwI+YM&)*f~V_b-5BPRv~{rx5=kKBuW zyOBA5z!FC6-DSLI1m%$vLj-)Yf_(IM3b(_*(yl#M4v!JJalZWaTsbgD9NjxtJ`P?* z?A7g*R;fQcnZGQ!FA*HMk(>{Dt`7JNdc=>N;LG2hD@RN7TWJ6OTsb_T`7M<1$;qpk z%b}ez-iHGIqJOLm&2ORold_aIwn$pfKfS)C zK=q_u<6TlORX=5Xt*T$zfkD$_{xEG4KMDD)Tb<3v;Ff-mV*Ki{JLbgG}%_&Dj*_(J6qu{9^T ziWzhRiSEgSH$AsLU7!P^Y18%=?}z%oPxXHR;WEDL0I~E`)jvUBnhAzJBaMr1E>J@0 zO7|8{SE;{Rt^Vp3^;fsjoI<_Z)L-4M{^}0(S8LQ?-KqX+t@^9Cs=s=h`m49AzxogL zSMN}NwNCxjUFxsyR)6(Q_E&N79*(r*;=7m+aq(V`#N*<-dFLlCuIETSF5buXCN6H^ zNIovUhwXn{yq_&(Tzs!1^FZTxpM#%?iw`ItHnPQyixrMP;$o+RO2x%ZZY-yp*<#1V z2iaK#_yPAKhCj%jCoXPdxyQxr z4oj%G_#t;Hryq8warzMl6^V-AGS)4xV;0fd6f4YC= z^b_tSoPLr$XI%W0JDcH8yO(iVbu&2K#U3{Pw?$@!mi=W+U&JD=14au;y= z-|pp{e%{UEbho>Z(=WK$oPN>0g3~X#S91Dg_bN`m!VyGV{2w=m;jg;6obGWKar!lm zK;q)p-6afv!(GbhH)$<_aed2O#_9jMd7OUR&F6Hldkv@m=Pu{;JMOide%HN@)5qNv zoPN)}p40EU1)M(N7IM1Jl{x)^Tg2%PT^pxAa#wQtq+87CkKG$M{fS$`=}+BNoId5Q z=JaRo8cu)iuI2O>?mA9?>8|JWSMH6R{@UHZ>2KUpPWQWZPJio`ar!$4*AW+g?{4Ju z5AG&T|LESt>7U$kPXFvyaC*SOWyHm&-OZf-#ig7+l6JZA;_q2z}R1o}U4>vIUM-P*^q4M8*7dc55wX{?z*41deR$20s55629D&BL(Uk^8ELPhj{f9)<^rIEoYgpUChRe7VF=pZD-d zT>f7kmiYE_9+r6SGaiP$i(J*iqZs~_hnpGxgoj%ge$>OG8HT0^ePbB@n1^8>Bli&x zk7M{l9)`V)+%^wSVE6+bp2+aS9zKQPtsaIwjocOwzkuNfJq-IAxlJB!Ww_#D*xSfG z;NcfD{9X@FX81iGp2F~b9)>-R+`Bz|8pH4MFzj>W-s#~p7{1HHu-B1$hlkH(`0XBs z{f^vQJ$x3!cX}B1JaTt<_+J^m&BL(ok-NpiFJ*X@hhgs{_U+!00(*Zz?eSS;XE};MXz?E(;9*7O}S$ z7$dXw^A7qO{w`u~FXiBbt)o%CkYQ+*l!J2-M-u|WAKLmE_1hSRmPrMD z(Tu?Gm$uGE`y~uxmP$GNXT;Hxz>s{z(UidOrx8b60$BQ{@?R@ z;4cSJ9v5e@$B2vPxYZ1w%N8as&UCjje4e|F)AQZ!oL;~dDlWd!f!w~^t6uEJ^A zb#l6hJxp9|bDJ5y(mlxOV)qcIZ(t7<7nis$hOcs4IlbDwpVMpH!<=609^v#l_W@3? zcOT^RjqD-g;tg&)!%N+VIBjnn^?ASak<;c@Cx@R zr-}PdPH%Rf;52og5+ z#lz4u^8X%&zD4f69)@R*+{fpdv9)=#0|MxKTF>>$nFg!o`e-A@HBX^gFG55&- zdl>o}xwm`x-3-6g!_eQz-RWWIG5LQFL!Tpen}?y- z@B<8|9^S}s;$h5#$SwCU=0oIe^6(~xJ3Nf}5xI5`W1f)z_b}#5F> zuJ$nI5&3@)V?IT0v4=6Q$p3p7^DA;?4`ZH1Zh?m}-y(N~hqp2OS`TCXMea2o#ypJN zJP%_&My}-Hk1%|RhcQ3N|9cqol>EPkF<;65dl++${J)1Wf64!Q81tC?zlSlOBX_=s zF|W!0dl>VZ{J)1W&&mIL81tR{zlV1byrdxeLCZCb3uhffeq1=leUa1WxzBQXtot0N z&v%b;+UWj^(;@D^IX%vOp3|XjH>bng7bq>!d)m)Xim@V%T&wRN_{sl!82ym{_b~b+ z|L@M*rmhJq$X?|9cqpkpK5E=%W2{4}(7P{~rDl!zVIq`=Ryr*^Z-nWqog0 zDnz_5m2A@Yhr0h~`6&HTRYyCy@WJULGA*}iT%N%{}RxnK7aXYM&NcwY3U z?sz~aMg$v)$LK`rymg!o`4Rt+w~oKpF(H`-6A>zjIHk$Ixvs$*!$$hRRWK?Q_}Qam2XZ@s>;6+ zMCZY?GQ4;%ek|}osp+{|)3|UM!#HPwo^iecL&UiXYC`ErvOiq-7t(FOEwVpc7$f_` zh4FeGW1{R27beO6aG_Q9hYOSSJjQ9VKU|n9`@@B^WPiBu64@UvoGtsqg>!g+81!HA zuNnQe-a-EJric0Ci7MhF<&Uh%ALH|16y$>sm?E~`LAx2ba>&HiJ1C!@D?f?LU+nRB z1m%%y_W8F5`RMog+-{;Tzb7cSb0+9-V@-aGufHwG2fmo`{fjT3H8tZ4%Ap|<@0JUH z9?P|d3W!yc@^9tJp;{4hOUi$dD}NrBV^mW9=Un+PEO}Yk6dY5oGM=2IGL;TN=QiXVG!lIiUY9@^kvg!ao&1r-l4ffAG($myrK0 z|77|&B;+5A7CruaE`=tM!b5Twrlo zT4-k2-qWWo47&6pL=R-w&jA-Q{$=w~>^JP^${-%J^Af0sCp(v+{(dU;~^ON@aOqMK>uAw75uq# zz)urguin$tudOe^FK8C(|B3SLT)LGb|M!$%4-c^@sft&)NyL!O$x_;tmCRx-Q*Cl)@?cYq!7yH+no_&J)xA7!nF>1uSm{qhtNXCND987iHVBb$+{f_xUM~9=_ztza zY%%w{cu8_y^?1Rv?Q}`g*}J5Elg7Nyqi?qw{hd|%j zC|*e_TJJ&Wzj}$(ZycuWE|PRPG50k3a|!YPSW4%_lD|#)QVr^1C(zanOXzQ~W)Ag- zC)q6V`x4^cG{E}3&>S6S(kc9~{j6tc>2y%_$EnB zZyJ>zi>H@UKQEvvz=tm8Owo3iYCGZ+9{au=^_jmHNd6KUC-Zlzz@CmV0;7I2{b}c* z=jc4#Lh}!bR8_e&q7prgx&F4bNxpqou$=m1J}MsIWj-#_dY4Ixdd>7F^abf08@I-v zD6ipTnJUqn<(frt4qPGI!M{HW7k4WIM<%}{)lG}Hf~pGfBz zKa5W^D3hK>8aJM8+HW-&H}gm32RzK5E2VzO$7ag%dS-m?FMq&9dKzimc(!T3hte<4 z=kui<^qToNQ((}=eC$F0O+J`MV9gwrQ&sul=MnL#8Ra@Z?R$-F%1`@_66f<-K2(?I z0UFV>&Bmwp!R!L;Q8SfS&qb|3zlRFEty$91R4!-p8hw%lp}IU#sr>R~=7P z?G@-4et`D3)W7=gcgE{DRX=?HleK>^jUJ<;TffSeu%DCr(~siE`2jqT+kfQb20H-y zhX(R8dt>yL$8vMtS1ld_&)@(L;!&t9@NXb}s)vuN^h(#kFkaN}tkNljkRCgiVEdr9 z|7Ch*{n>k-zVP9bD?ffu05}hA*t|DpCdiq^? z_?`@U3wb|UN8X61_viL$Lq6x}j2j>&SzpYh6!Kgc$TMkard+av`W;`Q->um`az?kr zqZ!>2zlU_gZuds_kXtjm13I1x`K|siT$f?$dHxMm`!neP+vjAc=#iaINroyv^}VQH zXV`xCCLSi==hI(0zY)?oM)24l!t=h6Kjt6R{w5tF-;%ypf&WIRgZCCshVz8@ z-K2av198Jq($4&S#Jj&_f7EZbPo&E9Wmnhj;S-Vm+$s@e^d+Nk!r_8^7_pgYJgHV|?)E)hYW>^7|`EATPf>AKMXp z)6>x;dmSNvX!!iy_r=d<^QABK=l8SzD}(y}-Y?VJ6~Z5kA9kR1l@uhrBT}n(e1FK@ zJRE;c%-$bCzUy*w_&ymL(?i^Q1N@;q#`t@)$9n=lQV;*>f!zcBhmzOl>&VN_0oi#p zvzzwbGL#p`^ZgkAM|k|Acm6x-+&nZry>>Xblj-5L`T3Zie}UY{6dab^&<=d<%E@g@ zPHw38T;!I=|JUbbadlthwyqCyE9*S#kKB41|C{?Ex3=NHlF6;DA9BOYJDmB3c97ey zoZOb?`gpq0OKhQlJ&OTFV%4`uw(!KWKIpXa{I0f_ImyaS)6Qfd1xoM5WN9pr|@32$y9Fqvwb;q zucWfSH~IN}juz<``|!>5=Wj_+n^!(RoF82FR z$ykAHe;o7>tFv|bR4KRf);2#wKCzu$B=w9A;F(7AE7>CJ`ef^Vp3Amxn;3uEq#f$F zlJQTkqCe-BX&O)=>5ze0z0OBIuu=J5?v4h+TTLy--E6{hIEaWaohVwyXVT@ zSJ=JD#xJv%pl=E2({U%uz$N07@r@=Ca?+b~eCyoYxvA&0-9UzNiI;s^9BPlv^QuxluX{MVn5-51xRe8x~^dTgK8?t2@h_O^8< z34!sN0(vb}E;82%6s=qHoll8s{L zWA-B1_{^?^_VfM3JhVfc=i@-tqtZYU2tC<*N%5EH~h|W66M)Fz7`MU)ALj181mz_LA`qX0B1vfe2yNBk&=db{1{DTec=c2hWr@R zM}Fkn9n%9pxZY||D)V4YFwWliaaK?-&kuWl(s+*GHGQ@9xvdXu{>>xx1RbRTGI5g| zMX#%~r2P^CQH~f3kHvB0C`N>4@1Sx{X^g0WYLA8dUnT8~4&&zpnkS>FerW*xK@ao; zkFC#5-z<)pK%Jnzoolo4`15Xduk=lf#?f(Cjx$1i-2;2ES<49@;tc5FhbY1{Jx@Ay zUv$$Rjyo-GPVW%Dq-x)=epQlzaHZ|rq&a(V@ip+3WA|3H1hk%k&NU@?Qkoc?WxsfczPhZ+6T4u-yyP8@@Ywz&G_U z@lE}2(r3L-HKgx_AwG*w4>4|x3;SmE@Qpr9d{_5?Z^U8ZyKaB|@jgGqr+y`!KJBlN zbUJA=r=eUn(gs@i9(gjIi^BMC-b(voTQ(w0Vm~vT(sI{khLf#boQ8Pj;yhI+YEx`O zc49l>MI1Pn34biasT{d|Cn(PJ3{0B3(cS-G46B! zLF4`d9WXf(^8W4-8uxqtL3;kJBQ)+$9-(oc@bQ0)o_{ox=~azn2AaxBmP-kc0#5?<;iP)W+lYKV*5}&(+%dQCoM# z10?{mb)UVTgg++M`rlQs{rn^rJ+|*&cF=l?s=C&(_?P@O!FUemckedQ+7a?3@$&E9 zVJjZ*NdVjaX{q5iy#5OP?OrMm#h_!|e!-xJr&wKiV zPhPXJJb2wUUE-$jyawiFI(_1P;KzQ~wBUCQ)9I6a{)0ihnob|#^GDRwAHw-|PZ;9Y zV+e2BBf=4vh9Dn&_yV01F**8keL8Ov{T;ke z-wcL;ZUj@io0h_6a5&CHrjYHF6%%-%n=0SEc(C>FnLC2l0U8iAw09)E{{l zqIrTJl=mWyN@%i@vq|3|4JyB#27>esXceWaWB|!6%?zh!Xc|j?dXv;k7fZWzoZv}y z{^IxbCR0o3DW}9Y5-+U}A`5~;vcOC z?W=^r?WzYIss|mhjXIulE}1hZ8ys~gD{Tcb0KAE0Xm3Owxj z*!ztqD zUm0EfX?Hux+1i=i3GK@+DhFLlsNZDEF0sP{)edhJdzN&GJBJ^22 zXZy^ye-ZJo($KinE_XDo;r`M|DGGnz`M6F2{R;n)~)_zrI41=s_IXMe<4YyOVUS z$RS-T{0-%C71ayptI2gR-QyK=^g#1-lj0?!)=_=*f4KMO-5k7cvG3Va|2w$Kho<#B zPJh2&(>jJjIzb2c5)*0Ap9AzF*}jj=j}PTPrSk%Q|4H9Bntr#7+Pd~JGEVz`Y0miq z&m+4D0xQhlR0#PjwK26|@nsGm;mPdY*$=C0Ro@dBUCTm15ftT3MyhX#-5t84ww(oJmFQ1`1RyB??zQluMAyIGK zzou~`?)pCUi*@5@>Nlq72>l<@ZyTTL$PMgvFMlQkbW3sr~pa1i6JT!f`^^v}nJDl5w@@ru~`9)h}J@TtR^vz@X z4z2$eQ$6TcPx9MD^cdfJ@;iCCwd=Xu65TI_C-fKRQGa=Sz0HTSmA-I(M(&SnXY+B5 z#V4f(vD1%h;i5g_ixs)`?kl56`W=g#?EF9YeCW=!{t`=vZ{cV99?Yv&O@)+xh|E^Bd z!nNi0lX})8aG&TTA`cbcqW_40o%G1)%T4{#m*;yr!Pkg?ot5}s;cSlc^ZK%y{B5Q$ z1(t{XURbC%h)3GG>5atKM_O!X)~p21mt%!`P3(*k``Idoj}7O{VUe6hZ^ z^G@~b&kI`@2#jdDfS z*QLT}##8RPpZhHh8pkl!d+1_^Of!F;kbd`w%e(8Btj9vVehc+$`w!vwflSY4M#J9+ zV%>*+&|iP_tTU%)D>2Shf;Y6kcl4lVzdMl0HI-qJT*WUYm5s~~<3oCi;5B}m9@4oZ zsYk@G0R5|_UTbg;kn<~&k9GZg9WQdib?ASP{Y-TK$i8n*D)U7j?DrC`k#%ZytKu6h z`#d4v?_<8t<#xu8CDd;-HCe7{TzDZP2;1fLONKZu12oV)+FyGlZS-g{=BH}=Up*WJf}DJyGU$dP-(zP!PFW z6_4^q+ehvZ&hObjW_S+eA1jy#-R>s=Hv{`NkwMq7_6F%UkO%8?FY(noX#c?UIO$v{ znZTD%6@zokbQ~wU;XjgcwgFe*B^Mof5v#fy^nfx zSr2;inLpS1A0Fo+;{9Z^_}89(e=y=(AZR{V{IB@3_@5>WJ$=!qPvz)?-u^xIA>Q9F zJJQ57A8L3$t})GlpMdpLkq3`~J_g4>7indN#NRJ-acGPMc7e_PZi{jqytB(4P=c+8*MJz3$pAn)hiyA^t4er^)C_wlX%UT8XXvgGT1 zZs}Bw$2LiPoK9`la-GLhM=+e~xiOttFtt^fJ4@Xc{xLd;Il4H-4`{{7S|fDphZv z}-n5_jaqaZ&BFD|b*K2h?r{i@V>-ntog3LZ*1d)?< z?~(A!Ukt|CU2iXBh8+pL-9hWCq;e>|?f!m1#(O5y5!%zdzJ6{0;pl5C$j^cXV_4Xg{zrXX_zWMJe&cD!L^7|%}0y$bd*!ZJKR@B0oULQ$4 z3j6P_M`w#XjzC;_LZ9@=;!5?~e%#Ok=o4;r0#pKuTww$*QXXn_|KqE>-wlq zH}#-T`~H~uZ99*W>66&IOwPhr#20wF>6dEvzh8+ziK>f5bZ!?dc0ot zq1|X|XZ3-PQ1N}0@btj@CnC+t?cTKH4D>?hvdD~sa`cNde~!}bK}#>8LV8LR{GlH3 zQW(Vh{z#K?=#}jMTu%6_WX4O|M2wgJrG8}F$l3Q9CHk#Ol-oIlq*>yXWPCFtYM&tM zl=g|I2s~QWHytgSu9Ex?U8tpHX_wOP&ErWg61waj{`4Hq_WUR-f8z%6J8{Dpp|@cI z@887@FW~o`al?!FO;p@4g=odO($o38LEJEn#~L@hMDDF=m?6J=*DzCl_padr`Q5vQ z+48&f4X@_kvxyrn;v2`~hS$mOH8w0{xy21v@|(=K;VS;6^tjDgXWepWa9p9#vruc1uG zh283XBUcZa6uI-d)%!)R9yBQ8+Zf5N26PVyr;QixcR}o!fb=$Rv?8P#e37qR4l_kt_Xg} zA9ewBT=<*JpY&GmUxRw)x6GcF#|qvnX3~J@G5=!sQW<~DPT^cWIE4Pk{|x+=_r&&{ zi{Veo%~Xr*um1jxeW!$W-=r~EIq-s?>2RrU{TV)!Bihk6CdZSEm#sd@iQ3}<9&)b- z>GdG<$KK1@`FW2Q#+M#DPl~u;F2UH}Fh4wv;*2T)di?&uB7rfFP#^Scqx<#I9)2d( z@4Qm|J%#iRsoz__GpI_~?>|_*?4xWOFyEdE>{I@{x3?$l5+}w5`EJto z1)(m@gf~2odnMKr?dXm2iB3-2B!!&t8;ho2-rmVjygtpN>KL5O-*ozJUZ0xXNGHku zNi`9?_ysQ{7j6I3zQ;iv&>ZMJ`gxR^*tsA(e`Dvymk?cq%HO9uszvZ8 zL!^I;3+z0%{hq7oQTt4(Kaa+O@g+A=hxCk6{kPw*N#{xaa^epn!{jDmRL8Ac-QR0# z-|a5Q*{_b%OseBFJ+S+m?LJe(AIb;( zhM&M=_5l3-4t=L-_SL?-^7nayNP03mtK;Yt`Wn@5;k*=~XFrH0t&60g+1so=qx1S( zZ5G&mPr~klv)}h@A1&qJ&ouh;^P1W?t{+1RLyxt`{9j2A@{pQ6w(n0pzYx6BGmz?o z?kz+vQmb!tc>6|ttFdpVN_&%6H~S{);ubVZz2$_Ty7K;^QOj+pz(&<3G9PV z^MlpNQoe-}zzgOITopBS%PA!hx)C&AYV>Kb)ElMsQC{OOwfzFE2YItTEtGcl`?D7R zpq%wd+ri4z=u?niqfeR-I-yUnKcLs*Zoqg_WYUw&l(bR#l8gqoGe2(oCZ=cUFjk(f2!Y)H?ER$TPN9k-gb`Un;mIXdy%&@3oep+ z>CxK-@6n>S_2vuv&rYciyJhRG$LRh<)Bo^3VTdT{>|(reaX8D--)pGnTpMo}d<)(r zseQkl3~T0$e4Oxh%7sNKxiMUB^C|Q*yD$$naWUVM!+w6V!X)2)RGxIMWV$f#?gp&i z&!kN&IN#1AB%6dUX20MEFfNM|^YXI!S!xi4Nj8aISp08z8dnQF3zkWmPFMYzF8br+ zsEJyx-}fh*MgI2wFt0z6`xMh}dgawzNuqM2s6HM~ zZ&_f(BP$^{yKi6A!Jq$weqzLS{==^yZjtu(eMjVeF5`NQAH;cHqyrXE1M;a=%ICDo!IO zKfIVM6FJ)NY}@{~{mwDgG4!9`KW`mNjG)K#ZUTW=G1@&|uy>&6rN_S%)%^Vetu4r< zdM%vz^?2*h5@#UZ2%hHFp;A9`U*c@YkLjR#C_%i0eW=!9d${~e$^;#dBMtwc*(r;6 z=Cp7%f8Q_i(Ph1S=t;eNaH?KDRH|NnbCAD9>B2ZeKNV7eck@r05BbC8K;OXEx134+jHyn||8;+`tQYB%7q)R{g!?h~ElO{#>EM?9LpG z2Q7|4JOO=KFhlA!E|k>X!(bi&KkO{VZTGo>58!9(&{f<3FzQ8)N%#54jEdsy?==10wr0v{fGZ#MF*1q>%Qa^mcF!Yv-P-{~wB=DC$r#qM!WCoN=oG)~lZ z8o!zzE+P6@FEkEGXV2&Qp`G1GcM2Yi2R|)fe6{#EjBD4y9-Cchk#-BpN{8YZ zrTS&R`#nnO*fvw^kCD{m+&bkX&R9-icj%iuK`c{>5_J|Nm|u>$Pw{{`B~Gu z#xYhO(xQFT0Vg^zVhQ~p{24`K#RIv(@5IIL3ZJZBZC|}r>iKo}`2x33mb3%N=;^pb z(ro_7_sF)MH@Vt4JB)8CKj;;YL;bFAr*Mb*WqU7R`flTC)baT7-!1gmx#Jx~CkEJY zuH>5^0lmHQZ>I1s*(7?DoGP%tFYZ*qm$qnsIv?zN8|>?VH1q>LlU-pxC+&jA=2@%$ zP7e5h`mk>h@dQdSpOaKhjAmm#w;f?o3;x**GSw>_nix@zpHHDC#D;A3x1u~iR$wf z&_ho?9$c_m==Z-nAnbAJbm5PE*XMtaOvLRVA6|!7q&@U#7vWEvgg@~BiCaQ>t$@5# zPT&jfiBBpkm|pCY5(+OTi#wpl&|HhV?LC0WFDn;%w-6GHbHOSdmwtyBxnB!kEUxa7 z`i-LI)b9Z<$2ibF)%(Zlt+zH(xwq%HNIw`4mabJO0X-(~a31_GjdwYfv)$G2D=m&Q zd(xry$<~XY2lgV_G>hq5Fh%e;wn*Bc-&^MGQy%XYVj{-1gi@GLZ?{zbdA&$fKkdCh zs{PtJ-t1B2J}Z3H{ce9RWT)uoDAm7o(wC(w_-voc&K=u4u-_r^^SMRat3Al>>6H0x~FGZ2J<5wgW5EH^@FyaT0(eOPNTWljZ(XBcHQ>REJc5>{dKl)w}j~7 z_QM3f?OWM<7x;1b2WxNZ5a9VCUF2wVq|^5?o#DEDAGJ$$JshrAcz=fQhVkZ$HI4vZ z@Ys6^FK-=pqVFN?{eGYk#$JyJ3PMaThp$6&sagwG@ySTjD z{>#Nu?(K%os|7Ug@YI|CduaUOccqx0;M={#H@gS1r1w@Oozkz($0dXps2AwGfc9Y= zlyJI$e zNs~>&*JQKsH7|$AeVy^yz90M)o_LT5dI|Xl*1wA28CAA&86f(H{^j3$J{P@xYoNEb zZ&)28{hB{XT1Zgzyi|W*EaV^P!TuqJ0(tJI`D^o_`f_gO*XOq1l1>*phJ9(sGOri5 z&H%o}zm)eNkW0+b7QH{AU<-_FdlR@t_Cxt--<|pX=W(&CPSbRpra(qd(jjqDIG!iK z-*ctBda0zKx$7-it@taFLasZh zU-a{88gEI**>P_+K9FqZUdZ0>N6UHCj`^V9EA+?*7#Vtu|H)|Th#uHQ{1uLW5UroE z4>v3K-f-)$5O)mXyyOp?i&1|odL#Z* zTr&do&F*$eeVZ+9dkdUJ*U+(0IMQ@_&p!Z5q;z zu1UFmzC$qNI$rrU7J3lq%|g^yyMTH|$0*RzuJjjb^*7lf{l)`PoAg)Z13e!AxRbMa zo&$e~_jn$k&a2@QxnjGnw>pMTW;n!$P%6ZS@rC0#n(%eJX$rUV=LGzFh>gr2^XuuP z^#ZHEOsB6CSp8-?dkw>OUd(?tHI)QSPx#%G=r?Eg$S1pZFVlaSA7szCpG^Pd+zIx- z81KD;*X#Fuf&K4b4Bs#3m8=??U*O7g?JS@{e28&Vj0?3{1LCuuj2*abvww?nH?PvR`p z1IOC+{cl{P0EV8J?#}~8lp~b1eMi9J$51Necn{spZ=cD6KZ z6Bb=f!qUg~uj<90h&OG1VceI|xa~YoHvT^_zl=_!%lt0#@mPL74)J<2OX}HvVEgR? zR~Jhf-kS`RhzF78&-+__JO46If>pX zY3l8^{a&``+c{Ft_-*I*@Vl~@7heC>F5x}F!d$%YIN5QtAHLn4g4g^N+O5d7`xdpc zdqZts&hPWyE$tz{Z~O@HnCQLjYeepQEI)hiW${jEXPY|RZWzxq5kQB|Bh#~Reqg8B-}?ulGAya-S9-bF$LfWW@8erI7kcdcn)x;O19*sV zU!}nGh@#s5VqAc>&^X#Ssqs%3N7AYN-XrCZ!`CUltm_rXS?0IkY2W%ukw-=9Tbyfn z(Ej`%FK8l#{3ygS)luc^t(byr(;5QKPGxhjG?Ej{Un=I z?>4I*J}CY+ou})Mm(u{~NzasaZHl)|*B3Tl^W(|*Bk_iv!?OKq|NA%koo-_HIO@5I zWV6s^@AK0Wlpfv3GkQ{8fA=gu`Q06FKUA*mEj%nb2R?x9eMtXYj92T~_|nTKGQSh; z59>-u)b?*|-xu;;#`4yCaMd5c&a-{GOZpA#U&r-jo!!3b!(1UQyoA&CRUhG$ey@r9 z59cr9UHF$Frx9{}(Es;;0(-wu^rLBn(7olVZtbzo3+?5%sU7S!#%24pVSoEDFLYl3 z^~kOGeIoGh@|R|G*t|?{?vnm1LPx5<^MLlyC_B%E_6N=g+pp7h^EtKm9!X`c!rQf6 z_|l=ja}mHSFIR;A%-&CDLNIQbhp%8b#5bc3eZaSwwK_tmdR%2$Ae1uW$NcIAO)ev_B~AxB6k7j zhkUw`_)X_Pnf@@Iy&w70*Kj`K+OgCUdiOQ@PBh$~WV_kPXn7xL7v_WdX|$W6?Y>Cu z(zPq4pDwLWPV9N8uj8h3nce#P9Qq6F%1-Qus+|V@$%_BeIl7(-_D#*tgyYzm!+TE- zuZ{O3RFBp_OjldeI!@_4t)wkFj)KO$s)zN~dEgWHH=ayQQkkXm{iCXPGZYqkn4Loq zdzkqHv4@yf^dH?9EP5UC2j`nYIodq<%z7o73OMd>E9kF$DZ|@2A z(|ECeyDqnW-%NHZ(R*d=91hM)q<+!QPw0iNy+1SkwtKn5eY~THKFe>@d$dj7Z930Q5A0l{m#eOS z?7ir?!cndn_*z8Z<7giQk&Ep+hyE3jj?KR?pMQr|@mjx0%M7YbXQ2q+Zc1&RC~SvF zC!*6XsCRYCEF@fi$b|a!PoeV2{Z8aOb}rYC2Y?$y|Ad8{j-!nlL`)Iyn$+5nv79gG z%#&5?xqe(|<}_K|#pwjngVFR?TpY9fiTzHY-N$A6mc>K=&Ep8iH;(bk`JXT!E85IH-w!hA_jZ+^Hm)(8 z?axz(4Uus?ZjS!F<%ejUg)l?xSx`qgP2E4U1Y?A&vjQ)8o3+kU*g9Wv5!`6 z@5S1WYvFP`j{!Nr#hBf+c=?LECvrVoCzogw6d7h$N(GuU40myUT$sdgI1cdH;tkOM zh66;u>RVs!N2Uv`ImElMD~=%GZl0Z|M9esZss&D3O?We zahgx|0#D?=%H!y*zrDHsPS^hM3Z{F1vagokpN`zw+@Hm_NvF&g@aOyc@LuB6Oh@Eo z-_DO`88-{Y^QhMQJ*QSLE`D6|_j8)buSxpVI6l^UhRi?M_fFY`ER)Wo*g?unOHS;N&M-^_y>mksO ze(t0DF=+k9^!fKpW2BysOI2@Rr#Q}-BIRiRlb>ST@Ln`e^d731{4*ZoYeu)^hjb65 zdiDG>+rylH-a&OS)6%2m_vVwiL-rF$u%yXc^;<{ld)<=yt;;2~bJ(C8c+Brt=K~AL z7eYar8-K{RxzMX~r5^BXCPZc32P_rNW<4Ui&*wanJC)8=N~fN0N$ybk)+k+pTo}Ld z8}=!D--`44&{*`lKIk{w*Lx8YmfpOd`={@lInC3bY>{&>w(r|pKX0wuPmA=kcAt!M z84*Me;tu>3_Gjmt_UG*0u|Go`Fi6&Y>d*Z7rmmjPH{soGdCOjzzXO|?AAj%jO=f5D zK8?hGFxy9vJ>qibbxcpT?{+1_so!^dox-;7cB#UcP1!z^*T=1#&F7VHE-LintRKOC zz#6Iweb`NFxsdONIv4fXTzx)obhq@2^@QR56xD0HSbZdnvlzFjXJ*VI|`vKh#2fu6I z%dFvgiM|){@ALG#S6eTH{XIebh4X0*!O(YbC|o~ZNbCORx_)ja=L^o+`o5k%@6_@C z^(Wany0`NR(Cd6WX#UmYW%~i(>ofGbRQ2*74)VR>Jl>GM6c+h=gFw&!@i^r@LKZK8``zOOqdV*$7B|v3%i@#X=zT5K&*&9TvcSv>JdjVII|Rs_F&N79?W z9dw6`6_|l{6vcl%`rC^5TRM+aH(o&d)whLy_qczI-|f^mO7+qEUyTW!kgemfDyXIRbp+R2C)D~~U4MChznhEqc)LaDc~uuVVI6{m&8-KUS23Bc`$2aB4 zT27@z0?{CGaQC>dnJe18)}=`9y-vnS{W$v5K~imMA4|QdlAoMG1EHtd#)%s_fD|9_ zB?4cuSPXYER_cwsRLV!50c^krs^|`xGvr)>eaAWSRM|HSv90>0Yy!*;Gm@1q5e{a&noe{J7?hUbH}6TP(S&9?;nuE?`M-`+w5RTC_A5=Vc9H%YU+rGgvbb|PuX`f%AugQ4{EiD} zi2W;^!+ebkuV(#<3k%u)#)TXB{dcI3>#1LyzXv9ams8>^9?-S%7ntWtcjtO9x5ZT6 zajN($$Z5fqi*W?NEnZT-F+G(U(u4drT)kug{=WLg#n-2LZwB(0F1uj~@)xw-@W#k- z)0+5nqSA#O641qIK-b%uuE|VK(m9b+;3YCZnj7$76FQOwSMy7r#n&&mGNhw? z(~Vc5&JD}1zA3ln**M4d{3?(=kKon8qpe82z^e{onA$1y=$$k+&pr zufU%*1w%{awXr#=lIz zOph?GP`}EHMBdP&P_Cw5%gWbZZ@*g?>ir6(3mzJDaT?Hd4%4+#>006Gx{ z%*(bLuTD=zjm$2mH}B+e**+5N-?VnA!}ZiZguk=aMRe1CJGTpeuNL+-^c(vDuM%8{ zH=2X>Ipj-S@x8W4VQ2Qzdu98cCw%|;ou^5k)W6$zI;cOA>|%AH;LqE~P|jxG@Fo*{ z<}~2*0Os>XmGgQ|yYpH1AF18aWmmruz1~Q2_x#N9(e9u8f0Lgd?17)#d*J8qQGX#u zmc@bPF#^X8L&d+)J&nXdj$0r%dvA|%P7dT7o>K<~i!1E@5Y(?-NA44Ss-QOcbt>OO z`UAb05#WW~C;UpHWQ}8&%KJYeYV_p0n?SGX9I6!#>yQpYZvR>&8wQ7xWKuw|m6Ee?*M+ z`2Uw51p24@VIjYI(z_|j2k3P;-)7YCVL$UV8{eVyOZa1a?e-l-FuuO(*Y|_**>`OI zyypbrSK2g<>1>;RE2rW3m-e7<$I`Y zwx2Ml8y!zk`?P7Ya72#pMc)aWqiq&`Q7@P zLG^822mHUM`svMkxL&IJAL%t>chU_z1&`jhVDW5H86x%VKIq(ca7jh@X5YbOda{n& zb)N@ow4Ygucb~#D6yD2eXonEz+j)G<19`pNro7wx4?QVf5b*y?KP{|NmKUF3#!A&4J$d_uLa@-u1_y-Ntg>sB&J) zX`oCmSR6V|-&0`x z*x9%D?Djq-Xdv2va>^+Ln)84+5i{rwJI1TOd z9(r$P{vmuH`w-pVR<6ic{U9lbqA_x*M8VOOntd!q;A{hv3` z`em!w*V5n-ybet&Pw9I>J(t(HPvA*=<(%Fod7nu4d;k(2() zo${SscHZqt>BpaUdt6~zpCp~%R2b0-VxPx&{y@%HAK3kzA-{Ihy>|2+?6asJ`VYtj zeuJ)q;CK-?^@QyJ}AREyMy z9-_p~i=}G?ui>Nb?6{wB+^pY0+yXv?-;FoD3Fi*-dM2!7pSy@2=-n*p5p;kvbY6_x(K_}32%&gF z;inXqercVju+T+uKf~$GdpXU{SBb+**KU{kU4v-XD>o0ommnT(CVA2K!aT0b&WZff zwOxuw=nKck{8c=m-8}eAi0?$!pK|9%8GmL+9uYozJF;0}vm^H_Y<2{@m*7uMKAkF` zNh+UBDxc{ppUo5D#}N3gSGaW^b~yiSY3P{Q+$*Ajql;Q_#Re#)A~Hl z&boLy1y5xLut^x{>{PY4Fh+cbGj?``D@T&CfrbZ+`xXT`@lZe*zC0 zxqEm#HlM=xDGTABIz^ulU$LE8C2;7se@c{v^j}KncEWZz=ZE@ZsVTSuk9&%eg{uCiO^<4|tyvhN=Dh}Y87RK}89N!0!eur`zOLC%pL&g)z>)~9x6(nEs%fio2={M}>L6)0)29)$P4*Ooq z=%%<#@Qo1qy}VBp82Snig!gAiF`kp@I|1ZFE^Q(fvj0L?I%x#UE!{ALQ?$cF_sxqx zo22pgq0`13tFN#mLB87vmb-yQVX^p;6&>fsUH_4K#B{WFOX@W_1RXQkEZW&;mbgq*=vm_{QOor^L)U#{}b}| zalWjpDK63ZvXA*n=YSNpbGpJOdf%(%GVkhr_kr~n`uTK?{MRzQp`1TKIkrBg`)as9 z`p#DRovQk7@vYw1OY-A#|GPTM?^03D|AzQK$oS=Z-!T6poc|GxFAnYRcB;SIlk;~& z*x&7BeKJ0Hf46lP=ZE~qJ~#OC-QCbT*>8vomrI;s`)GGDoA|vr^ddF8OiHkmieSg^ zBwKYp-YJ1p*w6Ode0~Z2UUpc%kIQA;pl4dHdbduS3ZNvlg=(4AKFi*a-%D= z%R(Q0r^NCK`v)IFdtq_Fs8540GgvOALI>MX(o^ndqzW){FXQot@e|`)#o08^GP*|Q z=w3?xoAyII-6!VCHzE#OCU`KeI9I+WSAJa0xa+KkiO*rYHJMNY?+1w^z3~Hn2KDZ$ z;m1X~FEL8SzeVW8`S6Lk_=4j+J==qLVKV6q@I65Nnw;p~arRFk9gssPm%XI-;l9aM z!qwegj%IrRys~d{rr3wh`7FosX89hp{NP-Mla2B|rM&4pDOY(X@6N`Ncv$%kHb9r2iVb=?WpjxxA8_b z;~g$`rLvpnpY4CrdrpOkLG<|jh2ge775hv3U(DaoZazTu={^(APx>fdyKlYi0y*E& zcA4~>9wq&?O||_F@k3?xJCHk`$aQc^{)p!--9yK4x$?M-!{YG@XS3ZL0=wBHb|Z{y z-b2676Y>vsgX|R}4DG_bsMn?Vh)jUcH@z2Wzq=iG#Oxu54gaprDD1p4Ve*J1m@fgJPpEtD(xFpU~Qu74rRnD4Kf zUA#r;M*XAME{fmE_>bs=@C5zW`YDN@(0*1gdKCJjnW$HhezC5Ao|}KMeJy;CbPQDi z-c2hZFL`g9(e(%;2=Ra~s6UkKOqdUPk>5!9k(& zcR2HPTyDIer`~*RM!kT~pkONpwobP9G-!us!3as`DV`EuCedT}O8x!jY4`XCatQf<2G7&iFg>B3<>w{G9Xh{o ze&Xcd{V?=#3FUQXP6IugFp9CKCSJl8cFy#cJ0-rz_%%TJWBhuS>t+0sdr~7O z_dJykli!is!|`xP*C&atdixMv#Amvnw%a&RKXOlTe!267Om{YpvAJ=aoRjea&k92eL2g0H;T~V&g;{Gzf^m=K(f)l>qznM%|H+ypx39fS(KN;@#gP-plLa9n}r*uDW=i z>Jjgpx_HH2q#O2eyKa2daX=qeE+V_ORo17a!6)jvX`$Geruh;Fcg`1h(p-Tz%@uh1 zEP*$RKBu#1Fl_6sp3j>gN`pO>@7*&y@jAil-xFV|u=$5qDlB$_>?Y$uT!1%~W;g8p zZ8+{398d2Respp(e!qix3O``yPBu`Du-(bjE{uOKJt+0Z5(@C|o?QJQ?DwUg5bryv zUfYHd%%2cH`lt8FOc(H7K=;^{H_1L@b`Q7go74FL8Nc3}L+en1C+T@jFPD{UPZKYf z6=Gix`956Zh+tk$rg4V!uAuSLJ}A>0@&)5ar)T3{qx@N#z{|mc>L@L|^$*H5%7S))V*#Xn4fGc2_G0&>Qmqbj9-@f5yF>QzLK z>3odB!Z*5yL+afucEjXG_t0{_#r3pqmU>MqISt1LIfUOifKT<}wg%FX$jSMLFrL}T zeEcf&x0>_s^t-_Po(DfW1AT9t3G5`V>3cXo)Q2HCzVD{@KhTSouOt2)!g4DWmdO6? zmhSVtr}ifX?elR#($#l7L+auw@pzJo;2}Qud0a=;#rKLFzOFvwd-iJ?pR#eDlf$>M z&-i{<7oP-j7T+fokuTX1#yhfw1Wix)9tv_#CrLfKzqftWM6MSXW^hWsugL8)KCZ7% z=Q6Hd6Q^AcyRQ0?ja%()Me6gu2I&{A8wGC=mt9=9J*;M}U4VYkdpK@iFTT03E}oNm zz$0?4hi67zJVO}|y@!+jDgN4?dvA0p+b1$c^hEr%yTsM<{&EoKmphN=d6aAteN8q` zWH|NjftwYkiAPWV_Z#wY0If&3eimnpmG(Z)I9XwfGo*i8huZIES)5_(t!{Qf}XoS(iaSAQ?rL3;nd{e*azQcj!3q4Yk2%X_=$Y$xG? zzU|EE&1&+Gv|q37S?t;Qn#wND_v6gXC&=T+Dde%9`JJ7!IgSw}6_bnjsZ1{Fue@Bu zUnO2HGsIsVLN3Oaq($h)Jf|?~U@Fsv=D`cNAml&ZGt#*g9#`lGO>PzkokumocK49H z!}|BDoM9=zhqH6_p&zuL#{Gox)mfC&wqc#>smQe_`s>|SHc{h|?>$~W-V(hO{SN65 z?HB0%A8teq|6nn|yL_e`=w_WMdnWfqS!i?_t?hjfA7kgw0G z-~SBLr}r9F#BS65VytI$zKHosagM@b7inK#=6%yNO($!)%#XB5^pW1LX?wL3H>>|% z+nsOoA!jk4e0~Z1f;=%2jQ2{y6y9&J8}U?N4={f%UPL>4Z+d{<*!(~2y$hUORdqjp zZo)Mas}dep!ZpAIf`n0Yh&;lG7zHGzpd&I6Pdsf4!f;nZ3{6 zYp=cbdhN9zXX)C#^qX9Hj8pK({5mNtt9*w?%lmPn|MgA6;c{kr{xp1NGrnbx)D}E% z)rWqcWOq&v^!`Nnbyc?(=qX*O&v?~bOEc~fubOY^^X>gYOS7IISKTjvo4!^$Jt#{@ zt^4E8)dI2rj>pcCeIw0n%N zBpqA_Q@X7|#EHM8YWKF)nTGyYg7LETG3+~&EFEvu!gwG4ec=bme6yq_?yY#oN3(v< zgwyqpus<%26E!(X_{J zCFtw;J?!gwM85ahSNnwayHYw*pM8rysZZd6IB~D-6G}RW=h6BX z?^(0+G$}s~8h>qMzsLK%%<#RGxIum$?Yk_ajsGNJN=Q_YZ}8KtqUS<2q>e zsjl7E^=Hs2&o4e+`EPO`WRS0((gRAbzB=g>_{ERIoKg7iJ>b6~jydpJ5_SI!_p4mOp+WA2mXk}b4X9vYb!S)S8leJ zBR(G9@AnEkN9XlI=P*ub|E8mtqMs)~M+rST7>ASZBWv8q`xE(b!}*fnPXPb#e^%P- zQ*dh6r#kzF_kaCk>Cb9NNpn8|H^_PLGqBDN`-ys+t}o(?mlncF$7`v+jsOStSKD`R z;-RTZ*KD4Co>u6qZPn*RGW_6xO6g8b$7^Qmv(k2%KEpXaz`f-G+~@czmRl_u@+&%5 z&@S}Hyr(lqm!Cc;{jawxKC#_zUzyOaaOBin6sK1h9 zNyp@5@=J0y`&U+PN7}b@tP(#^`t;g;J8{F#v)X+-apOU?`-$DRlb&Zi2HYif{Ez^* zSO4fU`&=8~IoWv7;GZ;2-^T+#BfOI;mVU_67QY{Oz|xQ=@vCQ8J6x_28KXZN@b5j& z;0C;pW5E>f<3hZ`?5BM! zEKR>k_AObO{*>%TVcPh|^9A%P;2-fLA1|kz_<6x%X)m0^^ZAgZ?EBs?0$NSq(}bS) z`b+uP<_*wuVLfNz-sXOfb&hyJt~~k?^oEDqBjw~iaOLNH(fK~3!(%`6bRggQ{(=s{ zUkh;oPtRQjXQ9wD>}RRf*J=Be(qxr0Uta0aP_pUxYd>ya8sqXg$Z5+tV61L8(<`+5 z8fiD^Mc4`Ta|`AR@S6eujf&^J%I|@npE`MbQ0D`|UWIcv(4TX2`t#0TP`|4loVQi= zLDIEFpW*(aVAtcD=CJ=YeW;yUn1gq_#0}zGy0t)M@-g}h^Uq3YhNhD%hN+itn$L1i zV}87PA=49%Wx0;^#HZR%eCnF5e)O>8JJ55}=P+KB$ILFZ6V8ftTEB1TZk0#R2YEPg zA)EyYN6sIZd>cN_FFI@m?AVc7&hstiwX%Oy+YfvjzWe9PeO$_gd#Qc?kmge#`ur`L zKSBQAJYTv;(m^kT`AA=b{jLpsN%8MJb(+?T2R^Uq;B!4IKEIw}Y%(3i#ejZzn{zmnKd^f$Rop_M=g!yZ7_Egeo z(C|8&^a_4O@3Cf|$v#5YdlYV--{L{?e{bps7~iFSfbrc)Ilen-G(TWa+wtvnFg|bp?`HmeOvc~a&yx^8l=9G2C?Ds_FOl+c9#i?WC|(EJuYog?+pubZVj?8YRiBJ*bD574=JUcXcGlNH9lSCBtY50+cz`Dwl8d3^Af z=4$)0PlEhcpJVCmn)dxd+}4i+#tnGhh3}{h&Zob;aWVZ2CwCb4JU!w8>TT5D_AA6$ zC`XySXx*pQISO4Dfv$DxrBn}W>NNV!ZlQ08tH1tFLf@G=`nHcs-zB5acd62MiP3lE z_~<)ide`QUp!4m8bUt4hvaddad=%Dk-1toyh3+pZqI-Rc_0QyQ)6^FFN5Nkr;4tnZ z>mZh1srq1P^{-mSLH!qJXK#Bw?98y%tH?L!x&G6o^pdAY5ihw1&(g$8)*&rT zyke8zFc0&3Bj||{@B4rpQpb4|NKF7`)@61-{TE=gxvf<>iK-=LFE5~ z<7^wtieBjv@%soo2H(5K`OI+5?Z-P@q`Sx>$% zq5Rl;1?O3E4vY4WRwB zrvA&kDzER+?nfW+_HPIA+q_eszWz3wmkmjI$SvYt5~ox;0nZy>>erigYksC5cBSPC z?c@fNv;DS{+ZE5uPHwX_<=ErjFXLZiCqE(=cE*GKCcj(s>Fa@S@^p&_)@wVU-;e%N z*CVuEW+w-QI?etP+DG}-%Acp}`*42nRH5Hq$&q_zTlCkG9NycE&)>1Lng0>z&f|&p z`kKafkK!F~-m6dF&s{dYSK9cF%fETTZa%UZDF6a{0St+`>K%9T&M5M7{&8eBj4nip14Iya?s4Lb=si ztZDwJ`#apc?=qCTsj~&%62M!;`hd5{?ju2t#Nn0SAgA(A(mPxDPwl7Emx!AI&TH;1 ziYIhwL%u(tN2+%n9EO@ou=KE&_k0oj zncbonz1_S{NS^m+e7zvY-d3t@;z=`?&?patn@>`#{J?P99axoZQ1t3hVja-?=`=S1QLQ=f2!^QZAhj9z;3t z_IRDmKfbmDai}Y_0QAxU@|Q0M_?6O{=oraA(EKPYr^>&m?`W+SRVMsK`NZp&7Ji33 z)>z)-xhU5krGxdy+x<1&pd3;48@k>pv2|vU=#1GbbVO|oyqa9$I>|37yl#b?i#e?i8_<2Lq zI2>yF%I0q^`ik?oOkdHTcI$VFS2?;|pQFi9Jizv`{)>nl@OWr>vs^t}^2-rcp`>5N zXDIi)oII01I4{8apUn6CK|eklXX>SFUP^r#r}I+k%WPgs{wzn&r(SDtK9cVj+WqLO zGyHo>Z%{f0euUjQO8Rla!GT|~+9dL3@z{|8P3n3vP_SO)yFx$)7Xy|xgAYTIey{0gCc(>@05>9iT+cNw;=a%DCY*09{KowpT+UxjpmQ^)89$DrwW#W=f=-+yt=7im(Gz6 z$}j0eR9E8H?d0!p&tEvt?EMj}M3+)AW z+T5=hxq5F9`gyy4S+4w4_%o|p;VzPLy(`+aU!&129w41Tzkk_-`9J%g$y-$c$3xaX z1aIVr@z^8J(cpp~^5>L)R0#iB3a_tkjXuNoEwg*;57Yd3`9gjA_NKXh1ONZ{2OoE$ z0D69f{tNdZ%J;yP53+MGJb#;{=V0DIK1uIm2Z$g|Bxy`r-I^XH!4ep6>_Zt6@KXIr^o-_3)I>u3`z z>{q3qCFH34Zag$)oWZ;J);Gu;5Emh-t`i9W^uQ24SQ zz_jtT#7Qlm`T_7ogatnJ1K>Bu^GVORzQ^jVH~70Py-w2}|CP{#Ieh>+o+RHbj|W$h zKQ^u_9#5lt*!PEemlVp=SxUzdC#UU#JMM>DCvU=dFRk9icAUO~zvJ;!d{-$rwSPzc zr1||_C~xoMVU8p8lr*R1s}D-Q?0a2uj!)Z@ec*clQNCBCPtf@oP1`+KE4c69(@pt# zBWl6E8Xw-k^vHYp1U>G)r-;vApaojsIX-^Lg64W$zc^LvNjy)-QQ}D)pN83!4*H|< zfZ3C-?R-CI_GI2R_IHf)P-X2s{7=eZZNT)>Dbv{gz$I+|l!~R_Xz8$i-eYN7Kezj0 z%F&hDzSkQU350UAT=Spm_A>j;c*!3_zk)w8&Yu+h=*JU$iu^5-Uyjbx{*BzMpt+SlfMh#KN$ad`EDB^rM)5Y zYg{LvhJCc@JbOTk>HG)&3+VdUha^s6_izWlb7TU`;r)U43tXBL-LD1t5BnU-(N}dm zYR2EO`CIXz;jz)==|a*c8Rb6Xa`btHGg>FK;9=WqV@bwcM&)C)aN3;yPA(-*sRoJ3zSJ>Kur9ABk_gMQt56_j?{gdWjc({j3x!x=K zTlmw{ZN&68_~rw0Z))ql%j2{|@sN#&vt!%j&1mCM`46q?|;&7i~BkW*OmK_mScNG{vh_rdOzuXO>hra zcwVp5Ud_<_Y=0^H`{eg~pV9<3^OtS^czdy!YGCV>?uSvTZ)qXCvvcsKjvLFQhK>Q^Y?>{N?(%; ze-3h|+_M?@CL8zD6mFhxA}|HM)1ldeVf;;V&l*v~v3LGSy1j1<;y zS3HunQ;O;D=IMJxan*+Dzx1>nZTp6|8-JOeYHVY=gZf?K(of@S4CG(c69vwS= zHCgeK`y3TcfEN#1yXie6*6t|&SN!F^3xyyZUjBaOEZpX}Zd z-s|Ysy_dsIIz6O%=~nHIp8pR1PU%0J{e}y2_3o2-f%ofw_aDvW=eoFs_jmqF%Jr`8 zrXS|)2Ji@c3cdJgH?9>yYJ#9x*3}m1k1)R5%wG)S%k_jX9&%2D@6+)e*7VrpyI2Y0 z={B5;FOEjHALi(AW3FDFZujTP>-gN(Iv(Fvx~*h?>eQQIU#-XUT+sbi)(0K0y&wM7 zT=vI}`}G)4S8j=xJF}(Vk(BGJGp>a1uV5Tn*ke2VbFhe1>LP;Ly&-4cgVw?_XlM5l;KF_7U`wUkly?%%3zdia__cK|Kw; z&XWDTt@`zNtq>1dzh1`r@xgR-<3psP{NgEXd>*6^H%}!UlWK{2=IN99>^g|gj>Gji z7M{V+jW<$10^hBL_#UDN)au-)3_C$NIzsck9#DC$5nmrKd+;*Ioe)TV-d}_V8T>+z z?>3<$@VQ;a)#r!x{dJOGj%rVdehd0D#B)7$hwD@%VH1O2GAh{kdl-y<+n(p9+8O zt9zR5-&p?K7jxxD^XKl9a^vym5;v}YdVlUX^6zGS=sN6p{ki@e9mewKHss2W=Fi=s zbfZ5P{Hu#fTnFkYG0q|9IduM$`cDr3yq`Dwdp?fFc!#%pu-8TL9oWfz+3^hm*Xxg9 z$KNmc<%r)e^?pR&uRK=q`ggU%|C9dQ(`7$+tA2eh`NH|d@ONAMyF<*sn>t0y$CWld zgMTL}8MxcTY1DyB6AG;0mXa05O zS6KYb)f>$Z{Gd2a*}2#!H!scnHlBwa?fcwG7w7A(@iortJzws~#n;xDA9~7H(Qn2> zdrGHSd~K)Bo5xfC1+IQZw@#&hvex1U!{diHqYxhXFj>p^gRBb|<7aUXA3u5jBj9WH zFT>CHx~^zuaoN>O7sYYjQwYcA33)h%2l#5+g+kxWww^P&UFR3seIUCyU)|sJoH5sb zL;QvD^6b7dTSrRcFSd>}&iG4M2gu?r`x~$Swdy&~S6E+?d#*XJb8-fL^>LB!{jyn} z$68)p_A<3RrEn@)c&+&`D;wHj*JHwc#+WCA^MhQb z^XwbRSH9mWj|0YkQV;lnUtTBsHe2~sUdXSO-bWJVsa$Z3>y)$boh!u2@aqlVsd98h zJL!7DQkFxz6IFk)9d8$*$-{jUPj>t>y(S%&pKnK+FSN5^o#CV3g+2Wk`G;YKbl+dn zHDCRJRz0{-^IPcd=rP~c8SeOs*MsMdTMvf%?{@VwJ#(h^b5wm*fX9u6aJmSmPQ54R zddJ z=nuI+m;C4W1NtoKVtml+*J(LDybSus)q5}(Cx3RX{9=($xra^L^KcK6a&o>~^Lx#H z^riPPOi=!T9OADKpF_W%E$K`Ty$;~}yDYEpzm9V~FKx=-*FE0wLm>xqZXqwBBH&Dx`GnlJZ2f`umUAc* z*6K5EbTi-l^0Dr-iyOqRQsO#$i+sEFvMUPZuXUds=oR1s-w*Y8|LDX&k$==`e-;ac zispG+%sxCM<6n+=Kg?6TzFv+#s&=jD9P3rq{zBzHVPN@{ZN0+VPtITeMd3VIJ6(=G zNIUD~fPR|U2`9f8PcN5u{`>y8G(B5BGK{7yPtVKWLiLCG*qflg}Hp|D&&m zuw3e=1wBx(9-?wSpv~w$Hua-cGYvjITK3y!cI_b5Gf#FLGCkGGua`>GMf@84rJI#s zH?@pocz=Vu4}RiO`(A?A%h0<;e&QD{2LF*iLVbTf-29wA`r$uPFNvCeymdW+^nWsb z+nc`Z{kGTbx8HW5tDpI8-8!)Q%#ttM{@fo_IEzNfBvD@=Rf$j zW;-#KU--RT`O*BsZ%Mh{wVdbgzdv{;}t<`0JNXvf1Q zS2rkl_0vw*_2_(Dihg0(KMwx8`tQA8NI2v13zy{d@K;3NxB7+eVt@D3FRYYKD3qJ9 zKfuS|jw_VA(dKvTANu{eCb^sSf2?1)LHM8?-L7`_srL)-R=7c*7R^tBe>hF~G5CkC z)bVNc5C2)=Jz2k`9DP9H_~H2LzFhM&KXDGrjYf}a^mWsB0AE*VSBv8|L2kXhdQD;d)_o$NU)&&F zi~P;Gg>Z`f)}nnPa|-LXYloAyqxk(s59p!3*Z!~HI{LYh)_uQfSDZYIX1`WHvVVT- z^||souP(4(*GakHx8~(4@F~Xi6gRG|`$WRLPZ`lH_8_3^RrebGNg z{OMWFzC97Y;%TmaHol9LzWd`>{Qhr?{D3F!SNy6F-V^sLeqIRgiTf2lDTMdO^DADZ z`;7AQ_3`@^uPCg)|9-_w3gL|3uQ<7|e!KG9c>Ib>&;MV);*a51TqoaA@%nA7cvxSq z{Ahl~RZ?y|e#P_Lxc=$=iZ?3%d3!(JctUrM4rBQh^K#`!izl3|bfZ5J?CC{f@8n<9 zQ?fW+L($grs_Iub`=oYtz}`=!|5D^{TvBM?86W$f^gFiyQ<2{>O*<6mQ7Lt(f8qHy z?@utkLH`i`#3kUf7JuR(h5Kaulq+jRaG#dKrJwQ)O?yA#8p#RkXk|T3EpY7nW>0jV z+rIpmL%&t*#~dX2<>;ywKc-vo3+Hr3xXmW29jx7ub$#x;txR~H@@4Iy1N6m+y}xLJ z-2W#zl?kV4ex>wMEmtY=yK$A$1$uG`en(kKl%wymzT@Nd*qTGh~MZ__!&Qp z=g+=}eHi_UcidP{{N?=x*VNvOGD|PLRc1bb| z)Rz5kQm^}(%au;cq(CxDv!z|rhi)9^Te}!xw5w0o?x(cfh1TwTePVv}+S*l@l%w+% zuqKi7N@ce%mEegxpDPY3rdg}c(=F4rgUyz260 zaqlb^M7n3$`g0!snla$tuJC!^mE3Ec!oTwJH(%!Azjj&e%5p?K>*3?O-Qf4p z#`k80Pd_kjY}fTOjN>(eKZs2J$;S6(*DhO({I^Jfa`bmC^xA3d;r5>Kb{kxP0$hDMxNlatdldh;!MFk7f=QRok_xCN z>7J!%N0ZVHuxoB-ueCE8U)Hpp$i{oGwgxT)O6R6p``{R~ zuTR(h^R@kUYk#Uf0T<&07~t!0|CjIAOoI!J3Ap-naHlKWPJ=r$g}dx>aBz^9%s!m? zmaDEp{w2N3E*Cwh<%3@!_wlp7^rzpgAIQDRX*txD?@nnt+^+^cYU|9#TfT>(?|nbO zFW`$U4lc%J(L-)s;zB@|@ju?4+(}_Vjyn66XT{%#|#(|BIgsUDnPqIIN#k z=USTZlWMo6*}mLUXlddn_nm4QeyNZ&s@2$DrF5_UQ7Jvg?DjEcx96DMKE~|!9MK{8 zuJ3Vbw=1PhY9A`4k19W6pCLvG>&wtImC_6JMjQAi@Z8GL3*`TlQxSbh4{5rEjtx6! z)M0cSFgkV_9S4n$^NbE--jDS$Z3lcPETVjljHF|>(j$C7B76rh`0co#2fxqoA^0Ab zra`}QbX1yFxX6@WQZ0sCw(mm%p6Xkg8NGiDRYN`S@!~n4B!9pgzp{Emq#x67*L>g$ zQ03@tX&Mq)juK7xl-{ZK(BES{2mEHbIh?mgf6-LvzuXI$!h=dEN2~z+kq&qTyvL%W zd|yH7-7^ujMW(D@`1`%`UuZAs=p^4Z*0cPC*7DHN<>(qzjpTcXT2A&gv)&;2yzgZ* z^?jwpxK}c7s=c4D&+Pr56VB6j?NxXScJn!JFP~jg2*0C^^=r_+7_UA0Ue=K`-^=rE z(IZJm#QfW}xXSVGe};LP=@F3&__uODp%#cYcNfA5;{$!Z=yJywGG5?2=!ahy!hN~I z&EqBCSJ!s-1@9My@MaUw`eLmw`kDPdNVD~Ou$?c;@B?1AZpQu>4s>6+G!iAwKV73apMt7qg9DVncWQg z^1U3(u%-7r2LB86{lqu2d&YeE4N^YbgY3r#-)l;$CO-|dDm0WnK(1gH!#zH2KX3MD z?~{7ojy`I3bcWOi9usoqw}>8$8*InJ{rzs~=Z38u*E;#9X#dXrQXg;!zwXZ`lTN;$ z2TJ?d{g-!2Pnz*Yzk*x>-Y>r_{Me!=-lO&6dhjcVcermjK7@Ly2v5KV+;4vpaKm}C z?J5_Q(hHT}J^dGpKjrBd=)T_Q_yeJjFOPjpzWj|sckz1^KIj_g|J_`@emy_9P0RWE zIuDh4YplJmqaK(`eppYOiv3ErGA-YWNZSeJznp6a{1o&k@CoI%NjbT1N$dAk7b~9! zeu$?Ne)_!vIQ{Yy`fK?jI*0E}E^6Y3s@C)T0F9R3Uz+-joxeBul(*_kO9#Kv?suxT zTfXU=D&w4Tf3oEhPGWwMoU_;bAC=Mp z>JLJX%XCt86afE)(XUd9RbRRG#O~o2?0tNrCIv6rrTCDLHhY_=yV;wd7n23N zKRVt>dz>ubJAu-MvReK}`qX~LK94O^P+%e|zQ zCVp}cx~9F{%tpJZ+>qXA?{9t}`E~YR=bI{*Gj+WD{Q2HoIm&ORmJ|D^ashbv8sGW;!;WCQP9K)}YV>g(5C0zR2fG1(Isbj_8+5;k>50^zzk8o>(BAE5sU0yr zAN1l@^E;=Sp0WG>8dI3=u>1Z7Ob>O@9>jw-U!F%hP>wV-(=@-jOULDxj2FTf%dd4D z_N{MQ^(pmkriZC_lPdKr{9bT~@b4b9*FsmPZ~J6Ynd#eCq4=Lv-;VstpH|;KGp8TH zhhkSKFRQits*l=GPIwc)Nh0phw;>Md_3gwQJjy%yD)6t@k9FyodoeqsLQ z=@95MYZUstSLjoY4rD(!oBf%5sHQy}@*(*qZcr~nUwvk0Gu@l*e&$Df0_7aPVtfL> z20jG5kD{FO=T*<9ehKvu%E{3!xu3IF{mj0`B7G(u!H=`=hHPlnFSF6E=@&Uyrg9Yz zn0(!+WK{WD{3h_HB)KJ@Dg&?_YSo2KyKwm$L;E&~JvwrL6DL|4;qHFpr>rDBokT z_v;Kl+8w!%#L}c+@HcDreeRyph1%b~`h7MJxWM}V0LRI`4<6UQY3b0PFIzhF_w$wx z^cdFh3jSiH^mT)4<7ocB?>G1n{XKKY$D2N_^*p`6XMrDJ&-diwPPgcOj=VkpgtgzV zcvVUxOdB4U_vGQsYQkfXcr^5(ag42)kJc?MQaBsyaDvZF$wEK=?Vp?V^LlwN=d-kZ zyx$`CsmOgwwC5I|NWWiX^DJ3!vG+DFs;<*?7PnogY2j0QuF>bxs;!UK84vBPUYypG zuFAbM3pFkG)+m37-Lf?2BNB(zwC|tFx9k5*=@0rbv=H)_r#A&OUq6f+>OJeuqIy>- zL9=;Sm=A^dGx3x8u+mZPolyJ(J>o{Y=7;YRhx_=$Itjj$JxN-KYZxW=lQ%`dMB!up>5M0-%YOVjV12K~VM!ZQ8wS(CpI@A;yo&EKnjjpacYOoR6JYYo4;Ph=e4wpp)E(}wB%!^T#nAt{PcU4 z$4I99;+xv_*|&)LqB7wnnvQQFz4~qo`CXcyEa>NaaNZW17j7lpZ>Ai3eucdV=iHO7 zVJ(O8zeo4|%~t)J#V;5bh*xub@^F%l^;&;l>p`0f;XarBS;c-N)pf+z*~R>N(5p9j z`+U6B@8r|f%dZE`ZK`*)me2Pi&;xY%nuJnn19NnIdnQh|d3CqaMb^!Ayky;u>5e%} z56oe@Yc|t^q+>F#lj)5dkECM!ek|#koXqh_&Q`R9AF%sKd}cA0uou14H7WVrf0KRp zitw`U@tn){)9=lnW$7T7r(5~}1=wqG(y(8}^D*QnZWz7?hxUJ~^zd}+)OqJh9XGGf z!C!I@p60_Z3F|FC{8_VK@(j#Jon1dm^c2d^kR~w>_vgw-I?rT#9`3iLoP0lB+v}~i zk=}LkWl@}chQI;*Yq$Hjl{73rPY0~Kl+$~eC|8$>5p0+I>+D%6k&huSk|pJ*%l`$R zRo(imlup)XQnh%86hgW1y~5b=3~>)nU-Y*q{-N@_QNhYMQGUa@rjcdLNB`cr3-em? zVNdCB+D#kR_=ee9Q2ckLTNtl+kb0yt(d2Ut;kEYrSbb3`asR2O8&E4phwJ;+{EFuL zerkUlAE4i%Kj_D6T|X-FackP83zVPZfu}|e$~voW9)IjB$jgDp8}SU__W|Lfa`e6y zd+-IN!(PqroyT$T_0{ij{RSNX?}o1e-qVy$`%`a+)PsMxP-=#KUV!&Y>`#L-k)LmW zkIrW@{r^{Af*du=P3BLMFEam_SAIgf?*KgW!{Yib@>!MgEAeehFV|;W z=loXY6P6D1fyJ8k?d;HY7HU4)S&nvAvw!jC9jtHP5B2r7X}#{WUZ3`N8_PGg>oYlS zz5+@P>*n+3xqQBVjy?rXmOCuS5AjV7>$LamPjVRTn#^;oe7n6T-LmzyetRz+ksrnn z79p&!L4KOP6Km_|C~h=M7h%U~2fcoHw-m|CTeyF; zufg_f119%_TeTm(Crwkp@xXbsZzoZ|!~$IO} z_lKe@f?9<(v?%TxgipGuk3O}CC@sM?Q_G`K{D8D4kGpBkJ?2&p5?rPY%xR9GqR6_Tv`x>ZLE0 zalhgu^$!9ad#mQ3**S)!db0Ts>}OJaiKVwF+@yN8rAeox%I~Yjb>g2?d+a^?msHy;7W8unrYYM`fJuY+9orv4=3 z22Zs=TaIqhar%FezaKl}|4sg0_{AdrCZjzO{{Etz$Erjdm5=0mj4DU|`^3M4-%u;& z_i}WK*7J6a{xIVtHM3)}%@^ZAlfR9eU&j4xFX=XYJje8KC-r7;yV)&fi~P;h_C{z1 zvi<20I{A6MuoHguEXzNWPssCGiS%c9r^X{IZ7ae26Mwf|;9qCY$|MFD(Jp@J{%pDfQDxF8s<$eWp0D%$%A})o{kbx!U1tWBNzW5n z%R2w0>FOsT4VN0_F*21&CvgAzq?@>ZUF!dt=HH6)qa;{ozr--+S< z*?j->NZ%(5+W4HeSL4`8*A%8Z+F8FrzV`9~xlB5y>U%%$eW}iSO)k^@A#5++Z16(d zyfKIM>xz#0<%GLO@qnD2TnO*QS|K(#dAxjF?1aMlrY|wsy9Q5;rg?JvR?Ijy_D?Rf4l<#Y%^R0tl zo?-F7p#OHOJ&<#A)OQ`bm>w`W>)OfmAjd74$N6YETBmv%{t)GW6A#D*=3V2_1Ly$y zcY@G1#0fpWBhK7YJ=T+n91}kF!HO9%u?dJcq**@JZ^K%~$eAw*73;}_DpPwt=FZwc!n}>Uzl)Uy}NO`{XUeb8gDVmC|hGPmfPNt`z9LUN8Wh<8yR;gTVK6UzVf$F}d>V z5Z|rtq#U{NU2gOKHP+5F)B{t>5Bsj$FwZspAm>@NKE^GSe|D}N@KI_9UAcp#oX}0{ z_g3eVekQ;1bZbxKOwf5evH6DVSEHV>w5J#1V4%wIpH7F;s z$-SK4ARZe{Ph4pJ+Zf;13FopT&PKVbwsRhq`d3pe9sH~HnwEI7<(ry+3?``X90`ujAVzb`n2X|I=qW?iS~Xmuj~3-|VOH1ID>& zzqjo>guccv#BY4RAy*-81OC~0kI*&sqtDfPnZMSle3Okc{q{^BwA=eI&Qn|TYJT5$ z@T1|0p#6{i6a47K28Z>teME#G_TdPmhz2j_CJcsobv8?nGYCJ%;b!Ir)ZO z$mE;y2E6_o+e9CEJ&zIY%$s_I8{B zy&mS}K_8xM{wvo-C4Ni$(1_?qbo4VlV0_d?`w|bDJ({;p- z=!qcLaU+{|T$hu7`VZzGUZY@Ce_exe!%SnpvidQrm=5$pKd(l)AuA`&G{!TWNA~l8 z#Zs=X{#vyoeU0<=*|Pt0MQc31dA|(l7!Me}H)i6O`;;IFLF{#+#b4}XO33dMEq7fboFZu`hm z{B3_(HT2K+oteKA*8P0Hz+b?BvdCX{e)ugQuda&<3bMcBZp8=o9qD+mP5JtWGm(;V zd_BlSj)-0uWC6Q}a}dWF*h-}G0J9(rM+9(rX9 z9LS6KN5sd;{a)E;>f7`B!TS-v%9U%<4~8H5_wOj@^n=o8m7*iMHk<$dT*{3p_iJ0_ zelFyJVkq#eKjuHkzC`UG@VHauKJP!wQba@^e}g|Ve)zcR&w$U;>SWcseRb-irRjbZ z;xGF>sFydGKHgyRzQX1gHlLOGpW1y-U(UNZ{yty*GRH^3{=M;y;1|kwrL^G>;FntC zZpAmV--DXY?DvN(9rV|GHC+^sc!R(zM^p8^_g^+>ens20P$_@nvs1kv`f?Rdd~Z@d(2PO{AAu1<)381FrOX$`i$Q% zhE9Hdy!ODJIlK6Uhu{y84>Et?1zIn@O&GGEzU<3W`1{(IcW)tFb~L_qCgHodTt1%n z#=6(9&t>^rJ6r1I_v2mIRPQJ)zpws({~Y1Vp}i^>J(G^e`7@Eqf%saKn~uF|=i}6$ z*~4_|&oF)$=Ecc#Nx$S2jzjVaMMwA+(Ro4q&^feSb{x$^2XL zclb{ISn;$l|1Qg6aJ28~f_?B}cXeF-JO}*Z<6r-_tGV8na&fiC%+Ajc_|Rwnk}Hq% z1#yGzc)0&0<$70aBR%WnucElxgBB0C719pPo51u znaLmVK40ree1YSi&ZC3BwOq$f&g=$uenzL2#Lw0r_Z1ffo`8G39F4Bk*BG6bYkl-vhFsv^ZScdmEK$3V zwYS*PVO(}=TJF2D{NQizvi$j$PdGu3$i6VXPvdK>pQQ0M)(i3-H>{u26+K0!(n0QB zu=F15_Z&;_)wFMKm-yWwo&)$F=!bncT3VwXeS*bdJLd*Xb zOCM)x)8BMgFwdjh9cFZ^Q%`{258j7yJXl|hu5TD%&Gd}~{N?BQL&tScv!hAE2JMbt z;sN?aBiHgh+P(ZIh$nMEy#9mUE=TlJd_Fj<9JMO|pU-vRBQ*cX;4?+=k@J?OXUy+N z^}G2UrnlsNFSCs(`SM)%tz=ij#&H}<-T_p`rB$( zpCeU*p7HR*{W259zLX>KN$|geza;ZY?O(jn`Z>lrMmZX6@l$l3>f{&W4nJbO;03&Y z@RMe~$lrJVuAG1JJ(M3J9dPfb>KO+g>rNs5;V&W z{TTWbCq&9o>-=2f=c@r#;L14U@1dCWAoMB6JDXq79~-%u^+5Md`P1{0!ZkiFf_q#c z+!GYw$Snp})WpIfxJGxt-7-rG*$@0Ptq|_fS|ZpV#7ovmP99&mZ&vkO5!{0c;Zh-b zxZ4WhQa+35@U%j>oIm%i;z(CY)SvOHc4dU3{S!wu@re*Zex6VMQqn>0v-`|vs61!$ zm1)#p>3ro-ONaS9?c8Yd74Vs`g7Dd6 zdEDXmW#fL1zE4)ro_o04q&@IeUjDnkAmd>277uNsAFz45K7IWyz}Mmm=>IFFUf>7u zH*_4qj|*Os$*b(+05vW}hBYdYtU#>KWO9&%1x%LV^K))_SI@zZld zgW7%dFEGCO{>B58)39$1{kTc}N%B{w|G3WO;RBxR&sTg<@4g?3-W*z|c=Sw~_9x$tmF+#4|0;eR=Uf+0yhcmhNNSZL0YrzZd=s>y@4! zlq>T~8b){FuOL^Lii1u+`f;;deF=8hm49!}-amlyn+=|@687&0yB+)>Z;!t%vJ6J}b3PKEDHY@wpG5~fnb)RzE$Pzb2${cd{w02< z_6PDL|75*^-*xu=nF9Z4K*8xg(?levUIG=)k9jWp| zJ=P<346aFE-Rww}{%#(gm!sE0jy9VdO`e*QqdCMYwG*?MPVGb|)2W@9sp+zME6sYc zUGS3qqx!zj;&`0KEu%$`2xc;D}ISzTgiR;uC=;uS*;1_Tr;OU5Q4e?tqr_YrBWaAs| zkEY^+{$M^18^=6 zd?)eTY&rel(k+jV>7%0g*&7S-A-wU7 z-_AmOE+amTVtkSgo<}Ug=g$lAA-wV6^OZt;7`K=Gv4!*r=c|jx>$F0A2yZ<2e5w#1 z3QAljK61`R<67Bwo6jFNKMWVb=lW;{f3Ct0>pYO7{JP^ADo1CJyWI~IwmXyKZv8?# z>vHniq3zH=4EIZjURC^#qS{RNiOBp%^D7fds$atVdpvZ1hr_R@RH3}4^d{UEI3~By zA1^vf=TF^yO{3SseV8_VB^`fgXux)wPfCQO^-d#<{Yoq zxL@H0c(MzxA{uvK7`)`9e|LsBXyHS3I zw1aV*oh!dy?2Oz~rtNsRFOqV-8~Sxz{JiKsDKFnAPiOl;f5>eXh$nT#q3A>)qBK`5>;FJ}U`<JM-<<{FXd>nrac`ZnO6k+P^3TB2xg$q zy$?3a=LI=G^bI-vu_agD>#4ZG@q-=-@INf&<2vcyTkR&FrE)^P1s>zEmFT6ouTye_1YEQUh;+UUD3Xi-69u& z^YCYUoQ3n2z^}ot2=&wb1&2yS`NaoWyrnu#+e_vh%4gRz`0O}9pBQiSw6{wB3;M?Q zYmJP%kGp^mfZvDF&fpq_6K`CvPhYP{p8dMt9+e~Njh@mH^%wd~FE6bQ5pJDuy zuhXaa1D0m{;X7&Q4=CX84ch%B!M`q<^25AlNbpZcCe@GVe=>!9l04n?dzJc1?wKcl z*?d^OyJl&&AM~N@R}XmUd*9wL=;z{qYOjpnz}L`Bpl3<$JqmF@q$h~q7Ow)E${JpG$)T3PjAo95cvWfu@2Xp_Lp#ys`*Lv(0}?d zS^w$h$aw^#2mPSFI>&Qqb*k!3kB_iH(>xXQLH|4DoP^A;6i%hBi*%^}qrtU%KU2Ez z(EL8bYpH!_FIl@y^W{DzKI>cgYz*r&$=!1kr}l&UYLgATXSoI6jf#%WkGxb;;VSM6v1<(Tb8p1`rBR^YI?|G!dkd3?bH|T?~zUR+h3wa-pExrW) zSAMts5g2FUGgIOAnIBvmF#k1;C;m+NIAX_qWV((6uxJftliH3*8z?CdYAM^;+dL{eq1h7Z8^8D>A*kn z5XT4iWk_f&8sV~6bfE1sNxJqD{|?rZ^#-QRj`U2T!u0e&yJ+vTxcAH9v)$O%6tHqbSTDL%eU200m`}D~NAu=dFAm zFSP$HX&?I>_6VLMH3chv%ulu5k=HRD;3r)>euJFWc%q$)cKu%bTY=YLee6rvVfWYG z^a<#x#lOXyp3+?<=#zM8iKab%THo|rQQYUZaxZXzllk>`mH@W}P5~a*Iy|!a@7DTp zqZpqeJbqq?2jPqdkBbZOxSn_n6OU|P{kz89zt72?x5ZhR&c;=}KINE|@rKYWiD_9(xO@cz#up7VlnxA&WS zGCh;Y_d)D`+TL9DSK8Z^`wsFg+TM(DxA%*}_T(TP^jq5AoV2|yxwvbh?HxYu_8uy1 z@7HK9Z4Y$J$9LbX?bvrs>eS~FcUQbxQ`7xQI_izys$pRN*ng?UX_ zCmr}(|qx3EKNLQ-mGc(W$+`s z|5z43!Q=O8!Ef|=@k)cwb>nouNP142_Fv|G+Me9kV0aQgIUlNN-(EN9=9bz= zN6xz{Jn&B_HzQYWyOv|WeYvA@<@Rbh_S=`M=E}{Ke#^PtmUb(-a`Uwu`|aUP$(5Te z^iKJ0fb_<_A7OrR!DnFyXRANoQ~HqVVL$$$8|d>>u?s#w#1&s8`C`8m-`M6QMe>91 z0z-bHwOk_urvh$80>Y~Y zeX~;gDRT8&ya?yao&62^#>!C-i9N9V#gr-G;$3S{PsanM{|2oe={lnIW0ZBoEDmv__BV@XT+jZe@r-4b4(o?(H>@9qI7FCF zLjTK@Q0V)Z_S45x{t|Sx{UzZ$)c(CJ>qw#RZ57GH}7|XCMc^mYtCOO`9aP+9`J*}CyBPldqLv*d>!w7>F0ja&r@^yxu*KP z-|9=F^2_w|u;$D9r#?L%K|epQNk3EXW%`+pjjTUYZmP32o}cMwJCB&ok4&%H_t3;| zvwEgq(|h6wC)3Y_lj&!kYs}7}wrjhAo^gZyDMx><)4mVCDVpjke@By#2DEH9tc zTzSeF<-*s?%jY4vdi1}7ys^HQ&u8Y!QxC9Qkvvb5^0I#M8;o~vb(`A1K$mzFyOp2k z1o;L(e)w-7pWJ^aBlkf=ld0}S=QcSD=!GkugH&CXL8b+lat3p-sHS!Q+t`5tk-;5 z-`8gm9$y*oehm39lM@m=lanp9f2kicY-zK1CLa>-xBMU{#^=@bmQOgDoDfbXC#;{z z3F~EY!v2(_e^&YN`gU08SB|bUyFL0i%oq6pe_X2L0B%6NZ)rUb?`@i2YmrCDi33yY?-jy0g?oh%$H>oD@m;fcGx@eMc@@|~`oAXV1LLb;H|T$7c7t{z z^Xq9Jyxh%|`Krfvo$<{~X$SPVOz53#pu7luw7u{hPJwIbV4uwYPwkh@5A3|TU$4h} zEUe2*+*ij3?R{Fl+voA){G56qcJuBnxpL$)(_K{pU+7;7pjX7z%oATv^4l*yI zp1MuRuJ@RC8ePuH@h#`c#1s4-%DpsKZo9yheNPJ4m+Q)v<2;#o`f?{rIk7K6-`n_i z2Ys(;Pe1mTe24jp@~eGE-Ir_2=?}GJ8ULkEYLLUEA=^=>fYp zMAoOs7wJ6E^gucfG&vq~9(af1CFd;^UN&!@rs?e7FWU!|?gOy>P`Q1YMSQVE_#&K- zX?maIM~oxnpj&zl{=5JCkb|v(?{)mWet|yldJT3e=o`S9V{pDMaD2PaH(|Zrm-`CZ zYwUYmg8itJIG=&MK`x5sNsur3{s8&tMkyq}R6bhl4f(_ONA;rFl@>c>_Y?bmfqp@r zi|ovcWiM4`XI^jk1pW$sRR$ypp*;BZ*X71DZ)Y|LeUlC3%eWroDcG+d zcfr1yf0o{>Wd57oo9OvK_1dl49T~sO&g|YU^l*9t_DJIEa^<#TysaO; z+|{{qurr|_zT9Fdm)V)%KLR(v&aok^lD3LEgf=E63NV-ZZ{W^``YZ)tknbW9rS-svqS$!a2S981;M_ zUwPEhA>PRIx%qfwQ-7?VlSk?2BJJlj+V04y#;4pC7lr*P#~v_I?Kt$4?ni`@lAoN1 z&}Y1Sn?A#N<|2LZozMC>6TE`vxXc-n4)&}Z?bi45eGT=SleN3_ITCBO-v7T&{`CFC zdC#DSF@E2cdpm2xQ?y?dxs+joln?hx%KTRIlj?&?Pl>ZJ-ElwD1MN(AeS_)2sZ7uN zlBR>59{t{k9mM~Noy0$!t8n-)1peZyNgVNbr{ZZkmt!zmfoZe4W7^j!3unS$cC@3mx~szi{)6Ur7D19w7H_ zDSu(!-h3`(4*6~a`E%rQ!cW$cKT%&yjI76yKT*H=JP5}}?vW)QR%a_bSs%7E<-pT% zagGlmSLp8qneW7di?v_z#^o*jL%Bd7KfW^x`Gkh9uTDR3Y4zqpcwV0WY)28FT&o1} z_I6DUf9=mI(hdE<{U_cpx*6r|J>*76sB-oXUy%N6A|J%PtRL==gnn!KzJv3tP%@q0 z2fu2V_2Cbwo-+Ft+70JZ!uL~w=djfA;{yF4`v}OFs|gS5g2TCa&%rY9@qR@=^oaQt zLGA(nTiEAr{ObK5A#>{Y#tq65%5O*cy7fbdv;^f$kKp~6@IK#9qif)Mua_2yzVYzp z0A7JV^F}Er{+;qi*heq%3`_4&e)e#HzpT&i0j7`-`6u$xx_`VM;~xA7lk0H49qYV8 zTIGYgL{4kuH{@R{_7C_xTk3`LxY_qBX?SaPzk4{p zmVJ+%^Qk2L9=pw_jtPE%&8Lo`UCn=wJ?NR%cDbI|*I@nH!1Jje25Gml@8KRxJ(PYA z_Xtae^}C9tIgiPI54Rj0sq}iP`&W)Q@u@ZL)A4Dg+o#AUDc$b2bfBBvN1daaAHOiJ zJ7xTQei+xTTt4&<#(P#SA9@J+FO>Xp^bMugAJg9&*!rjSx9Z>d|EIt8%?Fz0d@O(K ztGV){`CET0<@U$lI;YTI*?)iQ7L3#XfWP(5!gj~+Z+%%dKs*_L>+@1S=)wOR{H^=N zo<0$O>%R)=y#M~zL-O6)RzA5_^|-h5WBFSj*(Q3^`TwK&Tf>MY4kDOOp*Vbzf3!c2)MvTe^*YadnEN z_tO8YPPOzNOSdyU#JHuLr?Y&XqmS)A7SHDke<~5VV9bYWv zhV(g zS)3!xr$E1Dr$erderdc&GUX@d>a@L7Kh`u`=U*4zgZr!mVfmG#m+7;wHdCLK(nb1= z2T1SA#Q99yzSfb0HJQ!>d*nVH{Gb=`llw$S_d)h6naB0`q^o@o-lyO9vhRAyK5fl! z>95f{#oxvy^gkZV?nAivH5nb`e1f)LDV?YDLb>!$1rZ0r1L+< zEtA|lrX0N(-4K1glW-g4D~aPVJ!t%Q&QvXzoGJq)zvQ?!K21IlpF~cGM~3?^5N`^< zo8=GLf1&+JjPB)#=cJ$dejTOF)&@C#E#r}+KltzX*A~$|#Dx!6coG*QovWWx{+4(a z(;as+Jz#R#HOTZJ>6*;@5YzqE|0zb#cB5yf(Q}SABYe!Xr{7KTs%XFB9g?4?Yv7v^ z;U)agzw1Bq_~R&xUy%2}{})ZL`*kK9s_)}E^-mJ^r`4%{lIm9NN3voI`;*>3GtB%e zcyCa$cD<%s=>9qdAB~*PC-6E@J}uN&|BmuStua;WRVMJfQ_qCS8kfgp^b{YvX(D zdw{V&PqbYWWzyBzlD|gwU1j>g_U|wZk?NHm9#7MAK7gC*5z`+bKIQ4RSm65h*CRcM z+Oj?%VI9@0re|cmLg@+me()6-@5Q8Rs#lNDdVvq1?-9?cl#bQ>c!+!|{H}Tz-^Imy z(Def^%gz~qUMEUE+7YwSUhe16edrMyGxwYUn90#`f`I&zQ%#O&wuE1nXnw76qxQd5Z(gtYS=?-yr9=GZ zDoZ!V&C1aw3OA!qN$C^pkLSNlQZg@}vQDq{D-&)bJxaGejuw2qeo4!Bh4sCUzk=`l zd`anitAf-0M8VE~?t9H~iNRdG2W7luo~wKUKA#~VFpi(h)l)vZtrh;qq+IV6T+i&Q z?@%~Pt8;Z+0$=r3&($>IqUc{)Pd5l&4Z_2E)J)-H+0UBdG2QTJ2>f7&du=~}mxnxm zeRPY{-~1Q#YAi3wAD?gE2eg;iA8w!>?W_@(>-Rh^Fyz!52TgpDXi_xW+X6 zCi&;qCy&$@;_qp{MojTmN^CH0m_IUs<`$=5x+#`BFKl_ zd&l%flc$SJF3u4|?6C{h?ne{_!5*Dd+q2nb{-i;iSX-ueIint)Sl}=N6RSA)X(n zb}Ldiew>T+WjtWxbun7!IG4}N_$rIrCM{Ab%vsF%|Z+LB5nPr!zn7|9!Ilmgq0`??r@r z;4D6&N5ElAXFbmMCSR?1bm{y2_eH?xu<-HgI9|d2o^a+|Z6N6|KlL>n@1(k&eDfNv z-^l$Wn)crjStm99xOc)%U8CTl;D1A&ABKIhb^o5yEY%xf{SERB29kZM#CsL%BY&Ib z+kSu*Txa$57V3Mhv-eXj}m zuhew1VB2r-M>200pB>xv8P*TuYuLUY|6#GuNyl!MyIsLhuV`GK^SMku&tN-icM|>; zJ6NyIdeDOlWgl74Q=rT3_Xs{KwrIJyPcq~u^GBu+X1#cr`_AJ{-1qG93+L_D0?rjY zCzY(_{1bW!JxZ#ahk~BX`&x*<*iW6mh+R-WUd~-vdb_55drN@-Ao0R{O6jvSY@DokHcIzh76nEm8Ox-6*%d9l#BIk9w0&6MZ6X`1PJZ-mUQisix ztpC-m^aC0;5AWDw{sHNbbPY2-$o?nu`Zb*#ZvNo$HV&te9}==ps$W6ZNz$JXf2cLS zq;%??IZZnpH}2DPGV@SNGY*o>4Em69kYpzLyw~`;NN!Gaa?{t~IqBNKyOe(MAp4)~ zFL(#XE!|&W=Y>xS`wQ&6@JTWt^2_fp2y`t+|4w~0ti^QyZ9n3qP7mt%tm1pBKPoxMJRCQJ@z{c`lz`bS(hIh;CM(;lw&*YE=TI|}9homwwhOa6~0}5oHqy;c-<}a{dn&9PBWen{2o_+hn4RX5NP-BbLAft{*dnpX*(Y7 zm!+J1uZ!|;dX_T@ctd;HWU7&$#VV*<9#dB$=a2KyENxZ<9XE(C#=TRr0RN^O zf9w?gi5tY%ms^!9$M3BWUtjL}T)EkZ_ZWSAx$AP}I)zSwFT*`Y*xv^J0-k@fC7Tyz zd~&jIVDq>Hd7C40>c;`&2{<1Ry%p?QJWTqPBku1=?ac8xJ9B)_&K#e!Gsjc^^&Spv zn(^6c`f{nXTaI36ektvKQoWD*wqbU)<5Nrzm|g9!5!w)CHjr`02!zx9;%uza0*FvLeZUdxPL-M0!{(~CV3<2c^F(my(c|*KaoH9KJB0HH&k0W;(WNT zL3yYRm>jkEYoDgPr2g7REgk%|jg}tEUwa4X++ZA8zC&l}vy>i5xM#a@x}|s9`;#oa zOVe`RP}{?Oda$46=pORJ>M8mx@3gs@yVTdQ!ryAe) zQ-15L2z?Or<9X-9AF=VAbZBlo=PJJnA5b1U<}f`F{QKEVr~ZAXrpMGv=6A%?Gkiqt zr}v_jqxniFKi+U5%F(?F-=9NKJLvoi<=-0N!p^LbKS8Gt?`YP?&lmgV{iTy~{_x=Q$J1f12qg_L-f-sldjbU<$fFT zg`Ee9>tS9vhkRc*zDdt<7~fPo^}W}3_+F$RkKn(~l$Ox`HA3fP1^FwBs~G>;y=L;A z3G!o|?aO+SrRQpUa=#e)$N5F@-_m%FjW62UhI1Fr-_rP$=wkeQJn(N&ZkYKvF9H88 z_^GmfsB#B*_oCb;DJ^w-k|{#dX}zTB&G<=~G6{Cv52xpE89Zzne@ zN5*IVJi)s^4t>8+>6h8Nx!MtrU(hF$MIZTmz(xPB{3QII|I&IPZYt|VN}p`sh{aFS zeIph>P4|sh{4}?3#J3mlDCO`7e%}JcgXiSDei0N!zjP>n^psxpxTr{7{}lAK??C)7 zOzwIn%+vKzIj3ZNMZTByS4)%c#m-uqd>`%|PpXt7_(5RBxM6QYK`kzzJ(8#X@0g2ewC$zzkRW#&EGzR z@Qe81Gcw*jKk(z>$_EE1oROPYzf#)VGJlQW|2F9Vf{&jr^3dY<4!6<%4n_L(?d|yu z`G2C+>#HA4yI=Z_HYog~{2w>==(BI&7=6FA>p;Z|@(a?1@80@2^ntLKU5p!}9Q?qf zi*aN42~GZV*KXzOuwIp{;Cc(zXE1_E$2NVB^}D9HWye<5tM1}+?GAmW@tbtsuuY)Q zuZIZ=ekH~omLm9}o-V5Y22o3Jmtj=Bxshq$QS^Nb(EsmbA908mWcuhsS~0V)^s6eR zZ_=J#Ot|T|8NUQSIQZY;{u{(m@%v-g(cO0m{|uWQZKEN|duB(MSUT9z#g-0s zlzQ3Ibu2r2%eSRpYl9tSeYyA2#+C3hJIeMmJ4*agJL>Es=ItRK5#|jTukUY@{9)1) z_EGKaDyDth`MW4rw{pTajF-@V3HA~2zL_frK9YH)%1@Dfyi)q>;i?^7&33$fyfjyi zaSgWb%hjYD_?mpaRma1_2OrBiKubRRW9!ILU;Y)!#grnuY;qCeX5K#6B@Whl?%@*c zpvNoNjmjwc@H(+4t@86Ol^^t1_pPLQo%HkYLww^Rfm4o-(fLlMC+2H~%A}L^nfbHk z-Mu)jyOXL27eIz_TWeS z`Er#NtEHW=-s1D`(tL1z^S-bRnlB1FmruXO{01A}a`bF%FF(#f zzG1h#-Ul82Qto%o=A~$b|1iCo`ZcCEQ@_UaX6n~a@0BC2m%DSGIe&D9 zwv+iG(<~kQ5S~Mob)1%d1%GqX0QPM$-Zz>a^!oC|oPML;F#Wbj$*l9C=jY0SpJjh6 z^`P-P=yx2-IX$g**XA*@Us%hd+|g35uTKA?uR%Yhg`YRH@^dHjmFcH2?+AJ-9wa`1 z|NU2hpF5R)J*63{Ps-6VH0}9eCHxxG=bkRl%Jr9YG5g@?GC9{@&?VTVz-Pec0F-lf zN$IkR^!0Rkd}JJSS-Wq#gn3|W_r&}C0wXHtfiB^mF@A^j$e$^F~U#M`~GNXu;2IvKNIc4 zPZoL5{5aol+h3k^uwB`g$$oZOzXz?~^VsjP?t4kc>y_GEl&c;f&?|_G zb_xz5&g%2QqOyO4`eZfXp!^Q`FR||%`ttpnzq6(M63ySyl0QfDx3}a^(fn;K`Mabq zNr(A|t22DIX#SR#@+&oexFx?^^ZQ%!_Xx)cln#`JI|i z{tt2?-wn}nJ(DLYUtqlSekJ3Za`YUjC3vk^NO(4{#`rIk@d@KuH2&)`{!ljZ`^+Rh zQ$7$Kfgjq#3fu{|wrLvkiFJUxN|X9s#XMiV+rdrYy@TcRavEElD;`)({FkvD~D{!FR}Nwk9In=kkGBVQsKnY7g~A_ z)6b><98d4I{Jo4fOrLA%$m-3ubep9yih#>~#PResmY!|zXIgrRrQ0pN(9%;aJ>Swo z2>HoYIeZ?ck0yPO?{X$xi?w`e7i`@fbiG-o1ai(!=LZFI);|lMJU5v9VVtasd%}D}hn0BJ4ZK|zf@cx;w&KT~kkvNF@jp9dhU1pT;z{eYj z$JlYN4}m?h@3*J>llWcJY=6>&Y%krPWZ!Sk?N2I4w<)}2#ZLBfi2cMm{`TCu1?Umx zE3ju_f6%+WDRNP_dID{%@3G&ME06E72Y<=KT_xpu*Y+#@g1(k}eiYBz(7B}RCep1U z3r!;Wq7V2!9wy%edBwPO{(tPf3!Gh5bw7S?$R!ix5ds&&1$06J3?un5AdeyP7~~Zq z)~JjGiaM8}5>ot|A++S=SWN;-h@uH1#t}64-brSnXj4Ur3QQDQqs2C@sL?`=7He#2 zO{@M4ist|QuE#n1o_i+~9`^q{pGnT$=j^@q+H0@ZUVERtKSh0iv>+PdEA2A9iGEUk z7*aSack$&J-!Kj|0TBHA0r&+<=kn8vkL{O}bUtbQuHW}0v!Bqk?H`lOeO%Jm4_@%` zeW>CQm5<{wZ^MomNat~-n^3PRz;L`oNh?Mt37qWQzT+jG(apaTP(H^wIJ=kuevGu& z<7U`*Q{>0vYE5)nFJRMoiIiT3_vmfPXjKa9$q4`2@w!i&#Y{z`@w|WZ?z9f2OJpT;qhoyb^u15IS z^;A4$`Yb&c#`t*5b76``C*`lWWbM1#?*2W8$3use&te-thW$*vgYO%JPe_*p4r};6 zg8zs0wDR8d2J>C{2=qvxzvj^&9zsxr$mrv|QS?j~NQu2p!y?=c?*Ph>_9-~8;??<_OGQKpt!+cNX^7R2;TvI&5 zeBaCE>q2|*J%qNWuK0%QpN_tt{MaS_NhViN%D6(hu)Uy-TG#TxM%rlFgH~mGtCf)};d_TmsT3>0>WAY=@>(iwD zkbl$jQ!H)rP;Rrd*?0RMA*92uoE_Z_da+;V5fP6e^b6)m)IOx+v2B)C_}Jgg(prD& zXQbab?3DLD?g;r>{2m0R?5ecUT=Eg?%+Ato(oZW!=gV{d{*~qP=Z5}N%0H|0 zsHvZFJ@U1qW*{|bd0@|Eky$_GNfXP#tqIiq}iA2OKvMXxvf z-B0uQRhP6c^G8(Qj^~e%?qM9<*WZP4!;#m7{?!~AH{m-<;h*e#F&lrR-;3G!Bi+Z| z#vi%w#X@@Qu=TRXK%PFL`d1^a~$C-Nk5B*)QL$@@(=N>gjv=Jwm6-P54~u(eQxS%j-lhTnDT;ewk)`-(&sK zZs~4IyIgcxdYQfNu(XcvF&>h1vTB+=-z|C&@?`1A%C%YA-0xYpJc+CD@f-s1O|zvupn@4p<5Q|}f1Hc$96qt^^U4E~|; zfsZ4I=TXqdqwt4#?d^LWo>xe@=+4El{~Yp%bG@WJaebxU?fZ86C!K&hOOFAE9x6*c z>~Fue;zx3Vrrcj<{25&tCDZje&M9q@?@4&&^RShg#w#-Yt?`QNdt!}OWakxnyy8-U zi*rQfo|U(CQmOvGM&4)T6(87rd0uJ*A#u^Kggdim)(T=z{B(d2pC-xZ5YsR8_sE}% z?fF>kPW=Ik3wa#a$5-sX<*#BrU4jSo-|BP0ymoP&5=qE@o0sdik3W*lKe&Bnz6H5_ zBVoR?6ma@oti=_2ac%%F^0CCV(|E%j@;v0@ZNd1E`2YA+$oI&0!N0fdAdeG>9>Tal z_0RL&PiEY682()FN(L9{I3tkzechL3(%&|Y8(Zzm!hW8Kn%K@lF8|u+pdZXni#IAB z%tUxPU#54}&_j;pImRP;_wNj*YWWaf@1J8%@PN&3V02g&rB?{i>!srIDME7NPbAB~?g;^$~oYL6@4b;IBMzu3-MNZxGo z9b?ARsO`qamvpDfXmuWv??3bT&t!hIe-A%8L%;^-m*_rXghxG8jMT6U49u5|CF~=_ zE2MW`A063Jjx+$cw{n+Jk8LzPHu5wF2Cr<70qzxBVYgEI_1ZaF1eT5j+2b>#vVzZ2ouOBrOEdE zrY?E!c8>D10lpOdynlwhrrl=FY+Slq^cLyLYw@(-OFC>%o^ReP^1n^wDU*LSgz>QI zH^lE%U+shZ-wW0)+5AW=`Tws^jV(__l;jnThoDX%4-=Z6ci>4jKX9{{eYF z1M*&s{!-~qByssUMagWnOWX#FJ$uN7@*3;7o9&eJQ%&E~ucV!0{~ueWzSjQgEp&urqnR^=l=*=sH>sa!{T1}%0@DKz0*>3y@%&Z9hlP%qw--LdJdmZIum8Hp%DMl# zz|#BXzq0+``U>a~v`6IF#uK4F_}yQ(>JK<8xZ)x8SMi;a*v%$*DCJPbd}T!a&kzpfjqQCe_%_>@T=(yKp?CH23{Nei49xWVLnK-Dz%S>h-e^eMbd;w7cRA>!ZC{tF#!|4ojYHU)9eVx;4u zR{54Si`r}$n2mB)axIo|g_yFtB1%K9mpNvPmKm9wQKav9Ru+i20vamgb z8`qRhgmZUGI2S9NG0Q8Tu>1#FmOod^*OVVh6I4Gn;Ssdw)|PNENs243DIe_%oTV+{ zyi(y*dxUVxO1Zf9oam$G zb6=?Svhl0Sk?$7~(wXu}`hErWG1ZeBq_~{3+=2PsaNHttRnhlh{9)wljdK44^=M#k zCISxg&qPtySIgz!NCm5Bd5Bw@EwW4XS^9+(bBUgx{6LXD<=N;y+XTpjTC2 z>9hv)Y?r1Fv-ca@HSOc$K!32^m~RrkWIdA>HMVc3#8tK^-&*;2qt?g#Ki-V?1n?F~ zd}yoWk86qt^F5Wz$MfxBFB{-8-*)EPsCbi)x*7K{K61J--?y1B=)bVPuwGW+hI)7) z*AE@CKZoKO=DRVMZ@%PHJi~m~=kl#YzE=AG+FU+PsK>rnN_OqQaQ47f-}F$_MvaF?n3CgUh#Bz z-cH9q9?JUpf=h}2GLhfj!YlrV_1E2kDE3JcKQM06GyWa6&;RLn*xC6~`dx8e&xG`K zx<3c{hR>buy?9=XZWTP@+P$jBI(rnK+TE7!wzTdOjQ#8_eVe`SkTmwWk^bVoKKlb= zFZ|L1?(`oJ)|5P~DBE9Xs;sNP{<888>zxE1&XcgT+Kp@Jrn<*zpCdY*@nzc+fjl*{J}*Ke$Ql<~Q|e}c(*Tv!h#dW-&Wem&}a zsHZo2ljWO|=39Atqdi_L`OM#`wx#*<>r+20`7Sefk>ulfLA+T1_MAU{r{urT^3Rj} z*?LX&_i$eQE@nsubzM_P2ik#F>p4StW4+;ePJF+s`tmlZR_x0cus>Kc{ZwDXcR>;0 z;=B)kh4rsQ{q$dUW%73X^;1F?DKUSSIhGdj`#bC(9t0ts1kO+lhvai-LJmL z;=DHtKgPEIU{bd4ziQ@RblztC`Yz3v`Zu~x3F{V=-ZQ5O{N!+LXQFN(>Qng1;p&(8 zdG;J%L2UM9`$``y<^Pa=3FUTge#sQm;}3$LiqX#`ewgJ~{&PKDjD9Wj2-nr+{g&tG zj|@n&#J`w{pR@;S{)XS5b*O$Rt6YWi+G1yyYdM$C(2uzT<->lb{dT`K%y%2|jYaRU zKKd0tA03V}w&T2*uzmC=ygir?l71Ka&wPQOAo!S{fb$a4e1FG$8?;^go>z5O#UpB55{w+;D5<7^u;K!P_pZbpF z*q*g`#Qr@sN9V`Le?flkOV9i+FRx_&VS_g*ii8h?{7cCHft;@?P~sb4y1@u0yz)l-A*cDFdspz=$yO8F<`lgGuLy1%3R4Ew9)7cSEK(j{^a9H59FZ{Y?-z)k&ir>JaHXpDYxF3kPpP2m3rdrHz4jl= z3+y+i{aaIcpEdRS_I!yv_k9lH^!<0_eKDFX_lk{|JuVOrDZS$LMxS+NKj$f4Nso3| z(xv^`-_BkKpIJW^qi-8rv%mF4YL|zW>D}WC7c0DYXo);eE;hKf-(&yr2Dc>Oz)M@@ zM<{0^w~9uT$`|;MS6o;Bjr@2&*8ep1hv`@PHM?_u&E>gq(pt!G-oGN;pS>4wA@5RO z)#84KYP*=f>s1-wGXK9Ie>N`P-^z9EmhyWk_ay7_c2=dB#DB?Gg!}l&o;`27U7x!j zTq+c09FX__;|;2h-)8b&+W%CM+dN%EeJ6fHpueU-CRy(X{<=|r37;=lJY0V<-$dZ% z_M3F+_&_6F&H%kBXR7yqsD6$4^OOrx%Flhk{-$Er+}{lQbw2bo>-(b6Svxq*Z~Dy3 zrGBdUgR#*+DQny*nQL*T*%n{w)VLDy#9!2JdMK=){20=0nc?$6_yJkJUn2wx%WX!! zV)Rydj_=}xA4-LOOKUv5RH#_m^tH!pJf5=`d-w~)88iQRiqTi)9P2nNZThn8-?f;Y zO}}eVyqta^{`{_$biciUzkM9lq5Sw|K9XWj4E$#SDv$V}4w4e)?C!W1d6XYxSmlwqcn(Z=f6ag>;*;ns#8d*bVYK zRah}Xa2HqBZ;`A!$;L;!RqtE;75k;gd(3;;bEn55d%n~2E1Yy5VVc#W@Dgh`zH^iO z$y8N5_zu$2lD**^P^h6j9?$i4C!wAH1d8~)VA%d8;QQ1*xqO$L-ghEhjQ&B|72-p>VZXZ+&h^uQ zhwW$F+jg+U{W^>;D~&(fHQl*P({=ODW-Fh^L+YO-bCtjMecoc!`oZ;``6m`vO3Lbw zWpZcm)A>NFziRlE4Zn1}toVoRUk5^mc7f-eu>ZP2FY-|x{gSLwKPaxcyqdpMb-7+? zcx!#W4;(=e@c_}y!Fhl*7aE&-xSb4rNR{OH19~^ z&-c#@`TjoP`4)XH`o|%hwetK1fkS%z7su%aFXW$X^fRi+jEm#B zIkuj*18J7KDyI)d@IEZh`7QEam*syG^@7b~`u9Xx{9v8v#cUk3*6geLVch&yX79XmwXkrr`zoLq35-V zBdh^b=*MBX$vIW|@?-h8U7*XrP$F_MP+uzd5D&2rH!0kBefMtuBK>bazty0RYDW6ugp2cO8gX(;Ni|>@>y_ZkNt7+U`{X+P2me1*}dM?D9aRVPm zhtFHtJGN(HPpkIaZ0*VP`lIV;A8xbf<}U_%|Naf3AAGgsyAu3OhVPy1`x~fymI@x% zH-F#cI*nJP{-n~!{Zh(P<2oPRk2K@o=J>ks!+JqS`tM~B(0Ew=r_!WbtvzOc%IzYb znSGdMX{9^j`j&S4(kAJU&Z~2Jl;>7a9$kKa^eF7XQny1|J}JAMQuvS~t4HgHoLO4& z^L;}@z8Ak>K-(F{-%jEAWh45W-}i{$vE1`1{mBQN%(v0n073~Py z&3V)?PUZUN(OkZrIejI3Nu`O4^Plweh|m6Y@};hirM}hiqsB$TdWk3V$FNi3bHe5G zr@@!ubLNk2ok?787x`no{DO@6-+5^(J#z-plXw{MaKF>n7x?}#FZKLEh`0N*Q_xO7 zAGlJ}`H{G3epBa5vVEs*9wgn*TkTVRezPdif=0hj=l>R|o@(J|`@F?E(770WN8-wH zRp|I=H_l8vOv7e^Q&ibi0d)uYUZ(3}erai6 z7o+Rb^Zl8pE9rLl7;^F+>A%OMe*c~{TQABe4gH=2#BXvx68?nbr(W(0#>>8sVyk>T z4f!B=xLW0#7vy`v>nVfr9oJKa>p%B=IanuorJxawQc2_T@9P@q$N8tz?EQs$7kvSh zg!rrY>7@zR$ctpv4(0cSZQ@rT?xX!x-m3guHUFo)#nS2rp`Di2xCqAkme#lk?7F5m zvpikVZ>{u8<@j7FUurv`B^)?=0qcBXg^l8gsUBx#|z_E!GCY?mb8SYbZLfX^sg%)u$}K{S-xG`hwtH~{!*be z-LTJ!(l>zD-x8h%etn%3P1$%8$u7>jFKV@8d1#Yb{^5`eq!Xbg3OE&oetV(bBH>o)f;=o8BXzlm2Hz zzkNiYf-fujgY?O-|9BJV6K_;JvbZi74VTl&&u1-0|E>DEs&ZW_sGh`mBi5g@q#WzX zuN%2U`r%hn{y`K&mUQ3hQrluFm**?zd$worlVbm*{eB(-@K!o{|Lko()#AGELSUz| zVYQSi?eFo)8B#y&w$i&i&+NMEmpPVp{i5+rocm+XRUi61pU+dnzt{K1SIN|AR*%BV z;*$y|i%;r2x5xSN>k3+Z&wURdXX`s27CK`-Oz4IAFs0Lw(QCc6^D4zBxeNqmc{^9W z-_GSNMq30<*e_%41GyIE*-!mnYt)06V|*5lZ|{abjQvN2U#Nd)TDfajE*RI!y22W< z(zta*du0AT%dNz7*4N8&Ba%(_;pt|aVY#M5Ki>1dK3C5c<~O*`Zz254kiUQ7oaYEJ z-w#f2#1*AIK5lb(#P2_cLjBVm?>&TYQ~prAi;^1?{P9(DsDou0T1gA8qQFlSr?s*2DNrzF$~h2!DpqYl`rFSnhDrxn^)C zMA+F$%X$AFldFgJ6YE6p1M@pY+#g8$ z72_Aft6lY7b&93cPKWK4ejE}q$a#9?H*a@bSO0_i^wMJ|9WH%@ew*@5Ha>s10AT;{ z8mQ_zg%VcjfW@Qm{J|LgU#WF9(dlu}-Tg;?zk#MT$XmC(pD6J}oL4CFzxZj#XF-NJsNzDeWI5H4xYQBpj55MW4${`aXI z6F+)NA$}#`$NH^{gucE`hxysR{kERR&*@7RD*q&_oG-hSKM&Ax3(N7se#82_+-LWd za%X+lU6}dl7{`b{VmWE2oxAJj1R^dV<=l^QzQ=sO^f%(8_FnrL^Ez7J(nWe7Tw?Ea zT;g%+aQ)w7&Tf4a>vkA5gPf;u50rBKXHzODhed^Rwl4IE z>%WtFle2XmCa(X=(kcy$&ChcGI_#G==nK;2P0-(1S1GQjDbY)06 zr0au7m-u0c-0eG9?6*vQEKNF8a)^> zsUnwH*Ch6m^WfA2p1(AqUFs=D8gFTSe~i@c{$^J0Cx8O{pvOqDV)T&O<|0zuU|tsRQ-|J9p&#zRsDPSbFW9^+X43$yL@* z-!lED0*CFu@I-#|y~la{JCkfbZq*OIKW#7KguDi-y1!@|*Qm>5+3$8M{l9=Ow7+#6 z&3>*toW+HD3m*{;md_vh&m8BozvcsHmVaOi@U3dU^p};-aL%dt;coY_-@tE(5xyfA zyHcquUxoFM0O7pI7N%>~4v=j-(xv^>aVYIKMfAP%XJ~Ian9t$^T)zi2A+Hlu&p=Me z|3%EO@e0ea{`gvX47q=U{1~VXnB1HuY1)-n<5w~IH+^q>Qrb`XBtA^}GVy&-4pV&n zyUqA8)q{!cgAyM~>_8V4E6Ce+;X}$V`R4e-QG$Pnr`XMk6chTd1*+lrRmNch`riFY z_R}MeH1dJK*`WD2zF0>-F@7HH&)|2FKSs>|gdapyi;?^FD}`s>-@-nbmNt4+b$%^t z-xg_KrZ2xP`75^mwzn`(^gzYV4fF45aPGDALw|X*++AO%ej|+zt^tdm(!TS;cx;#4 zZGI%=x4a)NQrIO&JpiS6=WVErPYd#U(xNmC9w@v9i!q3`S6;=lLq_dV&~xZY=J-JfT`&eQDO z@7up;*9_FZVekJz(v|w-mi~Jw5B*^2Z&}*bne3hqU4o{95n1uon7c4^-` zq5`km`xjMD-6BcWQccs^;&t`~x-hx4j(e`Kr{@CmF;3 zVZ17oYw9QVE3eXis@I%vO8f06eomJ5aaxz;ugs7e$|)a^Poy7eHg247l4K}N=#=Ng zYZ0Clqu0rErZ1B;Hl@N9>S?Y&4C7!T56dN+@bTy8HOhhLr;5J!xEJ~TkVgQgs?Vwa z2*>5v_wyRPE`xgx)&n=|CFTE+j#A$mEl2s9j-RaWsnJZniZUzBwE5NLD>Qyuov40w z`EpCEKcAE@vb2srlJYr{_WiYpH|e{$Q|Ws>+S88oEp{&z4n~T0=^(RD2ZPp{rsfRw zz38P1b1U8lzXtECnkJnj?s%=H<9ZgiI`&Mo$Ik8WaY@KeyTMNt(_5Ln*|i5CQ+s|T z63DhEkAE-hDpXDy56tvApK9FYZrqE~Dox)mX>w$W)pFXMvUoI2<@9V(hn#MPh zPFufPx46gb1=6l~$l@V$`!v1Y@Ed2}56(leVeyEzqo~`Da9kjIBpHmfJymU2van6k zA^-G29zs1Q?W>Slpd-Z0mh3h39j&s%TO5{1#B?-UA zb>+Kkf0uVDAE)bBbbV;H@4}7t-t?FGmrf_AV=+>P_zzW2G5VOaqp81F$YbH-5&UF7 z|MU@#bAM!XIaSMT(0t^Zwcz8iU zj2G&g$&acJGCpjVf_sx+!M~#LDPIfllX!1MyPN#2HJXp_|GA2GTiWF`EWd^213Ga( z$bobp>NAA@Lijr&?>L9R=zNY;R4SY=`9eNigg33`gI|s3aZUL#i^C|LvA>4u$tHfi zS)OO>4{o%y+1uK6mTu(NkZ(vo(&Hh}qZrK zzcg;TxKsHpq@VP=^%LuRvWI-7?REO&yGfxR-#>GDqdud*%;&>`=f#3pNLTvV&GZ8h zTw!~KA2-Tl*;k(Z=6u}BU;ZxEoi_93(F(T(|Bz(Fa|!vUYW09v`FiO`hV+?2`*N4& z590_Yir&x5g~qkxHK+(z8b`44zt7kB{I|yuLVRdvi0Au2PwFpT#pq(HS;T?=aQx== z-V)Rs;^qCkknce^{6$>%!Ha2Lg%}$j^i; z{u7;ET*c_mwTBX51dIeeMVD zdAPB^$2%Y6*<60yUtAGuy| z(`l2H2>74*;$(>Tch^I_Z5xr>QcH@6$Hn-K0do+UARt012&;PekF<&Bh|kz zc7Oj?`$_x0(LVtmF5i@6Mly<#o!?pQ5WXxGUf$FW;paeZ{9L5}L)vw;t^ZUX# z*x$m^yX^fYOY6AD?F`pXE(+FzlOM_r^1&z-^oZ$Y=3kP_KTYzZakvnlm-?A6zdrV` z2Dlmz3;X%qDDV5|#Z|>Ou4y~sy3uLK+BwELSU}GDJvhcy^ZUUluwhT+P-8^ z+skroKn-?R`iles9 zEXRC#y2N$mBbNUw8ILPInf_3_mWLA$8{KbGy0iR$LKG?+bw0dz^0DT37(L3HMSnRT zB%PZyT{nK7J)-F$tv{Ko_9z+Gu6|Mw2>EZ6^fozmy*yC6U+7w?KPLD7^qhi+C7qmE z5;$@F3zpV#L|p%jrB#0XoPvxWKP2VC`0Tn|oJRDA>B-RUPJFo0uD>f+?tz>?^!tYz z^DiPF82v-I&mv#{;1Z?hM$;1}_w1+7V(1?B=iPt->L+ZD(a+mY}0R^Kb! z+>(#p+8W+Ql}Gn8_V#_jjVG{HFH)R)v?NSJ6i7}%Uk1NeA24F z&I|C!>hEX`$LP4XembiqJjDy^Nrj&K(odI2KS5#Q^7HvZy3=pKdRuu9|6SmgFb~bk z_m`ypC4{~0FG)FS^_`vY&3XYR`&x$m=jY~(-mLE_S2u88tLpl5iuf0mn(3Ppj**OU z?IG1C>3OjaSlZ(ys;B&1Mb|SSKW_%UtnVJsCwxx3z~}4nycqSV|5H`@kN2{lO9#ra zKbPhFf%n7C$L7Drwhu~a!ZX&6uIKo}*M0hTaIv~WvVLU`uH-by&U4PJf7*3Cov!D& zKr)2w5Ir+udTWK2XS-h6PW!F&?Jej4)cp(p&Mxzpc3At}{_M2$R;9O{kC3l79LH`4 z{%JqoZS7dEei88*oJD`z`@6&Z$*)R1A)m6S+oNLiH}XDTFTS@H`nWzP{AbYK%0`1f z`R<7P9?sVael^8w67<*o^i!8>n)ZMmCgGg*5a>3nennWXj3bS{Vg6H*KUu2d+I~Af zF)Sx?5!6e2=YE9u^Joitoq{Rsj9$JT<>T5f?oXJVB;J3!7VV|;go|?Ahc?by{uW8^ z?fmI!#?S3{H;y|`1wZ?_%UJiV{A+xjox`jigU9ilzYj6G4yZd6#nT0iY#*7anr@<# z>0OWO#Y4(JTqpiEw1@5N>uE)oiv(c!UC2h#!? zyn%WwrOU9kW7br>z*UU?Snjc!|L72#kMVh_#ikF#{-WHHPT&6o;2OWP9zx8<&*utU z^8GjPtG_%X{v6Z1iqVO3Pwh+RL+bCjed&C_(q>;e@3*wsmrjLOjNC4}UUWa*^k(X( zE1x8(pKgA;`RTsTOE!M}rOI{9;XI@1y2E)|(?bsDN$vk}{r#8c;5xnr*YT|x+)f== zRLuYHw{}-*O0Vo3HKkW}4!qJUJMY-(b)eO+@WXzk9%R2>crWdXIwKqQdd2b%+_?$xy|w`oMce*CkH6J1VIxb zlw7@5?{l?0))#2HtaxGHDNApWd-&aswAx=&%?JxEd`->O*ixGi8WPcqL z_E)!58~HvHzTbk|WnV8_DyW|j-=X|4u%<%_mI@chJ()Z2H~f)YJzwte?d|eB)LSF8 zr{<@yo$u0djM&diPn|90;uXq&`SMBU9Dz?gG}e9tH@Acfrw~`XLixKHZqK-IZQPZ^ zcZR}UuJmv}j^q5U+`hC^63pX?RI-0H?h!BV((;GOO~!wVa_1LKZYcK($(YAKr0c5b z@XLlZ?$tXfjgJZ5NqMvSW3|ni?%bs5`X)`!9?|rW&etJ6Ch76xV>+)w`=W6Q*^f%wd`M;XMK|ekKdmD~h26Oor;X4|f%OUlIa6f{4{Y#f9J*&#M>37%xp4{i0 z@^H?5;XISJKWuN~{KX<*)`&OT71xxH3GeogihnNj%;bxs8_2=1_CNe&d7ti|I+bsm z;S%10_Yz)@_;;nknG$zR=^L~szrUH&doAD+&P_S`olLzD(EFksy>HCr@5A^L`{oP1 z!*WaD2ax_Nt)4$YxnyaV<{M_Kz?)#s?SSv&ow%lO!g6oT)u()9^_l((^Up_q#L=am z{<6-)#Wk&WtnnN6p^^N~4~TzAn)LbYz2pP6_t;lbo?j1sZU_Fi*uA%KjA#;khbjEU z{^eDS)Bz0nX0tq3`;PB8Rldr*)K9nXDlji(Y10G#9cOumJvV)jeowmHo+}*J4~UP+ z^K_j?Rm-RAG^$$8K<4-2R1 zdgI&YXnGQYCb;&dCm;Qy#LUiv%ykyCfa9pYlvTJq!A(797ax;ivJVS2A?*sPZ1{fq9auNbdd+o;SYdenfwtVf zYi41-KOj>6T-s*$DR$o`^h}oSlsn^@ELEqB>{w>{q__t877T&wlAkeg6ndOx0o zF;(;V2weim&qttsVLue3Qw6iq*~{Da3Ww!pLvgr0?Va>0IR^#v?ba_U z_i@emt5fAZt}8zzvlob*$3v!<=c?QnqvNICN=@Y(`wFT&4XOU{eVt-E*8%co{o!^6 zh~Z+qmcK$e^npLw{!PI+pY0fgJjZq(Y-*Q$-=gXQkxT4DAomcS_v^;l;G+@kC$3AN z{1u}!hz8`}+FRCj#~6nzeL8jBaa>n9N@nZ2<9NvAYwmTb2gixmV4qFZ8`Gt_rg$mV zdm4RO+tX5EL<)xOB0pgt1%+Evx{^*`k3|5<#J z?OD*pemX(u)7-9Z?eChk1D$$5xS1nM!AE$rwc!zrKUh2IMEB?fHHzvHX&Q{I_AB+^e;}e7v7rZRb3;*nd;_!1|GRp8*sR?-sc!P5PMO<8meD5BQi| zm3OONN&UNBmNvO6@04^%x0Up3f`0aX>p)Gc|F>`?lch?xY#pQNud2d_|6=uM{k{)1 z;@#4Z>HZ%9zqhe}jNjuxelE+&SvT^z{=vSn!k5JBqFlaEZe8Cq-}%fJj01$e7C&=& zBwu$~d!IskL;7q7AG@9j^KH-NYlGf!c?oB4uviyvw7>Gk{k0poZ;Md*IST9h+9 z*^Ya_&`vL=pZz%fFa%R^h4{K&p1}N7c`bU8dWhxrpd7!W`vhKPe)}}Zh;fDb7q(9h z?3txe09XI}=ndfh=W>JoAe6@X!+r?+>)>2}jio;)Gv7Yw&xys7y}{(^_=-}sh(@}^TK|@G;o7HUzhQL z#{Dxs(fEEgj$Etybg7Qp!g_`O13gZ8kInA0-upkFje9fu*9okMkNOEXA3)$Y!8N~| zaOPs5hxuQ%M;IS){ETtC!pX;%{Cpc5=Y{hau8;1)c#h*9!YN4fRmpT~pspGy6O z{L7KQ{|MDbK5p{3VHjT^#}%Vrh#X*^PVkN`?iml69Im(if3xbrT-b8F7BG`iHZ-A5Cf z?p!k1q4)-RKhMvCuldoGC;xuorw=x^>o&++=+8U^|AzTzqWx^=k8=5!{1*9* zzeBiBB47W~Wx_9&>im@7X`g+5G}O1f^4#Lt!$$uY4{>put7TwB_YV;VkoGdJmH&QW z>EPbu!RG;<$I*(>eFA@AV7}Z-g&Qor($d$<-NzFg7q$Zr_bV7L-+~ zF3LygIHOI|W%XM_J|g{Et#6b5GCvk^Q7u1virhzU6SSm1Zv7nflJZTamy~d$OEe$( zcQx^>S{itBJ({mO!#*&=cfO7+=~TYV;!X-bEWab?_Yz-@kGJN=#}n=Ye+2b1pSQ>9 z=Ix2sE8WOvACmD#zFlWY1Cqhr;>QkDZCyZF%Z2qU0A0Nv!u$L^;Gdh=d(_tptUn^T z$0LiBt^uC&QJ&@QmvW>Y%59vl`G@+b++OrD{K8lZ#}~?CUDkyYYy&AL9kV3-eVed~|&pcO(4V`gX||+rGxw zSIW|J49-qTFJ84r^qTWM>GCp+g-a48Y23H(O{lLJeOT-u+lQ$Gq));B_|CYd{e^vL zgf7`S@4uFGw$A%uOLJBibgzHG(ij-x@^#)t(MsU!o8__W3$_V$I-ZozKD-L`8TBIe zJ=nz(T)%;-dCF^=o+@dN4_I8F%G_uR;I)!V=VQ|4_&=k4&+{#*K)zGIE8eL3de%_@ zh^yFwj&GLd8V|12j9%$F)yI?`>3pEBm&w+{_55U5PUI!~6{YC)_LLj_>T{ zee7`)+Yiq5w68}B`I``&ziBTU;wq_qYR%`+UsB&_YKPjj{vowT#C!gGL654SC-Tt_ zdSO07^VPIG?ZS?nU08f?qg_}e<#vf)$NIc9PI1aeV?K@#9Iv>h^dKIcxqR$5_Y*=t z?f6{29_VANM-+SsXFA`HSbM-E!uQKH9oG~;`Z-;A#QdXQY;6Ayv2WV$FyEJR`KCy{ zO3yIg7jpR`w8QO~uXkpggObhqpOx{1j{h?IKSK}>`^Dw#-H`Y2Is1!nzV|WIV|Jt% z>A0$5`WkU&kvGKK^}gQx+Hw5-Vx;4tf$9Z9U+2$$UB;1)KX9J9$Z4wQI=fYF>!#;s zcWHXa^xWJIO|LgScXhj@eZ53-rjDnQOVpoC-mH4CD7(`_&(w}-$RF1ZmvpwiP}dP= z`gEebA6uU~ecw0+eLoHQ;=4P6L;Z6EdT6ZiRyLn|ndBcmTe`F1yL8$u!Z{#-Q;a?> z8L|J6(0dd*iI?oBhkMxXJeSV>TK0eEv7pOHkJOj#$39Q&0roZ4ey`2ZbZ3vI>phyD z-L2`NZcWealJxlT1+_fM;4Z~C7{7EujzYUF__i3oygnx{2jz$O}!g11h0GW+n7D@#J%YFQEg{1xaOV@iGw`{>1kAKpSS}N^HuAc|$D))TOi{*HE`;NWv^^*2=Bv;}+v-B1&m**kBUWF$azg{Ts2~Wob8rZ7T zu8{V*{*G&xOFGlX7g^fmtETku@34H`XT_e!*1mYi_+Y*1v2pB`>*J8FbHL}M>ywa6 z_mji+QR1@pbx8YOe0z=+I0M$6O5Ny_ei!>XrAPW*?8TP$@zS}L&X1QkxN6WAJ>fDovOzxb?uM-RL?8@2aK-$Zf{S}{;AZ|ukJtLD63E5djBp?`=QaE zVqLk)?{q09{v`P-QM93VA6I)=&E7LUoW(J$zb$T>bejBI{1oeFq}^fvE`WW=+Zp<+ zyL0~P&tTWX^&Z5a7@a4GV0@x@tJ{J7=TvX{cPHeR2EW_pEnTksoWXFOE!3kvp2ay| zQV;oY)oqRXh4vD01=VY&AJ~p5cTQGf;eVE!_;Iv%SRc_Zpg*0#-=4zXg71XFc_jMN zzCVH66~g-p^9B8}R{UC}2j4%)_ajyxl#H}%xu)I!2>svQFEqAm0sWM_G=G?HUM}A< z;OF10 zdX{j0(@p--bD1l(dxfuEKH}QllFszs2Q2OS?>0*}%1?-|x8v8qH+=5xxC+mUk?v91@^fDnw_T31bR9qRKj>xzynY~%=&JQPozq^pnzpqMWZ_)PwUv#k_YXVNz*Npr| z>G{NmDOX)0m%W9ziC>KMUXu3nw;J#t1J2(DaI*fTSH`%D!&|n`czv}cyw~R7E!k&y z54MDNK@Q%ceTMg$mhjHX!CSD;@aiq$>3YzNetr84?_(|D%@F+dr9aoSEPr%UdFP|g zL0*#s=C{NzuC0{kl)F0tg8a;Jldm`6IRL!){AIXTYC8Vxop7YhTJVoQ9H5>PI zwrRR<{53n$^pNs%GFSES_~Rhe(Usci!uPSkbN?k^`Di#^Vy`xi%V?on?Q2BM;C@@kk5kY-^2sLvw-|j_^epC+g^t;G zJ;$>e;HUEM_8aSiC7-V=3*nLut;S2D7j3-cb~Uc6+{Qy0-(G_KV_Z*W`b`ydSP$Wa z{C*1f9pfdn7q%|17v~?q^#}gW*EXI{`P#-`#AOQ-QKBOxWDgyx}Q%Iw!cICpn1yod*SOjIX<13iyP$m`Zc-yl%rOB zeQpC^M;aHgeLoXBCkpF?L{GRsnyq_Py%DyjkA7FK9rWuua(>-M7{9A(ylvKzPDfn_ z=yE>mL?3VU89(U!PSUCL>hIL~CYQ@s3`lpR^HaVK=GZmXFD4gpP5G5_^XNUHJbpj` zgyW@a@uZbJz8TM59*faAqL4#6jO9N(epf5Gc%_t&Yt8;X;b2@K{6T!Dt)YA<-7~qm zQ}D(*ZSzCAC4XiIyEJ{M@^h(hvgFU>8{4`(_eWXZZ4&p$*Voaqe5MZ+-*`y%P`qCC z4&nVN*pl_sKW*sSF0=m=MHrh?fV=>Zkd!`4hznzcvvObC9Me@ZpFu{2Qxm_{4U>n zlMm{Bw(Cduu&@*zEDdVat~1dtx5H!MU5q{{<(vIJi(9NW`)u)&Vx*qkcyKYFFa6(J zSSj#bpC3Em{rz5hK1cE$yGG~dLpsh8KO)mJkKo+8a2zl3faL-P?acK5mzZx@pEJHd zdz{4=)Sn~$9$>zzuk+|SpvvU7hV<*_bA#|;+u3E{1?arQy569XHo(%%Esb#kX(CVgr5com~J z$UQ8-8s*&%v0vsQ78AaI59A!*dx)Py`R!>~zoPR->__T%%nPUa8rPwye#5>Kf*1PR z{HE2GR)2Ky;CqB_JU^5eCxd;GkMJ1@!MH}!A%1y2{*xRZOFu7{W=GB!Y-eANkA>gO zABt-kAOG<-vKYA2jgUUf0FH;EquL9=$rXL z>PM2k`nBNuV&iAU=syIGulG#% zb2v%s@15LiAL$ACxs({^OZoUj5E9pk(0j+>UYexx?s$iCbjSyFUiP2SA6(za{Mo#p z!K>ofqwKt>jDr z2YFI~%lJp*EKT&)Y*9F_`hH9LsQ${Ahuu&Mp^r+k;KS5|)YIHB*;9QJ#@eX_o!uI=NxZMK*5Ga#Yx!+FrlnD3K* z%=8LZ-GFX^UnuRV$YYELGr5}&6^QSc-(@h zocAQ(P8NIlHPNqOKVOfUTg6$vjOT72iqTi3TnLx#f&V4-hwT*lfKDF2-SJnAdM|%o z#FL<37#IFQF2AnN)_Oud_zv>*uUaJfBgBjKyWVBH?*?nQUdz6lwsClRp2iOKBUAmh zUDK)DZqsxsw_7DWzTB#U5AoY7amVrL_N5#h#*?4V=kl|E;#xD^J}Y!vC-@K8xj9#Y znaR(+IA@4g*q`ly6XGxO9>|06_1#h|ir1^1BHjNE-vc(=A+@*h63hQFz4KgdaxnFh z+%b;gop6pB%*J>9Nb|xvFx6|TRKB?$1%Hr#m2bxXyU$dalKh=&53$}y<)w4J@UyRD zNjlpljrFf~*L8%6tuI6z(4Nn+`#ih%>AmcJOQh)o)K3ZNcCW~b*}bJxG~dz=xrh14 zA0hooZ_;50LI^l_TKX;1GiDd6y6*c@(2sQBdx#+9S_$U;+>3M5`975Yd6G}%!TrW! zq<)#NvyAJ`|0*YLkK-Ygld-Si*&k=^4{eCH0xv0^A&{?2l{x!iJ?AzarSm^`uNJb8|7*f>?-zA zP=1)Nc#XRM@;ve)<7jVwcfBRtbEKfJe=il5 zNjlFz;X3YZTnAS-e{R2b=lr=%!jG9h_kTCP_n4OLOUG|PKP+uYFJ-JeeUeqykJb;; zkEGL>ey`e@j85IkH>v&X(sXJ+J0v~6{Z#!%{Xo5u#T8x}xg6W?eYMhcL~in3XU9JJ zy+f`n~z~jpg@}BIJi3!cJAH+K-t%YSy#To>b2& zU$OkNCxm)d&-cyz-gA^cQor|fOS|8DvZeR#_ZFk+(t)A9yB<$6`!m{Pe~B-?ZNEn1H9%nSPjNV~8iq|d{B za<<;zicW8rjCp+=#+!eG4$kzjo?jdP!N)OQ6n+WiXaO+Is z>I><7vOIVH+{P6EO8LUN9l-$YrnG8;6wpBn`h!^}8@SM;I_D%B-YU#7=Z z&sM7MlIP9iin7qx)!(Qq9f!3YoTqFxKJ$6Y&@O!SFB|pdcz(cFa{0w> zn%)TEei8Zl%@26~<0REb)Tf>F=bFYzHeR?x+Z)PVeq4SM9{;ECnkSBUJv;( z91pvl!}skXmxQZw_x~O_+X;~h+wXC?d$2C47=7p^;p0|z-|M+U>iJztU-q!aKWHcC z=j`N9;lq$m;J)lh=Fa&A^ZWOOh=*tzV{KmXoAf*Q(tfHJ;wziK6YhQ%ev$Dr;rG9?b{>1d7C}&wP#_x zoB7$^*W%vaVSc0GANC9JB!1@zy-*>pP%k`;bYkUv{E)2b7J9_P=SX`hBNxd%ggft- zjecPt>S24j(ID3MYRdJh(|^hOlBFlhy?`#kEble&d522PV=DE~p> zOW)U%{;0^!bS=+w9Z0`o^iL}1RqfYOVM>7^o_M3St5kSER<_~%vk9-n^HSm8qLbzR zjcAtKpNRgK+;5kiGD?NFP9WS;;qbz#a{n22YG!&*fv97B^n^3yenNqfo>Ji#(OGi; zr$V>fr%pHE3f<2&Ukk}rJz9eLi?^n5<}8{ZlIH^4XOy>sRH zw5I&isAtMi({qlW;IB2oxk%PY@091x-^X^`s{Ls6Ogfc64$4B)}JD%r5vFii+ zo_zPG!jIC;>3OSs{vPmSx}Ix|=BFN7m*ca3lqX(bdGN38CvWgu<$nwE$2G;bH(Ds| zARj5;{EP67^S|di#Q1A(axn4>m4ih&{I){B;T$%hcQy{11B}5BwN3i`P;K9M;~>X} z{v$POG1{qqQ&r20UO~V6d^+h^DX|{qgV@@Ulx==^y2>TaaZ$f;Itt+O{rW1?RUh=1 z|5Dx$Y%sk$top99vFA6;Uz&X81QIf?D&OL}FXb0>>rOl;9v}ED-z$INySel|H5uQp z#QVhybzIZGROb;YRjn8MN(i0$r@DU9_+Vn^^u<-hKk@aCRc)Vtr`T`%j#R4pofhfG z4k#54*^hLDKQHi!$C`jYijl4(8%y6HE~9!Z)hh+j7e#O`uW;Snf?uOwj4$I_m((YA z3Hey>0<@p@?{6-GlDCWcfnQMYJ@p*cLn^=2RFC*LHy)ZJ&qx1E3o`$S;787%@^b%? z@KA-!AM@_}(^F3T`Z#{zdl!M1%+RN!Q}s?cc^r5UZx)#~<3QoRQsHHLDEg&B*Mv^F zA8&fKdxGlKNqbbUo;pGGYS#pMz@odGz|G6&V5BBRrFA-k9{09Bl?+g5pK3(JR z*D*$)o?yO?{6G!xLilU_e=8rboq7JkcOXspZTsJ?`>)UHZ!`Qh<>m`gF6^J@a`jRQz20Q0`q{}s^}o~ocgB%NmcRCg?5}!5z2t{oXEe%r z*bn2?gMBbn518Duo^GtSDMt4+(S6+*e9>d=`~GKC?`+Gp|Ao>$`3dN*;iV?N@NtF9 zO)+|}(k1nabiHX@Rr~CADH&9K9{NKv&Z(ghxTu#njwcIH*61^PK7JwDE)L zZ(koH_2$l5dXv=4h!F4}HhXP$?kz{kW9Z4h6UWKz3hjRv^1FQxpXdFswYCoVe5q%) z08Ew|U)=;k;v!x7E2R5&^K<%EW&BIJe_HJ3cIC?% zoZqejUEw$KiYt0&yJ-JB4$64RC|+?q#Or$GZ^!)>yO$=NG~p1WL%eQ{$429i z$?bYxO4Y}CwvH(s2Wniz`N!8W#T%4PqlO>xK6+JSeKNla`tjoXpMMbja)8=tUx(`W zm(W0(YOG(C67GMp-0$xIT=5psUbOpF$mjNc)^!J3eueuAGCM9`=j8a2zw+mJJxF{5 z{QoPg2lVEJ?^U%wO+JawGt6iGbIA3Y&)ea0J!_EyNc|7P`${yDZ+a*GN;3F($o=A0 z{I=EjZAp&b-bj9{ZdLqDesMm9q!ahErYnBrA6Dr8p!Z*Vt@3HBdN*6Wt8?{UDD`g6 z)w@O0b){EArv{hvdA|OT?oR}CFYW)2LYF+>;=4wHPx_yZe#`iGtLQh5dnq3tzldw9 z?_!g~S^WYQd_gV)UdPETI^MrrUGVo5koI;`h;x6)&EwFY*6a z{%YP&Wjk*9IqlUJDI5ARJI1hA^HCSe?PvDt$(+490Fd}z>7e=@bP&4b>ks|pZ~hGS zO0dZMguI{ZeC>W-F+vatSLi?d>q~^sInc|#4{iv5R}TIQwa-<|zhIvU!JmE`O*PhQ z37uw5f4w}<^Pi6=8RyBLH*<}wKU000+Et9NsEUfwWeV@3a+7_}`tZ~368mcf_&-}` zuZqRj=TmN$AV2lj%G)zOW4Y@-gnongA`8ls8h~4sTV|hc(lqfNYhC%J0sfi2RzVW~w(=$VAKjZZ(cge-{Hb9>t zi>F=;#Bn8OC_(#=S3T6P!(_lcU*5BR%5hvXI8)^@-oILYU|%GuH(S4Ox%yM-`UQ;# zXZr*yp8lPBQBEb*c9PSI5^Yp^YE8cWMdPDmwKJPHv-XX%|3fjFA>f_9J}x0&ZVBQ= zF89eQmB-PwiWT>Z-h!Wgo+AW7bRhmsxx5|pFGl|+_4O8X0EGCQrY9;R7o&gG zbWsJX7(FfN5MB@9IiF_wNri`b9bU!g2bQlr%~z3p-_v~Qe9d>7 z^3A|Fpcwt5=Cl1*iqSU9m(}|@f%h$W9^#!JkC86)EBb&M>2o0TmXFIy1yY~!pH9&` zB}C`hU!_8a%x{zmo5fRcy;~|AkGApsOpDt+BJ0#jh2tcCQ7TNAmHmh(iT)_z*shmJ z{^JX@45h-cBKf7lSLF*~)K@q}o|k2OQz~qgok&ZCOR;j7`R5f5llvbR4ww7e3shC5 z!aK#^DixL#j+FHB0$I3J_=cQVSt|Ua%&e3O{}df9&u_;Gmwf*((Xn!WMoy3~6+T#a zg`|HOwafh;w1D|Oj~4O%NP!xoRQNht%=@#^@pAu%!U=LO6i$@;c2G?98R>`bwuKQw zxu@P9OU^2FwcE$<*Cs2G-OqXF=_rJY=RQRpW%+$LAiwPj`3e91PU-*Bev>1i<9-Lr z3a~ss6r+2o21LGzANAe#K;MxsF8yTYxAnqr;8l#4H}O{w#@o#Qfbid6$ot-uPm?k8 z@Ax43`WAVgtm+WC2=!_g>I?C1<9rs7&HFh|!SD8c965H7Kl@gN$95lteVa3R zcYlyv+(>WH8+T>pUJC)NY|wdy-u>6uIfOnw-=uie%^G_XIi&Wq|8&Er{yKS{oNoBk zFSfMdQ$N?zhR;mH=WxZR7;TXDBA%%5jjkT=sEnAOXZ0qh8~tV){SG&O?lyyax5F(8 z{^avl=EjH9k%j!hahmUc8{0UoRa|q=GnC&2DZI;b@DAMzy!^P2^>+T4@ZOrj``n9! z*S3T3t`KrBJGEl%hRn@ zJKwXJcwLaTb0i0k9Q1tPJ^gLMJ1d3vSPmYo#PfmI_8r2TmBRa74j%3H^MUu{za+dF zDZI&-HqtMo`@Ycc-yb2oqf&S;wm)wF65$<^!uxyfkUrQD!oL;sg2m`_DLu~3!EMEtuK!!{=TsRFjim5;bMWYgKOg%3 z?90UO6DhpaLEMIVw*7g++wld$yE}zmL%{vJ~F* zoSt~Tc4hZh2=C7Y-V2fY*Ft`=4n+MDUx!lNZE0P{F|c8$q$$_up8fs^hPVW{=L|pSxtEE&cSQtSLES^ z_`U1zh~IGvH}J#WlGDql1?|u5+CqtNvfmHD`+h1bT*YXbzzg@=%G)a$$1Kv&r8=%o7HXVm(&Xmz8QG3x(BhhR zX)VO#PW-EE(026h|0#(BW#8HOcu(hLlXSeN^Rn4^PuGh*|MA{af`3RSp||0Oai#d@ z#PfXYo9^rQdkZ(o{6;*Y^Ky*OcA-g4aID`L_sE}%ct!v(i=SREgz0$I`?@P2zviy z^gLI}`+R!1j;#kc;Jh!52Thf1y52#aD?f+%cc6akU(l4F=i6aF1AVT;@Msc-+slYA z_=XqIk9h8w@AJg@+Ff`yYWl_F`=eBzxF|oXaUVTQk`ebq>0>&;ll&GBDgM}>Qs^8H znH>6gOhbxqGPm8{E1t>RY1%JW+xJliv}t;&_4h*S=T+9fgVwLJT@FqD>^m&#OI}z9 zuW;=9sj$C9U)D9B@9*M%pRZ@2oPACDQR6mQKfYaR$ksn9e=z?CkiQsRCye9!_W1h6 z{uvzQQjgjGJu@`$k?!9!gQHBoxBYu&SRBLXj_;KOuG@z!KKU6b7q*Z3HMVud&cDQO z$_?a8)yG+T7Fv5U{qpViBF-z(ZQx_;@9EFXK~0pG?mNH~b`kDl@2CF0O5l+G)W@!` z`;AXoC^ZI*hHk_8)sE*E_5gx%2f3={}(w)~P%O__Lj)+pJFX$40dm zrOEe-z1bUmo9Z?GpWP_Oc6Xs=toNB*{>Pc$^ms_epCVuX;8vwi zC8Iycovwch>s<-@#3RZ_zK;{@o3s-AVC$0OyNCtS8&)d4hF3SyX9?iAeq;GPw})|S z)erO!8spZcFP55qSg87e{Md&2lCtUvmTQbxEB)i@GQk7iFQ~s(U1Dj~J7GJ8Z-=BA za;~WG?Fvo1KgfAPN-*(1cRlTOUXT2%6bS7$?aILLz0dJQSYHUA@ZK!&jBnEQ4R=B^ zL07X^)N89-!KpD9`UJVRvft0vG~q75ew592YlXng+M37 ze*R4!?kFdfa1o#5TEesMDyo`~`rskRcU)_i{t|kNKGOA+1J&Kazoo*($f@y6Es5Xw zf%*KV=jY+M>#xk;+$jB$>4`-Grj?%9X?kMI%0@l87Jdc%Nx`#OPwbNCAw8(a*`Dae zOmF)-xL(Yq;_t)sB@|2Vx&~&;lpr0Rac767cz8^CCK3C-;UDq+r{K?q( z>IYom_(|lTX6*w}&tbf8xu!#Xcro=RYz8m-MS1(h@B6|yA^DvBcOJ_POEK{u#U7cR zb3GZ>)6M)<%Mb1(fBU+pP>;VVhmU;Ev_{K?`uqyRV;1s{MHlxg`tAF{uzsP(T?&`= zJG>b=JoEG*|H?dTP7Y$Rms_NNdu7&`d^|_^f6V%W{$V>qJf`j=9=mdQEC3#vUjClx zedSAhm#p$pRlbD%u(a|e*56uM`4ZpJSz7I%%Xdge@)`NzO}BEL*f*rUkS}ChY<$6d zpIz3-2g0Xjr}=)az}aGWzi|5f8+bFsbF6-B%lSP$K|lI_uHm?S9askY_XvJCFIMH* zzURsOVWV&A4;y_`e^}{@^+*C|pr+|c-QYFNLw-i;$>t%~TiWL#@3Zt+^N^o~#5K~R z(Vy1-CH*&CpXp`#;~Izfq_x-PNt@bh>%`Lb+B&hcy|zv)=l_TO!|x_p|F^h4Jspdg#X- zs&;aNwxg-s3ih_*_nL-s2WUCpf7tSc<+mHXPXWEN^-=1;gmQ2!;1{D`nVdR(AFw!v z`Kz7xTO1>`f11whpQMYCo_GIZ^yQ8FDDUk#dH)*x7uT0==dXKK~e`i>6OQy0?xSo+k2L+W#$0 za4r@&rcVPux)0Aoyu=?5?2&)JQmBP|=lY;5^^J=1ZD%Zyb-!ljpv!1o0S4D_+=#AceyoP0Af9oW}84yOry+ zcFeK#d`tI8dht?SR~7aX^`Xzd5MOG1&~+DX(oRNTvh%r=zOByTlKx)LQsiIN|31!& zhqRwa|DRn)dypTO{XoivcoClWJIj3?g5i3(RJcIvsjA+_K4Gdi((hn3P>1i9<#{p# z0C2^=kF9-YksKzE(dH-QN2$d=^_Soc_R|qKd&BS7@H3j*RTj7&r^UH3il=|~e5lqR z*HynKvlZXCJ|g{~XkJRXbv5 zR^1MIRID8O9e=@kW8LR1)Dxs&FSMV z>ZSg6)oZ>#PBHqi>hUq~Az!wRm;IBLGrA__B|@j{d&Je2?lL)8DQUvvm6VsIX`!c; z-&;R_xn)07i-MlRMu#!_Iqat;rnkGKpQdVg`#v@7r(X7x`McD6L_Is-{x785OWz;r zD2!FFkM9Q#JxVB9D!ftfXr`O`+vLYf;96FOZQcm$v&o8?U-iQFncmlNZZVqQgdfL8 zz8*C6$EbHGUp^oBryCmW+Iag_{YfrA#|g3N!w_yC^7Sv({idBC`^%S0{CaP6To~|Q zFIGj1Ts#lHBR=epN3ec;Z~9^Xz49Hs6!hmytOvq&`~FP5chFC7m-@!y$5!S32<1}y zbD9*#K0Wqb_$vD@ysFPBSE2nHg^l7xIz0`?yjicS{u)~^Iem2hwaouJM&Q2C`HhBl zBHls#P8Ish7Ckt6qJU55tp=7Xk_V->HkG%5J0y*9yXf(FV7aALF5`jN(%bC4Djtj{ zB<>4@_^EWKRve?{Z;)M1y$0q)qPq?~|LI$_9LL{2S6|o$=TA z@5%5c9};qy56fspE}qEc>&oTZX8Hae`EFQZe5qzn;{N1}5U%t0E#M>a+v?o=m883s zXTM#Cd>D@_9c=$UjMpt4$sOy_Ev@73xN3AOPqFt(SNP+WHvTKOTUz@C;{r@{tV!$e{K96SEpEh?QiTSYw30? zuXra%bXZ#PPLAlZwBnr{(QWB&E8k;j#VeCr#XC7-zP;CWB}ep0`cL1d{hF6M>IaCJ z$kz$)Mn4C7<9K^MBlrAbd)|)c)cZti@e!FF;LNn|Gq|_}qe*ZRse`QhIeC|4<)=8cV4{{kCr?3FOs{%=XZz1w* z^gJ<^dkpD{*^}PF6!rVejiOds=wL?ZCj$E={lBE%(IE-Hg}&m!%ICK#Rxu{x+p6=(j7F zA0r)Zd4cr1w}E~y8;5=x7-*v3fYR^v>|9)nkJPmLfkjz`NWb_GPnWh2HuBXKFOV)f zmeapgzE8&!Bi4@_cWQs$rSciC(RNcVc(FgWE`VLgy&qkiz3;~Nf9bw}6@4C#Z{zo7 z{zjf|@fiFx*10OKe~Y5sRZk25@;_YuJvQ|}{?hL(o!@-@T6#`|l^a!ePR{9L)8pkT zUt9-9%FJ2=g^0`NAL*A6emFnM_oUZXKa@T9{L~Zex&GXQbHIn8tiiT>uEd>s+vW6~ z=HCsV9$v4Xs`th1dUt+wdU*Sit2ar1j$WqulG&RzZSp;8dLs0D=$8`j`{jGL?UKLL zcDw+Ht3Q%-QvQbA|4;JX22QT3ydR$p3~Z=Mh)hUUlwA_SvXZ(%j3GZO%ZspVQJ0pH zh*&Zigb=aaWf5|7sU!h)jaWB;utwYN%x`l> z)f!tYzwh^Xx%bSS+1*63{eM3H`2_Ah=brPN=R9xcIj=W~t1Vu{z5J3sWaEc;=K;+( zaY&xodZMb)^RUwAbVd_Kw>5Wy6QjefuP z$=9KYhjMXyNc3r$((U}mxj;!s#sO`|8rpr#_jtE|9%k*U#Kz)RY|(Ku`Nx{s{}2z$e+=s?evc9P6y8(i z^I>iu@mWeQ?jK&EVTTWwA<^;=YFIx3*krp@<)HRHsjB> zJr=}0?pG_CpZ3Fehwb{_&agjPR@YC*ZJ$uSx7Fn7@`&3$XX)xE(s9lgE#3S|P+tx8 zk>0c6x>@fa`XT2h>FI&V@jd>`ty;wYr|rAJ($n+o>VQN0eIfE^@9!v8pA>q^UFx@@ zakb}o-63tygxPc7L5262J>RH))9+E4@B8(BA2aDEtIOu!UC&&;zOStz4z+%se7SoY z_@d?{b_A5URol1h?E6k3U*!LC^lt_6HfTpl)5AFBmIoN8 zz_H;syPQE7e)I#vchmFP`iVpiFP36+eqUJThb-iyTyMsIVI20J2cd7f)h{`Jz1=Ba z)X#@};Jn84My;RnJ_~&C`|EsM6-_AKu-rkG+hKGM>63EVmXXV6D3_|~x6PmVc){_q z+-RoU7jLfDC(=p!e&a2ucZ1fK@UMwK;e0OhzZ@<)n$&eNpGPaUo+R_3loQy9aXV-G zDPP_H;_{~-pz4S7Ylm%pfR2IqZyQhbh54U>|HAy7kE5Jc01xw*kiYyEtuMZtwWj{I z*}S0p#hbQ;e(^N*i<7F?9(N^nr0yuuW7dm*b?ZnV>nZP#W*g5n#^2o}7xQt_+(@So z@%uoBK5sBQ#g_Xdp|DUh6c)C~xZLg7=Sf|USr6}_aX&-8J%YfXXm+1%SLg>WMLGX& zo3B^IJGY46iMI~OGqfMtbEqc+fLY&b`_lNd*rMa*(xk>~$5VbG)&mBExR?y^xNorb z<>RJfU2jT9L2f&M{!}$y4DEvwC*23`haA9nayl#bQpMuXwoS)f|6j}esrHkFmYJsn zuYJEK^=})F8{c&gH{;9Kr98eXBu}Ta-1Z)Wm0#tyQOl?P6YItXtNq1o!vf!K>jtlH z8?pDwFMPLM;Ny|sHMoC19M`D)4jDehIQ?=&tQLu!RJf< zc*!f}na^oEX`ds(y*=a`T?FHjeVP8I?NQZZ`b8gi9*?|E3G}Mi;(GHHZC9J}|Mtq? z3;g;vm5=-DxJ~(;#<41oRFAYBuJ40Yv)@OgnXv1RYx~=d%DQpXu}NOgkD=+gc-@So z$8G-ISoMRnQ~nSiG^{s&6#X048~&T`!}cGL^hFKn@0Ik{hV*Xm5&MK1(w`ye$7ZRw zzahWY+uM+?_4YKRYrSh5(zV|84e5U*^lxgQXTPKmH>58V`nNTtGv0{X_BW*Olk|NJ z={gb^uASc}?5OoX~+%4^h&Nn;g=(KeEK6mG^z>(Wc$9j9e#qhQp zyiM_RY%%zty&o~S)84lVd>izJ8WgGdL%)XQ{pmV91-!xX_t)jeK3c=OHj{qH(zi)^ zcwVv{DRI{kc^|j63tq-oy-aT#(DbbXk{<8eq|eP;wEn8n1-~xvYZs?+yS4;KfD8M> z6p!u4qFpeQ%;$a?f1&^D;Qw`eLyr}ADc(ZM&x?H(@KqV|qtNnFSr_2GMdr)rzAJW# zeMnM&q2)UQhxXJ8IY(7}Pk)rXzijfi*blTyxnj$Ob}zc}0r%4>{&AI8;iPp!ZzKL~ zQa}6W->YzZ)Gp<4K3L29`dl)eUS#jxFKPXdpQJb9SNe&+20f4VE|q-6Q}(EyOqd>D zrh1C=0b1|nD1cwwbx7eWR4Vbhg9>Xmkc;~c2%OJ}n38*F!|}?+8yTNHTiTENiM4#i z`SdY^Rgc1Qt5B|x`(t^}eqt`Y;Ct!?f0Q@w1(Ny-t@?e(EMGjHY$n$`>g4)w8pl`6 z4yM$8{XWX5qIO9}4yDNB2@se@6(3Ce?36wM~+r=cMR2qKe9qeh%BT zu`RA&} zEAe72pUk5r-*3|T((gAZJ$?=p=U0SojHi_TsTrwP#*r=HqwR;u%0H@ZQToP5)drGGe~yNGjNHyhnW%scRx&*^?ZkG}`2`-MLRx&0P&j&IWTuQs~O zza?_8eF@2Tv{oB^zMp)xRFix+!Reiy-vbG~FTyycko%VCV)6NxXVDL#MCa{l9xFO63DeTmZVPDn91|1b0Hg9`J0jHAK$ z+vUi01adH}zw>8Q|G~QYb^lPa`j6?=*H3%<4+r&AF62A+eey{^cI*A{dnKth*{8|$ zm%|^r+~c;RDkrG-Os_c`;=yJCj(AEPTAufG^M~(V3jRXNGer+5ciKS#w`Y^@1&lI4 z0-npHd;zy%bKgOs<#}TFOHt42Ytnk~{TsZ+d)+Mk`)^;K(#`VJc;6?&_vZ!W^THi% z*Ykt)Q13;bgE#@`L~fhk20PuWb`nj^sJ)nf@^L}9Kj!8!_gcXh@@oL)y#1u(LMUid8}y6qj>;*>Zn? zHbqlbZs95^7gdG@56;~w{nc#>cdb`=e2c>C`W2oSP`IyGpSErh+kAB!5h zL;ZYMFZp2mbhGpLwC7F~WjmLTq1=?U2P$}oe5e`>e}{0p>3s-iqa69V0a@9vQ6q@& zHyIDnDDg|jLE3+Y^`1Ef{dp^?Nb^yC-Gm>KWLZ}sOuWw;)B4zz&_9p8Gqo509!3~X zv;AQ^L{8gNyx#tkfH&RetZ_lQ54v0Bk?fORYw&)Ri``2_KJ){1$cKBI@WJ&fzANY4E{wlAu zc)#iC2(M<_Q9Pl*f%2%kv2~KNZ=%Sh2^27Kg+Z4@MT}#1;=m=W4xihme!( z=ZKbTyVHEimux=z6LN(0KY??_PIt6i^7ygp* zSWBzHhXqe$aaVF4r^Wbd{dz)2P)~QJo(k(ZRrwhDTedT*D*s5&H@9;BK?(AGs?@_5 zf}-f5^+MOG^ED1S$NIJS0!{aR*y#!JehvFM{QLCT`YDd?;J#kc@$);d9*%l1M~x{x z>@`3~^?>lj&%L{yr1ztpC;6NXw|DZtH^Y}VfiJ$F$LS5_Jn-A7Z&K?kv?|{@UqpLr zhJQl_|D6H;X6^hF@Vk)NPY}|(C4+Ahvg+zPwkS(Zy(}z&-Mes08@M7p;{i{gUA65HRr{*?mIMMT)ms{4oPQQZBU&z1JbG z#d&Sjf5RW0tMo**W0D@mM_wtqKixB1y5jwmVp;Xa;k-3PgE*L3!W_}8!x zI9k*%8JvG{M?D-ZQoBgs*TetQ{yJKra*NJY`JNB?or@MsU!m|C}4qJZx zf6lK@D|wCkKHxIRf69;aQtt?pzHQtmw!Iw?PWc~2@Af8oC z8^3*B>{N7r#5{eO=4bofeXsgKu`|=#q@MFNUs{j0Z!SAKkg>BT;Ac<7&Tf60?d(m* zV`tA+en)4R-hzLkx5n4g7aJc|r+hmV`@ft|{&YTt{m&8jpAUoY)Ci8NiHdxG1u96- zJJ>!d-_P5n`$eO1^-u9S-B;@SqvJlEubu5Yqo14npW5e{wEt@3(s;96HnSUpxN0$DT!*Jpi9kA5i3; zy9I`fAHS2!pVM)Jy`SD7$!Wi0`X8^;xX9N9{l1rJ(+Bq7Y!c^G_j6aM*$sf8P()khL53S(% z_;Vll6We(_@{@Xy&z&XZL;E_M;mbke%d?<^;r=Bp4+F%{`9(j^^z6Q5pD!&{zbfra z&nIYH5MR|!G`L6E?DHz?2kiX9eCL81`8jI*9FZsIg($aXd}Vs5cL=+ncgN4y>&e%< zsfFaPJiyn2{1NhXEW_6k<7*f7ZE{TcQrjob>^@f4XP@_qrfQPU<5}u6ITq?O=Wl6O zy-26NJoC3e-&>5%N40&k?NdD-d*wV`$hXZW$hTEZ^|6s}v)Soiz9jYc+@Ddek4IJR zv+5(Q3FR~A`Lgz#>Z9?^?0B~EzxP-1x_bRiCX9{$Yt8Qj<4b+?FR1`e)~Sz`=+ z9v{D-?F#tW%+JoIx33XDt9qOI+5Ia2CF&0w{OqLdW2kAq(C)MRI$-?jv3C8_d)Y6U zpS>Fjgdg$qH@lymUmv-C8tUVvC#a7L0{;Cp`uOhuWqtfG`Fq^@XnvrCNmsu%P2|MxfAxvztV_RaVu^sCex`ju~zpLTAsFhlIyW~&iHnDmuHC&`+_cp}vv+klt4ay;^TnHG1rPPBfwP$B&x+>`{IU zy$VS2i?8a|=Nj#g;w{M%9mKdso`}VO9NXywcVWtGZt-&a9)- zUopP$_+`HN{DyUlIrMWs_|i;2*NJ|r-|&078qQl*q`A_M9tIxj0q=|S`y^8RRJ&}p zpUU-f#eL26Q~gk0b~{tQ^gNK?&zFoB+CDFQ^>cvfet*}mT7!O(-nsPaU8Cu_v|@|j zpQH8oyhD0Fo{mowJGAj%{HTpXEN&bM`df0&*CmsCE>^A(hDOzIo1UC)@Dmxm(fK0R z8;lzypZDW#*H5RObkCwEKY-luU2vh#_ZQ5iCt$1S$@@i5R4%EWjA}dQ@MHh*re=Dg ze9q^dul%aK+wcqCv^KM_OO8D^hM({E7)s)78#kMu zDX+X(^Cf;pHycLP3FS|se;O~xD^>qyJLmj#>KFZeGe0zSAN6aW(C79wmwp`v9?I!$ z(64k}_8b{EMAg63c6NPK@m2LaY~1xpgZ11}-1Qd%Cw6J;B!f0jJJh9qylm$kLw#WV zbLqorq7R1!KjR&W%J&l_`_7a3ql1z!oj+24ocf>r_CB?L3fS%6Mjyw=r74wHVPTu-+h9f4)BOB&*skA1xzMiv0x<2G9*p|CF5_od z&s=&n{TTJ#>TfBCUimz3Dj)ARykF6{IZomcqu1gQmxrIHPxNyR|NQ)>{yEjp`gs8D zH^aD%^S>cq$fuAmXMrzKWt+&y@8>H&O6@`Vc71Z*U-U}5g>>ER1mgCksJEGZoOK`d zquumlsXph3+ed(h@;pR;G^Ki#>c=BeKG|1UN$KxIxsVU<0!%)Xk%jpFV|RT&#Cqn^ zk684>>QD6JeNum_AKnkRet19N@v6}~tA31HduI1D|NQIC`tP~*!_VVo<<0$YS$z?{ zm6VSmzYc+4Pp5ut1HYT;$N#0Dstv1Nn4Zq%KMny8<++ghk&J_pfL}VEnw_5?1wVOD z$fd|n|IMG~JPeBu7x=V9jKM;dQ?|LgbP?YH)QQ=ZBBEWcN}?aw8BaO$u; zOBMAqg_dtUMf=Bh3#`26i`ym*w*D=-CqV1*^SD0$it~PwKlG>M<6Qdx9MS(nQjY8W z?04}!R)JIf_c+`2-~FZQzxzw&r}G)#(=htA9qIbdn54%`bRIwQxO2%FsGaiFcEn56 z4-VP9PE=9;WY;P3IrZ-+GQMk_MNi)(db-HyZq?@;eynDCx>5ABCi(pwO)8gTk{-&% zg~ zr+ntp|3AU~nm%7rXjy3b{|S*-s{b0-dfXo+^AoQB%4Z*U;`>S}Z}Ye8ceDPO?GD>{ z4(Sf^a|9B$lkE=MxeWRJ-tDs8qdoNK$-;N{cX>P4ej;|!je45-@8^hJtd(}1$ar@7 zlkodf>c388Je!AIG{)J|Jn-@b^2c1`)j`TV(DQd2|9oEV-`-sxC-h|UAGG}4sEK<0 ztxWz==D$jf1^Pfb!uF6}=6CwOg#5|8 zoSnOzeZ1>_-1XM=*y9}a<5AV)9MvoDAE{p$qWnze-Kgiy^zXCKho2 zPDT&ri!WawdZ7JT;eRo{e9yhj?CED8Up@eS%@;;(OE0F%bIK0bZA;xCo&@x;qF z{6zYpdVM_O58<2nv#{Umd7AZO_{Yul;|kFay-xu17e9u$a&zg&!T;9rMgY>?LbpGXw^4{aCsQNjBE#5(A#QvzfQu*xtRM?Iqpo4sR2IMlY zKYF!@BR74uJ=faP=Hq;w#p7J!edvum=X08zPpqpK{=v`t_<5G;M-(6Tqxa)amjA08 zxZZn2{ZKOhY5nvLWWcZ4Ix77O{b6pGj+K~?_`c3|R@JZhx!vS^&E#RJXS&SXntu(> z;}mS)g|FX)=Vjg;!CnS6e=?7BmcqVYdbanp1CA@Bs*cmrb6ni@!SUvkLQgbq@9msp zWb>%$eGBRrGS$ ze{+7;<210pE>3+N@VH$&9nd#mP+R36KEKO(ZvF`KcLSbGDK1dP#!Iyv>Ef@vROu!? zn=|yBgZGp>>6uUenD09h1qYgp-UmQnTzWE#wDoKZ!R_{blfu3q-S1gz+h_S&E#DS}t>1_)LFa;Bs6TQZ;UP&DJI_BN z<9zW0Mcfz5ek0yFpm-kEeA)8x=3aRp-XHmO+|TCUp`9Mpa;e`?1@iOGS$w!xLGaOE zd^Nxm?NvHbJk%o4x3x#>uV_8gKej8X8b2mYuI87C_X`2u6yF7cpn;w#m8bhr_m9ML zuqmE)!GrT}Lf7D+%CXRLzNS}HUl@lBzcuw2#P{Wh`^w!OmrH*7ODvVbj-D$p*1beN zfMt?_?WPA(tv@h>rsF4_8JaiQKH2EQkK!~Jhh zk^|n~#a%}gw(rB^JN61s9Mb%KhXwY1JPqexuX29=i}oiwOXs-n?wD-&t}y5L(ab-ZdZSjkr(+njQniR z$1;4sh4V*2{+lv#+Mmf!eQ-Y);{8L)Y3Bx=_uD1p$=q2;H}*FPeZ^LK1?usz`k4;* zGf*b{^0`~(ncB_00()E-jc=23g_F=Q{01-5^3&>`bA<)RG)~a4qmECazdNLu-UpSr zH|M;-FBV%~r}J$U%}@Wl3I3V>Z|O_x{ofC^XneQ^&xf!sa^n0S6y@d$qsJ+EvrJZm+*vd!RbgEyIf z=`?tY!R-Qv^=`6yehc+@93M??5`0l@K%Qa#^_G8B=vuG&svER^yZ0zw)-Mc>-`19S4j<)qZ!Y9f$4J{#)f5jjO)X=-2cNL?m-tlB!54N zv2YqUsqSGPPV)Za@?O(Je$eyz{@pOWUDEZtC}Fzh|C0E{539O^j(t)O{S|-o=ae+* zqRABH)=9Ms$$+}Sehu^6Po29?osBkpCN#zZ-r{Gb)pMdV#<`Fca#qy1Vc|0&4D$H|43j|+atQT4LzNx>6U%^teGukg6qMZE6Y z3QwqA#C@t~(H`>)^WI1PN^MU?9ujBv(%x%k?>jV|?Rp#aZEvsHdw+wyug&y#_nUqh zzeSH|w+~zz#07RA?eXeU?0#YMiTZ_}j9*x5u=2S<&RM%RKDfLYf4JR$1Q+XIzD?wi z+Wq;$xWT<9kHFq%2H78UH}E~H7b8D{94d!I4+<@ll0Vf8A1Aq9_&CY+!pBLj7fRpp z>&3G~UV~Mozcg-oIH7t~zS!e~A4t37i#%ueBEGj3-Q4UI8yw|XUd~4z8Zm@ z=5zlo?fqHTi+fR}o>1El3~m0f@P+n>e@VaKaZTK{P2R^{TlCpBkmRSt%T4^R_|2-y zoAV4FZy$RT_1o>{cO?V-oa&3+*Wz&t^j_234oZDt+#>$HOZ_|eD}EdCg*=bfpS#@M zzC(XL^-HNgPwo5NN^fP8@nJ-sQAPQYdXU&#=o|jwj=Pn9)BIYhjwt0;1Abx zvV8m^^vlQbj5k-H1pV~yJ_0?gN%0)#oyc!rPw??bn12uQyZ>z_Kl0b-lcI5rD_PH* zz8m&izRvI#sd%uWvW%S6Zc;g!J+--+=wC!GfnCvmhVuDHQ~BH^c%q8ZnXUtC zyFD(${GPR6`G)xugAd6wq$8`Bq${-}ji;hLX?*laq08;w_2xC&KJDgm96x8n#FzH2Cp}`(_n1};$egP z?Y;UNd}qsG^+P@$O#RRndp~zv>3+!97ov$CDMx$z2ehFv&bV3H#r7@1Uv5|Fz9k(m zyI+NWO7I;)f930PPq*EL`r&?U75rM5J^(#IR3Q44oqvcL;tZRINa75ehq&0|44sGY ze%(fei}|Ny{kzG$`3U4fd;aX*)c=z7Pt$9ZKgst} znv~-@Kg$*WUB(+yuGq5H)@MhwpPRCE^V*EU_qPk;P*28y$Jfc;fwvF|<`KpuKjRy^ z%6#s{QZC-A`()5x$a}x{CEls~F{1lBC0~etAMnS!hb4cu9-edLFDmIXITvX6+WWn` z&wcDd$&khC_^5|lGJ5zZ4rEQ+e4NKS=y!z<@t?oSaTlW`#7EoZA@?F?@B9A>^7;MU z1)I0yz8vz!_nGDGyJNmj?eq@C$2gh!R@pre9yhaJXL@l#pJep!#^!UnZ*ksz4>#z% zdwew%0KfRf+R*a(Hl8wl$>*+?_o1KaW`5Tv4bLFoOB?dN2q}5F^&9Qq-Y)N_Z<9yv zbSs~|apSLfXQn?ow=zG6MIHZRw_EA;!Do*AtRo>BF)vH2_o|~c_L7kq8 zm$tJS=;;B|!^86Qdu@{MRoFc(zVA7@rC-X0dPF-4{SnVkxn9v8$kE3AGs^GimVxH@ z9+G6-^QU&$h^I~LG~GX}?N8U)RS$yrR@<4zw+5TMpRM)zcLV0*ugk^x8*S9`zR!&G zQk3t~=QD5Prloh)f3z2Ra~I$p&l`QDKA$7Vmrs0&*^%4 zwteY$@V=q-w`)1yhg!sUp7@Kd(|D_J@)=@((RDg5bpP!7OuxrpR-cOt)b2cvZ_pzh zrsbyTMDe4axeEH!s6RUYf%#gYH)-!mE$8>hr16;USB{c>P{&mM$$mKX@AK`0YW7_| z%G1|>T;9&NZ2Q=rfBH4p!{iaEzj&&i`-!}N@qJ`xYCEDc)J`yuKf@Ht?LgqCJsuA0 zIYj&0qvc&5jPKc1u>K3Mp%C+6Qon!a%lQ?aj{q9@3B}|0U6d-u$Exx@vU@kv`{m59 z&T_wO`EJFR@acSoonOV1o-YZ7lUK(NaMX_at_4hU9hx6aR2M)eb^H(juUg>c;nS2H>(+;@Ma-O`8yHYvT z0{V_ePOOJ=q6;UT-~Y8nIr;uq&ijabI;Gm&s%2_#{(a@pzPBPvvwmZXq^lzd^OumG z)#rKp(^r|?=eaL1pZkIGdArh`_D2dwHI)PBS=b_u!}#R>7XEzPd4r`F&CmF{U)T=X zWj?3=%<-<;u6Xi-6>|et&s_Hv9sr{mAI${vq7FQ|&dHJSgo9^^kEK z?UAap6z;3_Aq(CB6W^yr-ut9nY;^iK)c0Y|9>0G^$}vB&lHQRFy@%ec^-yo7Z$Kve z;=VB*_grFd*FJ^WdEggqQTxlb!{@I^_eS6=S)5R8xj^k{cTMobJJ;)T^Qb(dL7hLq z{!fE@XIKyJuQ0gB;9-MxJ^}Y*7_9RN_|BifotA%-!R-bQC>+^*!WNrHxJT_1>wit< z6Siv*ChzSk*mt?yZ^mtR$ao3kG?iD^EeelYJhkp7g(tM0xbJ#}_b4CYdDoG4ku4ZU zJudhoyAL^)#-+-ait(qSe2q8vSh~r@&q-EPF7fVud#`+txAqDg z&ZqWc92U+aKTbUh=9gB00_sZ(V!KeEE@V0L&*41klVCxZkM zzVXhrO7{-QCiMJ9fG66k^1*i#Bwso|eTx(rtf)NVwr<5!Q9SfxLSNNj*g4x3)s%13 z_owxZ;k(eOUOC-m$-r?D`IK!p?t81Fd-`F;7fr3#^hZ+suYY?p{Pgzqc$bpz0Uv%i z!0~|cC*7a5RP+|>*-}qbX*amNq5ZAE7k4d{bjtZVz(;?CsYlrNu#}^JuHT=qP3@wh z_;4?(!CD{eTwvm%XN}v|CwS`jEA(5rMe>BdFu2v=0eNQm8tNvlCM zNB!(Yl8G_g)%`>HfX#9ifN7tZZ#LxFV z<#V@5dZ}vo?Eb~b?vYK;Cw@fBC+8E*PHq2GKKCo~KIGRasK?`+eD0s*y`P8ncpLkn zlwY>~70OHOeUFfvd{349*aJOkv~T6B^O5(qL!}|F(*;3PQGZfsIYZ!(PDWPgccp$h zK6o6F@ZdZ+;@KmJttxl`$icC8n>u>Vu-YFEF(I&bLT-^4w$_I|+LA2xWC!G{dq zVDLeMw;6oYVD(qY`pK?gdp{%ZZ+~n=;AmIP(if@xc8wX_YVfGRIe~p%g7Ncyyb0yG z6#WbBzXR!%$e^gx_n)_wCi!nAB~$oo#+=m ziuP>L=h%QgYn$X*!1;OVBjZC_0Q((UAYn;|69s9Kjz{txz4*q%O2t{Gy7>zu>vZ@05Hq@&N~kNoMxll*A~f8wncf9=$` zEaWGKW8Ci~3}k)$`8bGtVp6icyjA0~c&o-|A)fW5V_b^oINvRHH(@ZCw2%Fm*C-M!;9YwjQx>WM9-03J6 zZ#^dUg!xE6_UB4^+8-VjnD$rSA0CnPc(=+C^TbAn$}{Z;Rjw>w-w*DS^6C2iVS|qv z9@P`~H$!d8myllaH{9PveGK<^vFC9)V*ShTHp)%($Ni)6H=68~{ONj}+5`Ew_Z5`W zu+W>vUDt|W<{F(o+TlGk!Y=JhV5qkK?no8GMCb z#e3w#KlFzOrTmaCw?JRkYduZ+_5Hxh_I(!f4(M++-}rv5A2!4v>nC2;-;O8i@6Xi# zJ+R_L`lpcpQPup9#StD?kggq|uaWOMF@tq#)yHKWpo;Y?p0t15AnkB{2=z$nuSzjF z55xE-vD0zYgJ@!{lqVf8#u&Ph4y~7b=a3ZZ50XD!k5xMTylZ+c>JFtlITv+{!Oswa zqUuco`@BKP?t4zlzc*9@HK-;1B{^V&Xt9Zif${>J!gRPnqF48U); z_$$2^eM0e!8y}K$WkzpuuFU97=DUsF%zlp9=Mz4u{?6tJrmqq5@ICHBy1%LGusnU< zF1l04*`fToE{X3HNlpjLRY6C)lvDMFyY5bpz@2eWO$6eMxjt>YO@w!bS_h>@L z2RL7DW8#>F51VVR`x+#w*>H|9hGI za=r{>JN}r@@qP#S%DYv+W}6QW+q;P8&Zf*BEKVYQ&!oTKsQ9DFMasY0(guFv=q2oA zoA5*I1m)5=VNCSDF-{m&yVz;=vsLXTjStjr8sh_%LsV%MIxz1hc(Bjk;9-Ho_K933 zv>n{HFZyfkjKvt-oQsYUU_riDi61|PN52H4t`aE?^o*~{Nl0VhAHzL2s zyFRWD^Zz#G9qi-8kPiLfJpO98>L0$>B=yHTPt&DJOpa<&0z1|E2yxC(=Ux3BJJ3ed~LWS5i(l zi1_+8;t;jh+S;vEJ=lR?p2)^_?7H4a| zBE6SS{RQ@a8J$K)a&P%jqtn|lVtn3j`9}@jCouJnKj>Ei(*nzeuYr$& zJ&!>zT`rWz@^7X6Q7Vr%!4G~&y@O|{zbdqJOS;!zZhNAkzoy*0Um*TtuTSM&Y|(uw z(WJK5@8wF)^Dh?kb#WK{5Aj?IItz9m1^kTA74F}Agz?0nmiO_Z??cMxV&Rj^EgIMI z>3$gX_vwBZlV37EFn^yJALMcC5o>Jx9=Ek`&y2i}#hMLdaD-}gmwO`*?a{4ZoP^Zk`u0NXx+!l@d6Ci_6$FWbCfWc9{7 zk4gD>>rtiSVXOZktv77nDEat+rgJ{&NJg%Y4UwKP#be)L@Oing+_6l#3P`0LEC6bs z-&nQ!GO5V-ttEPF{7Loq3dt84y?%ZJ=gNhSA;^?J&hvu&rxN~7D&JOJW70U!{Am*BeNpI2 z`^(Q6tbQ@sUuElD@PG0?nlOFab42qW=RO;c3;iBg-&f1_ewK0Ic5*qLcTD$rseaK< zjRtb}eQ)lk`2Jw(e%i!eZUkfS%jfQtXX3}vNAY)hNuPrrzmaM9&E>zvkC{EW{rY?( z{bd)*ldcy)RU7@~m4ZLy%NFuQ-AY|Q7S=<2Uq-%0d@mDx@n-WYcHgjnpEa>(>lZ?Q zIelF-f2s0gefRNP#9s5yr(B`+Y}NNk9P9ql>h<-Qc=P=JvIcog-z8{e{(U$j*MEat zlm5;6LBB^iEH{=Z_g~j=|C%Z!_nV1cc)u}jdgk-Pd$m2QcDSBsxx~+HSNeu7(`Vw} ztp9K3-!SF+R8-RZ)Fb>0{8ziYXz*_u#z9Xt{&)Z8{yvQ>J#O@If%&;4t~5W_5Lc?g z`?~&Y{w^9*{v3Z?Y2)k`P{#&pL&(j7xyjI__k}2jsrY?THPrbu>KQ6 z*Vd}}FR^%GZM(eiejr+Gc)Dtu&+ajcyL3G4{wwa%ahk_>m_JndDq0WqtHya-^GD?S z@GI#jYBJAJY_Wa4c8>_=9i@D{^RUnx?>?f>tw)vqinbr~l}cAd`4Zh` z<)42i2jg3vUut_$@}s{|xa$Fh$M-9|?p}o__9@(Vm%#B^rWfZ!kofsInfPjLM^saJ zxL#AZ+hjvKu)=|~RKu@+&uXadtH15Lq#n&6AZ`1T>?>24! z)UZ55x&~05^m6_p-aVuI*nCv;C-<`Hx&Y*&@7s>all=}o3HIj*%>JW(AHrct59<@Z zP*u9P{&X1i({AvuZogga4U~`RX`8J(RZLIYbo~V9flaS%T_rhxt?L@;_cX>D__iU# zx5MPyoyM=J4MKOccT)rX{l>4qLVmU%|Iq#oqOV~)e+_;D`xI4=H}~js_gZLIxbE0XTQwX;~cp8y?qLgoBph;DLkS4^8N799_@eT-=}@E z)LW{4Lhwi9S}u)`KBDy|>++8q?DPHaGg!xgWsBqTx$6~g#r?M7U3|7QJgTVwju%_p zRZ;&PFE+dt^WTdNukkCNyGHPa@g9ZCj^{VL!8BlO4T_q@NsxLNf1l^seK=KBR6JU5BIX80!VviZk~(cfb6&$z+h z%Lx6A>%Ymt#|x}~2Ip)W{eD>xgnSf!?@+uf_hC$RHI`GyAC}t!{(HPQSTTL`cyYVp zYgpGm6BXbWo#FB2`^3-0eG~dz_g;N=Jt9x{59K!PzeBl%dXmHoK|3ykd_wu}XPjm7 zXFCh{SV*H{zCi5p|%dS=j47;wFA62|7mefvd&|1P^D(?&E707#XP^r zo%%<~v%YVlzUjA+I~bQEE*!x#aUaa>JJ+HOra|pf4{->s*UT_iT($CqhBST z_G5oxC@yY5V{$7I*3LMh+51i+! z0x5os!|AVRuatP`2VQ)K(hU-FLv7~@{{|~hiM|ZAEm!;Warqqgp0|S^^fyO?`3~~C zU(P$WD<3WHNY+VfoyymsA9&9{hu7Y^kzH@)ujw zahImfY>*T4`??|PxdZYTv~u*D{Q39SN);_%Zo6OPQmULG_{x6HGC8+peznpm@7=Bk zE8PMQ`8mtl(s}8*v5p?MTP?@=e*8@_-_?(q!WW2Rs2M(41>PWOnw zDcz&?UhDV%Gt2*wpZ7yn=-&i?p;hOH2G%1_N-{_}BIzs4C+Wp4R}^wg~1`q~@V zKYc>{LcDvav^(C}F3-WrVv}F1!AckQ<23MROGe+%!un`f-*D#rfvxrL_hsJyP3HdV zgPHf&bNzKl+g&_K`+xcYj;J`_^u*dUZlix}LptUBx9~~vY3j$BpL))Ft3}`KK4m{Q z=KJL_?m{l;+nw^r+2>1gL)dG^l(3!a!B@@;(ZacIyXDO(9jyP8;x{cGtZEz>pReuk z{UF);3#}&$zGCaMgaFb_{G^*tkE=pDvi|2o7p8uz&~mQeaeeWAEz6I5?gGijerx?% zX*>KpOjsY+MR0$f@}nljt$*=(3Fhm-I*;o?=r0}&&`wd^e z!3PXpFHh116o^YY#g1L>xc@JK8GrdX>A|UXDOZ|Q{AvHE3md*(P+U+zDahIEq_E)G z5|>2PbG801UH^-!T?X4aPxT^$4~zap)pZ8zx_MN+)?ix)tX^;MjN~s>^}K$$>zKfU z)ed{F?I~5YpNhM>rF}T}CQp1Bh$6bpIR{AhcV6f7UdqNCWdg88jgO#4R zt5e`oRp}{rbqic*A=m5u1QpZ{3)$)AB82KGXXm@=4rR(Q=HN`Yqp? z$me!fnmj1{D4eQ00ZMisX*toq&nds!O#dRIqxDp^+W`Ms+FjM^gOPsK^l=4}*p45+ zjsA#K)wlDA(bYn_%x{&fzbY>|8I)6xwf=aCOiLin(s-!6MEk{(eeW3gnH}-e7Lnu7 zlH~#?=h>D#$KXvW?Uh;8+Rqx^@pEg+aGG6ipflC$Dqw#zR$oe~A=J&g&-Qf?b+U zzV+RTc0-@}E3~%DGsJs@dhw7X3m<+5`ThQ)bpGhMk|Eh&kliQj_ZU8+6*W7jaESHn z(0Ukmv}V@ZesC+twd&t|zXj#bAKUTPmqQ-e{b+;f`+xd6$8U=z!SyN}zpceMx!Hbt zT8fV!{C&C~LH%d+6Momw?|Wna#UY#PCFcFqA9pDI$#_!9PwsV_ezm@j`_!(cUu>{_ zr`7izMUUz_iu+yiYZ>HNG(Q*4$G-z8d|aB(sX!1BrEws&9fovPUOx5qp8D;-<+Dc(tKU%b1fu+fcmT7ij|8twO9HqMWX zSh-$##%->@Jq90=XV^|G-`4d{Bi4>VjF+N`0mZjxTSGbKi)w2%-R#KsW07wandet} zQoCIxj2x^yBISnK-r0aROV2w|5A6<37kS>y{fiZ)2lo|AdBh(xL>#Wy^$Tozv$;`U zNPn_Uk)a>+mP*$H#H#hqTm?Vm`Uy#5^8tPIx40Y`@ zddK8h?i!WnU{(1r)TMl3IrbpsF6Cd?PYB&ZQe85CHT`CV$@lyW{jpBS3|19iY5aXs zPc)%)r{|IOC_Tyi?r$25$wB<0@pl;v2Z>+fc_g*9QdRMnJIURzOR5QQu!`q-Q zTQ^C+72U1%P z8zf&;8525ipOC?$25%EMZ0CN`SJifKo|FENbe>8(-sSsz2DE*<+%9x{Qowgw`9nOY z@850yyJGmzPL89R@$oOghiGzG%hg8Y8PX;592*r6={ab814{5s{al!zaYg9wj*GuL zw+KJtttZM~A6Ghmw+Q`loAP&P_o0UNETZ3-Fup*2oAo=<-hM4t)_fe7(}LLklf^GC z5_(g=xIr>H|NZ`(XwN~x6HV-IpktcXBeFNzT!>xOC2%z zfIQJ}7_9YTf0)2SyT2>Xth_?K`!47T{q+Iv*-1aw8!P14>l-%B(34v>DclQ7>U$cJ-PQXcat<_AV?|AaeKeq`;!%F-^sp`vkmtCz&)v zU{&?LH2w^&XTs`9);sfxC+QD=tb9+-=l;Op=KW#VZu(=kdllO4bP!%c{*+V?312Ml z)gB06B(UmB`h8`iw}CGwTf4oVeNyR5_9JV1y;+<}dI!e@Q>Rcc0=7=$3xk^qF-1C&nGe-A`|} ze&m0lpI(gl{n`5IpQeBQ3fdp-9Tq;}yT1+mlKwg1*B6anFx960^I_=qPuxFme!BbT z!wu~bzdj*qBHvx4erMwr?03|1Wz84%&x@qL%=XV3Ai5p9KRNOKxxQZN=aaJkWJc&I zw!E>fe;(TXB}q^BHEaBy?iXFEek|E9s&rG|fm-fY;&_qu@P6gczQ05{G82ADKe33p zA@|eN4luu{{-w?1QLPX6R$9GEkAK%8!DI20$E{(#)Ss{)rhI(}(`9}qj4N)mxI*1{F23=^;tKL}A>)a?il6(UMDMJ= zFrGM=i6_2-@yx98gxO(T|Nqv;c%oU{ad9T!0r0h1-0=eB!#R7=t2CZaJ9^sU35#Qz z#S??@FDDXDe0?+cRWUwOuPvUq8tK%x|G&i(%h7K4L)60y$sf}@!d>!S?SOEnz}kQQ zPsS6!N4lEGZBZtj{(yA03Y`O@mxRv%oWv8aQu+J6T&T-a_$ zfpMRr()W;nxoElK#l2!$|8nI|yw2!bZuG4)`ixI;m(nq`NGJBOzOQsGTBr5eJ(KaG z%MCWV7WElybS?TNfu|u4{@j0&zqf*~z8(PA1&S@$oQ$8J zTMzN%P|m+Y8J6Gv613a&jPvcAP@Z@<(msN8N`~q0xE<$g0ki}5_e{SO?G5YOZ+zH* z{A}k2Hhn~h^P zyd|ZZ?d@j$rk`Pd_Zs9Q-5?q9=RwuC%zQb{PfERh{x7*VF-H-JMOuI%^b^3b@{+IgDiFDGj z4Ehj_Zj$`2FDy5ZDc26Ug?h9YBlvebcbfmUc@>X`u+LHXv{m&w-mLs6 zJq9CVeL4(~eehVWe(JVU)E@7#r|a#ik_v#|BcnV9cU>ywZ-4j~E zCVvb*0;hmq@su;(sSb@1i-6u!<8t*7w>HG<6QM=)LU)S^RrQNVl=Yfg~HQaW=6xHMnx(V+fJo4&Fgckkf*r}kUL7Ck>2 z?;cS+=EpcbJd%miuf}+T?V%){Uq#!8>h1J<{yjbK>%A4D2Ze>cs==rytEV4*Gv%xM z)JoMmlyBzW(sNa}NP2po(M<;X{MYpc*Ux|DbJLQZn@d4MA-=bGYSoG?nff!tjCCXIhkOKG3I zXZ!jy?fX5p&&JVyKfu%8&a6DM?R2?qH2K-Q2itQ-Mt-02&*;WE67jQPyFQ+2*AcWUvUy6z;ZRZRFW4vLUioaJ z-|t_yb>YbF$M^YW-}m9`?KtNs<^8_PbiKUses1x9O}=YF`T2U;ZE&ugq|4&OlrD`w zNOwEldwSSUh~L>`?Wm3jUHE>LJZX>rjDXSa$@I7=T~E{f79l>;%X0Ux0X@}W!IRJZ zjXcBn{tD7tJtXPP;`?oe=Xb$>{|=?!o0#6;^=Gx%xFxcaZuNrLe1%pidn$i0P z(3{WwzR)whUh~C$hh?83?o*fNw8dwI)&a?Y_)^oiYku10u}nNx;5g_pea~~#OYvt| ze-Gk5j29KpotlsQ`_3Ct?jb7&yuF!nV?;-6-$0aM& z@1*PU2D{&|a|D_7=V;3GDbSaPZ=-+Gac;@>i>3A0ezByU2h|@X>)a0*Z2rmmldS%c zKUbliFr9kK^ed3=ZdE2Rphg{ivizD?0^_I@|Sm z0TAOCb+pP8@vP=Mm);BMS4?QSt&c}nyw~7f<@Xhj7~G@qWe+OcwpQS<9c#t^^#~ur z^d9I9+xIB;GZ%C3)ACzYALGsHH{#vqe|DTaNKsqwiL<>K8oKetoIB@+j^6H z_4o8N^5+of3dc$07yS)2iTjbV`-%VH7R={N%`~*HbrwGT3wRSbe+ctxb34Q(@!lB(C(DW(QE8l;Qb!^rD#nLR!mn*zo;l}-6K5uX9 zwV(&0fqb<;B)$CkzTc3al%xAg-#XB_pK-Ms1>CT zen`rDKNa>nV{CWeSN{UvGx2z1wtkF!ImmoLyf6G4)OL71;r!~YXh+!Y0oJ=i^N0CL z$VWNl@yhq3hvn9b-X3b;Bl~ribG*56y=Py>ZpDr*9`b$Yl=}kI%l1`aSNKk$@Gb2R zpDP51^swE`|IAE3LHtbL!gC=N?FYE7BJ-^s+U|UAx%?5v=N~}|=Cjm3Oz-o#=SjW> zz4LWGJO37qo8GN6y_+z->r=gn_88uIzpo3z@x%R-s(0gBF703S{*QEi@gtHiPWso! z4W?Jb{8O|3)%f-e@F(P_^X)pMd;c2uncrEb{v+KNX8sG#{+CcsYTsLw?(2MAuis$z z59XJyQ$0z(t9rey2Vd7dL%Mz6nSUS6?+@qt9b7B!6IX{)Y`He~cLGy;FdyI2m-`Qa zPcpbS_cei)AH|lda{nqYwVe5{%)L=yVkLZe?wta&7bD!0`+I>kE-1FVRK_!dt2IU~ zw!9>Fxx7D(*^z%;?zIBzxw2x*+8p(2aJ9x+#g;X>Cj{1UWU=Mq+#LezymAryyZ=I9 zjjxI=-MOy_ta?#w`T5+31U>`*XMGn+WH`856}H&Ym3va&tKf?*ow=6_tnp;AWn~U# z4tfgsKg+Mk{gJ@54#MZ=NZH`(vkY#}oiDJ)LB*EmNry7HTGzX|Uh`3T-)8ySaz7II zIR-D4ar)ruvkhJ%({+ z_#WTk>hkW}i`4R9}2?q+5Uh525EG};+V5_Yyv`0wB6@%R{eFX@rGnFjk#=omMBd_bxZJ-qQ% zj1$eiPgO;YlKIJ_V)yC%k&BA!)$yi@-HhL;lBU7AhSlje!XN2@wxsMp}*c= z5BYr$+m-Qg+c%Z2?PwT&aog7nhCvGaWrGF8`!5LWesrjNi}^Ouppn`l~~^-Yf`Gxz4D*B!2Oz!7kS$2CF~!xIN^@d~$tidnnh#f-fCcDE?Hg zT7N26rLUP>#ZL$NG@D#65xMS{dJ8T662F$JU(|MukIFNR3+X?V zv1;f`Rp({OUB<_8o0nNWql|a9@Ssi5lpChT>1?KiOcX|4HS4W_~J^r~B=mf0pfVJ3Vp>>1H&>aqo!o!^V^7Clu~7c_nn4yb`)iUVBtt z^W7U8@@>q0a&~>?iHJsJB5K+JWI7drn^_=#uY8u%E=g z+^WSIN4tNDY6GV4z49!atol$m`R8=rr$zDb|8K>AQPtYHSkv=4t%&##qF^4w@Kx8> zQ1E0s(U!qUs zkNo%pzMn0wyHoRp@j^f7&&H>|e&}&av-s}e4Je1Q{ITBC=m#vWB78UW)W@|A@<{B- zc<+%o}1QTSuQ$P%QIf#U!icHm3S->$S;fmg);Eb2qW^0hz@ zInD{nZ9qBZ@3H)?nf&rSgom_zi1#GqD{rj{U4xY`%d^n3Su%272+K3lCMHC-PJFZR zA*yM63JdO#{2_e@GVQ9FAN~72#%TwXPZ%$7#GTJ|NO{b+N_x64pxb4z57WiJ1oC(Oep>iDCUm6ZyANyoCTo%&4SJkw z{2es@+B!ru=y9&`*ZfB_{$}Ou{CzX{i|=|%y?$;0-}N@we4C3ue}??d zpU@tthaR7ms@Ds?vfa}bjbAHpysllI(ZrPs_bpX;&r1}(QT;(QX7Xvbc6M8Q-(&H8 zzs6r>JAdeOr*Zyu!hn!I+AHn&GW5TxTwg8qQvOFerHK5`&$)%=kD`2X-Yw`qL=Psk zEXS2I$mjhB<$V=g)VDXM5DN9WVo68HbM)>l$`{x|&cr#I3slWWvdou8(>K9KP*FT#80hqv4L z(9F(1f&6o||1|K?zdIP}f%E4S%B@POFy9I})Bb(_h;oKo6nakG2KzU8az2aliq&l; z`{jKcXcHpz~;x?&WVR8aL?2X-?QCg!l&~Ha(zEuUUM??;ksso^|aD{Cbua*yB~0_`0R+q9siepv9?9@so1*2;xiu? zY!H7O**@;LYrVp@&)n|=h$fCmdfcbuuxO99Z=Ut6L=OtX(%!tRrlkErd6}k%{Q#%< z9S__@7@`oG`y9lTn~ zzfkfOPgx`u>if_NEzeRv2R(&ppubf)l@~YixgGDb-#3^mKUbt;kAIEsd`=zk)#%sY zf^+OAiT56R78bThdOo)nsr7OYc?EV%K9fHmyM}SH>RTGeX+M;l`)Rvg_0i@Vqp70` z@4nWa2SwkbNz?PKH);CT!;*d*#!S=N(&2kQcb+}k80 z$Hh=5`3^Dp-PoVc&VSK!<0rn8E%-`Ro#!fdnO%;5U+K2{v!V%G|LL=NiaoYYbfekj znAJDWyqwRsl-pG=(*Aq1&=Kn0L9~x@xdwh0^CRki=D1gX6XZz$wJGDj23US-!2H)H zefl`V?WQ8t%e;{LuYJ^qNz(^7VCA#fkKa4m_7RnL)#TmvxWeNz#=rL|JYjm$H=*!P z=*Qe{eST^BQKdUvuVH@``ll!8cTB%Re{<1&^>#6k@lUPhpL$RQ<#1sp|Fz8jkkx;? zK12K~k-xlE^&>0SkUsiT;(xK&pWBc5r@g9IsXglYZip}2Ubp98*}#2{8ebP$&a}9E zzwkf3Z(8GQ-!Bu{cTCg$G+N&G+a&$Ul19JwDX_rrW1YW z6^Vlf^*!U@ZGjzyqJ#cNq)lL z0X~<{T>Z%>QKk1M&G`K%kW)T)k<^#mPaF7$QPj)&E~LIrjY_`4!nNWb!Z-ojrRwbP zy@ZWFp`Co3^0PSK^%CFf6TCxhIe89NJ}pn|zZSm|(*0K%yZsF6A%2FEr1OKoAC5Qr zNym2OyYo4u=L5_i$oay6?oj_$82%~d5BNjB;dt>qccC}LKaTv~|G8dMKiQ9me!=~Z z*YjSK!*_55U%aGK8Zc;ncxXwtz#+biGWf`+kS`Ab-_YHwrJUb;k=p-F#_zTAjH>20 zd_HX4{KmQ-eLrD-!tT9|SGEh@c#Q`B@e&9Ezxb+FdAgqZdDlF8aQ?!0l>CU7Sbo?C z`=7G;d!J8krk_3FKjmH){X8u66&7}>eg<;l_$A~w%aK1Gzy2!t6Uy~2#*~7`M6}kuJ~wnoK=Mp9EcoH`GthUqt@$ zZted)AL~6A`#jTeCTDHx^-u7YlrGl44}Y@UF9Kgar~8r1LfY;80Bk>teR`+dxQ zzNVS}95Ma*iyn!Gh0bEjV(C8zYflIq>IL(A96+RKyvE}7ah>0a*O;8fza#JCHMXuf{&j=Z?uJ&< z+K5;A-guw(G0kss!ntUJOWgn6nSwAew?i*J%v+RG)`e6emCGhrk@CXhm5{E|B-S%B>XCZK8|BU`n(-a zZFm~#Gv$cN>ror}%Z8R36aujT_|Itlrb`hWHtxhV`-p(;u&^*YG9vZbGiG-VF+uhxNH}i#(gv zdk}AkpY@0JvINuj)YWVFl6toxS6HvsljsHdTK*FMYI2~zqW)l?rSOq3f64vsc7M&P z#~7_3-R|=~ClWBXY8kB;cqypq2~zb?T4%8`gWzJ@xc4K6d&z9jggyIj{=l1j6*OA{wpT$pZ?;aQ7oSf2a{0{lFA9S$3&5&y%r=T9b=X?Zz%olMT z+T!kfPVv%Tjr_tw&4m9me+Qmy*GQ&aj}IswpnYokRa(UFBa3(Iyh^-I$HP9~iFG=O z8zQ@xEbi(NIE2;()k$gXR0ZF-s7;I@3&ihHXdibhj|AWLE@Q3^5qN>u%_rsa@oA4gzsFdD7AG?8)`By@}LVR5R_x_1`>-jn|`3^Fl zwIj@THu7PS^X311!nte*P{J_-H4n#-Z8#Yj}Bz?XgBPGde`i}sCc)@Gmw+$ zhtp+ph-^P2{XBa{(sR{KjORMVUWzSe>3qnP>R-qw(#ibiz;42H>P-~v>+QRg_|(r7 zuRcxQvJo*+@Ro|lVMt!^SIQ8vtM&GWTL*LT=Y8Jg4SGh2ZB7c(o z{O%6|{rivCg!*@y=wGN`&i6y$JL{!OPuKJRQsfrKl{?5ko9D{sJ|pj4FUl9#IG}3! zaDm6KrVkfn^x*>2hXmjMWA9zy>#C~!@pF@28t~EbIBAJu(+6oS1X_er&`Jte%0mf4 zXhD=*uF{tJXo%7t&isZHu$1}=h^1DL+_cbCajXxd3Yw^l)KSNxqjpAT%#6-hA3x2E zGh-E<{?~Us&e`YOb8c=6sK3vD&ZnR5>~q&%d+qhwYd_ED8DF$6H(S~njhD=hK?FU+ z=V^L{q|lRJ{Ug>f$#Kww_8|VVae>>8bLhSGXGKruH^WWZuidQuZv|N7oX0-0Lw6j$ zhw`XzF%1Rm{s@dCP@jFzCm57|)Qx*!StxhX>tp(g932s!!5?t@gKbhS*e?DwT+&LV z$j+njkJxLleLus8O9&x7Va|9c>W3FHSRChD|6K=PpJ2Xy@GxC8-DSjl{qB-V7NZ1=|EJ=9~I89p9JRME}Y* zzhnBYe2Dn8RO*S&AEous(Th=~|Ku|0mv-B@J>0rRv)F>u1>Kis(F+sl*vJ^BmxdsaJ`Pof{uI8(oS6KvCQ=Gl9Nj_tz7@FLOw z;6WMZhgXmA?>0US7wG{2+V6Y?r*!UF z`6B#-yte~xBLD17x>r(*C)(GLAn7r`xs0-SgY$KGEKZxI@fV#pOyCc6A5zx({1)OT z;)Su4|2~rXBig4bc#h_3X#I77^P$>PFkRkb`~Z4v{yW&A?YmJLo>lKgt+#2jGL6cE z55Kz>^=?en+b{WT(ynm6(vwsF30pu7^ywH*UHgPS+c?6;If!TQp#I;I98AySd6N_Q zcatmDA?Bs-C75>=>XUfI^&eQ-w)LCH{wj=4)&*1afO40fpJdV_fogE0=zSJ{*AZUG zwKc`xzh(Ze6Mm{*_zl7*+V7I!yYZ)Y9DYMDVH!p6LtH+x!#3+L4|TrhZpLH!ZsRGm zXJM*6Gu|e0qvjs8?=%fxApE6!e>je_bH4VSIP0$!1%~ zj3?HIH!?ik%fs)Z^@Hg9H6GJ%>w7_^N%Q-Ko*|(Rko?zzq_iQKJZ83?cOHf*_XN(eiz{GQhILKBY21hc!Gz74wQQe z-M?}6H1heq4E%E~Y&tSii zrdRr{Dtg8Iz4qB;O!Qd&)0FOT96E~LDO@i01JZAn2A|LEDBJghw@r|Coh|JPw(B{l zii&?ch3@hk@gLzr?T?pgf4p4#A_|EhuQVC_43~QXe8&7@}1<4$K)J5 zD1IW6x5|&kova6TZa;Xflyk@5Y%e|X9(*5qLsE~~?zYMA7|*aBm_AT5paLk@bv68q z_}^H+ByLa38So3=c279aJGb;XLX8%dILec}?;XIJ<8t*rxSH-(9*#^duT@zgzPn|6uTT z%`4s_hl*wah2jIkI$pHckh97k-V_DIO9#?mD7@;{)6ygeUsGY(?ZzZuz~|r*deKcp<2$ z99n*%zpETte$4rF{iyfj%MTtb-!NMBJK{e4{$aAS=-hfm=z(8F9E{)lVOQPq-sCu} z`UUx{boRvq{4~ZCt)qJ@0bhLj8UK@Qt1%b>RQ3{pJJI zu4upcLjsHM#*EN=_ny!(|?*_7kc*FL>qEFO2bdNx$%)JQLv;4)6_ge&P2XJc>`tbC?123nRQ@ zN#AuZV4lM-Oe_$olz!d=Nq*sR9#Qy($2_QnU-+DduEH;TmRIim!fw`czwju}6!?V+ zyu#`i9^rK@ZCm?Q2fFW@6YiIClqFLemdQlg7+sDUdri9=|&ag+Y3+| zzc8O?82!Q-g?7#la8K?R9`H`({D*ly-7nnl&FB1meE!BSoL+bt=eK&NbNWH=3{Dpm z7I1oY;Y?09dkZ;TR5**%d%Uwb?I^sQ)AI|j;PftU5vLc>jWnR^;=(zczSryE^gVp? z)i1o8@2Bw#mlV$9_nma(4dAaRz~ubGa=H-*={1E5INiYaZu*653m0;}OgHM__Z0;? z=11wR-X)ygPHUS*aat){!)XuSv*Q=G7btw7-`~b3$o#@v zy>8AwRDfyvg^v_qT7KcL3S~}j@ZQMjCkk)k^pV2#oPM^jg453zR&x5q!YWSxnok7! zg|8N{eol5(cr&L@72d+>cc@0Zzt%$}NOs02>d4LtJ)9pcAawDG`d&_dL@h%8C-em| zq(3j*#ObdJYdQUOVI8NxDIl`-3r`nr=Ja482(9a5tydHoTwHa>G5Gu4sVo@(ZgQ zHgo#6hI={fZP>!;+J+Bu8a8a@^p=JXae72Df7%jwe%pX2mk!(*H_Ha^bj#KzBa`n<+3a5~8w=JbW$2&XS-{354E z^NpB(!EgLB=Z|lEg3}Wk|C-b3jbGvP#KtE%eQDz!PTL#5N-5%8`bYibzJrwTzPP@D zBk?CcMQ?s1{;+um>o*Oa_U}gJlIv&^AJO@PsC-pDPW)E%X63^5C*Y>#!VM(gT5{ob zCE)yAxRC_hv@@OTfv8L9*oJ-RsKf_YEeeG@mCY;C_<}*P4L) zk6gHU3Aq283)hi=+n)>9nSlFIE?jp4?w@ku)+FHmJ{PVp0rz*gaQz9mZ|1@cB;fug z7j9Pq?yqyrTMEKNoII0`9%JaD555J9FXs6L7cZ!VM(gZpnq)m4LfB7j7g0 z_l{h+y$QHAxp1QixYfCEV+pwHbK$%Xy82hjg`1p!yCxT|H37FQ7j9kx?zOpa9SOKs z=fZU+;FjjXbtmA?%Y|E$fO};wTwen2tX#PM1l;Mla03arQ*+^VCE(`f!i^;0PRxbd zn}BQ0g&R%4y(kxMECJV?3+HWh_3!9hxXB5)=jXz;Cg89#l^u7@OTabc!gVCz{($a2 zGmh&_z#Yhi>rTM^Iu~wD0`8Z&aD555pXS2#C*XdZ3pbE}`(ZBJt_0jaJT|Hy3U+0r$0BxUmG>S90OJ54rmH#ay__3Ao2|;aU@L63J)9lk*aApUlPAk%0SH zE?j2YJ0llvBmp-s z7jAC??xb9}(F9yuF5FlGZh9`9cfZTO7w5uFPQXpcg=!%ZNjk$2$3Aq0Oy3BrmO#<$>xo~|6xc|(B>rcS_Di>}b0r#_9xLpajf6av(Nx+Tf z!tG7KeLoj&Gy(VBT)43W+*7%5-iKZN`+6?i>4e4YKih54du}DnvHi?Z^6vMcPkyU&{#L{keP`a?SAu;K*spN$H3tv&=zSXa zt_Yr<$`_fQ-j*Gl`i0~9J@#dEiYjqFYhT6=!fWku_fcf-%jjV=e2-JV!$;ds=-z+w z{Hv(FSoEU@`%8IWhPc(}I|*pdztesT@bx%?vwfL(f6eQ$KW8GBPxHa{O#|+i#1Gp~ z6y5VRAp5~g{&p@UJc08|awIa?yQ8 zg;!9EAeT?5TrO3)%wYsR?M?sy?H|y6B-iOak~O-IWP|P_c|iA(4Cy|SQQb%KOAi{3 zeIrM4dQ{Nx-kJ;^jH77Z!$#o{T$c@vA$NU)U{xeZ1q#`HD=sv0DNDXhh{`bXz-&Zfi%Z zpXkBt`w6w^zA%$+G5C7uUX)4q1BXp_5$)YB{bVpG^04!>MNc};TJ1SCvpv{7Bk|u6 zz_0QsdLQKXweX#oiSITpXLxFr|IC%}_q$krvEK=~e5oS)MawPQ`1g|b3uWCu8OvS# zaO|i08NNsF0qAYn!g<>_A8LPJi>@Ow<+f7VW&K}Er{XpJTCRDKOMH$?i&obSBdvH`86K zz58*WYn2>iToChhjnZT7U8i|#Z){(P7>eF&#;y17GwYRc!Qs@4{=Mj(Gj6?qlUc8f z3l67V#1r;Cn_7H+EVEu27aUH#h+pj9{#x}uoLR4o3l67V#7kM{^V0p~{fT?Lfr8J;Ma4_b`Hk;Q1GDWXLzN=lfi?czu}dwOhE9U=y!Of zjK6{hJDJY#$`1W~x#pK@zFYI1niu;HuUyXgaLGbR+vZ7X_jql9b;Res={s$VCpt%7 z;h#Cq9HaOTaqFYPMn{aal} zPT+VNeg*w|xp@~O3S}B7Q_uMxw?U4_e&Dsi7r~G7CCu+O<#*P&iRuME2bRVA?Q4y*_A319tHB>VC&jzd(eFOR z^!2tp{d>^i7p5cOeU-`a@7N#k-sR)dzsLA@zr2@li0}VIeeU>Y{~t&X$SIR5d51C% zI*Honj)QhX-s?dH;c><-lPUgyo*swyxAM=dZ{h3Wagg1QSY)jRzxQ#w!Eex&=YQPx za=V;*zeA2D7!rP#cJ{Hpz-}Nv(8G9DexcfX(Gz`l@s1IHT)o11QdgfkNS|Ez`@r8p zX(x7P+^%+ZI=zBkoK5iMty{Q#^j(N5xux6jGKzCdFZ_b|kzhdilWvF31Gw}+zNq(Y zB(I{kkNFVv3ZAY;M3ce~$Oq?Ae!NiTnD{+0tA+kR`7V-|eV?eO@-3miWgqLM=_#GZ zUMR4@Fd?;b>X(>IzA<9b{=y_EdfUZ~K1DhE0} zk|*>DkIk=`-$DHt$GH0Nd$gO%|AOA5{DEH7FZELYz?jM#4gekKVtRU;|6TLK-`?gi z&KJE8GhZFUJqO zkNkg@ANbwX(5r_;KWg-c1B4g+`U>eqkKQYc?-%3gsZ_XpZ;N~f<51=aA0WJ0d_jG# zpRD4`IP-*e)XA4O=kVow%$G^rK35-^F2~QUqJG2fS&qkN+HVaFRoPv-{OdVx8DO}0 zo>9tG`APLJQT(UhNpkglDpBm}JK{~~%^Z3U`E4bA4{xGB=rO;Wwx6Q+>v8pA4&ep9 z+gTsxFnm|xOsWa`@KVnAwtSQA$1lvpYwABAW&`sJ>kH6hzwj;I5KrgC8epgN-OL6= zzKeT3x_ROV@Z9p_6bH@bI`^Q(5Y^Y1-Fr*c}0&hOEEp+Oa${{=DA zeL}mmKB*_%I;452hvq3HzfG#cy+6ssvsCe{sT&M$Oue@gQp%AZJHD&G#3 z*B;F;)%;$~n>?3t`BLR%{k?QKHR#6)1YGQ-*x$U@Ki4WuZswK{k zzf*P~MTeK7PdXn9Hc;Fl{6+i?Kaq~VU3~Ck(Et0%KhbyN*xuaV z)B1l_rr!2(x!9k0>{nxd(!}}LpG?-g$!A36WqPqr`5<<5IR50-nf^r1Bm6P^$s^>0 z!#e!Q^_l#9Qta*!{0ZWKYCr!4#Z$jOIo7c+zwjB3udDsZRF$v!lUB`}KbfO>ljouP zlaD9pcu?th_Wj9~PCc#OS7OYxZ-pQ1l^seJ6mvhsu2FZISN;xc+V>jTeK#0j9^r z`*OxR#^u76<%;)!=DV#PvA33mnxCxs4$V*1{8G)gYQEEcSA4DZyXNO;UhIm#=d5{8 z%P(hoNlz4hs{Sr|O84(-Ui5?RC(*pBS1UFD-1S!vX43fqrZe1Rd|tdT^?~B-v^bB3hX{jRX1$2+j zxbn-6vtG%32sia{`B;uppT(!d-zxm$$!inKOZXG}ci~SgFX4|XFX~=ndFgl&{94HG zg+KB4YUN_PT5>FOXN^~hgJwVJs3x3jYHk>=i8`VVIA?}v`l+huktqkWc`on zkM%zmCs_Z}ndpCd^gE2kw|HK4i}nwDxt^HsreDiNkD~8QuhG1$579b3=T}v(w)!|9 zuLG^*d@%H+>gPAL9+gXFzve|>N)`PMqUvW)12!^lo8!yZF=g-=DC5Ru?L_kISvn@#^Z9-Ar#! znU{M({=S?41=Rd})6URzg-=4Zq|ki9cp>^F9+oL;VYh2K4uS8Lw< z!)rL-GbH;3YqjrdnRf9ww=drx2p!MFFHB@P314FU6u!iMVTjvr`VF~ZzA*CRl~3{S zMQ?=Zt<}!;)c;k_S3SVx4#)m~wWdD%e;|{;1*SXhKmOPLA&9W$ncM&4B$uEvzWrmq z+4LyfD*C*ZCSO|VebHM-|JRlF#oogf*$-^-?z0gWT$HJ&FK0gF`vIeC3;QM4{?dLrJ?^`MASV((Dh&m>w)ks z)&t>NtOvq3R}Zj06zjqFg|8}ayBC|*ExEt%)$d@^xP5C;_udcr{?=LFL7L`-zOx$puhL&@6r#1&EkKk zf7RcmKc@am^Pg7uR?R=G`N^6e)BIG;AJF^(w$rfr`K;hH@1nmSr+JxY2%BHbd6yqJ zLG1Da^Exg+UO@b?d$wVZlc~nuX4y}-dNX#%f!{JeMBgQ4{N6*ar>Hcw^ zN22w8%}>_f_bZ+z&X=2itoc^Wk7|CQ!tc}kJk9Ub{8WYiy5{F-evjstD*SfMcj)f} znqR5;`!wIl`PI!^HQ&wo-e%b^98}h5evAITobzEbF^wJ@FNgYlR$5=?-^>+gM9 z{v^$B)V!?6(>_(rzf^x8(Y$Qj2sg;OHtjRy_x3#v(tpi!G4glo)jtWo@yG9nKHs^j`B~hh#6B7dxf>IhsGf_z(}{iT$9+J@^yP_Z8{( ze9%>MA0EvEV;$?}F3k6dfA1=Y{#v_wwmrpode4@9-hSf(p!NL57qesV8&BYQo$B*R z5?8r;a|_i+<1V3V2cwbwdGy}hCoq6KjZN@if7W>ClNP>~;D-br=hJqh{Cdf|`|?hp z{a|Pp_IMMeO4h}gc<T&iB{E}i<<72e1MDM$f_faz1 zXkT%3Z+YeKm@nlP{oX}I_+F0A35@dZcFraGo@L8Me!oh;|G2uPpYxy_^KEt>qqI%% zmKVrAnbJ0i`^pP23MaU2eS&|%v6>gW;er=&e$_U?yL!P)&WCN-rq(y>1cnP2OB&3gO=|RnizN+aVb?VBk_Iy*UJ9p6+B;wHpgy#- zDBYBYJmgiLkJ9n^z*=@NLUM#19M5(z!hGoBTaM#>Zao8EA6g=o$>p$TM+g6 z^gS%lQEdkYxSXpu&(%52?_HL(gP|Ndc%0U)v+Q6t*@4|_AL~6ah8~xG@XOWv_mDxA z^!s@}O#?y&?Ed+{IlqbbwqMZft;nnG!e`c3y%#>vd*n;oo)Ep%{qOeOuJQtjXUe+I z-0+$n+WvC115atDq(1Lzg8w>#<>tm$e8 zG{1oF8&|;3=IGahs$W;DeibF|@QbQn#Xr4%y{k_BT9cz+-|x!Ouj>7NY5n>Q`BhiH z#NKuMG_HP$zcV>j>6Ly5Zd`q;Az$ItVmv;H*RUmx2WYOJ-t zeiiA5eaEiIUo#+mxk2M{jjMW8uDv%@?}u!~d;pOR`Dg8?Uv@d>n{}KpDC2{w{q*Ep z&~pEXm(l;U=l=+aQ(XT&FV)|jPNEKm=5fDN+Nu8h$Z3oxcu44ne#1kRoBJ7Wd`{^K z&eyYl{Zy(qYo9jLvz^g+6N&gS(C5a> z-2S;Lz8Szi-$A91oVCq6ZlqW6lLy{H{Htic9<956UHbW!ejY!@b}Q=>t~~aq)~8V) z?85N>g7M3GZrJj6o#z+4Vas1Ae!*LAxt;Unt!p@??{Mjo0 z8(gZK&h03-oFw{DnXmadoI3N+O4m|;Py2~EzpB#7`PI7a6bvood@XuzPtZdOO^>Z> zVB7&c47Q10mS?IyN|>LB>qd#Mkk2FJ_dmc`$nWkH`fJtycBg*R|9Jc(^cx-JHmzUd zr=s^OZeNe`$@bm2`^e{|Q_4c zJBi{dT4!KB#`Y%ttHn`8{_0NcamtJYp6d7!aixtTEN(G7uy`dH+{1K)sc}bizl`d& z+aCC5=;tw9{}`9=DxlYaor=8D_tq476qWruiM;yzpEdMrKAc^?uBZ)TJD8vdY#-0)pQNdjQCBjmwTa_-XPx>Yr0;(FV?iedjYlEZ+e@2U##hR z`MwzCp@)9c8u`9h(^~nySkpTBzF1Qq$8&zuM)|&2)1C5tv8GM(eX*u@@qReJ>D}^u zv8MOP_r;pt%U^u=o8HIY8}ys{JMVe?~65k%!3Q_oBoRJ#cz7ddnxBHm+y-;y>SkvUfLWVnptvHity!M7tk7?BsfH%IyBbN+jruZ4GFCf+>}9@t&cD@A$Ca2LI=M|s#^(UW+v zRz3IphUnj^^dI>+^Bw%dNTTTJ{pXZFhVvuze=@#F^J9$kt(qU7;Bvr=kiY0%#Q9ou z{Uj65P=x0=#E zqUa4odCY9kIq)bCKUDPYi}EkwysQJG-SA6APu6{rZ{_@E`oA_mZy@=UDjggTbm{jY zi~JR_+U15b`Pawwq5c{4m(6z|Ka=yf^Y69f^65;x?}+fuVmz|m0=)3AMK6ec$5cwu zTN~x!Z;PIkuT}4Zne|>D;h95!!B5&l=0kd|<91=YcP)fYPtoh*v{rpv{tJ9h+x2oT zhjyV~z^cDEHKZa4#W8=RfM>j&xLV6#qI}!X@ zlsEcpUIP2%VOmifJ4hLNaBh2#?hoptJmT0pS3_@Qe}dirWBXCUX7S&)k0opte@ycu zzX!gsS^R6fPv&vFCVT_@d%5`u&Ic9Q7ZWz`(Y)+$37hwF-lcmVy>aIWJ*9Uey$8L$ zMDGf+^H$2+zR*jzP=+4b$HDP-xp}wd<$P1QSpedFPK1aayLX{1>K)Ce*OKE*iq}e& zR`vh)kt|TJ*h|rSlHb=V|HJ=|?F8T91s%Z-@mJ}1%*KTjM~#a&NZ9RVf_E?nug+80 zeO`gWg>z-!ki{Q#j}7zFZ4dOvm5sL%pbpI2#qv*+g2jjQ3(`_He z6X=#~zPwX+14^$Fg1@j{lfL^+C@*1pU?VYQ7$` z*^~XtI#8Owwfufme_aOaW+mEE}&ishsyZw;pu3jG+&;MCHYnAR(neO)< zHr?~6B>4T7L!!H-s-GQ?ZmXwT>7LJY%XsRU=*g9bM0evM({22|Q0aad)7?$~AD*7{ zQ91DY9Ksj(|9>X^&5(@OT{}wmo7T@P)ONIi|GE8)+2>cee{}J(zGU<_=D&?kEzBq3 zPaG%x`Sdr|KN>$KsGiIv{-pgTt&dBbn2NuO-diMo(D*hhe#!GYW>^2U>QMaUV75Fo zuFdLCt)Bf<5Bi0ZGTN7pD~sN1r5;R{&|~}iPHddO^Y4h`kD~XspZ`%zcG6SyuH$rg z9VdKGAnLdITg?A1biM}#ds=%5#VMk0G)p3X7<_cUp@Xk~9DHx7iBHxcUHV$7N~iR5vou6b#H*xadk;cwWyRP!Rgu(?C?2ekY`&fES7%n#tp z({`@Kec!?F(L0cwdN|>8SCjdDE5C5xkI|dxoR$MW!tY6e>2cw^DR1){VcSalOzWSV z#N+P_yJ{kke7lkf6PN$<2)>q{J-<#lj;CLZHRV{%cj$i>-(hET{)!pl@=N&JTJ?T} z-emE;pWlzi_YQ)pkM9`A=JWj;#&alq|Hq4;U4QoNn)s^y*$7cwOE11!6W^ixv*DWf zs{I-4vKD=h0AC&cXJ8yW4+9Uu;HmaMVwW^-RR7VD1rN-z??4tH$yP8^6uh`XicCv=c)#INqW1Md%r9}N%{pigk zkMJnr->@vtcoRv^{Z!K8(5!O3S^A}MYqU?)!8bAvzEw5xss20oz~5T*U0V~M>c4{z z`cMmBS517tp(nC~Es}Tfuw5!&v*d7Y4Y~sT{sHEj@N~(wINort z*fzNezMJ0*W9=L^_CL(S`#Spj=M!AMJzYt^l3dqu zQdb=P!Yui$m-1P0G}=|m4^OCxue#r1JJI^G^yhaM)#?|k`yI%&7JXwi@m2RbYPeJR5;0S5Jjuv0koWxzPH7#!D+TZ}CvK$j#9| z#Ai0HbM+GajE(Qy_Yyye{=4sCw`K?Kd-#X2d0|z(dG;_ykXdon2)}Qo|A}7?{r!~B zvWISd4=ql@&*S&nAGG!1=eqg<%%kMn!v&1TwFlHwORlf3sc+Tx&^->m^J?O&wuiBC z@SRl?U$s3zp0(P0D)7}|573udc-nx6bPZ3n-!{Lj@kuoQVD=z-81HL`l#==ZKar(> zdH%SS;ip#N8KE~}^W>`c{rukauakeT#~)M3QbYf+JA~RH?x3zXTM>zeqg^>1Vxd_M)gI`n0196Y1IldCWDs5I6IOh4vm-uz`N=UsmzdQdHI z(SylV@ZIz#o@WyMt4H2PP`P}6^HQnj5ac~vQ$E%HW;}U6Qm1@Ih|*em^Kec1j4zKo z`-5I&*O zex>jHFrdvx1l!avFHpa*P5t(QR)O25{r&=xd$3LCE%ZB*wqMck**UpG`3@w`dx6fS zbneF1Mf}2YrBmylyPWB_?`no~J0%`(S<0!qPRr-V)J}nSQ3CI4Q+U^;@UB$6-Q(gt zErIuv6yA*~ynTvy-MDz~r*(%_x;_-2bHHv7&s#P0595|!z~>J4FK9uI?DX+%)s5Jf-E)Z@#!0V?yJO6%MI#0~RgI)Tj>1qA6_I$MnSYjc8ulAGYjC&`bLhIq$YJt+(%@ z^OSKtPw4kjcBk+TrSR?=7w^jxdPAKsJw>{e3~9;u+5BZ>DX;u&9^t&xKH-GH zHNQslb2PtB^Yb*{r}>4N->CTx&2N!>bGNp)U-RPMOZpw=vfj&Bs)*i|n`Hvs&g+KF zw`lnxEq@2+>3l!uS2w?p^X@(nkM!L3pJRT0DM7#<9(oh{&nFEZ%QuYnm5aQc{qp>~ zeSetFr7Qf9mKQw?n+G&6dRUJ3&x;=0KJ{?3?jO^8^)H|vQanD(LjB5a##?SS{WCwRanKTC zDm_K-rwo_2pdUhyjhk$I0e^s+&HvhX6n+@* zQU0w|zFKc&d}4mb&Ur=nbR6aC>EkqiS&hf!|KHpmlhd=&?zg>~?59ciT#K$Jh%UPq zF@h@zU$gk}M#5|U_t~WPm5@^%^xk)f^uCPnR?%C-zDG3f{0P+(-+OZoGogL7xi9`qW69AJ5G7fHEF%j*)yp3olIA{iO2%|N%s_R zIkXq~$gfu2*YhpGpV1$lOns)^OV`ze*2n!EjZk^VNm*Kv>hJ|uCT8$Y6c zvjf=O(Hu|gWq6B+<9I^uYpzxP1+4E@-r@;xXk)!R=qocIZ|6Tr<&!F`zbW)0NfERS9^V&+9=eJ#Tf(?}XojoaOV} zzk@!OB#*d>REFcG3tr{Yc_IS@>%bMnCBz_l&|eOprHg%(cja;=`fY8e&$s%(kG?1> zH2tfOzZb~3@36l zeaw>kD;ZAK7grsIypJG$#PU6Y>8d5)HprKuAXk}}1$>@dTM0g2ek2fj4n=-r=N~*6 z$xZ3-vEh}TEV-RhLvBUZdhso;zD51mY^fk?K46saLZ229|E+)U3-h@^6erA;`J`Y_ z`-`~)tS@xVSM;VL_#s!|bNh$W5_oaC$caDZ+Bnbfsvp*QQMcbn2q505p>Ajo$=PI4gFu!R1*{UJ=u2L2~Xn%}GWjhf%1`94nF`B3T} z6aE%G5A0(?8S|l?On*GDI4b2+_67KtTtRkRVsKs`V!!CDiz5%X<5O_+=)Ktw<`s|! z+;IfAYMu0hQUC7X?{vxm{_)owJZN;9e4_hMYOI5zT)Ymth_J$5rJdni<@ccScdp38 zdJ$`8ArKqxp53pQrgg%`epa zM$LCd-+>xU=Rj5V5#$n;4D)a(dw3UxnW@#&7Q=I2(Ui{A&KGDsE5pORipH z-534DDr&E-_jVP|;QH%XM`)%6@^~G<{98MhuV)?M&F8u6NLF6rxNz$_mQS=^6n+26 z*4ah>Eslx5|1>6Y*Y7{|sNULlDuO!J*{{l^>lkiVxOGj6uE#WQbcr3rbPe$D@%|C9 ztC%jayL#walAsG(7WX6Y_jRqi=sD?l-3?ksc%ryo>^89dO=pYU_G~}E?dZK|ueN7Y z@=M;x<--MX4mNC~AcdaNpvb4(CUlktKg96%U7}#HkMm*MaSR^}Zq&T2-}DUj3;woQ zick1jZWH||4GQ1Ox-Tyn6h4P-rz<|;bJ+F@&i4!opL^TR=X}_FiKLrP7Wm2)+Hc%OAw(BN0(fBOb12QS^9s$Un|)iq zS5N%*E25*M``Ds+!6EsMnrr{*e(iVX#`B5hPl|l&D)8BN*u?KY<9Wq8;+sp!zr^vA z@XPkSji>Jh7tae4qv?s~1tAsaz36+`Ci6P+JmF0JUGy&8{5;Ov{(M_c4BI3gwE6Wc zz1YNhA>(?;oAsf@=%P3u^H$U775wqoxzCP&hkSwWV+nnj!4;QOA8fuTZ25`ke~Z{P z?JLr}*t@GYW9YB93H=j^ZdZ@6E@JI?E9tMzCwJ+7%AU%KY*0Pa?>L0bQ}{i7zf<9+ao*%)>taRk|MKry z`u2CEW3~3I(+0X@TIPFT-Evae?&j@Yky*3EjhpXur<$iBYPQ1!iyfVc52 z^z$m(-)Q}%ulrDJ-YMR9EA6oPd7C#eJ1nwQp+6ks`p5GRX}*>8{B`fv`q%Z}e<97c z3R^w@NSSYp>^mcFdxAm;pS6qRpyjI8zh;YkvgYZ`o_|8~EjI3pzQ?He^1r7h{i1!J z6!VrobMg)`=r~?BRv?B_F^7-H}qn?w%@V)J4sKW z|A-Xa_2U1$faYV4kcP+KQJcjG+;+JB3Ho+IqMxhoSI~~M{oB3NaX%>WOxzD{VZDpT z^?jNbdkCX=RP@v0QM3#7*}m}jd!*`Lwh7+y0%>QkP4I>b)K5Ia@1~{g0Q|Xx{fGD` zlV?^Oa|7vfR=fiVxZ|3;Nv>|ZvlPE)*vBcP4~G)(j1XSX`xMbj_xGybnM>FyKlB9W zT|EH2>vw*z81asji{l;bAN4z2@%~;}SC9AiO226KVf}~A=k)0OPH(ioSNwS`disby zYe!5EHIN>QgXsQH)hp|tMW155JD|TyeKtQ|ioPq>KQ3Qh$Nt6mV)h~U#_Jz#-$NXq zs~+q6s>T0tJT49|7#w1{!nSGB-oXLQtDful#Kt9ojYl+&Pxp^;zbfr5>pAyuv*`7-mTdc8VL&jjL6ExufGD0GNAMfcIU z_BN*e;$q@cN#g|D_gWhIPmyzl*~jzsfOq}H=ZQ{>FXDZ#VsEeu`bX!%#9q_qdV-w- zm!`+X{}j(-i#_{=|2znMc5jO9Q?PS|;bw3V@gpY`$IAL~t?{AF_ngG--^cBZ+duxk z(fhgmQZ8=)DB~Hw{Q~ElRMynJ1afe%M{CYHxzmeb9YTpYp>5}n4L|5~9Om9S2a|h=wKDK>2 z79WSYFE)Q)^tD_*Y+EL2^QD}|`(Rr&uk$I*GdUlhCpcB}>Ytm>JB$5^_-XT>S#im-qv4l@E*CBxm%NtY zs`sxfrIxt<`*!+{Q5Sy|Dd|^trQrS=>lPZT(`Rnjj<{MtbaI$~Q#dm;ycj10W`@C!)bE&6^4a+Z( zQ0S?(&*ImM9>ybR+$q=6BDH&?XvRHpKGC+sE>x!&kKXQ&i67 z@0aQKQV+C_?5I%W?^dPjbKyTv@I{;VT*t-u9tdElCEw40j`afX;#)u=XO}1bq^Ad3 zEp(titVPGeIdlL+Ejk{|q2qnOh5Q$wdau+o$|?BKNPpF$<3l-g07ET0?#ZF!`U&92 ziCllF2Ub}hKQ`si0SvY1xGjf{V`-z^;n;sb_-tHvDE5CN=um&^>c@A8{%2zU`&hr; z0X9Z)c$WQ7WxHNe1(&w}V;OFB6zsvl)z6vgF|JWDSrQcDr_Gk4cKW8{ucS-AG z><`G%(o;*`XAz&fyvwWTO8bS=8SXUw|z>m0k_}>S>H(5VA6#wwNI_Wrq>1eH@V>Cs_UUEoXx-a!m{KM}$ z>XXZV5nPvj-%0He`UpSb^5bgumva6gjt^wrBdwoqxt|jLdhNNE9*;VBx(X7fTbyBb zO%ru0p3f zZrDNfMdwj=5Rvqt9iYSgZsr*BDvx^RTmKvU8B%;r^nGxYxBl0eZ~Z>S7g_VIOI`(j zi9d1aWxAa5`-o5RIUIoxobS@2|Fm@so4=JPBR>CQ`&r}p(LFpauzNGCe6%jwChfBM z5R*%9Oa1ft>(7Cn6JzKBzsb!+{e!DFzo7F9_3CS+S5q0S_o#$tUhfS#bg4YC9!RRf z>olv0uAcRvM*4lERrciY|6L?!i^r<$#ev&EaF}b&zIa1U=C@i0+q&=c9EVCE8yi{9UE{OIDw~i{%k)mwHz(n92BTJw9xf z_4ROx%p;qAnf=#V2mKJo39=u_{A5;~cRcYwYupV_;M)EBX#mp2UnPzG0`N^v!3~nW zcJbGrlW?smxH7}(xvd@&B0U&i!0@Z=+ZnGn620}rQ~z@g=%}6#Mon(Jpee53TZ-S+ z54mu-H?nTOCt~zUM-wu7mQkA^Xy_GONxOXZkI$t=_tT*BBDCgJ7)-*t?Z@twr>D0+hKS|}BM1p%x=?2^=rbqe|R<7Iv=Sho;aue|m5{F}SK zHShdZFX^$J6Prz8@;oZpA?7WlU-S$0B`-)sU zUyk21?EKMZY5&m{qLbI#NlqnQZ|f?^dQfTTKH*b^h@b~@1H8K)Fz1z6pWVW6c3(m; zB>f5Wm{caZFE!l2Xh@%+Wmqp&I09lg-6xn{mnl_*j&e)L^^__f7poZr440PTkt|ue{C^w#<|AN_W_@kn^?hoKF5N=Ial% zyp#(!&EdT9Io#UH`K~7658@P3p=ke#^{Yis{2%gN^t<_q_Vx4v@Wf1hU*Rmvt&`+t z`?%nz@$fzZidVv|GVdQ$mUI2K4RCZ#wbGw! zHT4S;FSz*9{D)oG`a@UY5yoe73o1(`KPdH}J@6A~&+RnT5^mjFMQe30{5?M~BwGRZxr|9-&}>5cNIqVy17(&cA4 z<>P$@_ayL?2E`uAZ5rPVuH)a$F9(C7{}!k942u5uw$0>x8KE*EYVKN@c65IoyN9^rzenwR#43m`?wz3;YSGD6O0G>*&5>>-P;V-p6bENe+m-_5O-@AMZ3q6z}7m&G|t0 zpO}9NWm=!k@lWF4!_AN?^hx!wO#+Qt=lds-Z|LINEn6E z{9*^8#uIM81wOg)&;qifm|y>_<;4$|wh7
Q1c8pY1czN~+@emdAL<%-@V!lzB* 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 z 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%0DgD}_A0RLi+Bo_C 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 { return sendAndConfirmIxs( this.provider, - await this.client.transferNativeTokens(this.publicKey, params), + await this.client.transferTokens(this.publicKey, params), { signers }, ); } - async transferWrappedTokens(params: TransferWrappedParameters): Promise { + async completeTransfer(vaa: VaaMessage): Promise { return sendAndConfirmIxs( this.provider, - await this.client.transferWrappedTokens(this.publicKey, params), - ); - } - - async completeNativeTransfer( - vaa: VaaMessage, - recipientTokenAccount: PublicKey, - ): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.completeNativeTransfer(this.publicKey, vaa, recipientTokenAccount), - ); - } - - async completeWrappedTransfer( - vaa: VaaMessage, - recipientTokenAccount: PublicKey, - ): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.completeWrappedTransfer(this.publicKey, vaa, recipientTokenAccount), + await this.client.completeTransfer(this.publicKey, vaa), ); } @@ -254,17 +234,18 @@ const guardians = new mocks.MockGuardians(0, [guardianKey]); export class WormholeCoreWrapper { public readonly provider: AnchorProvider; public readonly client: SolanaWormholeCore; + public readonly pda: WormholeCorePdas; private sequence = 0n; constructor(provider: AnchorProvider) { this.provider = provider; - this.client = new SolanaWormholeCore( WormholeContracts.Network, 'Solana', provider.connection, WormholeContracts.addresses, ); + this.pda = new WormholeCorePdas(WormholeContracts.coreBridge); } async initialize() { @@ -274,27 +255,13 @@ export class WormholeCoreWrapper { Array.from(encoding.hex.decode('beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe')), //address associated with the private key ]; - //const guardianSet = await $.airdrop(Keypair.generate()); - const guardianSet = PublicKey.findProgramAddressSync( - [Buffer.from('GuardianSet'), Buffer.from([0, 0, 0, 0])], - WormholeContracts.coreBridge, - )[0]; - const bridge = PublicKey.findProgramAddressSync( - [Buffer.from('Bridge')], - WormholeContracts.coreBridge, - )[0]; - const feeCollector = PublicKey.findProgramAddressSync( - [Buffer.from('fee_collector')], - WormholeContracts.coreBridge, - )[0]; - // https://github.com/wormhole-foundation/wormhole/blob/main/solana/bridge/program/src/api/initialize.rs const ix = await this.client.coreBridge.methods .initialize(guardianSetExpirationTime, fee, initialGuardians) .accounts({ - bridge, - guardianSet, - feeCollector, + bridge: this.pda.bridge(), + guardianSet: this.pda.guardianSet(), + feeCollector: this.pda.feeCollector(), payer: this.provider.publicKey, }) .instruction(); @@ -329,27 +296,27 @@ export class WormholeCoreWrapper { } /** - * `source`: the origin of the token (chain and mint) + * `source`: the peers (Token Bridge and TBR) emitting the transfer. + * `token`: the origin of the token (chain and mint). */ async postVaa( payer: Keypair, - source: { chain: Chain; address: UniversalAddress }, - mint: PublicKey, - message_: { recipient: UniversalAddress; gasDropoff: number; unwrapIntent?: boolean }, + token: { amount: bigint; chain: Chain; address: UniversalAddress }, + source: { chain: Chain; tokenBridge: UniversalAddress; tbrPeer: UniversalAddress }, + message: { recipient: UniversalAddress; gasDropoff: number; unwrapIntent: boolean }, ): Promise { - const message = { unwrapIntent: false, ...message_ }; const seq = this.sequence++; const timestamp = await getBlockTime(this.client.connection); - const emittingPeer = new mocks.MockEmitter(source.address, source.chain, seq); + const emittingPeer = new mocks.MockEmitter(source.tokenBridge, source.chain, seq); const payload = serializePayload('TokenBridge:TransferWithPayload', { - token: { amount: 123n, address: new UniversalAddress(mint.toBuffer()), chain: 'Solana' }, + token, to: { address: new UniversalAddress($.pubkey.from(testProgramKeypair).toBuffer()), chain: 'Solana', }, - from: source.address, + from: source.tbrPeer, payload: serializeTbrV3Message(message), }); @@ -373,6 +340,7 @@ export class TokenBridgeWrapper { static sequence = 100n; public readonly provider: TestProvider; public readonly client: SolanaTokenBridge; + public readonly pda: TokenBridgePdas; constructor(provider: TestProvider) { this.provider = provider; @@ -382,19 +350,15 @@ export class TokenBridgeWrapper { provider.connection, WormholeContracts.addresses, ); + this.pda = new TokenBridgePdas(WormholeContracts.tokenBridge); } async initialize() { - const config = PublicKey.findProgramAddressSync( - [Buffer.from('config')], - WormholeContracts.tokenBridge, - )[0]; - const ix = await this.client.tokenBridge.methods .initialize(WormholeContracts.coreBridge) .accounts({ payer: this.provider.publicKey, - config, + config: this.pda.config(), }) .instruction(); @@ -432,31 +396,14 @@ export class TokenBridgeWrapper { Buffer.from(vaa.hash), ); - const chainBytes = Buffer.alloc(2); - chainBytes.writeUInt16BE(chainToChainId(chain)); - const sequenceBytes = Buffer.alloc(8); - sequenceBytes.writeBigUInt64BE(sequence); - const endpoint = PublicKey.findProgramAddressSync( - [chainBytes, address.toUint8Array()], - WormholeContracts.tokenBridge, - )[0]; - const config = PublicKey.findProgramAddressSync( - [Buffer.from('config')], - WormholeContracts.tokenBridge, - )[0]; - const claim = PublicKey.findProgramAddressSync( - [emitterAddress.toUint8Array(), Buffer.from([0, 1]), sequenceBytes], - WormholeContracts.tokenBridge, - )[0]; - const ix = await this.client.tokenBridge.methods .registerChain() .accounts({ payer: this.provider.publicKey, vaa: vaaAddress, - endpoint, - config, - claim, + endpoint: this.pda.endpoint(chain, address), + config: this.pda.config(), + claim: this.pda.claim(emitterAddress, sequence), wormholeProgram: WormholeContracts.coreBridge, rent: SYSVAR_RENT_PUBKEY, systemProgram: SystemProgram.programId, @@ -464,4 +411,44 @@ export class TokenBridgeWrapper { .instruction(); await sendAndConfirmIxs(this.provider, ix); } + + async attestToken( + emitter: UniversalAddress, + chain: Chain, + mint: UniversalAddress, + info: { decimals: number }, + ) { + const signer = new SolanaSendSigner( + this.client.connection, + 'Solana', + this.provider.keypair, + false, + {}, + ); + + const sequence = TokenBridgeWrapper.sequence++; + const timestamp = await getBlockTime(this.client.connection); + const rawVaa = createVAA('TokenBridge:AttestMeta', { + guardianSet: 0, + timestamp, + nonce: 0, + emitterChain: chain, + emitterAddress: emitter, + sequence, + consistencyLevel: 1, + signatures: [], + payload: { + decimals: info.decimals, + symbol: '12345678901234567890123456789012', + name: '12345678901234567890123456789012', + token: { chain, address: mint }, + }, + }); + const vaa = guardians.addSignatures(rawVaa, [0]); + const txsPostVaa = this.client.coreBridge.postVaa(this.provider.publicKey, vaa); + await signAndSendWait(txsPostVaa, signer); + + const txsAttest = this.client.submitAttestation(rawVaa, this.provider.publicKey); + await signAndSendWait(txsAttest, signer); + } } diff --git a/solana/tests/utils/helpers.ts b/solana/tests/utils/helpers.ts index 6f7aabf2..2d37f0fb 100644 --- a/solana/tests/utils/helpers.ts +++ b/solana/tests/utils/helpers.ts @@ -322,6 +322,7 @@ export class WormholeContracts { private static core: PublicKey = PublicKey.default; private static token: PublicKey; + private static mplTokenMetadata: PublicKey; static get coreBridge(): PublicKey { WormholeContracts.init(); @@ -331,6 +332,11 @@ export class WormholeContracts { WormholeContracts.init(); return WormholeContracts.token; } + static get splMetadata(): PublicKey { + WormholeContracts.init(); + return WormholeContracts.mplTokenMetadata; + } + static get addresses(): Contracts { WormholeContracts.init(); return { @@ -349,6 +355,9 @@ export class WormholeContracts { WormholeContracts.token = new PublicKey( anchorCfg.test.genesis.find((cfg: any) => cfg.name == 'wormhole-bridge').address, ); + WormholeContracts.mplTokenMetadata = new PublicKey( + anchorCfg.test.genesis.find((cfg: any) => cfg.name == 'mpl-token-metadata').address, + ); } } } diff --git a/solana/tests/utils/wormhole-pdas.ts b/solana/tests/utils/wormhole-pdas.ts new file mode 100644 index 00000000..10fab331 --- /dev/null +++ b/solana/tests/utils/wormhole-pdas.ts @@ -0,0 +1,49 @@ +import { PublicKey } from '@solana/web3.js'; +import { Chain, chainToChainId } from '@wormhole-foundation/sdk-base'; +import { UniversalAddress } from '@wormhole-foundation/sdk-definitions'; + +export class WormholeCorePdas { + constructor(public programId: PublicKey) {} + + private pda(...seeds: Array) { + return PublicKey.findProgramAddressSync(seeds, this.programId)[0]; + } + + guardianSet() { + return this.pda(Buffer.from('GuardianSet'), Buffer.from([0, 0, 0, 0])); + } + + bridge() { + return this.pda(Buffer.from('Bridge')); + } + + feeCollector() { + return this.pda(Buffer.from('fee_collector')); + } +} + +export class TokenBridgePdas { + constructor(public programId: PublicKey) {} + + private pda(...seeds: Array) { + return PublicKey.findProgramAddressSync(seeds, this.programId)[0]; + } + + config() { + return this.pda(Buffer.from('config')); + } + + endpoint(chain: Chain, address: UniversalAddress) { + const chainBytes = Buffer.alloc(2); + chainBytes.writeUInt16BE(chainToChainId(chain)); + + return this.pda(chainBytes, address.toUint8Array()); + } + + claim(emitterAddress: UniversalAddress, sequence: bigint) { + const sequenceBytes = Buffer.alloc(8); + sequenceBytes.writeBigUInt64BE(sequence); + + return this.pda(emitterAddress.toUint8Array(), Buffer.from([0, 1]), sequenceBytes); + } +} diff --git a/target/idl/token_bridge_relayer.json b/target/idl/token_bridge_relayer.json index 19f14f82..bd42dbd4 100644 --- a/target/idl/token_bridge_relayer.json +++ b/target/idl/token_bridge_relayer.json @@ -1990,6 +1990,14 @@ "name": "evm_transaction_size", "type": "u64" }, + { + "name": "mint_authority", + "docs": [ + "The mint authority used by the Token Bridge. Used to check whether a transfer is native", + "or wrapped." + ], + "type": "pubkey" + }, { "name": "sender_bump", "type": "u8" diff --git a/target/types/token_bridge_relayer.ts b/target/types/token_bridge_relayer.ts index 59103e75..d97be39f 100644 --- a/target/types/token_bridge_relayer.ts +++ b/target/types/token_bridge_relayer.ts @@ -1996,6 +1996,14 @@ export type TokenBridgeRelayer = { "name": "evmTransactionSize", "type": "u64" }, + { + "name": "mintAuthority", + "docs": [ + "The mint authority used by the Token Bridge. Used to check whether a transfer is native", + "or wrapped." + ], + "type": "pubkey" + }, { "name": "senderBump", "type": "u8" diff --git a/yarn.lock b/yarn.lock index 4897d526..96171cc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2104,6 +2104,7 @@ __metadata: resolution: "@xlabs-xyz/solana-arbitrary-token-transfers@workspace:sdk/solana" dependencies: "@coral-xyz/anchor": "npm:^0.30.1" + "@solana/spl-token": "npm:^0.4.9" "@solana/web3.js": "npm:^1.95.3" "@types/node": "npm:20.17.5" "@wormhole-foundation/sdk-base": "npm:^0.12.0" From 0e8acdbeec1ddc3912c37614118211b46e15a88b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix?= Date: Mon, 18 Nov 2024 14:34:02 +0100 Subject: [PATCH 2/8] Code simplification --- sdk/solana/tbrv3/token-bridge-relayer.ts | 155 ++++--- solana/tests/token-bridge-relayer-tests.ts | 36 +- solana/tests/utils/client-wrapper.ts | 454 -------------------- solana/tests/utils/tbr-wrapper.ts | 202 +++++++++ solana/tests/utils/token-bridge-wrapper.ts | 158 +++++++ solana/tests/utils/wormhole-core-wrapper.ts | 149 +++++++ solana/tests/utils/wormhole-pdas.ts | 49 --- 7 files changed, 634 insertions(+), 569 deletions(-) delete mode 100644 solana/tests/utils/client-wrapper.ts create mode 100644 solana/tests/utils/tbr-wrapper.ts create mode 100644 solana/tests/utils/token-bridge-wrapper.ts create mode 100644 solana/tests/utils/wormhole-core-wrapper.ts delete mode 100644 solana/tests/utils/wormhole-pdas.ts diff --git a/sdk/solana/tbrv3/token-bridge-relayer.ts b/sdk/solana/tbrv3/token-bridge-relayer.ts index e764688f..b21867c4 100644 --- a/sdk/solana/tbrv3/token-bridge-relayer.ts +++ b/sdk/solana/tbrv3/token-bridge-relayer.ts @@ -21,7 +21,7 @@ import { chainIdToChain, layout, } from '@wormhole-foundation/sdk-base'; -import { layoutItems, UniversalAddress } from '@wormhole-foundation/sdk-definitions'; +import { layoutItems, toNative, UniversalAddress } from '@wormhole-foundation/sdk-definitions'; import { SolanaPriceOracle, bigintToBn, bnToBigint } from '@xlabs-xyz/solana-price-oracle-sdk'; import { deserializeTbrV3Message, VaaMessage, throwError } from 'common-arbitrary-token-transfer'; import { BpfLoaderUpgradeableProgram } from './bpf-loader-upgradeable.js'; @@ -73,6 +73,7 @@ export type TbrNetwork = Exclude | 'Localnet'; * Transforms a `UniversalAddress` into an array of numbers `number[]`. */ export const uaToArray = (ua: UniversalAddress): number[] => Array.from(ua.toUint8Array()); +const uaToPubkey = (address: UniversalAddress) => toNative('Solana', address).unwrap(); export class SolanaTokenBridgeRelayer { public readonly program: anchor.Program; @@ -135,42 +136,52 @@ export class SolanaTokenBridgeRelayer { /** Raw Solana accounts. */ get account() { return { - config: () => this.accountInfo(this.program.account.tbrConfigState, [Buffer.from('config')]), + config: () => this.accountInfo(this.program.account.tbrConfigState, ['config']), chainConfig: (chain: Chain) => - this.accountInfo(this.program.account.chainConfigState, [ - Buffer.from('chainconfig'), - chainSeed(chain), - ]), + this.accountInfo(this.program.account.chainConfigState, ['chainconfig', chainSeed(chain)]), peer: (chain: Chain, peerAddress: UniversalAddress) => this.accountInfo(this.program.account.peerState, [ - Buffer.from('peer'), + 'peer', chainSeed(chain), peerAddress.toUint8Array(), ]), signerSequence: (signer: PublicKey) => - this.accountInfo(this.program.account.signerSequenceState, [ - Buffer.from('seq'), - signer.toBuffer(), - ]), + this.accountInfo(this.program.account.signerSequenceState, ['seq', signer.toBuffer()]), authBadge: (account: PublicKey) => - this.accountInfo(this.program.account.authBadgeState, [ - Buffer.from('authbadge'), - account.toBuffer(), - ]), + this.accountInfo(this.program.account.authBadgeState, ['authbadge', account.toBuffer()]), - temporary: (mint: PublicKey) => - findPda(this.program.programId, [Buffer.from('tmp'), mint.toBuffer()]), + temporary: (mint: PublicKey) => this.pda('tmp', mint.toBuffer()), //TODO read the VAA with `fetch` - vaa: (vaaHash: Uint8Array) => - findPda(this.wormholeProgramId, [Buffer.from('PostedVAA'), vaaHash]), - redeemer: () => findPda(this.program.programId, [Buffer.from('redeemer')]), + /** VAA address used to complete a transfer (inbound transfer). */ + vaa: (vaaHash: Uint8Array) => { + const { address, bump } = findPda(this.wormholeProgramId, ['PostedVAA', vaaHash]); + return { + address, + bump, + fetch: async (): Promise => { + const { data } = + (await this.connection.getAccountInfo(address)) ?? + throwError('No VAA with this hash'); + return data; + }, + }; + }, + redeemer: () => this.pda('redeemer'), + /** Message emitted during an outbound transfer. */ wormholeMessage: (payer: PublicKey, payerSequence: bigint) => { - const buf = Buffer.alloc(8); - buf.writeBigInt64BE(payerSequence); + const { address, bump } = this.pda( + 'bridged', + payer.toBuffer(), + layout.serializeLayout({ binary: 'uint', size: 8, endianness: 'big' }, payerSequence), + ); return { - ...findPda(this.program.programId, [Buffer.from('bridged'), payer.toBuffer(), buf]), - fetch: async () => { - return; + address, + bump, + fetch: async (): Promise => { + const { data } = + (await this.connection.getAccountInfo(address)) ?? + throwError('No message found at this address'); + return data; }, }; }, @@ -188,6 +199,17 @@ export class SolanaTokenBridgeRelayer { evmTransactionSize: bnToBigint(evmTransactionSize), ...rest, })), + /** Returns all Wormhole messages emitted by a user */ + allWormholeMessages: async (payer: PublicKey) => { + const maxSequence = await this.payerSequenceNumber(payer); + + return Promise.all( + range(0n, maxSequence).map((seq) => { + const vaa = this.account.wormholeMessage(payer, seq).fetch(); + return vaa; + }), + ); + }, allAdminAccounts: async () => { const [accounts, owner] = await Promise.all([ this.program.account.authBadgeState.all().then((state) => state.map((pa) => pa.account)), @@ -213,11 +235,12 @@ export class SolanaTokenBridgeRelayer { return new UniversalAddress(Uint8Array.from(canonicalPeer)); }, allPeers: async (chain?: Chain) => { - let filter: Buffer | undefined = undefined; - if (chain !== undefined) { - filter = Buffer.alloc(2); - filter.writeUInt16LE(chainToChainId(chain)); - } + const filter = + chain === undefined + ? undefined + : Buffer.from( + layout.serializeLayout({ ...layoutItems.chainItem(), endianness: 'big' }, chain), + ); const states = await this.program.account.peerState .all(filter) .then((state) => state.map((pa) => pa.account)); @@ -280,28 +303,26 @@ export class SolanaTokenBridgeRelayer { }; } + /** Returns a PDA belonging to this program. */ + pda(...seeds: Array) { + return findPda(this.program.programId, seeds); + } + /** Returns the end user's wallet and associated token account */ getRecipientAccountsFromVaa(vaa: VaaMessage): { wallet: PublicKey; associatedTokenAccount: PublicKey; } { - const native = vaa.payload.token.chain === 'Solana'; - let mint; - - if (native) { - mint = new PublicKey(vaa.payload.token.address.toUint8Array()); - } else { - [mint] = PublicKey.findProgramAddressSync( - [ - Buffer.from('wrapped'), - chainSeed(vaa.payload.token.chain), - vaa.payload.token.address.toUint8Array(), - ], - this.tokenBridgeProgramId, - ); - } - - const wallet = new PublicKey(deserializeTbrV3Message(vaa.payload.payload).recipient.address); + const mint = + vaa.payload.token.chain === 'Solana' + ? uaToPubkey(vaa.payload.token.address) + : findPda(this.tokenBridgeProgramId, [ + 'wrapped', + chainSeed(vaa.payload.token.chain), + vaa.payload.token.address.toUint8Array(), + ]).address; + + const wallet = uaToPubkey(deserializeTbrV3Message(vaa.payload.payload).recipient); const associatedTokenAccount = getAssociatedTokenAccount(wallet, mint); return { wallet, associatedTokenAccount }; @@ -703,14 +724,17 @@ export class SolanaTokenBridgeRelayer { /* HELPERS */ + /** Generates an object with a PDA address, bump and fetching function + * from an Anchor's `AccountClient`. + */ private accountInfo>( account: anchor.AccountClient, - seeds: Array, + seeds: Array, ) { - const { address, seed } = findPda(this.program.programId, seeds); + const { address, bump } = this.pda(...seeds); return { address, - seed, + bump, fetch: () => account.fetch(address), }; } @@ -732,10 +756,7 @@ export class SolanaTokenBridgeRelayer { /** Get the info about the foreign address from a Wormhole mint */ private async getWormholeAddressFromWrappedMint(mint: PublicKey): Promise { - const [metaAddress] = PublicKey.findProgramAddressSync( - [Buffer.from('meta'), mint.toBuffer()], - this.tokenBridgeProgramId, - ); + const metaAddress = findPda(this.tokenBridgeProgramId, ['meta', mint.toBuffer()]).address; const { data } = (await this.connection.getAccountInfo(metaAddress)) ?? throwError( @@ -755,7 +776,7 @@ export class SolanaTokenBridgeRelayer { } private get tokenBridgeMintAuthority(): PublicKey { - return findPda(this.tokenBridgeProgramId, [Buffer.from('mint_signer')]).address; + return findPda(this.tokenBridgeProgramId, ['mint_signer']).address; } private logDebug(message?: any, ...optionalParams: any[]) { @@ -771,11 +792,20 @@ function conditionalDebug(debug: boolean, message?: any, ...optionalParams: any[ const chainSeed = (chain: Chain) => encoding.bignum.toBytes(chainToChainId(chain), 2); -function findPda(programId: PublicKey, seeds: Array) { - const [address, seed] = PublicKey.findProgramAddressSync(seeds, programId); +function findPda(programId: PublicKey, seeds: Array) { + const [address, bump] = PublicKey.findProgramAddressSync( + seeds.map((seed) => { + if (typeof seed === 'string') { + return Buffer.from(seed); + } else { + return seed; + } + }), + programId, + ); return { address, - seed, + bump, }; } @@ -884,3 +914,12 @@ function patchAddress(idl: any, address?: PublicKey) { return idl; } + +function range(from: bigint, to: bigint): bigint[] { + function* generator(from: bigint, to: bigint) { + for (let i = from; from < to; i++) { + yield i; + } + } + return Array.from(generator(from, to)); +} diff --git a/solana/tests/token-bridge-relayer-tests.ts b/solana/tests/token-bridge-relayer-tests.ts index 45ec405f..80ed1029 100644 --- a/solana/tests/token-bridge-relayer-tests.ts +++ b/solana/tests/token-bridge-relayer-tests.ts @@ -1,14 +1,23 @@ import { chainToChainId } from '@wormhole-foundation/sdk-base'; -import { UniversalAddress } from '@wormhole-foundation/sdk-definitions'; +import { toNative, UniversalAddress } from '@wormhole-foundation/sdk-definitions'; import { Keypair, PublicKey, SendTransactionError, Transaction } from '@solana/web3.js'; import { NATIVE_MINT } from '@solana/spl-token'; -import { assert, TestMint, TestsHelper, tokenBridgeEmitter } from './utils/helpers.js'; -import { TbrWrapper, TokenBridgeWrapper, WormholeCoreWrapper } from './utils/client-wrapper.js'; +import { + assert, + TestMint, + TestsHelper, + tokenBridgeEmitter, + WormholeContracts, +} from './utils/helpers.js'; +import { TbrWrapper } from './utils/tbr-wrapper.js'; import { SolanaPriceOracle, uaToArray } from '@xlabs-xyz/solana-arbitrary-token-transfers'; import { expect } from 'chai'; +import testProgramKeypair from '../programs/token-bridge-relayer/test-program-keypair.json' with { type: 'json' }; import oracleKeypair from './oracle-program-keypair.json' with { type: 'json' }; import { toVaaWithTbrV3Message } from 'common-arbitrary-token-transfer'; +import { WormholeCoreWrapper } from './utils/wormhole-core-wrapper.js'; +import { TokenBridgeWrapper } from './utils/token-bridge-wrapper.js'; const DEBUG = false; @@ -21,6 +30,8 @@ const authorityKeypair = './target/deploy/token_bridge_relayer-keypair.json'; const $ = new TestsHelper(); +const uaToPubkey = (address: UniversalAddress) => toNative('Solana', address).unwrap(); + describe('Token Bridge Relayer Program', () => { const oracleClient = new SolanaPriceOracle($.connection, $.pubkey.from(oracleKeypair)); const clients = (['owner', 'owner', 'admin', 'admin', 'admin', 'regular'] as const).map( @@ -36,10 +47,20 @@ describe('Token Bridge Relayer Program', () => { ] = clients; const wormholeCoreOwner = $.provider.generate(); - const wormholeCoreClient = new WormholeCoreWrapper(wormholeCoreOwner); + const wormholeCoreClient = new WormholeCoreWrapper( + wormholeCoreOwner, + WormholeContracts.Network, + $.pubkey.from(testProgramKeypair), + WormholeContracts.addresses, + ); const tokenBridgeOwner = $.provider.generate(); - const tokenBridgeClient = new TokenBridgeWrapper(tokenBridgeOwner); + const tokenBridgeClient = new TokenBridgeWrapper( + tokenBridgeOwner, + WormholeContracts.Network, + wormholeCoreClient.guardians, + WormholeContracts.addresses, + ); const feeRecipient = PublicKey.unique(); const evmTransactionGas = 321_000n; @@ -445,7 +466,7 @@ describe('Token Bridge Relayer Program', () => { expect(vaa.sequence).equal(sequence); expect(vaa.emitterChain).equal('Solana'); - assert.key(new PublicKey(vaa.emitterAddress.address)).equal(tokenBridgeEmitter()); + assert.key(uaToPubkey(vaa.emitterAddress)).equal(tokenBridgeEmitter()); expect(vaa.payload.to).deep.equal({ address: canonicalEthereum, chain: ETHEREUM }); // Since the native mint has 9 decimals, the last digit is removed by the token bridge: @@ -486,7 +507,7 @@ describe('Token Bridge Relayer Program', () => { ); expect(vaa.sequence).equal(sequence); expect(vaa.emitterChain).equal('Solana'); - assert.key(new PublicKey(vaa.emitterAddress.address)).equal(tokenBridgeEmitter()); + assert.key(uaToPubkey(vaa.emitterAddress)).equal(tokenBridgeEmitter()); expect(vaa.payload.to).deep.equal({ address: canonicalEthereum, chain: ETHEREUM }); // Since the mint has 10 decimals, the last digit is removed by the token bridge: @@ -592,7 +613,6 @@ describe('Token Bridge Relayer Program', () => { expect(vaa.payload.to).deep.equal({ address: canonicalEthereum, chain: ETHEREUM }); - console.log('vaa amount:', vaa.payload.token.amount); expect(vaa.payload.token).deep.equal({ // It is a wrapped token, so it has the expected number of decimals. No shenanigans, then: amount: transferredAmount, diff --git a/solana/tests/utils/client-wrapper.ts b/solana/tests/utils/client-wrapper.ts deleted file mode 100644 index 9a7aec05..00000000 --- a/solana/tests/utils/client-wrapper.ts +++ /dev/null @@ -1,454 +0,0 @@ -import { AnchorProvider } from '@coral-xyz/anchor'; -import anchor from '@coral-xyz/anchor'; -import { - Keypair, - PublicKey, - SystemProgram, - SYSVAR_RENT_PUBKEY, - TransactionSignature, -} from '@solana/web3.js'; -import { Chain, chainToChainId, encoding, layout } from '@wormhole-foundation/sdk-base'; -import { - serializePayload, - serialize as serializeVaa, - deserialize as deserializeVaa, - UniversalAddress, - createVAA, -} from '@wormhole-foundation/sdk-definitions'; -import { - SolanaPriceOracle, - SolanaTokenBridgeRelayer, - TransferParameters, - VaaMessage, -} from '@xlabs-xyz/solana-arbitrary-token-transfers'; -import { - getBlockTime, - sendAndConfirmIxs, - TestProvider, - TestsHelper, - WormholeContracts, -} from './helpers.js'; -import { SolanaWormholeCore, utils as coreUtils } from '@wormhole-foundation/sdk-solana-core'; -import { SolanaTokenBridge } from '@wormhole-foundation/sdk-solana-tokenbridge'; -import { mocks } from '@wormhole-foundation/sdk-definitions/testing'; -import { SolanaSendSigner } from '@wormhole-foundation/sdk-solana'; -import { signAndSendWait } from '@wormhole-foundation/sdk-connect'; - -import testProgramKeypair from '../../programs/token-bridge-relayer/test-program-keypair.json' with { type: 'json' }; -import { serializeTbrV3Message } from 'common-arbitrary-token-transfer'; -import { accountDataLayout } from './layout.js'; -import { TokenBridgePdas, WormholeCorePdas } from './wormhole-pdas.js'; - -const $ = new TestsHelper(); - -export class TbrWrapper { - readonly client: SolanaTokenBridgeRelayer; - readonly provider: TestProvider; - readonly logs: { [key: string]: string[] }; - readonly logsSubscriptionId: number; - - constructor(provider: TestProvider, tbrClient: SolanaTokenBridgeRelayer) { - this.provider = provider; - this.client = tbrClient; - this.logs = {}; - - this.logsSubscriptionId = provider.connection.onLogs( - 'all', - (l) => (this.logs[l.signature] = l.logs), - ); - } - - static from( - provider: TestProvider, - accountType: 'owner' | 'admin' | 'regular', - oracleClient: SolanaPriceOracle, - debug: boolean, - ) { - const clientProvider = - accountType === 'regular' ? provider : { connection: provider.connection }; - - const client = new SolanaTokenBridgeRelayer( - clientProvider, - 'Localnet', - $.pubkey.from(testProgramKeypair), - oracleClient, - debug, - ); - - return new TbrWrapper(provider, client); - } - - static async create(provider: TestProvider, debug: boolean) { - const client = await SolanaTokenBridgeRelayer.create( - { connection: provider.connection }, - debug, - ); - - return new TbrWrapper(provider, client); - } - - get publicKey(): PublicKey { - return this.provider.publicKey; - } - - get account() { - return this.client.account; - } - - get read() { - return this.client.read; - } - - /** Unregister the logs event so that the test does not hang. */ - async close() { - await this.provider.connection.removeOnLogsListener(this.logsSubscriptionId); - } - - displayLogs(signature: string) { - let lines = this.logs[signature]; - if (lines === undefined) { - lines = ['']; - } - console.log(`Signature '${signature}':`); - for (const line of lines) { - console.log(` > ${line}`); - } - } - - async initialize(args: { - owner: PublicKey; - feeRecipient: PublicKey; - admins: PublicKey[]; - }): Promise { - return sendAndConfirmIxs(this.provider, await this.client.initialize(args)); - } - - async submitOwnerTransferRequest(newOwner: PublicKey): Promise { - return sendAndConfirmIxs(this.provider, await this.client.submitOwnerTransferRequest(newOwner)); - } - - async confirmOwnerTransferRequest(): Promise { - return await sendAndConfirmIxs(this.provider, await this.client.confirmOwnerTransferRequest()); - } - - async cancelOwnerTransferRequest(): Promise { - return sendAndConfirmIxs(this.provider, await this.client.cancelOwnerTransferRequest()); - } - - async addAdmin(newAdmin: PublicKey): Promise { - return sendAndConfirmIxs(this.provider, await this.client.addAdmin(newAdmin)); - } - - async removeAdmin(adminToRemove: PublicKey): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.removeAdmin(this.publicKey, adminToRemove), - ); - } - - async registerPeer(chain: Chain, peerAddress: UniversalAddress): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.registerPeer(this.publicKey, chain, peerAddress), - ); - } - - async updateCanonicalPeer( - chain: Chain, - peerAddress: UniversalAddress, - ): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.updateCanonicalPeer(chain, peerAddress), - ); - } - - async setPauseForOutboundTransfers(chain: Chain, paused: boolean): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.setPauseForOutboundTransfers(this.publicKey, chain, paused), - ); - } - - async updateMaxGasDropoff(chain: Chain, maxGasDropoff: number): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.updateMaxGasDropoff(this.publicKey, chain, maxGasDropoff), - ); - } - - async updateRelayerFee(chain: Chain, relayerFee: number): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.updateRelayerFee(this.publicKey, chain, relayerFee), - ); - } - - async updateFeeRecipient(newFeeRecipient: PublicKey): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.updateFeeRecipient(this.publicKey, newFeeRecipient), - ); - } - - async updateEvmTransactionConfig( - evmTransactionGas: bigint, - evmTransactionSize: bigint, - ): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.updateEvmTransactionConfig( - this.publicKey, - evmTransactionGas, - evmTransactionSize, - ), - ); - } - - async transferTokens( - params: TransferParameters, - signers?: Keypair[], - ): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.transferTokens(this.publicKey, params), - { signers }, - ); - } - - async completeTransfer(vaa: VaaMessage): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.completeTransfer(this.publicKey, vaa), - ); - } - - async relayingFee(chain: Chain, dropoffAmount: number): Promise { - return this.client.relayingFee(chain, dropoffAmount); - } -} - -const guardianKey = 'cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0'; -const guardians = new mocks.MockGuardians(0, [guardianKey]); - -export class WormholeCoreWrapper { - public readonly provider: AnchorProvider; - public readonly client: SolanaWormholeCore; - public readonly pda: WormholeCorePdas; - private sequence = 0n; - - constructor(provider: AnchorProvider) { - this.provider = provider; - this.client = new SolanaWormholeCore( - WormholeContracts.Network, - 'Solana', - provider.connection, - WormholeContracts.addresses, - ); - this.pda = new WormholeCorePdas(WormholeContracts.coreBridge); - } - - async initialize() { - const guardianSetExpirationTime = 1_000_000; - const fee = new anchor.BN(1_000_000); - const initialGuardians = [ - Array.from(encoding.hex.decode('beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe')), //address associated with the private key - ]; - - // https://github.com/wormhole-foundation/wormhole/blob/main/solana/bridge/program/src/api/initialize.rs - const ix = await this.client.coreBridge.methods - .initialize(guardianSetExpirationTime, fee, initialGuardians) - .accounts({ - bridge: this.pda.bridge(), - guardianSet: this.pda.guardianSet(), - feeCollector: this.pda.feeCollector(), - payer: this.provider.publicKey, - }) - .instruction(); - - return await sendAndConfirmIxs(this.provider, ix); - } - - /** Parse a VAA generated from the postVaa method, or from the Token Bridge during - * and outbound transfer - */ - async parseVaa(key: PublicKey): Promise { - const info = await this.provider.connection.getAccountInfo(key); - if (info === null) { - throw new Error(`No message account exists at that address: ${key.toString()}`); - } - - const message = layout.deserializeLayout(accountDataLayout, info.data); - - const vaa = createVAA('Uint8Array', { - guardianSet: 0, - timestamp: message.timestamp, - nonce: message.nonce, - emitterChain: message.emitterChain, - emitterAddress: message.emitterAddress, - sequence: message.sequence, - consistencyLevel: message.consistencyLevel, - signatures: [], - payload: message.payload, - }); - - return deserializeVaa('TokenBridge:TransferWithPayload', serializeVaa(vaa)); - } - - /** - * `source`: the peers (Token Bridge and TBR) emitting the transfer. - * `token`: the origin of the token (chain and mint). - */ - async postVaa( - payer: Keypair, - token: { amount: bigint; chain: Chain; address: UniversalAddress }, - source: { chain: Chain; tokenBridge: UniversalAddress; tbrPeer: UniversalAddress }, - message: { recipient: UniversalAddress; gasDropoff: number; unwrapIntent: boolean }, - ): Promise { - const seq = this.sequence++; - const timestamp = await getBlockTime(this.client.connection); - - const emittingPeer = new mocks.MockEmitter(source.tokenBridge, source.chain, seq); - - const payload = serializePayload('TokenBridge:TransferWithPayload', { - token, - to: { - address: new UniversalAddress($.pubkey.from(testProgramKeypair).toBuffer()), - chain: 'Solana', - }, - from: source.tbrPeer, - payload: serializeTbrV3Message(message), - }); - - const published = emittingPeer.publishMessage( - 0, // nonce, - payload, - 1, // consistencyLevel - timestamp, - ); - const vaa = guardians.addSignatures(published, [0]); - - const txs = this.client.postVaa(payer.publicKey, vaa); - const signer = new SolanaSendSigner(this.client.connection, 'Solana', payer, false, {}); - await signAndSendWait(txs, signer); - - return coreUtils.derivePostedVaaKey(WormholeContracts.coreBridge, Buffer.from(vaa.hash)); - } -} - -export class TokenBridgeWrapper { - static sequence = 100n; - public readonly provider: TestProvider; - public readonly client: SolanaTokenBridge; - public readonly pda: TokenBridgePdas; - - constructor(provider: TestProvider) { - this.provider = provider; - this.client = new SolanaTokenBridge( - WormholeContracts.Network, - 'Solana', - provider.connection, - WormholeContracts.addresses, - ); - this.pda = new TokenBridgePdas(WormholeContracts.tokenBridge); - } - - async initialize() { - const ix = await this.client.tokenBridge.methods - .initialize(WormholeContracts.coreBridge) - .accounts({ - payer: this.provider.publicKey, - config: this.pda.config(), - }) - .instruction(); - - return await sendAndConfirmIxs(this.provider, ix); - } - - async registerPeer(chain: Chain, address: UniversalAddress) { - const sequence = TokenBridgeWrapper.sequence++; - const timestamp = await getBlockTime(this.client.connection); - const emitterAddress = new UniversalAddress('00'.repeat(31) + '04'); - const rawVaa = createVAA('TokenBridge:RegisterChain', { - guardianSet: 0, - timestamp, - nonce: 0, - emitterChain: 'Solana', - emitterAddress, - sequence, - consistencyLevel: 1, - signatures: [], - payload: { chain: 'Solana', actionArgs: { foreignChain: chain, foreignAddress: address } }, - }); - const vaa = guardians.addSignatures(rawVaa, [0]); - const txs = this.client.coreBridge.postVaa(this.provider.publicKey, vaa); - const signer = new SolanaSendSigner( - this.client.connection, - 'Solana', - this.provider.keypair, - false, - {}, - ); - await signAndSendWait(txs, signer); - - const vaaAddress = coreUtils.derivePostedVaaKey( - WormholeContracts.coreBridge, - Buffer.from(vaa.hash), - ); - - const ix = await this.client.tokenBridge.methods - .registerChain() - .accounts({ - payer: this.provider.publicKey, - vaa: vaaAddress, - endpoint: this.pda.endpoint(chain, address), - config: this.pda.config(), - claim: this.pda.claim(emitterAddress, sequence), - wormholeProgram: WormholeContracts.coreBridge, - rent: SYSVAR_RENT_PUBKEY, - systemProgram: SystemProgram.programId, - }) - .instruction(); - await sendAndConfirmIxs(this.provider, ix); - } - - async attestToken( - emitter: UniversalAddress, - chain: Chain, - mint: UniversalAddress, - info: { decimals: number }, - ) { - const signer = new SolanaSendSigner( - this.client.connection, - 'Solana', - this.provider.keypair, - false, - {}, - ); - - const sequence = TokenBridgeWrapper.sequence++; - const timestamp = await getBlockTime(this.client.connection); - const rawVaa = createVAA('TokenBridge:AttestMeta', { - guardianSet: 0, - timestamp, - nonce: 0, - emitterChain: chain, - emitterAddress: emitter, - sequence, - consistencyLevel: 1, - signatures: [], - payload: { - decimals: info.decimals, - symbol: '12345678901234567890123456789012', - name: '12345678901234567890123456789012', - token: { chain, address: mint }, - }, - }); - const vaa = guardians.addSignatures(rawVaa, [0]); - const txsPostVaa = this.client.coreBridge.postVaa(this.provider.publicKey, vaa); - await signAndSendWait(txsPostVaa, signer); - - const txsAttest = this.client.submitAttestation(rawVaa, this.provider.publicKey); - await signAndSendWait(txsAttest, signer); - } -} diff --git a/solana/tests/utils/tbr-wrapper.ts b/solana/tests/utils/tbr-wrapper.ts new file mode 100644 index 00000000..368af711 --- /dev/null +++ b/solana/tests/utils/tbr-wrapper.ts @@ -0,0 +1,202 @@ +import anchor from '@coral-xyz/anchor'; +import { Keypair, PublicKey, TransactionSignature } from '@solana/web3.js'; +import { Chain } from '@wormhole-foundation/sdk-base'; +import { UniversalAddress } from '@wormhole-foundation/sdk-definitions'; +import { + SolanaPriceOracle, + SolanaTokenBridgeRelayer, + TransferParameters, + VaaMessage, +} from '@xlabs-xyz/solana-arbitrary-token-transfers'; +import { sendAndConfirmIxs, TestProvider, TestsHelper } from './helpers.js'; + +import testProgramKeypair from '../../programs/token-bridge-relayer/test-program-keypair.json' with { type: 'json' }; + +const $ = new TestsHelper(); + +export class TbrWrapper { + readonly client: SolanaTokenBridgeRelayer; + readonly provider: TestProvider; + readonly logs: { [key: string]: string[] }; + readonly logsSubscriptionId: number; + + constructor(provider: TestProvider, tbrClient: SolanaTokenBridgeRelayer) { + this.provider = provider; + this.client = tbrClient; + this.logs = {}; + + this.logsSubscriptionId = provider.connection.onLogs( + 'all', + (l) => (this.logs[l.signature] = l.logs), + ); + } + + static from( + provider: TestProvider, + accountType: 'owner' | 'admin' | 'regular', + oracleClient: SolanaPriceOracle, + debug: boolean, + ) { + const clientProvider = + accountType === 'regular' ? provider : { connection: provider.connection }; + + const client = new SolanaTokenBridgeRelayer( + clientProvider, + 'Localnet', + $.pubkey.from(testProgramKeypair), + oracleClient, + debug, + ); + + return new TbrWrapper(provider, client); + } + + static async create(provider: TestProvider, debug: boolean) { + const client = await SolanaTokenBridgeRelayer.create( + { connection: provider.connection }, + debug, + ); + + return new TbrWrapper(provider, client); + } + + get publicKey(): PublicKey { + return this.provider.publicKey; + } + + get account() { + return this.client.account; + } + + get read() { + return this.client.read; + } + + /** Unregister the logs event so that the test does not hang. */ + async close() { + await this.provider.connection.removeOnLogsListener(this.logsSubscriptionId); + } + + displayLogs(signature: string) { + let lines = this.logs[signature]; + if (lines === undefined) { + lines = ['']; + } + console.log(`Signature '${signature}':`); + for (const line of lines) { + console.log(` > ${line}`); + } + } + + async initialize(args: { + owner: PublicKey; + feeRecipient: PublicKey; + admins: PublicKey[]; + }): Promise { + return sendAndConfirmIxs(this.provider, await this.client.initialize(args)); + } + + async submitOwnerTransferRequest(newOwner: PublicKey): Promise { + return sendAndConfirmIxs(this.provider, await this.client.submitOwnerTransferRequest(newOwner)); + } + + async confirmOwnerTransferRequest(): Promise { + return await sendAndConfirmIxs(this.provider, await this.client.confirmOwnerTransferRequest()); + } + + async cancelOwnerTransferRequest(): Promise { + return sendAndConfirmIxs(this.provider, await this.client.cancelOwnerTransferRequest()); + } + + async addAdmin(newAdmin: PublicKey): Promise { + return sendAndConfirmIxs(this.provider, await this.client.addAdmin(newAdmin)); + } + + async removeAdmin(adminToRemove: PublicKey): Promise { + return sendAndConfirmIxs( + this.provider, + await this.client.removeAdmin(this.publicKey, adminToRemove), + ); + } + + async registerPeer(chain: Chain, peerAddress: UniversalAddress): Promise { + return sendAndConfirmIxs( + this.provider, + await this.client.registerPeer(this.publicKey, chain, peerAddress), + ); + } + + async updateCanonicalPeer( + chain: Chain, + peerAddress: UniversalAddress, + ): Promise { + return sendAndConfirmIxs( + this.provider, + await this.client.updateCanonicalPeer(chain, peerAddress), + ); + } + + async setPauseForOutboundTransfers(chain: Chain, paused: boolean): Promise { + return sendAndConfirmIxs( + this.provider, + await this.client.setPauseForOutboundTransfers(this.publicKey, chain, paused), + ); + } + + async updateMaxGasDropoff(chain: Chain, maxGasDropoff: number): Promise { + return sendAndConfirmIxs( + this.provider, + await this.client.updateMaxGasDropoff(this.publicKey, chain, maxGasDropoff), + ); + } + + async updateRelayerFee(chain: Chain, relayerFee: number): Promise { + return sendAndConfirmIxs( + this.provider, + await this.client.updateRelayerFee(this.publicKey, chain, relayerFee), + ); + } + + async updateFeeRecipient(newFeeRecipient: PublicKey): Promise { + return sendAndConfirmIxs( + this.provider, + await this.client.updateFeeRecipient(this.publicKey, newFeeRecipient), + ); + } + + async updateEvmTransactionConfig( + evmTransactionGas: bigint, + evmTransactionSize: bigint, + ): Promise { + return sendAndConfirmIxs( + this.provider, + await this.client.updateEvmTransactionConfig( + this.publicKey, + evmTransactionGas, + evmTransactionSize, + ), + ); + } + + async transferTokens( + params: TransferParameters, + signers?: Keypair[], + ): Promise { + return sendAndConfirmIxs( + this.provider, + await this.client.transferTokens(this.publicKey, params), + { signers }, + ); + } + + async completeTransfer(vaa: VaaMessage): Promise { + return sendAndConfirmIxs( + this.provider, + await this.client.completeTransfer(this.publicKey, vaa), + ); + } + + async relayingFee(chain: Chain, dropoffAmount: number): Promise { + return this.client.relayingFee(chain, dropoffAmount); + } +} diff --git a/solana/tests/utils/token-bridge-wrapper.ts b/solana/tests/utils/token-bridge-wrapper.ts new file mode 100644 index 00000000..6aac422c --- /dev/null +++ b/solana/tests/utils/token-bridge-wrapper.ts @@ -0,0 +1,158 @@ +import { SolanaTokenBridge } from '@wormhole-foundation/sdk-solana-tokenbridge'; +import { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from '@solana/web3.js'; +import { Chain, layout, Network } from '@wormhole-foundation/sdk-base'; +import { Contracts, UniversalAddress, createVAA } from '@wormhole-foundation/sdk-definitions'; +import { mocks } from '@wormhole-foundation/sdk-definitions/testing'; +import { utils as coreUtils } from '@wormhole-foundation/sdk-solana-core'; + +import { getBlockTime, sendAndConfirmIxs, TestProvider } from './helpers.js'; +import { SolanaSendSigner } from '@wormhole-foundation/sdk-solana'; +import { signAndSendWait } from '@wormhole-foundation/sdk-connect'; +import { layoutItems } from '@wormhole-foundation/sdk-definitions'; + +export class TokenBridgeWrapper { + static sequence = 100n; + public readonly provider: TestProvider; + public readonly client: SolanaTokenBridge; + public readonly guardians: mocks.MockGuardians; + + /** + * + * @param solanaProgram The Solana Program used as a destination for the VAAs, _i.e._ the program being tested. + * @param contracts At least the addresses `coreBridge` and `tokenBridge` must be provided. + */ + constructor( + provider: TestProvider, + network: N, + guardians: mocks.MockGuardians, + contracts: Contracts, + ) { + this.provider = provider; + this.guardians = guardians; + this.client = new SolanaTokenBridge(network, 'Solana', provider.connection, contracts); + } + + get coreBridgeId() { + return this.client.coreBridge.coreBridge.programId; + } + + get pda() { + return { + config: () => this.findPda(Buffer.from('config')), + + endpoint: (chain: Chain, address: UniversalAddress) => { + return this.findPda( + layout.serializeLayout({ ...layoutItems.chainItem(), endianness: 'big' }, chain), + address.toUint8Array(), + ); + }, + + claim: (emitterAddress: UniversalAddress, sequence: bigint) => { + const sequenceBytes = Buffer.alloc(8); + sequenceBytes.writeBigUInt64BE(sequence); + + return this.findPda(emitterAddress.toUint8Array(), Buffer.from([0, 1]), sequenceBytes); + }, + }; + } + + async initialize() { + const ix = await this.client.tokenBridge.methods + .initialize(this.coreBridgeId) + .accounts({ + payer: this.provider.publicKey, + config: this.pda.config(), + }) + .instruction(); + + return await sendAndConfirmIxs(this.provider, ix); + } + + async registerPeer(chain: Chain, address: UniversalAddress) { + const sequence = TokenBridgeWrapper.sequence++; + const timestamp = await getBlockTime(this.client.connection); + const emitterAddress = new UniversalAddress('00'.repeat(31) + '04'); + const rawVaa = createVAA('TokenBridge:RegisterChain', { + guardianSet: 0, + timestamp, + nonce: 0, + emitterChain: 'Solana', + emitterAddress, + sequence, + consistencyLevel: 1, + signatures: [], + payload: { chain: 'Solana', actionArgs: { foreignChain: chain, foreignAddress: address } }, + }); + const vaa = this.guardians.addSignatures(rawVaa, [0]); + const txs = this.client.coreBridge.postVaa(this.provider.publicKey, vaa); + const signer = new SolanaSendSigner( + this.client.connection, + 'Solana', + this.provider.keypair, + false, + {}, + ); + await signAndSendWait(txs, signer); + + const vaaAddress = coreUtils.derivePostedVaaKey(this.coreBridgeId, Buffer.from(vaa.hash)); + + const ix = await this.client.tokenBridge.methods + .registerChain() + .accounts({ + payer: this.provider.publicKey, + vaa: vaaAddress, + endpoint: this.pda.endpoint(chain, address), + config: this.pda.config(), + claim: this.pda.claim(emitterAddress, sequence), + wormholeProgram: this.coreBridgeId, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + }) + .instruction(); + await sendAndConfirmIxs(this.provider, ix); + } + + async attestToken( + emitter: UniversalAddress, + chain: Chain, + mint: UniversalAddress, + info: { decimals: number }, + ) { + const signer = new SolanaSendSigner( + this.client.connection, + 'Solana', + this.provider.keypair, + false, + {}, + ); + + const sequence = TokenBridgeWrapper.sequence++; + const timestamp = await getBlockTime(this.client.connection); + const rawVaa = createVAA('TokenBridge:AttestMeta', { + guardianSet: 0, + timestamp, + nonce: 0, + emitterChain: chain, + emitterAddress: emitter, + sequence, + consistencyLevel: 1, + signatures: [], + payload: { + decimals: info.decimals, + symbol: '12345678901234567890123456789012', + name: '12345678901234567890123456789012', + token: { chain, address: mint }, + }, + }); + const vaa = this.guardians.addSignatures(rawVaa, [0]); + const txsPostVaa = this.client.coreBridge.postVaa(this.provider.publicKey, vaa); + await signAndSendWait(txsPostVaa, signer); + + const txsAttest = this.client.submitAttestation(rawVaa, this.provider.publicKey); + await signAndSendWait(txsAttest, signer); + } + + private findPda(...seeds: Array) { + return PublicKey.findProgramAddressSync(seeds, this.client.tokenBridge.programId)[0]; + } +} diff --git a/solana/tests/utils/wormhole-core-wrapper.ts b/solana/tests/utils/wormhole-core-wrapper.ts new file mode 100644 index 00000000..093e2aa5 --- /dev/null +++ b/solana/tests/utils/wormhole-core-wrapper.ts @@ -0,0 +1,149 @@ +import anchor from '@coral-xyz/anchor'; +import { Keypair, PublicKey } from '@solana/web3.js'; +import { + Chain, + encoding, + layout, + Network, + signAndSendWait, +} from '@wormhole-foundation/sdk-connect'; +import { SolanaSendSigner } from '@wormhole-foundation/sdk-solana'; +import { SolanaWormholeCore, utils as coreUtils } from '@wormhole-foundation/sdk-solana-core'; +import { + serializePayload, + serialize as serializeVaa, + deserialize as deserializeVaa, + UniversalAddress, + createVAA, + Contracts, +} from '@wormhole-foundation/sdk-definitions'; +import { mocks } from '@wormhole-foundation/sdk-definitions/testing'; +import { VaaMessage } from '@xlabs-xyz/solana-arbitrary-token-transfers'; + +import { getBlockTime, sendAndConfirmIxs, TestProvider } from './helpers.js'; +import { accountDataLayout } from './layout.js'; +import { serializeTbrV3Message } from 'common-arbitrary-token-transfer'; + +const guardianKey = 'cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0'; +const guardianAddress = 'beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe'; +const guardians = new mocks.MockGuardians(0, [guardianKey]); + +export class WormholeCoreWrapper { + public readonly provider: TestProvider; + public readonly client: SolanaWormholeCore; + private sequence = 0n; + private readonly toProgram: PublicKey; + + /** + * + * @param solanaProgram The Solana Program used as a destination for the VAAs, _i.e._ the program being tested. + * @param contracts At least the core program address `coreBridge` must be provided. + */ + constructor(provider: TestProvider, network: N, testedProgram: PublicKey, contracts: Contracts) { + this.provider = provider; + this.toProgram = testedProgram; + this.client = new SolanaWormholeCore(network, 'Solana', provider.connection, contracts); + } + + get guardians(): mocks.MockGuardians { + return guardians; + } + + get pda() { + return { + guardianSet: (): PublicKey => + this.findPda(Buffer.from('GuardianSet'), Buffer.from([0, 0, 0, 0])), + bridge: (): PublicKey => this.findPda(Buffer.from('Bridge')), + feeCollector: (): PublicKey => this.findPda(Buffer.from('fee_collector')), + }; + } + + async initialize() { + const guardianSetExpirationTime = 1_000_000; + const fee = new anchor.BN(1_000_000); + const initialGuardians = [Array.from(encoding.hex.decode(guardianAddress))]; + + // https://github.com/wormhole-foundation/wormhole/blob/main/solana/bridge/program/src/api/initialize.rs + const ix = await this.client.coreBridge.methods + .initialize(guardianSetExpirationTime, fee, initialGuardians) + .accounts({ + bridge: this.pda.bridge(), + guardianSet: this.pda.guardianSet(), + feeCollector: this.pda.feeCollector(), + payer: this.provider.publicKey, + }) + .instruction(); + + return await sendAndConfirmIxs(this.provider, ix); + } + + /** Parse a VAA generated from the postVaa method, or from the Token Bridge during + * and outbound transfer + */ + async parseVaa(key: PublicKey): Promise { + const info = await this.provider.connection.getAccountInfo(key); + if (info === null) { + throw new Error(`No message account exists at that address: ${key.toString()}`); + } + + const message = layout.deserializeLayout(accountDataLayout, info.data); + + const vaa = createVAA('Uint8Array', { + guardianSet: 0, + timestamp: message.timestamp, + nonce: message.nonce, + emitterChain: message.emitterChain, + emitterAddress: message.emitterAddress, + sequence: message.sequence, + consistencyLevel: message.consistencyLevel, + signatures: [], + payload: message.payload, + }); + + return deserializeVaa('TokenBridge:TransferWithPayload', serializeVaa(vaa)); + } + + /** + * `source`: the peers (Token Bridge and TBR) emitting the transfer. + * `token`: the origin of the token (chain and mint). + */ + async postVaa( + payer: Keypair, + token: { amount: bigint; chain: Chain; address: UniversalAddress }, + source: { chain: Chain; tokenBridge: UniversalAddress; tbrPeer: UniversalAddress }, + message: { recipient: UniversalAddress; gasDropoff: number; unwrapIntent: boolean }, + ): Promise { + const seq = this.sequence++; + const timestamp = await getBlockTime(this.client.connection); + + const emittingPeer = new mocks.MockEmitter(source.tokenBridge, source.chain, seq); + + const payload = serializePayload('TokenBridge:TransferWithPayload', { + token, + to: { + address: new UniversalAddress(this.toProgram.toBuffer()), + chain: 'Solana', + }, + from: source.tbrPeer, + payload: serializeTbrV3Message(message), + }); + + const published = emittingPeer.publishMessage( + 0, // nonce, + payload, + 1, // consistencyLevel + timestamp, + ); + const vaa = guardians.addSignatures(published, [0]); + + const txs = this.client.postVaa(payer.publicKey, vaa); + const signer = new SolanaSendSigner(this.client.connection, 'Solana', payer, false, {}); + await signAndSendWait(txs, signer); + + return coreUtils.derivePostedVaaKey(this.client.coreBridge.programId, Buffer.from(vaa.hash)); + } + + private findPda(...seeds: Array) { + return PublicKey.findProgramAddressSync(seeds, this.client.coreBridge.programId)[0]; + } +} diff --git a/solana/tests/utils/wormhole-pdas.ts b/solana/tests/utils/wormhole-pdas.ts deleted file mode 100644 index 10fab331..00000000 --- a/solana/tests/utils/wormhole-pdas.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { PublicKey } from '@solana/web3.js'; -import { Chain, chainToChainId } from '@wormhole-foundation/sdk-base'; -import { UniversalAddress } from '@wormhole-foundation/sdk-definitions'; - -export class WormholeCorePdas { - constructor(public programId: PublicKey) {} - - private pda(...seeds: Array) { - return PublicKey.findProgramAddressSync(seeds, this.programId)[0]; - } - - guardianSet() { - return this.pda(Buffer.from('GuardianSet'), Buffer.from([0, 0, 0, 0])); - } - - bridge() { - return this.pda(Buffer.from('Bridge')); - } - - feeCollector() { - return this.pda(Buffer.from('fee_collector')); - } -} - -export class TokenBridgePdas { - constructor(public programId: PublicKey) {} - - private pda(...seeds: Array) { - return PublicKey.findProgramAddressSync(seeds, this.programId)[0]; - } - - config() { - return this.pda(Buffer.from('config')); - } - - endpoint(chain: Chain, address: UniversalAddress) { - const chainBytes = Buffer.alloc(2); - chainBytes.writeUInt16BE(chainToChainId(chain)); - - return this.pda(chainBytes, address.toUint8Array()); - } - - claim(emitterAddress: UniversalAddress, sequence: bigint) { - const sequenceBytes = Buffer.alloc(8); - sequenceBytes.writeBigUInt64BE(sequence); - - return this.pda(emitterAddress.toUint8Array(), Buffer.from([0, 1]), sequenceBytes); - } -} From b195b0f191eb3f98aa42ceb6a413da5c72e94b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix?= Date: Wed, 20 Nov 2024 17:26:52 +0100 Subject: [PATCH 3/8] Cleanup + relayingFee is OK --- .pnp.cjs | 8 - .../borsh-npm-2.0.0-69f1235e8b-59a96dd9c7.zip | Bin 86169 -> 0 bytes sdk/solana/package.json | 3 +- sdk/solana/tbrv3/token-bridge-relayer.ts | 152 +++++++++++---- .../token-bridge-relayer/src/utils.rs | 2 +- solana/tests/token-bridge-relayer-tests.ts | 109 ++++++----- solana/tests/utils/helpers.ts | 174 +++++++++--------- solana/tests/utils/tbr-wrapper.ts | 102 +++++----- ...dge-wrapper.ts => testing-token-bridge.ts} | 63 +++---- ...re-wrapper.ts => testing-wormhole-core.ts} | 27 ++- yarn.lock | 8 - 11 files changed, 345 insertions(+), 303 deletions(-) delete mode 100644 .yarn/cache/borsh-npm-2.0.0-69f1235e8b-59a96dd9c7.zip rename solana/tests/utils/{token-bridge-wrapper.ts => testing-token-bridge.ts} (76%) rename solana/tests/utils/{wormhole-core-wrapper.ts => testing-wormhole-core.ts} (86%) diff --git a/.pnp.cjs b/.pnp.cjs index 7a022d03..c2a50808 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -2691,7 +2691,6 @@ const RAW_RUNTIME_STATE = ["@wormhole-foundation/sdk-solana", "npm:0.12.0"],\ ["@wormhole-foundation/sdk-solana-tokenbridge", "npm:0.12.0"],\ ["@xlabs-xyz/solana-price-oracle-sdk", "npm:0.0.16"],\ - ["borsh", "npm:2.0.0"],\ ["tsup", "virtual:9bef669af1c3b4b384f01597b4618f767933970b69e6fd62d81f9bbc7b8c4900225bffb44e48b8c05eea593d7778ed11aa109309394c7caa9adf8cbab67e8377#npm:8.3.5"],\ ["tsx", "npm:4.19.2"],\ ["typescript", "patch:typescript@npm%3A5.6.3#optional!builtin::version=5.6.3&hash=8c6c40"]\ @@ -3047,13 +3046,6 @@ const RAW_RUNTIME_STATE = ["text-encoding-utf-8", "npm:1.0.2"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:2.0.0", {\ - "packageLocation": "./.yarn/cache/borsh-npm-2.0.0-69f1235e8b-59a96dd9c7.zip/node_modules/borsh/",\ - "packageDependencies": [\ - ["borsh", "npm:2.0.0"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["brace-expansion", [\ diff --git a/.yarn/cache/borsh-npm-2.0.0-69f1235e8b-59a96dd9c7.zip b/.yarn/cache/borsh-npm-2.0.0-69f1235e8b-59a96dd9c7.zip deleted file mode 100644 index bc9aa0ced3e8a131dd93e71869c6c9faf61a134e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86169 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 { - const network = await networkFromConnection(provider.connection); + const network = await networkFromConnection(connection); const programId = programIdFromNetwork(network); - const priceOracle = await SolanaPriceOracle.create(provider.connection); + const priceOracle = await SolanaPriceOracle.create(connection); conditionalDebug(debug, 'Detected environment', { network, relayerProgramId: programId.toString(), oracleProgramId: priceOracle.program.programId.toString(), }); - return new SolanaTokenBridgeRelayer(provider, network, programId, priceOracle, debug); + return new SolanaTokenBridgeRelayer(connection, network, programId, priceOracle, debug); } get connection(): Connection { return this.program.provider.connection; } + get programId(): PublicKey { + return this.program.programId; + } + /** Raw Solana accounts. */ get account() { return { @@ -151,7 +160,6 @@ export class SolanaTokenBridgeRelayer { this.accountInfo(this.program.account.authBadgeState, ['authbadge', account.toBuffer()]), temporary: (mint: PublicKey) => this.pda('tmp', mint.toBuffer()), - //TODO read the VAA with `fetch` /** VAA address used to complete a transfer (inbound transfer). */ vaa: (vaaHash: Uint8Array) => { const { address, bump } = findPda(this.wormholeProgramId, ['PostedVAA', vaaHash]); @@ -172,7 +180,7 @@ export class SolanaTokenBridgeRelayer { const { address, bump } = this.pda( 'bridged', payer.toBuffer(), - layout.serializeLayout({ binary: 'uint', size: 8, endianness: 'big' }, payerSequence), + serializeLayout({ binary: 'uint', size: 8, endianness: 'big' }, payerSequence), ); return { address, @@ -239,7 +247,7 @@ export class SolanaTokenBridgeRelayer { chain === undefined ? undefined : Buffer.from( - layout.serializeLayout({ ...layoutItems.chainItem(), endianness: 'big' }, chain), + serializeLayout({ ...layoutItems.chainItem(), endianness: 'big' }, chain), ); const states = await this.program.account.peerState .all(filter) @@ -305,7 +313,7 @@ export class SolanaTokenBridgeRelayer { /** Returns a PDA belonging to this program. */ pda(...seeds: Array) { - return findPda(this.program.programId, seeds); + return findPda(this.programId, seeds); } /** Returns the end user's wallet and associated token account */ @@ -348,7 +356,7 @@ export class SolanaTokenBridgeRelayer { isWritable: true, })); - const program = new BpfLoaderUpgradeableProgram(this.program.programId, this.connection); + const program = new BpfLoaderUpgradeableProgram(this.programId, this.connection); const deployer = (await program.getdata()).upgradeAuthority ?? throwError('The program must be upgradeable'); @@ -696,15 +704,46 @@ export class SolanaTokenBridgeRelayer { /* Queries */ /** - * * @param chain The target chain where the token will be sent to. - * @param dropoffAmount The amount to send to the target chain. + * @param dropoffAmount The amount to send to the target chain, in µUSD. * @returns The fee to pay for the transfer in SOL. */ async relayingFee(chain: Chain, dropoffAmount: number): Promise { - providerAssert(this.program.provider); + const [tbrConfig, chainConfig, evmPrices, oracleConfig] = await Promise.all([ + this.read.config(), + this.account.chainConfig(chain).fetch(), + this.priceOracleClient.read.evmPrices(chain), + this.priceOracleClient.read.config(), + ]); + + const MWEI_PER_MICRO_ETH = 1_000_000n; + const MWEI_PER_ETH = 1_000_000_000_000n; - const tx = await this.program.methods + const totalFeesMwei = + tbrConfig.evmTransactionGas * BigInt(evmPrices.gasPrice) + + tbrConfig.evmTransactionSize * BigInt(evmPrices.pricePerByte) + + BigInt(dropoffAmount) * MWEI_PER_MICRO_ETH; + const totalFeesMicroUsd = + (totalFeesMwei * evmPrices.gasTokenPrice) / MWEI_PER_ETH + + BigInt(chainConfig.relayerFeeMicroUsd); + + return Number(totalFeesMicroUsd) / Number(oracleConfig.solPrice); + } + + /** + * This function simulates a program call to get the exact relaying fee. + * + * @param payer Any account with gas. No signature is required, no fee is taken. + * @param chain The target chain where the token will be sent to. + * @param dropoffAmount The amount to send to the target chain, in µUSD. + * @returns The fee to pay for the transfer in SOL. + */ + async relayingFeeSimulated( + payer: PublicKey, + chain: Chain, + dropoffAmount: number, + ): Promise { + const ix = await this.program.methods .relayingFee(dropoffAmount) .accountsStrict({ tbrConfig: this.account.config().address, @@ -712,12 +751,13 @@ export class SolanaTokenBridgeRelayer { oracleConfig: this.priceOracleClient.account.config().address, oracleEvmPrices: this.priceOracleClient.account.evmPrices(chain).address, }) - .rpc({ commitment: 'confirmed' }); - const txResponse = await this.connection.getTransaction(tx, { - commitment: 'confirmed', - maxSupportedTransactionVersion: undefined, - }); - const result = returnedDataFromTransaction('u64', txResponse); + .instruction(); + const txResponse = await simulateTransaction(this.connection, payer, [ix]); + + const result = returnedDataFromTransaction( + { binary: 'uint', size: 8, endianness: 'little' }, + txResponse, + ); return Number(result) / LAMPORTS_PER_SOL; } @@ -763,7 +803,7 @@ export class SolanaTokenBridgeRelayer { 'Cannot find the meta info\nThe mint authority indicates that the token is a Wormhole one, but no meta information is associated with it.', ); - const { chain, address } = layout.deserializeLayout( + const { chain, address } = deserializeLayout( [ { name: 'chain', ...layoutItems.chainItem(), endianness: 'little' }, { name: 'address', ...layoutItems.universalAddressItem }, @@ -811,7 +851,7 @@ function findPda(programId: PublicKey, seeds: Array) { function getAssociatedTokenAccount(wallet: PublicKey, mint: PublicKey): PublicKey { return PublicKey.findProgramAddressSync( - [wallet.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], + [wallet.toBuffer(), spl.TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], spl.ASSOCIATED_TOKEN_PROGRAM_ID, )[0]; } @@ -838,25 +878,29 @@ async function createAssociatedTokenAccountIdempotent({ return createAtaIdempotentIx; } -function providerAssert(provider: anchor.Provider) { - if (provider.sendAndConfirm === undefined) { - throw new Error('The client must be created with a full provider to use this method'); - } -} - -export function returnedDataFromTransaction( - schema: borsh.Schema, - confirmedTransaction: VersionedTransactionResponse | null, -) { +export function returnedDataFromTransaction( + typeLayout: L, + confirmedTransaction: + | VersionedTransactionResponse + | TransactionResponse + | SimulatedTransactionResponse, +): LayoutToType { const prefix = 'Program return: '; - const log = confirmedTransaction?.meta?.logMessages?.find((log) => log.startsWith(prefix)); - if (log === undefined) { - throw new Error('Internal error: The transaction did not return any value'); + const logs = + 'meta' in confirmedTransaction + ? confirmedTransaction.meta?.logMessages + : confirmedTransaction.logs; + if (logs == null) { + throw new Error('Internal error: No logs in this transaction'); } + const log = + logs.find((log) => log.startsWith(prefix)) ?? + throwError('No returned value specified in these logs'); + // The line looks like 'Program return: ': const [, data] = log.slice(prefix.length).split(' ', 2); - return borsh.deserialize(schema, Buffer.from(data, 'base64')) as T; + return deserializeLayout(typeLayout, Buffer.from(data, 'base64'), { consumeAll: true }); } /** @@ -923,3 +967,31 @@ function range(from: bigint, to: bigint): bigint[] { } return Array.from(generator(from, to)); } + +async function simulateTransaction( + connection: Connection, + payer: PublicKey, + instructions: TransactionInstruction[], +): Promise { + const { + value: { blockhash }, + } = await connection.getLatestBlockhashAndContext(); + const txMessage = new TransactionMessage({ + payerKey: payer, + recentBlockhash: blockhash, + instructions, + }).compileToV0Message(); + + const { value: response } = await connection.simulateTransaction( + new VersionedTransaction(txMessage), + { + sigVerify: false, + }, + ); + + if (response.err !== null) { + throw new Error('Transaction simulation failed', { cause: response.err }); + } + + return response; +} diff --git a/solana/programs/token-bridge-relayer/src/utils.rs b/solana/programs/token-bridge-relayer/src/utils.rs index 4e6d8073..e0b95236 100644 --- a/solana/programs/token-bridge-relayer/src/utils.rs +++ b/solana/programs/token-bridge-relayer/src/utils.rs @@ -85,7 +85,7 @@ fn check_prices_are_set(evm_prices: &EvmPricesState) -> Result<()> { TokenBridgeRelayerError::EvmChainPriceNotSet ); - // We don't need to check the SOL price, because it will generate a division by 0 + // We don't need to check the SOL price, because it will cause a division by 0 Ok(()) } diff --git a/solana/tests/token-bridge-relayer-tests.ts b/solana/tests/token-bridge-relayer-tests.ts index 80ed1029..ac4a21c1 100644 --- a/solana/tests/token-bridge-relayer-tests.ts +++ b/solana/tests/token-bridge-relayer-tests.ts @@ -1,7 +1,7 @@ import { chainToChainId } from '@wormhole-foundation/sdk-base'; import { toNative, UniversalAddress } from '@wormhole-foundation/sdk-definitions'; import { Keypair, PublicKey, SendTransactionError, Transaction } from '@solana/web3.js'; -import { NATIVE_MINT } from '@solana/spl-token'; +import * as spl from '@solana/spl-token'; import { assert, TestMint, @@ -16,8 +16,8 @@ import { expect } from 'chai'; import testProgramKeypair from '../programs/token-bridge-relayer/test-program-keypair.json' with { type: 'json' }; import oracleKeypair from './oracle-program-keypair.json' with { type: 'json' }; import { toVaaWithTbrV3Message } from 'common-arbitrary-token-transfer'; -import { WormholeCoreWrapper } from './utils/wormhole-core-wrapper.js'; -import { TokenBridgeWrapper } from './utils/token-bridge-wrapper.js'; +import { TestingWormholeCore } from './utils/testing-wormhole-core.js'; +import { TestingTokenBridge } from './utils/testing-token-bridge.js'; const DEBUG = false; @@ -34,8 +34,8 @@ const uaToPubkey = (address: UniversalAddress) => toNative('Solana', address).un describe('Token Bridge Relayer Program', () => { const oracleClient = new SolanaPriceOracle($.connection, $.pubkey.from(oracleKeypair)); - const clients = (['owner', 'owner', 'admin', 'admin', 'admin', 'regular'] as const).map( - (typeAccount) => TbrWrapper.from($.provider.generate(), typeAccount, oracleClient, DEBUG), + const clients = Array.from({ length: 6 }).map(() => + TbrWrapper.from($.keypair.generate(), oracleClient, DEBUG), ); const [ ownerClient, @@ -46,17 +46,19 @@ describe('Token Bridge Relayer Program', () => { unauthorizedClient, ] = clients; - const wormholeCoreOwner = $.provider.generate(); - const wormholeCoreClient = new WormholeCoreWrapper( + const wormholeCoreOwner = $.keypair.generate(); + const wormholeCoreClient = new TestingWormholeCore( wormholeCoreOwner, + $.connection, WormholeContracts.Network, $.pubkey.from(testProgramKeypair), WormholeContracts.addresses, ); - const tokenBridgeOwner = $.provider.generate(); - const tokenBridgeClient = new TokenBridgeWrapper( + const tokenBridgeOwner = $.keypair.generate(); + const tokenBridgeClient = new TestingTokenBridge( tokenBridgeOwner, + $.connection, WormholeContracts.Network, wormholeCoreClient.guardians, WormholeContracts.addresses, @@ -66,22 +68,18 @@ describe('Token Bridge Relayer Program', () => { const evmTransactionGas = 321_000n; const evmTransactionSize = 654_000n; - const ethereumTokenBridge = new UniversalAddress( - Buffer.from('e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1', 'hex'), - ); - const oasisTokenBridge = new UniversalAddress( - Buffer.from('0A51533333333333333333333333333333333333333333333333333333333333', 'hex'), - ); + const ethereumTokenBridge = $.universalAddress.generate(); + const oasisTokenBridge = $.universalAddress.generate(); - const ethereumTbrPeer1 = new UniversalAddress('0x' + '00'.repeat(12) + 'ab'.repeat(20)); - const ethereumTbrPeer2 = new UniversalAddress('0x' + '00'.repeat(12) + 'ca'.repeat(20)); - const oasisTbrPeer = new UniversalAddress('0x' + '00'.repeat(12) + '05'.repeat(20)); + const ethereumTbrPeer1 = $.universalAddress.generate('ethereum'); + const ethereumTbrPeer2 = $.universalAddress.generate('ethereum'); + const oasisTbrPeer = $.universalAddress.generate('ethereum'); before(async () => { await $.airdrop([ wormholeCoreOwner, tokenBridgeOwner, - ...clients.map((client) => client.provider), + ...clients.map((client) => client.signer), ]); // Programs Deployment @@ -104,11 +102,9 @@ describe('Token Bridge Relayer Program', () => { // Oracle Setup // ============ - const oracleAuthorityProvider = await $.provider.read(authorityKeypair); - const oracleAuthorityClient = await SolanaPriceOracle.create( - oracleAuthorityProvider.connection, - ); - await oracleAuthorityProvider.sendAndConfirm( + const oracleAuthorityProvider = await $.keypair.read(authorityKeypair); + const oracleAuthorityClient = await SolanaPriceOracle.create($.connection); + await $.sendAndConfirm( new Transaction().add( await oracleAuthorityClient.initialize(oracleAuthorityProvider.publicKey, [], []), await oracleAuthorityClient.registerEvmPrices(oracleAuthorityProvider.publicKey, { @@ -119,6 +115,7 @@ describe('Token Bridge Relayer Program', () => { }), await oracleAuthorityClient.updateSolPrice(oracleAuthorityProvider.publicKey, 113_000_000n), // SOL is at $113 ), + oracleAuthorityProvider, ); // Wormhole Core Setup @@ -137,7 +134,8 @@ describe('Token Bridge Relayer Program', () => { it('Is initialized!', async () => { const upgradeAuthorityClient = await TbrWrapper.create( - await $.provider.read(authorityKeypair), + await $.keypair.read(authorityKeypair), + $.connection, DEBUG, ); @@ -148,7 +146,7 @@ describe('Token Bridge Relayer Program', () => { }); const config = await unauthorizedClient.read.config(); - assert.key(config.owner).equal(ownerClient.publicKey); + expect(config.owner).deep.equal(ownerClient.publicKey); // The owner has an auth badge: expect(await unauthorizedClient.account.authBadge(ownerClient.publicKey).fetch()).deep.equal({ @@ -341,12 +339,7 @@ describe('Token Bridge Relayer Program', () => { it('Does not let unauthorized signers register or update a peer', async () => { // Unauthorized cannot register a peer: await assert - .promise( - unauthorizedClient.registerPeer( - ETHEREUM, - new UniversalAddress(PublicKey.unique().toBuffer()), - ), - ) + .promise(unauthorizedClient.registerPeer(ETHEREUM, $.universalAddress.generate())) .failsWith('AnchorError caused by account: auth_badge. Error Code: AccountNotInitialized.'); // Admin cannot make another peer canonical: @@ -397,7 +390,7 @@ describe('Token Bridge Relayer Program', () => { const config = await unauthorizedClient.read.config(); - assert.key(config.feeRecipient).equal(feeRecipient); + expect(config.feeRecipient).deep.equal(feeRecipient); expect(config.evmTransactionGas).equal(evmTransactionGas); expect(config.evmTransactionSize).equal(evmTransactionSize); }); @@ -412,25 +405,28 @@ describe('Token Bridge Relayer Program', () => { }); }); - describe('Querying the quote', () => { - it('Fetches the quote', async () => { + describe('Querying the relaying fee', () => { + it('No discrepancy between SDK and program calculation', async () => { const dropoff = 50000; // ETH0.05 - const result = await unauthorizedClient.relayingFee(ETHEREUM, dropoff); + const simulatedResult = await unauthorizedClient.relayingFeeSimulated(ETHEREUM, dropoff); + const offChainResult = await unauthorizedClient.relayingFee(ETHEREUM, dropoff); - expect(result).closeTo(0.361824, 0.000001); // SOL0.36, which is roughly $40 + expect(simulatedResult).closeTo(offChainResult, 0.000001); + // SOL.36, which is roughly $40: + expect(simulatedResult).closeTo(0.361824, 0.000001); }); }); describe('Running transfers', () => { - const ethereumTokenAddressFoo = new UniversalAddress('0x' + '00'.repeat(12) + '11'.repeat(20)); - const recipientForeignToken = $.provider.generate(); + const ethereumTokenAddressFoo = $.universalAddress.generate('ethereum'); + const recipientForeignToken = $.keypair.generate(); let recipientTokenAccountForeignToken = PublicKey.default; // Will be initialized down the line let clientForeignToken: TbrWrapper = null as any; before(async () => { [clientForeignToken] = await Promise.all([ - TbrWrapper.create(recipientForeignToken, DEBUG), + TbrWrapper.create(recipientForeignToken, $.connection, DEBUG), tokenBridgeClient.attestToken(ethereumTokenBridge, ETHEREUM, ethereumTokenAddressFoo, { decimals: 12, }), @@ -439,12 +435,12 @@ describe('Token Bridge Relayer Program', () => { }); it('Transfers SOL to another chain', async () => { - const tokenAccount = await $.wrapSol(unauthorizedClient.provider, 1_000_000); + const tokenAccount = await $.wrapSol(unauthorizedClient.signer, 1_000_000); const gasDropoffAmount = 5; const unwrapIntent = false; // Does not matter anyway const transferredAmount = 123789n; - const foreignAddress = new UniversalAddress(Keypair.generate().publicKey.toBuffer()); + const foreignAddress = $.universalAddress.generate(); const canonicalEthereum = await unauthorizedClient.read.canonicalPeer(ETHEREUM); await unauthorizedClient.transferTokens({ @@ -466,7 +462,7 @@ describe('Token Bridge Relayer Program', () => { expect(vaa.sequence).equal(sequence); expect(vaa.emitterChain).equal('Solana'); - assert.key(uaToPubkey(vaa.emitterAddress)).equal(tokenBridgeEmitter()); + expect(uaToPubkey(vaa.emitterAddress)).deep.equal(tokenBridgeEmitter()); expect(vaa.payload.to).deep.equal({ address: canonicalEthereum, chain: ETHEREUM }); // Since the native mint has 9 decimals, the last digit is removed by the token bridge: @@ -483,10 +479,10 @@ describe('Token Bridge Relayer Program', () => { const unwrapIntent = false; // Does not matter anyway const transferredAmount = 321654n; - const mint = await TestMint.create(ownerClient.provider, 10); - const tokenAccount = await mint.mint(1_000_000_000n, unauthorizedClient.provider); + const mint = await TestMint.create($.connection, ownerClient.signer, 10); + const tokenAccount = await mint.mint(1_000_000_000n, unauthorizedClient.signer); // - const foreignAddress = new UniversalAddress(Keypair.generate().publicKey.toBuffer()); + const foreignAddress = $.universalAddress.generate(); const canonicalEthereum = await unauthorizedClient.read.canonicalPeer(ETHEREUM); await unauthorizedClient.transferTokens({ @@ -507,7 +503,7 @@ describe('Token Bridge Relayer Program', () => { ); expect(vaa.sequence).equal(sequence); expect(vaa.emitterChain).equal('Solana'); - assert.key(uaToPubkey(vaa.emitterAddress)).equal(tokenBridgeEmitter()); + expect(uaToPubkey(vaa.emitterAddress)).deep.equal(tokenBridgeEmitter()); expect(vaa.payload.to).deep.equal({ address: canonicalEthereum, chain: ETHEREUM }); // Since the mint has 10 decimals, the last digit is removed by the token bridge: @@ -520,15 +516,20 @@ describe('Token Bridge Relayer Program', () => { }); it('Gets wrapped SOL back from another chain', async () => { - const [payer, recipient] = await $.airdrop([Keypair.generate(), $.provider.generate()]); + const [payer, recipient] = await $.airdrop([Keypair.generate(), $.keypair.generate()]); // Associated token account already existing (to test if it breaks the transfer completion): - const recipientTokenAccount = await $.createAssociatedTokenAccount(recipient, NATIVE_MINT); + const recipientTokenAccount = await spl.createAssociatedTokenAccount( + $.connection, + recipient, + spl.NATIVE_MINT, + recipient.publicKey, + ); const amount = 123n; const vaaAddress = await wormholeCoreClient.postVaa( payer, // The token originally comes from Solana's native mint - { amount, chain: 'Solana', address: new UniversalAddress(NATIVE_MINT.toBuffer()) }, + { amount, chain: 'Solana', address: new UniversalAddress(spl.NATIVE_MINT.toBuffer()) }, { chain: ETHEREUM, tokenBridge: ethereumTokenBridge, tbrPeer: ethereumTbrPeer1 }, { recipient: new UniversalAddress(recipient.publicKey.toBuffer()), @@ -540,8 +541,7 @@ describe('Token Bridge Relayer Program', () => { await unauthorizedClient.completeTransfer(vaa); - const balance = - await unauthorizedClient.provider.connection.getTokenAccountBalance(recipientTokenAccount); + const balance = await $.connection.getTokenAccountBalance(recipientTokenAccount); // The wrapped token has 8 decimals, but the native one has 9. We must multiply the amount by 10: expect(balance.value.amount).equal((amount * 10n).toString()); }); @@ -577,8 +577,7 @@ describe('Token Bridge Relayer Program', () => { await unauthorizedClient.completeTransfer(vaa); - const balance = - await unauthorizedClient.provider.connection.getTokenAccountBalance(associatedTokenAccount); + const balance = await $.connection.getTokenAccountBalance(associatedTokenAccount); expect(balance.value.amount).equal(amount.toString()); }); @@ -587,7 +586,7 @@ describe('Token Bridge Relayer Program', () => { const unwrapIntent = false; // Does not matter anyway const transferredAmount = 301_000_000n; - const foreignAddress = new UniversalAddress(Keypair.generate().publicKey.toBuffer()); + const foreignAddress = $.universalAddress.generate(); const canonicalEthereum = await unauthorizedClient.read.canonicalPeer(ETHEREUM); await clientForeignToken.transferTokens({ diff --git a/solana/tests/utils/helpers.ts b/solana/tests/utils/helpers.ts index 2d37f0fb..144c32f6 100644 --- a/solana/tests/utils/helpers.ts +++ b/solana/tests/utils/helpers.ts @@ -1,4 +1,4 @@ -import { AnchorProvider, BN, Provider, Wallet } from '@coral-xyz/anchor'; +import { BN } from '@coral-xyz/anchor'; import { PublicKey, Transaction, @@ -10,9 +10,11 @@ import { AccountInfo, Signer, SystemProgram, + sendAndConfirmTransaction, + Finality, } from '@solana/web3.js'; import * as spl from '@solana/spl-token'; -import { Contracts } from '@wormhole-foundation/sdk-definitions'; +import { Contracts, UniversalAddress } from '@wormhole-foundation/sdk-definitions'; import { ChainConfigAccount } from '@xlabs-xyz/solana-arbitrary-token-transfers'; import { expect } from 'chai'; import * as toml from 'toml'; @@ -20,13 +22,12 @@ import fs from 'fs/promises'; import fsSync from 'fs'; import { promisify } from 'util'; import { exec } from 'child_process'; +import { isTypedArray } from 'util/types'; const execAsync = promisify(exec); const LOCALHOST = 'http://localhost:8899'; -type ProviderWallet = AnchorProvider['wallet']; - export interface ErrorConstructor { new (...args: any[]): Error; } @@ -36,10 +37,6 @@ export const assert = { equal: (right: BN) => expect(left.toString()).equal(right.toString()), }), - key: (left: PublicKey) => ({ - equal: (right: PublicKey) => expect(left.toString()).equal(right.toString()), - }), - chainConfig: (left: ChainConfigAccount) => ({ equal: (right: ChainConfigAccount) => { expect(left.chainId).equal(right.chainId); @@ -90,79 +87,67 @@ export const assert = { }), }; -export async function sendAndConfirmIxs( - provider: AnchorProvider, - ixs: TransactionInstruction | Transaction | Array, - signatures: { signers?: Signer[]; wallets?: ProviderWallet[] } = {}, -): Promise { - const tx = Array.isArray(ixs) ? new Transaction().add(...ixs) : new Transaction().add(ixs); - tx.recentBlockhash = await provider.connection.getLatestBlockhash().then((ans) => ans.blockhash); - tx.feePayer = provider.publicKey; - - for (const wallet of signatures.wallets ?? []) { - await wallet.signTransaction(tx); - } - - return provider.sendAndConfirm(tx, signatures.signers); -} - -type HasPublicKey = PublicKey | Keypair | Wallet | Provider; +type HasPublicKey = PublicKey | Keypair | UniversalAddress | number[] | Uint8Array | Signer; function extractPubkey(from: HasPublicKey): PublicKey { if (from instanceof PublicKey) { return from; } else if (from instanceof Keypair) { return from.publicKey; - } else if (from instanceof Wallet) { - return from.publicKey; - } else if (from.publicKey !== undefined) { - return from.publicKey; + } else if (from instanceof UniversalAddress) { + return new PublicKey(from.toUint8Array()); + } else if (Array.isArray(from)) { + return Keypair.fromSecretKey(Uint8Array.from(from)).publicKey; + } else if (isTypedArray(from)) { + return Keypair.fromSecretKey(from).publicKey; } else { - throw new Error('The provider must have a public key to request airdrop'); + return from.publicKey; } } export class TestsHelper { static readonly LOCALHOST = 'http://localhost:8899'; readonly connection: Connection; + readonly finality: Finality; - private static readonly connections: Partial< - Record<'processed' | 'confirmed' | 'finalized', Connection> - > = {}; + /** Connections cache. */ + private static readonly connections: Partial> = {}; - constructor(commitment: 'processed' | 'confirmed' | 'finalized' = 'processed') { - if (TestsHelper.connections[commitment] === undefined) { - TestsHelper.connections[commitment] = new Connection(LOCALHOST, commitment); + constructor(finality: Finality = 'confirmed') { + if (TestsHelper.connections[finality] === undefined) { + TestsHelper.connections[finality] = new Connection(LOCALHOST, finality); } - this.connection = TestsHelper.connections[commitment]; + this.connection = TestsHelper.connections[finality]; + this.finality = finality; } pubkey = { + generate: (): PublicKey => PublicKey.unique(), read: async (path: string): Promise => this.keypair.read(path).then((kp) => kp.publicKey), - from: (bytes: number[]): PublicKey => this.keypair.from(bytes).publicKey, + from: (hasPublicKey: HasPublicKey): PublicKey => extractPubkey(hasPublicKey), + several: (amount: number): PublicKey[] => Array.from({ length: amount }).map(PublicKey.unique), }; keypair = { + generate: (): Keypair => Keypair.generate(), read: async (path: string): Promise => this.keypair.from(JSON.parse(await fs.readFile(path, { encoding: 'utf8' }))), from: (bytes: number[]): Keypair => Keypair.fromSecretKey(Uint8Array.from(bytes)), several: (amount: number): Keypair[] => Array.from({ length: amount }).map(Keypair.generate), }; - provider = { - generate: (): TestProvider => new TestProvider(this.connection, Keypair.generate()), - read: async (path: string): Promise => - new TestProvider(this.connection, await this.keypair.read(path)), - from: (bytesOrKeypair: number[] | Keypair): TestProvider => - new TestProvider( - this.connection, - Array.isArray(bytesOrKeypair) ? this.keypair.from(bytesOrKeypair) : bytesOrKeypair, - ), - several: (amount: number): TestProvider[] => - Array.from({ length: amount }).map(this.provider.generate), + universalAddress = { + generate: (ethereum?: 'ethereum'): UniversalAddress => + ethereum === 'ethereum' + ? new UniversalAddress( + Buffer.concat([Buffer.alloc(12), PublicKey.unique().toBuffer().subarray(12)]), + ) + : new UniversalAddress(PublicKey.unique().toBuffer()), + several: (amount: number, ethereum?: 'ethereum'): UniversalAddress[] => + Array.from({ length: amount }).map(() => this.universalAddress.generate(ethereum)), }; - /** Confirms a transaction. */ + /** Waits that a transaction is confirmed. */ async confirm(signature: TransactionSignature) { const latestBlockHash = await this.connection.getLatestBlockhash(); @@ -173,12 +158,20 @@ export class TestsHelper { }); } + async sendAndConfirm( + ixs: TransactionInstruction | Transaction | Array, + payer: Signer, + ...signers: Signer[] + ): Promise { + return sendAndConfirm(this.connection, ixs, payer, ...signers); + } + /** Requests airdrop to an account or several ones. */ async airdrop(to: T): Promise { const request = async (account: PublicKey) => this.confirm(await this.connection.requestAirdrop(account, 50 * LAMPORTS_PER_SOL)); - if (Array.isArray(to)) { + if (Array.isArray(to) && to.every((value) => typeof value !== 'number')) { await Promise.all(to.map((account) => request(extractPubkey(account)))); } else { await request(extractPubkey(to)); @@ -187,23 +180,14 @@ export class TestsHelper { return to; } - async createAssociatedTokenAccount(authority: TestProvider, mint: PublicKey) { - return await spl.createAssociatedTokenAccount( - authority.connection, - authority.signer, - mint, - authority.publicKey, - ); - } - /** Creates a new account and transfers wrapped SOL to it. */ - async wrapSol(from: AnchorProvider, amount: number): Promise { + async wrapSol(signer: Signer, amount: number): Promise { const tokenAccount = Keypair.generate(); const tx = new Transaction().add( // Allocate account: SystemProgram.createAccount({ - fromPubkey: from.publicKey, + fromPubkey: signer.publicKey, newAccountPubkey: tokenAccount.publicKey, space: spl.ACCOUNT_SIZE, lamports: await spl.getMinimumBalanceForRentExemptAccount(this.connection), @@ -213,11 +197,11 @@ export class TestsHelper { spl.createInitializeAccountInstruction( tokenAccount.publicKey, spl.NATIVE_MINT, - from.publicKey, + signer.publicKey, ), // Transfer SOL: SystemProgram.transfer({ - fromPubkey: from.publicKey, + fromPubkey: signer.publicKey, toPubkey: tokenAccount.publicKey, lamports: amount, }), @@ -225,7 +209,7 @@ export class TestsHelper { spl.createSyncNativeInstruction(tokenAccount.publicKey), ); - await sendAndConfirmIxs(from, tx, { signers: [tokenAccount] }); + await this.sendAndConfirm(tx, signer, tokenAccount); return tokenAccount; } @@ -255,22 +239,35 @@ export class TestsHelper { } export class TestMint { - private authority: TestProvider; + private authority: Signer; + + readonly connection: Connection; readonly address: PublicKey; readonly decimals: number; - private constructor(authority: TestProvider, address: PublicKey, decimals: number) { + private constructor( + connection: Connection, + authority: Signer, + address: PublicKey, + decimals: number, + ) { this.authority = authority; + this.connection = connection; this.address = address; this.decimals = decimals; } - static async create(authority: TestProvider, decimals: number): Promise { + static async create( + connection: Connection, + authority: Signer, + decimals: number, + ): Promise { return new TestMint( + connection, authority, await spl.createMint( - authority.connection, - authority.signer, + connection, + authority, authority.publicKey, authority.publicKey, decimals, @@ -279,7 +276,7 @@ export class TestMint { ); } - async mint(amount: number | bigint, accountAuthority: AnchorProvider) { + async mint(amount: number | bigint, accountAuthority: Signer) { const tokenAccount = Keypair.generate(); const tx = new Transaction().add( @@ -288,7 +285,7 @@ export class TestMint { fromPubkey: accountAuthority.publicKey, newAccountPubkey: tokenAccount.publicKey, space: spl.ACCOUNT_SIZE, - lamports: await spl.getMinimumBalanceForRentExemptAccount(this.authority.connection), + lamports: await spl.getMinimumBalanceForRentExemptAccount(this.connection), programId: spl.TOKEN_PROGRAM_ID, }), // Initialize token account: @@ -307,10 +304,7 @@ export class TestMint { ), ); - await sendAndConfirmIxs(accountAuthority, tx, { - wallets: [this.authority.wallet], - signers: [tokenAccount], - }); + await sendAndConfirm(this.connection, tx, accountAuthority, this.authority, tokenAccount); return tokenAccount; } @@ -362,19 +356,6 @@ export class WormholeContracts { } } -export class TestProvider extends AnchorProvider { - readonly signer: Signer; - - get keypair() { - return Keypair.fromSecretKey(this.signer.secretKey); - } - - constructor(connection: Connection, keypair: Keypair) { - super(connection, new Wallet(keypair)); - this.signer = keypair; - } -} - export function tokenBridgeEmitter(): PublicKey { return PublicKey.findProgramAddressSync( [Buffer.from('emitter')], @@ -389,3 +370,18 @@ export async function getBlockTime(connection: Connection): Promise { .then(async (slot) => connection.getBlockTime(slot)) .then((value) => value!); } + +export async function sendAndConfirm( + connection: Connection, + ixs: TransactionInstruction | Transaction | Array, + payer: Signer, + ...signers: Signer[] +): Promise { + const { value } = await connection.getLatestBlockhashAndContext(); + const tx = new Transaction({ + ...value, + feePayer: payer.publicKey, + }).add(...(Array.isArray(ixs) ? ixs : [ixs])); + + return sendAndConfirmTransaction(connection, tx, [payer, ...signers], {}); +} diff --git a/solana/tests/utils/tbr-wrapper.ts b/solana/tests/utils/tbr-wrapper.ts index 368af711..5700f5a1 100644 --- a/solana/tests/utils/tbr-wrapper.ts +++ b/solana/tests/utils/tbr-wrapper.ts @@ -1,5 +1,5 @@ import anchor from '@coral-xyz/anchor'; -import { Keypair, PublicKey, TransactionSignature } from '@solana/web3.js'; +import { Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; import { Chain } from '@wormhole-foundation/sdk-base'; import { UniversalAddress } from '@wormhole-foundation/sdk-definitions'; import { @@ -8,7 +8,7 @@ import { TransferParameters, VaaMessage, } from '@xlabs-xyz/solana-arbitrary-token-transfers'; -import { sendAndConfirmIxs, TestProvider, TestsHelper } from './helpers.js'; +import { TestsHelper } from './helpers.js'; import testProgramKeypair from '../../programs/token-bridge-relayer/test-program-keypair.json' with { type: 'json' }; @@ -16,52 +16,41 @@ const $ = new TestsHelper(); export class TbrWrapper { readonly client: SolanaTokenBridgeRelayer; - readonly provider: TestProvider; + readonly signer: Signer; readonly logs: { [key: string]: string[] }; readonly logsSubscriptionId: number; - constructor(provider: TestProvider, tbrClient: SolanaTokenBridgeRelayer) { - this.provider = provider; + constructor(signer: Signer, tbrClient: SolanaTokenBridgeRelayer) { this.client = tbrClient; + this.signer = signer; this.logs = {}; - this.logsSubscriptionId = provider.connection.onLogs( + this.logsSubscriptionId = this.client.connection.onLogs( 'all', (l) => (this.logs[l.signature] = l.logs), ); } - static from( - provider: TestProvider, - accountType: 'owner' | 'admin' | 'regular', - oracleClient: SolanaPriceOracle, - debug: boolean, - ) { - const clientProvider = - accountType === 'regular' ? provider : { connection: provider.connection }; - + static from(signer: Signer, oracleClient: SolanaPriceOracle, debug: boolean) { const client = new SolanaTokenBridgeRelayer( - clientProvider, + oracleClient.connection, 'Localnet', $.pubkey.from(testProgramKeypair), oracleClient, debug, ); - return new TbrWrapper(provider, client); + return new TbrWrapper(signer, client); } - static async create(provider: TestProvider, debug: boolean) { - const client = await SolanaTokenBridgeRelayer.create( - { connection: provider.connection }, - debug, - ); + static async create(signer: Signer, connection: Connection, debug: boolean) { + const client = await SolanaTokenBridgeRelayer.create(connection, debug); - return new TbrWrapper(provider, client); + return new TbrWrapper(signer, client); } get publicKey(): PublicKey { - return this.provider.publicKey; + return this.signer.publicKey; } get account() { @@ -74,7 +63,7 @@ export class TbrWrapper { /** Unregister the logs event so that the test does not hang. */ async close() { - await this.provider.connection.removeOnLogsListener(this.logsSubscriptionId); + await this.client.connection.removeOnLogsListener(this.logsSubscriptionId); } displayLogs(signature: string) { @@ -93,36 +82,36 @@ export class TbrWrapper { feeRecipient: PublicKey; admins: PublicKey[]; }): Promise { - return sendAndConfirmIxs(this.provider, await this.client.initialize(args)); + return $.sendAndConfirm(await this.client.initialize(args), this.signer); } async submitOwnerTransferRequest(newOwner: PublicKey): Promise { - return sendAndConfirmIxs(this.provider, await this.client.submitOwnerTransferRequest(newOwner)); + return $.sendAndConfirm(await this.client.submitOwnerTransferRequest(newOwner), this.signer); } async confirmOwnerTransferRequest(): Promise { - return await sendAndConfirmIxs(this.provider, await this.client.confirmOwnerTransferRequest()); + return await $.sendAndConfirm(await this.client.confirmOwnerTransferRequest(), this.signer); } async cancelOwnerTransferRequest(): Promise { - return sendAndConfirmIxs(this.provider, await this.client.cancelOwnerTransferRequest()); + return $.sendAndConfirm(await this.client.cancelOwnerTransferRequest(), this.signer); } async addAdmin(newAdmin: PublicKey): Promise { - return sendAndConfirmIxs(this.provider, await this.client.addAdmin(newAdmin)); + return $.sendAndConfirm(await this.client.addAdmin(newAdmin), this.signer); } async removeAdmin(adminToRemove: PublicKey): Promise { - return sendAndConfirmIxs( - this.provider, + return $.sendAndConfirm( await this.client.removeAdmin(this.publicKey, adminToRemove), + this.signer, ); } async registerPeer(chain: Chain, peerAddress: UniversalAddress): Promise { - return sendAndConfirmIxs( - this.provider, + return $.sendAndConfirm( await this.client.registerPeer(this.publicKey, chain, peerAddress), + this.signer, ); } @@ -130,37 +119,34 @@ export class TbrWrapper { chain: Chain, peerAddress: UniversalAddress, ): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.updateCanonicalPeer(chain, peerAddress), - ); + return $.sendAndConfirm(await this.client.updateCanonicalPeer(chain, peerAddress), this.signer); } async setPauseForOutboundTransfers(chain: Chain, paused: boolean): Promise { - return sendAndConfirmIxs( - this.provider, + return $.sendAndConfirm( await this.client.setPauseForOutboundTransfers(this.publicKey, chain, paused), + this.signer, ); } async updateMaxGasDropoff(chain: Chain, maxGasDropoff: number): Promise { - return sendAndConfirmIxs( - this.provider, + return $.sendAndConfirm( await this.client.updateMaxGasDropoff(this.publicKey, chain, maxGasDropoff), + this.signer, ); } async updateRelayerFee(chain: Chain, relayerFee: number): Promise { - return sendAndConfirmIxs( - this.provider, + return $.sendAndConfirm( await this.client.updateRelayerFee(this.publicKey, chain, relayerFee), + this.signer, ); } async updateFeeRecipient(newFeeRecipient: PublicKey): Promise { - return sendAndConfirmIxs( - this.provider, + return $.sendAndConfirm( await this.client.updateFeeRecipient(this.publicKey, newFeeRecipient), + this.signer, ); } @@ -168,32 +154,30 @@ export class TbrWrapper { evmTransactionGas: bigint, evmTransactionSize: bigint, ): Promise { - return sendAndConfirmIxs( - this.provider, + return $.sendAndConfirm( await this.client.updateEvmTransactionConfig( this.publicKey, evmTransactionGas, evmTransactionSize, ), + this.signer, ); } - async transferTokens( - params: TransferParameters, - signers?: Keypair[], - ): Promise { - return sendAndConfirmIxs( - this.provider, + /** Only the token owner can call this method. */ + async transferTokens(params: TransferParameters): Promise { + return $.sendAndConfirm( await this.client.transferTokens(this.publicKey, params), - { signers }, + this.signer, //...signers ); } async completeTransfer(vaa: VaaMessage): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.completeTransfer(this.publicKey, vaa), - ); + return $.sendAndConfirm(await this.client.completeTransfer(this.publicKey, vaa), this.signer); + } + + async relayingFeeSimulated(chain: Chain, dropoffAmount: number): Promise { + return this.client.relayingFeeSimulated(this.signer.publicKey, chain, dropoffAmount); } async relayingFee(chain: Chain, dropoffAmount: number): Promise { diff --git a/solana/tests/utils/token-bridge-wrapper.ts b/solana/tests/utils/testing-token-bridge.ts similarity index 76% rename from solana/tests/utils/token-bridge-wrapper.ts rename to solana/tests/utils/testing-token-bridge.ts index 6aac422c..051fad58 100644 --- a/solana/tests/utils/token-bridge-wrapper.ts +++ b/solana/tests/utils/testing-token-bridge.ts @@ -1,18 +1,26 @@ import { SolanaTokenBridge } from '@wormhole-foundation/sdk-solana-tokenbridge'; -import { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from '@solana/web3.js'; +import { + Connection, + Keypair, + PublicKey, + Signer, + SystemProgram, + SYSVAR_RENT_PUBKEY, +} from '@solana/web3.js'; import { Chain, layout, Network } from '@wormhole-foundation/sdk-base'; import { Contracts, UniversalAddress, createVAA } from '@wormhole-foundation/sdk-definitions'; import { mocks } from '@wormhole-foundation/sdk-definitions/testing'; import { utils as coreUtils } from '@wormhole-foundation/sdk-solana-core'; -import { getBlockTime, sendAndConfirmIxs, TestProvider } from './helpers.js'; +import { getBlockTime, sendAndConfirm } from './helpers.js'; import { SolanaSendSigner } from '@wormhole-foundation/sdk-solana'; import { signAndSendWait } from '@wormhole-foundation/sdk-connect'; import { layoutItems } from '@wormhole-foundation/sdk-definitions'; -export class TokenBridgeWrapper { +/** A Token Bridge wrapper allowing to write tests using this program in a local environment. */ +export class TestingTokenBridge { static sequence = 100n; - public readonly provider: TestProvider; + public readonly signer: Signer; public readonly client: SolanaTokenBridge; public readonly guardians: mocks.MockGuardians; @@ -22,20 +30,25 @@ export class TokenBridgeWrapper { * @param contracts At least the addresses `coreBridge` and `tokenBridge` must be provided. */ constructor( - provider: TestProvider, + signer: Signer, + connection: Connection, network: N, guardians: mocks.MockGuardians, contracts: Contracts, ) { - this.provider = provider; + this.signer = signer; this.guardians = guardians; - this.client = new SolanaTokenBridge(network, 'Solana', provider.connection, contracts); + this.client = new SolanaTokenBridge(network, 'Solana', connection, contracts); } get coreBridgeId() { return this.client.coreBridge.coreBridge.programId; } + get keypair() { + return Keypair.fromSecretKey(this.signer.secretKey); + } + get pda() { return { config: () => this.findPda(Buffer.from('config')), @@ -60,16 +73,16 @@ export class TokenBridgeWrapper { const ix = await this.client.tokenBridge.methods .initialize(this.coreBridgeId) .accounts({ - payer: this.provider.publicKey, + payer: this.signer.publicKey, config: this.pda.config(), }) .instruction(); - return await sendAndConfirmIxs(this.provider, ix); + return await sendAndConfirm(this.client.connection, ix, this.signer); } async registerPeer(chain: Chain, address: UniversalAddress) { - const sequence = TokenBridgeWrapper.sequence++; + const sequence = TestingTokenBridge.sequence++; const timestamp = await getBlockTime(this.client.connection); const emitterAddress = new UniversalAddress('00'.repeat(31) + '04'); const rawVaa = createVAA('TokenBridge:RegisterChain', { @@ -84,14 +97,8 @@ export class TokenBridgeWrapper { payload: { chain: 'Solana', actionArgs: { foreignChain: chain, foreignAddress: address } }, }); const vaa = this.guardians.addSignatures(rawVaa, [0]); - const txs = this.client.coreBridge.postVaa(this.provider.publicKey, vaa); - const signer = new SolanaSendSigner( - this.client.connection, - 'Solana', - this.provider.keypair, - false, - {}, - ); + const txs = this.client.coreBridge.postVaa(this.signer.publicKey, vaa); + const signer = new SolanaSendSigner(this.client.connection, 'Solana', this.keypair, false, {}); await signAndSendWait(txs, signer); const vaaAddress = coreUtils.derivePostedVaaKey(this.coreBridgeId, Buffer.from(vaa.hash)); @@ -99,7 +106,7 @@ export class TokenBridgeWrapper { const ix = await this.client.tokenBridge.methods .registerChain() .accounts({ - payer: this.provider.publicKey, + payer: this.signer.publicKey, vaa: vaaAddress, endpoint: this.pda.endpoint(chain, address), config: this.pda.config(), @@ -109,7 +116,7 @@ export class TokenBridgeWrapper { systemProgram: SystemProgram.programId, }) .instruction(); - await sendAndConfirmIxs(this.provider, ix); + await sendAndConfirm(this.client.connection, ix, this.signer); } async attestToken( @@ -118,15 +125,9 @@ export class TokenBridgeWrapper { mint: UniversalAddress, info: { decimals: number }, ) { - const signer = new SolanaSendSigner( - this.client.connection, - 'Solana', - this.provider.keypair, - false, - {}, - ); - - const sequence = TokenBridgeWrapper.sequence++; + const signer = new SolanaSendSigner(this.client.connection, 'Solana', this.keypair, false, {}); + + const sequence = TestingTokenBridge.sequence++; const timestamp = await getBlockTime(this.client.connection); const rawVaa = createVAA('TokenBridge:AttestMeta', { guardianSet: 0, @@ -145,10 +146,10 @@ export class TokenBridgeWrapper { }, }); const vaa = this.guardians.addSignatures(rawVaa, [0]); - const txsPostVaa = this.client.coreBridge.postVaa(this.provider.publicKey, vaa); + const txsPostVaa = this.client.coreBridge.postVaa(this.signer.publicKey, vaa); await signAndSendWait(txsPostVaa, signer); - const txsAttest = this.client.submitAttestation(rawVaa, this.provider.publicKey); + const txsAttest = this.client.submitAttestation(rawVaa, this.signer.publicKey); await signAndSendWait(txsAttest, signer); } diff --git a/solana/tests/utils/wormhole-core-wrapper.ts b/solana/tests/utils/testing-wormhole-core.ts similarity index 86% rename from solana/tests/utils/wormhole-core-wrapper.ts rename to solana/tests/utils/testing-wormhole-core.ts index 093e2aa5..85134d77 100644 --- a/solana/tests/utils/wormhole-core-wrapper.ts +++ b/solana/tests/utils/testing-wormhole-core.ts @@ -1,5 +1,5 @@ import anchor from '@coral-xyz/anchor'; -import { Keypair, PublicKey } from '@solana/web3.js'; +import { Connection, Keypair, PublicKey, Signer } from '@solana/web3.js'; import { Chain, encoding, @@ -20,7 +20,7 @@ import { import { mocks } from '@wormhole-foundation/sdk-definitions/testing'; import { VaaMessage } from '@xlabs-xyz/solana-arbitrary-token-transfers'; -import { getBlockTime, sendAndConfirmIxs, TestProvider } from './helpers.js'; +import { getBlockTime, sendAndConfirm } from './helpers.js'; import { accountDataLayout } from './layout.js'; import { serializeTbrV3Message } from 'common-arbitrary-token-transfer'; @@ -28,8 +28,9 @@ const guardianKey = 'cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee const guardianAddress = 'beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe'; const guardians = new mocks.MockGuardians(0, [guardianKey]); -export class WormholeCoreWrapper { - public readonly provider: TestProvider; +/** A Wormhole Core wrapper allowing to write tests using this program in a local environment. */ +export class TestingWormholeCore { + public readonly signer: Signer; public readonly client: SolanaWormholeCore; private sequence = 0n; private readonly toProgram: PublicKey; @@ -39,10 +40,16 @@ export class WormholeCoreWrapper { * @param solanaProgram The Solana Program used as a destination for the VAAs, _i.e._ the program being tested. * @param contracts At least the core program address `coreBridge` must be provided. */ - constructor(provider: TestProvider, network: N, testedProgram: PublicKey, contracts: Contracts) { - this.provider = provider; + constructor( + signer: Signer, + connection: Connection, + network: N, + testedProgram: PublicKey, + contracts: Contracts, + ) { + this.signer = signer; this.toProgram = testedProgram; - this.client = new SolanaWormholeCore(network, 'Solana', provider.connection, contracts); + this.client = new SolanaWormholeCore(network, 'Solana', connection, contracts); } get guardians(): mocks.MockGuardians { @@ -70,18 +77,18 @@ export class WormholeCoreWrapper { bridge: this.pda.bridge(), guardianSet: this.pda.guardianSet(), feeCollector: this.pda.feeCollector(), - payer: this.provider.publicKey, + payer: this.signer.publicKey, }) .instruction(); - return await sendAndConfirmIxs(this.provider, ix); + return await sendAndConfirm(this.client.connection, ix, this.signer); } /** Parse a VAA generated from the postVaa method, or from the Token Bridge during * and outbound transfer */ async parseVaa(key: PublicKey): Promise { - const info = await this.provider.connection.getAccountInfo(key); + const info = await this.client.connection.getAccountInfo(key); if (info === null) { throw new Error(`No message account exists at that address: ${key.toString()}`); } diff --git a/yarn.lock b/yarn.lock index 96171cc1..9386b4b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2112,7 +2112,6 @@ __metadata: "@wormhole-foundation/sdk-solana": "npm:^0.12.0" "@wormhole-foundation/sdk-solana-tokenbridge": "npm:^0.12.0" "@xlabs-xyz/solana-price-oracle-sdk": "npm:0.0.16" - borsh: "npm:^2.0.0" tsup: "npm:^8.3.5" tsx: "npm:4.19.2" typescript: "npm:5.6.3" @@ -2424,13 +2423,6 @@ __metadata: languageName: node linkType: hard -"borsh@npm:^2.0.0": - version: 2.0.0 - resolution: "borsh@npm:2.0.0" - checksum: 10c0/59a96dd9c707450862198510fc518dff92ac7f0ed0d228c9b38affd968bb4debec805c6e2afed8ec13efd9fad63fd47f8e6ed420253542a8d10fb59f28fc7d01 - languageName: node - linkType: hard - "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" From 30554be229aebcc9b953a7c8a61a81478af31b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix?= Date: Tue, 26 Nov 2024 22:13:34 +0100 Subject: [PATCH 4/8] More tests added --- sdk/solana/tbrv3/idl/token_bridge_relayer.ts | 6 ++ sdk/solana/tbrv3/token-bridge-relayer.ts | 31 +++++--- .../token-bridge-relayer/src/error.rs | 5 ++ .../src/processor/inbound.rs | 9 ++- solana/tests/token-bridge-relayer-tests.ts | 78 +++++++++++++++++-- target/idl/token_bridge_relayer.json | 6 ++ target/types/token_bridge_relayer.ts | 6 ++ 7 files changed, 121 insertions(+), 20 deletions(-) diff --git a/sdk/solana/tbrv3/idl/token_bridge_relayer.ts b/sdk/solana/tbrv3/idl/token_bridge_relayer.ts index d97be39f..7a723678 100644 --- a/sdk/solana/tbrv3/idl/token_bridge_relayer.ts +++ b/sdk/solana/tbrv3/idl/token_bridge_relayer.ts @@ -170,6 +170,7 @@ export type TokenBridgeRelayer = { "of the bridged tokens. Mutable." ], "writable": true, + "optional": true, "pda": { "seeds": [ { @@ -1787,6 +1788,11 @@ export type TokenBridgeRelayer = { "code": 6016, "name": "invalidPeerAddress", "msg": "invalidPeerAddress" + }, + { + "code": 6017, + "name": "missingAssociatedTokenAccount", + "msg": "missingAssociatedTokenAccount" } ], "types": [ diff --git a/sdk/solana/tbrv3/token-bridge-relayer.ts b/sdk/solana/tbrv3/token-bridge-relayer.ts index 1cad3a47..a7446809 100644 --- a/sdk/solana/tbrv3/token-bridge-relayer.ts +++ b/sdk/solana/tbrv3/token-bridge-relayer.ts @@ -668,15 +668,17 @@ export class SolanaTokenBridgeRelayer { */ async completeTransfer(signer: PublicKey, vaa: VaaMessage): Promise { const { wallet, associatedTokenAccount } = this.getRecipientAccountsFromVaa(vaa); + const { unwrapIntent } = deserializeTbrV3Message(vaa.payload.payload); const native = vaa.payload.token.chain === 'Solana'; const tokenBridgeAccounts = native ? this.tbAccBuilder.completeNative(vaa) : this.tbAccBuilder.completeWrapped(vaa); + const getSolDirectly = isNativeMint(tokenBridgeAccounts.mint) && unwrapIntent; const accounts = { payer: signer, tbrConfig: this.account.config().address, - recipientTokenAccount: associatedTokenAccount, + recipientTokenAccount: getSolDirectly ? null : associatedTokenAccount, recipient: wallet, vaa: this.account.vaa(vaa.hash).address, temporaryAccount: this.account.temporary(tokenBridgeAccounts.mint).address, @@ -688,17 +690,20 @@ export class SolanaTokenBridgeRelayer { this.logDebug('completeTransfer:', native ? 'Native:' : 'Wrapped:', objToString(accounts)); - const createAtaIdempotentIx = await createAssociatedTokenAccountIdempotent({ - signer, - mint: tokenBridgeAccounts.mint, - wallet, - }); - const completeTransferIx = await this.program.methods - .completeTransfer() - .accountsPartial(accounts) - .instruction(); + const [createAtaIdempotentIx, completeTransferIx] = await Promise.all([ + createAssociatedTokenAccountIdempotent({ + signer, + mint: tokenBridgeAccounts.mint, + wallet, + }), + this.program.methods.completeTransfer().accountsPartial(accounts).instruction(), + ]); - return [createAtaIdempotentIx, completeTransferIx]; + if (getSolDirectly) { + return [completeTransferIx]; + } else { + return [createAtaIdempotentIx, completeTransferIx]; + } } /* Queries */ @@ -995,3 +1000,7 @@ async function simulateTransaction( return response; } + +function isNativeMint(mintAddress: PublicKey): boolean { + return mintAddress.equals(spl.NATIVE_MINT) || mintAddress.equals(spl.NATIVE_MINT_2022); +} diff --git a/solana/programs/token-bridge-relayer/src/error.rs b/solana/programs/token-bridge-relayer/src/error.rs index ed5afc6e..d6403439 100644 --- a/solana/programs/token-bridge-relayer/src/error.rs +++ b/solana/programs/token-bridge-relayer/src/error.rs @@ -77,4 +77,9 @@ pub(crate) enum TokenBridgeRelayerError { /// The peer address is empty, please set a peer address. #[msg("InvalidPeerAddress")] InvalidPeerAddress, + + /// The associated token account can be skipped only when we get native tokens _and_ the + /// unwrap intent is set to `true`. + #[msg("MissingAssociatedTokenAccount")] + MissingAssociatedTokenAccount, } diff --git a/solana/programs/token-bridge-relayer/src/processor/inbound.rs b/solana/programs/token-bridge-relayer/src/processor/inbound.rs index e1d6e24c..65c3fe62 100644 --- a/solana/programs/token-bridge-relayer/src/processor/inbound.rs +++ b/solana/programs/token-bridge-relayer/src/processor/inbound.rs @@ -37,7 +37,7 @@ pub struct CompleteTransfer<'info> { associated_token::mint = mint, associated_token::authority = recipient )] - pub recipient_token_account: Box>, + pub recipient_token_account: Option>>, /// CHECK: recipient may differ from payer if a relayer paid for this /// transaction. This instruction verifies that the recipient key @@ -336,7 +336,12 @@ fn redeem_token( ctx.accounts.token_program.to_account_info(), anchor_spl::token::Transfer { from: ctx.accounts.temporary_account.to_account_info(), - to: ctx.accounts.recipient_token_account.to_account_info(), + to: ctx + .accounts + .recipient_token_account + .as_ref() + .ok_or(TokenBridgeRelayerError::MissingAssociatedTokenAccount)? + .to_account_info(), authority: ctx.accounts.wormhole_redeemer.to_account_info(), }, &[redeemer_seeds], diff --git a/solana/tests/token-bridge-relayer-tests.ts b/solana/tests/token-bridge-relayer-tests.ts index ac4a21c1..843bf306 100644 --- a/solana/tests/token-bridge-relayer-tests.ts +++ b/solana/tests/token-bridge-relayer-tests.ts @@ -423,10 +423,12 @@ describe('Token Bridge Relayer Program', () => { const recipientForeignToken = $.keypair.generate(); let recipientTokenAccountForeignToken = PublicKey.default; // Will be initialized down the line let clientForeignToken: TbrWrapper = null as any; + let barMint: TestMint = null as any; before(async () => { - [clientForeignToken] = await Promise.all([ + [clientForeignToken, barMint] = await Promise.all([ TbrWrapper.create(recipientForeignToken, $.connection, DEBUG), + await TestMint.create($.connection, ownerClient.signer, 10), tokenBridgeClient.attestToken(ethereumTokenBridge, ETHEREUM, ethereumTokenAddressFoo, { decimals: 12, }), @@ -479,8 +481,7 @@ describe('Token Bridge Relayer Program', () => { const unwrapIntent = false; // Does not matter anyway const transferredAmount = 321654n; - const mint = await TestMint.create($.connection, ownerClient.signer, 10); - const tokenAccount = await mint.mint(1_000_000_000n, unauthorizedClient.signer); // + const tokenAccount = await barMint.mint(1_000_000_000n, unauthorizedClient.signer); // const foreignAddress = $.universalAddress.generate(); const canonicalEthereum = await unauthorizedClient.read.canonicalPeer(ETHEREUM); @@ -533,8 +534,8 @@ describe('Token Bridge Relayer Program', () => { { chain: ETHEREUM, tokenBridge: ethereumTokenBridge, tbrPeer: ethereumTbrPeer1 }, { recipient: new UniversalAddress(recipient.publicKey.toBuffer()), - gasDropoff: 0, //TODO increase the dropoff - unwrapIntent: false, //TODO test with true + gasDropoff: 0, + unwrapIntent: false, }, ); const vaa = await wormholeCoreClient.parseVaa(vaaAddress); @@ -546,7 +547,70 @@ describe('Token Bridge Relayer Program', () => { expect(balance.value.amount).equal((amount * 10n).toString()); }); - //TODO get the other token back + it('Gets and unwraps wrapped SOL back from another chain', async () => { + const [payer, recipient] = await $.airdrop([Keypair.generate(), $.keypair.generate()]); + + const initialRecipientBalance = BigInt(await $.connection.getBalance(recipient.publicKey)); + + const amount = 432n; + + const vaaAddress = await wormholeCoreClient.postVaa( + payer, + // The token originally comes from Solana's native mint + { amount, chain: 'Solana', address: new UniversalAddress(spl.NATIVE_MINT.toBuffer()) }, + { chain: ETHEREUM, tokenBridge: ethereumTokenBridge, tbrPeer: ethereumTbrPeer1 }, + { + recipient: new UniversalAddress(recipient.publicKey.toBuffer()), + gasDropoff: 0, + unwrapIntent: true, + }, + ); + const vaa = await wormholeCoreClient.parseVaa(vaaAddress); + const { associatedTokenAccount } = unauthorizedClient.client.getRecipientAccountsFromVaa(vaa); + + await unauthorizedClient.completeTransfer(vaa); + + // No associated token account has been initialized because we want the SOL as gas, not SPL token: + const associatedTokenAccountBalance = await $.connection.getBalance(associatedTokenAccount); + expect(associatedTokenAccountBalance).equal(0); + + // Verify that we got the tokens: + const newRecipientBalance = BigInt(await $.connection.getBalance(recipient.publicKey)); + const transferredValue = amount * 10n; + + expect(newRecipientBalance).equal(initialRecipientBalance + transferredValue); + }); + + it('Gets wrapped Foo token back from another chain with dropoff', async () => { + const [payer, recipient] = await $.airdrop([Keypair.generate(), $.keypair.generate()]); + + const initialRecipientBalance = BigInt(await $.connection.getBalance(recipient.publicKey)); + const amount = 123n; + + const vaaAddress = await wormholeCoreClient.postVaa( + payer, + // The token originally comes from the Bar mint + { amount, chain: 'Solana', address: new UniversalAddress(barMint.address.toBuffer()) }, + { chain: ETHEREUM, tokenBridge: ethereumTokenBridge, tbrPeer: ethereumTbrPeer1 }, + { + recipient: new UniversalAddress(recipient.publicKey.toBuffer()), + gasDropoff: 0.1, // We want 0.1 SOL + unwrapIntent: false, + }, + ); + const vaa = await wormholeCoreClient.parseVaa(vaaAddress); + const { associatedTokenAccount } = unauthorizedClient.client.getRecipientAccountsFromVaa(vaa); + + await unauthorizedClient.completeTransfer(vaa); + + const balance = await $.connection.getTokenAccountBalance(associatedTokenAccount); + // The wrapped token has 8 decimals, but this token has 10. We must multiply the amount by 100: + expect(balance.value.amount).equal((amount * 100n).toString()); + + // Verify that we got the dropoff: + const newRecipientBalance = BigInt(await $.connection.getBalance(recipient.publicKey)); + expect(newRecipientBalance).equal(initialRecipientBalance + 100_000_000n); + }); it('Gets a foreign token from another chain', async () => { const payer = await $.airdrop(Keypair.generate()); @@ -563,7 +627,7 @@ describe('Token Bridge Relayer Program', () => { // TBRv3 message: { recipient: new UniversalAddress(recipientForeignToken.publicKey.toBuffer()), - gasDropoff: 0, //TODO increase the dropoff + gasDropoff: 0, unwrapIntent: false, }, ); diff --git a/target/idl/token_bridge_relayer.json b/target/idl/token_bridge_relayer.json index bd42dbd4..f51fc4bf 100644 --- a/target/idl/token_bridge_relayer.json +++ b/target/idl/token_bridge_relayer.json @@ -164,6 +164,7 @@ "of the bridged tokens. Mutable." ], "writable": true, + "optional": true, "pda": { "seeds": [ { @@ -1781,6 +1782,11 @@ "code": 6016, "name": "InvalidPeerAddress", "msg": "InvalidPeerAddress" + }, + { + "code": 6017, + "name": "MissingAssociatedTokenAccount", + "msg": "MissingAssociatedTokenAccount" } ], "types": [ diff --git a/target/types/token_bridge_relayer.ts b/target/types/token_bridge_relayer.ts index d97be39f..7a723678 100644 --- a/target/types/token_bridge_relayer.ts +++ b/target/types/token_bridge_relayer.ts @@ -170,6 +170,7 @@ export type TokenBridgeRelayer = { "of the bridged tokens. Mutable." ], "writable": true, + "optional": true, "pda": { "seeds": [ { @@ -1787,6 +1788,11 @@ export type TokenBridgeRelayer = { "code": 6016, "name": "invalidPeerAddress", "msg": "invalidPeerAddress" + }, + { + "code": 6017, + "name": "missingAssociatedTokenAccount", + "msg": "missingAssociatedTokenAccount" } ], "types": [ From 41e3725083ea1c44d87341df2e0c1e6a1e942cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix?= Date: Tue, 26 Nov 2024 22:20:37 +0100 Subject: [PATCH 5/8] Fix build error --- solana/programs/token-bridge-relayer/build.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/solana/programs/token-bridge-relayer/build.rs b/solana/programs/token-bridge-relayer/build.rs index 155b9726..28590768 100644 --- a/solana/programs/token-bridge-relayer/build.rs +++ b/solana/programs/token-bridge-relayer/build.rs @@ -33,7 +33,7 @@ struct Network { #[cfg(feature = "mainnet")] mainnet: String, #[cfg(feature = "testnet")] - testnet: NetworkAddresses, + testnet: String, } impl Network { @@ -48,12 +48,12 @@ impl Network { } #[cfg(feature = "mainnet")] - fn value(&self) -> Result { + fn value(&self) -> Result { Ok(self.mainnet.clone()) } #[cfg(feature = "testnet")] - fn value(&self) -> Result { + fn value(&self) -> Result { Ok(self.testnet.clone()) } From 282c434abfa820475061bd0677c0ee733adfa270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Claudio=20Nale?= Date: Tue, 26 Nov 2024 18:59:47 -0300 Subject: [PATCH 6/8] deployment: fixes solana scripts --- deployment/solana/configure.ts | 27 ++++++++++++------------- deployment/solana/initialize.ts | 25 +++++++++++------------ deployment/solana/read-configuration.ts | 2 +- deployment/solana/register-peers.ts | 27 ++++++++++++------------- deployment/solana/unpause-contract.ts | 24 +++++++++++----------- 5 files changed, 51 insertions(+), 54 deletions(-) diff --git a/deployment/solana/configure.ts b/deployment/solana/configure.ts index 0f7b0c8d..aa27a361 100644 --- a/deployment/solana/configure.ts +++ b/deployment/solana/configure.ts @@ -1,28 +1,23 @@ -import { BN } from '@coral-xyz/anchor'; import { SolanaTokenBridgeRelayer } from '@xlabs-xyz/solana-arbitrary-token-transfers'; -import { runOnSolana, ledgerSignAndSend, getConnection, SolanaSigner } from '../helpers/solana.js'; -import { SolanaChainInfo, LoggerFn } from '../helpers/interfaces.js'; +import { runOnSolana, ledgerSignAndSend, getConnection } from '../helpers/solana.js'; +import { SolanaScriptCb } from '../helpers/interfaces.js'; import { dependencies } from '../helpers/env.js'; import { PublicKey } from '@solana/web3.js'; -import { getChainConfig, getChainInfo } from '../helpers/env'; +import { getChainConfig } from '../helpers/env'; import { SolanaTbrV3Config } from '../config/config.types.js'; -runOnSolana('configure-tbr', configureSolanaTbr).catch((error) => { - console.error('Error executing script: ', error); -}); - -async function configureSolanaTbr( - chain: SolanaChainInfo, - signer: SolanaSigner, - log: LoggerFn, -): Promise { +const configureSolanaTbr: SolanaScriptCb = async function ( + chain, + signer, + log, +) { const signerKey = new PublicKey(await signer.getAddress()); const connection = getConnection(chain); const solanaDependencies = dependencies.find((d) => d.chainId === chain.chainId); if (solanaDependencies === undefined) { throw new Error(`No dependencies found for chain ${chain.chainId}`); } - const tbr = await SolanaTokenBridgeRelayer.create({ connection }); + const tbr = await SolanaTokenBridgeRelayer.create(connection); const config = await getChainConfig('tbr-v3', chain.chainId); @@ -56,3 +51,7 @@ async function configureSolanaTbr( await ledgerSignAndSend(connection, [ix], []); } } + +runOnSolana('configure-tbr', configureSolanaTbr).catch((error) => { + console.error('Error executing script: ', error); +}); diff --git a/deployment/solana/initialize.ts b/deployment/solana/initialize.ts index adf8164d..f091efcc 100644 --- a/deployment/solana/initialize.ts +++ b/deployment/solana/initialize.ts @@ -1,21 +1,16 @@ import { SolanaTokenBridgeRelayer } from '@xlabs-xyz/solana-arbitrary-token-transfers'; -import { runOnSolana, ledgerSignAndSend, getConnection, SolanaSigner } from '../helpers/solana.js'; -import { SolanaChainInfo, LoggerFn } from '../helpers/interfaces.js'; +import { runOnSolana, ledgerSignAndSend, getConnection } from '../helpers/solana.js'; +import { SolanaScriptCb } from '../helpers/interfaces.js'; import { PublicKey } from '@solana/web3.js'; import { loadSolanaTbrInitParams } from '../helpers/env.js'; -runOnSolana('initialize-tbr', initializeSolanaTbr).catch((e) => { - console.error('Error executing script: ', e); -}); - -async function initializeSolanaTbr( - chain: SolanaChainInfo, - signer: SolanaSigner, - log: LoggerFn, -): Promise { - const signerKey = new PublicKey(await signer.getAddress()); +const initializeSolanaTbr: SolanaScriptCb = async function ( + chain, + // signer, + // log, +) { const connection = getConnection(chain); - const tbr = await SolanaTokenBridgeRelayer.create({ connection }); + const tbr = await SolanaTokenBridgeRelayer.create(connection); const tbrInitParams = loadSolanaTbrInitParams(); @@ -35,3 +30,7 @@ async function initializeSolanaTbr( await ledgerSignAndSend(connection, [initializeIx], []); } + +runOnSolana('initialize-tbr', initializeSolanaTbr).catch((e) => { + console.error('Error executing script: ', e); +}); diff --git a/deployment/solana/read-configuration.ts b/deployment/solana/read-configuration.ts index 6daad0e4..4a400bbe 100644 --- a/deployment/solana/read-configuration.ts +++ b/deployment/solana/read-configuration.ts @@ -15,7 +15,7 @@ const readChainConfig: SolanaScriptCb = async function ( if (solanaDependencies === undefined) { throw new Error(`No dependencies found for chain ${operatingChain.chainId}`); } - const tbr = await SolanaTokenBridgeRelayer.create({ connection }); + const tbr = await SolanaTokenBridgeRelayer.create(connection); let allChainConfigs: ChainConfigEntry[]; try { diff --git a/deployment/solana/register-peers.ts b/deployment/solana/register-peers.ts index 546e8831..ebca1888 100644 --- a/deployment/solana/register-peers.ts +++ b/deployment/solana/register-peers.ts @@ -1,9 +1,8 @@ -import { BN } from '@coral-xyz/anchor'; import { chainIdToChain } from '@wormhole-foundation/sdk-base'; import { UniversalAddress } from '@wormhole-foundation/sdk-definitions'; import { SolanaTokenBridgeRelayer } from '@xlabs-xyz/solana-arbitrary-token-transfers'; -import { runOnSolana, ledgerSignAndSend, getConnection, SolanaSigner } from '../helpers/solana.js'; -import { SolanaChainInfo, LoggerFn } from '../helpers/interfaces.js'; +import { runOnSolana, ledgerSignAndSend, getConnection } from '../helpers/solana.js'; +import { SolanaScriptCb } from '../helpers/interfaces.js'; import { dependencies } from '../helpers/env.js'; import { PublicKey } from '@solana/web3.js'; import { contracts } from '../helpers'; @@ -18,23 +17,18 @@ type ChainConfigEntry = { canonicalPeer: UniversalAddress; }; -runOnSolana('configure-tbr', configureSolanaTbr).catch((error) => { - console.error('Error executing script: ', error); - console.log('extra logs', error.getLogs()); -}); - -async function configureSolanaTbr( - chain: SolanaChainInfo, - signer: SolanaSigner, - log: LoggerFn, -): Promise { +const configureSolanaTbr: SolanaScriptCb = async function ( + chain, + signer, + log, +) { const signerKey = new PublicKey(await signer.getAddress()); const connection = getConnection(chain); const solanaDependencies = dependencies.find((d) => d.chainId === chain.chainId); if (solanaDependencies === undefined) { throw new Error(`No dependencies found for chain ${chain.chainId}`); } - const tbr = await SolanaTokenBridgeRelayer.create({ connection }); + const tbr = await SolanaTokenBridgeRelayer.create(connection); for (const tbrDeployment of contracts['TbrV3Proxies']) { if (tbrDeployment.chainId === chain.chainId) continue; // skip self; @@ -113,3 +107,8 @@ async function configureSolanaTbr( } } } + +runOnSolana('configure-tbr', configureSolanaTbr).catch((error) => { + console.error('Error executing script: ', error); + console.log('extra logs', error.getLogs()); +}); diff --git a/deployment/solana/unpause-contract.ts b/deployment/solana/unpause-contract.ts index 2f5ff28a..5f6a1cae 100644 --- a/deployment/solana/unpause-contract.ts +++ b/deployment/solana/unpause-contract.ts @@ -1,27 +1,27 @@ import { SolanaTokenBridgeRelayer } from '@xlabs-xyz/solana-arbitrary-token-transfers'; -import { runOnSolana, ledgerSignAndSend, getConnection, SolanaSigner } from '../helpers/solana.js'; -import { SolanaChainInfo, LoggerFn } from '../helpers/interfaces.js'; +import { runOnSolana, ledgerSignAndSend, getConnection } from '../helpers/solana.js'; +import { SolanaScriptCb } from '../helpers/interfaces.js'; import { dependencies } from '../helpers/env.js'; import { PublicKey } from '@solana/web3.js'; -runOnSolana('unpause-contract', unpauseContract).catch((e) => { - console.error('Error executing script: ', e); -}); - -async function unpauseContract( - chain: SolanaChainInfo, - signer: SolanaSigner, - log: LoggerFn, -): Promise { +const unpauseContract: SolanaScriptCb = async function ( + chain, + signer, + // log, +) { const signerKey = new PublicKey(await signer.getAddress()); const connection = getConnection(chain); const solanaDependencies = dependencies.find((d) => d.chainId === chain.chainId); if (solanaDependencies === undefined) { throw new Error(`No dependencies found for chain ${chain.chainId}`); } - const tbr = await SolanaTokenBridgeRelayer.create({ connection }); + const tbr = await SolanaTokenBridgeRelayer.create(connection); const initializeIx = await tbr.setPauseForOutboundTransfers(signerKey, 'Sepolia', false); await ledgerSignAndSend(connection, [initializeIx], []); } + +runOnSolana('unpause-contract', unpauseContract).catch((e) => { + console.error('Error executing script: ', e); +}); From c807718766f7d97b4020a5fac19d390bf37e668c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Claudio=20Nale?= Date: Tue, 26 Nov 2024 19:00:20 -0300 Subject: [PATCH 7/8] connect: fixes solana implementation --- connect/platforms/solana/src/automatic.ts | 72 +++++++---------------- 1 file changed, 21 insertions(+), 51 deletions(-) diff --git a/connect/platforms/solana/src/automatic.ts b/connect/platforms/solana/src/automatic.ts index 4cef39b8..10b375fa 100644 --- a/connect/platforms/solana/src/automatic.ts +++ b/connect/platforms/solana/src/automatic.ts @@ -8,7 +8,6 @@ import { PublicKey, SystemProgram, Transaction, - TransactionInstruction, VersionedTransaction, } from '@solana/web3.js'; import { Chain, Network, amount as sdkAmount } from '@wormhole-foundation/sdk-base'; @@ -81,7 +80,7 @@ export class AutomaticTokenBridgeV3Solana Date: Tue, 26 Nov 2024 19:00:59 -0300 Subject: [PATCH 8/8] deployment: fixes test transfer in solana --- deployment/test/test-transfer.ts | 53 +++++++++----------------------- 1 file changed, 15 insertions(+), 38 deletions(-) diff --git a/deployment/test/test-transfer.ts b/deployment/test/test-transfer.ts index 6b56ee49..2a8909a0 100644 --- a/deployment/test/test-transfer.ts +++ b/deployment/test/test-transfer.ts @@ -22,8 +22,7 @@ import { EvmAddress } from '@wormhole-foundation/sdk-evm'; import { PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js'; import { SolanaTokenBridgeRelayer, - TransferNativeParameters, - TransferWrappedParameters, + TransferParameters, } from '@xlabs-xyz/solana-arbitrary-token-transfers'; import { createAssociatedTokenAccountIdempotentInstruction, @@ -304,48 +303,26 @@ async function sendSolanaTestTransaction( throw new Error(`No dependencies found for chain ${chain.chainId}`); } - const tbr = await SolanaTokenBridgeRelayer.create({ connection }); + const tbr = await SolanaTokenBridgeRelayer.create(connection); const evmAddress = getEnv('RECIPIENT_EVM_ADDRESS'); const maxFeeLamports = BigInt(getEnvOrDefault('MAX_FEE_KLAMPORTS', '5000000')); let transferIx: TransactionInstruction; - if (testTransfer.tokenChain !== "Solana") { - const tokenChain = testTransfer.tokenChain ?? getEnvOrDefault('TOKEN_CHAIN', 'Sepolia') as Chain; - const params = { - recipient: { - chain: targetChain.name as Chain, - address: toUniversal(targetChain.name as Chain, evmAddress), - }, - token: { - chain: tokenChain, - address: toUniversal(testTransfer.tokenChain, testTransfer.sourceTokenAddress ?? testTransfer.tokenAddress!), - }, - userTokenAccount: tokenAccount, - transferredAmount, - gasDropoffAmount, - maxFeeLamports, - unwrapIntent, - } satisfies TransferWrappedParameters; - - transferIx = await tbr.transferWrappedTokens(signerKey, params); - } else { - const params = { - recipient: { - chain: targetChain.name as Chain, - address: toUniversal(targetChain.name as Chain, evmAddress), - }, - mint, - tokenAccount, - transferredAmount, - gasDropoffAmount, - maxFeeLamports, - unwrapIntent, - } satisfies TransferNativeParameters; - - transferIx = await tbr.transferNativeTokens(signerKey, params); - } + const params = { + recipient: { + chain: targetChain.name as Chain, + address: toUniversal(targetChain.name as Chain, evmAddress), + }, + userTokenAccount: tokenAccount, + transferredAmount, + gasDropoffAmount, + maxFeeLamports, + unwrapIntent, + } satisfies TransferParameters; + + transferIx = await tbr.transferTokens(signerKey, params); const ixs: TransactionInstruction[] = []; // if transferring SOL first we have to wrap it