diff --git a/.changeset/orange-wasps-sip.md b/.changeset/orange-wasps-sip.md new file mode 100644 index 0000000000..0218f4ab42 --- /dev/null +++ b/.changeset/orange-wasps-sip.md @@ -0,0 +1,5 @@ +--- +"create-lz-oapp": minor +--- + +add Solana OApp example diff --git a/examples/oapp-solana/.env.example b/examples/oapp-solana/.env.example new file mode 100644 index 0000000000..52e4ee22b9 --- /dev/null +++ b/examples/oapp-solana/.env.example @@ -0,0 +1,20 @@ +# .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- +# / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ +# `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' +# +# Example environment configuration +# +# .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- +# / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ +# `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' + +# By default, the examples support both mnemonic-based and private key-based authentication +# +# You don't need to set both of these values, just pick the one that you prefer and set that one +MNEMONIC= +PRIVATE_KEY= +# You also don't need to set both of the following values. Also, by default, if no value is set, the default keypair path of ./config/solana/id.json will be used +SOLANA_PRIVATE_KEY= +SOLANA_KEYPAIR_PATH= +RPC_URL_SOLANA= +RPC_URL_SOLANA_TESTNET= \ No newline at end of file diff --git a/examples/oapp-solana/.eslintignore b/examples/oapp-solana/.eslintignore new file mode 100644 index 0000000000..c758d6a3d5 --- /dev/null +++ b/examples/oapp-solana/.eslintignore @@ -0,0 +1,11 @@ +lib/client/generated/ +artifacts +cache +dist +node_modules +out +*.log +*.sol +*.yaml +*.lock +package-lock.json \ No newline at end of file diff --git a/examples/oapp-solana/.eslintrc.js b/examples/oapp-solana/.eslintrc.js new file mode 100644 index 0000000000..a01ea1c3b9 --- /dev/null +++ b/examples/oapp-solana/.eslintrc.js @@ -0,0 +1,12 @@ +require('@rushstack/eslint-patch/modern-module-resolution'); + +module.exports = { + root: true, + extends: ['@layerzerolabs/eslint-config-next/recommended'], + rules: { + // @layerzerolabs/eslint-config-next defines rules for turborepo-based projects + // that are not relevant for this particular project + 'turbo/no-undeclared-env-vars': 'off', + 'import/no-unresolved': 'warn', // Updated to warn to avoid erratic popping up of this particular error even when the package has been installed correctly + }, +}; diff --git a/examples/oapp-solana/.gitignore b/examples/oapp-solana/.gitignore new file mode 100644 index 0000000000..ebd495ee39 --- /dev/null +++ b/examples/oapp-solana/.gitignore @@ -0,0 +1,30 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +# Solana +target +target/debug +target/sbf-solana-solana +buffer.json +.anchor + +# Hardhat files +cache +artifacts + +# LayerZero specific files +.layerzero + +# foundry test compilation files +out + +# pnpm +pnpm-error.log + +# Editor and OS files +.DS_Store +.idea diff --git a/examples/oapp-solana/.nvmrc b/examples/oapp-solana/.nvmrc new file mode 100644 index 0000000000..b714151ef9 --- /dev/null +++ b/examples/oapp-solana/.nvmrc @@ -0,0 +1 @@ +v18.18.0 \ No newline at end of file diff --git a/examples/oapp-solana/.prettierignore b/examples/oapp-solana/.prettierignore new file mode 100644 index 0000000000..5b8ce9a010 --- /dev/null +++ b/examples/oapp-solana/.prettierignore @@ -0,0 +1,11 @@ +lib/client/generated/ +artifacts/ +cache/ +dist/ +node_modules/ +out/ +*.log +*ignore +*.yaml +*.lock +package-lock.json \ No newline at end of file diff --git a/examples/oapp-solana/.prettierrc.js b/examples/oapp-solana/.prettierrc.js new file mode 100644 index 0000000000..6f55b4019a --- /dev/null +++ b/examples/oapp-solana/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('@layerzerolabs/prettier-config-next'), +}; diff --git a/examples/oapp-solana/Anchor.toml b/examples/oapp-solana/Anchor.toml new file mode 100644 index 0000000000..fb37f668ca --- /dev/null +++ b/examples/oapp-solana/Anchor.toml @@ -0,0 +1,18 @@ +[toolchain] + +[features] +seeds = false +skip-lint = false + +[programs.localnet] +my_oapp = "41NCdrEvXhQ4mZgyJkmqYxL6A1uEmnraGj31UJ6PsXd3" + +[registry] +url = "https://api.apr.dev" + +[provider] +cluster = "Localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "yarn run ts-mocha -b -p ./tsconfig.json -t 10000000 sdk/tests/**/*.test.ts" diff --git a/examples/oapp-solana/Cargo.lock b/examples/oapp-solana/Cargo.lock new file mode 100644 index 0000000000..0313d9b2a7 --- /dev/null +++ b/examples/oapp-solana/Cargo.lock @@ -0,0 +1,1792 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.14", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7d5a2cecb58716e47d67d5703a249964b14c7be1ec3cad3affc295b2d1c35d" +dependencies = [ + "cfg-if", + "getrandom 0.2.14", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "anchor-attribute-access-control" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f619f1d04f53621925ba8a2e633ba5a6081f2ae14758cbb67f38fd823e0a3e" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-account" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f2a3e1df4685f18d12a943a9f2a7456305401af21a07c9fe076ef9ecd6e400" +dependencies = [ + "anchor-syn", + "bs58 0.5.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-constant" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9423945cb55627f0b30903288e78baf6f62c6c8ab28fb344b6b25f1ffee3dca7" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-error" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ed12720033cc3c3bf3cfa293349c2275cd5ab99936e33dd4bf283aaad3e241" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-event" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef4dc0371eba2d8c8b54794b0b0eb786a234a559b77593d6f80825b6d2c77a2" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-program" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b18c4f191331e078d4a6a080954d1576241c29c56638783322a18d308ab27e4f" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-accounts" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de10d6e9620d3bcea56c56151cad83c5992f50d5960b3a9bebc4a50390ddc3c" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-serde" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e2e5be518ec6053d90a2a7f26843dbee607583c779e6c8395951b9739bdfbe" +dependencies = [ + "anchor-syn", + "borsh-derive-internal 0.10.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-space" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecc31d19fa54840e74b7a979d44bcea49d70459de846088a1d71e87ba53c419" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-lang" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35da4785497388af0553586d55ebdc08054a8b1724720ef2749d313494f2b8ad" +dependencies = [ + "anchor-attribute-access-control", + "anchor-attribute-account", + "anchor-attribute-constant", + "anchor-attribute-error", + "anchor-attribute-event", + "anchor-attribute-program", + "anchor-derive-accounts", + "anchor-derive-serde", + "anchor-derive-space", + "anchor-syn", + "arrayref", + "base64 0.13.1", + "bincode", + "borsh 0.10.3", + "bytemuck", + "getrandom 0.2.14", + "solana-program", + "thiserror", +] + +[[package]] +name = "anchor-syn" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9101b84702fed2ea57bd22992f75065da5648017135b844283a2f6d74f27825" +dependencies = [ + "anyhow", + "bs58 0.5.1", + "heck", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.8", + "syn 1.0.109", + "thiserror", +] + +[[package]] +name = "anyhow" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" + +[[package]] +name = "ark-bn254" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", + "itertools", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest 0.10.7", + "itertools", + "num-bigint", + "num-traits", + "paste", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +dependencies = [ + "serde", +] + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "blake3" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive 0.9.3", + "hashbrown 0.11.2", +] + +[[package]] +name = "borsh" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" +dependencies = [ + "borsh-derive 0.10.3", + "hashbrown 0.13.2", +] + +[[package]] +name = "borsh-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +dependencies = [ + "borsh-derive-internal 0.9.3", + "borsh-schema-derive-internal 0.9.3", + "proc-macro-crate", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" +dependencies = [ + "borsh-derive-internal 0.10.3", + "borsh-schema-derive-internal 0.10.3", + "proc-macro-crate", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + +[[package]] +name = "cpi-helper" +version = "0.1.0" +source = "git+https://github.com/LayerZero-Labs/LayerZero-v2.git?rev=34321ac15e47e0dafd25d66659e2f3d1b9b6db8f#34321ac15e47e0dafd25d66659e2f3d1b9b6db8f" +dependencies = [ + "bs58 0.5.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "endpoint" +version = "0.1.0" +source = "git+https://github.com/LayerZero-Labs/LayerZero-v2.git?rev=34321ac15e47e0dafd25d66659e2f3d1b9b6db8f#34321ac15e47e0dafd25d66659e2f3d1b9b6db8f" +dependencies = [ + "anchor-lang", + "cpi-helper", + "messagelib-interface", + "solana-helper", + "solana-program", + "utils", +] + +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "serde", + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.5", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac", +] + +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "rayon", + "serde", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libsecp256k1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" +dependencies = [ + "arrayref", + "base64 0.12.3", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "light-poseidon" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9a85a9752c549ceb7578064b4ed891179d20acd85f27318573b64d2d7ee7ee" +dependencies = [ + "ark-bn254", + "ark-ff", + "num-bigint", + "thiserror", +] + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "messagelib-interface" +version = "0.1.0" +source = "git+https://github.com/LayerZero-Labs/LayerZero-v2.git?rev=34321ac15e47e0dafd25d66659e2f3d1b9b6db8f#34321ac15e47e0dafd25d66659e2f3d1b9b6db8f" +dependencies = [ + "anchor-lang", +] + +[[package]] +name = "my_oapp" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "oapp", + "solana-helper", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oapp" +version = "0.1.0" +source = "git+https://github.com/LayerZero-Labs/LayerZero-v2.git?rev=34321ac15e47e0dafd25d66659e2f3d1b9b6db8f#34321ac15e47e0dafd25d66659e2f3d1b9b6db8f" +dependencies = [ + "anchor-lang", + "endpoint", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "pbkdf2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" +dependencies = [ + "crypto-mac", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.14", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + +[[package]] +name = "serde_json" +version = "1.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "solana-frozen-abi" +version = "1.17.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96734b05823c8b515f8e3cc02641a27aee2c9760b1a43c74cb20f2a1ab0ab76c" +dependencies = [ + "ahash 0.8.5", + "blake3", + "block-buffer 0.10.4", + "bs58 0.4.0", + "bv", + "byteorder", + "cc", + "either", + "generic-array", + "im", + "lazy_static", + "log", + "memmap2", + "rustc_version", + "serde", + "serde_bytes", + "serde_derive", + "serde_json", + "sha2 0.10.8", + "solana-frozen-abi-macro", + "subtle", + "thiserror", +] + +[[package]] +name = "solana-frozen-abi-macro" +version = "1.17.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a0f1291a464fd046135d019d57a81be165ee3d23aa7df880b47dac683a0582a" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.58", +] + +[[package]] +name = "solana-helper" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c6deff8c48efb84b5828db064ad9873ef3445f129f888b4b6a664bd5220e35" +dependencies = [ + "bs58 0.5.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "solana-program" +version = "1.17.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6412447793f8a3ef7526655906728325093b472e481791ac5c584e8d272166dc" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-serialize", + "base64 0.21.7", + "bincode", + "bitflags 2.5.0", + "blake3", + "borsh 0.10.3", + "borsh 0.9.3", + "bs58 0.4.0", + "bv", + "bytemuck", + "cc", + "console_error_panic_hook", + "console_log", + "curve25519-dalek", + "getrandom 0.2.14", + "itertools", + "js-sys", + "lazy_static", + "libc", + "libsecp256k1", + "light-poseidon", + "log", + "memoffset", + "num-bigint", + "num-derive", + "num-traits", + "parking_lot", + "rand 0.8.5", + "rustc_version", + "rustversion", + "serde", + "serde_bytes", + "serde_derive", + "serde_json", + "sha2 0.10.8", + "sha3", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-sdk-macro", + "thiserror", + "tiny-bip39", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "solana-sdk-macro" +version = "1.17.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cc46bbda0a5472d8d0a4c846b22941436ac45c31456d3e885a387a5f264f7" +dependencies = [ + "bs58 0.4.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.58", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + +[[package]] +name = "tiny-bip39" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" +dependencies = [ + "anyhow", + "hmac", + "once_cell", + "pbkdf2", + "rand 0.7.3", + "rustc-hash", + "sha2 0.9.9", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "utils" +version = "0.1.0" +source = "git+https://github.com/LayerZero-Labs/LayerZero-v2.git?rev=34321ac15e47e0dafd25d66659e2f3d1b9b6db8f#34321ac15e47e0dafd25d66659e2f3d1b9b6db8f" +dependencies = [ + "anchor-lang", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.58", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] diff --git a/examples/oapp-solana/Cargo.toml b/examples/oapp-solana/Cargo.toml new file mode 100644 index 0000000000..e09a70a391 --- /dev/null +++ b/examples/oapp-solana/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] +members = ["programs/*"] +resolver = "2" + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 + +[profile.release.build-override] +opt-level = 0 +incremental = false +codegen-units = 1 \ No newline at end of file diff --git a/examples/oapp-solana/README.md b/examples/oapp-solana/README.md new file mode 100644 index 0000000000..f3ebc66000 --- /dev/null +++ b/examples/oapp-solana/README.md @@ -0,0 +1,301 @@ +

+ + LayerZero + +

+ +

+ Homepage | Docs | Developers +

+ +

OApp Example

+ +

+ Core Concepts | Message Execution Options | Endpoint Addresses +

+ +

Template project for getting started with LayerZero's OApp development for Solana <> EVM.

+ +
+This is a simple cross-chain string-passing OApp example involving EVM and Solana. + +## How a Solana OApp works + +For a more thorough walkthrough of how a Solana OApp works, refer to [Solana OApp Reference](https://docs.layerzero.network/v2/developers/solana/oapp/overview) in our docs. The reference details the accounts that are required, which will help you identify modifications needed to support your specific use case. + +## Requirements + +- Rust `v1.75.0` +- Anchor `v0.29` +- Solana CLI `v1.17.31` +- Docker +- Node.js + +## Setup + +We recommend using `pnpm` as a package manager (but you can of course use a package manager of your choice). + +[Docker](https://docs.docker.com/get-started/get-docker/) is required to build using anchor. We highly recommend that you use the most up-to-date Docker version to avoid any issues with anchor +builds. + +:warning: You need anchor version `0.29` and solana version `1.17.31` specifically to compile the build artifacts. Using higher Anchor and Solana versions can introduce unexpected issues during compilation. See the following issues in Anchor's repo: [1](https://github.com/coral-xyz/anchor/issues/3089), [2](https://github.com/coral-xyz/anchor/issues/2835). After compiling the correct build artifacts, you can change the Solana version to higher versions. + +### Install Rust + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +``` + +### Install Solana + +```bash +sh -c "$(curl -sSfL https://release.anza.xyz/v1.17.31/install)" +``` + +If this is your first time installing the Solana CLI, run the following to [generate a keypair](https://solana.com/docs/intro/installation#create-wallet) at the default keypair path (`~/.config/solana/id.json`): + +```bash +solana-keygen new +``` + +### Install Anchor + +Install and use the correct version + +```bash +cargo install --git https://github.com/coral-xyz/anchor --tag v0.29.0 anchor-cli --locked +``` + +### Get the code + +```bash +LZ_ENABLE_SOLANA_OAPP_EXAMPLE=1 npx create-lz-oapp@latest +``` + +The CLI will also automatically run pnpm install for you. + +Make sure you select the **OApp (Solana)** example from the dropdown: + +```bash +✔ Where do you want to start your project? … ./example +? Which example would you like to use as a starting point? › - Use arrow-keys. Return to submit. + OApp + OFT + OFTAdapter + ONFT721 + OFT (Solana) +❯ OApp (Solana) +``` + +## Developing Contracts + +#### Compiling your contracts + +To compile the Solidity OApp contract and build the Solana OApp program, run: + +```bash +pnpm compile +``` + +#### Get Devnet SOL + +```bash +solana airdrop 5 -u devnet +``` + +We recommend that you request 5 devnet SOL, which should be sufficient for this walkthrough. For the example here, we will be using Solana Devnet. If you hit rate limits, you can also use the [official Solana faucet](https://faucet.solana.com/). + +#### Prepare `.env` + +Copy the example `.env` file: + +```bash +cp .env.example .env +``` + +and fill up the values. + +##### EVM Private Key + +Choose your preferred means of setting up your EVM deployer wallet/account: + +``` +MNEMONIC="test test test test test test test test test test test junk" +or... +PRIVATE_KEY="0xabc...def" +``` + +##### Solana Keypair + +By default, the scripts will use the keypair at the default location `~/.config/solana/id.json`. If you want to use this keypair, there is no need to set any environment variable. There will, however, be a prompt when running certain commands to confirm that you want to use the default keypair. + +If you wish to use a different keypair, then you can set either of the following in the `.env`: + +1. `SOLANA_PRIVATE_KEY` - this can be either in base58 string format (i.e. when imported from a wallet) or the Uint8 Array in string format (all in one line, e.g. `[1,1,...1]`). + +2. `SOLANA_KEYPAIR_PATH` - the location to the keypair file that you want to use. + +##### Solana RPC + +Also set the `RPC_URL_SOLANA_TESTNET` value. Note that while the naming used here is `TESTNET`, it refers to the [Solana Devnet](https://docs.layerzero.network/v2/developers/evm/technical-reference/deployed-contracts#solana-testnet). We use `TESTNET` to keep it consistent with the existing EVM testnets. + +## Deploying Contracts + +### Prepare the OApp Program ID + +Create the MyOApp `programId` keypair file by running: + +```bash +solana-keygen new -o target/deploy/my_oapp-keypair.json +anchor keys sync +``` + +Run + +``` +anchor keys list +``` + +to view the generated programId (public keys). The output should look something like this: + +``` +my_oapp: +``` + +Copy the OApp's program ID, which you will use in the build step. + +### Building and Deploying the Solana MyOApp Program + +Ensure you have Docker running before running the build command. + +#### Build the Solana MyOApp program + +```bash +anchor build -v -e MYOAPP_ID= +``` + +Where `` is replaced with your OApp Program ID copied from the previous step. + +#### Deploy the Solana OApp + +While for building, we must use Solana `v1.17.31`, for deploying, we will be using `v1.18.26` as it provides an improved program deployment experience (i.e. ability to attach priority fees and also exact-sized on-chain program length which prevents needing to provide 2x the rent as in `v1.17.31`). + +##### Temporarily switch to Solana `v1.18.26` + +First, we switch to Solana `v1.18.26` (remember to switch back to `v1.17.31` later) + +```bash +sh -c "$(curl -sSfL https://release.anza.xyz/v1.18.26/install)" +``` + +##### Run the deploy command + +```bash +solana program deploy --program-id target/deploy/my_oapp-keypair.json target/verifiable/my_oapp.so -u devnet --with-compute-unit-price +``` + +:information_source: `--with-compute-unit-price` takes in the microlamport value applied per compute unit. This is how we can attach a [priority fee](https://solana.com/vi/developers/guides/advanced/how-to-use-priority-fees) to our deployment. + +:information_source: the `-u` flag specifies the RPC URL that should be used. The options are `mainnet-beta, devnet, testnet, localhost`, which also have their respective shorthands: `-um, -ud, -ut, -ul` + +:warning: If the deployment is slow, it could be that the network is congested and you might need to increase the priority fee. + +##### Switch back to Solana `1.17.31` + +:warning: After deploying, make sure to switch back to v1.17.31 after deploying. If you need to rebuild artifacts, you must use Solana CLI version `1.17.31` and Anchor version `0.29.0` + +```bash +sh -c "$(curl -sSfL https://release.anza.xyz/v1.17.31/install)" +``` + +### Initialize the Solana OApp account + +Solana programs require accounts to be initialized before state can be written. + +Run the following to init the OApp store account: + +```bash +npx hardhat lz:oapp:solana:create --eid 40168 --program-id +``` + +:information_source: The address of this account (and **not** the OApp program ID) is what will be used as the OApp address in the context of cross-chain messaging. + +:information_source: The example `init_store` method being called by this task can be called by anyone, and can only be called once. Modify it accordingly if for your use case it should be access controlled or callable multiple times based on some param value. + +##### Deploy the EVM OApp + +To deploy your Solidity contracts to your desired EVM blockchain(s), run the following command in your project's folder: + +```bash +npx hardhat lz:deploy +``` + +You will be presented with a list of networks that have been defined via your `hardhat.config.ts`. Select the ones you want to deploy to. + +More information about available CLI arguments can be found using the `--help` flag: + +```bash +npx hardhat lz:deploy --help +``` + +### Wiring + +Wiring will apply the settings configured in the [LZ config](https://docs.layerzero.network/v2/concepts/glossary#lz-config) file. By default, this file is named `layerzero.config.ts`. + +#### Run the Solana `init-config` task + +This step is required only for the Solana OApp and is again required due to the need to init accounts explicitly. + +The task initializes the OApp's SendConfig and ReceiveConfig Accounts. + +You need to do this only when initializing the OApp for the first time. However, if a new pathway involving Solana is added, you need to run this again as the SendConfig and ReceiveConfig accounts are required per peer. + +```bash +npx hardhat lz:oapp:solana:init-config --oapp-config layerzero.config.ts +``` + +#### Run the [wire task](https://docs.layerzero.network/v2/concepts/glossary#wire--wiring) + +Run the following to [wire](https://docs.layerzero.network/v2/concepts/glossary#wire--wiring) the pathways specified in your `layerzero.config.ts` + +```bash +npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts +``` + +With a squads multisig, you can simply append the `--multisig-key` flag to the end of the above command. + +### Send Messages + +With your OApps wired, you can now send a message. + +Send from Solana Devnet (40168) to Optimism Sepolia (40232) : + +```bash +npx hardhat lz:oapp:send --from-eid 40168 --dst-eid 40232 --message "Hello from Solana Devnet" +``` + +Send from Optimism Sepolia (40232) to Solana Devnet (40168) : + +```bash +npx hardhat --network optimism-testnet lz:oapp:send --from-eid 40232 --dst-eid 40168 --message "Hello from Optimism Sepolia" +``` + +:information_source: For the list of supported chains and their endpoint ID's refer to the [Deployed Endpoints](https://docs.layerzero.network/v2/deployments/deployed-contracts) page. + +
+ +Congratulations, you have now successfully set up an EVM <> Solana OApp. + +### Running tests + +The `test` command will execute the hardhat and forge tests: + +```bash +pnpm test +``` + +

+ +

+ Join our community! | Follow us on X (formerly Twitter) +

diff --git a/examples/oapp-solana/contracts/MyOApp.sol b/examples/oapp-solana/contracts/MyOApp.sol new file mode 100644 index 0000000000..c9cb3f75c2 --- /dev/null +++ b/examples/oapp-solana/contracts/MyOApp.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.22; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { OApp, MessagingFee, Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; +import { MessagingReceipt } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppSender.sol"; +import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; +import { StringMsgCodec } from "./libs/StringMsgCodec.sol"; + +contract MyOApp is OApp, OAppOptionsType3 { + using StringMsgCodec for bytes; + + constructor(address _endpoint, address _delegate) OApp(_endpoint, _delegate) Ownable(_delegate) {} + + string public data = "Nothing received yet."; + + /** + * @notice Sends a message from the source chain to a destination chain. + * @param _dstEid The endpoint ID of the destination chain. + * @param _string The message string to be sent. + * @param _options Additional options for message execution. + * @dev Encodes the message as bytes and sends it using the `_lzSend` internal function. + * @return receipt A `MessagingReceipt` struct containing details of the message sent. + */ + function send( + uint32 _dstEid, + string calldata _string, + bytes calldata _options + ) external payable returns (MessagingReceipt memory receipt) { + bytes memory _message = abi.encodePacked(abi.encode(uint256(bytes(_string).length)), bytes(_string)); + bytes memory options = combineOptions(_dstEid, StringMsgCodec.VANILLA_TYPE, _options); + receipt = _lzSend(_dstEid, _message, options, MessagingFee(msg.value, 0), payable(msg.sender)); + } + + /** + * @notice Quotes the gas needed to pay for the full omnichain transaction in native gas or ZRO token. + * @param _dstEid Destination chain's endpoint ID. + * @param _message The message. + * @param _options Message execution options (e.g., for sending gas to destination). + * @param _payInLzToken Whether to return fee in ZRO token. + * @return fee A `MessagingFee` struct containing the calculated gas fee in either the native token or ZRO token. + */ + function quote( + uint32 _dstEid, + string calldata _message, + bytes calldata _options, + bool _payInLzToken + ) public view returns (MessagingFee memory fee) { + bytes memory payload = abi.encodePacked(abi.encode(uint256(bytes(_message).length)), bytes(_message)); + bytes memory options = combineOptions(_dstEid, StringMsgCodec.VANILLA_TYPE, _options); + fee = _quote(_dstEid, payload, options, _payInLzToken); + } + + /** + * @dev Internal function override to handle incoming messages from another chain. + * @dev _origin A struct containing information about the message sender. + * @dev _guid A unique global packet identifier for the message. + * @param payload The encoded message payload being received. + * + * @dev The following params are unused in the current implementation of the OApp. + * @dev _executor The address of the Executor responsible for processing the message. + * @dev _extraData Arbitrary data appended by the Executor to the message. + * + * Decodes the received payload and processes it as per the business logic defined in the function. + */ + function _lzReceive( + Origin calldata /*_origin*/, + bytes32 /*_guid*/, + bytes calldata payload, + address /*_executor*/, + bytes calldata /*_extraData*/ + ) internal override { + string memory stringValue = StringMsgCodec.decode(payload); + data = stringValue; + } +} diff --git a/examples/oapp-solana/contracts/libs/StringMsgCodec.sol b/examples/oapp-solana/contracts/libs/StringMsgCodec.sol new file mode 100644 index 0000000000..583c41a0e4 --- /dev/null +++ b/examples/oapp-solana/contracts/libs/StringMsgCodec.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.22; + +error MsgTooShort(); +error InvalidStringValueLength(); + +library StringMsgCodec { + uint8 public constant VANILLA_TYPE = 1; + + /// @notice Reconstructs `stringValue` from `_msg`. + function decode(bytes calldata _msg) internal pure returns (string memory stringValue) { + if (_msg.length < 32) revert MsgTooShort(); + + // 1) Grab the first 32 bytes and ABI-decode as uint256, then cast down to u32 + uint256 lenRaw = abi.decode(_msg[:32], (uint256)); + uint32 strLen = uint32(lenRaw); + uint256 N = uint256(strLen); + + // 2) Bounds check + if (_msg.length < 32 + N) revert InvalidStringValueLength(); + + // 3) Extract the UTF-8 string + stringValue = string(_msg[32:32 + N]); + + // 4) Anything after that is ignored + } +} diff --git a/examples/oapp-solana/deploy/MyOApp.ts b/examples/oapp-solana/deploy/MyOApp.ts new file mode 100644 index 0000000000..9c6fae3108 --- /dev/null +++ b/examples/oapp-solana/deploy/MyOApp.ts @@ -0,0 +1,52 @@ +import assert from 'assert' + +import { type DeployFunction } from 'hardhat-deploy/types' + +// TODO declare your contract name here +const contractName = 'MyOApp' + +const deploy: DeployFunction = async (hre) => { + const { getNamedAccounts, deployments } = hre + + const { deploy } = deployments + const { deployer } = await getNamedAccounts() + + assert(deployer, 'Missing named deployer account') + + console.log(`Network: ${hre.network.name}`) + console.log(`Deployer: ${deployer}`) + + // This is an external deployment pulled in from @layerzerolabs/lz-evm-sdk-v2 + // + // @layerzerolabs/toolbox-hardhat takes care of plugging in the external deployments + // from @layerzerolabs packages based on the configuration in your hardhat config + // + // For this to work correctly, your network config must define an eid property + // set to `EndpointId` as defined in @layerzerolabs/lz-definitions + // + // For example: + // + // networks: { + // fuji: { + // ... + // eid: EndpointId.AVALANCHE_V2_TESTNET + // } + // } + const endpointV2Deployment = await hre.deployments.get('EndpointV2') + + const { address } = await deploy(contractName, { + from: deployer, + args: [ + endpointV2Deployment.address, // LayerZero's EndpointV2 address + deployer, // owner + ], + log: true, + skipIfAlreadyDeployed: false, + }) + + console.log(`Deployed contract: ${contractName}, network: ${hre.network.name}, address: ${address}`) +} + +deploy.tags = [contractName] + +export default deploy diff --git a/examples/oapp-solana/docs/compose-implementation.md b/examples/oapp-solana/docs/compose-implementation.md new file mode 100644 index 0000000000..b80e5cbf2a --- /dev/null +++ b/examples/oapp-solana/docs/compose-implementation.md @@ -0,0 +1,404 @@ +# Compose Implementation Notes + +**THIS DOCUMENT IS CURRENTLY A WORK IN PROGRESS AND IS INCOMPLETE.** + +This document guides you on how to support `lz_compose`. + +The document is structured into sections based on modifications you need to make. + +## Solana program + +### Create `programs/my_oapp/src/instructions/lz_compose.rs` + +```rust +use crate::*; +use anchor_lang::prelude::*; +use oapp::{ + endpoint::{instructions::ClearComposeParams, ID as ENDPOINT_ID}, + LzComposeParams, +}; + +#[derive(Accounts)] +pub struct LzCompose<'info> { + #[account(mut, seeds = [STORE_SEED], bump = store.bump)] + pub store: Account<'info, Store>, +} + +impl LzCompose<'_> { + pub fn apply(ctx: &mut Context, params: &LzComposeParams) -> Result<()> { + ctx.accounts.store.composed_count += 1; + + let seeds: &[&[u8]] = + &[STORE_SEED, &[ctx.accounts.store.bump]]; + let params = ClearComposeParams { + from: params.from, + guid: params.guid, + index: params.index, + message: params.message.clone(), + }; + oapp::endpoint_cpi::clear_compose( + ENDPOINT_ID, + ctx.accounts.store.key(), + &ctx.remaining_accounts, + seeds, + params, + ) + } +} +``` + +### Create a `programs/my_oapp/src/instructions/lz_compose_types.rs` + +```rust +/dev/null +``` + +### Modify `programs/my_oapp/src/instructions/init_store.rs` + +Add the LZ Compose Types account: + +```rust + pub lz_receive_types_accounts: Account<'info, LzReceiveTypesAccounts>, ++ #[account( ++ init, ++ payer = payer, ++ space = LzComposeTypesAccounts::SIZE, ++ seeds = [LZ_COMPOSE_TYPES_SEED, &store.key().to_bytes()], ++ bump ++ )] ++ pub lz_compose_types_accounts: Account<'info, LzComposeTypesAccounts>, + pub system_program: Program<'info, System>, +// ... +impl InitStore<'_> { + pub fn apply(ctx: &mut Context, params: &InitStoreParams) -> Result<()> { + ctx.accounts.store.admin = params.admin; + ctx.accounts.store.bump = ctx.bumps.store; + ctx.accounts.store.endpoint_program = params.endpoint; + ctx.accounts.store.string = "Nothing received yet.".to_string(); + + ctx.accounts.lz_receive_types_accounts.store = ctx.accounts.store.key(); ++ ctx.accounts.lz_compose_types_accounts.store = ctx.accounts.store.key(); +``` + +### `programs/my_oapp/src/instructions/lz_receive.rs` + +Handle compose_msg in `lz_receive`: + +```rust + let string_value = msg_codec::decode(¶ms.message); + let store = &mut ctx.accounts.store; + store.string = string_value; + ++ // TODO: handle compose_msg + + Ok(()) +``` + + + +### Modify `programs/my_oapp/src/instructions/mod.rs` + +```rust + pub mod send; + pub mod init_store; ++ pub mod lz_compose; ++ pub mod lz_compose_types; + pub mod lz_receive; + pub mod lz_receive_types; + pub mod quote_send; + pub mod set_peer_config; + + + pub use send::*; + pub use init_store::*; ++ pub use lz_compose::*; ++ pub use lz_compose_types::*; + pub use lz_receive::*; + pub use lz_receive_types::*; + pub use quote_send::*; + pub use set_peer_config::*; +``` + +### Modify `programs/my_oapp/src/instructions/quote_send.rs` to take in `compose_msg` + +```rust +// ... +impl<'info> QuoteSend<'info> { + pub fn apply(ctx: &Context, params: &QuoteSendParams) -> Result { +- let message = msg_codec::encode(¶ms.message); ++ let message = msg_codec::encode(¶ms.message, params.compose_msg.as_deref()); +// ... +- .combine_options(¶ms.options)?, ++ .combine_options(¶ms.compose_msg, ¶ms.options)?, +// ... +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct QuoteSendParams { + pub dst_eid: u32, + pub receiver: [u8; 32], + pub message: String, + pub options: Vec, ++ pub compose_msg: Option>, + pub pay_in_lz_token: bool, +} +``` + +### Modify `programs/my_oapp/src/instructions/send.rs` to take in `compose_msg` + +```rust +impl<'info> Send<'info> { + pub fn apply(ctx: &mut Context, params: &SendMessageParams) -> Result<()> { +- let message = msg_codec::encode(¶ms.message); ++ let message = msg_codec::encode(¶ms.message, params.compose_msg.as_deref()); +// ... +- .combine_options(¶ms.options)?, ++ .combine_options(¶ms.compose_msg, ¶ms.options)?, +// ... +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct SendMessageParams { + pub dst_eid: u32, + pub message: String, + pub options: Vec, ++ pub compose_msg: Option>, + pub native_fee: u64, + pub lz_token_fee: u64, +} +// ... +``` + +### Modify `programs/my_oapp/lib.rs` + +```rust +- use oapp::{endpoint::MessagingFee, endpoint_cpi::LzAccount, LzReceiveParams}; ++ use oapp::{endpoint::MessagingFee, endpoint_cpi::LzAccount, LzComposeParams, LzReceiveParams}; +// ... + const LZ_RECEIVE_TYPES_SEED: &[u8] = b"LzReceiveTypes"; ++ const LZ_COMPOSE_TYPES_SEED: &[u8] = b"LzComposeTypes"; +// ... + + #[program] + pub mod my_oapp { + use super::*; +// ... ++ pub fn lz_compose(mut ctx: Context, params: LzComposeParams) -> Result<()> { ++ LzCompose::apply(&mut ctx, ¶ms) ++ } ++ ++ pub fn lz_compose_types( ++ ctx: Context, ++ params: LzComposeParams, ++ ) -> Result> { ++ LzComposeTypes::apply(&ctx, ¶ms) ++ } +// ... +``` + +### Update the `programs/my_oapp/src/msg_codec.rs` + +```rust + pub const VANILLA_TYPE: u8 = 1; ++ pub const COMPOSED_TYPE: u8 = 2; +// ... + fn decode_string_len(buf: &[u8]) -> usize { + let mut string_len_bytes = [0u8;32]; + string_len_bytes.copy_from_slice(&buf[LENGTH_OFFSET..LENGTH_OFFSET+32]); + u32::from_be_bytes(string_len_bytes[28..32].try_into().unwrap()) as usize + } + ++ pub fn msg_type(message: &[u8]) -> u8 { ++ let string_len = decode_string_len(message); ++ if message.len() > STRING_OFFSET + string_len { ++ COMPOSED_TYPE ++ } else { ++ VANILLA_TYPE ++ } ++ } + +- pub fn encode(string: &str) -> Vec { ++ pub fn encode(string: &str, compose_msg: Option<&[u8]>) -> Vec { +// ...\ + let mut msg = Vec::with_capacity( + 32 + // length word + string_bytes.len() + // string ++ compose_msg.map(|m| m.len()) // optional tail ++ .unwrap_or(0) + ); +// ... + // string + msg.extend_from_slice(string_bytes); + ++ // optional tail ++ if let Some(tail) = compose_msg { ++ msg.extend_from_slice(tail); ++ } + + msg + } +// ... +pub fn decode(message: &[u8]) -> String { + let string_len = decode_string_len(message); + String::from_utf8_lossy(&message[STRING_OFFSET..STRING_OFFSET+string_len]).to_string() +} + ++ pub fn compose_msg(message: &[u8]) -> Option> { ++ let string_len = decode_string_len(message); ++ if message.len() > STRING_OFFSET + string_len { ++ Some(message[STRING_OFFSET+string_len..].to_vec()) ++ } else { ++ None ++ } +``` + +### Update `programs/my_oapp/src/state/store.rs` + +```rust +// ... +#[account] +pub struct Store { + pub admin: Pubkey, ++ pub composed_count: u64, + pub bump: u8, + pub endpoint_program: Pubkey, + pub string: String, +} +// ... +/// LzComposeTypesAccounts includes accounts that are used in the LzComposeTypes +/// instruction. ++ #[account] ++ pub struct LzComposeTypesAccounts { ++ pub store: Pubkey, ++ } + ++ impl LzComposeTypesAccounts { ++ pub const SIZE: usize = 8 + std::mem::size_of::(); ++ } +// ... +``` + +## Solana Program Client + +### Modify `lib/client/myoapp.ts` + +```typescript +initStore(payer: Signer, admin: PublicKey): WrappedInstruction { + const [oapp] = this.pda.oapp() + const remainingAccounts = this.endpointSDK.getRegisterOappIxAccountMetaForCPI(payer.publicKey, oapp) + return instructions + .initStore( + { payer: payer, programs: this.programRepo }, + { + payer, + store: oapp, + lzReceiveTypesAccounts: this.pda.lzReceiveTypesAccounts()[0], ++ lzComposeTypesAccounts: this.pda.lzComposeTypesAccounts()[0], + // args + admin: admin, + endpoint: this.endpointSDK.programId, + } + ) + .addRemainingAccounts(remainingAccounts).items[0] + } +// ... +``` + +### Solana Tasks Scripts + +`tasks/common/send.ts`: + +```typescript ++ const composeMsg = '0x' +- const [nativeFee] = await myOApp.quote(dstEid, message, options, false) ++ const [nativeFee] = await myOApp.quote(dstEid, message, composeMsg, options, false) +// ... +- const txResponse = await myOApp.send(dstEid, message, options, { ++ const txResponse = await myOApp.send(dstEid, message, composeMsg, options, { +``` + +## Solidity contract + +### `MyOApp.sol` + +Modify `send` and `quote` to accept a `_composeMsg` argument +and appended it to the payload: + +```solidity + function send( + uint32 _dstEid, + string calldata _string, ++ bytes calldata _composeMsg, + bytes calldata _options + ) external payable returns (MessagingReceipt memory receipt) { + bytes memory _message = abi.encodePacked( + abi.encode(uint256(bytes(_string).length)), + bytes(_string), ++ _composeMsg + ); ++ uint8 msgType = _composeMsg.length > 0 ? StringMsgCodec.COMPOSED_TYPE : StringMsgCodec.VANILLA_TYPE; +- bytes memory options = combineOptions(_dstEid, StringMsgCodec.VANILLA_TYPE, _options); ++ bytes memory options = combineOptions(_dstEid, msgType, _options); +// ... + function quote( + uint32 _dstEid, + string calldata _string, ++ bytes calldata _composeMsg, + bytes calldata _options, + bool _payInLzToken + ) public view returns (MessagingFee memory fee) { + bytes memory _message = abi.encodePacked( + abi.encode(uint256(bytes(_string).length)), + bytes(_string), ++ _composeMsg + ); ++ uint8 msgType = _composeMsg.length > 0 ? StringMsgCodec.COMPOSED_TYPE : StringMsgCodec.VANILLA_TYPE; +- bytes memory options = combineOptions(_dstEid, StringMsgCodec.VANILLA_TYPE, _options); ++ bytes memory options = combineOptions(_dstEid, msgType, _options); +// ... + function _lzReceive( + Origin calldata /*_origin*/, + bytes32 /*_guid*/, + bytes calldata payload, + address /*_executor*/, + bytes calldata /*_extraData*/ + ) internal override { +_ (string memory stringValue) = StringMsgCodec.decode(payload); ++ (string memory stringValue, bytes memory composeMsg) = StringMsgCodec.decode(payload); + data = stringValue; ++ // if necessary, encode the composeMsg further ++ endpoint.sendCompose(toAddress, _guid, 0 /* the index of the composed message*/, composeMsg); + } +``` + +### `StringMsgCodec.sol` + +```solidity + library StringMsgCodec { + uint8 public constant VANILLA_TYPE = 1; ++ uint8 public constant COMPOSED_TYPE = 2; +// ... +- /// @notice Reconstructs `stringValue` from `_msg`. +- function decode(bytes calldata _msg) internal pure returns (string memory stringValue) { ++ /// @notice Reconstructs `(stringValue, composeMsg)` from `_msg`. ++ function decode(bytes calldata _msg) internal pure returns (string memory stringValue, bytes memory composeMsg) { +// ... + // 3) Extract the UTF-8 string + stringValue = string(_msg[32:32 + N]); +- // 4) Anything after that is ignored ++ // 4) Anything after that is `composeMsg` ++ if (_msg.length > 32 + N) { ++ composeMsg = _msg[32 + N:]; ++ } else { ++ composeMsg = ""; ++ } ++ } ++ ++ /** ++ * @dev Checks if the _msg is composed. ++ * @param _msg The _msg. ++ * @return A boolean indicating whether the _msg is composed. ++ */ ++ function isComposed(bytes calldata _msg) internal pure returns (bool) { ++ (, bytes memory composeMsg) = decode(_msg); ++ return composeMsg.length > 0; +// ... +``` + +Ensure you re-run `pnpm gen:api` to update the generated client code. diff --git a/examples/oapp-solana/docs/ordered-execution-part-1-overview.md b/examples/oapp-solana/docs/ordered-execution-part-1-overview.md new file mode 100644 index 0000000000..53e98383b9 --- /dev/null +++ b/examples/oapp-solana/docs/ordered-execution-part-1-overview.md @@ -0,0 +1,201 @@ +# Implementing Ordered Execution in OApp: Part 1, Overview + +This document outlines how to implement [ordered message execution](https://docs.layerzero.network/v2/developers/evm/oapp/message-design-patterns#message-ordering) in a LayerZero OApp on Solana. Ordered execution ensures that messages from a specific source are processed in the exact sequence they were sent, preventing out-of-order execution which can lead to inconsistent states. For a walkthrough on exact code changes given the MyOApp example, refer to [./ordered-execution-1-implementation.md](./ordered-execution-1-implementation.md). You are highly recommended to go through this document first. + +## Key Implementation Requirements + +To implement ordered execution, an OApp must implement several essential components: + +### 1. The `next_nonce` Instruction + +The cornerstone of ordered execution is the `next_nonce` instruction. This instruction allows offchain executor to determine the next expected nonce for a message from a specific source. All other related instructions exist primarily to support this functionality. + +**Required Implementation**: + +```rust +use crate::*; + +#[derive(Accounts)] +#[instruction(params: NextNonceParams)] +pub struct NextNonce<'info> { + pub oapp: UncheckedAccount<'info>, + #[account( + seeds = [NONCE_SEED, ¶ms.receiver.as_ref(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender], + bump = nonce_account.bump + )] + pub nonce_account: Account<'info, Nonce>, +} + +impl NextNonce<'_> { + pub fn apply(ctx: &Context, _params: &NextNonceParams) -> Result { + return Ok(ctx.accounts.nonce_account.max_received_nonce + 1); + // return Ok(0); // path nonce starts from 1. if 0 it means that there is no specific nonce enforcement + } +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct NextNonceParams { + pub src_eid: u32, + pub sender: [u8; 32], + pub receiver: Pubkey, +} +``` + +**Critical Requirements**: + +1. The `NextNonceParams` struct must be defined exactly as shown, with fields in this precise order. This structure cannot be modified as offchain executor relies on this exact definition. +2. The `next_nonce` instruction structure must include exactly 2 accounts in this specific order: + + - First account: OApp PDA (receiver) - can be an unchecked account if not used + - Second account: Nonce account PDA + +3. The instruction must return `Result` with the next expected nonce value. + +### 2. Nonce Account + +#### 2.1 PDA Seed Structure + +The Nonce account PDA must use these specific seeds in this exact order: + +```rust +seeds = [NONCE_SEED, ¶ms.receiver.as_ref(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender] +``` + +Where: + +- `NONCE_SEED` is a constant byte array (e.g., b"Nonce") +- `params.receiver` is the receiver's (OApp) public key (32 bytes) +- `params.src_eid` is the source endpoint ID (as bytes) +- `params.sender` is the sender's (OApp) address (32 bytes) + +**Critical Note**: This precise seed structure is essential for the offchain executor to derive the nonce account correctly. Any changes to this structure would prevent offchain executor from calling the `next_nonce` instruction properly. + +#### 2.2 Account Structure + +The internal structure of the Nonce account is flexible. The only requirement is that it must store a value representing the current maximum nonce. How this value is stored and what additional fields exist in the account is up to the OApp implementation. + +For example, your Nonce account might look like: + +```rust +pub struct Nonce { + // You can include any fields needed for your application + // The only requirement is to have a field storing the max nonce + pub max_received_nonce: u64, // This field name can be different in your implementation +} +``` + +What matters is that the `next_nonce` instruction can read the current maximum nonce value, increment it, and return it as a `u64`. + +### 3. The `skip_inbound_nonce` Instruction + +This instruction allows administrators to handle missing messages by advancing the nonce counter: + +```rust +use oapp::endpoint::{instructions::SkipParams, ID as ENDPOINT_ID}; + +use crate::*; + +#[derive(Accounts)] +#[instruction(params: SkipInboundNonceParams)] +pub struct SkipInboundNonce<'info> { + // .... Add additional accounts as needed for your OApp + #[account( + mut, + seeds = [NONCE_SEED, ¶ms.receiver.as_ref(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender], + bump = nonce_account.bump + )] + pub nonce_account: Account<'info, Nonce>, +} + +impl SkipInboundNonce<'_> { + pub fn apply( + ctx: &mut Context, + params: &SkipInboundNonceParams, + ) -> Result<()> { + // .... OApp-specific business logic + let _ = oapp::endpoint_cpi::skip_nonce( + ENDPOINT_ID, + ctx.remaining_accounts, + seeds, + SkipParams { + receiver: params.receiver, + src_eid: params.src_eid, + sender: params.sender, + nonce: params.nonce, + }, + )?; + + // Update the max nonce value in your Nonce account + // This line will vary based on your Nonce account's structure + ctx.accounts.nonce_account.max_received_nonce += 1; + + return Ok(()); + } +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct SkipInboundNonceParams { + pub receiver: Pubkey, + pub src_eid: u32, + pub sender: [u8; 32], + pub nonce: u64, +} +``` + +**Implementation Requirements**: + +1. Must call the endpoint's `skip_nonce` CPI +2. Must update the maximum nonce value in the nonce account + +### 4. Nonce Validation in `lz_receive` + +Your OApp must validate and update nonces in the `lz_receive` instruction. This is typically done with a helper function similar to: + +```rust +fn accept_nonce<'info>(nonce_acc: &mut Account<'info, Nonce>, nonce: u64) -> Result<()> { + // Access your nonce account's maximum nonce value + // This might look different depending on your Nonce account structure + let current_nonce = nonce_acc.max_received_nonce; + + // enforce strict ordering + require!(nonce == current_nonce + 1, ErrorCode::InvalidNonce); + + // Always update the max nonce if the new one is greater + if nonce > current_nonce { + nonce_acc.max_received_nonce = nonce; + } + + Ok(()) +} +``` + +**Critical Note**: This function must be called in the `lz_receive` instruction to ensure the nonce state is correctly maintained. Without proper nonce updates, the `next_nonce` instruction would return incorrect values, leading to message delivery failures or out-of-order execution. + +## Implementation Steps + +Here are the general steps needed to implement ordered execution in your OApp: + +1. **Create Nonce Account Structure**: + + - Define a Nonce account structure with a field to store the maximum received nonce + - The specific structure is flexible as long as it can store the max nonce value + +2. **Implement Required Instructions**: + + - Implement `next_nonce` with the exact parameter and account structure + - Implement `skip_inbound_nonce` for administrative recovery + +3. **Update Message Receiving Logic**: + + - Add nonce validation to your `lz_receive` implementation + - Ensure your `lz_receive` instruction validates and updates nonces + - Add proper error handling for invalid nonces + +4. **Add Account Derivation in Types Instruction**: + - Update your `lz_receive_types` instruction to include the nonce account with the proper seed derivation + +## Offchain Workflow with LayerZero + +When a message includes the `ExecutorOrderedExecutionOption`, the LayerZero Executor will call the `next_nonce` instruction before delivering a message to determine if it should be executed. The Executor checks if the message's nonce matches the next expected nonce (returned by `next_nonce`). If it matches or if ordered execution is disabled (when `next_nonce` returns 0), the message is delivered immediately. Otherwise, the message is queued for later delivery. + +> Reference: https://docs.layerzero.network/v2/developers/evm/oapp/message-design-patterns#message-ordering diff --git a/examples/oapp-solana/docs/ordered-execution-part-2-implementation.md b/examples/oapp-solana/docs/ordered-execution-part-2-implementation.md new file mode 100644 index 0000000000..f6c57abca8 --- /dev/null +++ b/examples/oapp-solana/docs/ordered-execution-part-2-implementation.md @@ -0,0 +1,572 @@ +# Implementing Ordered Execution in OApp: Part 1, Implementation for MyOApp Example + +Before proceeding, ensure that you have gone through [the overview](./ordered-execution-part-1-overview.md) + +The following is an example of how ordered execution can be implemented in the MyOApp app: + +### 1. State Structures + +In `programs/my_oapp/src/state/store.rs`, amend the `Store` struct to include an `ordered_nonce` boolean field. + +```rust +pub struct Store { + // ... other fields + pub ordered_nonce: bool, // <-- add this - flag to enable/disable ordered execution +} +``` + +Modify `programs/my_oapp/src/instructions/init_store.rs` to accept `ordered_nonce` in the params: + +```rust +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct InitStoreParams { + pub admin: Pubkey, + pub endpoint: Pubkey, + pub ordered_nonce: bool, // <-- add this +} +``` + +Use the `params.ordered_nonce` value in InitStore's `apply` in the same file: + +```rust +//... +ctx.accounts.store.endpoint_program = params.endpoint; // existing line +ctx.accounts.store.ordered_nonce = params.ordered_nonce; // <-- add this +ctx.accounts.store.string = "Nothing received yet.".to_string(); // existing line +//... +``` + +Create a file at `programs/my_oapp/src/state/nonce.rs` with the following contents to define the Nonce account structure: + +```rust +use crate::*; + +// This is just one possible implementation of the Nonce account +#[account] +pub struct Nonce { + pub bump: u8, + pub max_received_nonce: u64, +} + +impl Nonce { + pub const SIZE: usize = 8 + std::mem::size_of::(); +} +``` + +Add the struct into `programs/my_oapp/src/state/mod.rs`: + +```rust +//... +mod nonce; +//... +pub use nonce::*; +//... +``` + +Declare the `NONCE_SEED` variable in `programs/my_oapp/src/lib.rs`: + +```rust +const NONCE_SEED: &[u8] = b"Nonce"; +``` + +### 2. NextNonce Implementation + +Create a file for the NextNonce instruction at `programs/my_oapp/src/instructions/next_nonce.rs` with the following contents: + +```rust +use crate::*; + +#[derive(Accounts)] +#[instruction(params: NextNonceParams)] +pub struct NextNonce<'info> { + #[account( + seeds = [STORE_SEED], + bump = store.bump, + constraint = params.receiver == store.key() + )] + pub store: Account<'info, Store>, + #[account( + seeds = [NONCE_SEED, ¶ms.receiver.as_ref(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender], + bump = nonce_account.bump + )] + pub nonce_account: Account<'info, Nonce>, +} + +impl NextNonce<'_> { + pub fn apply(ctx: &Context, _params: &NextNonceParams) -> Result { + if ctx.accounts.store.ordered_nonce { + return Ok(ctx.accounts.nonce_account.max_received_nonce + 1); + } + return Ok(0); // path nonce starts from 1. if 0 it means that there is no specific nonce enforcement + } +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct NextNonceParams { + pub src_eid: u32, + pub sender: [u8; 32], + pub receiver: Pubkey, +} +``` + +Add the instruction into `programs/my_oapp/src/instructions/mod.rs` + +```rust +// ... +pub mod next_nonce; +//... +pub use next_nonce::*; +// ... +``` + +Register the instruction handler in `programs/my_oapp/src/lib.rs`: + +```rust +// existing methods +pub fn next_nonce(ctx: Context, params: NextNonceParams) -> Result { + NextNonce::apply(&ctx, ¶ms) +} +// existing methods +``` + +### 3. Message Receiving Logic + +Amend `programs/my_oapp/src/instructions/lz_receive.rs` to the following: + +```rust +use crate::*; +use anchor_lang::prelude::*; +use oapp::{ + endpoint::{ + cpi::accounts::Clear, + instructions::ClearParams, + ConstructCPIContext, ID as ENDPOINT_ID, + }, + LzReceiveParams, +}; + +#[derive(Accounts)] +#[instruction(params: LzReceiveParams)] +pub struct LzReceive<'info> { + #[account(mut, seeds = [STORE_SEED], bump = store.bump)] + pub store: Account<'info, Store>, + #[account( + seeds = [PEER_SEED, &store.key().to_bytes(), ¶ms.src_eid.to_be_bytes()], + bump = peer.bump, + constraint = params.sender == peer.address + )] + pub peer: Account<'info, Peer>, + #[account( + mut, + seeds = [NONCE_SEED, &store.key().to_bytes(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender], + bump = nonce_account.bump + )] + pub nonce_account: Account<'info, Nonce>, +} + +impl LzReceive<'_> { + pub fn apply(ctx: &mut Context, params: &LzReceiveParams) -> Result<()> { + let seeds: &[&[u8]] = + &[STORE_SEED, &[ctx.accounts.store.bump]]; + // the first 9 accounts are for clear() + let accounts_for_clear = &ctx.remaining_accounts[0..Clear::MIN_ACCOUNTS_LEN]; + let _ = oapp::endpoint_cpi::clear( + ENDPOINT_ID, + ctx.accounts.store.key(), + accounts_for_clear, + seeds, + ClearParams { + receiver: ctx.accounts.store.key(), + src_eid: params.src_eid, + sender: params.sender, + nonce: params.nonce, + guid: params.guid, + message: params.message.clone(), + }, + )?; + + accept_nonce(&ctx.accounts.store, &mut ctx.accounts.nonce_account, params.nonce)?; + + let string_value = msg_codec::decode(¶ms.message); + let store = &mut ctx.accounts.store; + store.string = string_value; + + Ok(()) + } +} + +fn accept_nonce<'info>( + store_acc: &Account<'info, Store>, + nonce_acc: &mut Account<'info, Nonce>, + nonce: u64, +) -> Result<()> { + let current_nonce = nonce_acc.max_received_nonce; + if store_acc.ordered_nonce { + require!(nonce == current_nonce + 1, MyOAppError::InvalidNonce); + } + // update the max nonce anyway. once the ordered mode is turned on, missing early nonces will be rejected + if nonce > current_nonce { + nonce_acc.max_received_nonce = nonce; + } + Ok(()) +} + +``` + +Changes: + +- Define an `accept_nonce` function +- Include `nonce_account` in the accounts list +- Call `accept_nonce` before committing the change + +Amend `programs/my_oapp/src/errors.rs` to include `InvalidNonce`: + +```rust +use anchor_lang::prelude::error_code; + +#[error_code] +pub enum MyOAppError { + InvalidMessageType +} + +``` + +### 4. Account Derivation in Type instruction + +Amend `programs/my_oapp/src/instructions/lz_receive_types.rs` to the following: + +```rust +use crate::*; +use oapp::endpoint_cpi::{get_accounts_for_clear, get_accounts_for_send_compose, LzAccount}; +use oapp::{endpoint::ID as ENDPOINT_ID, LzReceiveParams}; + +/// LzReceiveTypes instruction provides a list of accounts that are used in the LzReceive +/// instruction. The list of accounts required by this LzReceiveTypes instruction can be found +/// from the specific PDA account that is generated by the LZ_RECEIVE_TYPES_SEED. +#[derive(Accounts)] +pub struct LzReceiveTypes<'info> { + #[account(seeds = [STORE_SEED], bump = store.bump)] + pub store: Account<'info, Store>, +} + +impl LzReceiveTypes<'_> { + /// The list of accounts should follow the rules below: + /// 1. Include all the accounts that are used in the LzReceive instruction, including the + /// accounts that are used by the Endpoint program. + /// 2. Set the account is a signer with ZERO address if the LzReceive instruction needs a payer + /// to pay fee, like rent. + /// 3. Set the account is writable if the LzReceive instruction needs to modify the account. + pub fn apply( + ctx: &Context, + params: &LzReceiveParams, + ) -> Result> { + // There are two accounts that are used in the LzReceive instruction, + // except those accounts for endpoint program. + // The first account is the store account, that is the fixed one. + let store = ctx.accounts.store.key(); + + // The second account is the peer account, we find it by the params.src_eid. + let peer_seeds = [PEER_SEED, &store.to_bytes(), ¶ms.src_eid.to_be_bytes()]; + let (peer, _) = Pubkey::find_program_address(&peer_seeds, ctx.program_id); + + // The third account is the nonce account, we find it by the params.src_eid and params.sender. + let nonce_seeds = + [NONCE_SEED, &store.to_bytes(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender]; + let (nonce_account, _) = Pubkey::find_program_address(&nonce_seeds, ctx.program_id); + + let mut accounts = vec![ + // count + LzAccount { pubkey: store, is_signer: false, is_writable: true }, + // peer + LzAccount { pubkey: peer, is_signer: false, is_writable: false }, + // nonce_account + LzAccount { pubkey: nonce_account, is_signer: false, is_writable: true }, + ]; + + // append the accounts for the clear ix + let accounts_for_clear = get_accounts_for_clear( + ENDPOINT_ID, + &store, + params.src_eid, + ¶ms.sender, + params.nonce, + ); + accounts.extend(accounts_for_clear); + + // if the message type is composed, we need to append the accounts for the composing ix + let is_composed = msg_codec::msg_type(¶ms.message) == msg_codec::COMPOSED_TYPE; + if is_composed { + let accounts_for_composing = get_accounts_for_send_compose( + ENDPOINT_ID, + &store, + &store, // self + ¶ms.guid, + 0, + ¶ms.message, + ); + accounts.extend(accounts_for_composing); + } + + Ok(accounts) + } +} +``` + +Changes: + +- `LzAccount { pubkey: nonce_account, is_signer: false, is_writable: true },` added into the `accounts` vec. +- to find the `nonce_account` address, we do `let (nonce_account, _) = Pubkey::find_program_address(&nonce_seeds, ctx.program_id);` +- we define the `nonce_seeds` with `let nonce_seeds = [NONCE_SEED, &store.to_bytes(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender];` + +### 5. Enable skipping of `inbound_nonce` when needed: + +Create the instruction at `programs/my_oapp/src/instructions/skip_inbound_nonce.rs`: + +```rust +use oapp::endpoint::{instructions::SkipParams, ID as ENDPOINT_ID}; + +use crate::*; + +#[derive(Accounts)] +#[instruction(params: SkipInboundNonceParams)] +pub struct SkipInboundNonce<'info> { + #[account(address = store.admin)] + pub admin: Signer<'info>, + #[account(seeds = [STORE_SEED], bump = store.bump)] + pub store: Account<'info, Store>, + #[account( + mut, + seeds = [NONCE_SEED, ¶ms.receiver.as_ref(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender], + bump = nonce_account.bump + )] + pub nonce_account: Account<'info, Nonce>, +} + +impl SkipInboundNonce<'_> { + pub fn apply( + ctx: &mut Context, + params: &SkipInboundNonceParams, + ) -> Result<()> { + let seeds: &[&[u8]] = + &[STORE_SEED, &[ctx.accounts.store.bump]]; + + let _ = oapp::endpoint_cpi::skip_nonce( + ENDPOINT_ID, + ctx.remaining_accounts, + seeds, + SkipParams { + receiver: params.receiver, + src_eid: params.src_eid, + sender: params.sender, + nonce: params.nonce, + }, + )?; + if ctx.accounts.store.ordered_nonce { + ctx.accounts.nonce_account.max_received_nonce += 1; + } + return Ok(()); + } +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct SkipInboundNonceParams { + pub receiver: Pubkey, + pub src_eid: u32, + pub sender: [u8; 32], + pub nonce: u64, +} +``` + +Add the instruction into `programs/my_oapp/src/instructions/mod.rs`: + +```rust +//... +pub mod skip_inbound_nonce; +//... +pub use skip_inbound_nonce::*; +//... +``` + +Register the instruction handler in `programs/my_oapp/src/lib.rs`: + +```rust +//... +pub fn skip_inbound_nonce( + mut ctx: Context, + params: SkipInboundNonceParams, +) -> Result<()> { + SkipInboundNonce::apply(&mut ctx, ¶ms) +} +//... +``` + +### 6. Allow for toggling of ordered execution + +Create the instruction definition at `programs/my_oapp/src/instructions/set_ordered_nonce.rs`: + +```rust +use crate::*; + +#[derive(Accounts)] +#[instruction(params: SetOrderedNonceParams)] +pub struct SetOrderedNonce<'info> { + #[account(address = store.admin)] + pub admin: Signer<'info>, + #[account(mut, seeds = [STORE_SEED], bump = store.bump)] + pub store: Account<'info, Store>, +} + +impl SetOrderedNonce<'_> { + pub fn apply(ctx: &mut Context, params: &SetOrderedNonceParams) -> Result<()> { + ctx.accounts.store.ordered_nonce = params.ordered_nonce; + Ok(()) + } +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct SetOrderedNonceParams { + pub ordered_nonce: bool, +} +``` + +Add the instruction into `programs/my_oapp/src/instructions/mod.rs`: + +```rust +//... +pub mod set_ordered_nonce; +//... +pub use set_ordered_nonce::*; +//... +``` + +Register the instruction handler in `programs/my_oapp/src/lib.rs`; + +```rust +pub fn set_ordered_nonce( + mut ctx: Context, + params: SetOrderedNonceParams, +) -> Result<()> { + SetOrderedNonce::apply(&mut ctx, ¶ms) +} +``` + +### 6. Amend `set_peer` to init `nonce_account`: + +```rust +use crate::*; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +#[instruction(params: SetPeerParams)] +pub struct SetPeer<'info> { + #[account(mut, address = store.admin)] + pub admin: Signer<'info>, + #[account( + init_if_needed, + payer = admin, + space = Peer::SIZE, + seeds = [PEER_SEED, &store.key().to_bytes(), ¶ms.remote_eid.to_be_bytes()], + bump + )] + pub peer: Account<'info, Peer>, + #[account( + init_if_needed, + payer = admin, + space = Nonce::SIZE, + seeds = [NONCE_SEED, &store.key().to_bytes(), ¶ms.remote_eid.to_be_bytes(), ¶ms.peer_address], + bump + )] + pub nonce_account: Account<'info, Nonce>, // <-- add this + #[account(seeds = [STORE_SEED], bump = store.bump)] + pub store: Account<'info, Store>, + pub system_program: Program<'info, System>, +} + +impl SetPeer<'_> { + pub fn apply(ctx: &mut Context, params: &SetPeerParams) -> Result<()> { + ctx.accounts.peer.address = params.peer_address; + ctx.accounts.peer.bump = ctx.bumps.peer; + ctx.accounts.nonce_account.bump = ctx.bumps.nonce_account; // <-- add this + Ok(()) + } +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct SetPeerParams { + pub remote_eid: u32, + pub peer_address: [u8; 32], +} +``` + +Changes summary: + +- add `nonce_account` into accounts list +- save `nonce_account`'s bump in the `apply` + +### 7. Amend scripts + +In `tasks/common/send.ts`, amend the `options` construction in both `sendFromEvm` and `sendFromSolana`: + +```typescript +const options = Options.newOptions() + .addExecutorLzReceiveOption(100_000, 0) // adjust gas if necessary + .addExecutorOrderedExecutionOption() // <-- add this line + .toBytes(); +``` + +In `lib/client/myoapp.ts`, add in `orderedNonce` as a param like so: + +```typescript +// note: orderedNonce added as a second param +initStore(payer: Signer, admin: PublicKey, orderedNonce: boolean): WrappedInstruction { + const [oapp] = this.pda.oapp() + const remainingAccounts = this.endpointSDK.getRegisterOappIxAccountMetaForCPI(payer.publicKey, oapp) + return instructions + .initStore( + { payer: payer, programs: this.programRepo }, + { + payer, + count: oapp, + lzReceiveTypesAccounts: this.pda.lzReceiveTypesAccounts()[0], + lzComposeTypesAccounts: this.pda.lzComposeTypesAccounts()[0], + + // args + admin: admin, + endpoint: this.endpointSDK.programId, + orderedNonce, // <-- add this + string: '', + } + ) + .addRemainingAccounts(remainingAccounts).items[0] +} +``` + +Also in ``lib/client/myoapp.ts`, add: + +```typescript +setOrderedNonce(admin: Signer, orderedNonce: boolean): WrappedInstruction { + return instructions.setOrderedNonce( + { programs: this.programRepo }, + { + admin, + count: this.pda.oapp(), + orderedNonce, + } + ).items[0] +} +``` + +In `tasks/solana/oappCreate.ts`, add `orderedNonce` value when calling `initStore`: + +```typescript +let orderedNonce = true; +const txBuilder = transactionBuilder().add( + myoappInstance.initStore( + umiWalletSigner, + umiWalletSigner.publicKey, + orderedNonce, + ), +); +``` + +This example shows how the MyOApp app implements the required components while adding application-specific logic (like the `ordered_nonce` flag for toggling the feature). diff --git a/examples/oapp-solana/foundry.toml b/examples/oapp-solana/foundry.toml new file mode 100644 index 0000000000..00a0e5a0fd --- /dev/null +++ b/examples/oapp-solana/foundry.toml @@ -0,0 +1,33 @@ +[profile.default] +solc-version = '0.8.22' +src = 'contracts' +out = 'out' +test = 'test/foundry' +cache_path = 'cache/foundry' +verbosity = 3 +optimize = true +optimizer_runs = 1000 +libs = [ + # We provide a set of useful contract utilities + # in the lib directory of @layerzerolabs/toolbox-foundry: + # + # - forge-std + # - ds-test + # - solidity-bytes-utils + 'node_modules/@layerzerolabs/toolbox-foundry/lib', + 'node_modules', +] + +remappings = [ + # Due to a misconfiguration of solidity-bytes-utils, an outdated version + # of forge-std is being dragged in + # + # To remedy this, we'll remap the ds-test and forge-std imports to our own versions + 'ds-test/=node_modules/@layerzerolabs/toolbox-foundry/lib/ds-test', + 'forge-std/=node_modules/@layerzerolabs/toolbox-foundry/lib/forge-std', + '@layerzerolabs/=node_modules/@layerzerolabs/', + '@openzeppelin/=node_modules/@openzeppelin/', +] + +[fuzz] +runs = 1000 diff --git a/examples/oapp-solana/hardhat.config.ts b/examples/oapp-solana/hardhat.config.ts new file mode 100644 index 0000000000..cbbde5e0cb --- /dev/null +++ b/examples/oapp-solana/hardhat.config.ts @@ -0,0 +1,76 @@ +// Get the environment configuration from .env file +// +// To make use of automatic environment setup: +// - Duplicate .env.example file and name it .env +// - Fill in the environment variables +import 'dotenv/config' + +import 'hardhat-deploy' +import '@nomicfoundation/hardhat-ethers' +import 'hardhat-contract-sizer' +import '@nomiclabs/hardhat-ethers' +import 'hardhat-deploy-ethers' +import '@layerzerolabs/toolbox-hardhat' +import { HardhatUserConfig, HttpNetworkAccountsUserConfig } from 'hardhat/types' + +import { EndpointId } from '@layerzerolabs/lz-definitions' + +import './tasks/index' + +// Set your preferred authentication method +// +// If you prefer using a mnemonic, set a MNEMONIC environment variable +// to a valid mnemonic +const MNEMONIC = process.env.MNEMONIC + +// If you prefer to be authenticated using a private key, set a PRIVATE_KEY environment variable +const PRIVATE_KEY = process.env.PRIVATE_KEY + +const accounts: HttpNetworkAccountsUserConfig | undefined = MNEMONIC + ? { mnemonic: MNEMONIC } + : PRIVATE_KEY + ? [PRIVATE_KEY] + : undefined + +if (accounts == null) { + console.warn( + 'Could not find MNEMONIC or PRIVATE_KEY environment variables. It will not be possible to execute transactions in your example.' + ) +} + +const config: HardhatUserConfig = { + paths: { + cache: 'cache/hardhat', + }, + solidity: { + compilers: [ + { + version: '0.8.22', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + ], + }, + networks: { + 'optimism-testnet': { + eid: EndpointId.OPTSEP_V2_TESTNET, + url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', + accounts, + }, + hardhat: { + // Need this for testing because TestHelperOz5.sol is exceeding the compiled contract size limit + allowUnlimitedContractSize: true, + }, + }, + namedAccounts: { + deployer: { + default: 0, // wallet address of index[0], of the mnemonic in .env + }, + }, +} + +export default config diff --git a/examples/oapp-solana/layerzero.config.ts b/examples/oapp-solana/layerzero.config.ts new file mode 100644 index 0000000000..3e8dedd828 --- /dev/null +++ b/examples/oapp-solana/layerzero.config.ts @@ -0,0 +1,59 @@ +import { EndpointId } from '@layerzerolabs/lz-definitions' +import { ExecutorOptionType } from '@layerzerolabs/lz-v2-utilities' +import { TwoWayConfig, generateConnectionsConfig } from '@layerzerolabs/metadata-tools' +import { OAppEnforcedOption, OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat' + +import { getSolanaOAppAddress } from './tasks/solana' + +const optimismContract: OmniPointHardhat = { + eid: EndpointId.OPTSEP_V2_TESTNET, + contractName: 'MyOApp', +} + +const solanaContract: OmniPointHardhat = { + eid: EndpointId.SOLANA_V2_TESTNET, + address: getSolanaOAppAddress(EndpointId.SOLANA_V2_TESTNET), // NOTE: replace with the oapp account address +} + +// For this example's simplicity, we will use the same enforced options values for sending to all chains +// For production, you should ensure `gas` is set to the correct value through profiling the gas usage of calling OApp._lzReceive(...) on the destination chain +// To learn more, read https://docs.layerzero.network/v2/concepts/applications/oapp-standard#execution-options-and-enforced-settings +const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ + { + msgType: 1, + optionType: ExecutorOptionType.LZ_RECEIVE, + gas: 100_000, + }, +] + +const SOLANA_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ + { + msgType: 1, + optionType: ExecutorOptionType.LZ_RECEIVE, + gas: 100_000, + }, +] + +// To connect all the above chains to each other, we need the following pathways: +// Optimism <-> Solana + +// With the config generator, pathways declared are automatically bidirectional +// i.e. if you declare A,B there's no need to declare B,A +const pathways: TwoWayConfig[] = [ + [ + optimismContract, // Chain A contract + solanaContract, // Chain C contract + [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] + [1, 32], // [A to B confirmations, B to A confirmations] + [SOLANA_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain C enforcedOptions, Chain A enforcedOptions + ], +] + +export default async function () { + // Generate the connections config based on the pathways + const connections = await generateConnectionsConfig(pathways) + return { + contracts: [{ contract: optimismContract }, { contract: solanaContract }], + connections, + } +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/accounts/endpointSettings.ts b/examples/oapp-solana/lib/client/generated/my_oapp/accounts/endpointSettings.ts new file mode 100644 index 0000000000..0e2d2c4af5 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/accounts/endpointSettings.ts @@ -0,0 +1,144 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + Account, + Context, + Option, + OptionOrNullable, + Pda, + PublicKey, + RpcAccount, + RpcGetAccountOptions, + RpcGetAccountsOptions, + assertAccountExists, + deserializeAccount, + gpaBuilder, + publicKey as toPublicKey, +} from '@metaplex-foundation/umi' +import { + Serializer, + bytes, + mapSerializer, + option, + publicKey as publicKeySerializer, + struct, + u32, + u8, +} from '@metaplex-foundation/umi/serializers' + +export type EndpointSettings = Account + +export type EndpointSettingsAccountData = { + discriminator: Uint8Array + eid: number + bump: number + admin: PublicKey + lzTokenMint: Option +} + +export type EndpointSettingsAccountDataArgs = { + eid: number + bump: number + admin: PublicKey + lzTokenMint: OptionOrNullable +} + +export function getEndpointSettingsAccountDataSerializer(): Serializer< + EndpointSettingsAccountDataArgs, + EndpointSettingsAccountData +> { + return mapSerializer( + struct( + [ + ['discriminator', bytes({ size: 8 })], + ['eid', u32()], + ['bump', u8()], + ['admin', publicKeySerializer()], + ['lzTokenMint', option(publicKeySerializer())], + ], + { description: 'EndpointSettingsAccountData' } + ), + (value) => ({ + ...value, + discriminator: new Uint8Array([221, 232, 73, 56, 10, 66, 72, 14]), + }) + ) as Serializer +} + +export function deserializeEndpointSettings(rawAccount: RpcAccount): EndpointSettings { + return deserializeAccount(rawAccount, getEndpointSettingsAccountDataSerializer()) +} + +export async function fetchEndpointSettings( + context: Pick, + publicKey: PublicKey | Pda, + options?: RpcGetAccountOptions +): Promise { + const maybeAccount = await context.rpc.getAccount(toPublicKey(publicKey, false), options) + assertAccountExists(maybeAccount, 'EndpointSettings') + return deserializeEndpointSettings(maybeAccount) +} + +export async function safeFetchEndpointSettings( + context: Pick, + publicKey: PublicKey | Pda, + options?: RpcGetAccountOptions +): Promise { + const maybeAccount = await context.rpc.getAccount(toPublicKey(publicKey, false), options) + return maybeAccount.exists ? deserializeEndpointSettings(maybeAccount) : null +} + +export async function fetchAllEndpointSettings( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ) + return maybeAccounts.map((maybeAccount) => { + assertAccountExists(maybeAccount, 'EndpointSettings') + return deserializeEndpointSettings(maybeAccount) + }) +} + +export async function safeFetchAllEndpointSettings( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ) + return maybeAccounts + .filter((maybeAccount) => maybeAccount.exists) + .map((maybeAccount) => deserializeEndpointSettings(maybeAccount as RpcAccount)) +} + +export function getEndpointSettingsGpaBuilder(context: Pick) { + const programId = context.programs.getPublicKey('myOapp', 'HFyiETGKEUS9tr87K1HXmVJHkqQRtw8wShRNTMkKKxay') + return gpaBuilder(context, programId) + .registerFields<{ + discriminator: Uint8Array + eid: number + bump: number + admin: PublicKey + lzTokenMint: OptionOrNullable + }>({ + discriminator: [0, bytes({ size: 8 })], + eid: [8, u32()], + bump: [12, u8()], + admin: [13, publicKeySerializer()], + lzTokenMint: [45, option(publicKeySerializer())], + }) + .deserializeUsing((account) => deserializeEndpointSettings(account)) + .whereField('discriminator', new Uint8Array([221, 232, 73, 56, 10, 66, 72, 14])) +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/accounts/index.ts b/examples/oapp-solana/lib/client/generated/my_oapp/accounts/index.ts new file mode 100644 index 0000000000..6715ff0fa7 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/accounts/index.ts @@ -0,0 +1,12 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +export * from './endpointSettings' +export * from './lzReceiveTypesAccounts' +export * from './peerConfig' +export * from './store' diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/accounts/lzReceiveTypesAccounts.ts b/examples/oapp-solana/lib/client/generated/my_oapp/accounts/lzReceiveTypesAccounts.ts new file mode 100644 index 0000000000..e22ebdbdc4 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/accounts/lzReceiveTypesAccounts.ts @@ -0,0 +1,124 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + Account, + Context, + Pda, + PublicKey, + RpcAccount, + RpcGetAccountOptions, + RpcGetAccountsOptions, + assertAccountExists, + deserializeAccount, + gpaBuilder, + publicKey as toPublicKey, +} from '@metaplex-foundation/umi' +import { + Serializer, + bytes, + mapSerializer, + publicKey as publicKeySerializer, + struct, +} from '@metaplex-foundation/umi/serializers' + +/** LzReceiveTypesAccounts includes accounts that are used in the LzReceiveTypes instruction. */ +export type LzReceiveTypesAccounts = Account + +export type LzReceiveTypesAccountsAccountData = { + discriminator: Uint8Array + store: PublicKey +} + +export type LzReceiveTypesAccountsAccountDataArgs = { store: PublicKey } + +export function getLzReceiveTypesAccountsAccountDataSerializer(): Serializer< + LzReceiveTypesAccountsAccountDataArgs, + LzReceiveTypesAccountsAccountData +> { + return mapSerializer( + struct( + [ + ['discriminator', bytes({ size: 8 })], + ['store', publicKeySerializer()], + ], + { description: 'LzReceiveTypesAccountsAccountData' } + ), + (value) => ({ + ...value, + discriminator: new Uint8Array([248, 87, 167, 117, 5, 251, 21, 126]), + }) + ) as Serializer +} + +export function deserializeLzReceiveTypesAccounts(rawAccount: RpcAccount): LzReceiveTypesAccounts { + return deserializeAccount(rawAccount, getLzReceiveTypesAccountsAccountDataSerializer()) +} + +export async function fetchLzReceiveTypesAccounts( + context: Pick, + publicKey: PublicKey | Pda, + options?: RpcGetAccountOptions +): Promise { + const maybeAccount = await context.rpc.getAccount(toPublicKey(publicKey, false), options) + assertAccountExists(maybeAccount, 'LzReceiveTypesAccounts') + return deserializeLzReceiveTypesAccounts(maybeAccount) +} + +export async function safeFetchLzReceiveTypesAccounts( + context: Pick, + publicKey: PublicKey | Pda, + options?: RpcGetAccountOptions +): Promise { + const maybeAccount = await context.rpc.getAccount(toPublicKey(publicKey, false), options) + return maybeAccount.exists ? deserializeLzReceiveTypesAccounts(maybeAccount) : null +} + +export async function fetchAllLzReceiveTypesAccounts( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ) + return maybeAccounts.map((maybeAccount) => { + assertAccountExists(maybeAccount, 'LzReceiveTypesAccounts') + return deserializeLzReceiveTypesAccounts(maybeAccount) + }) +} + +export async function safeFetchAllLzReceiveTypesAccounts( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ) + return maybeAccounts + .filter((maybeAccount) => maybeAccount.exists) + .map((maybeAccount) => deserializeLzReceiveTypesAccounts(maybeAccount as RpcAccount)) +} + +export function getLzReceiveTypesAccountsGpaBuilder(context: Pick) { + const programId = context.programs.getPublicKey('myOapp', 'HFyiETGKEUS9tr87K1HXmVJHkqQRtw8wShRNTMkKKxay') + return gpaBuilder(context, programId) + .registerFields<{ discriminator: Uint8Array; store: PublicKey }>({ + discriminator: [0, bytes({ size: 8 })], + store: [8, publicKeySerializer()], + }) + .deserializeUsing((account) => deserializeLzReceiveTypesAccounts(account)) + .whereField('discriminator', new Uint8Array([248, 87, 167, 117, 5, 251, 21, 126])) +} + +export function getLzReceiveTypesAccountsSize(): number { + return 40 +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/accounts/peerConfig.ts b/examples/oapp-solana/lib/client/generated/my_oapp/accounts/peerConfig.ts new file mode 100644 index 0000000000..c8cf3b6ea8 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/accounts/peerConfig.ts @@ -0,0 +1,126 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + Account, + Context, + Pda, + PublicKey, + RpcAccount, + RpcGetAccountOptions, + RpcGetAccountsOptions, + assertAccountExists, + deserializeAccount, + gpaBuilder, + publicKey as toPublicKey, +} from '@metaplex-foundation/umi' +import { Serializer, bytes, mapSerializer, struct, u8 } from '@metaplex-foundation/umi/serializers' +import { EnforcedOptions, EnforcedOptionsArgs, getEnforcedOptionsSerializer } from '../types' + +export type PeerConfig = Account + +export type PeerConfigAccountData = { + discriminator: Uint8Array + peerAddress: Uint8Array + enforcedOptions: EnforcedOptions + bump: number +} + +export type PeerConfigAccountDataArgs = { + peerAddress: Uint8Array + enforcedOptions: EnforcedOptionsArgs + bump: number +} + +export function getPeerConfigAccountDataSerializer(): Serializer { + return mapSerializer( + struct( + [ + ['discriminator', bytes({ size: 8 })], + ['peerAddress', bytes({ size: 32 })], + ['enforcedOptions', getEnforcedOptionsSerializer()], + ['bump', u8()], + ], + { description: 'PeerConfigAccountData' } + ), + (value) => ({ + ...value, + discriminator: new Uint8Array([181, 157, 86, 198, 33, 193, 94, 203]), + }) + ) as Serializer +} + +export function deserializePeerConfig(rawAccount: RpcAccount): PeerConfig { + return deserializeAccount(rawAccount, getPeerConfigAccountDataSerializer()) +} + +export async function fetchPeerConfig( + context: Pick, + publicKey: PublicKey | Pda, + options?: RpcGetAccountOptions +): Promise { + const maybeAccount = await context.rpc.getAccount(toPublicKey(publicKey, false), options) + assertAccountExists(maybeAccount, 'PeerConfig') + return deserializePeerConfig(maybeAccount) +} + +export async function safeFetchPeerConfig( + context: Pick, + publicKey: PublicKey | Pda, + options?: RpcGetAccountOptions +): Promise { + const maybeAccount = await context.rpc.getAccount(toPublicKey(publicKey, false), options) + return maybeAccount.exists ? deserializePeerConfig(maybeAccount) : null +} + +export async function fetchAllPeerConfig( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ) + return maybeAccounts.map((maybeAccount) => { + assertAccountExists(maybeAccount, 'PeerConfig') + return deserializePeerConfig(maybeAccount) + }) +} + +export async function safeFetchAllPeerConfig( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ) + return maybeAccounts + .filter((maybeAccount) => maybeAccount.exists) + .map((maybeAccount) => deserializePeerConfig(maybeAccount as RpcAccount)) +} + +export function getPeerConfigGpaBuilder(context: Pick) { + const programId = context.programs.getPublicKey('myOapp', 'HFyiETGKEUS9tr87K1HXmVJHkqQRtw8wShRNTMkKKxay') + return gpaBuilder(context, programId) + .registerFields<{ + discriminator: Uint8Array + peerAddress: Uint8Array + enforcedOptions: EnforcedOptionsArgs + bump: number + }>({ + discriminator: [0, bytes({ size: 8 })], + peerAddress: [8, bytes({ size: 32 })], + enforcedOptions: [40, getEnforcedOptionsSerializer()], + bump: [null, u8()], + }) + .deserializeUsing((account) => deserializePeerConfig(account)) + .whereField('discriminator', new Uint8Array([181, 157, 86, 198, 33, 193, 94, 203])) +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/accounts/store.ts b/examples/oapp-solana/lib/client/generated/my_oapp/accounts/store.ts new file mode 100644 index 0000000000..2ba28e4e36 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/accounts/store.ts @@ -0,0 +1,138 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + Account, + Context, + Pda, + PublicKey, + RpcAccount, + RpcGetAccountOptions, + RpcGetAccountsOptions, + assertAccountExists, + deserializeAccount, + gpaBuilder, + publicKey as toPublicKey, +} from '@metaplex-foundation/umi' +import { + Serializer, + bytes, + mapSerializer, + publicKey as publicKeySerializer, + string, + struct, + u8, +} from '@metaplex-foundation/umi/serializers' + +export type Store = Account + +export type StoreAccountData = { + discriminator: Uint8Array + admin: PublicKey + bump: number + endpointProgram: PublicKey + string: string +} + +export type StoreAccountDataArgs = { + admin: PublicKey + bump: number + endpointProgram: PublicKey + string: string +} + +export function getStoreAccountDataSerializer(): Serializer { + return mapSerializer( + struct( + [ + ['discriminator', bytes({ size: 8 })], + ['admin', publicKeySerializer()], + ['bump', u8()], + ['endpointProgram', publicKeySerializer()], + ['string', string()], + ], + { description: 'StoreAccountData' } + ), + (value) => ({ + ...value, + discriminator: new Uint8Array([130, 48, 247, 244, 182, 191, 30, 26]), + }) + ) as Serializer +} + +export function deserializeStore(rawAccount: RpcAccount): Store { + return deserializeAccount(rawAccount, getStoreAccountDataSerializer()) +} + +export async function fetchStore( + context: Pick, + publicKey: PublicKey | Pda, + options?: RpcGetAccountOptions +): Promise { + const maybeAccount = await context.rpc.getAccount(toPublicKey(publicKey, false), options) + assertAccountExists(maybeAccount, 'Store') + return deserializeStore(maybeAccount) +} + +export async function safeFetchStore( + context: Pick, + publicKey: PublicKey | Pda, + options?: RpcGetAccountOptions +): Promise { + const maybeAccount = await context.rpc.getAccount(toPublicKey(publicKey, false), options) + return maybeAccount.exists ? deserializeStore(maybeAccount) : null +} + +export async function fetchAllStore( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ) + return maybeAccounts.map((maybeAccount) => { + assertAccountExists(maybeAccount, 'Store') + return deserializeStore(maybeAccount) + }) +} + +export async function safeFetchAllStore( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ) + return maybeAccounts + .filter((maybeAccount) => maybeAccount.exists) + .map((maybeAccount) => deserializeStore(maybeAccount as RpcAccount)) +} + +export function getStoreGpaBuilder(context: Pick) { + const programId = context.programs.getPublicKey('myOapp', 'HFyiETGKEUS9tr87K1HXmVJHkqQRtw8wShRNTMkKKxay') + return gpaBuilder(context, programId) + .registerFields<{ + discriminator: Uint8Array + admin: PublicKey + bump: number + endpointProgram: PublicKey + string: string + }>({ + discriminator: [0, bytes({ size: 8 })], + admin: [8, publicKeySerializer()], + bump: [40, u8()], + endpointProgram: [41, publicKeySerializer()], + string: [73, string()], + }) + .deserializeUsing((account) => deserializeStore(account)) + .whereField('discriminator', new Uint8Array([130, 48, 247, 244, 182, 191, 30, 26])) +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/errors/index.ts b/examples/oapp-solana/lib/client/generated/my_oapp/errors/index.ts new file mode 100644 index 0000000000..a38217af56 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/errors/index.ts @@ -0,0 +1,9 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +export * from './myOapp' diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/errors/myOapp.ts b/examples/oapp-solana/lib/client/generated/my_oapp/errors/myOapp.ts new file mode 100644 index 0000000000..7296cf8056 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/errors/myOapp.ts @@ -0,0 +1,44 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { Program, ProgramError } from '@metaplex-foundation/umi' + +type ProgramErrorConstructor = new (program: Program, cause?: Error) => ProgramError +const codeToErrorMap: Map = new Map() +const nameToErrorMap: Map = new Map() + +/** InvalidMessageType */ +export class InvalidMessageTypeError extends ProgramError { + override readonly name: string = 'InvalidMessageType' + + readonly code: number = 0x1770 // 6000 + + constructor(program: Program, cause?: Error) { + super('', program, cause) + } +} +codeToErrorMap.set(0x1770, InvalidMessageTypeError) +nameToErrorMap.set('InvalidMessageType', InvalidMessageTypeError) + +/** + * Attempts to resolve a custom program error from the provided error code. + * @category Errors + */ +export function getMyOappErrorFromCode(code: number, program: Program, cause?: Error): ProgramError | null { + const constructor = codeToErrorMap.get(code) + return constructor ? new constructor(program, cause) : null +} + +/** + * Attempts to resolve a custom program error from the provided error name, i.e. 'Unauthorized'. + * @category Errors + */ +export function getMyOappErrorFromName(name: string, program: Program, cause?: Error): ProgramError | null { + const constructor = nameToErrorMap.get(name) + return constructor ? new constructor(program, cause) : null +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/index.ts b/examples/oapp-solana/lib/client/generated/my_oapp/index.ts new file mode 100644 index 0000000000..442708c958 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/index.ts @@ -0,0 +1,14 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +export * from './accounts' +export * from './errors' +export * from './instructions' +export * from './programs' +export * from './shared' +export * from './types' diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/instructions/index.ts b/examples/oapp-solana/lib/client/generated/my_oapp/instructions/index.ts new file mode 100644 index 0000000000..148e47ba1a --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/instructions/index.ts @@ -0,0 +1,14 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +export * from './initStore' +export * from './lzReceive' +export * from './lzReceiveTypes' +export * from './quoteSend' +export * from './send' +export * from './setPeerConfig' diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/instructions/initStore.ts b/examples/oapp-solana/lib/client/generated/my_oapp/instructions/initStore.ts new file mode 100644 index 0000000000..0d9c613258 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/instructions/initStore.ts @@ -0,0 +1,122 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { Context, Pda, PublicKey, Signer, TransactionBuilder, transactionBuilder } from '@metaplex-foundation/umi' +import { + Serializer, + bytes, + mapSerializer, + publicKey as publicKeySerializer, + struct, +} from '@metaplex-foundation/umi/serializers' +import { ResolvedAccount, ResolvedAccountsWithIndices, getAccountMetasAndSigners } from '../shared' + +// Accounts. +export type InitStoreInstructionAccounts = { + payer?: Signer + store: PublicKey | Pda + lzReceiveTypesAccounts: PublicKey | Pda + systemProgram?: PublicKey | Pda +} + +// Data. +export type InitStoreInstructionData = { + discriminator: Uint8Array + admin: PublicKey + endpoint: PublicKey +} + +export type InitStoreInstructionDataArgs = { + admin: PublicKey + endpoint: PublicKey +} + +export function getInitStoreInstructionDataSerializer(): Serializer< + InitStoreInstructionDataArgs, + InitStoreInstructionData +> { + return mapSerializer( + struct( + [ + ['discriminator', bytes({ size: 8 })], + ['admin', publicKeySerializer()], + ['endpoint', publicKeySerializer()], + ], + { description: 'InitStoreInstructionData' } + ), + (value) => ({ + ...value, + discriminator: new Uint8Array([250, 74, 6, 95, 163, 188, 19, 181]), + }) + ) as Serializer +} + +// Args. +export type InitStoreInstructionArgs = InitStoreInstructionDataArgs + +// Instruction. +export function initStore( + context: Pick, + input: InitStoreInstructionAccounts & InitStoreInstructionArgs +): TransactionBuilder { + // Program ID. + const programId = context.programs.getPublicKey('myOapp', 'HFyiETGKEUS9tr87K1HXmVJHkqQRtw8wShRNTMkKKxay') + + // Accounts. + const resolvedAccounts = { + payer: { + index: 0, + isWritable: true as boolean, + value: input.payer ?? null, + }, + store: { + index: 1, + isWritable: true as boolean, + value: input.store ?? null, + }, + lzReceiveTypesAccounts: { + index: 2, + isWritable: true as boolean, + value: input.lzReceiveTypesAccounts ?? null, + }, + systemProgram: { + index: 3, + isWritable: false as boolean, + value: input.systemProgram ?? null, + }, + } satisfies ResolvedAccountsWithIndices + + // Arguments. + const resolvedArgs: InitStoreInstructionArgs = { ...input } + + // Default values. + if (!resolvedAccounts.payer.value) { + resolvedAccounts.payer.value = context.payer + } + if (!resolvedAccounts.systemProgram.value) { + resolvedAccounts.systemProgram.value = context.programs.getPublicKey( + 'splSystem', + '11111111111111111111111111111111' + ) + resolvedAccounts.systemProgram.isWritable = false + } + + // Accounts in order. + const orderedAccounts: ResolvedAccount[] = Object.values(resolvedAccounts).sort((a, b) => a.index - b.index) + + // Keys and Signers. + const [keys, signers] = getAccountMetasAndSigners(orderedAccounts, 'programId', programId) + + // Data. + const data = getInitStoreInstructionDataSerializer().serialize(resolvedArgs as InitStoreInstructionDataArgs) + + // Bytes Created On Chain. + const bytesCreatedOnChain = 0 + + return transactionBuilder([{ instruction: { keys, programId, data }, signers, bytesCreatedOnChain }]) +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/instructions/lzReceive.ts b/examples/oapp-solana/lib/client/generated/my_oapp/instructions/lzReceive.ts new file mode 100644 index 0000000000..863ba4eaec --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/instructions/lzReceive.ts @@ -0,0 +1,84 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { Context, Pda, PublicKey, TransactionBuilder, transactionBuilder } from '@metaplex-foundation/umi' +import { Serializer, bytes, mapSerializer, struct } from '@metaplex-foundation/umi/serializers' +import { ResolvedAccount, ResolvedAccountsWithIndices, getAccountMetasAndSigners } from '../shared' +import { LzReceiveParams, LzReceiveParamsArgs, getLzReceiveParamsSerializer } from '../types' + +// Accounts. +export type LzReceiveInstructionAccounts = { + store: PublicKey | Pda + peer: PublicKey | Pda +} + +// Data. +export type LzReceiveInstructionData = { + discriminator: Uint8Array + params: LzReceiveParams +} + +export type LzReceiveInstructionDataArgs = { params: LzReceiveParamsArgs } + +export function getLzReceiveInstructionDataSerializer(): Serializer< + LzReceiveInstructionDataArgs, + LzReceiveInstructionData +> { + return mapSerializer( + struct( + [ + ['discriminator', bytes({ size: 8 })], + ['params', getLzReceiveParamsSerializer()], + ], + { description: 'LzReceiveInstructionData' } + ), + (value) => ({ + ...value, + discriminator: new Uint8Array([8, 179, 120, 109, 33, 118, 189, 80]), + }) + ) as Serializer +} + +// Args. +export type LzReceiveInstructionArgs = LzReceiveInstructionDataArgs + +// Instruction. +export function lzReceive( + context: Pick, + input: LzReceiveInstructionAccounts & LzReceiveInstructionArgs +): TransactionBuilder { + // Program ID. + const programId = context.programs.getPublicKey('myOapp', 'HFyiETGKEUS9tr87K1HXmVJHkqQRtw8wShRNTMkKKxay') + + // Accounts. + const resolvedAccounts = { + store: { + index: 0, + isWritable: true as boolean, + value: input.store ?? null, + }, + peer: { index: 1, isWritable: false as boolean, value: input.peer ?? null }, + } satisfies ResolvedAccountsWithIndices + + // Arguments. + const resolvedArgs: LzReceiveInstructionArgs = { ...input } + + // Accounts in order. + const orderedAccounts: ResolvedAccount[] = Object.values(resolvedAccounts).sort((a, b) => a.index - b.index) + + // Keys and Signers. + const [keys, signers] = getAccountMetasAndSigners(orderedAccounts, 'programId', programId) + + // Data. + const data = getLzReceiveInstructionDataSerializer().serialize(resolvedArgs as LzReceiveInstructionDataArgs) + + // Bytes Created On Chain. + const bytesCreatedOnChain = 0 + + return transactionBuilder([{ instruction: { keys, programId, data }, signers, bytesCreatedOnChain }]) +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/instructions/lzReceiveTypes.ts b/examples/oapp-solana/lib/client/generated/my_oapp/instructions/lzReceiveTypes.ts new file mode 100644 index 0000000000..9de0e5d9d5 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/instructions/lzReceiveTypes.ts @@ -0,0 +1,84 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { Context, Pda, PublicKey, TransactionBuilder, transactionBuilder } from '@metaplex-foundation/umi' +import { Serializer, bytes, mapSerializer, struct } from '@metaplex-foundation/umi/serializers' +import { ResolvedAccount, ResolvedAccountsWithIndices, getAccountMetasAndSigners } from '../shared' +import { LzReceiveParams, LzReceiveParamsArgs, getLzReceiveParamsSerializer } from '../types' + +// Accounts. +export type LzReceiveTypesInstructionAccounts = { + store: PublicKey | Pda +} + +// Data. +export type LzReceiveTypesInstructionData = { + discriminator: Uint8Array + params: LzReceiveParams +} + +export type LzReceiveTypesInstructionDataArgs = { params: LzReceiveParamsArgs } + +export function getLzReceiveTypesInstructionDataSerializer(): Serializer< + LzReceiveTypesInstructionDataArgs, + LzReceiveTypesInstructionData +> { + return mapSerializer( + struct( + [ + ['discriminator', bytes({ size: 8 })], + ['params', getLzReceiveParamsSerializer()], + ], + { description: 'LzReceiveTypesInstructionData' } + ), + (value) => ({ + ...value, + discriminator: new Uint8Array([221, 17, 246, 159, 248, 128, 31, 96]), + }) + ) as Serializer +} + +// Args. +export type LzReceiveTypesInstructionArgs = LzReceiveTypesInstructionDataArgs + +// Instruction. +export function lzReceiveTypes( + context: Pick, + input: LzReceiveTypesInstructionAccounts & LzReceiveTypesInstructionArgs +): TransactionBuilder { + // Program ID. + const programId = context.programs.getPublicKey('myOapp', 'HFyiETGKEUS9tr87K1HXmVJHkqQRtw8wShRNTMkKKxay') + + // Accounts. + const resolvedAccounts = { + store: { + index: 0, + isWritable: false as boolean, + value: input.store ?? null, + }, + } satisfies ResolvedAccountsWithIndices + + // Arguments. + const resolvedArgs: LzReceiveTypesInstructionArgs = { ...input } + + // Accounts in order. + const orderedAccounts: ResolvedAccount[] = Object.values(resolvedAccounts).sort((a, b) => a.index - b.index) + + // Keys and Signers. + const [keys, signers] = getAccountMetasAndSigners(orderedAccounts, 'programId', programId) + + // Data. + const data = getLzReceiveTypesInstructionDataSerializer().serialize( + resolvedArgs as LzReceiveTypesInstructionDataArgs + ) + + // Bytes Created On Chain. + const bytesCreatedOnChain = 0 + + return transactionBuilder([{ instruction: { keys, programId, data }, signers, bytesCreatedOnChain }]) +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/instructions/quoteSend.ts b/examples/oapp-solana/lib/client/generated/my_oapp/instructions/quoteSend.ts new file mode 100644 index 0000000000..d6f460d2c9 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/instructions/quoteSend.ts @@ -0,0 +1,103 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { Context, Pda, PublicKey, TransactionBuilder, transactionBuilder } from '@metaplex-foundation/umi' +import { Serializer, bool, bytes, mapSerializer, string, struct, u32 } from '@metaplex-foundation/umi/serializers' +import { ResolvedAccount, ResolvedAccountsWithIndices, getAccountMetasAndSigners } from '../shared' + +// Accounts. +export type QuoteSendInstructionAccounts = { + store: PublicKey | Pda + peer: PublicKey | Pda + endpoint: PublicKey | Pda +} + +// Data. +export type QuoteSendInstructionData = { + discriminator: Uint8Array + dstEid: number + receiver: Uint8Array + message: string + options: Uint8Array + payInLzToken: boolean +} + +export type QuoteSendInstructionDataArgs = { + dstEid: number + receiver: Uint8Array + message: string + options: Uint8Array + payInLzToken: boolean +} + +export function getQuoteSendInstructionDataSerializer(): Serializer< + QuoteSendInstructionDataArgs, + QuoteSendInstructionData +> { + return mapSerializer( + struct( + [ + ['discriminator', bytes({ size: 8 })], + ['dstEid', u32()], + ['receiver', bytes({ size: 32 })], + ['message', string()], + ['options', bytes({ size: u32() })], + ['payInLzToken', bool()], + ], + { description: 'QuoteSendInstructionData' } + ), + (value) => ({ + ...value, + discriminator: new Uint8Array([207, 0, 49, 214, 160, 211, 76, 211]), + }) + ) as Serializer +} + +// Args. +export type QuoteSendInstructionArgs = QuoteSendInstructionDataArgs + +// Instruction. +export function quoteSend( + context: Pick, + input: QuoteSendInstructionAccounts & QuoteSendInstructionArgs +): TransactionBuilder { + // Program ID. + const programId = context.programs.getPublicKey('myOapp', 'HFyiETGKEUS9tr87K1HXmVJHkqQRtw8wShRNTMkKKxay') + + // Accounts. + const resolvedAccounts = { + store: { + index: 0, + isWritable: false as boolean, + value: input.store ?? null, + }, + peer: { index: 1, isWritable: false as boolean, value: input.peer ?? null }, + endpoint: { + index: 2, + isWritable: false as boolean, + value: input.endpoint ?? null, + }, + } satisfies ResolvedAccountsWithIndices + + // Arguments. + const resolvedArgs: QuoteSendInstructionArgs = { ...input } + + // Accounts in order. + const orderedAccounts: ResolvedAccount[] = Object.values(resolvedAccounts).sort((a, b) => a.index - b.index) + + // Keys and Signers. + const [keys, signers] = getAccountMetasAndSigners(orderedAccounts, 'programId', programId) + + // Data. + const data = getQuoteSendInstructionDataSerializer().serialize(resolvedArgs as QuoteSendInstructionDataArgs) + + // Bytes Created On Chain. + const bytesCreatedOnChain = 0 + + return transactionBuilder([{ instruction: { keys, programId, data }, signers, bytesCreatedOnChain }]) +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/instructions/send.ts b/examples/oapp-solana/lib/client/generated/my_oapp/instructions/send.ts new file mode 100644 index 0000000000..005a637531 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/instructions/send.ts @@ -0,0 +1,100 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { Context, Pda, PublicKey, TransactionBuilder, transactionBuilder } from '@metaplex-foundation/umi' +import { Serializer, bytes, mapSerializer, string, struct, u32, u64 } from '@metaplex-foundation/umi/serializers' +import { ResolvedAccount, ResolvedAccountsWithIndices, getAccountMetasAndSigners } from '../shared' + +// Accounts. +export type SendInstructionAccounts = { + peer: PublicKey | Pda + store: PublicKey | Pda + endpoint: PublicKey | Pda +} + +// Data. +export type SendInstructionData = { + discriminator: Uint8Array + dstEid: number + message: string + options: Uint8Array + nativeFee: bigint + lzTokenFee: bigint +} + +export type SendInstructionDataArgs = { + dstEid: number + message: string + options: Uint8Array + nativeFee: number | bigint + lzTokenFee: number | bigint +} + +export function getSendInstructionDataSerializer(): Serializer { + return mapSerializer( + struct( + [ + ['discriminator', bytes({ size: 8 })], + ['dstEid', u32()], + ['message', string()], + ['options', bytes({ size: u32() })], + ['nativeFee', u64()], + ['lzTokenFee', u64()], + ], + { description: 'SendInstructionData' } + ), + (value) => ({ + ...value, + discriminator: new Uint8Array([102, 251, 20, 187, 65, 75, 12, 69]), + }) + ) as Serializer +} + +// Args. +export type SendInstructionArgs = SendInstructionDataArgs + +// Instruction. +export function send( + context: Pick, + input: SendInstructionAccounts & SendInstructionArgs +): TransactionBuilder { + // Program ID. + const programId = context.programs.getPublicKey('myOapp', 'HFyiETGKEUS9tr87K1HXmVJHkqQRtw8wShRNTMkKKxay') + + // Accounts. + const resolvedAccounts = { + peer: { index: 0, isWritable: false as boolean, value: input.peer ?? null }, + store: { + index: 1, + isWritable: false as boolean, + value: input.store ?? null, + }, + endpoint: { + index: 2, + isWritable: false as boolean, + value: input.endpoint ?? null, + }, + } satisfies ResolvedAccountsWithIndices + + // Arguments. + const resolvedArgs: SendInstructionArgs = { ...input } + + // Accounts in order. + const orderedAccounts: ResolvedAccount[] = Object.values(resolvedAccounts).sort((a, b) => a.index - b.index) + + // Keys and Signers. + const [keys, signers] = getAccountMetasAndSigners(orderedAccounts, 'programId', programId) + + // Data. + const data = getSendInstructionDataSerializer().serialize(resolvedArgs as SendInstructionDataArgs) + + // Bytes Created On Chain. + const bytesCreatedOnChain = 0 + + return transactionBuilder([{ instruction: { keys, programId, data }, signers, bytesCreatedOnChain }]) +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/instructions/setPeerConfig.ts b/examples/oapp-solana/lib/client/generated/my_oapp/instructions/setPeerConfig.ts new file mode 100644 index 0000000000..68fc3c594f --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/instructions/setPeerConfig.ts @@ -0,0 +1,110 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { Context, Pda, PublicKey, Signer, TransactionBuilder, transactionBuilder } from '@metaplex-foundation/umi' +import { Serializer, bytes, mapSerializer, struct, u32 } from '@metaplex-foundation/umi/serializers' +import { ResolvedAccount, ResolvedAccountsWithIndices, getAccountMetasAndSigners } from '../shared' +import { PeerConfigParam, PeerConfigParamArgs, getPeerConfigParamSerializer } from '../types' + +// Accounts. +export type SetPeerConfigInstructionAccounts = { + admin: Signer + peer: PublicKey | Pda + store: PublicKey | Pda + systemProgram?: PublicKey | Pda +} + +// Data. +export type SetPeerConfigInstructionData = { + discriminator: Uint8Array + remoteEid: number + config: PeerConfigParam +} + +export type SetPeerConfigInstructionDataArgs = { + remoteEid: number + config: PeerConfigParamArgs +} + +export function getSetPeerConfigInstructionDataSerializer(): Serializer< + SetPeerConfigInstructionDataArgs, + SetPeerConfigInstructionData +> { + return mapSerializer( + struct( + [ + ['discriminator', bytes({ size: 8 })], + ['remoteEid', u32()], + ['config', getPeerConfigParamSerializer()], + ], + { description: 'SetPeerConfigInstructionData' } + ), + (value) => ({ + ...value, + discriminator: new Uint8Array([79, 187, 168, 57, 139, 140, 93, 47]), + }) + ) as Serializer +} + +// Args. +export type SetPeerConfigInstructionArgs = SetPeerConfigInstructionDataArgs + +// Instruction. +export function setPeerConfig( + context: Pick, + input: SetPeerConfigInstructionAccounts & SetPeerConfigInstructionArgs +): TransactionBuilder { + // Program ID. + const programId = context.programs.getPublicKey('myOapp', 'HFyiETGKEUS9tr87K1HXmVJHkqQRtw8wShRNTMkKKxay') + + // Accounts. + const resolvedAccounts = { + admin: { + index: 0, + isWritable: true as boolean, + value: input.admin ?? null, + }, + peer: { index: 1, isWritable: true as boolean, value: input.peer ?? null }, + store: { + index: 2, + isWritable: false as boolean, + value: input.store ?? null, + }, + systemProgram: { + index: 3, + isWritable: false as boolean, + value: input.systemProgram ?? null, + }, + } satisfies ResolvedAccountsWithIndices + + // Arguments. + const resolvedArgs: SetPeerConfigInstructionArgs = { ...input } + + // Default values. + if (!resolvedAccounts.systemProgram.value) { + resolvedAccounts.systemProgram.value = context.programs.getPublicKey( + 'splSystem', + '11111111111111111111111111111111' + ) + resolvedAccounts.systemProgram.isWritable = false + } + + // Accounts in order. + const orderedAccounts: ResolvedAccount[] = Object.values(resolvedAccounts).sort((a, b) => a.index - b.index) + + // Keys and Signers. + const [keys, signers] = getAccountMetasAndSigners(orderedAccounts, 'programId', programId) + + // Data. + const data = getSetPeerConfigInstructionDataSerializer().serialize(resolvedArgs as SetPeerConfigInstructionDataArgs) + + // Bytes Created On Chain. + const bytesCreatedOnChain = 0 + + return transactionBuilder([{ instruction: { keys, programId, data }, signers, bytesCreatedOnChain }]) +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/programs/index.ts b/examples/oapp-solana/lib/client/generated/my_oapp/programs/index.ts new file mode 100644 index 0000000000..a38217af56 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/programs/index.ts @@ -0,0 +1,9 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +export * from './myOapp' diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/programs/myOapp.ts b/examples/oapp-solana/lib/client/generated/my_oapp/programs/myOapp.ts new file mode 100644 index 0000000000..d767c0131d --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/programs/myOapp.ts @@ -0,0 +1,40 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { ClusterFilter, Context, Program, PublicKey } from '@metaplex-foundation/umi' +import { getMyOappErrorFromCode, getMyOappErrorFromName } from '../errors' + +export const MY_OAPP_PROGRAM_ID = + 'HFyiETGKEUS9tr87K1HXmVJHkqQRtw8wShRNTMkKKxay' as PublicKey<'HFyiETGKEUS9tr87K1HXmVJHkqQRtw8wShRNTMkKKxay'> + +export function createMyOappProgram(): Program { + return { + name: 'myoapp', + publicKey: MY_OAPP_PROGRAM_ID, + getErrorFromCode(code: number, cause?: Error) { + return getMyOappErrorFromCode(code, this, cause) + }, + getErrorFromName(name: string, cause?: Error) { + return getMyOappErrorFromName(name, this, cause) + }, + isOnCluster() { + return true + }, + } +} + +export function getMyOappProgram( + context: Pick, + clusterFilter?: ClusterFilter +): T { + return context.programs.get('myOapp', clusterFilter) +} + +export function getMyOappProgramId(context: Pick, clusterFilter?: ClusterFilter): PublicKey { + return context.programs.getPublicKey('myOapp', MY_OAPP_PROGRAM_ID, clusterFilter) +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/shared/index.ts b/examples/oapp-solana/lib/client/generated/my_oapp/shared/index.ts new file mode 100644 index 0000000000..70554c289d --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/shared/index.ts @@ -0,0 +1,101 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { AccountMeta, isSigner, Pda, publicKey, PublicKey, Signer, isPda } from '@metaplex-foundation/umi' + +/** + * Transforms the given object such that the given keys are optional. + * @internal + */ +export type PickPartial = Omit & Partial> + +/** + * Asserts that the given value is not null or undefined. + * @internal + */ +export function expectSome(value: T | null | undefined): T { + if (value == null) { + throw new Error('Expected a value but received null or undefined.') + } + return value +} + +/** + * Asserts that the given value is a PublicKey. + * @internal + */ +export function expectPublicKey(value: PublicKey | Pda | Signer | null | undefined): PublicKey { + if (!value) { + throw new Error('Expected a PublicKey.') + } + return publicKey(value, false) +} + +/** + * Asserts that the given value is a PDA. + * @internal + */ +export function expectPda(value: PublicKey | Pda | Signer | null | undefined): Pda { + if (!value || !Array.isArray(value) || !isPda(value)) { + throw new Error('Expected a PDA.') + } + return value +} + +/** + * Defines an instruction account to resolve. + * @internal + */ +export type ResolvedAccount = { + isWritable: boolean + value: T +} + +/** + * Defines a set of instruction account to resolve. + * @internal + */ +export type ResolvedAccounts = Record + +/** + * Defines a set of instruction account to resolve with their indices. + * @internal + */ +export type ResolvedAccountsWithIndices = Record + +/** + * Get account metas and signers from resolved accounts. + * @internal + */ +export function getAccountMetasAndSigners( + accounts: ResolvedAccount[], + optionalAccountStrategy: 'omitted' | 'programId', + programId: PublicKey +): [AccountMeta[], Signer[]] { + const keys: AccountMeta[] = [] + const signers: Signer[] = [] + + accounts.forEach((account) => { + if (!account.value) { + if (optionalAccountStrategy === 'omitted') return + keys.push({ pubkey: programId, isSigner: false, isWritable: false }) + return + } + + if (isSigner(account.value)) { + signers.push(account.value) + } + keys.push({ + pubkey: publicKey(account.value, false), + isSigner: isSigner(account.value), + isWritable: account.isWritable, + }) + }) + + return [keys, signers] +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/types/enforcedOptions.ts b/examples/oapp-solana/lib/client/generated/my_oapp/types/enforcedOptions.ts new file mode 100644 index 0000000000..ea99c95aff --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/types/enforcedOptions.ts @@ -0,0 +1,23 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { Serializer, bytes, struct, u32 } from '@metaplex-foundation/umi/serializers' + +export type EnforcedOptions = { send: Uint8Array; sendAndCall: Uint8Array } + +export type EnforcedOptionsArgs = EnforcedOptions + +export function getEnforcedOptionsSerializer(): Serializer { + return struct( + [ + ['send', bytes({ size: u32() })], + ['sendAndCall', bytes({ size: u32() })], + ], + { description: 'EnforcedOptions' } + ) as Serializer +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/types/index.ts b/examples/oapp-solana/lib/client/generated/my_oapp/types/index.ts new file mode 100644 index 0000000000..5cc85a6f00 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/types/index.ts @@ -0,0 +1,13 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +export * from './enforcedOptions' +export * from './lzAccount' +export * from './lzReceiveParams' +export * from './messagingFee' +export * from './peerConfigParam' diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/types/lzAccount.ts b/examples/oapp-solana/lib/client/generated/my_oapp/types/lzAccount.ts new file mode 100644 index 0000000000..15771c19b6 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/types/lzAccount.ts @@ -0,0 +1,30 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { PublicKey } from '@metaplex-foundation/umi' +import { Serializer, bool, publicKey as publicKeySerializer, struct } from '@metaplex-foundation/umi/serializers' + +/** same to anchor_lang::prelude::AccountMeta */ +export type LzAccount = { + pubkey: PublicKey + isSigner: boolean + isWritable: boolean +} + +export type LzAccountArgs = LzAccount + +export function getLzAccountSerializer(): Serializer { + return struct( + [ + ['pubkey', publicKeySerializer()], + ['isSigner', bool()], + ['isWritable', bool()], + ], + { description: 'LzAccount' } + ) as Serializer +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/types/lzReceiveParams.ts b/examples/oapp-solana/lib/client/generated/my_oapp/types/lzReceiveParams.ts new file mode 100644 index 0000000000..ca661c26a9 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/types/lzReceiveParams.ts @@ -0,0 +1,41 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { Serializer, bytes, struct, u32, u64 } from '@metaplex-foundation/umi/serializers' + +export type LzReceiveParams = { + srcEid: number + sender: Uint8Array + nonce: bigint + guid: Uint8Array + message: Uint8Array + extraData: Uint8Array +} + +export type LzReceiveParamsArgs = { + srcEid: number + sender: Uint8Array + nonce: number | bigint + guid: Uint8Array + message: Uint8Array + extraData: Uint8Array +} + +export function getLzReceiveParamsSerializer(): Serializer { + return struct( + [ + ['srcEid', u32()], + ['sender', bytes({ size: 32 })], + ['nonce', u64()], + ['guid', bytes({ size: 32 })], + ['message', bytes({ size: u32() })], + ['extraData', bytes({ size: u32() })], + ], + { description: 'LzReceiveParams' } + ) as Serializer +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/types/messagingFee.ts b/examples/oapp-solana/lib/client/generated/my_oapp/types/messagingFee.ts new file mode 100644 index 0000000000..310352e1a5 --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/types/messagingFee.ts @@ -0,0 +1,26 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { Serializer, struct, u64 } from '@metaplex-foundation/umi/serializers' + +export type MessagingFee = { nativeFee: bigint; lzTokenFee: bigint } + +export type MessagingFeeArgs = { + nativeFee: number | bigint + lzTokenFee: number | bigint +} + +export function getMessagingFeeSerializer(): Serializer { + return struct( + [ + ['nativeFee', u64()], + ['lzTokenFee', u64()], + ], + { description: 'MessagingFee' } + ) as Serializer +} diff --git a/examples/oapp-solana/lib/client/generated/my_oapp/types/peerConfigParam.ts b/examples/oapp-solana/lib/client/generated/my_oapp/types/peerConfigParam.ts new file mode 100644 index 0000000000..1c77da73cb --- /dev/null +++ b/examples/oapp-solana/lib/client/generated/my_oapp/types/peerConfigParam.ts @@ -0,0 +1,67 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + GetDataEnumKind, + GetDataEnumKindContent, + Serializer, + bytes, + dataEnum, + struct, + tuple, + u32, +} from '@metaplex-foundation/umi/serializers' + +export type PeerConfigParam = + | { __kind: 'PeerAddress'; fields: [Uint8Array] } + | { __kind: 'EnforcedOptions'; send: Uint8Array; sendAndCall: Uint8Array } + +export type PeerConfigParamArgs = PeerConfigParam + +export function getPeerConfigParamSerializer(): Serializer { + return dataEnum( + [ + [ + 'PeerAddress', + struct>([ + ['fields', tuple([bytes({ size: 32 })])], + ]), + ], + [ + 'EnforcedOptions', + struct>([ + ['send', bytes({ size: u32() })], + ['sendAndCall', bytes({ size: u32() })], + ]), + ], + ], + { description: 'PeerConfigParam' } + ) as Serializer +} + +// Data Enum Helpers. +export function peerConfigParam( + kind: 'PeerAddress', + data: GetDataEnumKindContent['fields'] +): GetDataEnumKind +export function peerConfigParam( + kind: 'EnforcedOptions', + data: GetDataEnumKindContent +): GetDataEnumKind +export function peerConfigParam( + kind: K, + data?: any +): Extract { + return Array.isArray(data) ? { __kind: kind, fields: data } : { __kind: kind, ...(data ?? {}) } +} +export function isPeerConfigParam( + kind: K, + value: PeerConfigParam +): value is PeerConfigParam & { __kind: K } { + return value.__kind === kind +} diff --git a/examples/oapp-solana/lib/client/index.ts b/examples/oapp-solana/lib/client/index.ts new file mode 100644 index 0000000000..cfb387c98e --- /dev/null +++ b/examples/oapp-solana/lib/client/index.ts @@ -0,0 +1,2 @@ +export * from './pda' +export * as myoapp from './myoapp' diff --git a/examples/oapp-solana/lib/client/myoapp.ts b/examples/oapp-solana/lib/client/myoapp.ts new file mode 100644 index 0000000000..bda8a02d0f --- /dev/null +++ b/examples/oapp-solana/lib/client/myoapp.ts @@ -0,0 +1,393 @@ +import { + AccountMeta, + Cluster, + ClusterFilter, + Commitment, + Program, + ProgramError, + ProgramRepositoryInterface, + PublicKey, + RpcInterface, + Signer, + WrappedInstruction, + createNullRpc, +} from '@metaplex-foundation/umi' +import { createDefaultProgramRepository } from '@metaplex-foundation/umi-program-repository' +import { toWeb3JsInstruction } from '@metaplex-foundation/umi-web3js-adapters' +import { ComputeBudgetProgram } from '@solana/web3.js' +import { hexlify } from 'ethers/lib/utils' + +import { + EndpointProgram, + EventPDA, + MessageLibInterface, + SimpleMessageLibProgram, + SolanaPacketPath, + UlnProgram, + simulateWeb3JsTransaction, +} from '@layerzerolabs/lz-solana-sdk-v2/umi' + +import * as accounts from './generated/my_oapp/accounts' +import * as errors from './generated/my_oapp/errors' +import * as instructions from './generated/my_oapp/instructions' +import * as types from './generated/my_oapp/types' +import { MyOAppPDA as MyOAppPDA } from './pda' +import { SetPeerAddressParam, SetPeerEnforcedOptionsParam } from './types' + +export { accounts, errors, instructions, types } +export { MY_OAPP_PROGRAM_ID } from './generated/my_oapp' + +const ENDPOINT_PROGRAM_ID: PublicKey = EndpointProgram.ENDPOINT_PROGRAM_ID + +export enum MessageType { + VANILLA = 1, + COMPOSED_TYPE = 2, +} + +export class MyOApp { + public readonly pda: MyOAppPDA + public readonly eventAuthority: PublicKey + public readonly programRepo: ProgramRepositoryInterface + public readonly endpointSDK: EndpointProgram.Endpoint + + constructor( + public readonly programId: PublicKey, + public endpointProgramId: PublicKey = EndpointProgram.ENDPOINT_PROGRAM_ID, + rpc?: RpcInterface + ) { + this.pda = new MyOAppPDA(programId) + if (rpc === undefined) { + rpc = createNullRpc() + rpc.getCluster = (): Cluster => 'custom' + } + this.programRepo = createDefaultProgramRepository({ rpc: rpc }, [ + { + name: 'myOapp', + publicKey: programId, + getErrorFromCode(code: number, cause?: Error): ProgramError | null { + return errors.getMyOappErrorFromCode(code, this, cause) + }, + getErrorFromName(name: string, cause?: Error): ProgramError | null { + return errors.getMyOappErrorFromName(name, this, cause) + }, + isOnCluster(): boolean { + return true + }, + } satisfies Program, + ]) + this.eventAuthority = new EventPDA(programId).eventAuthority()[0] + this.endpointSDK = new EndpointProgram.Endpoint(endpointProgramId) + } + + async getEnforcedOptions(rpc: RpcInterface, remoteEid: number): Promise { + const [peer] = this.pda.peer(remoteEid) + const peerInfo = await accounts.fetchPeerConfig({ rpc }, peer) + return peerInfo.enforcedOptions + } + + getProgram(clusterFilter: ClusterFilter = 'custom'): Program { + return this.programRepo.get('myOapp', clusterFilter) + } + + async getStore(rpc: RpcInterface, commitment: Commitment = 'confirmed'): Promise { + const [count] = this.pda.oapp() + return accounts.safeFetchStore({ rpc }, count, { commitment }) + } + + initStore(payer: Signer, admin: PublicKey): WrappedInstruction { + const [oapp] = this.pda.oapp() + const remainingAccounts = this.endpointSDK.getRegisterOappIxAccountMetaForCPI(payer.publicKey, oapp) + return instructions + .initStore( + { payer: payer, programs: this.programRepo }, + { + payer, + store: oapp, + lzReceiveTypesAccounts: this.pda.lzReceiveTypesAccounts()[0], + + // args + admin: admin, + endpoint: this.endpointSDK.programId, + } + ) + .addRemainingAccounts(remainingAccounts).items[0] + } + + async send( + rpc: RpcInterface, + payer: PublicKey, + params: EndpointProgram.types.MessagingFee & { + dstEid: number + message: string + options: Uint8Array + composeMsg?: Uint8Array + }, + remainingAccounts?: AccountMeta[], + commitment: Commitment = 'confirmed' + ): Promise { + const { dstEid, nativeFee, lzTokenFee, message, options, composeMsg } = params + const msgLibProgram = await this.getSendLibraryProgram(rpc, payer, dstEid) + const [oapp] = this.pda.oapp() + const [peer] = this.pda.peer(dstEid) + const receiverInfo = await accounts.fetchPeerConfig({ rpc }, peer, { commitment }) + const packetPath: SolanaPacketPath = { + dstEid, + sender: oapp, + receiver: receiverInfo.peerAddress, + } + remainingAccounts = + remainingAccounts ?? + (await this.endpointSDK.getSendIXAccountMetaForCPI( + rpc, + payer, + { + path: packetPath, + msgLibProgram, + }, + commitment + )) + if (remainingAccounts === undefined) { + throw new Error('Failed to get remaining accounts for send instruction') + } + return instructions + .send( + { programs: this.programRepo }, + { + store: oapp, + peer: peer, + endpoint: this.endpointSDK.pda.setting()[0], + // args + dstEid, + message, + composeMsg: composeMsg ?? null, + options, + nativeFee: nativeFee, + lzTokenFee: lzTokenFee ?? 0, + } + ) + .addRemainingAccounts(remainingAccounts).items[0] + } + + setPeerConfig( + accounts: { + admin: Signer + }, + param: (SetPeerAddressParam | SetPeerEnforcedOptionsParam) & { + remote: number + } + ): WrappedInstruction { + const { admin } = accounts + const { remote } = param + let config: types.PeerConfigParamArgs + if (param.__kind === 'PeerAddress') { + if (param.peer.length !== 32) { + throw new Error('Peer must be 32 bytes (left-padded with zeroes)') + } + config = types.peerConfigParam('PeerAddress', [param.peer]) + } else if (param.__kind === 'EnforcedOptions') { + config = { + __kind: 'EnforcedOptions', + send: param.send, + sendAndCall: param.sendAndCall, + } + } else { + throw new Error('Invalid peer config') + } + + return instructions.setPeerConfig( + { programs: this.programRepo }, + { + admin, + store: this.pda.oapp()[0], + peer: this.pda.peer(remote)[0], + // args + remoteEid: remote, + config, + } + ).items[0] + } + + async quote( + rpc: RpcInterface, + payer: PublicKey, + params: { + dstEid: number + message: string + options: Uint8Array + composeMsg?: Uint8Array + payInLzToken: boolean + }, + remainingAccounts?: AccountMeta[], + commitment: Commitment = 'confirmed' + ): Promise { + const { dstEid, message, options, payInLzToken, composeMsg } = params + const msgLibProgram = await this.getSendLibraryProgram(rpc, payer, dstEid) + const [oapp] = this.pda.oapp() + + // const [endpointSettingPDA] = endpoint.deriver.setting() + const [peer] = this.pda.peer(dstEid) + const receiverInfo = await accounts.fetchPeerConfig({ rpc }, peer, { commitment }) + const packetPath: SolanaPacketPath = { + dstEid, + sender: oapp, + receiver: receiverInfo.peerAddress, + } + remainingAccounts = + remainingAccounts ?? + (await this.endpointSDK.getQuoteIXAccountMetaForCPI(rpc, payer, { + path: packetPath, + msgLibProgram, + })) + if (remainingAccounts === undefined) { + throw new Error('Failed to get remaining accounts for quote instruction') + } + const ix = instructions + .quoteSend( + { + programs: this.programRepo, + }, + { + store: oapp, + peer, + endpoint: this.endpointSDK.pda.setting()[0], + // args + dstEid, + message, + composeMsg: composeMsg ?? null, + options, + payInLzToken, + receiver: packetPath.receiver, + } + ) + .addRemainingAccounts(remainingAccounts).items[0] + + //TODO: use @solana-developers/helpers to get the compute units + const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({ + units: 400000, + }) + + return simulateWeb3JsTransaction( + rpc, + [modifyComputeUnits, toWeb3JsInstruction(ix.instruction)], + this.programId, + payer, + EndpointProgram.types.getMessagingFeeSerializer(), + 'confirmed' + ) + } + + async getSendLibraryProgram( + rpc: RpcInterface, + payer: PublicKey, + dstEid: number + ): Promise { + const [oapp] = this.pda.oapp() + const sendLibInfo = await this.endpointSDK.getSendLibrary(rpc, oapp, dstEid) + if (!sendLibInfo.programId) { + throw new Error('Send library not initialized or blocked message library') + } + const { programId: msgLibProgram } = sendLibInfo + const msgLibVersion = await this.endpointSDK.getMessageLibVersion(rpc, payer, msgLibProgram) + if (msgLibVersion.major === 0n && msgLibVersion.minor == 0 && msgLibVersion.endpointVersion == 2) { + return new SimpleMessageLibProgram.SimpleMessageLib(msgLibProgram) + } else if (msgLibVersion.major === 3n && msgLibVersion.minor == 0 && msgLibVersion.endpointVersion == 2) { + return new UlnProgram.Uln(msgLibProgram) + } + throw new Error(`Unsupported message library version: ${JSON.stringify(msgLibVersion, null, 2)}`) + } +} + +export async function getPeer(rpc: RpcInterface, dstEid: number, oftProgramId: PublicKey): Promise { + const [peer] = new MyOAppPDA(oftProgramId).peer(dstEid) + const info = await accounts.fetchPeerConfig({ rpc }, peer) + return hexlify(info.peerAddress) +} + +export function initConfig( + programId: PublicKey, + accounts: { + admin: Signer + payer: Signer + }, + remoteEid: number, + programs?: { + msgLib?: PublicKey + endpoint?: PublicKey + } +): WrappedInstruction { + const { admin, payer } = accounts + const pda = new MyOAppPDA(programId) + + let msgLibProgram: PublicKey, endpointProgram: PublicKey + if (programs === undefined) { + msgLibProgram = UlnProgram.ULN_PROGRAM_ID + endpointProgram = EndpointProgram.ENDPOINT_PROGRAM_ID + } else { + msgLibProgram = programs.msgLib ?? UlnProgram.ULN_PROGRAM_ID + endpointProgram = programs.endpoint ?? EndpointProgram.ENDPOINT_PROGRAM_ID + } + + const endpoint = new EndpointProgram.Endpoint(endpointProgram) + let msgLib: MessageLibInterface + if (msgLibProgram === SimpleMessageLibProgram.SIMPLE_MESSAGELIB_PROGRAM_ID) { + msgLib = new SimpleMessageLibProgram.SimpleMessageLib(SimpleMessageLibProgram.SIMPLE_MESSAGELIB_PROGRAM_ID) + } else { + msgLib = new UlnProgram.Uln(msgLibProgram) + } + return endpoint.initOAppConfig( + { + delegate: admin, + payer: payer.publicKey, + }, + { + msgLibSDK: msgLib, + oapp: pda.oapp()[0], + remote: remoteEid, + } + ) +} + +export function initSendLibrary( + accounts: { + admin: Signer + oapp: PublicKey + }, + remoteEid: number, + endpointProgram: PublicKey = ENDPOINT_PROGRAM_ID +): WrappedInstruction { + const { admin, oapp } = accounts + const endpoint = new EndpointProgram.Endpoint(endpointProgram) + return endpoint.initOAppSendLibrary(admin, { sender: oapp, remote: remoteEid }) +} + +export function initReceiveLibrary( + accounts: { + admin: Signer + oapp: PublicKey + }, + remoteEid: number, + endpointProgram: PublicKey = ENDPOINT_PROGRAM_ID +): WrappedInstruction { + const { admin, oapp } = accounts + const endpoint = new EndpointProgram.Endpoint(endpointProgram) + return endpoint.initOAppReceiveLibrary(admin, { receiver: oapp, remote: remoteEid }) +} + +export function initOAppNonce( + accounts: { + admin: Signer + oapp: PublicKey + }, + remoteEid: number, + remoteOappAddr: Uint8Array, // must be 32 bytes + endpointProgram: PublicKey = ENDPOINT_PROGRAM_ID +): WrappedInstruction { + const { admin, oapp } = accounts + const endpoint = new EndpointProgram.Endpoint(endpointProgram) + + return endpoint.initOAppNonce(admin, { + localOApp: oapp, + remote: remoteEid, + remoteOApp: remoteOappAddr, + }) +} diff --git a/examples/oapp-solana/lib/client/pda.ts b/examples/oapp-solana/lib/client/pda.ts new file mode 100644 index 0000000000..06f0ae0570 --- /dev/null +++ b/examples/oapp-solana/lib/client/pda.ts @@ -0,0 +1,41 @@ +import { Pda, PublicKey, publicKeyBytes } from '@metaplex-foundation/umi' +import { Endian, u32 } from '@metaplex-foundation/umi/serializers' +import { createWeb3JsEddsa } from '@metaplex-foundation/umi-eddsa-web3js' + +import { OmniAppPDA } from '@layerzerolabs/lz-solana-sdk-v2/umi' + +const eddsa = createWeb3JsEddsa() + +export class MyOAppPDA extends OmniAppPDA { + static STORE_SEED = 'Store' + static NONCE_SEED = 'Nonce' + + constructor(public readonly programId: PublicKey) { + super(programId) + } + + // seeds = [STORE_SEED], + oapp(): Pda { + return eddsa.findPda(this.programId, [Buffer.from(MyOAppPDA.STORE_SEED, 'utf8')]) + } + + // seeds = [PEER_SEED, &count.key().to_bytes(), ¶ms.dst_eid.to_be_bytes()], + peer(dstChainId: number): Pda { + const [count] = this.oapp() + return eddsa.findPda(this.programId, [ + Buffer.from(OmniAppPDA.PEER_SEED, 'utf8'), + publicKeyBytes(count), + u32({ endian: Endian.Big }).serialize(dstChainId), + ]) + } + + // seeds = [NONCE_SEED, ¶ms.receiver, ¶ms.src_eid.to_be_bytes(), ¶ms.sender] + nonce(receiver: PublicKey, remoteEid: number, sender: Uint8Array): Pda { + return eddsa.findPda(this.programId, [ + Buffer.from(MyOAppPDA.NONCE_SEED, 'utf8'), + publicKeyBytes(receiver), + u32({ endian: Endian.Big }).serialize(remoteEid), + sender, + ]) + } +} diff --git a/examples/oapp-solana/lib/client/types.ts b/examples/oapp-solana/lib/client/types.ts new file mode 100644 index 0000000000..30d04ca6be --- /dev/null +++ b/examples/oapp-solana/lib/client/types.ts @@ -0,0 +1,10 @@ +export interface SetPeerAddressParam { + peer: Uint8Array + __kind: 'PeerAddress' +} + +export interface SetPeerEnforcedOptionsParam { + send: Uint8Array + sendAndCall: Uint8Array + __kind: 'EnforcedOptions' +} diff --git a/examples/oapp-solana/lib/config.ts b/examples/oapp-solana/lib/config.ts new file mode 100644 index 0000000000..543bc400bc --- /dev/null +++ b/examples/oapp-solana/lib/config.ts @@ -0,0 +1,80 @@ +import { + type CreateTransactionsFromOmniEdges, + OmniPoint, + OmniSDKFactory, + type OmniVector, + createConfigureEdges, + createConfigureMultiple, + formatOmniVector, +} from '@layerzerolabs/devtools' +import { isOmniPointOnSolana } from '@layerzerolabs/devtools-solana' +import { createModuleLogger, createWithAsyncLogger } from '@layerzerolabs/io-devtools' + +import { CustomOAppSDK } from './sdk' + +import type { IOApp, OAppConfigurator, OAppOmniGraph } from '@layerzerolabs/ua-devtools' + +const createMyOAppLogger = () => createModuleLogger('MyOApp') +const withMyOAppLogger = createWithAsyncLogger(createMyOAppLogger) + +/** + * Helper function that checks whether a vector originates from a Solana network + * + * @param {OmniVector} vector + * @returns {boolean} + */ +const isVectorFromSolana = (vector: OmniVector): boolean => isOmniPointOnSolana(vector.from) + +/** + * Helper function that wraps a edge configuration function, + * only executing it for edges that originate in Solana + * + * @param {CreateTransactionsFromOmniEdges} createTransactions + * @returns {CreateTransactionsFromOmniEdges} + */ +const onlyEdgesFromSolana = ( + createTransactions: CreateTransactionsFromOmniEdges +): CreateTransactionsFromOmniEdges => { + const logger = createMyOAppLogger() + + return (edge, sdk, graph, createSdk) => { + if (!isVectorFromSolana(edge.vector)) { + return logger.verbose(`Ignoring connection ${formatOmniVector(edge.vector)}`), undefined + } + + return createTransactions( + edge, + sdk as CustomOAppSDK, + graph, + createSdk as OmniSDKFactory + ) + } +} + +export const initConfig: OAppConfigurator = createConfigureEdges( + onlyEdgesFromSolana( + withMyOAppLogger(async ({ vector: { to } }, sdk) => { + const logger = createMyOAppLogger() + if (typeof sdk.sendConfigIsInitialized !== 'function') { + return ( + logger.warn(`Could not find sendConfigIsInitialized() method on OAppWrapperSDK SDK, skipping`), + undefined + ) + } + if (typeof sdk.initConfig !== 'function') { + return logger.warn(`Could not find initConfig() method on OAppWrapperSDK SDK, skipping`), undefined + } + + logger.verbose(`Checking if the sendConfig for ${to.eid} ${to.address} is initialized`) + + const isInitialized = await sdk.sendConfigIsInitialized(to.eid) + if (isInitialized) { + return logger.verbose(`sendConfig for ${to.eid} ${to.address} is already initialized`), undefined + } + logger.verbose(`Initializing sendConfig for ${to.eid} ${to.address}`) + return sdk.initConfig(to.eid) + }) + ) +) + +export const initOAppAccounts = createConfigureMultiple(initConfig) diff --git a/examples/oapp-solana/lib/factory.ts b/examples/oapp-solana/lib/factory.ts new file mode 100644 index 0000000000..ca20ec897f --- /dev/null +++ b/examples/oapp-solana/lib/factory.ts @@ -0,0 +1,36 @@ +import pMemoize from 'p-memoize' + +import { + type ConnectionFactory, + type PublicKeyFactory, + createConnectionFactory, + defaultRpcUrlFactory, +} from '@layerzerolabs/devtools-solana' + +import { CustomOAppSDK } from './sdk' + +import type { OAppFactory } from '@layerzerolabs/ua-devtools' + +/** + * Syntactic sugar that creates an instance of Solana `OApp` SDK + * based on an `OmniPoint` with help of an `ConnectionFactory` + * and a `PublicKeyFactory` + * + * @param {PublicKeyFactory} userAccountFactory A function that accepts an `OmniPoint` representing an OApp and returns the user wallet public key + * @param {ConnectionFactory} connectionFactory A function that returns a `Connection` based on an `EndpointId` + * @returns {OAppFactory} + */ +export const createSimpleOAppFactory = ( + userAccountFactory: PublicKeyFactory, + programIdFactory: PublicKeyFactory, + connectionFactory: ConnectionFactory = createConnectionFactory(defaultRpcUrlFactory) +): OAppFactory => + pMemoize( + async (point) => + new CustomOAppSDK( + await connectionFactory(point.eid), + point, + await userAccountFactory(point), + await programIdFactory(point) + ) + ) diff --git a/examples/oapp-solana/lib/scripts/exchange.ts b/examples/oapp-solana/lib/scripts/exchange.ts new file mode 100755 index 0000000000..5e182f66fd --- /dev/null +++ b/examples/oapp-solana/lib/scripts/exchange.ts @@ -0,0 +1,125 @@ +import fs from 'fs' +import path from 'path' +import readline from 'readline' + +import { Idl } from '@coral-xyz/anchor' +import { IdlEvent } from '@coral-xyz/anchor/dist/cjs/idl' +import { camelCase } from 'kinobi' + +interface TypeField { + index: boolean + name: string + type: any +} + +interface TypeSubItem { + kind: 'struct' + fields: TypeField[] +} + +interface TypeItem { + name: string + type: TypeSubItem +} + +/** + * modify the idl json file, move events to types + * @param path idl file path + */ +export function exchangeIDLJson(path: string): Idl { + const content = fs.readFileSync(path, 'utf8') + const jsonContent = JSON.parse(content) as Idl + const types = jsonContent.types ?? [] + const { events } = jsonContent + if (events) { + for (const event of events) { + const { name } = event + const { fields } = event + + const typeFields: TypeField[] = fields + const typeSubItem: TypeSubItem = { + kind: 'struct', + fields: typeFields, + } + const newType: TypeItem = { + name: name, + type: typeSubItem, + } + types.push(newType) + } + } + return jsonContent +} + +/** + * move generated event files to events folder + * @param generatedSDKDir generated sdk directory + */ +export async function moveGenEventFiles(generatedSDKDir: string, events: IdlEvent[]): Promise { + const generatedTypesDir = path.join(generatedSDKDir, 'types') + const generatedEventsDir = path.join(generatedSDKDir, 'events') + //special event names which are not end with Event + // const specialEventNames = ['OFTSent', 'OFTReceived'] + const index = 'index.ts' + + // if types folder not exist, return directly + if (!fs.existsSync(generatedTypesDir)) { + return + } + + //get all event file names + const eventFileNames = events.map((event) => { + const { name } = event + return `${camelCase(name)}.ts` + }) + if (eventFileNames.length > 0) { + if (!fs.existsSync(generatedEventsDir)) { + fs.mkdirSync(generatedEventsDir) + } + eventFileNames.forEach((fileName) => { + const originalPath = path.join(generatedTypesDir, fileName) + const newPath = path.join(generatedEventsDir, fileName) + //move event file from types to events + fs.renameSync(originalPath, newPath) + //fix event file issue + fixEventFiles(newPath) + }) + + //move invalid lines from types/index.ts to events/index.ts + const needMoveLines: string[] = [] + const typesIndexFileReadStream = fs.createReadStream(path.join(generatedTypesDir, index)) + const typesInvalidLines = readline.createInterface({ input: typesIndexFileReadStream, crlfDelay: Infinity }) + const newTypeIndexFileLines = [] + for await (const line of typesInvalidLines) { + let invalid = false + events.forEach((event) => { + if (line.endsWith(`${camelCase(event.name)}';`)) { + needMoveLines.push(line) + invalid = true + } + }) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!invalid) { + newTypeIndexFileLines.push(line) + } + } + // first: remove invalid lines from types/index.ts. + fs.writeFileSync(path.join(generatedTypesDir, index), newTypeIndexFileLines.join('\n'), 'utf8') + typesIndexFileReadStream.close() + + // second: add invalid lines to events/index.ts. + fs.writeFileSync(path.join(generatedEventsDir, index), needMoveLines.join('\n'), 'utf8') + } +} + +/** + * some event file need to import types from types folder, but they are not in the same folder now, so fix it + * @param path + */ +function fixEventFiles(path: string): void { + let content = fs.readFileSync(path, 'utf8') + if (content.includes("from '.'")) { + content = content.replace("from '.'", "from '../types'") + fs.writeFileSync(path, content, 'utf8') + } +} diff --git a/examples/oapp-solana/lib/scripts/generate.ts b/examples/oapp-solana/lib/scripts/generate.ts new file mode 100644 index 0000000000..c9dd189c2b --- /dev/null +++ b/examples/oapp-solana/lib/scripts/generate.ts @@ -0,0 +1,31 @@ +import * as path from 'path' + +import { IdlV00, rootNodeFromAnchor } from '@kinobi-so/nodes-from-anchor' +import { renderVisitor } from '@kinobi-so/renderers-js-umi' +import { createFromRoot } from 'kinobi' + +import { exchangeIDLJson, moveGenEventFiles } from './exchange' + +async function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +async function generateTypeScriptSDK(): Promise { + const generatedSDKDir = path.join(__dirname, '..', 'client', 'generated', 'my_oapp') + const anchorIdlPath = path.join(__dirname, '..', '..', 'target', 'idl', 'my_oapp.json') + const anchorIdl = exchangeIDLJson(anchorIdlPath) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + anchorIdl.metadata = { ...anchorIdl.metadata, address: 'HFyiETGKEUS9tr87K1HXmVJHkqQRtw8wShRNTMkKKxay' } + console.error('Generating TypeScript SDK to %s. IDL from %s', generatedSDKDir, anchorIdlPath) + const kinobi = createFromRoot(rootNodeFromAnchor(anchorIdl as IdlV00)) + void kinobi.accept(renderVisitor(generatedSDKDir)) + await sleep(1000) + await moveGenEventFiles(generatedSDKDir, anchorIdl.events ?? []) +} + +;(async (): Promise => { + await generateTypeScriptSDK() +})().catch((err: unknown) => { + console.error(err) + process.exit(1) +}) diff --git a/examples/oapp-solana/lib/sdk.ts b/examples/oapp-solana/lib/sdk.ts new file mode 100644 index 0000000000..dbfc3e13f8 --- /dev/null +++ b/examples/oapp-solana/lib/sdk.ts @@ -0,0 +1,480 @@ +import assert from 'assert' + +import { mplToolbox } from '@metaplex-foundation/mpl-toolbox' +import { + Signer, + TransactionBuilder, + Umi, + type PublicKey as UmiPublicKey, + WrappedInstruction, + createNoopSigner, + publicKey, +} from '@metaplex-foundation/umi' +import { createUmi } from '@metaplex-foundation/umi-bundle-defaults' +import { fromWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters' +import { Connection, PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js' + +import { AsyncRetriable, mapError } from '@layerzerolabs/devtools' +import { + Bytes, + type OmniAddress, + OmniPoint, + type OmniTransaction, + areBytes32Equal, + denormalizePeer, + formatEid, + fromHex, + makeBytes32, + normalizePeer, + toHex, +} from '@layerzerolabs/devtools' +import { OmniSDK } from '@layerzerolabs/devtools-solana' +import { type Logger, printBoolean, printJson } from '@layerzerolabs/io-devtools' +import { EndpointProgram, MessageLibPDADeriver, UlnProgram } from '@layerzerolabs/lz-solana-sdk-v2' +import { Options } from '@layerzerolabs/lz-v2-utilities' +import { EndpointV2 } from '@layerzerolabs/protocol-devtools-solana' + +import { myoapp } from './client' + +import type { EndpointId } from '@layerzerolabs/lz-definitions' +import type { IOApp, OAppEnforcedOptionParam } from '@layerzerolabs/ua-devtools' + +// TODO: Use exported interfaces when they are available +interface SetPeerAddressParam { + peer: Uint8Array + __kind: 'PeerAddress' +} + +interface SetPeerEnforcedOptionsParam { + send: Uint8Array + sendAndCall: Uint8Array + __kind: 'EnforcedOptions' +} + +/* + * refer to the OFT wrapper SDK in the devtools repo at packages/ua-devtools-solana/src/oft/sdk.ts to understand why this wrapper SDK is needed. + */ +export class CustomOAppSDK extends OmniSDK implements IOApp { + protected readonly umi: Umi + protected readonly umiUserAccount: UmiPublicKey + protected readonly umiProgramId: UmiPublicKey + protected readonly umiPublicKey: UmiPublicKey + protected readonly umiMyOAppSdk: myoapp.MyOApp + + constructor( + connection: Connection, + point: OmniPoint, + userAccount: PublicKey, + public readonly programId: PublicKey, + logger?: Logger + ) { + super(connection, point, userAccount, logger) + // cache Umi-specific objects for reuse + this.umi = createUmi(connection.rpcEndpoint).use(mplToolbox()) + this.umiUserAccount = fromWeb3JsPublicKey(userAccount) + this.umiProgramId = fromWeb3JsPublicKey(this.programId) + this.umiPublicKey = fromWeb3JsPublicKey(this.publicKey) + this.umiMyOAppSdk = new myoapp.MyOApp(fromWeb3JsPublicKey(this.programId)) + } + + @AsyncRetriable() + async getOwner(): Promise { + this.logger.debug(`Getting owner`) + + const config = await mapError( + () => { + return myoapp.accounts.fetchStore(this.umi, this.umiPublicKey) + }, + (error) => new Error(`Failed to get owner for ${this.label}: ${error}`) + ) + + const owner = config.admin + return this.logger.debug(`Got owner: ${owner}`), owner + } + + async hasOwner(address: OmniAddress): Promise { + this.logger.debug(`Checking whether ${address} is an owner`) + + const owner = await this.getOwner() + const isOwner = areBytes32Equal(normalizePeer(address, this.point.eid), normalizePeer(owner, this.point.eid)) + + return this.logger.debug(`Checked whether ${address} is an owner (${owner}): ${printBoolean(isOwner)}`), isOwner + } + + // TODO: to implement after we implement OAppConfig + // async setOwner(address: OmniAddress): Promise { + // this.logger.debug(`Setting owner to ${address}`) + + // return { + // ...(await this.createTransaction(this._umiToWeb3Tx([await this._setOFTAdminIx(address)]))), + // description: `Setting owner to ${address}`, + // } + // } + + @AsyncRetriable() + async getEndpointSDK(): Promise { + this.logger.debug(`Getting EndpointV2 SDK`) + + return new EndpointV2( + this.connection, + { eid: this.point.eid, address: EndpointProgram.PROGRAM_ID.toBase58() }, + this.userAccount + ) + } + + @AsyncRetriable() + async getPeer(eid: EndpointId): Promise { + const eidLabel = `eid ${eid} (${formatEid(eid)})` + + this.logger.debug(`Getting peer for ${eidLabel}`) + try { + const peer = await myoapp.getPeer(this.umi.rpc, eid, this.umiProgramId) + // We run the hex string we got through a normalization/de-normalization process + // that will ensure that zero addresses will get stripped + // and any network-specific logic will be applied + return denormalizePeer(fromHex(peer), eid) + } catch (error) { + if (String(error).match(/was not found at the provided address/i)) { + return undefined + } + + throw new Error(`Failed to get peer for ${eidLabel} for OFT ${this.label}: ${error}`) + } + } + + async hasPeer(eid: EndpointId, address: OmniAddress | null | undefined): Promise { + const peer = await this.getPeer(eid) + return areBytes32Equal(normalizePeer(peer, eid), normalizePeer(address, eid)) + } + + async setPeer(eid: EndpointId, address: OmniAddress | null | undefined): Promise { + const eidLabel = formatEid(eid) + // We use the `mapError` and pretend `normalizePeer` is async to avoid having a let and a try/catch block + const normalizedPeer = await mapError( + async () => normalizePeer(address, eid), + (error) => + new Error(`Failed to convert peer ${address} for ${eidLabel} for ${this.label} to bytes: ${error}`) + ) + const peerAsBytes32 = makeBytes32(normalizedPeer) + const delegate = await this.safeGetDelegate() + + const oapp = this.umiPublicKey + + this.logger.debug(`Setting peer for eid ${eid} (${eidLabel}) to address ${peerAsBytes32}`) + const umiTxs = [ + await this._createSetPeerAddressIx(normalizedPeer, eid), // admin + // this.umiMyOAppSdk.setPeer(delegate, { peer: normalizedPeer, remoteEid: eid }),TODO: remove, since things are now handled by _createSetPeerAddressIx and _setPeerEnforcedOptionsIx + await this._setPeerEnforcedOptionsIx(new Uint8Array([0, 3]), new Uint8Array([0, 3]), eid), // admin + myoapp.initOAppNonce({ admin: delegate, oapp }, eid, normalizedPeer), // delegate + ] + + const isSendLibraryInitialized = await this.isSendLibraryInitialized(eid) + const isReceiveLibraryInitialized = await this.isReceiveLibraryInitialized(eid) + + if (!isSendLibraryInitialized) { + umiTxs.push(myoapp.initSendLibrary({ admin: delegate, oapp }, eid)) + } + + if (!isReceiveLibraryInitialized) { + umiTxs.push(myoapp.initReceiveLibrary({ admin: delegate, oapp }, eid)) + } + + return { + ...(await this.createTransaction(this._umiToWeb3Tx(umiTxs))), + description: `Setting peer for eid ${eid} (${eidLabel}) to address ${peerAsBytes32} ${delegate.publicKey} ${(await this._getAdmin()).publicKey}`, + } + } + + @AsyncRetriable() + async getDelegate(): Promise { + this.logger.debug(`Getting delegate`) + + const endpointSdk = await this.getEndpointSDK() + const delegate = await endpointSdk.getDelegate(this.point.address) + + return this.logger.verbose(`Got delegate: ${delegate}`), delegate + } + + @AsyncRetriable() + async isDelegate(delegate: OmniAddress): Promise { + this.logger.debug(`Checking whether ${delegate} is a delegate`) + + const endpointSdk = await this.getEndpointSDK() + const isDelegate = await endpointSdk.isDelegate(this.point.address, delegate) + + return this.logger.verbose(`Checked delegate: ${delegate}: ${printBoolean(isDelegate)}`), isDelegate + } + + async setDelegate(delegate: OmniAddress): Promise { + this.logger.debug(`Setting delegate to ${delegate}`) + return { + ...(await this.createTransaction(this._umiToWeb3Tx([await this._setOFTDelegateIx(delegate)]))), + description: `Setting delegate to ${delegate}`, + } + } + + @AsyncRetriable() + async getEnforcedOptions(eid: EndpointId, msgType: number): Promise { + // First we check that we can understand the message type + this.assertMsgType(msgType) + + const eidLabel = `eid ${eid} (${formatEid(eid)})` + this.logger.verbose(`Getting enforced options for ${eidLabel} and message type ${msgType}`) + try { + const options = await this.umiMyOAppSdk.getEnforcedOptions(this.umi.rpc, eid) + const optionsForMsgType = msgType === MSG_TYPE_SEND ? options.send : options.sendAndCall + + return toHex(optionsForMsgType) + } catch (error) { + if (String(error).match(/was not found at the provided address/)) { + return toHex(new Uint8Array(0)) + } + + throw new Error( + `Failed to get enforced options for ${this.label} for ${eidLabel} and message type ${msgType}: ${error}` + ) + } + } + + async setOutboundRateLimit( + eid: EndpointId, + rateLimit: { refillPerSecond: bigint; capacity: bigint } + ): Promise { + this.logger.verbose(`Setting outbound rate limit for ${eid} to ${printJson(rateLimit)}`) + + return { + ...(await this.createTransaction( + this._umiToWeb3Tx([await this._setPeerOutboundRateLimit(eid, rateLimit)]) + )), + description: `Setting outbound rate limit for ${eid} to ${printJson(rateLimit)}`, + } + } + + async setInboundRateLimit( + eid: EndpointId, + rateLimit: { refillPerSecond: bigint; capacity: bigint } + ): Promise { + this.logger.verbose(`Setting outbound rate limit for ${eid} to ${printJson(rateLimit)}`) + + return { + ...(await this.createTransaction(this._umiToWeb3Tx([await this._setPeerInboundRateLimit(eid, rateLimit)]))), + description: `Setting outbound rate limit for ${eid} to ${printJson(rateLimit)}`, + } + } + + async setEnforcedOptions(enforcedOptions: OAppEnforcedOptionParam[]): Promise { + this.logger.verbose(`Setting enforced options to ${printJson(enforcedOptions)}`) + const optionsByEidAndMsgType = this.reduceEnforcedOptions(enforcedOptions) + const emptyOptions = Options.newOptions().toBytes() + const ixs: WrappedInstruction[] = [] + for (const [eid, optionsByMsgType] of optionsByEidAndMsgType) { + const sendOption = optionsByMsgType.get(MSG_TYPE_SEND) ?? emptyOptions + const sendAndCallOption = optionsByMsgType.get(MSG_TYPE_SEND_AND_CALL) ?? emptyOptions + ixs.push(await this._setPeerEnforcedOptionsIx(sendOption, sendAndCallOption, eid)) + } + + return { + ...(await this.createTransaction(this._umiToWeb3Tx(ixs))), + description: `Setting enforced options to ${printJson(enforcedOptions)}`, + } + } + + async isSendLibraryInitialized(eid: EndpointId): Promise { + const endpointSdk = await this.getEndpointSDK() + return endpointSdk.isSendLibraryInitialized(this.point.address, eid) + } + + async initializeSendLibrary(eid: EndpointId): Promise<[OmniTransaction] | []> { + this.logger.verbose(`Initializing send library on ${formatEid(eid)}`) + + const endpointSdk = await this.getEndpointSDK() + return endpointSdk.initializeSendLibrary(this.point.address, eid) + } + + async isReceiveLibraryInitialized(eid: EndpointId): Promise { + const endpointSdk = await this.getEndpointSDK() + return endpointSdk.isReceiveLibraryInitialized(this.point.address, eid) + } + + async initializeReceiveLibrary(eid: EndpointId): Promise<[OmniTransaction] | []> { + this.logger.verbose(`Initializing receive library on ${formatEid(eid)}`) + + const endpointSdk = await this.getEndpointSDK() + return endpointSdk.initializeReceiveLibrary(this.point.address, eid) + } + + async isOAppConfigInitialized(eid: EndpointId): Promise { + const endpointSdk = await this.getEndpointSDK() + return endpointSdk.isOAppConfigInitialized(this.point.address, eid) + } + + async initializeOAppConfig(eid: EndpointId, lib: OmniAddress | null | undefined): Promise<[OmniTransaction] | []> { + this.logger.verbose(`Initializing OApp config for library ${lib} on ${formatEid(eid)}`) + + const endpointSdk = await this.getEndpointSDK() + return endpointSdk.initializeOAppConfig(this.point.address, eid, lib ?? undefined) + } + + /** + * Helper utility that takes an array of `OAppEnforcedOptionParam` objects and turns them into + * a map keyed by `EndpointId` that contains another map keyed by `MsgType`. + * + * @param {OAppEnforcedOptionParam[]} enforcedOptions + * @returns {Map>} + */ + private reduceEnforcedOptions( + enforcedOptions: OAppEnforcedOptionParam[] + ): Map> { + return enforcedOptions.reduce((optionsByEid, enforcedOption) => { + const { + eid, + option: { msgType, options }, + } = enforcedOption + + // First we check that we can understand the message type + this.assertMsgType(msgType) + + // Then we warn the user if they are trying to specify enforced options for eid & msgType more than once + // in which case the former option will be ignored + const optionsByMsgType = optionsByEid.get(eid) ?? new Map() + if (optionsByMsgType.has(msgType)) { + this.logger.warn(`Duplicate enforced option for ${formatEid(eid)} and msgType ${msgType}`) + } + + // We wrap the call with try/catch to deliver a better error message in case malformed options were passed + try { + optionsByMsgType.set(msgType, Options.fromOptions(options).toBytes()) + } catch (error) { + throw new Error( + `Invalid enforced options for ${this.label} for ${formatEid(eid)} and msgType ${msgType}: ${options}: ${error}` + ) + } + + optionsByEid.set(eid, optionsByMsgType) + + return optionsByEid + }, new Map>()) + } + + /** + * Helper method that asserts that `value` is a `MsgType` that the OFT understands + * and prints out a friendly error message if it doesn't + * + * @param {unknown} value + * @returns {undefined} + */ + private assertMsgType(value: unknown): asserts value is MsgType { + assert( + isMsgType(value), + `${this.label}: Invalid msgType received: ${value}. Expected one of ${MSG_TYPE_SEND} (send), ${MSG_TYPE_SEND_AND_CALL} (send and call)` + ) + } + + async setCallerBpsCap(callerBpsCap: bigint): Promise { + this.logger.debug(`Setting caller BPS cap to ${callerBpsCap}`) + + throw new TypeError(`setCallerBpsCap() not implemented on Solana OFT SDK`) + } + + @AsyncRetriable() + async getCallerBpsCap(): Promise { + this.logger.debug(`Getting caller BPS cap`) + + throw new TypeError(`getCallerBpsCap() not implemented on Solana OFT SDK`) + } + + public async sendConfigIsInitialized(_eid: EndpointId): Promise { + const deriver = new MessageLibPDADeriver(UlnProgram.PROGRAM_ID) + const [sendConfig] = deriver.sendConfig(_eid, new PublicKey(this.point.address)) + const accountInfo = await this.connection.getAccountInfo(sendConfig) + return accountInfo != null + } + + public async initConfig(eid: EndpointId): Promise { + const delegateAddress = await this.getDelegate() + // delegate may be undefined if it has not yet been set. In this case, use admin, which must exist. + const delegate = delegateAddress ? createNoopSigner(publicKey(delegateAddress)) : await this._getAdmin() + return { + ...(await this.createTransaction( + this._umiToWeb3Tx([ + myoapp.initConfig( + this.umiProgramId, + { + admin: delegate, + payer: delegate, + }, + eid, + { + msgLib: fromWeb3JsPublicKey(UlnProgram.PROGRAM_ID), + } + ), + ]) + )), + description: `oapp.initConfig(${eid})`, + } + } + + protected async _setPeerConfigIx( + param: (SetPeerAddressParam & { remote: number }) | (SetPeerEnforcedOptionsParam & { remote: number }) + ) { + return this.umiMyOAppSdk.setPeerConfig({ admin: await this._getAdmin() }, param) + } + + protected async _createSetPeerAddressIx(normalizedPeer: Uint8Array, eid: EndpointId) { + return this._setPeerConfigIx({ + __kind: 'PeerAddress', + peer: normalizedPeer, + remote: eid, + }) + } + + protected async _setPeerEnforcedOptionsIx(send: Uint8Array, sendAndCall: Uint8Array, eid: EndpointId) { + return this._setPeerConfigIx({ + __kind: 'EnforcedOptions', + send, + sendAndCall, + remote: eid, + }) + } + + // Convert Umi instructions to Web3JS Transaction + protected _umiToWeb3Tx(ixs: WrappedInstruction[]): Transaction { + const web3Transaction = new Transaction() + const txBuilder = new TransactionBuilder(ixs) + txBuilder.getInstructions().forEach((umiInstruction) => { + const web3Instruction = new TransactionInstruction({ + programId: new PublicKey(umiInstruction.programId), + keys: umiInstruction.keys.map((key) => ({ + pubkey: new PublicKey(key.pubkey), + isSigner: key.isSigner, + isWritable: key.isWritable, + })), + data: Buffer.from(umiInstruction.data), + }) + + // Add the instruction to the Web3.js transaction + web3Transaction.add(web3Instruction) + }) + return web3Transaction + } + + protected async safeGetDelegate() { + const delegateAddress = await this.getDelegate() + if (!delegateAddress) { + throw new Error('No delegate found') + } + return createNoopSigner(publicKey(delegateAddress)) + } + + protected async _getAdmin(): Promise { + const owner = await this.getOwner() + return createNoopSigner(publicKey(owner)) + } +} + +type MsgType = 1 | 2 + +const MSG_TYPE_SEND = 1 satisfies MsgType +const MSG_TYPE_SEND_AND_CALL = 2 satisfies MsgType + +const isMsgType = (value: unknown): value is MsgType => value === MSG_TYPE_SEND || value === MSG_TYPE_SEND_AND_CALL diff --git a/examples/oapp-solana/package.json b/examples/oapp-solana/package.json new file mode 100644 index 0000000000..c892ee76e1 --- /dev/null +++ b/examples/oapp-solana/package.json @@ -0,0 +1,110 @@ +{ + "name": "@layerzerolabs/oapp-solana-example", + "version": "0.1.0", + "private": true, + "license": "MIT", + "scripts": { + "clean": "rm -rf artifacts cache out", + "compile": "concurrently -c auto --names forge,hardhat '$npm_execpath run compile:forge' '$npm_execpath run compile:hardhat' '$npm_execpath run compile:anchor'", + "compile:anchor": "anchor build", + "compile:forge": "forge build", + "compile:hardhat": "hardhat compile", + "gen:api": "ts-node ./lib/scripts/generate.ts", + "lint": "$npm_execpath run lint:js && $npm_execpath run lint:sol", + "lint:fix": "eslint --fix '**/*.{js,ts,json}' && prettier --write . && solhint 'contracts/**/*.sol' --fix --noPrompt", + "lint:js": "eslint '**/*.{js,ts,json}' && prettier --check .", + "lint:sol": "solhint 'contracts/**/*.sol'", + "test": "$npm_execpath run test:forge && $npm_execpath run test:hardhat", + "test:anchor": "anchor test", + "test:forge": "forge test", + "test:hardhat": "hardhat test" + }, + "resolutions": { + "ethers": "^5.7.2", + "hardhat-deploy": "^0.12.1" + }, + "dependencies": { + "@kinobi-so/nodes": "^0.21.4", + "@kinobi-so/nodes-from-anchor": "^0.21.2", + "@kinobi-so/renderers": "^0.21.3", + "@kinobi-so/renderers-js-umi": "^0.21.6", + "kinobi": "^0.21.4" + }, + "devDependencies": { + "@babel/core": "^7.23.9", + "@coral-xyz/anchor": "^0.29.0", + "@layerzerolabs/devtools": "~1.0.0", + "@layerzerolabs/devtools-evm-hardhat": "^3.1.0", + "@layerzerolabs/devtools-solana": "~2.1.0", + "@layerzerolabs/eslint-config-next": "~2.3.39", + "@layerzerolabs/io-devtools": "~0.2.0", + "@layerzerolabs/lz-definitions": "^3.0.86", + "@layerzerolabs/lz-evm-messagelib-v2": "^3.0.86", + "@layerzerolabs/lz-evm-protocol-v2": "^3.0.86", + "@layerzerolabs/lz-evm-v1-0.7": "^3.0.86", + "@layerzerolabs/lz-solana-sdk-v2": "^3.0.86", + "@layerzerolabs/lz-v2-utilities": "^3.0.86", + "@layerzerolabs/metadata-tools": "^2.0.0", + "@layerzerolabs/oapp-evm": "^0.3.2", + "@layerzerolabs/prettier-config-next": "^2.3.39", + "@layerzerolabs/protocol-devtools": "^2.0.0", + "@layerzerolabs/protocol-devtools-solana": "^7.0.0", + "@layerzerolabs/solhint-config": "^3.0.12", + "@layerzerolabs/test-devtools-evm-foundry": "~6.0.3", + "@layerzerolabs/toolbox-foundry": "~0.1.12", + "@layerzerolabs/toolbox-hardhat": "~0.6.11", + "@layerzerolabs/ua-devtools": "~4.0.0", + "@layerzerolabs/ua-devtools-evm": "~6.0.0", + "@layerzerolabs/ua-devtools-evm-hardhat": "~8.0.0", + "@layerzerolabs/ua-devtools-solana": "~7.0.0", + "@metaplex-foundation/mpl-toolbox": "^0.9.4", + "@metaplex-foundation/umi": "^0.9.2", + "@metaplex-foundation/umi-bundle-defaults": "^0.9.2", + "@metaplex-foundation/umi-eddsa-web3js": "^0.9.2", + "@metaplex-foundation/umi-program-repository": "^0.9.2", + "@metaplex-foundation/umi-public-keys": "^0.8.9", + "@metaplex-foundation/umi-web3js-adapters": "^0.9.2", + "@nomicfoundation/hardhat-ethers": "^3.0.5", + "@nomiclabs/hardhat-ethers": "^2.2.3", + "@openzeppelin/contracts": "^5.0.2", + "@openzeppelin/contracts-upgradeable": "^5.0.2", + "@rushstack/eslint-patch": "^1.7.0", + "@solana-developers/helpers": "~2.8.1", + "@solana/web3.js": "~1.95.8", + "@types/chai": "^4.3.11", + "@types/mocha": "^10.0.6", + "@types/node": "~18.18.14", + "bs58": "^6.0.0", + "chai": "^4.4.1", + "concurrently": "~9.1.0", + "dotenv": "^16.4.5", + "eslint": "^8.55.0", + "eslint-plugin-jest-extended": "~2.0.0", + "ethers": "^5.7.2", + "exponential-backoff": "~3.1.1", + "hardhat": "^2.22.10", + "hardhat-contract-sizer": "^2.10.0", + "hardhat-deploy": "^0.12.1", + "hardhat-deploy-ethers": "^0.4.2", + "mocha": "^10.2.0", + "p-memoize": "~4.0.4", + "prettier": "^3.2.5", + "solhint": "^4.1.1", + "solidity-bytes-utils": "^0.8.2", + "ts-node": "^10.9.2", + "typescript": "^5.4.4" + }, + "engines": { + "node": ">=18.16.0" + }, + "pnpm": { + "overrides": { + "ethers": "^5.7.2", + "hardhat-deploy": "^0.12.1" + } + }, + "overrides": { + "ethers": "^5.7.2", + "hardhat-deploy": "^0.12.1" + } +} diff --git a/examples/oapp-solana/programs/my_oapp/Cargo.toml b/examples/oapp-solana/programs/my_oapp/Cargo.toml new file mode 100644 index 0000000000..e5b8460491 --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "my_oapp" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "my_oapp" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] +idl-build = ["oapp/idl-build"] + +[dependencies] +anchor-lang = { version = "0.29.0", features = ["init-if-needed"] } +solana-helper = "0.1.0" +oapp = { git = "https://github.com/LayerZero-Labs/LayerZero-v2.git", rev= "34321ac15e47e0dafd25d66659e2f3d1b9b6db8f" } \ No newline at end of file diff --git a/examples/oapp-solana/programs/my_oapp/Xargo.toml b/examples/oapp-solana/programs/my_oapp/Xargo.toml new file mode 100644 index 0000000000..475fb71ed1 --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/examples/oapp-solana/programs/my_oapp/build.rs b/examples/oapp-solana/programs/my_oapp/build.rs new file mode 100644 index 0000000000..3f1b4c93ea --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rerun-if-env-changed=MYOAPP_ID"); +} diff --git a/examples/oapp-solana/programs/my_oapp/src/errors.rs b/examples/oapp-solana/programs/my_oapp/src/errors.rs new file mode 100644 index 0000000000..848b7c2515 --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/src/errors.rs @@ -0,0 +1,6 @@ +use anchor_lang::prelude::error_code; + +#[error_code] +pub enum MyOAppError { + InvalidMessageType +} diff --git a/examples/oapp-solana/programs/my_oapp/src/instructions/init_store.rs b/examples/oapp-solana/programs/my_oapp/src/instructions/init_store.rs new file mode 100644 index 0000000000..7fa831edf7 --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/src/instructions/init_store.rs @@ -0,0 +1,65 @@ +use crate::*; +use oapp::endpoint::{instructions::RegisterOAppParams, ID as ENDPOINT_ID}; + +#[derive(Accounts)] +#[instruction(params: InitStoreParams)] +pub struct InitStore<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + init, + payer = payer, + space = Store::SIZE, + seeds = [STORE_SEED], // You can namespace this further if your program manages multiple stores. + // e.g. If there can be a store for each user, you can use something like: + // seeds = [STORE_SEED, &user.key().as_ref()] + bump + )] + pub store: Account<'info, Store>, + #[account( + init, + payer = payer, + space = LzReceiveTypesAccounts::SIZE, + seeds = [LZ_RECEIVE_TYPES_SEED, &store.key().to_bytes()], + bump + )] + pub lz_receive_types_accounts: Account<'info, LzReceiveTypesAccounts>, + pub system_program: Program<'info, System>, +} + +// NOTE: This example init_store may be front-run. It can be called by anyone, and can only be called once. +// The first caller will be able to set the admin and endpoint program for the store. If front-runned, the program will need to be redeployed and re-initialized with the correct parameters. +// You should modify this instruction accordingly for your use case with the appropriate access control and checks. +impl InitStore<'_> { + pub fn apply(ctx: &mut Context, params: &InitStoreParams) -> Result<()> { + ctx.accounts.store.admin = params.admin; + ctx.accounts.store.bump = ctx.bumps.store; + ctx.accounts.store.endpoint_program = params.endpoint; + ctx.accounts.lz_receive_types_accounts.store = ctx.accounts.store.key(); + // the above lines are required for all OApp implementations + + // the line below is specific to this string-passing example + ctx.accounts.store.string = "Nothing received yet.".to_string(); + + // Prepare the delegate address for the OApp registration. + let register_params = RegisterOAppParams { delegate: ctx.accounts.store.admin }; + + // The Store PDA 'signs' CPI to the Endpoint program to register the OApp. + let seeds: &[&[u8]] = &[STORE_SEED, &[ctx.accounts.store.bump]]; + oapp::endpoint_cpi::register_oapp( + ENDPOINT_ID, + ctx.accounts.store.key(), + ctx.remaining_accounts, + seeds, + register_params, + )?; + + Ok(()) + } +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct InitStoreParams { + pub admin: Pubkey, + pub endpoint: Pubkey, +} diff --git a/examples/oapp-solana/programs/my_oapp/src/instructions/lz_receive.rs b/examples/oapp-solana/programs/my_oapp/src/instructions/lz_receive.rs new file mode 100644 index 0000000000..41552f85f3 --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/src/instructions/lz_receive.rs @@ -0,0 +1,63 @@ +use crate::*; +use anchor_lang::prelude::*; +use oapp::{ + endpoint::{ + cpi::accounts::Clear, + instructions::ClearParams, + ConstructCPIContext, ID as ENDPOINT_ID, + }, + LzReceiveParams, +}; + +#[derive(Accounts)] +#[instruction(params: LzReceiveParams)] +pub struct LzReceive<'info> { + /// OApp Store PDA. This account represents the "address" of your OApp on + /// Solana and can contain any state relevant to your application. + /// Customize the fields in `Store` as needed. + #[account(mut, seeds = [STORE_SEED], bump = store.bump)] + pub store: Account<'info, Store>, + /// Peer config PDA for the sending chain. Ensures `params.sender` can only be the allowed peer from that remote chain. + #[account( + seeds = [PEER_SEED, &store.key().to_bytes(), ¶ms.src_eid.to_be_bytes()], + bump = peer.bump, + constraint = params.sender == peer.peer_address + )] + pub peer: Account<'info, PeerConfig> +} + +impl LzReceive<'_> { + pub fn apply(ctx: &mut Context, params: &LzReceiveParams) -> Result<()> { + // The OApp Store PDA is used to sign the CPI to the Endpoint program. + let seeds: &[&[u8]] = &[STORE_SEED, &[ctx.accounts.store.bump]]; + + // The first Clear::MIN_ACCOUNTS_LEN accounts were returned by + // `lz_receive_types` and are required for Endpoint::clear + let accounts_for_clear = &ctx.remaining_accounts[0..Clear::MIN_ACCOUNTS_LEN]; + // Call the Endpoint::clear CPI to clear the message from the Endpoint program. + // This is necessary to ensure the message is processed only once and to + // prevent replays. + let _ = oapp::endpoint_cpi::clear( + ENDPOINT_ID, + ctx.accounts.store.key(), + accounts_for_clear, + seeds, + ClearParams { + receiver: ctx.accounts.store.key(), + src_eid: params.src_eid, + sender: params.sender, + nonce: params.nonce, + guid: params.guid, + message: params.message.clone(), + }, + )?; + + // From here on, you can process the message as needed by your use case. + let string_value = msg_codec::decode(¶ms.message)?; + let store = &mut ctx.accounts.store; + store.string = string_value; + + Ok(()) + } +} + diff --git a/examples/oapp-solana/programs/my_oapp/src/instructions/lz_receive_types.rs b/examples/oapp-solana/programs/my_oapp/src/instructions/lz_receive_types.rs new file mode 100644 index 0000000000..adcaffbc65 --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/src/instructions/lz_receive_types.rs @@ -0,0 +1,52 @@ +use crate::*; +use oapp::endpoint_cpi::{get_accounts_for_clear, LzAccount}; +use oapp::{endpoint::ID as ENDPOINT_ID, LzReceiveParams}; + +/// `lz_receive_types` is queried off-chain by the Executor before calling +/// `lz_receive`. It must return **every** account that will be touched by the +/// actual `lz_receive` instruction as well as the accounts required by +/// `Endpoint::clear`. +/// +/// The return order must match exactly what `lz_receive` expects or the +/// cross-program invocation will fail. +#[derive(Accounts)] +pub struct LzReceiveTypes<'info> { + #[account(seeds = [STORE_SEED], bump = store.bump)] + pub store: Account<'info, Store>, +} + +impl LzReceiveTypes<'_> { + pub fn apply( + ctx: &Context, + params: &LzReceiveParams, + ) -> Result> { + // 1. The store PDA is always the first account and is mutable. If your + // program derives the store PDA with additional seeds, ensure the same + // seeds are used when providing the store account. + let store = ctx.accounts.store.key(); + + // 2. The peer PDA for the remote chain needs to be retrieved, for later verification of the `params.sender`. + let peer_seeds = [PEER_SEED, &store.to_bytes(), ¶ms.src_eid.to_be_bytes()]; + let (peer, _) = Pubkey::find_program_address(&peer_seeds, ctx.program_id); + + // Accounts used directly by `lz_receive` + let mut accounts = vec![ + // store (mutable) + LzAccount { pubkey: store, is_signer: false, is_writable: true }, + // peer (read-only) + LzAccount { pubkey: peer, is_signer: false, is_writable: false } + ]; + + // Append the additional accounts required for `Endpoint::clear` + let accounts_for_clear = get_accounts_for_clear( + ENDPOINT_ID, + &store, + params.src_eid, + ¶ms.sender, + params.nonce, + ); + accounts.extend(accounts_for_clear); + + Ok(accounts) + } +} diff --git a/examples/oapp-solana/programs/my_oapp/src/instructions/mod.rs b/examples/oapp-solana/programs/my_oapp/src/instructions/mod.rs new file mode 100644 index 0000000000..a09baf4145 --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/src/instructions/mod.rs @@ -0,0 +1,14 @@ +pub mod send; +pub mod init_store; +pub mod lz_receive; +pub mod lz_receive_types; +pub mod quote_send; +pub mod set_peer_config; + + +pub use send::*; +pub use init_store::*; +pub use lz_receive::*; +pub use lz_receive_types::*; +pub use quote_send::*; +pub use set_peer_config::*; diff --git a/examples/oapp-solana/programs/my_oapp/src/instructions/quote_send.rs b/examples/oapp-solana/programs/my_oapp/src/instructions/quote_send.rs new file mode 100644 index 0000000000..7c0af2f14c --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/src/instructions/quote_send.rs @@ -0,0 +1,55 @@ +use crate::*; +use anchor_lang::prelude::*; +use oapp::endpoint::{ + instructions::QuoteParams, state::EndpointSettings, ENDPOINT_SEED, + ID as ENDPOINT_ID, +}; + +#[derive(Accounts)] +#[instruction(params: QuoteSendParams)] +pub struct QuoteSend<'info> { + #[account(seeds = [STORE_SEED], bump = store.bump)] + pub store: Account<'info, Store>, + #[account( + seeds = [ + PEER_SEED, + store.key().as_ref(), + ¶ms.dst_eid.to_be_bytes() + ], + bump = peer.bump + )] + pub peer: Account<'info, PeerConfig>, + #[account(seeds = [ENDPOINT_SEED], bump = endpoint.bump, seeds::program = ENDPOINT_ID)] + pub endpoint: Account<'info, EndpointSettings>, +} + +impl<'info> QuoteSend<'info> { + pub fn apply(ctx: &Context, params: &QuoteSendParams) -> Result { + // Encode the payload for quoting + let message = msg_codec::encode(¶ms.message); + + // Ask the Endpoint how much a send would cost + let quote_params = QuoteParams { + sender: ctx.accounts.store.key(), + dst_eid: params.dst_eid, + receiver: params.receiver, + message, + pay_in_lz_token: params.pay_in_lz_token, + options: ctx + .accounts + .peer + .enforced_options + .combine_options(&None::>,¶ms.options)?, + }; + oapp::endpoint_cpi::quote(ENDPOINT_ID, ctx.remaining_accounts, quote_params) + } +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct QuoteSendParams { + pub dst_eid: u32, + pub receiver: [u8; 32], + pub message: String, + pub options: Vec, + pub pay_in_lz_token: bool, +} diff --git a/examples/oapp-solana/programs/my_oapp/src/instructions/send.rs b/examples/oapp-solana/programs/my_oapp/src/instructions/send.rs new file mode 100644 index 0000000000..9c428245e9 --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/src/instructions/send.rs @@ -0,0 +1,66 @@ +use crate::*; +use anchor_lang::prelude::*; +use oapp::endpoint::{ + instructions::SendParams, state::EndpointSettings, ENDPOINT_SEED, ID as ENDPOINT_ID, +}; + +#[derive(Accounts)] +#[instruction(params: SendMessageParams)] +pub struct Send<'info> { + #[account( + seeds = [ + PEER_SEED, + &store.key().to_bytes(), + ¶ms.dst_eid.to_be_bytes() + ], + bump = peer.bump + )] + /// Configuration for the destination chain. Holds the peer address and any + /// enforced messaging options. + pub peer: Account<'info, PeerConfig>, + #[account(seeds = [STORE_SEED], bump = store.bump)] + /// OApp Store PDA that signs the send instruction + pub store: Account<'info, Store>, + #[account(seeds = [ENDPOINT_SEED], bump = endpoint.bump, seeds::program = ENDPOINT_ID)] + pub endpoint: Account<'info, EndpointSettings>, +} +impl<'info> Send<'info> { + pub fn apply(ctx: &mut Context, params: &SendMessageParams) -> Result<()> { + // Serialize the message according to our codec + let message = msg_codec::encode(¶ms.message); + // Prepare the seeds for the OApp Store PDA, which is used to sign the CPI call to the Endpoint program. + let seeds: &[&[u8]] = &[STORE_SEED, &[ctx.accounts.store.bump]]; + + // Prepare the SendParams for the Endpoint::send CPI call. + let send_params = SendParams { + dst_eid: params.dst_eid, + receiver: ctx.accounts.peer.peer_address, + message, + options: ctx + .accounts + .peer + .enforced_options + .combine_options( &None::>, ¶ms.options)?, + native_fee: params.native_fee, + lz_token_fee: params.lz_token_fee, + }; + // Call the Endpoint::send CPI to send the message. + oapp::endpoint_cpi::send( + ENDPOINT_ID, + ctx.accounts.store.key(), // payer/signer derived from seeds + ctx.remaining_accounts, + seeds, + send_params, + )?; + Ok(()) + } +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct SendMessageParams { + pub dst_eid: u32, + pub message: String, + pub options: Vec, + pub native_fee: u64, + pub lz_token_fee: u64, +} diff --git a/examples/oapp-solana/programs/my_oapp/src/instructions/set_peer_config.rs b/examples/oapp-solana/programs/my_oapp/src/instructions/set_peer_config.rs new file mode 100644 index 0000000000..d5b7f5a164 --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/src/instructions/set_peer_config.rs @@ -0,0 +1,60 @@ +use crate::*; +use anchor_lang::prelude::*; + +// PeerConfig PDAs are used to store configuration for each remote chain +// For each remote chain, a PeerConfig PDA is created with the remote EID as part of the seed +// The PDA holds the peer address and any enforced options for messaging + +#[derive(Accounts)] +#[instruction(params: SetPeerConfigParams)] +pub struct SetPeerConfig<'info> { + #[account(mut, address = store.admin)] + /// Admin of the OApp store + pub admin: Signer<'info>, + #[account( + init_if_needed, + payer = admin, + space = PeerConfig::SIZE, + seeds = [PEER_SEED, &store.key().to_bytes(), ¶ms.remote_eid.to_be_bytes()], + bump + )] + /// Peer configuration PDA for a specific remote chain + pub peer: Account<'info, PeerConfig>, + #[account(seeds = [STORE_SEED], bump = store.bump)] + /// Store PDA of this OApp + pub store: Account<'info, Store>, + pub system_program: Program<'info, System>, +} + +impl SetPeerConfig<'_> { + pub fn apply(ctx: &mut Context, params: &SetPeerConfigParams) -> Result<()> { + // Update or create the peer config PDA + match params.config.clone() { + PeerConfigParam::PeerAddress(peer_address) => { + ctx.accounts.peer.peer_address = peer_address; + }, + PeerConfigParam::EnforcedOptions { send, send_and_call } => { + oapp::options::assert_type_3(&send)?; + ctx.accounts.peer.enforced_options.send = send; + oapp::options::assert_type_3(&send_and_call)?; + ctx.accounts.peer.enforced_options.send_and_call = send_and_call; + }, + } + // Store the PDA bump for later validation + ctx.accounts.peer.bump = ctx.bumps.peer; + Ok(()) + } +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct SetPeerConfigParams { + pub remote_eid: u32, + pub config: PeerConfigParam, +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub enum PeerConfigParam { + PeerAddress([u8; 32]), + /// Optionally enforce specific send options for this peer + EnforcedOptions { send: Vec, send_and_call: Vec }, +} \ No newline at end of file diff --git a/examples/oapp-solana/programs/my_oapp/src/lib.rs b/examples/oapp-solana/programs/my_oapp/src/lib.rs new file mode 100644 index 0000000000..1a72c9ec90 --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/src/lib.rs @@ -0,0 +1,68 @@ +mod errors; +mod instructions; +mod msg_codec; +mod state; + +use anchor_lang::prelude::*; +use instructions::*; +use oapp::{endpoint::MessagingFee, endpoint_cpi::LzAccount, LzReceiveParams}; +use solana_helper::program_id_from_env; +use state::*; + +// to build in verifiable mode and using environment variable (what the README instructs), run: +// anchor build -v -e MYOAPP_ID= +// to build in normal mode and using environment, run: +// MYOAPP_ID=$PROGRAM_ID anchor build +declare_id!(anchor_lang::solana_program::pubkey::Pubkey::new_from_array(program_id_from_env!( + "MYOAPP_ID", + "41NCdrEvXhQ4mZgyJkmqYxL6A1uEmnraGj31UJ6PsXd3" // It's not necessary to change the ID here if you are building using environment variable +))); + +const LZ_RECEIVE_TYPES_SEED: &[u8] = b"LzReceiveTypes"; // The Executor relies on this exact seed to derive the LzReceiveTypes PDA. Keep it the same. +const STORE_SEED: &[u8] = b"Store"; // You are free to edit this seed. +const PEER_SEED: &[u8] = b"Peer"; // The Executor relies on this exact seed to derive the LzReceiveTypes PDA. Keep it the same. + +#[program] +pub mod my_oapp { + use super::*; + + // ============================== Initializers ============================== + // In this example, init_store can be called by anyone and can be called only once. Ensure you implement your own access control logic if needed. + pub fn init_store(mut ctx: Context, params: InitStoreParams) -> Result<()> { + InitStore::apply(&mut ctx, ¶ms) + } + + // ============================== Admin ============================== + // admin instruction to set or update cross-chain peer configuration parameters. + pub fn set_peer_config( + mut ctx: Context, + params: SetPeerConfigParams, + ) -> Result<()> { + SetPeerConfig::apply(&mut ctx, ¶ms) + } + + // ============================== Public ============================== + // public instruction returning the estimated MessagingFee for sending a message. + pub fn quote_send(ctx: Context, params: QuoteSendParams) -> Result { + QuoteSend::apply(&ctx, ¶ms) + } + + // public instruction to send a message to a cross-chain peer. + pub fn send(mut ctx: Context, params: SendMessageParams) -> Result<()> { + Send::apply(&mut ctx, ¶ms) + } + + // handler for processing incoming cross-chain messages and executing the LzReceive logic + pub fn lz_receive(mut ctx: Context, params: LzReceiveParams) -> Result<()> { + LzReceive::apply(&mut ctx, ¶ms) + } + + // handler that returns the list of accounts required to execute lz_receive + pub fn lz_receive_types( + ctx: Context, + params: LzReceiveParams, + ) -> Result> { + LzReceiveTypes::apply(&ctx, ¶ms) + } + +} diff --git a/examples/oapp-solana/programs/my_oapp/src/msg_codec.rs b/examples/oapp-solana/programs/my_oapp/src/msg_codec.rs new file mode 100644 index 0000000000..8b03b0d0f8 --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/src/msg_codec.rs @@ -0,0 +1,90 @@ +use anchor_lang::prelude::error_code; +use std::str; + +// ----------------------------------------------------------------------------- +// This file defines how the example program encodes and decodes its messages. +// Each OApp can implement its own layout as long as the sending and receiving +// chains agree. Here we simply prefix a UTF-8 string with a 32 byte length +// header. In this example, the EVM-side equivalant is in `contracts/libs/StringMsgCodec.sol` +// ----------------------------------------------------------------------------- + + +// The message is a UTF-8 encoded string prefixed with a 32 byte header. +// The following is the layout of the message: +// Offset → +// 0 28 32 32+N +// |---------------------|------|----------------------------> +// | 28 bytes | 4B | N bytes | +// | zero padding | len | UTF-8 encoded string | +// |---------------------|------|----------------------------| + + +// We prefix the encoded string with a 32 byte length header. +pub const LENGTH_OFFSET: usize = 0; +pub const STRING_OFFSET: usize = 32; + +#[error_code] +pub enum MsgCodecError { + /// Buffer too short to even contain the 32‐byte length header + InvalidLength, + /// Header says "string is N bytes" but buffer < 32+N + BodyTooShort, + /// Payload bytes aren’t valid UTF-8 + InvalidUtf8, +} + +/// Extract the string length +fn decode_string_len(buf: &[u8]) -> Result { + // Header not long enough + if buf.len() < STRING_OFFSET { + return Err(MsgCodecError::InvalidLength); + } + let mut string_len_bytes = [0u8;32]; + string_len_bytes.copy_from_slice(&buf[LENGTH_OFFSET..LENGTH_OFFSET+32]); + // The length is stored in the last 4 bytes (big endian) + Ok(u32::from_be_bytes(string_len_bytes[28..32].try_into().unwrap()) as usize) +} + +// Encode a UTF-8 string into a message format with a 32 byte header +pub fn encode(string: &str) -> Vec { + let string_bytes = string.as_bytes(); + let mut msg = Vec::with_capacity( + STRING_OFFSET + // header length + string_bytes.len() // string bytes + ); + + // 4 byte length stored at the end of the 32 byte header + msg.extend(std::iter::repeat(0).take(28)); // padding + msg.extend_from_slice(&(string_bytes.len() as u32).to_be_bytes()); + + // string + msg.extend_from_slice(string_bytes); + + msg +} + +// Decode a message format with a 32 byte header into a UTF-8 string +// Returns an error if the message is malformed or not valid UTF-8 +pub fn decode(message: &[u8]) -> Result { + // Read the declared payload length from the header + let string_len = decode_string_len(message)?; + + let start = STRING_OFFSET; + // Safely compute end index and check for overflow + let end = start + .checked_add(string_len) + .ok_or(MsgCodecError::InvalidLength)?; + + // Ensure the buffer actually contains the full payload + if end > message.len() { + return Err(MsgCodecError::BodyTooShort); + } + + // Slice out the payload bytes + let payload = &message[start..end]; + // Attempt to convert the bytes into a Rust string + match str::from_utf8(payload) { + Ok(s) => Ok(s.to_string()), + Err(_) => Err(MsgCodecError::InvalidUtf8), + } +} diff --git a/examples/oapp-solana/programs/my_oapp/src/state/mod.rs b/examples/oapp-solana/programs/my_oapp/src/state/mod.rs new file mode 100644 index 0000000000..af6e6c8ee0 --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/src/state/mod.rs @@ -0,0 +1,5 @@ +pub mod store; +mod peer_config; + +pub use store::*; +pub use peer_config::*; diff --git a/examples/oapp-solana/programs/my_oapp/src/state/peer_config.rs b/examples/oapp-solana/programs/my_oapp/src/state/peer_config.rs new file mode 100644 index 0000000000..fb9d67b706 --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/src/state/peer_config.rs @@ -0,0 +1,42 @@ +use crate::*; + +pub const ENFORCED_OPTIONS_SEND_MAX_LEN: usize = 512; +pub const ENFORCED_OPTIONS_SEND_AND_CALL_MAX_LEN: usize = 1024; + +#[account] +pub struct PeerConfig { + pub peer_address: [u8; 32], + pub enforced_options: EnforcedOptions, + pub bump: u8, +} + +impl PeerConfig { + pub const SIZE: usize = 8 + std::mem::size_of::(); +} + +#[derive(Clone, Default, AnchorSerialize, AnchorDeserialize, InitSpace)] +pub struct EnforcedOptions { + #[max_len(ENFORCED_OPTIONS_SEND_MAX_LEN)] + pub send: Vec, + #[max_len(ENFORCED_OPTIONS_SEND_AND_CALL_MAX_LEN)] + pub send_and_call: Vec, +} + +impl EnforcedOptions { + pub fn get_enforced_options(&self, composed_msg: &Option>) -> Vec { + if composed_msg.is_none() { + self.send.clone() + } else { + self.send_and_call.clone() + } + } + + pub fn combine_options( + &self, + compose_msg: &Option>, + extra_options: &Vec, + ) -> Result> { + let enforced_options = self.get_enforced_options(compose_msg); + oapp::options::combine_options(enforced_options, extra_options) + } +} diff --git a/examples/oapp-solana/programs/my_oapp/src/state/store.rs b/examples/oapp-solana/programs/my_oapp/src/state/store.rs new file mode 100644 index 0000000000..89263751ec --- /dev/null +++ b/examples/oapp-solana/programs/my_oapp/src/state/store.rs @@ -0,0 +1,27 @@ +use crate::*; + +#[account] +pub struct Store { + pub admin: Pubkey, // This is required and should be consistent. + pub bump: u8, // This is required and should be consistent. + pub endpoint_program: Pubkey, // This is required and should be consistent. + pub string: String, // This is specific to this string-passing example. + // You can add more fields as needed for your OApp implementation. +} + +impl Store { + pub const MAX_STRING_LENGTH: usize = 256; + pub const SIZE: usize = 8 + std::mem::size_of::() + Self::MAX_STRING_LENGTH; +} + +// The LzReceiveTypesAccounts PDA is used by the Executor as a prerequisite to calling `lz_receive`. +#[account] +pub struct LzReceiveTypesAccounts { + pub store: Pubkey, // This is required and should be consistent. +} + +impl LzReceiveTypesAccounts { + pub const SIZE: usize = 8 + std::mem::size_of::(); +} + + diff --git a/examples/oapp-solana/rust-toolchain.toml b/examples/oapp-solana/rust-toolchain.toml new file mode 100644 index 0000000000..4dd8e5c567 --- /dev/null +++ b/examples/oapp-solana/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.75.0" \ No newline at end of file diff --git a/examples/oapp-solana/rustfmt.toml b/examples/oapp-solana/rustfmt.toml new file mode 100644 index 0000000000..e9f667f2e9 --- /dev/null +++ b/examples/oapp-solana/rustfmt.toml @@ -0,0 +1,23 @@ +# Basic +hard_tabs = false +max_width = 100 +use_small_heuristics = "Max" +# Imports +imports_granularity = "Preserve" +reorder_imports = true +# Consistency +newline_style = "Unix" +# Format comments +comment_width = 100 +wrap_comments = true +# Misc +chain_width = 80 +spaces_around_ranges = false +binop_separator = "Front" +reorder_impl_items = false +match_arm_leading_pipes = "Preserve" +match_arm_blocks = true +match_block_trailing_comma = true +trailing_comma = "Vertical" +trailing_semicolon = true +use_field_init_shorthand = true \ No newline at end of file diff --git a/examples/oapp-solana/solhint.config.js b/examples/oapp-solana/solhint.config.js new file mode 100644 index 0000000000..52efe629c0 --- /dev/null +++ b/examples/oapp-solana/solhint.config.js @@ -0,0 +1 @@ +module.exports = require('@layerzerolabs/solhint-config'); diff --git a/examples/oapp-solana/tasks/common/config.get.ts b/examples/oapp-solana/tasks/common/config.get.ts new file mode 100644 index 0000000000..9b2f3e411a --- /dev/null +++ b/examples/oapp-solana/tasks/common/config.get.ts @@ -0,0 +1,200 @@ +import { PublicKey } from '@solana/web3.js' +import { task } from 'hardhat/config' +import { ActionType } from 'hardhat/types' + +import { OmniPoint, createDefaultApplicative } from '@layerzerolabs/devtools' +import { createConnectedContractFactory, getNetworkNameForEid } from '@layerzerolabs/devtools-evm-hardhat' +import { createLogger, printCrossTable, setDefaultLogLevel } from '@layerzerolabs/io-devtools' +import { ChainType, EndpointId, endpointIdToChainType, getNetworkForChainId } from '@layerzerolabs/lz-definitions' +import { EndpointV2 } from '@layerzerolabs/protocol-devtools-solana' +import { OAppOmniGraph } from '@layerzerolabs/ua-devtools' +import { createOAppFactory } from '@layerzerolabs/ua-devtools-evm' +import { + OAppOmniGraphHardhatSchema, + SUBTASK_LZ_OAPP_CONFIG_LOAD, + type SubtaskLoadConfigTaskArgs, + TASK_LZ_OAPP_CONFIG_GET, +} from '@layerzerolabs/ua-devtools-evm-hardhat' +import { getReceiveConfig, getSendConfig } from '@layerzerolabs/ua-devtools-evm-hardhat' + +import { getSolanaReceiveConfig, getSolanaSendConfig } from './taskHelper' +import { createSolanaConnectionFactory } from './utils' + +interface TaskArgs { + logLevel?: string + oappConfig: string +} + +/** + * Helper function to determine if the point is Solana + * @param point {OmniPoint} + */ +const isSolana = (point: OmniPoint) => endpointIdToChainType(point.eid) === ChainType.SOLANA + +/** + * Helper function to get the hardhat.config.ts network name for a given endpoint id, or use the convention of + * networkName-environment for Solana. + * @param eid {EndpointId} + */ +const getEid = (eid: EndpointId) => { + switch (eid) { + // In the case of solana-testnet and solana-mainnet, we'll use the convention of networkName-environment + case EndpointId.SOLANA_V2_TESTNET: + case EndpointId.SOLANA_V2_MAINNET: { + const { chainName, env } = getNetworkForChainId(eid) + return `${chainName}-${env}` + } + default: + // For all other chains, we'll use the network name from hardhat.config.ts + return getNetworkNameForEid(eid) + } +} + +const action: ActionType = async ({ logLevel = 'info', oappConfig }, hre) => { + setDefaultLogLevel(logLevel) + const logger = createLogger(logLevel) + + const graph: OAppOmniGraph = await hre.run(SUBTASK_LZ_OAPP_CONFIG_LOAD, { + configPath: oappConfig, + schema: OAppOmniGraphHardhatSchema, + task: TASK_LZ_OAPP_CONFIG_GET, + } satisfies SubtaskLoadConfigTaskArgs) + + const evmSdkFactory = createOAppFactory(createConnectedContractFactory()) + const configs: Record> = {} + + // Iterate over the graph of connections not from Solana + const tasks = graph.connections + .filter(({ vector: { from } }) => !isSolana(from)) + .map(({ vector: { from, to } }) => async () => { + const endpointV2Sdk = await (await evmSdkFactory(from)).getEndpointSDK() + + // OApp User Set Config + const receiveCustomConfig = await getReceiveConfig(endpointV2Sdk, to.eid, from.address, true) + const sendCustomConfig = await getSendConfig(endpointV2Sdk, to.eid, from.address, true) + const [sendCustomLibrary, sendCustomUlnConfig, sendCustomExecutorConfig] = sendCustomConfig ?? [] + const [receiveCustomLibrary, receiveCustomUlnConfig] = receiveCustomConfig ?? [] + + // Default Config + const receiveDefaultConfig = await getReceiveConfig(endpointV2Sdk, to.eid) + const sendDefaultConfig = await getSendConfig(endpointV2Sdk, to.eid) + const [sendDefaultLibrary, sendDefaultUlnConfig, sendDefaultExecutorConfig] = sendDefaultConfig ?? [] + const [receiveDefaultLibrary, receiveDefaultUlnConfig] = receiveDefaultConfig ?? [] + + // OApp Config + const receiveOAppConfig = await getReceiveConfig(endpointV2Sdk, to.eid, from.address) + const sendOAppConfig = await getSendConfig(endpointV2Sdk, to.eid, from.address) + const [sendOAppLibrary, sendOAppUlnConfig, sendOAppExecutorConfig] = sendOAppConfig ?? [] + const [receiveOAppLibrary, receiveOAppUlnConfig] = receiveOAppConfig ?? [] + + const localNetworkName = getEid(from.eid) + const remoteNetworkName = getEid(to.eid) + + // Update the global state + configs[localNetworkName] = { + ...configs[localNetworkName], + [remoteNetworkName]: { + defaultSendLibrary: sendOAppLibrary, + defaultReceiveLibrary: receiveOAppLibrary, + sendUlnConfig: sendOAppUlnConfig, + sendExecutorConfig: sendOAppExecutorConfig, + receiveUlnConfig: receiveOAppUlnConfig, + }, + } + + console.log( + printCrossTable( + [ + { + localNetworkName, + remoteNetworkName, + sendLibrary: sendCustomLibrary, + receiveLibrary: receiveCustomLibrary, + sendUlnConfig: sendCustomUlnConfig, + sendExecutorConfig: sendCustomExecutorConfig, + receiveUlnConfig: receiveCustomUlnConfig, + }, + { + localNetworkName, + remoteNetworkName, + sendLibrary: sendDefaultLibrary, + receiveLibrary: receiveDefaultLibrary, + sendUlnConfig: sendDefaultUlnConfig, + sendExecutorConfig: sendDefaultExecutorConfig, + receiveUlnConfig: receiveDefaultUlnConfig, + }, + { + localNetworkName, + remoteNetworkName, + sendLibrary: sendOAppLibrary, + receiveLibrary: receiveOAppLibrary, + sendUlnConfig: sendOAppUlnConfig, + sendExecutorConfig: sendOAppExecutorConfig, + receiveUlnConfig: receiveOAppUlnConfig, + }, + ], + ['', 'Custom OApp Config', 'Default OApp Config', 'Active OApp Config'] + ) + ) + }) + // Iterate over the graph of connections from Solana + const solTasks = graph.connections + .filter(({ vector: { from } }) => isSolana(from)) + .map(({ vector: { from, to } }) => async () => { + const endpointV2Sdk = new EndpointV2( + await createSolanaConnectionFactory()(from.eid), + from, + new PublicKey(from.address) // doesn't matter as we are not sending transactions + ) + // OApp Config + const receiveOAppConfig = await getSolanaReceiveConfig(endpointV2Sdk, to.eid, from.address) + const sendOAppConfig = await getSolanaSendConfig(endpointV2Sdk, to.eid, from.address) + const [sendOAppLibrary, sendOAppUlnConfig, sendOAppExecutorConfig] = sendOAppConfig ?? [] + const [receiveOAppLibrary, receiveOAppUlnConfig] = receiveOAppConfig ?? [] + + const localNetworkName = getEid(from.eid) + const remoteNetworkName = getEid(to.eid) + + // Update the global state + configs[localNetworkName] = { + ...configs[localNetworkName], + [remoteNetworkName]: { + defaultSendLibrary: sendOAppLibrary, + defaultReceiveLibrary: receiveOAppLibrary, + sendUlnConfig: sendOAppUlnConfig, + sendExecutorConfig: sendOAppExecutorConfig, + receiveUlnConfig: receiveOAppUlnConfig, + }, + } + // Defaults are treated much differently in Solana, so we only output the active OApp config. + console.log( + printCrossTable( + [ + { + localNetworkName, + remoteNetworkName, + sendLibrary: sendOAppLibrary, + receiveLibrary: receiveOAppLibrary, + sendUlnConfig: sendOAppUlnConfig, + sendExecutorConfig: sendOAppExecutorConfig, + receiveUlnConfig: receiveOAppUlnConfig, + }, + ], + ['', 'Active OApp Config'] + ) + ) + }) + + // We allow this script to be executed either in parallel or in series + const applicative = createDefaultApplicative(logger) + await applicative(tasks) + await applicative(solTasks) + + return configs +} + +task( + TASK_LZ_OAPP_CONFIG_GET, + 'Outputs Custom OApp Config, Default OApp Config, and Active OApp Config. Each config contains Send & Receive Libraries, Send Uln & Executor Configs, and Receive Executor Configs', + action +) diff --git a/examples/oapp-solana/tasks/common/oappGet.ts b/examples/oapp-solana/tasks/common/oappGet.ts new file mode 100644 index 0000000000..c6ac7271c7 --- /dev/null +++ b/examples/oapp-solana/tasks/common/oappGet.ts @@ -0,0 +1,65 @@ +import { publicKey } from '@metaplex-foundation/umi' +import { task, types } from 'hardhat/config' +import { ActionType, HardhatRuntimeEnvironment } from 'hardhat/types' + +import { EndpointId } from '@layerzerolabs/lz-definitions' + +import { myoapp } from '../../lib/client' +import { deriveConnection, getSolanaDeployment } from '../solana' + +import { isEvmEid, isSolanaEid, listEidsInLzConfig } from './utils' + +interface Args { + contractName: string + oappConfig: string +} + +const action: ActionType = async ({ oappConfig, contractName }, hre: HardhatRuntimeEnvironment) => { + const eids = await listEidsInLzConfig(hre, oappConfig) + + console.log('\n🌐 LayerZero OApp States\n') + + for (const eid of eids) { + console.group(`→ endpoint ${eid}`) + + if (isSolanaEid(eid)) { + const solanaState = await fetchSolanaOappState(eid) + console.log('Solana OApp PDA:', solanaState._oappPda) + console.log('Solana OApp Data:', solanaState.string) + } else if (isEvmEid(eid)) { + const evm = await fetchEvmOappState(eid, hre, contractName) + console.log('EVM OApp Address:', evm.address) + console.log('EVM OApp Data:', evm.data) + } else { + console.log('Unknown endpoint type:', eid) + } + + console.groupEnd() + console.log() // blank line for spacing + } +} + +async function fetchEvmOappState(eid: EndpointId, hre: HardhatRuntimeEnvironment, contractName: string) { + const contract = await hre.ethers.getContract(contractName) + return { + address: contract.address, + // @ts-expect-error data method exists on MyOApp + data: await contract.data(), + } +} + +async function fetchSolanaOappState(eid: EndpointId) { + const { umi } = await deriveConnection(eid, true) + const deployment = getSolanaDeployment(eid) + const client = new myoapp.MyOApp(publicKey(deployment.programId)) + const [pda] = client.pda.oapp() + const store = await myoapp.accounts.fetchStore(umi, pda) + return { + _oappPda: pda.toString(), + ...store, + } +} + +task('lz:oapp:get', 'Fetch and pretty-print OApp state for each configured endpoint', action) + .addParam('oappConfig', 'The LZ config file to use', undefined, types.string, true) + .addOptionalParam('contractName', 'Name of the EVM OApp contract (default: MyOApp)', 'MyOApp', types.string) diff --git a/examples/oapp-solana/tasks/common/send.ts b/examples/oapp-solana/tasks/common/send.ts new file mode 100644 index 0000000000..178001ee9e --- /dev/null +++ b/examples/oapp-solana/tasks/common/send.ts @@ -0,0 +1,105 @@ +import { publicKey, transactionBuilder } from '@metaplex-foundation/umi' +import bs58 from 'bs58' +import { task, types } from 'hardhat/config' +import { ActionType, HardhatRuntimeEnvironment } from 'hardhat/types' + +import { ChainType, endpointIdToChainType } from '@layerzerolabs/lz-definitions' +import { Options } from '@layerzerolabs/lz-v2-utilities' + +import { myoapp } from '../../lib/client' +import { TransactionType, addComputeUnitInstructions, deriveConnection, getSolanaDeployment } from '../solana/index' +import { getLayerZeroScanLink, isV2Testnet } from '../utils' + +interface TaskArguments { + fromEid: number + dstEid: number + message: string + computeUnitPriceScaleFactor: number + contractName: string +} + +const action: ActionType = async ( + { fromEid, dstEid, message, computeUnitPriceScaleFactor, contractName }, + hre: HardhatRuntimeEnvironment +) => { + if (endpointIdToChainType(fromEid) === ChainType.SOLANA) { + await sendFromSolana(fromEid, dstEid, message, computeUnitPriceScaleFactor) + } else if (endpointIdToChainType(fromEid) === ChainType.EVM) { + await sendFromEvm(dstEid, message, contractName, hre) + } else { + throw new Error(`Unsupported ChainType for fromEid ${fromEid}`) + } +} + +async function sendFromSolana(fromEid: number, dstEid: number, message: string, computeUnitPriceScaleFactor: number) { + const solanaEid = fromEid + const solanaDeployment = getSolanaDeployment(solanaEid) + const { connection, umi, umiWalletSigner } = await deriveConnection(solanaEid) + + const myoappInstance: myoapp.MyOApp = new myoapp.MyOApp(publicKey(solanaDeployment.programId)) + + const options = Options.newOptions().toBytes() // leaving empty, relying on enforced options instead + + const { nativeFee } = await myoappInstance.quote(umi.rpc, umiWalletSigner.publicKey, { + dstEid, + message, + options, + payInLzToken: false, + }) + + console.log('🔖 Native fee quoted:', nativeFee.toString()) + + let txBuilder = transactionBuilder().add( + await myoappInstance.send(umi.rpc, umiWalletSigner.publicKey, { + dstEid, + message, + options, + nativeFee, + }) + ) + txBuilder = await addComputeUnitInstructions( + connection, + umi, + fromEid, + txBuilder, + umiWalletSigner, + computeUnitPriceScaleFactor, + TransactionType.SendMessage + ) + const tx = await txBuilder.sendAndConfirm(umi) + const txHash = bs58.encode(tx.signature) + + console.log('✉️ Cross-chain message:', `"${message}"`, '→ endpointId', dstEid) + console.log('🧾 Transaction hash:', txHash) + console.log('🌐 Track transfer:', getLayerZeroScanLink(txHash, isV2Testnet(dstEid))) +} + +async function sendFromEvm(dstEid: number, message: string, contractName: string, hre: HardhatRuntimeEnvironment) { + const signer = await hre.ethers.getNamedSigner('deployer') + + // @ts-expect-error signer is fine + const myOApp = (await hre.ethers.getContract(contractName)).connect(signer) + + const options = Options.newOptions().toHex().toString() // leaving empty, relying on enforced options instead + + const [nativeFee] = await myOApp.quote(dstEid, message, options, false) + + console.log('🔖 Native fee quoted:', nativeFee.toString()) + + const txResponse = await myOApp.send(dstEid, message, options, { + value: nativeFee, + }) + const txReceipt = await txResponse.wait() + + console.log('✉️ Cross-chain message:', `"${message}"`, '→ endpointId', dstEid) + console.log('🧾 Transaction hash:', txReceipt.transactionHash) + console.log('🌐 Track transfer:', getLayerZeroScanLink(txReceipt.transactionHash, isV2Testnet(dstEid))) +} + +// Note: for testing reference, Optimism Sepolia's eid is 40232 and Solana Devnet's eid is 40168 +task('lz:oapp:send', 'Sends a string message cross-chain', action) + .addParam('fromEid', 'Source endpoint ID', undefined, types.int, false) + .addParam('dstEid', 'Destination endpoint ID', undefined, types.int, false) + .addParam('message', 'String message to send', undefined, types.string, false) + .addParam('computeUnitPriceScaleFactor', 'The compute unit price scale factor', 4, types.float, true) // only if fromEid is Solana + .addOptionalParam('contractName', 'Name of the OApp contract in deployments folder', 'MyOApp', types.string) // only if fromEid is EVM diff --git a/examples/oapp-solana/tasks/common/taskHelper.ts b/examples/oapp-solana/tasks/common/taskHelper.ts new file mode 100644 index 0000000000..ca763a8fe6 --- /dev/null +++ b/examples/oapp-solana/tasks/common/taskHelper.ts @@ -0,0 +1,56 @@ +import { OmniAddress } from '@layerzerolabs/devtools' +import { EndpointId } from '@layerzerolabs/lz-definitions' +import { UlnProgram } from '@layerzerolabs/lz-solana-sdk-v2' +import { + IEndpointV2, + Timeout, + Uln302ConfigType, + Uln302ExecutorConfig, + Uln302UlnConfig, +} from '@layerzerolabs/protocol-devtools' + +/** + * Get the receive config for a Solana OApp + * @param endpointV2Sdk {IEndpointV2} SDK for the endpoint + * @param remoteEid {EndpointId} remote eid + * @param address {OmniAddress} address of the OApp + */ +export async function getSolanaReceiveConfig( + endpointV2Sdk: IEndpointV2, + remoteEid: EndpointId, + address: OmniAddress +): Promise<[OmniAddress, Uln302UlnConfig, Timeout] | undefined> { + const [receiveLibrary] = await endpointV2Sdk.getReceiveLibrary(address, remoteEid) + return [ + receiveLibrary ?? UlnProgram.PROGRAM_ADDRESS, + await endpointV2Sdk.getAppUlnConfig( + address, + UlnProgram.PROGRAM_ID.toBase58(), + remoteEid, + Uln302ConfigType.Receive + ), + { + lib: UlnProgram.PROGRAM_ID.toBase58(), + expiry: 0n, // unsupported for Solana + }, + ] +} + +/** + * Get the send config for a Solana OApp. + * @param endpointV2Sdk {IEndpointV2} SDK for the endpoint + * @param eid {EndpointId} remote eid + * @param address {OmniAddress} address of the OApp + */ +export async function getSolanaSendConfig( + endpointV2Sdk: IEndpointV2, + eid: EndpointId, + address: OmniAddress +): Promise<[OmniAddress, Uln302UlnConfig, Uln302ExecutorConfig] | undefined> { + const sendLibrary = (await endpointV2Sdk.getSendLibrary(address, eid)) ?? UlnProgram.PROGRAM_ADDRESS + return [ + sendLibrary, + await endpointV2Sdk.getAppUlnConfig(address, UlnProgram.PROGRAM_ID.toBase58(), eid, Uln302ConfigType.Send), + await endpointV2Sdk.getAppExecutorConfig(address, sendLibrary, eid), + ] +} diff --git a/examples/oapp-solana/tasks/common/types.ts b/examples/oapp-solana/tasks/common/types.ts new file mode 100644 index 0000000000..bf0b4bc870 --- /dev/null +++ b/examples/oapp-solana/tasks/common/types.ts @@ -0,0 +1,19 @@ +import { decode } from '@coral-xyz/anchor/dist/cjs/utils/bytes/bs58' +import { Keypair, PublicKey } from '@solana/web3.js' +import { CLIArgumentType } from 'hardhat/types' + +export const keyPair: CLIArgumentType = { + name: 'keyPair', + parse(name: string, value: string) { + return Keypair.fromSecretKey(decode(value)) + }, + validate() {}, +} + +export const publicKey: CLIArgumentType = { + name: 'keyPair', + parse(name: string, value: string) { + return new PublicKey(value) + }, + validate() {}, +} diff --git a/examples/oapp-solana/tasks/common/utils.ts b/examples/oapp-solana/tasks/common/utils.ts new file mode 100644 index 0000000000..a994d2bfb6 --- /dev/null +++ b/examples/oapp-solana/tasks/common/utils.ts @@ -0,0 +1,248 @@ +import assert from 'assert' + +import { Connection, Keypair, PublicKey } from '@solana/web3.js' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +import { + OmniPoint, + OmniSigner, + OmniTransactionReceipt, + OmniTransactionResponse, + firstFactory, + formatEid, +} from '@layerzerolabs/devtools' +import { createConnectedContractFactory } from '@layerzerolabs/devtools-evm-hardhat' +import { + OmniSignerSolana, + OmniSignerSolanaSquads, + createConnectionFactory, + createRpcUrlFactory, +} from '@layerzerolabs/devtools-solana' +import { ChainType, EndpointId, endpointIdToChainType } from '@layerzerolabs/lz-definitions' +import { UlnProgram } from '@layerzerolabs/lz-solana-sdk-v2' +import { Options } from '@layerzerolabs/lz-v2-utilities' +import { IOApp, OAppOmniGraph } from '@layerzerolabs/ua-devtools' +import { createOAppFactory } from '@layerzerolabs/ua-devtools-evm' +import { + OAppOmniGraphHardhatSchema, + SUBTASK_LZ_OAPP_CONFIG_LOAD, + SubtaskLoadConfigTaskArgs, + TASK_LZ_OAPP_CONFIG_GET, +} from '@layerzerolabs/ua-devtools-evm-hardhat' + +import { createSimpleOAppFactory } from '../../lib/factory' + +export const createSolanaConnectionFactory = () => + createConnectionFactory( + createRpcUrlFactory({ + [EndpointId.SOLANA_V2_MAINNET]: process.env.RPC_URL_SOLANA, + [EndpointId.SOLANA_V2_TESTNET]: process.env.RPC_URL_SOLANA_TESTNET, + }) + ) + +export const createSdkFactory = ( + userAccount: PublicKey, + programId: PublicKey, + connectionFactory = createSolanaConnectionFactory() +) => { + // To create a EVM/Solana SDK factory we need to merge the EVM and the Solana factories into one + // + // We do this by using the firstFactory helper function that is provided by the devtools package. + // This function will try to execute the factories one by one and return the first one that succeeds. + const evmSdkfactory = createOAppFactory(createConnectedContractFactory()) + const solanaSdkFactory = createSimpleOAppFactory( + // The first parameter to createOFTFactory is a user account factory + // + // This is a function that receives an OmniPoint ({ eid, address } object) + // and returns a user account to be used with that SDK. + // + // For our purposes this will always be the user account coming from the secret key passed in + () => userAccount, + // The second parameter is a program ID factory + // + // This is a function that receives an OmniPoint ({ eid, address } object) + // and returns a program ID to be used with that SDK. + // + // Since we only have one OFT deployed, this will always be the program ID passed as a CLI parameter. + // + // In situations where we might have multiple configs with OFTs using multiple program IDs, + // this function needs to decide which one to use. + () => programId, + // Last but not least the SDK will require a connection + connectionFactory + ) + + // We now "merge" the two SDK factories into one. + // + // We do this by using the firstFactory helper function that is provided by the devtools package. + // This function will try to execute the factories one by one and return the first one that succeeds. + return firstFactory<[OmniPoint], IOApp>(evmSdkfactory, solanaSdkFactory) +} + +export const createSolanaSignerFactory = ( + wallet: Keypair, + connectionFactory = createSolanaConnectionFactory(), + multisigKey?: PublicKey +) => { + return async (eid: EndpointId): Promise>> => { + assert( + endpointIdToChainType(eid) === ChainType.SOLANA, + `Solana signer factory can only create signers for Solana networks. Received ${formatEid(eid)}` + ) + + return multisigKey + ? new OmniSignerSolanaSquads(eid, await connectionFactory(eid), multisigKey, wallet) + : new OmniSignerSolana(eid, await connectionFactory(eid), wallet) + } +} + +export function uint8ArrayToHex(uint8Array: Uint8Array, prefix = false): string { + const hexString = Buffer.from(uint8Array).toString('hex') + return prefix ? `0x${hexString}` : hexString +} + +function formatBigIntForDisplay(n: bigint) { + return n.toLocaleString().replace(/,/g, '_') +} + +export function decodeLzReceiveOptions(hex: string): string { + try { + // Handle empty/undefined values first + if (!hex || hex === '0x') return 'No options set' + const options = Options.fromOptions(hex) + const lzReceiveOpt = options.decodeExecutorLzReceiveOption() + return lzReceiveOpt + ? `gas: ${formatBigIntForDisplay(lzReceiveOpt.gas)} , value: ${formatBigIntForDisplay(lzReceiveOpt.value)} wei` + : 'No executor options' + } catch (e) { + return `Invalid options (${hex.slice(0, 12)}...)` + } +} + +export async function getSolanaUlnConfigPDAs( + remote: EndpointId, + connection: Connection, + ulnAddress: PublicKey, + oappPda: PublicKey +) { + const uln = new UlnProgram.Uln(new PublicKey(ulnAddress)) + const sendConfig = uln.getSendConfigState(connection, new PublicKey(oappPda), remote) + + const receiveConfig = uln.getReceiveConfigState(connection, new PublicKey(oappPda), remote) + + return await Promise.all([sendConfig, receiveConfig]) +} + +export class DebugLogger { + static keyValue(key: string, value: any, indentLevel = 0) { + const indent = ' '.repeat(indentLevel * 2) + console.log(`${indent}\x1b[33m${key}:\x1b[0m ${value}`) + } + + static keyHeader(key: string, indentLevel = 0) { + const indent = ' '.repeat(indentLevel * 2) + console.log(`${indent}\x1b[33m${key}:\x1b[0m`) + } + + static header(text: string) { + console.log(`\x1b[36m${text}\x1b[0m`) + } + + static separator() { + console.log('\x1b[90m----------------------------------------\x1b[0m') + } + + /** + * Logs an error (in red) and corresponding fix suggestion (in blue). + * Uses the ERRORS_FIXES_MAP to retrieve text based on the known error type. + * + * @param type Required KnownErrors enum member + * @param errorMsg Optional string message to append to the error. + */ + static printErrorAndFixSuggestion(type: KnownErrors, errorMsg?: string) { + const fixInfo = ERRORS_FIXES_MAP[type] + if (!fixInfo) { + // Fallback if the error type is not recognized + console.log(`\x1b[31mError:\x1b[0m Unknown error type "${type}"`) + return + } + + // If errorMsg is specified, append it in parentheses + const errorOutput = errorMsg ? `${type}: (${errorMsg})` : type + + // Print the error type in red + console.log(`\x1b[31mError:\x1b[0m ${errorOutput}`) + + // Print the tip in green + console.log(`\x1b[32mFix suggestion:\x1b[0m ${fixInfo.tip}`) + + // Print the info in blue + if (fixInfo.info) { + console.log(`\x1b[34mElaboration:\x1b[0m ${fixInfo.info}`) + } + + // log empty line to separate error messages + console.log() + } +} + +export enum KnownErrors { + // variable name format: _ + // e.g. If the user forgets to deploy the OFT Program, the variable name should be: + // FIX_SUGGESTION_OFT_PROGRAM_NOT_DEPLOYED + ULN_INIT_CONFIG_SKIPPED = 'ULN_INIT_CONFIG_SKIPPED', + SOLANA_DEPLOYMENT_NOT_FOUND = 'SOLANA_DEPLOYMENT_NOT_FOUND', +} + +interface ErrorFixInfo { + tip: string + info?: string +} + +export const ERRORS_FIXES_MAP: Record = { + [KnownErrors.ULN_INIT_CONFIG_SKIPPED]: { + tip: 'Did you run `npx hardhat lz:oft:solana:init-config --oapp-config --solana-eid ` ?', + info: 'You must run lz:oft:solana:init-config once before you run lz:oapp:wire. If you have added new pathways, you must also run lz:oft:solana:init-config again.', + }, + // TODO: verify below (was copied from OFT) + [KnownErrors.SOLANA_DEPLOYMENT_NOT_FOUND]: { + tip: 'Did you run `npx hardhat lz:oapp:solana:create` ?', + info: 'The Solana deployment file is required to run config tasks. The default path is ./deployments/solana-/OApp.json', + }, +} + +export const listEidsInLzConfig = async (hre: HardhatRuntimeEnvironment, oappConfig: string): Promise => { + if (!oappConfig) throw new Error('Missing oappConfig') + + let graph: OAppOmniGraph + try { + graph = await hre.run(SUBTASK_LZ_OAPP_CONFIG_LOAD, { + configPath: oappConfig, + schema: OAppOmniGraphHardhatSchema, + task: TASK_LZ_OAPP_CONFIG_GET, + } satisfies SubtaskLoadConfigTaskArgs) + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to load OApp configuration: ${error.message}`) + } else { + throw new Error('Failed to load OApp configuration: Unknown error') + } + } + // init a Set + const eids = new Set() + // loop through the connections and add the eids to the Set + for (const { vector } of graph.connections) { + eids.add(vector.from.eid) + eids.add(vector.to.eid) + } + + return Array.from(eids) +} + +export function isSolanaEid(eid: EndpointId): boolean { + return endpointIdToChainType(eid) === ChainType.SOLANA +} + +export function isEvmEid(eid: EndpointId): boolean { + return endpointIdToChainType(eid) === ChainType.EVM +} diff --git a/examples/oapp-solana/tasks/common/wire.ts b/examples/oapp-solana/tasks/common/wire.ts new file mode 100644 index 0000000000..c76e27b3bd --- /dev/null +++ b/examples/oapp-solana/tasks/common/wire.ts @@ -0,0 +1,202 @@ +import { PublicKey } from '@solana/web3.js' +import { subtask, task } from 'hardhat/config' + +import { firstFactory } from '@layerzerolabs/devtools' +import { SUBTASK_LZ_SIGN_AND_SEND, types as devtoolsTypes } from '@layerzerolabs/devtools-evm-hardhat' +import { setTransactionSizeBuffer } from '@layerzerolabs/devtools-solana' +import { type LogLevel, createLogger } from '@layerzerolabs/io-devtools' +import { ChainType, endpointIdToChainType } from '@layerzerolabs/lz-definitions' +import { type IOApp, type OAppConfigurator, type OAppOmniGraph, configureOwnable } from '@layerzerolabs/ua-devtools' +import { + SUBTASK_LZ_OAPP_WIRE_CONFIGURE, + type SubtaskConfigureTaskArgs, + TASK_LZ_OAPP_WIRE, + TASK_LZ_OWNABLE_TRANSFER_OWNERSHIP, +} from '@layerzerolabs/ua-devtools-evm-hardhat' + +import { getSolanaDeployment, useWeb3Js } from '../solana' +import { findSolanaEndpointIdInGraph } from '../solana/utils' + +import { publicKey as publicKeyType } from './types' +import { + DebugLogger, + KnownErrors, + createSdkFactory, + createSolanaConnectionFactory, + createSolanaSignerFactory, + getSolanaUlnConfigPDAs, +} from './utils' + +import type { SignAndSendTaskArgs } from '@layerzerolabs/devtools-evm-hardhat/tasks' + +/** + * Additional CLI arguments for our custom wire task + */ +interface Args { + logLevel: LogLevel + multisigKey?: PublicKey + isSolanaInitConfig: boolean // For internal use only. This helps us to control which code runs depdending on whether the task ran is wire or init-config + oappConfig: string + internalConfigurator?: OAppConfigurator +} + +/** + * We extend the default wiring task to add functionality required by Solana + */ +task(TASK_LZ_OAPP_WIRE) + .addParam('multisigKey', 'The MultiSig key', undefined, publicKeyType, true) + // We use this argument to get around the fact that we want to both override the task action for the wiring task + // and wrap this task with custom configurators + // + // By default, this argument will be left empty and the default OApp configurator will be used. + // The tasks that are using custom configurators will override this argument with the configurator of their choice + .addParam('internalConfigurator', 'FOR INTERNAL USE ONLY', undefined, devtoolsTypes.fn, true) + .addParam('isSolanaInitConfig', 'FOR INTERNAL USE ONLY', undefined, devtoolsTypes.boolean, true) + .setAction(async (args: Args, hre, runSuper) => { + const logger = createLogger(args.logLevel) + + // + // + // ENVIRONMENT SETUP + // + // + + // The Solana transaction size estimation algorithm is not very accurate, so we increase its tolerance by 192 bytes + setTransactionSizeBuffer(192) + + // + // + // USER INPUT + // + // + + // construct the user's keypair via the SOLANA_PRIVATE_KEY env var + const keypair = (await useWeb3Js()).web3JsKeypair // note: this can be replaced with getSolanaKeypair() if we are okay to export that + const userAccount = keypair.publicKey + + const solanaEid = await findSolanaEndpointIdInGraph(hre, args.oappConfig) + const solanaDeployment = getSolanaDeployment(solanaEid) + + // Then we grab the programId from the args + const programId = new PublicKey(solanaDeployment.programId) + + // TODO: refactor to instead use a function such as verifySolanaDeployment that also checks for oapp key + if (!programId) { + logger.error('Missing programId in solana deployment') + return + } + const configurator = args.internalConfigurator + + // + // + // TOOLING SETUP + // + // + + // We'll need a connection factory to be able to query the Solana network + // + // If you haven't set RPC_URL_SOLANA and/or RPC_URL_SOLANA_TESTNET environment variables, + // the factory will use the default public RPC URLs + const connectionFactory = createSolanaConnectionFactory() + + // We'll need SDKs to be able to use devtools + const sdkFactory = createSdkFactory(userAccount, programId, connectionFactory) + + // We'll also need a signer factory + const solanaSignerFactory = createSolanaSignerFactory(keypair, connectionFactory, args.multisigKey) + + // + // + // SUBTASK OVERRIDES + // + // + + // We'll need to override the default implementation of the configure subtask + // (responsible for collecting the on-chain configuration of the contracts + // and coming up with the transactions that need to be sent to the network) + // + // The only thing we are overriding is the sdkFactory parameter - we supply the SDK factory we created above + subtask( + SUBTASK_LZ_OAPP_WIRE_CONFIGURE, + 'Configure OFT', + async (subtaskArgs: SubtaskConfigureTaskArgs, _hre, runSuper) => { + // start of pre-wiring checks. we only do this when the current task is wire. if the current task is init-config, we shouldn't run this. + if (!args.isSolanaInitConfig) { + logger.debug('Running pre-wiring checks...') + const { graph } = subtaskArgs + for (const connection of graph.connections) { + // check if from Solana Endpoint + if (endpointIdToChainType(connection.vector.from.eid) === ChainType.SOLANA) { + if (connection.config?.sendLibrary) { + // if from Solana Endpoint, ensure the PeerConfig account was already initialized + const solanaConnection = await connectionFactory(connection.vector.from.eid) + + const [sendConfig, receiveConfig] = await getSolanaUlnConfigPDAs( + connection.vector.to.eid, + solanaConnection, + new PublicKey(connection.config.sendLibrary), + new PublicKey(solanaDeployment.oapp) + ) + + if (sendConfig == null) { + DebugLogger.printErrorAndFixSuggestion( + KnownErrors.ULN_INIT_CONFIG_SKIPPED, + `SendConfig on ${connection.vector.from.eid} not initialized for remote ${connection.vector.to.eid}.` + ) + } + + if (receiveConfig == null) { + DebugLogger.printErrorAndFixSuggestion( + KnownErrors.ULN_INIT_CONFIG_SKIPPED, + `ReceiveConfig on ${connection.vector.from.eid} not initialized for remote ${connection.vector.to.eid}.` + ) + } + + if (sendConfig == null || receiveConfig == null) { + throw new Error('SendConfig or ReceiveConfig not initialized. ') + } + } else { + logger.debug( + `No sendLibrary found in connection config for ${connection.vector.from.eid} -> ${connection.vector.to.eid}` + ) + } + } + } + // end of pre-wiring checks + } + + return runSuper({ + ...subtaskArgs, + configurator: configurator ?? subtaskArgs.configurator, + sdkFactory, + }) + } + ) + + // We'll also need to override the default implementation of the signAndSend subtask + // (responsible for sending transactions to the network and waiting for confirmations) + // + // In this subtask we need to override the createSigner function so that it uses the Solana + // signer for all Solana transactions + subtask(SUBTASK_LZ_SIGN_AND_SEND, 'Sign OFT transactions', (args: SignAndSendTaskArgs, _hre, runSuper) => + runSuper({ + ...args, + createSigner: firstFactory(solanaSignerFactory, args.createSigner), + }) + ) + + return runSuper(args) + }) + +// We'll change the default ownership transfer task to use our wire implementation +// +// The reason for this is the fact that the ownership transfer task has a deficiency +// and that is the fact that it does not support a custom SDK factory as of yet +// +// The two tasks are identical and the only drawback of this approach is the fact +// that the logs will say "Wiring OApp" instead of "Transferring ownership" +task(TASK_LZ_OWNABLE_TRANSFER_OWNERSHIP) + .addParam('multisigKey', 'The MultiSig key', undefined, publicKeyType, true) + .setAction(async (args: Args, hre) => { + return hre.run(TASK_LZ_OAPP_WIRE, { ...args, internalConfigurator: configureOwnable }) + }) diff --git a/examples/oapp-solana/tasks/index.ts b/examples/oapp-solana/tasks/index.ts new file mode 100644 index 0000000000..54ffc46856 --- /dev/null +++ b/examples/oapp-solana/tasks/index.ts @@ -0,0 +1,7 @@ +import './common/wire' +import './common/send' +import './common/oappGet' +import './solana/oappCreate' +import './solana/initConfig' +import './solana/retryPayload' +import './solana/debug' diff --git a/examples/oapp-solana/tasks/solana/debug.ts b/examples/oapp-solana/tasks/solana/debug.ts new file mode 100644 index 0000000000..c28a9f46f2 --- /dev/null +++ b/examples/oapp-solana/tasks/solana/debug.ts @@ -0,0 +1,246 @@ +import { publicKey } from '@metaplex-foundation/umi' +import { toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters' +import { Keypair, PublicKey } from '@solana/web3.js' +import { task } from 'hardhat/config' + +import { OmniPoint, denormalizePeer } from '@layerzerolabs/devtools' +import { types } from '@layerzerolabs/devtools-evm-hardhat' +import { EndpointId, getNetworkForChainId } from '@layerzerolabs/lz-definitions' +import { EndpointPDADeriver, EndpointProgram } from '@layerzerolabs/lz-solana-sdk-v2' +import { EndpointV2 } from '@layerzerolabs/protocol-devtools-solana' + +import { MyOAppPDA, myoapp } from '../../lib/client' +import { getSolanaReceiveConfig, getSolanaSendConfig } from '../common/taskHelper' +import { DebugLogger, createSolanaConnectionFactory, decodeLzReceiveOptions, uint8ArrayToHex } from '../common/utils' + +import { deriveConnection, getSolanaOAppAddress } from './index' + +const DEBUG_ACTIONS = { + STORE: 'store', + GET_ADMIN: 'admin', + GET_DELEGATE: 'delegate', + CHECKS: 'checks', + GET_PEERS: 'peers', +} + +type DebugTaskArgs = { + eid: EndpointId + store?: string + endpoint: string + dstEids: EndpointId[] + action?: string +} + +const getStore = (eid: EndpointId, store?: string) => publicKey(store ?? getSolanaOAppAddress(eid)) + +task('lz:oapp:solana:debug', 'Prints OApp Store and Peer information') + .addParam( + 'eid', + 'Solana mainnet (30168) or testnet (40168). Defaults to mainnet.', + EndpointId.SOLANA_V2_MAINNET, + types.eid + ) + .addParam('store', 'The Store public key. Derived from deployments if not provided.', undefined, types.string, true) + .addParam('endpoint', 'The Endpoint public key', EndpointProgram.PROGRAM_ID.toBase58(), types.string) + .addOptionalParam('dstEids', 'Destination eids to check (comma-separated list)', [], types.csv) + .addOptionalParam( + 'action', + `The action to perform: ${Object.keys(DEBUG_ACTIONS).join(', ')} (defaults to all)`, + undefined, + types.string + ) + .setAction(async (taskArgs: DebugTaskArgs) => { + const { eid, store: storeArg, endpoint, dstEids, action } = taskArgs + const { umi, connection } = await deriveConnection(eid, true) + const store = getStore(eid, storeArg) + + let storeInfo + try { + storeInfo = await myoapp.accounts.fetchStore(umi, store) + } catch (e) { + console.error(`Failed to fetch Store at ${store.toString()}:`, e) + return + } + + const epDeriver = new EndpointPDADeriver(new PublicKey(endpoint)) + const [oAppRegistry] = epDeriver.oappRegistry(toWeb3JsPublicKey(store)) + const oAppRegistryInfo = await EndpointProgram.accounts.OAppRegistry.fromAccountAddress( + connection, + oAppRegistry + ) + + if (!oAppRegistryInfo) { + console.warn('OAppRegistry info not found.') + return + } + + const oappDeriver = new MyOAppPDA(storeInfo.header.owner) + + const printStore = async () => { + DebugLogger.header('Store Information') + DebugLogger.keyValue('Owner', storeInfo.header.owner) + DebugLogger.keyValue('Admin', storeInfo.admin) + DebugLogger.keyValue('Endpoint Program', storeInfo.endpointProgram) + DebugLogger.keyValue('String', storeInfo.string) + DebugLogger.separator() + } + + const printAdmin = async () => { + const admin = storeInfo.admin + DebugLogger.keyValue('Admin', admin) + } + + const printDelegate = async () => { + const delegate = oAppRegistryInfo?.delegate?.toBase58() + DebugLogger.header('OApp Registry Information') + DebugLogger.keyValue('Delegate', delegate) + DebugLogger.separator() + } + + const printChecks = async () => { + const delegate = oAppRegistryInfo?.delegate?.toBase58() + + DebugLogger.header('Checks') + DebugLogger.keyValue('Admin (Owner) same as Delegate', storeInfo.admin === delegate) + DebugLogger.separator() + } + + const printPeerConfigs = async () => { + const peerConfigs = dstEids.map((dstEid) => { + const peerConfig = oappDeriver.peer(dstEid) + return publicKey(peerConfig) + }) + const mockKeypair = new Keypair() + const point: OmniPoint = { + eid, + address: store.toString(), + } + const endpointV2Sdk = new EndpointV2( + await createSolanaConnectionFactory()(eid), + point, + mockKeypair.publicKey // doesn't matter as we are not sending transactions + ) + + DebugLogger.header('Peer Configurations') + + const peerConfigInfos = await myoapp.accounts.fetchAllPeerConfig(umi, peerConfigs) + for (let index = 0; index < dstEids.length; index++) { + const dstEid = dstEids[index] + const info = peerConfigInfos[index] + const network = getNetworkForChainId(dstEid) + const oAppReceiveConfig = await getSolanaReceiveConfig(endpointV2Sdk, dstEid, store) + const oAppSendConfig = await getSolanaSendConfig(endpointV2Sdk, dstEid, store) + + // Show the chain info + DebugLogger.header(`${dstEid} (${network.chainName})`) + + if (info) { + // Existing PeerConfig info + DebugLogger.keyValue('PeerConfig Account', peerConfigs[index].toString()) + DebugLogger.keyValue('Peer Address', denormalizePeer(info.peerAddress, dstEid)) + DebugLogger.keyHeader('Enforced Options') + DebugLogger.keyValue( + 'Send', + decodeLzReceiveOptions(uint8ArrayToHex(info.enforcedOptions.send, true)), + 2 + ) + DebugLogger.keyValue( + 'SendAndCall', + decodeLzReceiveOptions(uint8ArrayToHex(info.enforcedOptions.sendAndCall, true)), + 2 + ) + + printOAppReceiveConfigs(oAppReceiveConfig, network.chainName) + printOAppSendConfigs(oAppSendConfig, network.chainName) + } else { + // No PeerConfig account + console.log(`No PeerConfig account found for ${dstEid} (${network.chainName}).`) + } + + DebugLogger.separator() + } + } + if (action) { + switch (action) { + case DEBUG_ACTIONS.STORE: + await printStore() + break + case DEBUG_ACTIONS.GET_ADMIN: + await printAdmin() + break + case DEBUG_ACTIONS.GET_DELEGATE: + await printDelegate() + break + case DEBUG_ACTIONS.CHECKS: + await printChecks() + break + case DEBUG_ACTIONS.GET_PEERS: + await printPeerConfigs() + break + default: + console.error(`Invalid action specified. Use any of ${Object.keys(DEBUG_ACTIONS)}.`) + } + } else { + await printStore() + await printDelegate() + if (dstEids.length > 0) await printPeerConfigs() + await printChecks() + } + }) + +function printOAppReceiveConfigs( + oAppReceiveConfig: Awaited>, + peerChainName: string +) { + const oAppReceiveConfigIndexesToKeys: Record = { + 0: 'receiveLibrary', + 1: 'receiveUlnConfig', + 2: 'receiveLibraryTimeoutConfig', + } + + if (!oAppReceiveConfig) { + console.log('No receive configs found.') + return + } + + DebugLogger.keyValue(`Receive Configs (${peerChainName} to solana)`, '') + for (let i = 0; i < oAppReceiveConfig.length; i++) { + const item = oAppReceiveConfig[i] + if (typeof item === 'object' && item !== null) { + // Print each property in the object + DebugLogger.keyValue(`${oAppReceiveConfigIndexesToKeys[i]}`, '', 2) + for (const [propKey, propVal] of Object.entries(item)) { + DebugLogger.keyValue(`${propKey}`, String(propVal), 3) + } + } else { + // Print a primitive (string, number, etc.) + DebugLogger.keyValue(`${oAppReceiveConfigIndexesToKeys[i]}`, String(item), 2) + } + } +} + +function printOAppSendConfigs(oAppSendConfig: Awaited>, peerChainName: string) { + const sendOappConfigIndexesToKeys: Record = { + 0: 'sendLibrary', + 1: 'sendUlnConfig', + 2: 'sendExecutorConfig', + } + + if (!oAppSendConfig) { + console.log('No send configs found.') + return + } + + DebugLogger.keyValue(`Send Configs (solana to ${peerChainName})`, '') + for (let i = 0; i < oAppSendConfig.length; i++) { + const item = oAppSendConfig[i] + if (typeof item === 'object' && item !== null) { + DebugLogger.keyValue(`${sendOappConfigIndexesToKeys[i]}`, '', 2) + for (const [propKey, propVal] of Object.entries(item)) { + DebugLogger.keyValue(`${propKey}`, String(propVal), 3) + } + } else { + DebugLogger.keyValue(`${sendOappConfigIndexesToKeys[i]}`, String(item), 2) + } + } +} diff --git a/examples/oapp-solana/tasks/solana/index.ts b/examples/oapp-solana/tasks/solana/index.ts new file mode 100644 index 0000000000..c72a404005 --- /dev/null +++ b/examples/oapp-solana/tasks/solana/index.ts @@ -0,0 +1,361 @@ +import assert from 'assert' +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import path from 'node:path' + +import { + fetchAddressLookupTable, + mplToolbox, + setComputeUnitLimit, + setComputeUnitPrice, +} from '@metaplex-foundation/mpl-toolbox' +import { + AddressLookupTableInput, + EddsaInterface, + Instruction, + KeypairSigner, + PublicKey, + TransactionBuilder, + Umi, + createSignerFromKeypair, + publicKey, + signerIdentity, + transactionBuilder, +} from '@metaplex-foundation/umi' +import { createUmi } from '@metaplex-foundation/umi-bundle-defaults' +import { createWeb3JsEddsa } from '@metaplex-foundation/umi-eddsa-web3js' +import { toWeb3JsInstruction, toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters' +import { AddressLookupTableAccount, Connection, Keypair } from '@solana/web3.js' +import { getKeypairFromEnvironment, getKeypairFromFile, getSimulationComputeUnits } from '@solana-developers/helpers' +import { backOff } from 'exponential-backoff' + +import { formatEid } from '@layerzerolabs/devtools' +import { getPrioritizationFees } from '@layerzerolabs/devtools-solana' +import { createLogger, promptToContinue } from '@layerzerolabs/io-devtools' +import { EndpointId, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' + +import { MyOApp } from '../../lib/client/myoapp' +import { DebugLogger, KnownErrors, createSolanaConnectionFactory } from '../common/utils' + +const LOOKUP_TABLE_ADDRESS: Partial> = { + [EndpointId.SOLANA_V2_MAINNET]: publicKey('AokBxha6VMLLgf97B5VYHEtqztamWmYERBmmFvjuTzJB'), + [EndpointId.SOLANA_V2_TESTNET]: publicKey('9thqPdbR27A1yLWw2spwJLySemiGMXxPnEvfmXVk4KuK'), +} + +// create a safe version of getKeypairFromFile that returns undefined if the file does not exist, for checking the default keypair +async function safeGetKeypairDefaultPath(filePath?: string) { + try { + return await getKeypairFromFile(filePath) + } catch (error) { + // If the error is due to the file not existing, return undefined + if (error instanceof Error && error.message.includes('Could not read keypair')) { + return undefined + } + throw error // Rethrow if it's a different error + } +} + +// TODO in another PR: consider moving keypair related functions to tasks/solana/utils.ts +async function getSolanaKeypair(readOnly = false): Promise { + const logger = createLogger() + + // Early exit if read-only: ephemeral Keypair is enough. + if (readOnly) { + logger.info('Read-only mode: Using ephemeral (randomly generated) keypair.') + return Keypair.generate() + } + + // Attempt to load from each source + const keypairEnvPrivate = process.env.SOLANA_PRIVATE_KEY + ? getKeypairFromEnvironment('SOLANA_PRIVATE_KEY') + : undefined // #1 SOLANA_PRIVATE_KEY + const keypairEnvPath = process.env.SOLANA_KEYPAIR_PATH + ? await getKeypairFromFile(process.env.SOLANA_KEYPAIR_PATH) + : undefined // #2 SOLANA_KEYPAIR_PATH + const keypairDefaultPath = await safeGetKeypairDefaultPath() // #3 ~/.config/solana/id.json + + // Throw if no keypair is found via all 3 methods + if (!keypairEnvPrivate && !keypairEnvPath && !keypairDefaultPath) { + throw new Error( + 'No Solana keypair found. Provide SOLANA_PRIVATE_KEY, ' + + 'SOLANA_KEYPAIR_PATH, or place a valid keypair at ~/.config/solana/id.json.' + ) + } + + // If both environment-based keys exist, ensure they match + if (keypairEnvPrivate && keypairEnvPath) { + if (keypairEnvPrivate.publicKey.equals(keypairEnvPath.publicKey)) { + logger.info('Both SOLANA_PRIVATE_KEY and SOLANA_KEYPAIR_PATH match. Using environment-based keypair.') + return keypairEnvPrivate + } else { + throw new Error( + `Conflict: SOLANA_PRIVATE_KEY and SOLANA_KEYPAIR_PATH are different keypairs.\n` + + `Path: ${process.env.SOLANA_KEYPAIR_PATH} => ${keypairEnvPath.publicKey.toBase58()}\n` + + `Env : ${keypairEnvPrivate.publicKey.toBase58()}` + ) + } + } + + // If exactly one environment-based keypair is found, use it immediately + if (keypairEnvPrivate) { + logger.info(`Using Solana keypair from SOLANA_PRIVATE_KEY => ${keypairEnvPrivate.publicKey.toBase58()}`) + return keypairEnvPrivate + } + + if (keypairEnvPath) { + logger.info( + `Using Solana keypair from SOLANA_KEYPAIR_PATH (${process.env.SOLANA_KEYPAIR_PATH}) => ${keypairEnvPath.publicKey.toBase58()}` + ) + return keypairEnvPath + } + + // Otherwise, default path is the last fallback + logger.info( + `No environment-based keypair found. Found keypair at default path => ${keypairDefaultPath.publicKey.toBase58()}` + ) + const doContinue = await promptToContinue( + `Defaulting to ~/.config/solana/id.json with address ${keypairDefaultPath.publicKey.toBase58()}. Use this keypair?` + ) + if (!doContinue) process.exit(1) + + return keypairDefaultPath +} + +/** + * Derive common connection and UMI objects for a given endpoint ID. + * @param eid {EndpointId} + */ +export const deriveConnection = async (eid: EndpointId, readOnly = false) => { + const keypair = await getSolanaKeypair(readOnly) + const connectionFactory = createSolanaConnectionFactory() + const connection = await connectionFactory(eid) + const umi = createUmi(connection.rpcEndpoint).use(mplToolbox()) + const umiWalletKeyPair = umi.eddsa.createKeypairFromSecretKey(keypair.secretKey) + const umiWalletSigner = createSignerFromKeypair(umi, umiWalletKeyPair) + umi.use(signerIdentity(umiWalletSigner)) + return { + connection, + umi, + umiWalletKeyPair, + umiWalletSigner, + } +} + +export const useWeb3Js = async () => { + // note: if we are okay with exporting getSolanaKeypair, then useWeb3js can be removed + const keypair = await getSolanaKeypair() + return { + web3JsKeypair: keypair, + } +} + +/** + * Derive the keys needed for the OFT program. + * @param programIdStr {string} + */ +export const deriveKeys = (programIdStr: string) => { + const programId = publicKey(programIdStr) + const eddsa: EddsaInterface = createWeb3JsEddsa() + const myoappDeriver = new MyOApp(programId) + const lockBox = eddsa.generateKeypair() + const escrowPK = lockBox.publicKey + const [oappPda] = myoappDeriver.pda.oapp() + return { + programId, + lockBox, + escrowPK, + oappPda, + eddsa, + } +} + +/** + * Outputs the OFT accounts to a JSON file. + * @param eid {EndpointId} + * @param programId {string} + * @param oapp {string} + */ +export const saveSolanaDeployment = (eid: EndpointId, programId: string, oapp: string) => { + const outputDir = `./deployments/${endpointIdToNetwork(eid)}` + if (!existsSync(outputDir)) { + mkdirSync(outputDir, { recursive: true }) + } + writeFileSync( + `${outputDir}/OApp.json`, + JSON.stringify( + { + programId, + oapp, + }, + null, + 4 + ) + ) + console.log(`Accounts have been saved to ${outputDir}/OApp.json`) +} + +/** + * Reads the OFT deployment info from disk for the given endpoint ID. + * @param eid {EndpointId} + * @returns The contents of the OApp.json file as a JSON object. + */ +export const getSolanaDeployment = ( + eid: EndpointId +): { + programId: string + oapp: string +} => { + if (!eid) { + throw new Error('eid is required') + } + const outputDir = path.join('deployments', endpointIdToNetwork(eid)) + const filePath = path.join(outputDir, 'OApp.json') // Note: if you have multiple deployments, change this filename to refer to the desired deployment file + + if (!existsSync(filePath)) { + DebugLogger.printErrorAndFixSuggestion(KnownErrors.SOLANA_DEPLOYMENT_NOT_FOUND) + throw new Error(`Could not find Solana deployment file for eid ${eid} at: ${filePath}`) + } + + const fileContents = readFileSync(filePath, 'utf-8') + return JSON.parse(fileContents) +} + +export const getSolanaOAppAddress = (eid: EndpointId) => { + const { oapp } = getSolanaDeployment(eid) + if (!oapp) { + throw new Error('oapp not defined in the deployment file') + } + return oapp +} + +// TODO: move below outside of solana folder since it's generic +export const getLayerZeroScanLink = (hash: string, isTestnet = false) => + isTestnet ? `https://testnet.layerzeroscan.com/tx/${hash}` : `https://layerzeroscan.com/tx/${hash}` + +export const getExplorerTxLink = (hash: string, isTestnet = false) => + `https://solscan.io/tx/${hash}?cluster=${isTestnet ? 'devnet' : 'mainnet-beta'}` + +export const getAddressLookupTable = async (connection: Connection, umi: Umi, fromEid: EndpointId) => { + // Lookup Table Address and Priority Fee Calculation + const lookupTableAddress = LOOKUP_TABLE_ADDRESS[fromEid] + assert(lookupTableAddress != null, `No lookup table found for ${formatEid(fromEid)}`) + const addressLookupTableInput: AddressLookupTableInput = await fetchAddressLookupTable(umi, lookupTableAddress) + if (!addressLookupTableInput) { + throw new Error(`No address lookup table found for ${lookupTableAddress}`) + } + const { value: lookupTableAccount } = await connection.getAddressLookupTable(toWeb3JsPublicKey(lookupTableAddress)) + if (!lookupTableAccount) { + throw new Error(`No address lookup table account found for ${lookupTableAddress}`) + } + return { + lookupTableAddress, + addressLookupTableInput, + lookupTableAccount, + } +} + +export enum TransactionType { + CreateToken = 'CreateToken', + CreateMultisig = 'CreateMultisig', + InitOft = 'InitOft', + SetAuthority = 'SetAuthority', + InitConfig = 'InitConfig', + SendOFT = 'SendOFT', + SendMessage = 'SendMessage', +} + +const TransactionCuEstimates: Record = { + // for the sample values, they are: devnet, mainnet + [TransactionType.CreateToken]: 125_000, // actual sample: (59073, 123539), 55785 (more volatile as it has CPI to Metaplex) + [TransactionType.CreateMultisig]: 5_000, // actual sample: 3,230 + [TransactionType.InitOft]: 70_000, // actual sample: 59207, 65198 (note: this is the only transaction that createOFTAdapter does) + [TransactionType.SetAuthority]: 8_000, // actual sample: 6424, 6472 + [TransactionType.InitConfig]: 42_000, // actual sample: 33157, 40657 + [TransactionType.SendOFT]: 230_000, // actual sample: 217,784 + [TransactionType.SendMessage]: 230_000, // this is an estimate, not based on actual sample +} + +export const getComputeUnitPriceAndLimit = async ( + connection: Connection, + ixs: Instruction[], + wallet: KeypairSigner, + lookupTableAccount: AddressLookupTableAccount, + transactionType: TransactionType +) => { + const { averageFeeExcludingZeros } = await getPrioritizationFees(connection) + const priorityFee = Math.round(averageFeeExcludingZeros) + const computeUnitPrice = BigInt(priorityFee) + + let computeUnits + + try { + computeUnits = await backOff( + () => + getSimulationComputeUnits( + // @ts-expect-error complain about the type of connection, but it's good. cause: versions differing. + connection, + ixs.map((ix) => toWeb3JsInstruction(ix)), + toWeb3JsPublicKey(wallet.publicKey), + [lookupTableAccount] + ), + { + maxDelay: 10000, + numOfAttempts: 3, + } + ) + } catch (e) { + console.error(`Error retrieving simulations compute units from RPC:`, e) + const continueByUsingHardcodedEstimate = await promptToContinue( + 'Failed to call simulateTransaction on the RPC. This can happen when the network is congested. Would you like to use hardcoded estimates (TransactionCuEstimates) ? This may result in slightly overpaying for the transaction.' + ) + if (!continueByUsingHardcodedEstimate) { + throw new Error( + 'Failed to call simulateTransaction on the RPC and user chose to not continue with hardcoded estimate.' + ) + } + console.log( + `Falling back to hardcoded estimate for ${transactionType}: ${TransactionCuEstimates[transactionType]} CUs` + ) + computeUnits = TransactionCuEstimates[transactionType] + } + + if (!computeUnits) { + throw new Error('Unable to compute units') + } + + return { + computeUnitPrice, + computeUnits, + } +} + +export const addComputeUnitInstructions = async ( + connection: Connection, + umi: Umi, + eid: EndpointId, + txBuilder: TransactionBuilder, + umiWalletSigner: KeypairSigner, + computeUnitPriceScaleFactor: number, + transactionType: TransactionType +) => { + const computeUnitLimitScaleFactor = 1.1 // hardcoded to 1.1 as the estimations are not perfect and can fall slightly short of the actual CU usage on-chain + const { addressLookupTableInput, lookupTableAccount } = await getAddressLookupTable(connection, umi, eid) + const { computeUnitPrice, computeUnits } = await getComputeUnitPriceAndLimit( + connection, + txBuilder.getInstructions(), + umiWalletSigner, + lookupTableAccount, + transactionType + ) + // Since transaction builders are immutable, we must be careful to always assign the result of the add and prepend + // methods to a new variable. + const newTxBuilder = transactionBuilder() + .add( + setComputeUnitPrice(umi, { + microLamports: computeUnitPrice * BigInt(Math.floor(computeUnitPriceScaleFactor)), + }) + ) + .add(setComputeUnitLimit(umi, { units: computeUnits * computeUnitLimitScaleFactor })) + .setAddressLookupTables([addressLookupTableInput]) + .add(txBuilder) + return newTxBuilder +} diff --git a/examples/oapp-solana/tasks/solana/initConfig.ts b/examples/oapp-solana/tasks/solana/initConfig.ts new file mode 100644 index 0000000000..c1b01905a7 --- /dev/null +++ b/examples/oapp-solana/tasks/solana/initConfig.ts @@ -0,0 +1,32 @@ +import { PublicKey } from '@solana/web3.js' +import { ConfigurableTaskDefinition } from 'hardhat/types' + +import { inheritTask } from '@layerzerolabs/devtools-evm-hardhat' +import { type LogLevel } from '@layerzerolabs/io-devtools' +import { type OAppConfigurator } from '@layerzerolabs/ua-devtools' +import { TASK_LZ_OAPP_WIRE } from '@layerzerolabs/ua-devtools-evm-hardhat' + +import { initOAppAccounts } from '../../lib/config' + +// We'll create clones of the wire task and only override the configurator argument +const wireLikeTask = inheritTask(TASK_LZ_OAPP_WIRE) + +// TODO: export from wire.ts instead of re-declaring +/** + * Additional CLI arguments for our custom wire task + */ +interface Args { + logLevel: LogLevel + multisigKey?: PublicKey + internalConfigurator?: OAppConfigurator +} + +// This task will use the `initOFTAccounts` configurator that initializes the Solana accounts +const initConfigTask = wireLikeTask('lz:oapp:solana:init-config') as ConfigurableTaskDefinition + +// TODO: currently the message for 'already done' state is "OApp is already wired." which is misleading -> should be changed to "Pathway Config already initialized" +initConfigTask + .setDescription('Initialize OApp accounts for Solana') + .setAction(async (args: Args, hre) => + hre.run(TASK_LZ_OAPP_WIRE, { ...args, internalConfigurator: initOAppAccounts, isSolanaInitConfig: true }) + ) diff --git a/examples/oapp-solana/tasks/solana/oappCreate.ts b/examples/oapp-solana/tasks/solana/oappCreate.ts new file mode 100644 index 0000000000..7693ac0d9f --- /dev/null +++ b/examples/oapp-solana/tasks/solana/oappCreate.ts @@ -0,0 +1,36 @@ +import { publicKey, transactionBuilder } from '@metaplex-foundation/umi' +import bs58 from 'bs58' +import { task, types } from 'hardhat/config' +import { ActionType, HardhatRuntimeEnvironment } from 'hardhat/types' + +import { EndpointId } from '@layerzerolabs/lz-definitions' + +import { myoapp } from '../../lib/client' + +import { deriveConnection, getExplorerTxLink, saveSolanaDeployment } from '.' + +interface Args { + programId: string + /** + * The endpoint ID for the Solana network. + */ + eid: EndpointId +} + +const action: ActionType = async ({ programId, eid }, hre: HardhatRuntimeEnvironment) => { + // TODO: accept program ID as a param + + const isTestnet = eid == EndpointId.SOLANA_V2_TESTNET + + const myoappInstance: myoapp.MyOApp = new myoapp.MyOApp(publicKey(programId)) + const [oapp] = myoappInstance.pda.oapp() + const { umi, umiWalletSigner } = await deriveConnection(eid) + const txBuilder = transactionBuilder().add(myoappInstance.initStore(umiWalletSigner, umiWalletSigner.publicKey)) + const tx = await txBuilder.sendAndConfirm(umi) + console.log(`createTx: ${getExplorerTxLink(bs58.encode(tx.signature), isTestnet)}`) + saveSolanaDeployment(eid, programId, oapp) +} + +task('lz:oapp:solana:create', 'inits the oapp account', action) + .addParam('programId', 'The program ID of the OApp', undefined, types.string, false) + .addParam('eid', 'The endpoint ID for the Solana network.', undefined, types.int, false) diff --git a/examples/oapp-solana/tasks/solana/retryPayload.ts b/examples/oapp-solana/tasks/solana/retryPayload.ts new file mode 100644 index 0000000000..3cbd8d12f3 --- /dev/null +++ b/examples/oapp-solana/tasks/solana/retryPayload.ts @@ -0,0 +1,96 @@ +import { toWeb3JsKeypair } from '@metaplex-foundation/umi-web3js-adapters' +import { ComputeBudgetProgram, Keypair, Transaction, sendAndConfirmTransaction } from '@solana/web3.js' +import bs58 from 'bs58' +import { task } from 'hardhat/config' + +import { makeBytes32 } from '@layerzerolabs/devtools' +import { types } from '@layerzerolabs/devtools-evm-hardhat' +import { EndpointId } from '@layerzerolabs/lz-definitions' +import { lzReceive } from '@layerzerolabs/lz-solana-sdk-v2' + +import { deriveConnection, getExplorerTxLink } from './index' + +interface Args { + srcEid: EndpointId + nonce: bigint + sender: string + dstEid: EndpointId + receiver: string + guid: string + payload: string + computeUnits: number + lamports: number + withPriorityFee: number +} + +task('lz:oapp:solana:retry-payload', 'Retry a stored payload on Solana') + .addParam('srcEid', 'The source EndpointId', undefined, types.eid) + .addParam('nonce', 'The nonce of the payload', undefined, types.bigint) + .addParam('sender', 'The source OApp address (hex)', undefined, types.string) + .addParam('dstEid', 'The destination EndpointId (Solana chain)', undefined, types.eid) + .addParam('receiver', 'The receiver address on the destination Solana chain (bytes58)', undefined, types.string) + .addParam('guid', 'The GUID of the message (hex)', undefined, types.string) + .addParam('payload', 'The message payload (hex)', undefined, types.string) + .addParam('computeUnits', 'The CU for the lzReceive instruction', undefined, types.int) + .addParam('lamports', 'The lamports for the lzReceive instruction', undefined, types.int) + .addParam('withPriorityFee', 'The priority fee in microLamports', undefined, types.int) + .setAction( + async ({ + srcEid, + nonce, + sender, + dstEid, + receiver, + guid, + payload, + computeUnits, + lamports, + withPriorityFee, + }: Args) => { + if (!process.env.SOLANA_PRIVATE_KEY) { + throw new Error('SOLANA_PRIVATE_KEY is not defined in the environment variables.') + } + + const { connection, umiWalletKeyPair } = await deriveConnection(dstEid) + const signer = toWeb3JsKeypair(umiWalletKeyPair) + const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash() + const tx = new Transaction({ + feePayer: signer.publicKey, + blockhash, + lastValidBlockHeight, + }) + + const instruction = await lzReceive( + connection, + signer.publicKey, + { + nonce: nonce.toString(), + srcEid, + sender: makeBytes32(sender), + receiver, + guid, + message: payload, // referred to as "payload" in scan-api + }, + Uint8Array.from([computeUnits, lamports]), + 'confirmed' + ) + + if (withPriorityFee) { + tx.add( + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: withPriorityFee, + }) + ) + } + tx.add(instruction) + tx.recentBlockhash = blockhash + + const keypair = Keypair.fromSecretKey(bs58.decode(process.env.SOLANA_PRIVATE_KEY)) + tx.sign(keypair) + + const signature = await sendAndConfirmTransaction(connection, tx, [keypair]) + console.log( + `View Solana transaction here: ${getExplorerTxLink(signature.toString(), dstEid == EndpointId.SOLANA_V2_TESTNET)}` + ) + } + ) diff --git a/examples/oapp-solana/tasks/solana/utils.ts b/examples/oapp-solana/tasks/solana/utils.ts new file mode 100644 index 0000000000..9978784fca --- /dev/null +++ b/examples/oapp-solana/tasks/solana/utils.ts @@ -0,0 +1,51 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +import { ChainType, EndpointId, endpointIdToChainType } from '@layerzerolabs/lz-definitions' +import { OAppOmniGraph } from '@layerzerolabs/ua-devtools' +import { + OAppOmniGraphHardhatSchema, + SUBTASK_LZ_OAPP_CONFIG_LOAD, + SubtaskLoadConfigTaskArgs, + TASK_LZ_OAPP_CONFIG_GET, +} from '@layerzerolabs/ua-devtools-evm-hardhat' + +export const findSolanaEndpointIdInGraph = async ( + hre: HardhatRuntimeEnvironment, + oappConfig: string +): Promise => { + if (!oappConfig) throw new Error('Missing oappConfig') + + let graph: OAppOmniGraph + try { + graph = await hre.run(SUBTASK_LZ_OAPP_CONFIG_LOAD, { + configPath: oappConfig, + schema: OAppOmniGraphHardhatSchema, + task: TASK_LZ_OAPP_CONFIG_GET, + } satisfies SubtaskLoadConfigTaskArgs) + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to load OApp configuration: ${error.message}`) + } else { + throw new Error('Failed to load OApp configuration: Unknown error') + } + } + + let solanaEid: EndpointId | null = null + + const checkSolanaEndpoint = (eid: EndpointId) => { + if (endpointIdToChainType(eid) === ChainType.SOLANA) { + if (solanaEid && solanaEid !== eid) { + throw new Error(`Multiple Solana Endpoint IDs found: ${solanaEid}, ${eid}`) + } + solanaEid = eid + } + } + + for (const { vector } of graph.connections) { + checkSolanaEndpoint(vector.from.eid) + checkSolanaEndpoint(vector.to.eid) + if (solanaEid) return solanaEid + } + + throw new Error('No Solana Endpoint ID found. Ensure your OApp configuration includes a valid Solana endpoint.') +} diff --git a/examples/oapp-solana/tasks/utils.ts b/examples/oapp-solana/tasks/utils.ts new file mode 100644 index 0000000000..c0cd5a2583 --- /dev/null +++ b/examples/oapp-solana/tasks/utils.ts @@ -0,0 +1,6 @@ +import { EndpointId } from '@layerzerolabs/lz-definitions' + +export const getLayerZeroScanLink = (hash: string, isTestnet = false) => + isTestnet ? `https://testnet.layerzeroscan.com/tx/${hash}` : `https://layerzeroscan.com/tx/${hash}` + +export const isV2Testnet = (eid: EndpointId) => eid >= 10000 && eid.toString()[0] === '4' diff --git a/examples/oapp-solana/test/foundry/MyOApp.t.sol b/examples/oapp-solana/test/foundry/MyOApp.t.sol new file mode 100644 index 0000000000..fe60e68ec3 --- /dev/null +++ b/examples/oapp-solana/test/foundry/MyOApp.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +// MyOApp imports +import { MyOApp } from "../../contracts/MyOApp.sol"; + +// OApp imports +import { IOAppOptionsType3, EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; +import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; + +// OZ imports +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +// Forge imports +import "forge-std/console.sol"; + +// DevTools imports +import { TestHelperOz5 } from "@layerzerolabs/test-devtools-evm-foundry/contracts/TestHelperOz5.sol"; + +contract MyOAppTest is TestHelperOz5 { + using OptionsBuilder for bytes; + + uint32 private aEid = 1; + uint32 private bEid = 2; + + MyOApp private aOApp; + MyOApp private bOApp; + + address private userA = address(0x1); + address private userB = address(0x2); + uint256 private initialBalance = 100 ether; + + function setUp() public virtual override { + vm.deal(userA, 1000 ether); + vm.deal(userB, 1000 ether); + + super.setUp(); + setUpEndpoints(2, LibraryType.UltraLightNode); + + aOApp = MyOApp(_deployOApp(type(MyOApp).creationCode, abi.encode(address(endpoints[aEid]), address(this)))); + + bOApp = MyOApp(_deployOApp(type(MyOApp).creationCode, abi.encode(address(endpoints[bEid]), address(this)))); + + address[] memory oapps = new address[](2); + oapps[0] = address(aOApp); + oapps[1] = address(bOApp); + this.wireOApps(oapps); + } + + function test_constructor() public { + assertEq(aOApp.owner(), address(this)); + assertEq(bOApp.owner(), address(this)); + + assertEq(address(aOApp.endpoint()), address(endpoints[aEid])); + assertEq(address(bOApp.endpoint()), address(endpoints[bEid])); + } +} diff --git a/examples/oapp-solana/test/hardhat/MyOApp.test.ts b/examples/oapp-solana/test/hardhat/MyOApp.test.ts new file mode 100644 index 0000000000..7d24f80cc5 --- /dev/null +++ b/examples/oapp-solana/test/hardhat/MyOApp.test.ts @@ -0,0 +1,81 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { Contract, ContractFactory } from 'ethers' +import { deployments, ethers } from 'hardhat' + +import { Options } from '@layerzerolabs/lz-v2-utilities' + +describe('MyOApp Test', function () { + // Constant representing a mock Endpoint ID for testing purposes + const eidA = 1 + const eidB = 2 + // Declaration of variables to be used in the test suite + let MyOApp: ContractFactory + let EndpointV2Mock: ContractFactory + let ownerA: SignerWithAddress + let ownerB: SignerWithAddress + let endpointOwner: SignerWithAddress + let myOAppA: Contract + let myOAppB: Contract + let mockEndpointV2A: Contract + let mockEndpointV2B: Contract + + // Before hook for setup that runs once before all tests in the block + before(async function () { + // Contract factory for our tested contract + MyOApp = await ethers.getContractFactory('MyOApp') + + // Fetching the first three signers (accounts) from Hardhat's local Ethereum network + const signers = await ethers.getSigners() + + ;[ownerA, ownerB, endpointOwner] = signers + + // The EndpointV2Mock contract comes from @layerzerolabs/test-devtools-evm-hardhat package + // and its artifacts are connected as external artifacts to this project + // + // Unfortunately, hardhat itself does not yet provide a way of connecting external artifacts, + // so we rely on hardhat-deploy to create a ContractFactory for EndpointV2Mock + // + // See https://github.com/NomicFoundation/hardhat/issues/1040 + const EndpointV2MockArtifact = await deployments.getArtifact('EndpointV2Mock') + EndpointV2Mock = new ContractFactory(EndpointV2MockArtifact.abi, EndpointV2MockArtifact.bytecode, endpointOwner) + }) + + // beforeEach hook for setup that runs before each test in the block + beforeEach(async function () { + // Deploying a mock LZ EndpointV2 with the given Endpoint ID + mockEndpointV2A = await EndpointV2Mock.deploy(eidA) + mockEndpointV2B = await EndpointV2Mock.deploy(eidB) + + // Deploying two instances of MyOApp contract and linking them to the mock LZEndpoint + myOAppA = await MyOApp.deploy(mockEndpointV2A.address, ownerA.address) + myOAppB = await MyOApp.deploy(mockEndpointV2B.address, ownerB.address) + + // Setting destination endpoints in the LZEndpoint mock for each MyOApp instance + await mockEndpointV2A.setDestLzEndpoint(myOAppB.address, mockEndpointV2B.address) + await mockEndpointV2B.setDestLzEndpoint(myOAppA.address, mockEndpointV2A.address) + + // Setting each MyOApp instance as a peer of the other + await myOAppA.connect(ownerA).setPeer(eidB, ethers.utils.zeroPad(myOAppB.address, 32)) + await myOAppB.connect(ownerB).setPeer(eidA, ethers.utils.zeroPad(myOAppA.address, 32)) + }) + + // A test case to verify message sending functionality + it('should send a message to each destination OApp', async function () { + // Assert initial state of data in both MyOApp instances + expect(await myOAppA.data()).to.equal('Nothing received yet.') + expect(await myOAppB.data()).to.equal('Nothing received yet.') + const options = Options.newOptions().addExecutorLzReceiveOption(200000, 0).toHex().toString() + + // Define native fee and quote for the message send operation + let nativeFee = 0 + ;[nativeFee] = await myOAppA.quote(eidB, 'Test message.', options, false) + + // Execute send operation from myOAppA + await myOAppA.send(eidB, 'Test message.', options, { value: nativeFee.toString() }) + + // Assert the resulting state of data in both MyOApp instances + expect(await myOAppA.data()).to.equal('Nothing received yet.') + expect(await myOAppB.data()).to.equal('Test message.') + }) +}) diff --git a/examples/oapp-solana/tsconfig.json b/examples/oapp-solana/tsconfig.json new file mode 100644 index 0000000000..ed5b3c2dac --- /dev/null +++ b/examples/oapp-solana/tsconfig.json @@ -0,0 +1,24 @@ +{ + "exclude": ["node_modules"], + "include": [ + "deploy", + "test", + "tasks", + "hardhat.config.ts", + "lib/client", + "lib/config.ts", + "lib/factory.ts", + "lib/sdk.ts" + ], + "compilerOptions": { + "experimentalDecorators": true, // this can be removed after devtools-mock is removed + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": ["node", "mocha"] + } +} diff --git a/packages/create-lz-oapp/src/config.ts b/packages/create-lz-oapp/src/config.ts index 1f9227ae94..009f551b5f 100644 --- a/packages/create-lz-oapp/src/config.ts +++ b/packages/create-lz-oapp/src/config.ts @@ -148,6 +148,17 @@ export const getExamples = (): Example[] => { }, ] : []), + ...(process.env.LZ_ENABLE_SOLANA_OAPP_EXAMPLE + ? [ + { + id: 'oapp-solana', + label: 'OApp (Solana)', + repository, + directory: 'examples/oapp-solana', + ref, + }, + ] + : []), ...(process.env.LZ_ENABLE_EXPERIMENTAL_INITIA_EXAMPLES ? [ { diff --git a/packages/create-lz-oapp/test/config.test.ts b/packages/create-lz-oapp/test/config.test.ts index 4d40a5bded..74fc36c612 100644 --- a/packages/create-lz-oapp/test/config.test.ts +++ b/packages/create-lz-oapp/test/config.test.ts @@ -52,6 +52,18 @@ describe('config', () => { expect(getExamples()).toContainEqual(expect.objectContaining({ id: 'oft-solana' })) }) + it('should not include Solana OApp example if LZ_ENABLE_SOLANA_OAPP_EXAMPLE is empty', () => { + process.env.LZ_ENABLE_SOLANA_OAPP_EXAMPLE = '' + + expect(getExamples()).not.toContainEqual(expect.objectContaining({ id: 'oapp-solana' })) + }) + + it('should include Solana OApp example if LZ_ENABLE_SOLANA_OAPP_EXAMPLE is defined', () => { + process.env.LZ_ENABLE_SOLANA_OAPP_EXAMPLE = 'yes' + + expect(getExamples()).toContainEqual(expect.objectContaining({ id: 'oapp-solana' })) + }) + it('should not include OApp Read example if LZ_ENABLE_READ_EXAMPLE is empty', () => { process.env.LZ_ENABLE_READ_EXAMPLE = '' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a72c359f2..62f4c41801 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -894,6 +894,211 @@ importers: specifier: ^5.4.4 version: 5.5.3 + examples/oapp-solana: + dependencies: + '@kinobi-so/nodes': + specifier: ^0.21.4 + version: 0.21.5 + '@kinobi-so/nodes-from-anchor': + specifier: ^0.21.2 + version: 0.21.3 + '@kinobi-so/renderers': + specifier: ^0.21.3 + version: 0.21.5(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + '@kinobi-so/renderers-js-umi': + specifier: ^0.21.6 + version: 0.21.7(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + kinobi: + specifier: ^0.21.4 + version: 0.21.5 + devDependencies: + '@babel/core': + specifier: ^7.23.9 + version: 7.23.9 + '@coral-xyz/anchor': + specifier: ^0.29.0 + version: 0.29.0 + '@layerzerolabs/devtools': + specifier: ~1.0.0 + version: link:../../packages/devtools + '@layerzerolabs/devtools-evm-hardhat': + specifier: ^3.1.0 + version: link:../../packages/devtools-evm-hardhat + '@layerzerolabs/devtools-solana': + specifier: ~2.1.0 + version: link:../../packages/devtools-solana + '@layerzerolabs/eslint-config-next': + specifier: ~2.3.39 + version: 2.3.44(typescript@5.5.3) + '@layerzerolabs/io-devtools': + specifier: ~0.2.0 + version: link:../../packages/io-devtools + '@layerzerolabs/lz-definitions': + specifier: ^3.0.86 + version: 3.0.86 + '@layerzerolabs/lz-evm-messagelib-v2': + specifier: ^3.0.86 + version: 3.0.86(@axelar-network/axelar-gmp-sdk-solidity@5.10.0)(@chainlink/contracts-ccip@0.7.6)(@eth-optimism/contracts@0.6.0)(@layerzerolabs/lz-evm-protocol-v2@3.0.86)(@layerzerolabs/lz-evm-v1-0.7@3.0.86)(@openzeppelin/contracts-upgradeable@5.1.0)(@openzeppelin/contracts@5.1.0)(hardhat-deploy@0.12.4)(solidity-bytes-utils@0.8.2) + '@layerzerolabs/lz-evm-protocol-v2': + specifier: ^3.0.86 + version: 3.0.86(@openzeppelin/contracts-upgradeable@5.1.0)(@openzeppelin/contracts@5.1.0)(hardhat-deploy@0.12.4)(solidity-bytes-utils@0.8.2) + '@layerzerolabs/lz-evm-v1-0.7': + specifier: ^3.0.86 + version: 3.0.86(@openzeppelin/contracts-upgradeable@5.1.0)(@openzeppelin/contracts@5.1.0)(hardhat-deploy@0.12.4) + '@layerzerolabs/lz-solana-sdk-v2': + specifier: ^3.0.86 + version: 3.0.86(@types/node@18.18.14)(chai@4.5.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + '@layerzerolabs/lz-v2-utilities': + specifier: ^3.0.86 + version: 3.0.86 + '@layerzerolabs/metadata-tools': + specifier: ^2.0.0 + version: link:../../packages/metadata-tools + '@layerzerolabs/oapp-evm': + specifier: ^0.3.2 + version: link:../../packages/oapp-evm + '@layerzerolabs/prettier-config-next': + specifier: ^2.3.39 + version: 2.3.44 + '@layerzerolabs/protocol-devtools': + specifier: ^2.0.0 + version: link:../../packages/protocol-devtools + '@layerzerolabs/protocol-devtools-solana': + specifier: ^7.0.0 + version: link:../../packages/protocol-devtools-solana + '@layerzerolabs/solhint-config': + specifier: ^3.0.12 + version: 3.0.12(typescript@5.5.3) + '@layerzerolabs/test-devtools-evm-foundry': + specifier: ~6.0.3 + version: link:../../packages/test-devtools-evm-foundry + '@layerzerolabs/toolbox-foundry': + specifier: ~0.1.12 + version: link:../../packages/toolbox-foundry + '@layerzerolabs/toolbox-hardhat': + specifier: ~0.6.11 + version: link:../../packages/toolbox-hardhat + '@layerzerolabs/ua-devtools': + specifier: ~4.0.0 + version: link:../../packages/ua-devtools + '@layerzerolabs/ua-devtools-evm': + specifier: ~6.0.0 + version: link:../../packages/ua-devtools-evm + '@layerzerolabs/ua-devtools-evm-hardhat': + specifier: ~8.0.0 + version: link:../../packages/ua-devtools-evm-hardhat + '@layerzerolabs/ua-devtools-solana': + specifier: ~7.0.0 + version: link:../../packages/ua-devtools-solana + '@metaplex-foundation/mpl-toolbox': + specifier: ^0.9.4 + version: 0.9.4(@metaplex-foundation/umi@0.9.2) + '@metaplex-foundation/umi': + specifier: ^0.9.2 + version: 0.9.2 + '@metaplex-foundation/umi-bundle-defaults': + specifier: ^0.9.2 + version: 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.0) + '@metaplex-foundation/umi-eddsa-web3js': + specifier: ^0.9.2 + version: 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.0) + '@metaplex-foundation/umi-program-repository': + specifier: ^0.9.2 + version: 0.9.2(@metaplex-foundation/umi@0.9.2) + '@metaplex-foundation/umi-public-keys': + specifier: ^0.8.9 + version: 0.8.9 + '@metaplex-foundation/umi-web3js-adapters': + specifier: ^0.9.2 + version: 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.0) + '@nomicfoundation/hardhat-ethers': + specifier: ^3.0.5 + version: 3.0.5(ethers@5.7.2)(hardhat@2.22.12) + '@nomiclabs/hardhat-ethers': + specifier: ^2.2.3 + version: 2.2.3(ethers@5.7.2)(hardhat@2.22.12) + '@openzeppelin/contracts': + specifier: ^5.0.2 + version: 5.1.0 + '@openzeppelin/contracts-upgradeable': + specifier: ^5.0.2 + version: 5.1.0(@openzeppelin/contracts@5.1.0) + '@rushstack/eslint-patch': + specifier: ^1.7.0 + version: 1.7.0 + '@solana-developers/helpers': + specifier: ~2.8.1 + version: 2.8.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + '@solana/web3.js': + specifier: ^1.95.8 + version: 1.98.0 + '@types/chai': + specifier: ^4.3.11 + version: 4.3.20 + '@types/mocha': + specifier: ^10.0.6 + version: 10.0.6 + '@types/node': + specifier: ~18.18.14 + version: 18.18.14 + bs58: + specifier: ^6.0.0 + version: 6.0.0 + chai: + specifier: ^4.4.1 + version: 4.5.0 + concurrently: + specifier: ~9.1.0 + version: 9.1.0 + dotenv: + specifier: ^16.4.5 + version: 16.4.7 + eslint: + specifier: ^8.55.0 + version: 8.57.1 + eslint-plugin-jest-extended: + specifier: ~2.0.0 + version: 2.0.0(eslint@8.57.1)(typescript@5.5.3) + ethers: + specifier: ^5.7.2 + version: 5.7.2 + exponential-backoff: + specifier: ~3.1.1 + version: 3.1.1 + hardhat: + specifier: ^2.22.10 + version: 2.22.12(ts-node@10.9.2)(typescript@5.5.3) + hardhat-contract-sizer: + specifier: ^2.10.0 + version: 2.10.0(hardhat@2.22.12) + hardhat-deploy: + specifier: ^0.12.1 + version: 0.12.4 + hardhat-deploy-ethers: + specifier: ^0.4.2 + version: 0.4.2(@nomicfoundation/hardhat-ethers@3.0.5)(hardhat-deploy@0.12.4)(hardhat@2.22.12) + mocha: + specifier: ^10.2.0 + version: 10.2.0 + p-memoize: + specifier: ~4.0.4 + version: 4.0.4 + prettier: + specifier: ^3.2.5 + version: 3.2.5 + solhint: + specifier: ^4.1.1 + version: 4.1.1(typescript@5.5.3) + solidity-bytes-utils: + specifier: ^0.8.2 + version: 0.8.2 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + typescript: + specifier: ^5.4.4 + version: 5.5.3 + examples/oft: devDependencies: '@babel/core': @@ -6398,7 +6603,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.26.8 '@babel/types': 7.26.8 - debug: 4.3.7 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -8630,6 +8835,128 @@ packages: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 + /@kinobi-so/errors@0.21.5: + resolution: {integrity: sha512-IqPMOe0qbmuBI8vux9HU+tvx+SaCVk32g5GnRvYxJh6OmpYOEuJRg02nT2XtYPeYWB2IFKJ1X2AnwTbEz9Vx8Q==} + hasBin: true + dependencies: + '@kinobi-so/node-types': 0.21.5 + chalk: 5.3.0 + commander: 12.1.0 + dev: false + + /@kinobi-so/node-types@0.21.5: + resolution: {integrity: sha512-OKpk08+nTsO25xSZBT+PXpjx2FOHfn7jr0CbnQZ+ulfRa+dr3eSf7zSn+0ipKjG+FFWUUGpizgSkgpTQMjAqLA==} + dev: false + + /@kinobi-so/nodes-from-anchor@0.21.3: + resolution: {integrity: sha512-mar3UeJNx61k77NhucqYIwUXZvC0b6mhLYFksQY0Wz+gCFTWLr8exxB7KmgSycT+XuFNOydpYnEOeMa5lUywBQ==} + dependencies: + '@kinobi-so/errors': 0.21.5 + '@kinobi-so/nodes': 0.21.5 + '@kinobi-so/visitors': 0.21.5 + '@noble/hashes': 1.7.1 + dev: false + + /@kinobi-so/nodes@0.21.5: + resolution: {integrity: sha512-6iu9/GXrVs14olVeUAAw73BK2js7RGefVGHBDF6PKcvw6i0DlxhPkZiNOSpzweaeTmZsowHO7xEJKov0446fbQ==} + dependencies: + '@kinobi-so/errors': 0.21.5 + '@kinobi-so/node-types': 0.21.5 + dev: false + + /@kinobi-so/renderers-core@0.21.3: + resolution: {integrity: sha512-Ow1x0IaRmUlqQUAFTQZpq56wpkB6YKdnpNmfjM8IS/ssgz2IG5Jw3LREvNLvdXwpp/eyjSYOsux2RDFJkERl0g==} + dependencies: + '@kinobi-so/errors': 0.21.5 + '@kinobi-so/nodes': 0.21.5 + '@kinobi-so/visitors-core': 0.21.5 + dev: false + + /@kinobi-so/renderers-js-umi@0.21.7(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): + resolution: {integrity: sha512-OgM8L44jWYww7gnCSC7gSNPC+yWVEPCj/cffhDb1O1WfglpeQiHFiRUEnBc8GCGF8Oc6Azn2jjQlja8JNsqZmA==} + dependencies: + '@kinobi-so/errors': 0.21.5 + '@kinobi-so/nodes': 0.21.5 + '@kinobi-so/renderers-core': 0.21.3 + '@kinobi-so/validators': 0.21.5 + '@kinobi-so/visitors-core': 0.21.5 + '@solana/codecs-strings': 2.0.0-rc.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + nunjucks: 3.2.4 + prettier: 3.5.3 + transitivePeerDependencies: + - chokidar + - fastestsmallesttextencoderdecoder + - typescript + dev: false + + /@kinobi-so/renderers-js@0.21.9(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): + resolution: {integrity: sha512-ZwN3UIYGZHu1V9TYsCPolVPdScR71r4PrLVVLsxixIxPBhdTMIgUbXK4OqHkzxaR5kn8Ssrvuq6+V2f7XFbVSg==} + dependencies: + '@kinobi-so/errors': 0.21.5 + '@kinobi-so/nodes': 0.21.5 + '@kinobi-so/nodes-from-anchor': 0.21.3 + '@kinobi-so/renderers-core': 0.21.3 + '@kinobi-so/visitors-core': 0.21.5 + '@solana/codecs-strings': 2.0.0-rc.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + nunjucks: 3.2.4 + prettier: 3.5.3 + transitivePeerDependencies: + - chokidar + - fastestsmallesttextencoderdecoder + - typescript + dev: false + + /@kinobi-so/renderers-rust@0.21.8(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): + resolution: {integrity: sha512-rjWK+hWnniAcfaVAPwgzom3LtXkGj4iZ/LPsm8LpgfSdTbXLT2HihVgyQGQDpSTDdVvGb4PuuX7A01Et3N7iog==} + dependencies: + '@kinobi-so/errors': 0.21.5 + '@kinobi-so/nodes': 0.21.5 + '@kinobi-so/renderers-core': 0.21.3 + '@kinobi-so/visitors-core': 0.21.5 + '@solana/codecs-strings': 2.0.0-rc.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + nunjucks: 3.2.4 + transitivePeerDependencies: + - chokidar + - fastestsmallesttextencoderdecoder + - typescript + dev: false + + /@kinobi-so/renderers@0.21.5(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): + resolution: {integrity: sha512-JwHaYEjx1l/nf302b2RoZv3TEsBDsE1u+sgEt+noC3WXjqHVJJkjZ7DTL6IBSgBerR9OwukraCN7RWK2XQVwgA==} + dependencies: + '@kinobi-so/renderers-js': 0.21.9(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + '@kinobi-so/renderers-js-umi': 0.21.7(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + '@kinobi-so/renderers-rust': 0.21.8(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + transitivePeerDependencies: + - chokidar + - fastestsmallesttextencoderdecoder + - typescript + dev: false + + /@kinobi-so/validators@0.21.5: + resolution: {integrity: sha512-JYZWHgma1TrnjhiUJn5/oYS9XfeirUBhwpuTPDRmRrSiHHv329gjWimzZO2uLAm5S5PgFyhLTz+S6jjxRsdNXg==} + dependencies: + '@kinobi-so/errors': 0.21.5 + '@kinobi-so/nodes': 0.21.5 + '@kinobi-so/visitors-core': 0.21.5 + dev: false + + /@kinobi-so/visitors-core@0.21.5: + resolution: {integrity: sha512-qNq9CcDh1P/A0BRR7zEdRpzeWL6ObAowMNWpkxZTT2BKqejK9oKWSh2/gzg6ehcn2Fn/J5DE8bnKZfAlEww95g==} + dependencies: + '@kinobi-so/errors': 0.21.5 + '@kinobi-so/nodes': 0.21.5 + json-stable-stringify: 1.3.0 + dev: false + + /@kinobi-so/visitors@0.21.5: + resolution: {integrity: sha512-RCJFaN1Q1f/MQW6PP3Oz0fgNXFVmfUEyErMzPM9CjBi1j2BJzTjff7IzEjbTpG7SbDY91eEyTDcl430ygcDBkA==} + dependencies: + '@kinobi-so/errors': 0.21.5 + '@kinobi-so/nodes': 0.21.5 + '@kinobi-so/visitors-core': 0.21.5 + dev: false + /@layerzerolabs/devtools-evm-hardhat@2.0.9(@ethersproject/abi@5.7.0)(@ethersproject/abstract-signer@5.7.0)(@ethersproject/contracts@5.7.0)(@ethersproject/providers@5.7.2)(@layerzerolabs/devtools-evm@1.0.6)(@layerzerolabs/devtools@0.4.10)(@layerzerolabs/io-devtools@0.1.17)(@layerzerolabs/lz-definitions@3.0.86)(@nomiclabs/hardhat-ethers@2.2.3)(ethers@5.7.2)(fp-ts@2.16.2)(hardhat-deploy@0.12.4)(hardhat@2.22.12): resolution: {integrity: sha512-7vNPHtypjx3IjLV5+wPHq1nOOt5FWy2Bw4pBmPBt5XMsz8uIFuVtrYMr7CraICkwMOZUYYeu5EnJeT6erhRDIA==} peerDependencies: @@ -9016,7 +9343,6 @@ packages: - supports-color - typescript - utf-8-validate - dev: false /@layerzerolabs/lz-definitions@3.0.75: resolution: {integrity: sha512-TIbFBCfuElg6WcND4HNUROTSAayBETDC0YrISVadByo3iM2baAi+rpGC1kdrOxOTRlSBetd2khTOUCd7/sZdOQ==} @@ -9826,7 +10152,6 @@ packages: - supports-color - typescript - utf-8-validate - dev: false /@layerzerolabs/lz-movevm-sdk-v2@3.0.59(@jest/globals@29.7.0)(@types/node@18.18.14)(chai@4.5.0)(typescript@5.5.3): resolution: {integrity: sha512-qYP5R5nlmO/siy3q68MH3nDnvb0ORzae3KSo2BD3cctNLiEpCjBei+8OcWMo9qZpJDMovNlqtTEaHt1CJmKxBQ==} @@ -9965,7 +10290,6 @@ packages: - supports-color - typescript - utf-8-validate - dev: false /@layerzerolabs/lz-solana-sdk-v2@3.0.0(typescript@5.5.3): resolution: {integrity: sha512-sPvLXeQUO9QLpjOuWE7V+V8yfoI4E/NBYsH9lO2aPx0LYkQa+88ACgPq43B/zFROUD8238WuSb+doGrn3PKtJQ==} @@ -10176,7 +10500,6 @@ packages: - supports-color - typescript - utf-8-validate - dev: false /@layerzerolabs/lz-ton-sdk-v2@3.0.27: resolution: {integrity: sha512-AU1uOzmLjWvyHdJGTo689bXLsCS/QAmfQSjvZ4544muLfpGVLl3l6lOl8DwmN1UQuwKKK94C5rEvoElVUYf0zQ==} @@ -10630,7 +10953,7 @@ packages: '@ton/crypto': 3.3.0 '@ton/sandbox': 0.22.0(@ton/core@0.59.0)(@ton/crypto@3.3.0) '@ton/test-utils': 0.4.2(@jest/globals@29.7.0)(@ton/core@0.59.0)(chai@4.5.0) - axios: 1.8.4 + axios: 1.7.9 dataloader: 2.2.2 symbol.inspect: 1.0.1 teslabot: 1.5.0 @@ -10658,7 +10981,7 @@ packages: '@ton/crypto': 3.3.0 '@ton/sandbox': 0.22.0(@ton/core@0.59.0)(@ton/crypto@3.3.0) '@ton/test-utils': 0.4.2(@ton/core@0.59.0)(chai@4.4.1) - axios: 1.8.4 + axios: 1.7.9 dataloader: 2.2.2 symbol.inspect: 1.0.1 teslabot: 1.5.0 @@ -10715,7 +11038,7 @@ packages: '@ton/crypto': 3.3.0 '@ton/sandbox': 0.22.0(@ton/core@0.59.0)(@ton/crypto@3.3.0) '@ton/test-utils': 0.4.2(@jest/globals@29.7.0)(@ton/core@0.59.0)(chai@4.5.0) - axios: 1.8.4 + axios: 1.7.9 dataloader: 2.2.2 symbol.inspect: 1.0.1 teslabot: 1.5.0 @@ -10815,7 +11138,6 @@ packages: - supports-color - typescript - utf-8-validate - dev: false /@layerzerolabs/ua-devtools@3.0.6(@layerzerolabs/devtools@0.4.10)(@layerzerolabs/io-devtools@0.1.17)(@layerzerolabs/lz-definitions@3.0.86)(@layerzerolabs/lz-v2-utilities@3.0.86)(@layerzerolabs/protocol-devtools@1.1.7)(zod@3.22.4): resolution: {integrity: sha512-hZbhNB7iIrKrrkOvDMF74niBCBj5icHmmlYbCSV3S6BKeLuKnX5i6zvlMTq/v5kgU0CZ0kNS37PFHfhSkW+oMw==} @@ -11019,7 +11341,7 @@ packages: '@metaplex-foundation/beet': 0.7.2 '@solana/web3.js': 1.98.0 bs58: 5.0.0 - debug: 4.3.7 + debug: 4.4.0 transitivePeerDependencies: - bufferutil - encoding @@ -11045,7 +11367,7 @@ packages: dependencies: ansicolors: 0.3.2 bn.js: 5.2.1 - debug: 4.3.7 + debug: 4.4.0 transitivePeerDependencies: - supports-color dev: true @@ -12429,6 +12751,16 @@ packages: '@solana/errors': 2.0.0-rc.1(typescript@5.5.3) typescript: 5.5.3 + /@solana/codecs-core@2.0.0-rc.4(typescript@5.5.3): + resolution: {integrity: sha512-JIrTSps032mSE3wBxW3bXOqWfoy4CMy1CX/XeVCijyh5kLVxZTSDIdRTYdePdL1yzaOZF1Xysvt1DhOUgBdM+A==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5' + dependencies: + '@solana/errors': 2.0.0-rc.4(typescript@5.5.3) + typescript: 5.5.3 + dev: false + /@solana/codecs-data-structures@2.0.0-preview.2: resolution: {integrity: sha512-Xf5vIfromOZo94Q8HbR04TbgTwzigqrKII0GjYr21K7rb3nba4hUW2ir8kguY7HWFBcjHGlU5x3MevKBOLp3Zg==} dependencies: @@ -12482,6 +12814,17 @@ packages: '@solana/errors': 2.0.0-rc.1(typescript@5.5.3) typescript: 5.5.3 + /@solana/codecs-numbers@2.0.0-rc.4(typescript@5.5.3): + resolution: {integrity: sha512-ZJR7TaUO65+3Hzo3YOOUCS0wlzh17IW+j0MZC2LCk1R0woaypRpHKj4iSMYeQOZkMxsd9QT3WNvjFrPC2qA6Sw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5' + dependencies: + '@solana/codecs-core': 2.0.0-rc.4(typescript@5.5.3) + '@solana/errors': 2.0.0-rc.4(typescript@5.5.3) + typescript: 5.5.3 + dev: false + /@solana/codecs-strings@2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22): resolution: {integrity: sha512-EgBwY+lIaHHgMJIqVOGHfIfpdmmUDNoNO/GAUGeFPf+q0dF+DtwhJPEMShhzh64X2MeCZcmSO6Kinx0Bvmmz2g==} peerDependencies: @@ -12517,6 +12860,20 @@ packages: fastestsmallesttextencoderdecoder: 1.0.22 typescript: 5.5.3 + /@solana/codecs-strings@2.0.0-rc.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): + resolution: {integrity: sha512-LGfK2RL0BKjYYUfzu2FG/gTgCsYOMz9FKVs2ntji6WneZygPxJTV5W98K3J8Rl0JewpCSCFQH3xjLSHBJUS0fA==} + engines: {node: '>=20.18.0'} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: '>=5' + dependencies: + '@solana/codecs-core': 2.0.0-rc.4(typescript@5.5.3) + '@solana/codecs-numbers': 2.0.0-rc.4(typescript@5.5.3) + '@solana/errors': 2.0.0-rc.4(typescript@5.5.3) + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.5.3 + dev: false + /@solana/codecs@2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22): resolution: {integrity: sha512-4HHzCD5+pOSmSB71X6w9ptweV48Zj1Vqhe732+pcAQ2cMNnN0gMPMdDq7j3YwaZDZ7yrILVV/3+HTnfT77t2yA==} dependencies: @@ -12585,6 +12942,18 @@ packages: commander: 12.1.0 typescript: 5.5.3 + /@solana/errors@2.0.0-rc.4(typescript@5.5.3): + resolution: {integrity: sha512-0PPaMyB81keEHG/1pnyEuiBVKctbXO641M2w3CIOrYT/wzjunfF0FTxsqq9wYJeYo0AyiefCKGgSPs6wiY2PpQ==} + engines: {node: '>=20.18.0'} + hasBin: true + peerDependencies: + typescript: '>=5' + dependencies: + chalk: 5.3.0 + commander: 12.1.0 + typescript: 5.5.3 + dev: false + /@solana/options@2.0.0-preview.2: resolution: {integrity: sha512-FAHqEeH0cVsUOTzjl5OfUBw2cyT8d5Oekx4xcn5hn+NyPAfQJgM3CEThzgRD6Q/4mM5pVUnND3oK/Mt1RzSE/w==} dependencies: @@ -13872,6 +14241,10 @@ packages: jsonparse: 1.3.1 through: 2.3.8 + /a-sync-waterfall@1.0.1: + resolution: {integrity: sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==} + dev: false + /abi-decoder@2.4.0: resolution: {integrity: sha512-TOLU2q1HgYOjs1GKGtVzaqrYkar6I2fT9a80rzx6/9EJ/5crb4nCGuro0grZayixem93T7omrajYmLiMkYDLDA==} dependencies: @@ -13951,7 +14324,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.3.7 + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -14873,6 +15246,14 @@ packages: normalize-url: 6.1.0 responselike: 2.0.1 + /call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + dev: false + /call-bind@1.0.5: resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} dependencies: @@ -14890,6 +15271,24 @@ packages: get-intrinsic: 1.2.4 set-function-length: 1.2.1 + /call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.0 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + dev: false + + /call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + dev: false + /callsite@1.0.0: resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} dev: true @@ -15318,7 +15717,6 @@ packages: /commander@5.1.0: resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} engines: {node: '>= 6'} - dev: true /commander@8.3.0: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} @@ -15947,6 +16345,15 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} + /dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + dev: false + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -16143,6 +16550,11 @@ packages: dependencies: get-intrinsic: 1.2.4 + /es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + dev: false + /es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} @@ -16166,6 +16578,13 @@ packages: safe-array-concat: 1.1.0 dev: true + /es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: false + /es-set-tostringtag@2.0.2: resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} engines: {node: '>= 0.4'} @@ -17747,6 +18166,22 @@ packages: has-symbols: 1.0.3 hasown: 2.0.1 + /get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + dev: false + /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -17756,6 +18191,14 @@ packages: engines: {node: '>=4'} dev: false + /get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + dev: false + /get-stdin@9.0.0: resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} engines: {node: '>=12'} @@ -17970,6 +18413,11 @@ packages: dependencies: get-intrinsic: 1.2.2 + /gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + dev: false + /got@11.8.6: resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} engines: {node: '>=10.19.0'} @@ -18301,6 +18749,11 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + /has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + dev: false + /has-tostringtag@1.0.0: resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} engines: {node: '>= 0.4'} @@ -18333,6 +18786,13 @@ packages: dependencies: function-bind: 1.1.2 + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + /he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -19063,7 +19523,6 @@ packages: /isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - dev: true /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -19788,6 +20247,17 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true + /json-stable-stringify@1.3.0: + resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + isarray: 2.0.5 + jsonify: 0.0.1 + object-keys: 1.1.1 + dev: false + /json-stream-stringify@3.1.4: resolution: {integrity: sha512-oGoz05ft577LolnXFQHD2CjnXDxXVA5b8lHwfEZgRXQUZeCMo6sObQQRq+NXuHQ3oTeMZHHmmPY2rjVwyqR62A==} engines: {node: '>=7.10.1'} @@ -19829,6 +20299,10 @@ packages: optionalDependencies: graceful-fs: 4.2.11 + /jsonify@0.0.1: + resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} + dev: false + /jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} @@ -19894,6 +20368,15 @@ packages: dependencies: json-buffer: 3.0.1 + /kinobi@0.21.5: + resolution: {integrity: sha512-1xgN00od8x4tokR1Xuqv5Gbg0Iqo9bzrEvA9o2rm63vXqoW7/C+TN1voPhj1SQE4h2VmpfM3MD1clTUvJK0J2g==} + dependencies: + '@kinobi-so/errors': 0.21.5 + '@kinobi-so/nodes': 0.21.5 + '@kinobi-so/validators': 0.21.5 + '@kinobi-so/visitors': 0.21.5 + dev: false + /klaw@1.3.1: resolution: {integrity: sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==} optionalDependencies: @@ -20253,6 +20736,11 @@ packages: /match-all@1.2.6: resolution: {integrity: sha512-0EESkXiTkWzrQQntBu2uzKvLu6vVkUGz40nGPbSZuegcfE5UuSzNjLaIu76zJWuaT/2I3Z/8M06OlUOZLGwLlQ==} + /math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + dev: false + /mcl-wasm@0.7.9: resolution: {integrity: sha512-iJIUcQWA88IJB/5L15GnJVnSQJmf/YaxxV6zRavv83HILHaJQb6y0iFyDMdDO0gN8X37tdxmAOrH/P8B6RB8sQ==} engines: {node: '>=8.9.0'} @@ -20807,6 +21295,21 @@ packages: bn.js: 4.11.6 strip-hex-prefix: 1.0.0 + /nunjucks@3.2.4: + resolution: {integrity: sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==} + engines: {node: '>= 6.9.0'} + hasBin: true + peerDependencies: + chokidar: ^3.3.0 + peerDependenciesMeta: + chokidar: + optional: true + dependencies: + a-sync-waterfall: 1.0.1 + asap: 2.0.6 + commander: 5.1.0 + dev: false + /oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} @@ -21433,6 +21936,12 @@ packages: hasBin: true dev: true + /prettier@3.5.3: + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + engines: {node: '>=14'} + hasBin: true + dev: false + /pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -22334,6 +22843,18 @@ packages: gopd: 1.0.1 has-property-descriptors: 1.0.2 + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: false + /set-function-name@2.0.1: resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} engines: {node: '>= 0.4'} diff --git a/tests-user/tests/create-lz-oapp.bats b/tests-user/tests/create-lz-oapp.bats index be670d8eba..29694a915a 100644 --- a/tests-user/tests/create-lz-oapp.bats +++ b/tests-user/tests/create-lz-oapp.bats @@ -189,6 +189,15 @@ teardown() { pnpm test } +@test "should work with pnpm & oapp solana example in CI mode" { + local DESTINATION="$PROJECTS_DIRECTORY/pnpm-oapp-solana" + + LZ_ENABLE_SOLANA_OAPP_EXAMPLE=1 npx --yes create-lz-oapp --ci --example oapp-solana --destination $DESTINATION --package-manager pnpm + cd "$DESTINATION" + pnpm compile + pnpm test +} + @test "should work with pnpm & oapp read example in CI mode" { local DESTINATION="$PROJECTS_DIRECTORY/pnpm-oapp-read" diff --git a/turbo.json b/turbo.json index e81c104cca..432eb6a6da 100644 --- a/turbo.json +++ b/turbo.json @@ -52,6 +52,7 @@ "LZ_ENABLE_EXPERIMENTAL_RETRY", "LZ_ENABLE_EXPERIMENTAL_SIMULATION", "LZ_ENABLE_SOLANA_OFT_EXAMPLE", + "LZ_ENABLE_SOLANA_OAPP_EXAMPLE", "LZ_ENABLE_READ_EXAMPLE", "LZ_ENABLE_ZKSOLC_EXAMPLE", "LZ_ENABLE_NATIVE_EXAMPLE",