diff --git a/.pnp.cjs b/.pnp.cjs index 18e42153..c2a50808 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"],\ @@ -2690,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"]\ @@ -3046,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", [\ @@ -5986,7 +5979,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/.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 bc9aa0ce..00000000 Binary files a/.yarn/cache/borsh-npm-2.0.0-69f1235e8b-59a96dd9c7.zip and /dev/null differ 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/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 { - 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); +}); 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 diff --git a/sdk/solana/package.json b/sdk/solana/package.json index 51bcf37a..9fbde1f8 100644 --- a/sdk/solana/package.json +++ b/sdk/solana/package.json @@ -23,13 +23,13 @@ }, "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", "@wormhole-foundation/sdk-solana": "^0.12.0", "@wormhole-foundation/sdk-solana-tokenbridge": "^0.12.0", - "@xlabs-xyz/solana-price-oracle-sdk": "0.0.16", - "borsh": "^2.0.0" + "@xlabs-xyz/solana-price-oracle-sdk": "0.0.16" }, "devDependencies": { "@types/node": "20.17.5", diff --git a/sdk/solana/tbrv3/idl/token_bridge_relayer.ts b/sdk/solana/tbrv3/idl/token_bridge_relayer.ts index 59103e75..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": [ @@ -1996,6 +2002,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..a7446809 100644 --- a/sdk/solana/tbrv3/token-bridge-relayer.ts +++ b/sdk/solana/tbrv3/token-bridge-relayer.ts @@ -9,8 +9,12 @@ import { Cluster, LAMPORTS_PER_SOL, Keypair, + TransactionMessage, + VersionedTransaction, + SimulatedTransactionResponse, + TransactionResponse, } from '@solana/web3.js'; -import * as borsh from 'borsh'; +import * as spl from '@solana/spl-token'; import { Chain, chainToChainId, @@ -18,14 +22,12 @@ import { contracts, Network, chainIdToChain, + deserializeLayout, + serializeLayout, + Layout, + LayoutToType, } 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, 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'; @@ -34,6 +36,7 @@ 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 { TokenBridgeCpiAccountsBuilder } from './token-bridge-cpi-accounts-builder.js'; // Export IDL export * from './idl/token_bridge_relayer.js'; @@ -46,26 +49,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']; @@ -80,12 +78,14 @@ 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; private readonly priceOracleClient: SolanaPriceOracle; private readonly wormholeProgramId: PublicKey; private readonly tokenBridgeProgramId: PublicKey; + private readonly tbAccBuilder: TokenBridgeCpiAccountsBuilder; public debug: boolean; @@ -94,7 +94,7 @@ export class SolanaTokenBridgeRelayer { * use `SolanaTokenBridgeRelayer.create`. */ constructor( - provider: anchor.Provider, + connection: Connection, network: TbrNetwork, programId: PublicKey, priceOracle: SolanaPriceOracle, @@ -102,10 +102,15 @@ export class SolanaTokenBridgeRelayer { ) { const wormholeNetwork = network === 'Localnet' ? 'Testnet' : network; - this.program = new anchor.Program(patchAddress(IDL, programId), provider); + this.program = new anchor.Program(patchAddress(IDL, programId), { connection }); 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; } @@ -114,64 +119,77 @@ export class SolanaTokenBridgeRelayer { * Creates a new instance by using the values in `network.json` in the program directory. */ static async create( - provider: anchor.Provider, + connection: Connection, debug: boolean = false, ): Promise { - 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 { - 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()]), - //TODO read the VAA with `fetch` - vaa: (vaaHash: Uint8Array) => - findPda(this.wormholeProgramId, [Buffer.from('PostedVAA'), vaaHash]), - redeemer: () => findPda(this.program.programId, [Buffer.from('redeemer')]), + temporary: (mint: PublicKey) => this.pda('tmp', mint.toBuffer()), + /** 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(), + 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; }, }; }, @@ -189,6 +207,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)), @@ -214,11 +243,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( + serializeLayout({ ...layoutItems.chainItem(), endianness: 'big' }, chain), + ); const states = await this.program.account.peerState .all(filter) .then((state) => state.map((pa) => pa.account)); @@ -281,19 +311,29 @@ 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 a PDA belonging to this program. */ + pda(...seeds: Array) { + return findPda(this.programId, seeds); + } - const sequenceNumber = await impl(payer); - this.logDebug({ payerSequenceNumber: sequenceNumber.toString() }); - return sequenceNumber; + /** Returns the end user's wallet and associated token account */ + getRecipientAccountsFromVaa(vaa: VaaMessage): { + wallet: PublicKey; + associatedTokenAccount: PublicKey; + } { + 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 }; } /* Initialize */ @@ -316,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'); @@ -552,89 +592,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 +646,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 +665,21 @@ 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 { 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, - recipient: new PublicKey(recipient.address), + recipientTokenAccount: getSolDirectly ? null : associatedTokenAccount, + recipient: wallet, vaa: this.account.vaa(vaa.hash).address, temporaryAccount: this.account.temporary(tokenBridgeAccounts.mint).address, ...tokenBridgeAccounts, @@ -696,59 +688,67 @@ export class SolanaTokenBridgeRelayer { peer: this.account.peer(vaa.emitterChain, vaa.payload.from).address, }; - this.logDebug('completeNativeTransfer:', accounts); + this.logDebug('completeTransfer:', native ? 'Native:' : 'Wrapped:', objToString(accounts)); - return 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(), + ]); + + if (getSolDirectly) { + return [completeTransferIx]; + } else { + return [createAtaIdempotentIx, completeTransferIx]; + } } + /* Queries */ + /** - * Signer: typically the Token Bridge. - * - * @param signer - * @param vaa - * @param recipientTokenAccount The user's account receiving the SPL tokens. + * @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 completeWrappedTransfer( - signer: PublicKey, - vaa: VaaMessage, - recipientTokenAccount: PublicKey, - ): Promise { - const tokenBridgeAccounts = completeWrappedTokenBridgeAccounts({ - tokenBridgeProgramId: this.tokenBridgeProgramId, - wormholeProgramId: this.wormholeProgramId, - vaa, - }); - 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, - }; + async relayingFee(chain: Chain, dropoffAmount: number): Promise { + 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(), + ]); - this.logDebug('completeWrappedTransfer:', accounts); + const MWEI_PER_MICRO_ETH = 1_000_000n; + const MWEI_PER_ETH = 1_000_000_000_000n; - return this.program.methods.completeTransfer().accountsPartial(accounts).instruction(); - } + 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); - /* Queries */ + 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. + * @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 { - assertProvider(this.program.provider); - - const tx = await this.program.methods + async relayingFeeSimulated( + payer: PublicKey, + chain: Chain, + dropoffAmount: number, + ): Promise { + const ix = await this.program.methods .relayingFee(dropoffAmount) .accountsStrict({ tbrConfig: this.account.config().address, @@ -756,258 +756,156 @@ 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; } + /* 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), }; } - /* HELPERS */ - logDebug(message?: any, ...optionalParams: any[]) { - conditionalDebug(this.debug, message, ...optionalParams); + 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; } -} -function conditionalDebug(debug: boolean, message?: any, ...optionalParams: any[]) { - if (debug) { - console.debug('[SolanaTokenBridgeRelayer]', message, ...optionalParams); + /** Get the info about the foreign address from a Wormhole mint */ + private async getWormholeAddressFromWrappedMint(mint: PublicKey): Promise { + const metaAddress = findPda(this.tokenBridgeProgramId, ['meta', mint.toBuffer()]).address; + 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 } = deserializeLayout( + [ + { name: 'chain', ...layoutItems.chainItem(), endianness: 'little' }, + { name: 'address', ...layoutItems.universalAddressItem }, + { name: 'decimals', binary: 'uint', size: 1 }, + ], + data, + ); + + return { chain, address }; } -} -const chainSeed = (chain: Chain) => encoding.bignum.toBytes(chainToChainId(chain), 2); -function findPda(programId: PublicKey, seeds: Array) { - const [address, seed] = PublicKey.findProgramAddressSync(seeds, programId); - return { - address, - seed, - }; -} + private get tokenBridgeMintAuthority(): PublicKey { + return findPda(this.tokenBridgeProgramId, ['mint_signer']).address; + } -function assertProvider(provider: anchor.Provider) { - if (provider.sendAndConfirm === undefined) { - throw new Error('The client must be created with a full provider to use this method'); + private logDebug(message?: any, ...optionalParams: any[]) { + conditionalDebug(this.debug, message, ...optionalParams); } } -export function returnedDataFromTransaction( - schema: borsh.Schema, - confirmedTransaction: VersionedTransactionResponse | null, -) { - 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'); +function conditionalDebug(debug: boolean, message?: any, ...optionalParams: any[]) { + if (debug) { + console.debug('[SolanaTokenBridgeRelayer]', message, ...optionalParams); } - // The line looks like 'Program return: ': - const [, data] = log.slice(prefix.length).split(' ', 2); - - 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 chainSeed = (chain: Chain) => encoding.bignum.toBytes(chainToChainId(chain), 2); - const { - tokenBridgeConfig, - tokenBridgeCustody, - tokenBridgeAuthoritySigner, - tokenBridgeCustodySigner, - wormholeBridge, - tokenBridgeEmitter, - tokenBridgeSequence, - wormholeFeeCollector, - } = getTransferNativeWithPayloadCpiAccounts( +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, - 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, + address, + bump, }; } -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 getAssociatedTokenAccount(wallet: PublicKey, mint: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [wallet.toBuffer(), spl.TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], + spl.ASSOCIATED_TOKEN_PROGRAM_ID, + )[0]; } -function completeNativeTokenBridgeAccounts(params: { - tokenBridgeProgramId: PublicKey; - wormholeProgramId: PublicKey; - vaa: VaaMessage; -}): { - tokenBridgeConfig: PublicKey; - tokenBridgeClaim: PublicKey; - tokenBridgeForeignEndpoint: PublicKey; - tokenBridgeCustody: PublicKey; - tokenBridgeCustodySigner: PublicKey; - tokenBridgeMintAuthority: null; - tokenBridgeWrappedMeta: null; +/** Return both the address and an idempotent instruction to create it. */ +async function createAssociatedTokenAccountIdempotent({ + signer, + mint, + wallet, +}: { + signer: PublicKey; mint: PublicKey; -} { - const { tokenBridgeProgramId, wormholeProgramId, vaa } = params; - - const { - tokenBridgeConfig, - tokenBridgeClaim, - tokenBridgeForeignEndpoint, - tokenBridgeCustody, - tokenBridgeCustodySigner, + wallet: PublicKey; +}): Promise { + const recipientTokenAccount = getAssociatedTokenAccount(wallet, mint); + + const createAtaIdempotentIx = spl.createAssociatedTokenAccountIdempotentInstruction( + signer, + recipientTokenAccount, + wallet, mint, - } = getCompleteTransferNativeWithPayloadCpiAccounts( - tokenBridgeProgramId, - wormholeProgramId, - PublicKey.default, - vaa, - PublicKey.default, ); - return { - tokenBridgeConfig, - tokenBridgeClaim, - tokenBridgeForeignEndpoint, - tokenBridgeCustody, - tokenBridgeCustodySigner, - tokenBridgeMintAuthority: null, - tokenBridgeWrappedMeta: null, - mint, - }; + return createAtaIdempotentIx; } -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; +export function returnedDataFromTransaction( + typeLayout: L, + confirmedTransaction: + | VersionedTransactionResponse + | TransactionResponse + | SimulatedTransactionResponse, +): LayoutToType { + const prefix = 'Program return: '; + 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'); - const { - tokenBridgeConfig, - tokenBridgeClaim, - tokenBridgeForeignEndpoint, - tokenBridgeMintAuthority, - tokenBridgeWrappedMeta, - tokenBridgeWrappedMint, - } = getCompleteTransferWrappedWithPayloadCpiAccounts( - tokenBridgeProgramId, - wormholeProgramId, - PublicKey.default, - vaa, - PublicKey.default, - ); + // The line looks like 'Program return: ': + const [, data] = log.slice(prefix.length).split(' ', 2); - return { - tokenBridgeConfig, - tokenBridgeClaim, - tokenBridgeForeignEndpoint, - tokenBridgeCustody: null, - tokenBridgeCustodySigner: null, - tokenBridgeMintAuthority, - tokenBridgeWrappedMeta, - mint: tokenBridgeWrappedMint, - }; + return deserializeLayout(typeLayout, Buffer.from(data, 'base64'), { consumeAll: true }); } /** @@ -1035,9 +933,9 @@ async function networkFromConnection(connection: Connection): 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; +} + +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/build.rs b/solana/programs/token-bridge-relayer/build.rs index 68b6c82a..28590768 100644 --- a/solana/programs/token-bridge-relayer/build.rs +++ b/solana/programs/token-bridge-relayer/build.rs @@ -16,15 +16,11 @@ fn main() -> 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,17 +30,10 @@ 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, + testnet: String, } impl Network { @@ -59,17 +48,17 @@ 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()) } #[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/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 7033dfc9..65c3fe62 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}; @@ -38,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 @@ -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, @@ -334,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/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..e0b95236 100644 --- a/solana/programs/token-bridge-relayer/src/utils.rs +++ b/solana/programs/token-bridge-relayer/src/utils.rs @@ -85,26 +85,29 @@ 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(()) } -/// 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..843bf306 100644 --- a/solana/tests/token-bridge-relayer-tests.ts +++ b/solana/tests/token-bridge-relayer-tests.ts @@ -1,14 +1,23 @@ -import { chainToChainId, encoding } from '@wormhole-foundation/sdk-base'; -import { UniversalAddress } from '@wormhole-foundation/sdk-definitions'; +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 { assert, TestMint, TestsHelper, tokenBridgeEmitter } from './utils/helpers.js'; -import { TbrWrapper, TokenBridgeWrapper, WormholeCoreWrapper } from './utils/client-wrapper.js'; +import * as spl from '@solana/spl-token'; +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 { TestingWormholeCore } from './utils/testing-wormhole-core.js'; +import { TestingTokenBridge } from './utils/testing-token-bridge.js'; const DEBUG = false; @@ -21,12 +30,12 @@ const authorityKeypair = './target/deploy/token_bridge_relayer-keypair.json'; const $ = new TestsHelper(); -//TODO put the setup in its own object. +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( - (typeAccount) => TbrWrapper.from($.provider.generate(), typeAccount, oracleClient, DEBUG), + const clients = Array.from({ length: 6 }).map(() => + TbrWrapper.from($.keypair.generate(), oracleClient, DEBUG), ); const [ ownerClient, @@ -37,31 +46,40 @@ describe('Token Bridge Relayer Program', () => { unauthorizedClient, ] = clients; - const wormholeCoreOwner = $.provider.generate(); - const wormholeCoreClient = new WormholeCoreWrapper(wormholeCoreOwner); + 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(tokenBridgeOwner); + const tokenBridgeOwner = $.keypair.generate(); + const tokenBridgeClient = new TestingTokenBridge( + tokenBridgeOwner, + $.connection, + WormholeContracts.Network, + wormholeCoreClient.guardians, + WormholeContracts.addresses, + ); const feeRecipient = PublicKey.unique(); const evmTransactionGas = 321_000n; const evmTransactionSize = 654_000n; - const ethereumPeer1 = new UniversalAddress( - Buffer.from('e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1', 'hex'), - ); - const ethereumPeer2 = new UniversalAddress( - Buffer.from('e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2', 'hex'), - ); - const oasisPeer = new UniversalAddress( - Buffer.from('0A51533333333333333333333333333333333333333333333333333333333333', 'hex'), - ); + const ethereumTokenBridge = $.universalAddress.generate(); + const oasisTokenBridge = $.universalAddress.generate(); + + 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 @@ -84,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, { @@ -99,15 +115,15 @@ describe('Token Bridge Relayer Program', () => { }), await oracleAuthorityClient.updateSolPrice(oracleAuthorityProvider.publicKey, 113_000_000n), // SOL is at $113 ), + oracleAuthorityProvider, ); // Wormhole Core Setup // =================== 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), ]); }); @@ -118,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, ); @@ -129,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({ @@ -244,59 +261,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,30 +326,25 @@ 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); }); 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: await assert - .promise(adminClient1.updateCanonicalPeer(ETHEREUM, ethereumPeer1)) + .promise(adminClient1.updateCanonicalPeer(ETHEREUM, ethereumTbrPeer1)) .failsWith('Signature verification failed'); }); }); @@ -345,7 +361,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, @@ -374,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); }); @@ -389,30 +405,49 @@ 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 = $.universalAddress.generate('ethereum'); + 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, barMint] = await Promise.all([ + TbrWrapper.create(recipientForeignToken, $.connection, DEBUG), + await TestMint.create($.connection, ownerClient.signer, 10), + 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 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.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 @@ -429,14 +464,14 @@ describe('Token Bridge Relayer Program', () => { expect(vaa.sequence).equal(sequence); expect(vaa.emitterChain).equal('Solana'); - assert.key(new PublicKey(vaa.emitterAddress.address)).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: 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); }); @@ -446,16 +481,14 @@ 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 tokenAccount = await barMint.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.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 @@ -471,7 +504,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()); + 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: @@ -484,22 +517,177 @@ 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); + 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 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(spl.NATIVE_MINT.toBuffer()) }, + { chain: ETHEREUM, tokenBridge: ethereumTokenBridge, tbrPeer: ethereumTbrPeer1 }, + { + recipient: new UniversalAddress(recipient.publicKey.toBuffer()), + gasDropoff: 0, + unwrapIntent: false, + }, + ); + const vaa = await wormholeCoreClient.parseVaa(vaaAddress); + + await unauthorizedClient.completeTransfer(vaa); + + 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()); + }); + + 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, - { chain: ETHEREUM, address: ethereumPeer1 }, // The token originally comes from Solana's native mint - 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, //TODO increase the dropoff + 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()); + // 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, + 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 $.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 = $.universalAddress.generate(); + 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 }); + + 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); + }); - await unauthorizedClient.completeNativeTransfer(vaa, associatedTokenAccount); + after(async () => { + await clientForeignToken.close(); }); }); }); diff --git a/solana/tests/token_metadata.so b/solana/tests/token_metadata.so new file mode 100755 index 00000000..00467cb2 Binary files /dev/null and b/solana/tests/token_metadata.so differ diff --git a/solana/tests/utils/client-wrapper.ts b/solana/tests/utils/client-wrapper.ts deleted file mode 100644 index fd4e0e6e..00000000 --- a/solana/tests/utils/client-wrapper.ts +++ /dev/null @@ -1,467 +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, - TransferNativeParameters, - TransferWrappedParameters, - 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'; - -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 transferNativeTokens( - params: TransferNativeParameters, - signers?: Keypair[], - ): Promise { - return sendAndConfirmIxs( - this.provider, - await this.client.transferNativeTokens(this.publicKey, params), - { signers }, - ); - } - - async transferWrappedTokens(params: TransferWrappedParameters): 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), - ); - } - - 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; - private sequence = 0n; - - constructor(provider: AnchorProvider) { - this.provider = provider; - - this.client = new SolanaWormholeCore( - WormholeContracts.Network, - 'Solana', - provider.connection, - WormholeContracts.addresses, - ); - } - - 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 - ]; - - //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, - 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 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 }, - ): 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 payload = serializePayload('TokenBridge:TransferWithPayload', { - token: { amount: 123n, address: new UniversalAddress(mint.toBuffer()), chain: 'Solana' }, - to: { - address: new UniversalAddress($.pubkey.from(testProgramKeypair).toBuffer()), - chain: 'Solana', - }, - from: source.address, - 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; - - constructor(provider: TestProvider) { - this.provider = provider; - this.client = new SolanaTokenBridge( - WormholeContracts.Network, - 'Solana', - provider.connection, - WormholeContracts.addresses, - ); - } - - 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, - }) - .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 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, - wormholeProgram: WormholeContracts.coreBridge, - rent: SYSVAR_RENT_PUBKEY, - systemProgram: SystemProgram.programId, - }) - .instruction(); - await sendAndConfirmIxs(this.provider, ix); - } -} diff --git a/solana/tests/utils/helpers.ts b/solana/tests/utils/helpers.ts index 6f7aabf2..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; } @@ -322,6 +316,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 +326,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,23 +349,13 @@ 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, + ); } } } -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')], @@ -380,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 new file mode 100644 index 00000000..5700f5a1 --- /dev/null +++ b/solana/tests/utils/tbr-wrapper.ts @@ -0,0 +1,186 @@ +import anchor from '@coral-xyz/anchor'; +import { Connection, PublicKey, Signer, 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 { 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 signer: Signer; + readonly logs: { [key: string]: string[] }; + readonly logsSubscriptionId: number; + + constructor(signer: Signer, tbrClient: SolanaTokenBridgeRelayer) { + this.client = tbrClient; + this.signer = signer; + this.logs = {}; + + this.logsSubscriptionId = this.client.connection.onLogs( + 'all', + (l) => (this.logs[l.signature] = l.logs), + ); + } + + static from(signer: Signer, oracleClient: SolanaPriceOracle, debug: boolean) { + const client = new SolanaTokenBridgeRelayer( + oracleClient.connection, + 'Localnet', + $.pubkey.from(testProgramKeypair), + oracleClient, + debug, + ); + + return new TbrWrapper(signer, client); + } + + static async create(signer: Signer, connection: Connection, debug: boolean) { + const client = await SolanaTokenBridgeRelayer.create(connection, debug); + + return new TbrWrapper(signer, client); + } + + get publicKey(): PublicKey { + return this.signer.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.client.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 $.sendAndConfirm(await this.client.initialize(args), this.signer); + } + + async submitOwnerTransferRequest(newOwner: PublicKey): Promise { + return $.sendAndConfirm(await this.client.submitOwnerTransferRequest(newOwner), this.signer); + } + + async confirmOwnerTransferRequest(): Promise { + return await $.sendAndConfirm(await this.client.confirmOwnerTransferRequest(), this.signer); + } + + async cancelOwnerTransferRequest(): Promise { + return $.sendAndConfirm(await this.client.cancelOwnerTransferRequest(), this.signer); + } + + async addAdmin(newAdmin: PublicKey): Promise { + return $.sendAndConfirm(await this.client.addAdmin(newAdmin), this.signer); + } + + async removeAdmin(adminToRemove: PublicKey): Promise { + return $.sendAndConfirm( + await this.client.removeAdmin(this.publicKey, adminToRemove), + this.signer, + ); + } + + async registerPeer(chain: Chain, peerAddress: UniversalAddress): Promise { + return $.sendAndConfirm( + await this.client.registerPeer(this.publicKey, chain, peerAddress), + this.signer, + ); + } + + async updateCanonicalPeer( + chain: Chain, + peerAddress: UniversalAddress, + ): Promise { + return $.sendAndConfirm(await this.client.updateCanonicalPeer(chain, peerAddress), this.signer); + } + + async setPauseForOutboundTransfers(chain: Chain, paused: boolean): Promise { + return $.sendAndConfirm( + await this.client.setPauseForOutboundTransfers(this.publicKey, chain, paused), + this.signer, + ); + } + + async updateMaxGasDropoff(chain: Chain, maxGasDropoff: number): Promise { + return $.sendAndConfirm( + await this.client.updateMaxGasDropoff(this.publicKey, chain, maxGasDropoff), + this.signer, + ); + } + + async updateRelayerFee(chain: Chain, relayerFee: number): Promise { + return $.sendAndConfirm( + await this.client.updateRelayerFee(this.publicKey, chain, relayerFee), + this.signer, + ); + } + + async updateFeeRecipient(newFeeRecipient: PublicKey): Promise { + return $.sendAndConfirm( + await this.client.updateFeeRecipient(this.publicKey, newFeeRecipient), + this.signer, + ); + } + + async updateEvmTransactionConfig( + evmTransactionGas: bigint, + evmTransactionSize: bigint, + ): Promise { + return $.sendAndConfirm( + await this.client.updateEvmTransactionConfig( + this.publicKey, + evmTransactionGas, + evmTransactionSize, + ), + this.signer, + ); + } + + /** Only the token owner can call this method. */ + async transferTokens(params: TransferParameters): Promise { + return $.sendAndConfirm( + await this.client.transferTokens(this.publicKey, params), + this.signer, //...signers + ); + } + + async completeTransfer(vaa: VaaMessage): Promise { + 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 { + return this.client.relayingFee(chain, dropoffAmount); + } +} diff --git a/solana/tests/utils/testing-token-bridge.ts b/solana/tests/utils/testing-token-bridge.ts new file mode 100644 index 00000000..051fad58 --- /dev/null +++ b/solana/tests/utils/testing-token-bridge.ts @@ -0,0 +1,159 @@ +import { SolanaTokenBridge } from '@wormhole-foundation/sdk-solana-tokenbridge'; +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, 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'; + +/** A Token Bridge wrapper allowing to write tests using this program in a local environment. */ +export class TestingTokenBridge { + static sequence = 100n; + public readonly signer: Signer; + 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( + signer: Signer, + connection: Connection, + network: N, + guardians: mocks.MockGuardians, + contracts: Contracts, + ) { + this.signer = signer; + this.guardians = guardians; + 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')), + + 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.signer.publicKey, + config: this.pda.config(), + }) + .instruction(); + + return await sendAndConfirm(this.client.connection, ix, this.signer); + } + + async registerPeer(chain: Chain, address: UniversalAddress) { + const sequence = TestingTokenBridge.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.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)); + + const ix = await this.client.tokenBridge.methods + .registerChain() + .accounts({ + payer: this.signer.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 sendAndConfirm(this.client.connection, ix, this.signer); + } + + async attestToken( + emitter: UniversalAddress, + chain: Chain, + mint: UniversalAddress, + info: { decimals: number }, + ) { + 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, + 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.signer.publicKey, vaa); + await signAndSendWait(txsPostVaa, signer); + + const txsAttest = this.client.submitAttestation(rawVaa, this.signer.publicKey); + await signAndSendWait(txsAttest, signer); + } + + private findPda(...seeds: Array) { + return PublicKey.findProgramAddressSync(seeds, this.client.tokenBridge.programId)[0]; + } +} diff --git a/solana/tests/utils/testing-wormhole-core.ts b/solana/tests/utils/testing-wormhole-core.ts new file mode 100644 index 00000000..85134d77 --- /dev/null +++ b/solana/tests/utils/testing-wormhole-core.ts @@ -0,0 +1,156 @@ +import anchor from '@coral-xyz/anchor'; +import { Connection, Keypair, PublicKey, Signer } 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, sendAndConfirm } 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]); + +/** 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; + + /** + * + * @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( + signer: Signer, + connection: Connection, + network: N, + testedProgram: PublicKey, + contracts: Contracts, + ) { + this.signer = signer; + this.toProgram = testedProgram; + this.client = new SolanaWormholeCore(network, 'Solana', 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.signer.publicKey, + }) + .instruction(); + + 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.client.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/target/idl/token_bridge_relayer.json b/target/idl/token_bridge_relayer.json index 19f14f82..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": [ @@ -1990,6 +1996,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..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": [ @@ -1996,6 +2002,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..9386b4b4 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" @@ -2111,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" @@ -2423,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"