diff --git a/Dockerfile b/Dockerfile index 97cd80138..d7f6e1057 100644 --- a/Dockerfile +++ b/Dockerfile @@ -104,7 +104,7 @@ FROM machine AS aptos WORKDIR /app/aptos -ARG APTOS_VERSION=4.2.3 +ARG APTOS_VERSION=6.0.1 RUN \ (\ # We download the source code and extract the archive diff --git a/examples/oft-adapter-aptos-move/.env.example b/examples/oft-adapter-aptos-move/.env.example new file mode 100644 index 000000000..77d7a012f --- /dev/null +++ b/examples/oft-adapter-aptos-move/.env.example @@ -0,0 +1,15 @@ +# .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- +# / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ +# `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' +# +# 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= +EVM_PRIVATE_KEY= \ No newline at end of file diff --git a/examples/oft-adapter-aptos-move/.eslintignore b/examples/oft-adapter-aptos-move/.eslintignore new file mode 100644 index 000000000..ee9f768fd --- /dev/null +++ b/examples/oft-adapter-aptos-move/.eslintignore @@ -0,0 +1,10 @@ +artifacts +cache +dist +node_modules +out +*.log +*.sol +*.yaml +*.lock +package-lock.json \ No newline at end of file diff --git a/examples/oft-adapter-aptos-move/.eslintrc.js b/examples/oft-adapter-aptos-move/.eslintrc.js new file mode 100644 index 000000000..876745330 --- /dev/null +++ b/examples/oft-adapter-aptos-move/.eslintrc.js @@ -0,0 +1,11 @@ +require('@rushstack/eslint-patch/modern-module-resolution'); + +module.exports = { + root: true, // Add this line to prevent ESLint from looking up the directory tree + 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', + }, +}; diff --git a/examples/oft-adapter-aptos-move/.gitignore b/examples/oft-adapter-aptos-move/.gitignore new file mode 100644 index 000000000..3fcd075f0 --- /dev/null +++ b/examples/oft-adapter-aptos-move/.gitignore @@ -0,0 +1,32 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types +build/ + +# Hardhat files +cache +artifacts + + +# LayerZero specific files +.layerzero + +# foundry test compilation files +out + +# pnpm +pnpm-error.log + +# Editor and OS files +.DS_Store +.idea + +.aptos + +shell-scripts + +transactions/* +!transactions/.gitkeep diff --git a/examples/oft-adapter-aptos-move/.nvmrc b/examples/oft-adapter-aptos-move/.nvmrc new file mode 100644 index 000000000..b714151ef --- /dev/null +++ b/examples/oft-adapter-aptos-move/.nvmrc @@ -0,0 +1 @@ +v18.18.0 \ No newline at end of file diff --git a/examples/oft-adapter-aptos-move/.prettierignore b/examples/oft-adapter-aptos-move/.prettierignore new file mode 100644 index 000000000..6e8232f5a --- /dev/null +++ b/examples/oft-adapter-aptos-move/.prettierignore @@ -0,0 +1,10 @@ +artifacts/ +cache/ +dist/ +node_modules/ +out/ +*.log +*ignore +*.yaml +*.lock +package-lock.json \ No newline at end of file diff --git a/examples/oft-adapter-aptos-move/.prettierrc.js b/examples/oft-adapter-aptos-move/.prettierrc.js new file mode 100644 index 000000000..6f55b4019 --- /dev/null +++ b/examples/oft-adapter-aptos-move/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('@layerzerolabs/prettier-config-next'), +}; diff --git a/examples/oft-adapter-aptos-move/Move.toml b/examples/oft-adapter-aptos-move/Move.toml new file mode 100644 index 000000000..cacddf99c --- /dev/null +++ b/examples/oft-adapter-aptos-move/Move.toml @@ -0,0 +1,75 @@ +[package] +name = "oft" +version = "1.0.0" +authors = [] + +[addresses] +oft = "_" +oft_admin = "_" +oft_common = "_" +router_node_0 = "_" +simple_msglib = "_" +blocked_msglib = "_" +uln_302 = "_" +router_node_1 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +msglib_types = "_" +treasury = "_" +worker_peripherals = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +worker_common = "_" +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +executor_fee_lib_0 = "_" +dvn_fee_lib_0 = "_" +dvn = "_" +native_token_metadata_address = "0xa" +# For Initia: "0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9" + +[dev-addresses] +oft = "0x302814823" +oft_admin = "0x12321241" +oft_common = "0x30281482332" +router_node_0 = "0x10000f" +simple_msglib = "0x100011" +blocked_msglib = "0x100001" +uln_302 = "0x100013" +router_node_1 = "0x100010" +endpoint_v2_common = "0x100007" +endpoint_v2 = "0x100006" +layerzero_admin = "0x200001" +layerzero_treasury_admin = "0x200002" +msglib_types = "0x10000b" +treasury = "0x100012" +worker_peripherals = "0x3000" +price_feed_router_0 = "0x10000d" +price_feed_router_1 = "0x10000e" +price_feed_module_0 = "0x10000c" +worker_common = "0x100014" +executor_fee_lib_router_0 = "0x100009" +executor_fee_lib_router_1 = "0x10000a" +dvn_fee_lib_router_0 = "0x100004" +dvn_fee_lib_router_1 = "0x100005" +executor_fee_lib_0 = "0x100008" +dvn_fee_lib_0 = "0x100003" +dvn = "0x100002" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" + +[dependencies] +endpoint_v2_common = { git = "https://github.com/LayerZero-Labs/LayerZero-v2", rev = "aptos-v2-jan-6", subdir = "packages/layerzero-v2/aptos/contracts/endpoint_v2_common" } +endpoint_v2 = { git = "https://github.com/LayerZero-Labs/LayerZero-v2", rev = "aptos-v2-jan-6", subdir = "packages/layerzero-v2/aptos/contracts/endpoint_v2" } +oft_common = { git = "https://github.com/LayerZero-Labs/LayerZero-v2", rev = "aptos-v2-jan-6", subdir = "packages/layerzero-v2/aptos/contracts/oapps/oft_common" } + +[dev-dependencies] +simple_msglib = { git = "https://github.com/LayerZero-Labs/LayerZero-v2", rev = "aptos-v2-jan-6", subdir = "packages/layerzero-v2/aptos/contracts/msglib/libs/simple_msglib" } \ No newline at end of file diff --git a/examples/oft-adapter-aptos-move/README.md b/examples/oft-adapter-aptos-move/README.md new file mode 100644 index 000000000..a734711c9 --- /dev/null +++ b/examples/oft-adapter-aptos-move/README.md @@ -0,0 +1,311 @@ +## Move-VM OFT Adapter Setup and Deployment + +### connecting to aptos via cli + +To install aptos cli, run the following command: + +``` +brew install aptos +``` + +If you need to generate a new key, run the following command: + +``` +aptos key generate --output-file my_key.pub +``` + +Then initialize the aptos cli and connect to the aptos network: + +``` +aptos init --network=testnet --private-key= +``` + +You can then verify that your initialization was successful by running the following command: + +``` +cat .aptos/config.yaml +``` + +If successful the config will be populated with the RPC links and your account private key, account address, and network. + +Note: Your private key is stored in the .aptos/config.yaml file and will be extracted from there. + +## Setup + +Create a `.env` file with the following variables: + +```bash +ACCOUNT_ADDRESS= +EVM_PRIVATE_KEY= +``` + +Then run `source .env` in order for your values to be mapped to `$ACCOUNT_ADDRESS` and `$EVM_PRIVATE_KEY` + +Note: aptos account address can be found in .aptos/config.yaml + +## Build and deploy + +Note: to overwrite previous deploy and build, you can use `--force-build true` for the build script and `--force-deploy true` for the deploy script. + +### Builds the contracts + +```bash +pnpm run lz:sdk:move:build --oapp-config move.layerzero.config.ts --named-addresses oft=$ACCOUNT_ADDRESS,oft_admin=$ACCOUNT_ADDRESS +``` + +### Checks for build, builds if not, then deploys the contracts, sets the delegate and initializes + +First modify deploy-move/OFTAdpaterInitParams.ts and replace the oftMetadata with your desired values: + +```ts +const oftMetadata = { + move_vm_fa_address: "0x0", + shared_decimals: 6, +}; +``` + +```bash +pnpm run lz:sdk:move:deploy --oapp-config move.layerzero.config.ts --named-addresses oft=$ACCOUNT_ADDRESS,oft_admin=$ACCOUNT_ADDRESS --move-deploy-script deploy-move/OFTAdapterInitParams.ts +``` + +## Init and Set Delegate + +Before running the wire command, first inside of move.layerzero.config.ts, set the delegate address to your account address. + +```ts + contracts: [ + { + contract: bscContract, + config: { + owner: 'YOUR_EVM_ACCOUNT_ADDRESS', + delegate: 'YOUR_EVM_ACCOUNT_ADDRESS', + }, + }, + { + contract: aptosContract, + config: { + delegate: 'YOUR_APTOS_ACCOUNT_ADDRESS', + owner: 'YOUR_APTOS_ACCOUNT_ADDRESS', + }, + }, + ], +``` + +Then run the following command: + +```bash +pnpm run lz:sdk:move:init-fa-adapter --oapp-config move.layerzero.config.ts --move-deploy-script deploy-move/OFTAdapterInitParams.ts +``` + +```bash +pnpm run lz:sdk:move:set-delegate --oapp-config move.layerzero.config.ts +``` + +## Wire + +For EVM: +Ensure that in move.layerzero.config.ts, all of your evm contracts have the owner and delegate contract is specified. + +```ts + contracts: [ + { + contract: your_contract_name, + config: { + owner: 'YOUR_EVM_ACCOUNT_ADDRESS', + delegate: 'YOUR_EVM_ACCOUNT_ADDRESS', + }, + }, + ... + ] +``` + +Then run the wire command: + +```bash +pnpm run lz:sdk:evm:wire --oapp-config move.layerzero.config.ts +``` + +Troubleshooting: +Sometimes the command will fail part way through and need to be run multiple times. Also running running `pkill anvil` to reset the anvil node can help. + +For Move-VM: + +```bash +pnpm run lz:sdk:move:wire --oapp-config move.layerzero.config.ts +``` + +## Set Fee + +```bash +pnpm run lz:sdk:move:adapter-set-fee --oapp-config move.layerzero.config.ts --fee-bps 1000 --to-eid number +``` + +## Set Rate Limit + +```bash +pnpm run lz:sdk:move:adapter-set-rate-limit --oapp-config move.layerzero.config.ts --rate-limit 10000 --window-seconds 60 --to-eid number +``` + +Rate limit limits how much is sent netted by the amount that is received. It is set on a per pathway basis. +For example if the rate limit from Aptos to EVM is 100 tokens you can send 100 tokens from Aptos to EVM, however if you receive 50 tokens from EVM to Aptos you are then able to send 150 tokens from Aptos to EVM. +Window is the number of seconds over which the capacity is restored. If the rate limit is 1000 and window is 10 seconds, then each second you get 100 (1000/10) capacity back. The units of the rate limit are the tokens in local decimals. + +## Unset Rate Limit + +```bash +pnpm run lz:sdk:move:adapter-unset-rate-limit --oapp-config move.layerzero.config.ts --to-eid number +``` + +## Permanently Disable Blocklist + +> ⚠️ **Warning**: This will permanently disable the blocklist for the OFT. It is for OFTs that want to demonstrate to their holders that they will never use blocklisting abilities. + +```bash +pnpm run lz:sdk:move:adapter-permanently-disable-blocklist +``` + +### Transferring Ownership of your Move OApp (OFT) + +There are three steps to transferring ownership of your Move OFT: + +1. Transfer the delegate to the new delegate +2. Transfer the OApp owner of the your to the new owner +3. Transfer the Move-VM object owner to the new owner + +To set the delegate, run the following command: +First ensure that the delegate is specified in the move.layerzero.config.ts file. + +```ts + contracts: [ + { + contract: your_contract_name, + config: { + delegate: 'YOUR_DESIRED_DELEGATE_ACCOUNT_ADDRESS', + }, + }, + ... + ] +``` + +Then run the following command: + +```bash +pnpm run lz:sdk:move:set-delegate --oapp-config move.layerzero.config.ts +``` + +To transfer the OApp owner, run the following command: + +```bash +pnpm run lz:sdk:move:transfer-oapp-owner --new-owner +``` + +To transfer the Move-VM object owner, run the following command: + +```bash +pnpm run lz:sdk:move:transfer-object-owner --new-owner +``` + +Note: The object owner has the upgrade authority for the Object. + +### Mint to Account on Move VM OFT: + +> ⚠️ **Warning**: This mint command is only for testing and experimentation purposes. Do not use in production. +> First add this function to oft/sources/internal_oft/oft_impl.move in order to expose minting functionality to our move sdk script: + +``` +public entry fun mint( + admin: &signer, + recipient: address, + amount: u64, +) acquires OftImpl { + assert_admin(address_of(admin)); + primary_fungible_store::mint(&store().mint_ref, recipient, amount); +} +``` + +Then run the following command to mint the move oft: + +```bash +pnpm run lz:sdk:move:mint-to-move-oft --amount-ld 1000000000000000000 --to-address +``` + +## Send Tokens + +### Send from Move VM to EVM + +```bash +pnpm run lz:sdk:move:send-from-move-oft \ + --amount-ld \ + --min-amount-ld \ + --src-address \ + --to-address \ + --gas-limit 400000 \ + --dst-eid +``` + +### Send from EVM to Move VM + +```bash +pnpm run lz:sdk:evm:send-evm \ + --oapp-config move.layerzero.config.ts \ + --src-eid \ + --dst-eid \ + --to \ + --amount \ + --min-amount +``` + +## Help + +```bash +pnpm run lz:sdk:help +``` + +## EVM Deployment + +```bash +npx hardhat lz:deploy +``` + +Select only the evm networks (DO NOT SELECT APTOS or MOVEMENT) + +### Verifying successful ownership transfer of your Move-VM OFT: + +Run the following command: + +```bash +aptos account list \ + --account \ + --url https://fullnode.testnet.aptoslabs.com \ + --query resources +``` + +Note: replace the url with your desired aptos fullnode url. + +Look for the following in the output: + +```json +{ + "0x1::object::ObjectCore": { + ... + "owner": "0x", + ... + } + ... +} +``` + +If the owner is your desired address, then the ownership transfer was successful. + +For verifying the admin look for the following in the output: + +```json + { + "::oapp_store::OAppStore": { + "admin": "0x", + ... + } + } +``` + +If the admin is your desired address, then the ownership transfer was successful. diff --git a/examples/oft-adapter-aptos-move/contracts/MyOFT.sol b/examples/oft-adapter-aptos-move/contracts/MyOFT.sol new file mode 100644 index 000000000..f8bc7b47f --- /dev/null +++ b/examples/oft-adapter-aptos-move/contracts/MyOFT.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol"; + +contract MyOFT is OFT { + constructor( + string memory _name, + string memory _symbol, + address _lzEndpoint, + address _delegate + ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {} +} diff --git a/examples/oft-adapter-aptos-move/contracts/mocks/MyOFTMock.sol b/examples/oft-adapter-aptos-move/contracts/mocks/MyOFTMock.sol new file mode 100644 index 000000000..3ebb888d4 --- /dev/null +++ b/examples/oft-adapter-aptos-move/contracts/mocks/MyOFTMock.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import { MyOFT } from "../MyOFT.sol"; + +// @dev WARNING: This is for testing purposes only +contract MyOFTMock is MyOFT { + constructor( + string memory _name, + string memory _symbol, + address _lzEndpoint, + address _delegate + ) MyOFT(_name, _symbol, _lzEndpoint, _delegate) {} + + function mint(address _to, uint256 _amount) public { + _mint(_to, _amount); + } +} diff --git a/examples/oft-adapter-aptos-move/deploy-move/OFTAdapterInitParams.ts b/examples/oft-adapter-aptos-move/deploy-move/OFTAdapterInitParams.ts new file mode 100644 index 000000000..10c1247ba --- /dev/null +++ b/examples/oft-adapter-aptos-move/deploy-move/OFTAdapterInitParams.ts @@ -0,0 +1,8 @@ +const contractName = 'oft' + +const oftMetadata = { + move_vm_fa_address: '0x3ae92c16358f797d3bb55a94146665192ff7b7f98a3e9bcd948937d6b0526982', + shared_decimals: 6, +} + +export { contractName, oftMetadata } diff --git a/examples/oft-adapter-aptos-move/deploy/MyEVMOFT.ts b/examples/oft-adapter-aptos-move/deploy/MyEVMOFT.ts new file mode 100644 index 000000000..bcbdeb19b --- /dev/null +++ b/examples/oft-adapter-aptos-move/deploy/MyEVMOFT.ts @@ -0,0 +1,53 @@ +import assert from 'assert' + +import { type DeployFunction } from 'hardhat-deploy/types' + +const contractName = 'MyOFT' + +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: [ + 'MyOFT', // name + 'MOFT', // symbol + 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/oft-adapter-aptos-move/foundry.toml b/examples/oft-adapter-aptos-move/foundry.toml new file mode 100644 index 000000000..f76c3d4c3 --- /dev/null +++ b/examples/oft-adapter-aptos-move/foundry.toml @@ -0,0 +1,31 @@ +[profile.default] +solc-version = '0.8.22' +src = 'contracts' +out = 'out' +test = 'test/evm/foundry' +cache_path = 'cache/foundry' +verbosity = 3 +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 ou 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/oft-adapter-aptos-move/hardhat.config.ts b/examples/oft-adapter-aptos-move/hardhat.config.ts new file mode 100644 index 000000000..dac786566 --- /dev/null +++ b/examples/oft-adapter-aptos-move/hardhat.config.ts @@ -0,0 +1,87 @@ +// 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 'hardhat-contract-sizer' +import '@nomiclabs/hardhat-ethers' +import '@layerzerolabs/toolbox-hardhat' +import { HardhatUserConfig, HttpNetworkAccountsUserConfig } from 'hardhat/types' + +import { EndpointId } from '@layerzerolabs/lz-definitions' + +// 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.EVM_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: { + 'bsc-testnet': { + eid: EndpointId.BSC_V2_TESTNET, + url: process.env.RPC_URL_BSC || 'https://bsc-testnet.public.blastapi.io', + accounts, + }, + 'aptos-testnet': { + eid: EndpointId.APTOS_V2_TESTNET, + url: process.env.RPC_URL_APTOS_TESTNET || 'https://rpc.ankr.com/http/aptos_testnet/v1[1]', + accounts, + }, + 'eth-testnet': { + eid: EndpointId.ETHEREUM_V2_TESTNET, + url: process.env.RPC_URL_ETHEREUM_TESTNET || 'https://sepolia.infura.io/v3/', + accounts, + }, + 'solana-devnet': { + eid: EndpointId.SOLANA_V2_TESTNET, + url: process.env.RPC_URL_SOLANA_TESTNET || 'https://api.devnet.solana.com', + 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/oft-adapter-aptos-move/move.layerzero.config.ts b/examples/oft-adapter-aptos-move/move.layerzero.config.ts new file mode 100644 index 000000000..4348ed3aa --- /dev/null +++ b/examples/oft-adapter-aptos-move/move.layerzero.config.ts @@ -0,0 +1,73 @@ +import { EndpointId } from '@layerzerolabs/lz-definitions' + +import type { OAppOmniGraphHardhat, OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat' + +const bscContract: OmniPointHardhat = { + eid: EndpointId.BSC_V2_TESTNET, + contractName: 'MyOFT', +} + +const aptosContract: OmniPointHardhat = { + eid: EndpointId.APTOS_V2_TESTNET, + contractName: 'oft', +} + +const ethContract: OmniPointHardhat = { + eid: EndpointId.ETHEREUM_V2_TESTNET, + contractName: 'MyOFT', +} + +const solanaContract: OmniPointHardhat = { + eid: EndpointId.SOLANA_V2_TESTNET, + contractName: 'MyOFT', +} + +const config: OAppOmniGraphHardhat = { + contracts: [ + { + contract: bscContract, + config: { + owner: '', + delegate: '', + }, + }, + { + contract: ethContract, + config: { + owner: '', + delegate: '', + }, + }, + { + contract: aptosContract, + config: { + delegate: '', + owner: '', + }, + }, + ], + connections: [ + { + from: aptosContract, + to: ethContract, + }, + { + from: aptosContract, + to: bscContract, + }, + { + from: bscContract, + to: aptosContract, + }, + { + from: bscContract, + to: solanaContract, + }, + { + from: ethContract, + to: aptosContract, + }, + ], +} + +export default config diff --git a/examples/oft-adapter-aptos-move/package.json b/examples/oft-adapter-aptos-move/package.json new file mode 100644 index 000000000..c4fd998c3 --- /dev/null +++ b/examples/oft-adapter-aptos-move/package.json @@ -0,0 +1,113 @@ +{ + "name": "@layerzerolabs/oft-adapter-aptos-move-example", + "version": "0.0.1", + "private": true, + "license": "MIT", + "scripts": { + "clean": "rm -rf artifacts cache out build", + "compile": "$npm_execpath run compile:forge && $npm_execpath run compile:hardhat && $npm_execpath run compile:aptos", + "compile:aptos": "aptos move compile --dev --skip-attribute-checks --included-artifacts=none --skip-fetch-latest-git-deps", + "compile:forge": "forge build", + "compile:hardhat": "hardhat compile", + "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'", + "lz:sdk:evm:quote-send-evm": "ts-node scripts/cli.ts --vm evm --op quote-send", + "lz:sdk:evm:send-evm": "ts-node scripts/cli.ts --vm evm --op send", + "lz:sdk:evm:wire": "ts-node scripts/cli.ts --vm evm --op wire", + "lz:sdk:help": "ts-node scripts/cli.ts --op help --filter all", + "lz:sdk:move:adapter-permanently-disable-blocklist": "ts-node scripts/cli.ts --vm move --op adapter-permanently-disable-blocklist", + "lz:sdk:move:adapter-set-fee": "ts-node scripts/cli.ts --vm move --op adapter-set-fee", + "lz:sdk:move:adapter-set-rate-limit": "ts-node scripts/cli.ts --vm move --op adapter-set-rate-limit", + "lz:sdk:move:adapter-unset-rate-limit": "ts-node scripts/cli.ts --vm move --op adapter-unset-rate-limit", + "lz:sdk:move:build": "ts-node scripts/cli.ts --vm move --op build", + "lz:sdk:move:deploy": "ts-node scripts/cli.ts --vm move --op deploy", + "lz:sdk:move:init-fa-adapter": "ts-node scripts/cli.ts --vm move --op init-fa-adapter", + "lz:sdk:move:quote-send-move-oft": "ts-node scripts/cli.ts --vm move --op quote-send-move-oft", + "lz:sdk:move:send-from-move-oft": "ts-node scripts/cli.ts --vm move --op send-from-move-oft", + "lz:sdk:move:set-delegate": "ts-node scripts/cli.ts --vm move --op set-delegate", + "lz:sdk:move:transfer-oapp-owner": "ts-node scripts/cli.ts --vm move --op transfer-oapp-owner", + "lz:sdk:move:transfer-object-owner": "ts-node scripts/cli.ts --vm move --op transfer-object-owner", + "lz:sdk:move:wire": "ts-node scripts/cli.ts --vm move --op wire", + "test": "$npm_execpath run test:forge && $npm_execpath run test:hardhat && $npm_execpath run test:aptos", + "test:aptos": "aptos move test --dev --skip-fetch-latest-git-deps", + "test:forge": "forge test", + "test:hardhat": "hardhat test" + }, + "resolutions": { + "ethers": "^5.7.2", + "hardhat-deploy": "^0.12.1" + }, + "devDependencies": { + "@aptos-labs/ts-sdk": "^1.33.1", + "@babel/core": "^7.23.9", + "@jest/globals": "^29.7.0", + "@layerzerolabs/devtools-evm-hardhat": "^2.0.3", + "@layerzerolabs/devtools-extensible-cli": "^0.0.1", + "@layerzerolabs/devtools-move": "^0.0.1", + "@layerzerolabs/eslint-config-next": "~2.3.39", + "@layerzerolabs/lz-config-types": "^3.0.15", + "@layerzerolabs/lz-definitions": "^3.0.21", + "@layerzerolabs/lz-evm-messagelib-v2": "^3.0.12", + "@layerzerolabs/lz-evm-protocol-v2": "^3.0.12", + "@layerzerolabs/lz-evm-sdk-v2": "^3.0.27", + "@layerzerolabs/lz-evm-v1-0.7": "^3.0.12", + "@layerzerolabs/lz-movevm-sdk-v2": "^3.0.15", + "@layerzerolabs/lz-serdes": "^3.0.19", + "@layerzerolabs/lz-v2-utilities": "^3.0.12", + "@layerzerolabs/move-definitions": "^3.0.15", + "@layerzerolabs/oapp-evm": "^0.3.0", + "@layerzerolabs/oft-evm": "^3.0.0", + "@layerzerolabs/oft-move": "^0.0.1", + "@layerzerolabs/prettier-config-next": "^2.3.39", + "@layerzerolabs/solhint-config": "^3.0.12", + "@layerzerolabs/test-devtools-evm-foundry": "~5.1.0", + "@layerzerolabs/toolbox-foundry": "~0.1.9", + "@layerzerolabs/toolbox-hardhat": "~0.6.3", + "@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", + "@types/argparse": "^2.0.17", + "@types/chai": "^4.3.11", + "@types/jest": "^29.5.12", + "@types/mocha": "^10.0.6", + "@types/node": "~18.18.14", + "argparse": "^2.0.1", + "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", + "hardhat": "^2.22.10", + "hardhat-contract-sizer": "^2.10.0", + "hardhat-deploy": "^0.12.1", + "install": "^0.13.0", + "jest": "^29.7.0", + "mocha": "^10.2.0", + "prettier": "^3.2.5", + "solhint": "^4.1.1", + "solidity-bytes-utils": "^0.8.2", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "tsup": "^8.0.1", + "typescript": "^5.4.4", + "yaml": "^2.6.1" + }, + "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/oft-adapter-aptos-move/scripts/cli.ts b/examples/oft-adapter-aptos-move/scripts/cli.ts new file mode 100644 index 000000000..206574330 --- /dev/null +++ b/examples/oft-adapter-aptos-move/scripts/cli.ts @@ -0,0 +1,13 @@ +import { sdk } from '@layerzerolabs/devtools-extensible-cli/cli/AptosEVMCli' +import { attach_wire_evm, attach_wire_move } from '@layerzerolabs/devtools-move/cli/init' +import { attach_oft_move } from '@layerzerolabs/oft-move/cli/init' + +async function lzSdk() { + await attach_wire_move(sdk) + await attach_oft_move(sdk) + await attach_wire_evm(sdk) + + await sdk.execute() +} + +lzSdk() diff --git a/examples/oft-adapter-aptos-move/solhint.config.js b/examples/oft-adapter-aptos-move/solhint.config.js new file mode 100644 index 000000000..52efe629c --- /dev/null +++ b/examples/oft-adapter-aptos-move/solhint.config.js @@ -0,0 +1 @@ +module.exports = require('@layerzerolabs/solhint-config'); diff --git a/examples/oft-adapter-aptos-move/sources/oft_implementation/oft_adapter_fa.move b/examples/oft-adapter-aptos-move/sources/oft_implementation/oft_adapter_fa.move new file mode 100644 index 000000000..7bdbef13e --- /dev/null +++ b/examples/oft-adapter-aptos-move/sources/oft_implementation/oft_adapter_fa.move @@ -0,0 +1,336 @@ +/// This is an implementation of a FungibleAsset standard, Adapter-style OFT. +/// +/// An adapter can be used with a FungibleAsset that is already deployed, which cannot share burn and mint +/// capabilities. This relies on locking and unlocking the FungibleAsset in an escrow account rather than minting and +/// burning. +/// +/// Adapter OFTs should only be deployed on one chain per token (with the other EIDs having Native/mint-and-burn OFTs), +/// because there is not a rebalancing mechanism that can ensure pools maintain sufficient balance. +module oft::oft_adapter_fa { + use std::coin::Coin; + use std::fungible_asset::{Self, FungibleAsset, Metadata}; + use std::object::{Self, address_to_object, ExtendRef, Object, object_exists}; + use std::option::{Self, Option}; + use std::primary_fungible_store; + use std::signer::address_of; + + use endpoint_v2_common::bytes32::Bytes32; + use oft::oapp_core::{assert_admin, combine_options}; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core; + use oft::oft_impl_config::{ + Self, + assert_not_blocklisted, + debit_view_with_possible_fee, + fee_details_with_possible_fee, + redirect_to_admin_if_blocklisted, release_rate_limit_capacity, try_consume_rate_limit_capacity + }; + use oft_common::oft_fee_detail::OftFeeDetail; + use oft_common::oft_limit::{Self, OftLimit}; + + friend oft::oft; + friend oft::oapp_receive; + + #[test_only] + friend oft::oft_adapter_fa_tests; + + struct OftImpl has key { + metadata: Option>, + escrow_extend_ref: ExtendRef, + } + + // ================================================= OFT Handlers ================================================= + + /// The default *credit* behavior for a Adapter OFT is to unlock the amount from escrow and credit the recipient + public(friend) fun credit( + to: address, + amount_ld: u64, + src_eid: u32, + lz_receive_value: Option, + ): u64 acquires OftImpl { + // Default implementation does not make special use of LZ Receive Value sent; just deposit to the OFT address + option::for_each(lz_receive_value, |fa| primary_fungible_store::deposit(@oft_admin, fa)); + + // Release rate limit capacity for the pathway (net inflow) + release_rate_limit_capacity(src_eid, amount_ld); + + // unlock the amount from escrow + let escrow_signer = &object::generate_signer_for_extending(&store().escrow_extend_ref); + + // Deposit the extracted amount to the recipient, or redirect to the admin if the recipient is blocklisted + primary_fungible_store::transfer( + escrow_signer, + metadata(), + redirect_to_admin_if_blocklisted(to, amount_ld), + amount_ld + ); + + amount_ld + } + + /// The default *debit* behavior for a Adapter OFT is to deduct the amount from the sender and lock the deducted + /// amount in escrow + /// @return (amount_sent_ld, amount_received_ld) + public(friend) fun debit_fungible_asset( + sender: address, + fa: &mut FungibleAsset, + min_amount_ld: u64, + dst_eid: u32, + ): (u64, u64) acquires OftImpl { + assert_not_blocklisted(sender); + assert_metadata(fa); + + // Calculate the exact send amount + let amount_ld = fungible_asset::amount(fa); + let (amount_sent_ld, amount_received_ld) = debit_view(amount_ld, min_amount_ld, dst_eid); + + // Consume rate limit capacity for the pathway (net outflow), based on the amount received on the other side + try_consume_rate_limit_capacity(dst_eid, amount_received_ld); + + // Extract the exact send amount from the provided fungible asset + let extracted_fa = fungible_asset::extract(fa, amount_sent_ld); + + // Extract the fee and deposit it to the fee deposit address + let fee_ld = (amount_sent_ld - amount_received_ld); + if (fee_ld > 0) { + let fee_fa = fungible_asset::extract(&mut extracted_fa, fee_ld); + primary_fungible_store::deposit(fee_deposit_address(), fee_fa); + }; + + // Lock the amount in escrow + let escrow_address = escrow_address(); + primary_fungible_store::deposit(escrow_address, extracted_fa); + + (amount_sent_ld, amount_received_ld) + } + + /// Unused in this implementation + public(friend) fun debit_coin( + _sender: address, + _coin: &mut Coin, + _min_amount_ld: u64, + _dst_eid: u32, + ): (u64, u64) { + abort ENOT_IMPLEMENTED + } + + /// The default *debit_view* behavior for an Adapter OFT is to remove dust and use remainder as both the sent and + /// received amounts, reflecting that no additional fees are removed + public(friend) fun debit_view(amount_ld: u64, min_amount_ld: u64, _dst_eid: u32): (u64, u64) { + debit_view_with_possible_fee(amount_ld, min_amount_ld) + } + + /// Update this to override the Executor and DVN options of the OFT transmission + public(friend) fun build_options( + message_type: u16, + dst_eid: u32, + extra_options: vector, + _user_sender: address, + _amount_received_ld: u64, + _to: Bytes32, + _compose_msg: vector, + _oft_cmd: vector, + ): vector { + combine_options(dst_eid, message_type, extra_options) + } + + /// Implement this function to inspect the message and options before quoting and sending + public(friend) fun inspect_message( + _message: &vector, + _options: &vector, + _is_sending: bool, + ) {} + + /// Change this to override the OFT limit and fees provided when quoting. The fees should reflect the difference + /// between the amount sent and the amount received returned from debit() and debit_view() + public(friend) fun oft_limit_and_fees( + dst_eid: u32, + _to: vector, + amount_ld: u64, + min_amount_ld: u64, + _extra_options: vector, + _compose_msg: vector, + _oft_cmd: vector, + ): (OftLimit, vector) { + (rate_limited_oft_limit(dst_eid), fee_details_with_possible_fee(amount_ld, min_amount_ld)) + } + + // =========================================== Coin Deposit / Withdrawal ========================================== + + /// Deposit coin function abstracted from `oft.move` for cross-chain flexibility + public(friend) fun deposit_coin(_account: address, _coin: Coin) { + abort ENOT_IMPLEMENTED + } + + /// Withdraw coin function abstracted from `oft.move` for cross-chain flexibility + public(friend) fun withdraw_coin(_account: &signer, _amount_ld: u64): Coin { + abort ENOT_IMPLEMENTED + } + + // =================================================== Metadata =================================================== + + public(friend) fun send_standards_supported(): vector> { + vector[b"fungible_asset"] + } + + public(friend) fun metadata(): Object acquires OftImpl { + *option::borrow(&store().metadata) + } + + fun assert_metadata(fa: &FungibleAsset) acquires OftImpl { + let fa_metadata = fungible_asset::metadata_from_asset(fa); + assert!(fa_metadata == metadata(), EWRONG_FA_METADATA); + } + + public(friend) fun balance(account: address): u64 acquires OftImpl { + primary_fungible_store::balance(account, metadata()) + } + + /// Present for compatibility only + struct PlaceholderCoin {} + + // ================================================= Configuration ================================================ + + /// Set the fee (in BPS) for outbound OFT sends + public entry fun set_fee_bps(admin: &signer, fee_bps: u64) { + assert_admin(address_of(admin)); + oft_impl_config::set_fee_bps(fee_bps); + } + + #[view] + /// Get the fee (in BPS) for outbound OFT sends + public fun fee_bps(): u64 { oft_impl_config::fee_bps() } + + /// Set the fee deposit address for outbound OFT sends + public entry fun set_fee_deposit_address(admin: &signer, fee_deposit_address: address) { + assert_admin(address_of(admin)); + oft_impl_config::set_fee_deposit_address(fee_deposit_address); + } + + #[view] + /// Get the fee deposit address for outbound OFT sends + public fun fee_deposit_address(): address { oft_impl_config::fee_deposit_address() } + + /// Permanently disable the ability to blocklist addresses + public entry fun irrevocably_disable_blocklist(admin: &signer) { + assert_admin(address_of(admin)); + oft_impl_config::irrevocably_disable_blocklist(); + } + + /// Set the blocklist status of a wallet address + /// If a wallet is blocklisted + /// - OFT sends from the wallet will be blocked + /// - OFT receives to the wallet will be be diverted to the admin + public entry fun set_blocklist(admin: &signer, wallet: address, block: bool) { + assert_admin(address_of(admin)); + oft_impl_config::set_blocklist(wallet, block); + } + + #[view] + /// Get the blocklist status of a wallet address + public fun is_blocklisted(wallet: address): bool { oft_impl_config::is_blocklisted(wallet) } + + /// Set the rate limit configuration for a given endpoint ID + /// The rate limit is the maximum amount of OFT that can be sent to the endpoint within a given window + /// The rate limit capacity recovers linearly at a rate of limit / window_seconds + /// *Important*: Setting the rate limit does not reset the current "in-flight" volume (in-flight refers to the + /// decayed rate limit consumption). This means that if the rate limit is set lower than the current in-flight + /// volume, the endpoint will not be able to receive OFT until the in-flight volume decays below the new rate limit. + /// In order to reset the in-flight volume, the rate limit must be unset and then set again. + public entry fun set_rate_limit(admin: &signer, eid: u32, limit: u64, window_seconds: u64) { + assert_admin(address_of(admin)); + oft_impl_config::set_rate_limit(eid, limit, window_seconds); + } + + /// Unset the rate limit + public entry fun unset_rate_limit(admin: &signer, eid: u32) { + assert_admin(address_of(admin)); + oft_impl_config::unset_rate_limit(eid); + } + + #[view] + /// Get the rate limit configuration for a given endpoint ID + /// @return (limit, window_seconds) + public fun rate_limit_config(eid: u32): (u64, u64) { oft_impl_config::rate_limit_config(eid) } + + #[view] + /// Get the amount of rate limit capacity currently consumed on this pathway + public fun rate_limit_in_flight(eid: u32): u64 { oft_impl_config::in_flight(eid) } + + #[view] + /// Get the rate limit capacity for a given endpoint ID + public fun rate_limit_capacity(eid: u32): u64 { oft_impl_config::rate_limit_capacity(eid) } + + /// Create an OftLimit that reflects the rate limit for a given endpoint ID + public fun rate_limited_oft_limit(eid: u32): OftLimit { + oft_limit::new_oft_limit(0, oft_impl_config::rate_limit_capacity(eid)) + } + + #[view] + /// Total value locked in the contract + public fun tvl(): u64 acquires OftImpl { + let escrow = escrow_address(); + primary_fungible_store::balance(escrow, metadata()) + } + + // ================================================ Initialization ================================================ + + public entry fun initialize( + account: &signer, + token_metadata_address: address, + shared_decimals: u8 + ) acquires OftImpl { + // Only the admin can initialize the OFT + assert_admin(address_of(account)); + + // Ensure the metadata address provided is valid and store it + assert!(object_exists(token_metadata_address), EINVALID_METADATA_ADDRESS); + let metadata = address_to_object(token_metadata_address); + store_mut().metadata = option::some(metadata); + + // Initialize the OFT Core + let local_decimals = fungible_asset::decimals(metadata); + oft_core::initialize(local_decimals, shared_decimals); + } + + fun init_module(account: &signer) { + // Create the escrow object + let constructor_ref = &object::create_named_object(account, b"fa_escrow"); + let escrow_extend_ref = object::generate_extend_ref(constructor_ref); + + // Disable the transfer of the escrow object + object::disable_ungated_transfer(&object::generate_transfer_ref(constructor_ref)); + + // Initialize the storage and save the ExtendRef for future signer generation + move_to(move account, OftImpl { + metadata: option::none(), + escrow_extend_ref, + }); + } + + #[test_only] + public fun init_module_for_test() { + init_module(&std::account::create_signer_for_test(OAPP_ADDRESS())); + } + + // ================================================ Storage Helpers =============================================== + + #[view] + public fun escrow_address(): address acquires OftImpl { + object::address_from_extend_ref(&store().escrow_extend_ref) + } + + inline fun store(): &OftImpl { + borrow_global(OAPP_ADDRESS()) + } + + inline fun store_mut(): &mut OftImpl { + borrow_global_mut(OAPP_ADDRESS()) + } + + // ================================================== Error Codes ================================================= + + const EINVALID_METADATA_ADDRESS: u64 = 1; + const ENOT_IMPLEMENTED: u64 = 2; + const EWRONG_FA_METADATA: u64 = 3; +} \ No newline at end of file diff --git a/examples/oft-adapter-aptos-move/sources/shared_oapp/oapp_compose.move b/examples/oft-adapter-aptos-move/sources/shared_oapp/oapp_compose.move new file mode 100644 index 000000000..9eee66ab0 --- /dev/null +++ b/examples/oft-adapter-aptos-move/sources/shared_oapp/oapp_compose.move @@ -0,0 +1,82 @@ +/// Compose module for an OApp +/// This only needs to be included if the OApp needs to be a compose recipient +module oft::oapp_compose { + use std::fungible_asset::FungibleAsset; + use std::option::{Self, Option}; + use std::string::utf8; + use std::type_info::{module_name, type_of}; + + use endpoint_v2::endpoint::{Self, get_guid_and_index_from_wrapped, wrap_guid_and_index, WrappedGuidAndIndex}; + use endpoint_v2_common::bytes32::to_bytes32; + use oft::oapp_store; + use oft::oapp_store::is_native_token; + use oft::oft::lz_compose_impl; + + /// LZ Compose function for self-execution + public entry fun lz_compose( + from: address, + guid: vector, + index: u16, + message: vector, + extra_data: vector, + ) { + let guid = to_bytes32(guid); + endpoint::clear_compose(&oapp_store::call_ref(), from, wrap_guid_and_index(guid, index), message); + + lz_compose_impl( + from, + guid, + index, + message, + extra_data, + option::none(), + ) + } + + /// LZ Compose function to be called by the Executor + /// This is able to be provided a compose value in the form of a FungibleAsset + /// For self-executing with a value, this should be called with a script + public fun lz_compose_with_value( + from: address, + guid_and_index: WrappedGuidAndIndex, + message: vector, + extra_data: vector, + value: Option, + ) { + // Make sure that the value provided is of the native token type + assert!(option::is_none(&value) || is_native_token(option::borrow(&value)), EINVALID_TOKEN); + + // Unwrap the guid and index from the wrapped guid and index, this wrapping + let (guid, index) = get_guid_and_index_from_wrapped(&guid_and_index); + + endpoint::clear_compose(&oapp_store::call_ref(), from, guid_and_index, message); + + lz_compose_impl( + from, + guid, + index, + message, + extra_data, + value, + ); + } + + // ================================================ Initialization ================================================ + + fun init_module(account: &signer) { + let module_name = module_name(&type_of()); + endpoint::register_composer(account, utf8(module_name)); + } + + /// Struct to dynamically derive the module name to register on the endpoint + struct LzComposeModule {} + + #[test_only] + public fun init_module_for_test() { + init_module(&std::account::create_signer_for_test(oft::oapp_store::OAPP_ADDRESS())); + } + + // ================================================== Error Codes ================================================= + + const EINVALID_TOKEN: u64 = 1; +} diff --git a/examples/oft-adapter-aptos-move/sources/shared_oapp/oapp_core.move b/examples/oft-adapter-aptos-move/sources/shared_oapp/oapp_core.move new file mode 100644 index 000000000..2c5c0a7fd --- /dev/null +++ b/examples/oft-adapter-aptos-move/sources/shared_oapp/oapp_core.move @@ -0,0 +1,408 @@ +/// This OApp Core module provides the common functionality for OApps to interact with the LayerZero Endpoint V2 module. +/// It gives the ability to send messages, set configurations, manage peers and delegates, and enforce options. +/// +/// This should generally not need to be edited by OApp developers, except to update friend declarations to reflect +/// the modules that depend on the friend functions called in this module. +module oft::oapp_core { + use std::event::emit; + use std::fungible_asset::FungibleAsset; + use std::option::{Self, Option}; + use std::primary_fungible_store; + use std::signer::address_of; + use std::string::String; + use std::vector; + + use endpoint_v2::endpoint::{Self, wrap_guid}; + use endpoint_v2::messaging_receipt::MessagingReceipt; + use endpoint_v2_common::bytes32::{Bytes32, from_bytes32, to_bytes32, ZEROS_32_BYTES}; + use endpoint_v2_common::native_token; + use endpoint_v2_common::serde; + use endpoint_v2_common::universal_config::get_zro_metadata; + use oft::oapp_store::{Self, OAPP_ADDRESS}; + + friend oft::oft; + + #[test_only] + friend oft::oapp_core_tests; + #[test_only] + friend oft::oft_core_tests; + + // ==================================================== Send =================================================== + + /// This handles sending a message to a remote OApp on the configured peer + public(friend) fun lz_send( + dst_eid: u32, + message: vector, + options: vector, + native_fee: &mut FungibleAsset, + zro_fee: &mut Option, + ): MessagingReceipt { + endpoint::send( + &oapp_store::call_ref(), + dst_eid, + get_peer_bytes32(dst_eid), + message, + options, + native_fee, + zro_fee, + ) + } + + #[view] + /// This provides a LayerZero quote for sending a message + public fun lz_quote( + dst_eid: u32, + message: vector, + options: vector, + pay_in_zro: bool, + ): (u64, u64) { + endpoint::quote(OAPP_ADDRESS(), dst_eid, get_peer_bytes32(dst_eid), message, options, pay_in_zro) + } + + // ==================================================== Compose =================================================== + + public(friend) fun lz_send_compose( + to: address, + index: u16, + guid: Bytes32, + message: vector, + ) { + endpoint::send_compose(&oapp_store::call_ref(), to, index, guid, message); + } + + // ================================================ Delegated Calls =============================================== + + /// Asserts that the delegated call is "authorized," (the assigned delegate) + /// "authorized" indicates a wallet has permission to act on behalf of the OApp in respect to endpoint calls, + /// for example, "set_send_library()" or "skip()," but this does not extend to calls that are internal to (stored + /// on) the OApp like "set_peer()," which is are "admin only" permissions + fun assert_authorized(account: address) { + assert!(account == oapp_store::get_delegate(), EUNAUTHORIZED); + } + + /// Set the OApp configuration for a Message Library + public entry fun set_config( + account: &signer, + msglib: address, + eid: u32, + config_type: u32, + config: vector, + ) { + assert_authorized(address_of(move account)); + endpoint::set_config(&oapp_store::call_ref(), msglib, eid, config_type, config) + } + + /// Set the Send Library for an OApp + public entry fun set_send_library( + account: &signer, + remote_eid: u32, + msglib: address, + ) { + assert_authorized(address_of(move account)); + endpoint::set_send_library(&oapp_store::call_ref(), remote_eid, msglib) + } + + /// Set the Receive Library for an OApp + public entry fun set_receive_library( + account: &signer, + remote_eid: u32, + msglib: address, + grace_period: u64, + ) { + assert_authorized(address_of(move account)); + endpoint::set_receive_library(&oapp_store::call_ref(), remote_eid, msglib, grace_period) + } + + /// Update the Receive Library Expiry for an OApp + public entry fun set_receive_library_timeout( + account: &signer, + remote_eid: u32, + msglib: address, + expiry: u64, + ) { + assert_authorized(address_of(move account)); + endpoint::set_receive_library_timeout(&oapp_store::call_ref(), remote_eid, msglib, expiry) + } + + /// Register a Receive Pathway for an OApp + public entry fun register_receive_pathway( + account: &signer, + src_eid: u32, + src_oapp: vector, + ) { + assert_authorized(address_of(move account)); + endpoint::register_receive_pathway(&oapp_store::call_ref(), src_eid, to_bytes32(src_oapp)) + } + + /// Clear an OApp message + public entry fun clear( + account: &signer, + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + message: vector, + ) { + assert_authorized(address_of(move account)); + endpoint::clear( + &oapp_store::call_ref(), + src_eid, + to_bytes32(sender), + nonce, + wrap_guid(to_bytes32(guid)), + message, + ) + } + + /// Skip an OApp message + public entry fun skip( + account: &signer, + src_eid: u32, + sender: vector, + nonce: u64, + ) { + assert_authorized(address_of(move account)); + endpoint::skip(&oapp_store::call_ref(), src_eid, to_bytes32(sender), nonce) + } + + /// Burn an OApp message + public entry fun burn( + account: &signer, + src_eid: u32, + sender: vector, + nonce: u64, + payload_hash: vector, + ) { + assert_authorized(address_of(move account)); + endpoint::burn(&oapp_store::call_ref(), src_eid, to_bytes32(sender), nonce, to_bytes32(payload_hash)) + } + + /// Nilify an OApp message + public entry fun nilify( + account: &signer, + src_eid: u32, + sender: vector, + nonce: u64, + payload_hash: vector, + ) { + assert_authorized(address_of(move account)); + endpoint::nilify(&oapp_store::call_ref(), src_eid, to_bytes32(sender), nonce, to_bytes32(payload_hash)) + } + + // =============================================== Enforced Options =============================================== + + #[view] + public fun get_enforced_options(eid: u32, msg_type: u16): vector { + oapp_store::get_enforced_options(eid, msg_type) + } + + public entry fun set_enforced_options( + account: &signer, + eid: u32, + msg_type: u16, + enforced_options: vector, + ) { + assert_admin(address_of(move account)); + assert_options_type_3(enforced_options); + oapp_store::set_enforced_options(eid, msg_type, enforced_options); + emit(EnforcedOptionSet { eid, msg_type, enforced_options }); + } + + #[view] + public fun combine_options(eid: u32, msg_type: u16, extra_options: vector): vector { + let enforced_options = oapp_store::get_enforced_options(eid, msg_type); + if (vector::is_empty(&enforced_options)) { return extra_options }; + if (vector::is_empty(&extra_options)) { return enforced_options }; + assert_options_type_3(extra_options); + vector::append(&mut enforced_options, serde::extract_bytes_until_end(&extra_options, &mut 2)); + enforced_options + } + + // ===================================================== Admin ==================================================== + + #[view] + /// Gets the admin address + public fun get_admin(): address { + oapp_store::get_admin() + } + + /// Change the admin of the OApp to another account + public entry fun transfer_admin(account: &signer, new_admin: address) { + let admin = address_of(move account); + assert_admin(admin); + assert!(std::account::exists_at(new_admin), EINVALID_ACCOUNT); + oapp_store::set_admin(new_admin); + emit(AdminTransferred { admin: new_admin }); + } + + /// Permanently renounce OApp admin rights. Once this is called the admin cannot be reinstated + public entry fun renounce_admin(account: &signer) { + let admin = address_of(move account); + assert_admin(admin); + oapp_store::set_admin(@0x0); + emit(AdminTransferred { admin: @0x0 }); + } + + /// Asserts that a user address is the OApp admin. This admin can make any configuration change that directly lives + /// on the OApp (like setting the peer), but it does not include permission to make configuration changes or act on + /// behalf of the OApp on the Endpoint, which requires "authorized" permission + public fun assert_admin(admin: address) { + assert!(admin == oapp_store::get_admin(), EUNAUTHORIZED); + } + + // ===================================================== Peers ==================================================== + + #[view] + public fun has_peer(eid: u32): bool { + oapp_store::has_peer(eid) + } + + #[view] + public fun get_peer(eid: u32): vector { + from_bytes32(get_peer_bytes32(eid)) + } + + public fun get_peer_bytes32(eid: u32): Bytes32 { + assert!(oapp_store::has_peer(eid), EUNCONFIGURED_PEER); + oapp_store::get_peer(eid) + } + + public entry fun set_peer(account: &signer, eid: u32, peer: vector) { + assert_admin(address_of(move account)); + // Automatically register the receive pathway when a peer is set + endpoint::register_receive_pathway(&oapp_store::call_ref(), eid, to_bytes32(peer)); + // Set the peer + let peer_bytes32 = to_bytes32(peer); + oapp_store::set_peer(eid, peer_bytes32); + emit(PeerSet { eid, peer }); + } + + public entry fun remove_peer(account: &signer, eid: u32) { + assert_admin(address_of(move account)); + assert!(oapp_store::has_peer(eid), EUNCONFIGURED_PEER); + oapp_store::remove_peer(eid); + emit(PeerSet { eid, peer: ZEROS_32_BYTES() }); + } + + // =================================================== Delegates ================================================== + + #[view] + public fun has_delegate(): bool { + oapp_store::get_delegate() != @0x0 + } + + #[view] + public fun get_delegate(): address { + oapp_store::get_delegate() + } + + /// Set the delegate address for the OApp - set to @0x0 to remove the delegate + public entry fun set_delegate(account: &signer, delegate: address) { + assert_admin(address_of(move account)); + oapp_store::set_delegate(delegate); + emit(DelegateSet { delegate }); + } + + // ==================================================== General =================================================== + + #[view] + public fun get_lz_receive_module_name(): String { + endpoint::get_lz_receive_module(oft::oapp_store::OAPP_ADDRESS()) + } + + #[view] + public fun get_lz_compose_module_name(): String { + endpoint::get_lz_compose_module(oft::oapp_store::OAPP_ADDRESS()) + } + + // ===================================================== Utils ==================================================== + + /// Utility function to withdraw the specified native and zro fees from the provided account + public fun withdraw_lz_fees( + account: &signer, + native_fee: u64, + zro_fee: u64, + ): (FungibleAsset, Option) { + assert!(native_token::balance(address_of(account)) >= native_fee, EINSUFFICIENT_NATIVE_TOKEN_BALANCE); + let native_fee_fa = native_token::withdraw(account, native_fee); + let zro_fee_fa = if (zro_fee > 0) { + let zro_metadata = get_zro_metadata(); + assert!( + primary_fungible_store::balance(address_of(account), zro_metadata) >= zro_fee, + EINSUFFICIENT_ZRO_BALANCE, + ); + option::some(primary_fungible_store::withdraw(account, zro_metadata, zro_fee)) + } else option::none(); + (native_fee_fa, zro_fee_fa) + } + + /// Utility function to refund the specified fees to the provided account address + public fun refund_fees(account: address, native_fee_fa: FungibleAsset, zro_fee_fa: Option) { + primary_fungible_store::deposit(account, native_fee_fa); + option::destroy(zro_fee_fa, |zro_fee_fa| primary_fungible_store::deposit(account, zro_fee_fa)); + } + + // ==================================================== Helpers =================================================== + + /// Assert that an option is a type 3 option (begins with 0x0003) + public fun assert_options_type_3(options: vector) { + assert!(vector::length(&options) >= 2, EINVALID_OPTIONS); + let options_type = serde::extract_u16(&options, &mut 0); + assert!(options_type == 3, EINVALID_OPTIONS); + } + + // ==================================================== Events ==================================================== + + #[event] + struct AdminTransferred has drop, store { + admin: address, + } + + #[event] + struct PeerSet has drop, store { + eid: u32, + // Peer address - all zeros (0x00*32) if unset + peer: vector, + } + + #[event] + struct DelegateSet has drop, store { + delegate: address, + } + + #[event] + struct EnforcedOptionSet has drop, store { + eid: u32, + msg_type: u16, + enforced_options: vector, + } + + #[test_only] + public fun admin_transferred_event(admin: address): AdminTransferred { + AdminTransferred { admin } + } + + #[test_only] + public fun peer_set_event(eid: u32, peer: vector): PeerSet { + PeerSet { eid, peer } + } + + #[test_only] + public fun delegate_set_event(delegate: address): DelegateSet { + DelegateSet { delegate } + } + + #[test_only] + public fun enforced_option_set_event(eid: u32, msg_type: u16, enforced_options: vector): EnforcedOptionSet { + EnforcedOptionSet { eid, msg_type, enforced_options } + } + + // ================================================== Error Codes ================================================= + + const EUNAUTHORIZED: u64 = 1; + const EUNCONFIGURED_PEER: u64 = 2; + const EINSUFFICIENT_NATIVE_TOKEN_BALANCE: u64 = 3; + const EINSUFFICIENT_ZRO_BALANCE: u64 = 4; + const EINVALID_OPTIONS: u64 = 5; + const EINVALID_ACCOUNT: u64 = 6; +} diff --git a/examples/oft-adapter-aptos-move/sources/shared_oapp/oapp_receive.move b/examples/oft-adapter-aptos-move/sources/shared_oapp/oapp_receive.move new file mode 100644 index 000000000..e3bf7862b --- /dev/null +++ b/examples/oft-adapter-aptos-move/sources/shared_oapp/oapp_receive.move @@ -0,0 +1,93 @@ +/// This is an internal module that receives the lz_receive call from the Executor. This in turn calls the handler in +/// `oapp_receive_handler::lz_receive_impl` which is implemented by the OApp developer. +/// +/// This module should generally not be modified by the OApp developer. +module oft::oapp_receive { + use std::fungible_asset::FungibleAsset; + use std::option::{Self, Option}; + use std::string::utf8; + use std::type_info::{module_name, type_of}; + + use endpoint_v2::endpoint::{Self, get_guid_from_wrapped, wrap_guid, WrappedGuid}; + use endpoint_v2_common::bytes32::to_bytes32; + use oft::oapp_core::get_peer_bytes32; + use oft::oapp_store; + use oft::oapp_store::is_native_token; + use oft::oft::{lz_receive_impl, next_nonce_impl}; + + /// LZ Receive function for self-execution + public entry fun lz_receive( + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + message: vector, + extra_data: vector, + ) { + lz_receive_with_value( + src_eid, + sender, + nonce, + wrap_guid(to_bytes32(guid)), + message, + extra_data, + option::none(), + ) + } + + /// LZ Receive function to be called by the Executor + /// This is able to be provided a receive value in the form of a FungibleAsset + /// For self-executing with a value, this should be called with a script + /// The WrappedGuid is used by the caller script to enforce that the LayerZero endpoint is called by the OApp + public fun lz_receive_with_value( + src_eid: u32, + sender: vector, + nonce: u64, + wrapped_guid: WrappedGuid, + message: vector, + extra_data: vector, + value: Option, + ) { + assert!(option::is_none(&value) || is_native_token(option::borrow(&value)), EINVALID_TOKEN); + let sender = to_bytes32(sender); + assert!(get_peer_bytes32(src_eid) == sender, ENOT_PEER); + + let guid = get_guid_from_wrapped(&wrapped_guid); + endpoint::clear(&oapp_store::call_ref(), src_eid, sender, nonce, wrapped_guid, message); + + lz_receive_impl( + src_eid, + sender, + nonce, + guid, + message, + extra_data, + value, + ); + } + + #[view] + /// Get the next nonce for the given pathway + public fun next_nonce(src_eid: u32, sender: vector): u64 { + next_nonce_impl(src_eid, to_bytes32(sender)) + } + + // ================================================ Initialization ================================================ + + fun init_module(account: &signer) { + let module_name = module_name(&type_of()); + endpoint::register_oapp(account, utf8(module_name)); + } + + struct LzReceiveModule {} + + #[test_only] + public fun init_module_for_test() { + init_module(&std::account::create_signer_for_test(oft::oapp_store::OAPP_ADDRESS())); + } + + // ================================================== Error Codes ================================================= + + const EINVALID_TOKEN: u64 = 1; + const ENOT_PEER: u64 = 2; +} diff --git a/examples/oft-adapter-aptos-move/sources/shared_oapp/oapp_store.move b/examples/oft-adapter-aptos-move/sources/shared_oapp/oapp_store.move new file mode 100644 index 000000000..94ec90567 --- /dev/null +++ b/examples/oft-adapter-aptos-move/sources/shared_oapp/oapp_store.move @@ -0,0 +1,116 @@ +/// The Internal store module for the OApp Core and Receive modules. +/// +/// This module should generally not be modified by the OApp developer. +module oft::oapp_store { + use std::fungible_asset::{Self, FungibleAsset}; + use std::object::object_address; + use std::table::{Self, Table}; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::contract_identity::{Self, CallRef, ContractSigner, create_contract_signer, DynamicCallRef}; + + friend oft::oapp_core; + friend oft::oapp_receive; + friend oft::oapp_compose; + friend oft::oft; + + // ************************************************* CONFIGURATION ************************************************* + + /// The address of the OApp + public inline fun OAPP_ADDRESS(): address { @oft } + + // *********************************************** END CONFIGURATION *********************************************** + + struct OAppStore has key { + contract_signer: ContractSigner, + admin: address, + peers: Table, + delegate: address, + enforced_options: Table>, + } + + /// Enforced Options are stored by the OApp by EID and a OApp-specific "message type" (u16) + struct EnforcedOptionsKey has store, copy, drop { eid: u32, msg_type: u16 } + + // =================================================== Call Ref =================================================== + + public(friend) fun call_ref(): CallRef acquires OAppStore { + contract_identity::make_call_ref(&store().contract_signer) + } + + public(friend) fun dynamic_call_ref(target: address, auth: vector): DynamicCallRef acquires OAppStore { + contract_identity::make_dynamic_call_ref(&store().contract_signer, target, auth) + } + + // =============================================== Enforced Options =============================================== + + public(friend) fun get_enforced_options(eid: u32, msg_type: u16): vector acquires OAppStore { + *table::borrow_with_default(&store().enforced_options, EnforcedOptionsKey { eid, msg_type }, &b"") + } + + public(friend) fun set_enforced_options(eid: u32, msg_type: u16, option: vector) acquires OAppStore { + table::upsert(&mut store_mut().enforced_options, EnforcedOptionsKey { eid, msg_type }, option) + } + + // ===================================================== Admin ==================================================== + + public(friend) fun get_admin(): address acquires OAppStore { store().admin } + + public(friend) fun set_admin(admin: address) acquires OAppStore { + store_mut().admin = admin; + } + + // ===================================================== Peers ==================================================== + + public(friend) fun has_peer(eid: u32): bool acquires OAppStore { + table::contains(&store().peers, eid) + } + + public(friend) fun get_peer(eid: u32): Bytes32 acquires OAppStore { + *table::borrow(&store().peers, eid) + } + + public(friend) fun set_peer(eid: u32, peer: Bytes32) acquires OAppStore { + table::upsert(&mut store_mut().peers, eid, peer) + } + + public(friend) fun remove_peer(eid: u32) acquires OAppStore { + table::remove(&mut store_mut().peers, eid); + } + + // =================================================== Delegate =================================================== + + public(friend) fun get_delegate(): address acquires OAppStore { store().delegate } + + public(friend) fun set_delegate(delegate: address) acquires OAppStore { + store_mut().delegate = delegate; + } + + // ===================================================== Misc ===================================================== + + /// Checks that a token is the native token + public(friend) fun is_native_token(token: &FungibleAsset): bool { + object_address(&fungible_asset::asset_metadata(token)) == @native_token_metadata_address + } + + inline fun store(): &OAppStore { borrow_global(OAPP_ADDRESS()) } + + inline fun store_mut(): &mut OAppStore { borrow_global_mut(OAPP_ADDRESS()) } + + // ================================================ Initialization ================================================ + + fun init_module(account: &signer) { + move_to(account, OAppStore { + contract_signer: create_contract_signer(account), + admin: @oft_admin, + peers: table::new(), + delegate: @0x0, + enforced_options: table::new(), + }); + } + + #[test_only] + public fun init_module_for_test() { + init_module(&std::account::create_signer_for_test(OAPP_ADDRESS())); + } +} diff --git a/examples/oft-adapter-aptos-move/sources/shared_oft/oft.move b/examples/oft-adapter-aptos-move/sources/shared_oft/oft.move new file mode 100644 index 000000000..b992b6b68 --- /dev/null +++ b/examples/oft-adapter-aptos-move/sources/shared_oft/oft.move @@ -0,0 +1,419 @@ +/// This is the OFT interface that provides send, quote, and view functions for the OFT. +/// +/// The OFT developer should update the name of the implementation module in the configuration section of this module. +/// Other than that, this module generally does not need to be updated by the OFT developer. As much as possible, +/// customizations should be made in the OFT implementation module. +module oft::oft { + use std::coin::Coin; + use std::fungible_asset::{FungibleAsset, Metadata, metadata_from_asset}; + use std::object::{Object, object_address}; + use std::option::Option; + use std::primary_fungible_store; + use std::signer::address_of; + + use endpoint_v2::messaging_receipt::MessagingReceipt; + use endpoint_v2_common::bytes32::{Bytes32, to_bytes32}; + use endpoint_v2_common::contract_identity::{DynamicCallRef, get_dynamic_call_ref_caller}; + use oft::oapp_core::{Self, lz_quote, lz_send, lz_send_compose, refund_fees, withdraw_lz_fees}; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_adapter_fa::{ + balance as balance_internal, + build_options, + credit, + debit_coin, + debit_fungible_asset, + debit_view as debit_view_internal, + deposit_coin, + inspect_message, + metadata as metadata_internal, + oft_limit_and_fees, + PlaceholderCoin, + send_standards_supported as send_standards_supported_internal, + withdraw_coin, + }; + use oft::oft_core; + use oft_common::oft_fee_detail::OftFeeDetail; + use oft_common::oft_limit::OftLimit; + + friend oft::oapp_receive; + friend oft::oapp_compose; + + // ======================================== For FungibleAsset Enabled OFTs ======================================== + + /// This is called to send an amount in FungibleAsset to a recipient on another EID + public fun send( + call_ref: &DynamicCallRef, + dst_eid: u32, + to: Bytes32, + send_value: &mut FungibleAsset, + min_amount_ld: u64, + extra_options: vector, + compose_message: vector, + oft_cmd: vector, + native_fee: &mut FungibleAsset, + zro_fee: &mut Option, + ): (MessagingReceipt, OftReceipt) { + let sender = get_dynamic_call_ref_caller(call_ref, OAPP_ADDRESS(), b"send"); + send_internal( + sender, dst_eid, to, send_value, min_amount_ld, extra_options, compose_message, oft_cmd, native_fee, + zro_fee, + ) + } + + /// Send from an account to a recipient on another EID, deducting the fees and the amount to send from the sender's + /// account + public entry fun send_withdraw( + account: &signer, + dst_eid: u32, + to: vector, + amount_ld: u64, + min_amount_ld: u64, + extra_options: vector, + compose_message: vector, + oft_cmd: vector, + native_fee: u64, + zro_fee: u64, + ) { + // Withdraw the amount and fees from the account + assert!( + primary_fungible_store::balance(address_of(account), metadata()) >= amount_ld, + EINSUFFICIENT_BALANCE, + ); + let send_value = primary_fungible_store::withdraw(account, metadata(), amount_ld); + let (native_fee_fa, zro_fee_fa) = withdraw_lz_fees(account, native_fee, zro_fee); + let sender = address_of(move account); + + send_internal( + sender, dst_eid, to_bytes32(to), &mut send_value, min_amount_ld, extra_options, compose_message, oft_cmd, + &mut native_fee_fa, &mut zro_fee_fa, + ); + + // Return unused amounts and fees to the account + refund_fees(sender, native_fee_fa, zro_fee_fa); + primary_fungible_store::deposit(sender, send_value); + } + + fun send_internal( + sender: address, + dst_eid: u32, + to: Bytes32, + send_value: &mut FungibleAsset, + min_amount_ld: u64, + extra_options: vector, + compose_message: vector, + oft_cmd: vector, + native_fee: &mut FungibleAsset, + zro_fee: &mut Option, + ): (MessagingReceipt, OftReceipt) { + assert!(metadata_from_asset(send_value) == metadata(), EINVALID_METADATA); + let (messaging_receipt, amount_sent_ld, amount_received_ld) = oft_core::send( + sender, + dst_eid, + to, + compose_message, + |message, options| { + lz_send(dst_eid, message, options, native_fee, zro_fee) + }, + |_nothing| debit_fungible_asset(sender, send_value, min_amount_ld, dst_eid), + |amount_received_ld, message_type| build_options( + message_type, + dst_eid, + extra_options, + sender, + amount_received_ld, + to, + compose_message, + oft_cmd, + ), + |message, options| inspect_message(message, options, true), + ); + (messaging_receipt, OftReceipt { amount_sent_ld, amount_received_ld }) + } + + // ============================================= For Coin-enabled OFTs ============================================ + + /// This is called to send an amount in Coin to a recipient on another EID + public fun send_coin( + call_ref: &DynamicCallRef, + dst_eid: u32, + to: Bytes32, + send_value: &mut Coin, + min_amount_ld: u64, + extra_options: vector, + compose_message: vector, + oft_cmd: vector, + native_fee: &mut FungibleAsset, + zro_fee: &mut Option, + ): (MessagingReceipt, OftReceipt) { + let sender = get_dynamic_call_ref_caller(call_ref, OAPP_ADDRESS(), b"send_coin"); + send_coin_internal( + sender, dst_eid, to, send_value, min_amount_ld, extra_options, compose_message, oft_cmd, native_fee, + zro_fee, + ) + } + + /// Send from an amount to a recipient on another EID, deducting the fees and the amount from the sender's account + public entry fun send_withdraw_coin( + account: &signer, + dst_eid: u32, + to: vector, + amount_ld: u64, + min_amount_ld: u64, + extra_options: vector, + compose_message: vector, + oft_cmd: vector, + native_fee: u64, + zro_fee: u64, + ) { + // Withdraw the amount and fees from the account + let send_value = withdraw_coin(account, amount_ld); + let (native_fee_fa, zro_fee_fa) = withdraw_lz_fees(account, native_fee, zro_fee); + + let sender = address_of(move account); + + send_coin_internal( + sender, dst_eid, to_bytes32(to), &mut send_value, min_amount_ld, extra_options, compose_message, oft_cmd, + &mut native_fee_fa, &mut zro_fee_fa, + ); + + // Return unused amounts and fees back to the account + refund_fees(sender, native_fee_fa, zro_fee_fa); + deposit_coin(sender, send_value); + } + + /// This is called to send an amount in Coin to a recipient on another EID + fun send_coin_internal( + sender: address, + dst_eid: u32, + to: Bytes32, + send_value: &mut Coin, + min_amount_ld: u64, + extra_options: vector, + compose_message: vector, + oft_cmd: vector, + native_fee: &mut FungibleAsset, + zro_fee: &mut Option, + ): (MessagingReceipt, OftReceipt) { + let (messaging_receipt, amount_sent_ld, amount_received_ld) = oft_core::send( + sender, + dst_eid, + to, + compose_message, + |message, options| { + lz_send(dst_eid, message, options, native_fee, zro_fee) + }, + |_nothing| debit_coin(sender, send_value, min_amount_ld, dst_eid), + |amount_received_ld, message_type| build_options( + message_type, + dst_eid, + extra_options, + sender, + amount_received_ld, + to, + compose_message, + oft_cmd, + ), + |message, options| inspect_message(message, options, true), + ); + (messaging_receipt, OftReceipt { amount_sent_ld, amount_received_ld }) + } + + + // ===================================================== Quote ==================================================== + + #[view] + /// Quote the OFT for a particular send without sending + /// @return ( + /// oft_limit: The minimum and maximum limits that can be sent to the recipient + /// fees: The fees that will be applied to the amount sent + /// amount_sent_ld: The amount that would be debited from the sender in local decimals + /// amount_received_ld: The amount that would be received by the recipient in local decimals + /// ) + public fun quote_oft( + dst_eid: u32, + to: vector, + amount_ld: u64, + min_amount_ld: u64, + extra_options: vector, + compose_msg: vector, + oft_cmd: vector, + ): (OftLimit, vector, u64, u64) { + let (amount_sent_ld, amount_received_ld) = debit_view_internal(amount_ld, min_amount_ld, dst_eid); + + let (limit, fees) = oft_limit_and_fees( + dst_eid, + to, + amount_ld, + min_amount_ld, + extra_options, + compose_msg, + oft_cmd, + ); + (limit, fees, amount_sent_ld, amount_received_ld) + } + + #[view] + /// Quote the network fees for a particular send + /// @return (native_fee, zro_fee) + public fun quote_send( + user_sender: address, + dst_eid: u32, + to: vector, + amount_ld: u64, + min_amount_ld: u64, + extra_options: vector, + compose_message: vector, + oft_cmd: vector, + pay_in_zro: bool, + ): (u64, u64) { + oft_core::quote_send( + user_sender, + to, + compose_message, + |message, options| lz_quote(dst_eid, message, options, pay_in_zro), + |_nothing| debit_view_internal(amount_ld, min_amount_ld, dst_eid), + |amount_received_ld, message_type| build_options( + message_type, + dst_eid, + extra_options, + user_sender, + amount_received_ld, + to_bytes32(to), + compose_message, + oft_cmd, + ), + |message, options| inspect_message(message, options, false), + ) + } + + // ==================================================== Receive =================================================== + + public(friend) fun lz_receive_impl( + src_eid: u32, + _sender: Bytes32, + nonce: u64, + guid: Bytes32, + message: vector, + _extra_data: vector, + receive_value: Option, + ) { + oft_core::receive( + src_eid, + nonce, + guid, + message, + |to, index, message| lz_send_compose(to, index, guid, message), + |to, amount_ld| credit(to, amount_ld, src_eid, receive_value), + ); + } + + // ==================================================== Compose =================================================== + + public(friend) fun lz_compose_impl( + _from: address, + _guid: Bytes32, + _index: u16, + _message: vector, + _extra_data: vector, + _value: Option, + ) { + abort ECOMPOSE_NOT_IMPLEMENTED + } + + // =============================================== Ordered Execution ============================================== + + /// Provides the next nonce if executor options request ordered execution; returns 0 to indicate ordered execution + /// is disabled + public(friend) fun next_nonce_impl(_src_eid: u32, _sender: Bytes32): u64 { + 0 + } + + // ================================================== OFT Receipt ================================================= + + struct OftReceipt has drop, store { + amount_sent_ld: u64, + amount_received_ld: u64, + } + + public fun get_amount_sent_ld(receipt: &OftReceipt): u64 { receipt.amount_sent_ld } + + public fun get_amount_received_ld(receipt: &OftReceipt): u64 { receipt.amount_received_ld } + + public fun unpack_oft_receipt(receipt: &OftReceipt): (u64, u64) { + (receipt.amount_sent_ld, receipt.amount_received_ld) + } + + // ===================================================== View ===================================================== + + #[view] + public fun balance(account: address): u64 { + balance_internal(account) + } + + #[view] + /// The version of the OFT + /// @return (interface_id, protocol_version) + public fun oft_version(): (u64, u64) { + (1, 1) + } + + #[view] + public fun send_standards_supported(): vector> { + send_standards_supported_internal() + } + + #[view] + /// The address of the OFT token + public fun token(): address { + object_address(&metadata_internal()) + } + + #[view] + /// The metadata object of the OFT + public fun metadata(): Object { + metadata_internal() + } + + #[view] + public fun debit_view(amount_ld: u64, min_amount_ld: u64, _dst_eid: u32): (u64, u64) { + debit_view_internal(amount_ld, min_amount_ld, _dst_eid) + } + + #[view] + public fun to_ld(amount_sd: u64): u64 { oft_core::to_ld(amount_sd) } + + #[view] + public fun to_sd(amount_ld: u64): u64 { oft_core::to_sd(amount_ld) } + + #[view] + public fun remove_dust(amount_ld: u64): u64 { oft_core::remove_dust(amount_ld) } + + #[view] + public fun shared_decimals(): u8 { oft_core::shared_decimals() } + + #[view] + public fun decimal_conversion_rate(): u64 { oft_core::decimal_conversion_rate() } + + #[view] + /// Encode an OFT message + /// @return (message, message_type) + public fun encode_oft_msg( + sender: address, + amount_ld: u64, + to: vector, + compose_msg: vector, + ): (vector, u16) { + oft_core::encode_oft_msg(sender, amount_ld, to_bytes32(to), compose_msg) + } + + #[view] + public fun get_peer(eid: u32): vector { + oapp_core::get_peer(eid) + } + + // ================================================== Error Codes ================================================= + + const ECOMPOSE_NOT_IMPLEMENTED: u64 = 1; + const EINSUFFICIENT_BALANCE: u64 = 2; + const EINVALID_METADATA: u64 = 3; +} diff --git a/examples/oft-adapter-aptos-move/sources/shared_oft/oft_core.move b/examples/oft-adapter-aptos-move/sources/shared_oft/oft_core.move new file mode 100644 index 000000000..5ca625393 --- /dev/null +++ b/examples/oft-adapter-aptos-move/sources/shared_oft/oft_core.move @@ -0,0 +1,293 @@ +/// This provides core OFT functionality. +/// +/// This module should generally not be modified by the OFT developer except to correct the friend declarations to +/// match the modules that are actually used. +module oft::oft_core { + use std::event::emit; + use std::math64::pow; + use std::vector; + + use endpoint_v2::messaging_receipt::{get_guid, MessagingReceipt}; + use endpoint_v2_common::bytes32::{Self, Bytes32, from_bytes32, to_bytes32}; + use oft::oft_store; + use oft_common::oft_compose_msg_codec; + use oft_common::oft_msg_codec; + + friend oft::oft; + friend oft::oft_impl_config; + friend oft::oapp_receive; + #[test_only] + friend oft::oft_core_tests; + #[test_only] + friend oft::oft_impl_config_tests; + + friend oft::oft_adapter_fa; + #[test_only] + friend oft::oft_adapter_fa_tests; + #[test_only] + friend oft::oapp_receive_using_oft_adapter_fa_tests; + + // ===================================================== OFT Core ================================================= + + /// Send a message to a destination endpoint using the provided send implementation and debit behavior. + /// + /// @param user_sender: The address of the user sending the message + /// @param dst_eid: The destination endpoint ID + /// @param to: The destination wallet address + /// @param compose_message: The compose message to be sent + /// @param send_impl: A function to send the message + /// |message, options| MessagingReceipt + /// @param debit: A function to debit the user account (unused field included to prevent IDE error) + /// |_unused_field| (sent_amount_ld, received_amount_ld) + /// @param build_options: A function to build the options for the message + /// |received_amount_ld, msg_type| options + /// @param inspect: A function to inspect the message and options before sending + /// |message, options| () + /// @return (messaging_receipt, amount_sent_ld, amount_received_ld) + public(friend) inline fun send( + user_sender: address, + dst_eid: u32, + to: Bytes32, + compose_payload: vector, + send_impl: |vector, vector| MessagingReceipt, + debit: |bool /*unused*/| (u64, u64), + build_options: |u64, u16| vector, + inspect: |&vector, &vector|, + ): (MessagingReceipt, u64, u64) { + let (amount_sent_ld, amount_received_ld) = debit(true /*unused*/); + + let msg_type = if (vector::length(&compose_payload) > 0) { SEND_AND_CALL() } else { SEND() }; + let options = build_options(amount_received_ld, msg_type); + + // Construct message and options + let (message, _) = encode_oft_msg(user_sender, amount_received_ld, to, compose_payload); + + // Hook to inspect the message and options before sending + inspect(&message, &options); + + // Send by endpoint + let messaging_receipt = send_impl(message, options); + + emit_oft_sent( + from_bytes32(get_guid(&messaging_receipt)), + dst_eid, + user_sender, + amount_sent_ld, + amount_received_ld, + ); + + (messaging_receipt, amount_sent_ld, amount_received_ld) + } + + /// Handle a received packet + /// @param src_eid: The source endpoint ID + /// @param nonce: The nonce of the message + /// @param guid: The GUID of the message + /// @param message: The message received + /// @param send_compose: A function to send the compose message + /// |to_address, index, message| () + /// @param credit: A function to credit the user account + /// |to_address, amount_received_ld| credited_amount_ld + public(friend) inline fun receive( + src_eid: u32, + nonce: u64, + guid: Bytes32, + message: vector, + send_compose: |address, u16, vector| (), + credit: |address, u64| u64, + ) { + // Decode the message using the OFT v2 codec + let to_address = bytes32::to_address(oft_msg_codec::send_to(&message)); + let message_amount_ld = to_ld(oft_msg_codec::amount_sd(&message)); + let has_compose = oft_msg_codec::has_compose(&message); + + // Credit the user account + let amount_received_ld = credit(to_address, message_amount_ld); + + // Send compose payload if present + if (has_compose) { + let compose_payload = oft_msg_codec::compose_payload(&message); + let compose_message = oft_compose_msg_codec::encode( + nonce, + src_eid, + amount_received_ld, + compose_payload, + ); + // In the default implementation the compose index is always 0; send_compose accepts the index parameter + // for extensibility + send_compose(to_address, 0, compose_message); + }; + emit_oft_received(from_bytes32(guid), src_eid, to_address, amount_received_ld); + } + + /// Get a quote for the network fee for sending a message to a destination endpoint + /// @param user_sender: The address of the user sending the message + /// @param to: The destination wallet address + /// @param compose_message: The compose message to be included + /// @param quote_impl: A function to get a quote from the LayerZero endpoint + /// @param debit_view: A function that provides the debit amounts (unused param only included to prevent Move IDE error) + /// |_unused_field| (sent_amount_ld, received_amount_ld) + /// @param build_options: A function to build the options for the message + /// |received_amount_ld, msg_type| options + /// @param inspect: A function to inspect the message and options before quoting + /// |message, options| () + /// @return (native_fee, lz_fee) + public(friend) inline fun quote_send( + user_sender: address, + to: vector, + compose_message: vector, + quote_impl: |vector, vector| (u64, u64), + debit_view: |bool /*unused*/| (u64, u64), + build_options: |u64, u16| vector, + inspect: |&vector, &vector|, + ): (u64, u64) { + let (_, amount_received_ld) = debit_view(true /*unused*/); + let (message, msg_type) = encode_oft_msg(user_sender, amount_received_ld, to_bytes32(to), compose_message); + let options = build_options(amount_received_ld, msg_type); + + // Hook to inspect the message and options before sending + inspect(&message, &options); + + quote_impl(message, options) + } + + + // ===================================================== Utils ==================================================== + + /// This is a debit view implementation that can be used by the OFT to compute the amount to be sent and received + /// with no fees + public(friend) fun no_fee_debit_view(amount_ld: u64, min_amount_ld: u64): (u64, u64) { + let amount_sent_ld = remove_dust(amount_ld); + let amount_received_ld = amount_sent_ld; + assert!(amount_received_ld >= min_amount_ld, ESLIPPAGE_EXCEEDED); + (amount_sent_ld, amount_received_ld) + } + + /// Encode an OFT message + public(friend) fun encode_oft_msg( + sender: address, + amount_ld: u64, + to: Bytes32, + compose_payload: vector, + ): (vector, u16) { + let encoded_msg = oft_msg_codec::encode( + to, + to_sd(amount_ld), + bytes32::from_address(sender), + compose_payload, + ); + let msg_type = if (!vector::is_empty(&compose_payload)) { SEND_AND_CALL() } else { SEND() }; + + (encoded_msg, msg_type) + } + + // =================================================== Viewable =================================================== + + /// Convert an amount from shared decimals to local decimals + public(friend) fun to_ld(amount_sd: u64): u64 { + amount_sd * oft_store::decimal_conversion_rate() + } + + /// Convert an amount from local decimals to shared decimals + public(friend) fun to_sd(amount_ld: u64): u64 { + amount_ld / oft_store::decimal_conversion_rate() + } + + /// Calculate an amount in local decimals minus the dust + public(friend) fun remove_dust(amount_ld: u64): u64 { + let decimal_conversion_rate = oft_store::decimal_conversion_rate(); + (amount_ld / decimal_conversion_rate) * decimal_conversion_rate + } + + /// Get the shared decimals for the OFT + public(friend) fun shared_decimals(): u8 { oft_store::shared_decimals() } + + /// Get the decimal conversion rate + /// This is the multiplier to convert a shared decimals to a local decimals representation + public(friend) fun decimal_conversion_rate(): u64 { oft_store::decimal_conversion_rate() } + + // ===================================================== Store ==================================================== + + public(friend) fun initialize(local_decimals: u8, shared_decimals: u8) { + assert!(shared_decimals <= local_decimals, EINVALID_LOCAL_DECIMALS); + let decimal_conversion_rate = pow(10, ((local_decimals - shared_decimals) as u64)); + oft_store::initialize(shared_decimals, decimal_conversion_rate); + } + + // ==================================================== Events ==================================================== + + #[event] + struct OftReceived has store, drop { + guid: vector, + src_eid: u32, + to_address: address, + amount_received_ld: u64, + } + + #[event] + struct OftSent has store, drop { + // GUID of the OFT message + guid: vector, + // Destination Endpoint ID + dst_eid: u32, + // Address of the sender on the src chain + from_address: address, + // Amount of tokens sent in local decimals + amount_sent_ld: u64, + // Amount of tokens received in local decimals + amount_received_ld: u64 + } + + public(friend) fun emit_oft_received( + guid: vector, + src_eid: u32, + to_address: address, + amount_received_ld: u64, + ) { + emit(OftReceived { guid, src_eid, to_address, amount_received_ld }); + } + + public(friend) fun emit_oft_sent( + guid: vector, + dst_eid: u32, + from_address: address, + amount_sent_ld: u64, + amount_received_ld: u64, + ) { + emit(OftSent { guid, dst_eid, from_address, amount_sent_ld, amount_received_ld }); + } + + #[test_only] + public fun oft_received_event( + guid: vector, + src_eid: u32, + to_address: address, + amount_received_ld: u64, + ): OftReceived { + OftReceived { guid, src_eid, to_address, amount_received_ld } + } + + #[test_only] + public fun oft_sent_event( + guid: vector, + dst_eid: u32, + from_address: address, + amount_sent_ld: u64, + amount_received_ld: u64, + ): OftSent { + OftSent { guid, dst_eid, from_address, amount_sent_ld, amount_received_ld } + } + + // =============================================== Shared Constants =============================================== + + // Message type for a message that does not contain a compose message + public inline fun SEND(): u16 { 1 } + + // Message type for a message that contains a compose message + public inline fun SEND_AND_CALL(): u16 { 2 } + + // ================================================== Error Codes ================================================= + + const EINVALID_LOCAL_DECIMALS: u64 = 1; + const ESLIPPAGE_EXCEEDED: u64 = 2; +} diff --git a/examples/oft-adapter-aptos-move/sources/shared_oft/oft_impl_config.move b/examples/oft-adapter-aptos-move/sources/shared_oft/oft_impl_config.move new file mode 100644 index 000000000..fd684277b --- /dev/null +++ b/examples/oft-adapter-aptos-move/sources/shared_oft/oft_impl_config.move @@ -0,0 +1,444 @@ +module oft::oft_impl_config { + use std::event::emit; + use std::math64::min; + use std::string::utf8; + use std::table::{Self, Table}; + use std::timestamp; + + use oft::oapp_core::get_admin; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core::{no_fee_debit_view, remove_dust}; + use oft_common::oft_fee_detail::{new_oft_fee_detail, OftFeeDetail}; + + #[test_only] + friend oft::oft_impl_config_tests; + + friend oft::oft_adapter_fa; + + struct Config has key { + fee_bps: u64, + fee_deposit_address: address, + blocklist_enabled: bool, + blocklist: Table, + rate_limit_by_eid: Table, + } + + const MAX_U64: u64 = 0xffffffffffffffff; + + // =============================================== Fee Configuration ============================================== + + // The maximum fee that can be set is 100% + const MAX_FEE_BPS: u64 = 10_000; + + /// Set the fee deposit address + /// This is where OFT fees collected are deposited + public(friend) fun set_fee_deposit_address(fee_deposit_address: address) acquires Config { + // The fee deposit address must exist as an account to prevent revert for Coin deposits + assert!(std::account::exists_at(fee_deposit_address), EINVALID_DEPOSIT_ADDRESS); + assert!(store().fee_deposit_address != fee_deposit_address, ESETTING_UNCHANGED); + store_mut().fee_deposit_address = fee_deposit_address; + emit(FeeDepositAddressSet { fee_deposit_address }); + } + + /// Get the fee deposit address + public(friend) fun fee_deposit_address(): address acquires Config { store().fee_deposit_address } + + /// Set the fee for the OFT + public(friend) fun set_fee_bps(fee_bps: u64) acquires Config { + assert!(fee_bps <= MAX_FEE_BPS, EINVALID_FEE); + assert!(fee_bps != store().fee_bps, ESETTING_UNCHANGED); + store_mut().fee_bps = fee_bps; + emit(FeeSet { fee_bps }); + } + + /// Get the fee for the OFT + public(friend) fun fee_bps(): u64 acquires Config { store().fee_bps } + + /// Calculate the amount sent and received after applying the fee + /// If there is a zero fee, untransferable dust is to be left in user's wallet + /// If there is a non-zero fee, untransferable dust is to be consumed as a fee + /// This is consistent with EVM fee-enabled OFTs vs non-fee OFTs (we match fee = 0 with non-fee OFT behavior) + public(friend) fun debit_view_with_possible_fee( + amount_ld: u64, + min_amount_ld: u64, + ): (u64, u64) acquires Config { + let fee_bps = store().fee_bps; + if (fee_bps == 0) { + // If there is no fee, the amount sent and received is simply the amount provided minus dust, which is left + // in the wallet + no_fee_debit_view(amount_ld, min_amount_ld) + } else { + // The amount sent is the amount provided. The excess dust is consumed as a "fee" even if the dust could be + // left in the wallet in order to provide a more predictable experience for the user + let amount_sent_ld = amount_ld; + + // Calculate the preliminary fee based on the amount provided; this may increase when dust is added to it. + // The actual fee is the amount sent - amount received, which is fee + dust removed + let preliminary_fee = ((((amount_ld as u128) * (fee_bps as u128)) / 10_000) as u64); + + // Compute the received amount first, which is the amount after fee and dust removal + let amount_received_ld = remove_dust(amount_ld - preliminary_fee); + + // Ensure the amount received is greater than the minimum amount + assert!(amount_received_ld >= min_amount_ld, ESLIPPAGE_EXCEEDED); + + (amount_sent_ld, amount_received_ld) + } + } + + /// Specify the fee details based the configured fee and the amount sent + public(friend) fun fee_details_with_possible_fee( + amount_ld: u64, + min_amount_ld: u64 + ): vector acquires Config { + let (amount_sent_ld, amount_received_ld) = debit_view_with_possible_fee(amount_ld, min_amount_ld); + let fee = amount_sent_ld - amount_received_ld; + if (fee != 0) { + vector[new_oft_fee_detail(fee, false, utf8(b"OFT Fee"))] + } else { + vector[] + } + } + + // ============================================ Blocklist Configuration =========================================== + + /// Permanently disable the ability to blocklist accounts + /// This will also effectively restore any previously blocked accounts to unblocked status (is_blocklisted() will + /// return false for all accounts) + /// This is a one-way operation, once this is called, the blocklist capability cannot be restored + public(friend) fun irrevocably_disable_blocklist() acquires Config { + assert!(store().blocklist_enabled, EBLOCKLIST_ALREADY_DISABLED); + store_mut().blocklist_enabled = false; + emit(BlocklistingDisabled {}) + } + + /// Check if the blocklisting capability is enabled + public(friend) fun can_blocklist(): bool acquires Config { + store().blocklist_enabled + } + + /// Add or remove an account from the blocklist + public(friend) fun set_blocklist(wallet: address, blocklist: bool) acquires Config { + assert!(store().blocklist_enabled, EBLOCKLIST_DISABLED); + assert!(is_blocklisted(wallet) != blocklist, ESETTING_UNCHANGED); + if (blocklist) { + table::upsert(&mut store_mut().blocklist, wallet, true) + } else { + table::remove(&mut store_mut().blocklist, wallet); + }; + emit(BlocklistSet { wallet, blocked: blocklist }); + } + + /// Check if an account is blocked + public(friend) fun is_blocklisted(wallet: address): bool acquires Config { + store().blocklist_enabled && *table::borrow_with_default(&store().blocklist, wallet, &false) + } + + /// Revert if an account is blocked + public(friend) fun assert_not_blocklisted(wallet: address) acquires Config { + assert!(!is_blocklisted(wallet), EADDRESS_BLOCKED); + } + + /// Provide the admin address and emit a BlockedAmountRedirected event if an account is blocked + /// This is to be used in conjunction with a deposit(to) call and provides the admin address instead of the + /// recipient address if recipient is blocklisted. This also emits a message to alert that blocklisted funds have + /// been received + public(friend) fun redirect_to_admin_if_blocklisted(recipient: address, amount_ld: u64): address acquires Config { + if (!is_blocklisted(recipient)) { + recipient + } else { + emit(BlockedAmountRedirected { + amount_ld, + blocked_address: recipient, + redirected_to: get_admin(), + }); + get_admin() + } + } + + // ======================================= Sending Rate Limit Configuration ======================================= + + struct RateLimit has store, drop, copy { + limit: u64, + window_seconds: u64, + in_flight_on_last_update: u64, + last_update: u64, + } + + /// Set the rate limit (local_decimals) and the window (seconds) at the current timestamp + /// The capacity of the rate limit increased by limit_ld/window_s until it reaches the limit and stays there + public(friend) fun set_rate_limit(dst_eid: u32, limit: u64, window_seconds: u64) acquires Config { + set_rate_limit_at_timestamp(dst_eid, limit, window_seconds, timestamp::now_seconds()); + } + + /// Set or update the rate limit for a given EID at a specified timestamp + public(friend) fun set_rate_limit_at_timestamp( + dst_eid: u32, + limit: u64, + window_seconds: u64, + timestamp: u64 + ) acquires Config { + assert!(window_seconds > 0, EINVALID_WINDOW); + + // If the rate limit is already set, checkpoint the in-flight amount before updating the rate limit. + if (has_rate_limit(dst_eid)) { + let (prior_limit, prior_window_seconds) = rate_limit_config(dst_eid); + assert!(limit != prior_limit || window_seconds != prior_window_seconds, ESETTING_UNCHANGED); + + // Checkpoint the in-flight amount before updating the rate settings. If this is not saved, it could change + // the in-flight calculation amount retroactively + checkpoint_rate_limit_in_flight(dst_eid, timestamp); + + let rate_limit_store = table::borrow_mut(&mut store_mut().rate_limit_by_eid, dst_eid); + rate_limit_store.limit = limit; + rate_limit_store.window_seconds = window_seconds; + emit(RateLimitUpdated { dst_eid, limit, window_seconds }); + } else { + table::upsert(&mut store_mut().rate_limit_by_eid, dst_eid, RateLimit { + limit, + window_seconds, + in_flight_on_last_update: 0, + last_update: timestamp, + }); + emit(RateLimitSet { dst_eid, limit, window_seconds }); + }; + } + + /// Unset the rate limit for a given EID + public(friend) fun unset_rate_limit(eid: u32) acquires Config { + assert!(table::contains(&store().rate_limit_by_eid, eid), ESETTING_UNCHANGED); + table::remove(&mut store_mut().rate_limit_by_eid, eid); + emit(RateLimitUnset { eid }); + } + + /// Checkpoint the in-flight amount for a given EID for the provided timestamp. + /// This should whenever there is a change in rate limit or before consuming rate limit capacity + public(friend) fun checkpoint_rate_limit_in_flight(eid: u32, timestamp: u64) acquires Config { + let inflight = in_flight_at_time(eid, timestamp); + let rate_limit = table::borrow_mut(&mut store_mut().rate_limit_by_eid, eid); + rate_limit.in_flight_on_last_update = inflight; + rate_limit.last_update = timestamp; + } + + + /// Check if a rate limit is set for a given EID + public(friend) fun has_rate_limit(eid: u32): bool acquires Config { + table::contains(&store().rate_limit_by_eid, eid) + } + + /// Get the rate limit and window (in seconds) for a given EID + public(friend) fun rate_limit_config(eid: u32): (u64, u64) acquires Config { + if (!has_rate_limit(eid)) { + (0, 0) + } else { + let rate_limit = *table::borrow(&store().rate_limit_by_eid, eid); + (rate_limit.limit, rate_limit.window_seconds) + } + } + + /// Get the in-flight amount for a given EID at present + public(friend) fun in_flight(eid: u32): u64 acquires Config { + in_flight_at_time(eid, timestamp::now_seconds()) + } + + /// Get the in-flight amount for a given EID. The in-flight count is the amount of the rate limit that has been + /// consumed linearly decayed to the provided timestamp + public(friend) fun in_flight_at_time(eid: u32, timestamp: u64): u64 acquires Config { + if (!has_rate_limit(eid)) { + 0 + } else { + let rate_limit = *table::borrow(&store().rate_limit_by_eid, eid); + if (timestamp > rate_limit.last_update) { + // If the timestamp is greater than the last update, calculate the decayed in-flight amount + let elapsed = min(timestamp - rate_limit.last_update, rate_limit.window_seconds); + let decay = ((((elapsed as u128) * (rate_limit.limit as u128)) / (rate_limit.window_seconds as u128)) as u64); + + // Ensure the decayed in-flight amount is not negative + if (decay < rate_limit.in_flight_on_last_update) { + rate_limit.in_flight_on_last_update - decay + } else { + 0 + } + } else { + // If not, return the unaltered in-flight amount at the last checkpoint + rate_limit.in_flight_on_last_update + } + } + } + + /// Calculate the spare rate limit capacity for a given EID at present + public(friend) fun rate_limit_capacity(eid: u32): u64 acquires Config { + rate_limit_capacity_at_time(eid, timestamp::now_seconds()) + } + + /// Calculate the spare rate limit capacity for a given EID at the proviced timestamp + public(friend) fun rate_limit_capacity_at_time(eid: u32, timestamp: u64): u64 acquires Config { + if (!has_rate_limit(eid)) { + return MAX_U64 + }; + let rate_limit = *table::borrow(&store().rate_limit_by_eid, eid); + if (rate_limit.limit > in_flight_at_time(eid, timestamp)) { + rate_limit.limit - in_flight_at_time(eid, timestamp) + } else { + 0 + } + } + + /// Consume rate limit capacity for a given EID or abort if the capacity is exceeded + public(friend) fun try_consume_rate_limit_capacity(eid: u32, amount: u64) acquires Config { + if (!has_rate_limit(eid)) return; + try_consume_rate_limit_capacity_at_time(eid, amount, timestamp::now_seconds()); + } + + /// Consume rate limit capacity for a given EID or abort if the capacity is exceeded at a provided timestamp + public(friend) fun try_consume_rate_limit_capacity_at_time( + eid: u32, + amount: u64, + timestamp: u64 + ) acquires Config { + checkpoint_rate_limit_in_flight(eid, timestamp); + let rate_limit = table::borrow_mut(&mut store_mut().rate_limit_by_eid, eid); + assert!(rate_limit.in_flight_on_last_update + amount <= rate_limit.limit, EEXCEEDED_RATE_LIMIT); + rate_limit.in_flight_on_last_update = rate_limit.in_flight_on_last_update + amount; + } + + /// Release rate limit capacity for a given EID + /// This is used to when wanting to rate limit by net inflow - outflow + /// This will release the capacity back to the rate limit up to the limit itself + public(friend) fun release_rate_limit_capacity(eid: u32, amount: u64) acquires Config { + if (!has_rate_limit(eid)) return; + + let rate_limit = table::borrow_mut(&mut store_mut().rate_limit_by_eid, eid); + if (amount >= rate_limit.in_flight_on_last_update) { + rate_limit.in_flight_on_last_update = 0; + } else { + rate_limit.in_flight_on_last_update = rate_limit.in_flight_on_last_update - amount; + } + } + + // ==================================================== Helpers =================================================== + + inline fun store(): &Config { borrow_global(OAPP_ADDRESS()) } + + inline fun store_mut(): &mut Config { borrow_global_mut(OAPP_ADDRESS()) } + + // ==================================================== Events ==================================================== + + #[event] + struct FeeDepositAddressSet has drop, store { + fee_deposit_address: address, + } + + #[event] + struct FeeSet has drop, store { + fee_bps: u64, + } + + #[event] + struct BlocklistingDisabled has drop, store {} + + #[event] + struct BlocklistSet has drop, store { + wallet: address, + blocked: bool, + } + + #[event] + struct BlockedAmountRedirected has drop, store { + amount_ld: u64, + blocked_address: address, + redirected_to: address, + } + + #[event] + struct RateLimitSet has drop, store { + dst_eid: u32, + limit: u64, + window_seconds: u64, + } + + #[event] + struct RateLimitUpdated has drop, store { + dst_eid: u32, + limit: u64, + window_seconds: u64, + } + + #[event] + struct RateLimitUnset has drop, store { + eid: u32, + } + + #[test_only] + public fun fee_deposit_address_set_event(fee_deposit_address: address): FeeDepositAddressSet { + FeeDepositAddressSet { fee_deposit_address } + } + + #[test_only] + public fun fee_set_event(fee_bps: u64): FeeSet { + FeeSet { fee_bps } + } + + #[test_only] + public fun blocklisting_disabled_event(): BlocklistingDisabled { + BlocklistingDisabled {} + } + + #[test_only] + public fun blocklist_set_event(wallet: address, blocked: bool): BlocklistSet { + BlocklistSet { wallet, blocked } + } + + #[test_only] + public fun blocked_amount_redirected_event( + amount_ld: u64, + blocked_address: address, + redirected_to: address + ): BlockedAmountRedirected { + BlockedAmountRedirected { amount_ld, blocked_address, redirected_to } + } + + #[test_only] + public fun rate_limit_set_event(dst_eid: u32, limit: u64, window_seconds: u64): RateLimitSet { + RateLimitSet { dst_eid, limit, window_seconds } + } + + #[test_only] + public fun rate_limit_updated_event(dst_eid: u32, limit: u64, window_seconds: u64): RateLimitUpdated { + RateLimitUpdated { dst_eid, limit, window_seconds } + } + + #[test_only] + public fun rate_limit_unset_event(eid: u32): RateLimitUnset { + RateLimitUnset { eid } + } + + // ================================================ Initialization ================================================ + + fun init_module(account: &signer) { + move_to(move account, Config { + fee_bps: 0, + fee_deposit_address: @oft_admin, + blocklist_enabled: true, + blocklist: table::new(), + rate_limit_by_eid: table::new(), + }); + } + + #[test_only] + public fun init_module_for_test() { + init_module(&std::account::create_signer_for_test(OAPP_ADDRESS())); + } + + // ================================================== Error Codes ================================================= + + const EADDRESS_BLOCKED: u64 = 1; + const EBLOCKLIST_ALREADY_DISABLED: u64 = 2; + const EBLOCKLIST_DISABLED: u64 = 3; + const EEXCEEDED_RATE_LIMIT: u64 = 4; + const EINVALID_DEPOSIT_ADDRESS: u64 = 5; + const EINVALID_FEE: u64 = 6; + const EINVALID_WINDOW: u64 = 7; + const ESETTING_UNCHANGED: u64 = 8; + const ESLIPPAGE_EXCEEDED: u64 = 9; +} diff --git a/examples/oft-adapter-aptos-move/sources/shared_oft/oft_store.move b/examples/oft-adapter-aptos-move/sources/shared_oft/oft_store.move new file mode 100644 index 000000000..57ccc9715 --- /dev/null +++ b/examples/oft-adapter-aptos-move/sources/shared_oft/oft_store.move @@ -0,0 +1,58 @@ +/// This module contains the general/internal store of the OFT OApp. +/// +/// This module should generally not be modified by the OFT/OApp developer. OFT specific data should be stored in the +/// implementation module. +module oft::oft_store { + use oft::oapp_store::OAPP_ADDRESS; + + friend oft::oft_core; + + /// The OFT configuration that is general to all OFT implementations + struct OftStore has key { + shared_decimals: u8, + decimal_conversion_rate: u64, + } + + /// Get the decimal conversion rate of the OFT, this is the multiplier to convert a shared decimals to a local + /// decimals representation + public(friend) fun decimal_conversion_rate(): u64 acquires OftStore { + store().decimal_conversion_rate + } + + /// Get the shared decimals of the OFT, this is the number of decimals that are preserved on wire transmission + public(friend) fun shared_decimals(): u8 acquires OftStore { + store().shared_decimals + } + + // ==================================================== Helpers =================================================== + + inline fun store(): &OftStore { borrow_global(OAPP_ADDRESS()) } + + inline fun store_mut(): &mut OftStore { borrow_global_mut(OAPP_ADDRESS()) } + + // ================================================ Initialization ================================================ + + public(friend) fun initialize(shared_decimals: u8, decimal_conversion_rate: u64) acquires OftStore { + // Prevent re-initialization; the caller computes the decimal conversion rate, which cannot be 0 + assert!(store().decimal_conversion_rate == 0, EALREADY_INITIALIZED); + + store_mut().shared_decimals = shared_decimals; + store_mut().decimal_conversion_rate = decimal_conversion_rate; + } + + fun init_module(account: &signer) { + move_to(account, OftStore { + shared_decimals: 0, + decimal_conversion_rate: 0, + }) + } + + #[test_only] + public fun init_module_for_test() { + init_module(&std::account::create_signer_for_test(OAPP_ADDRESS())); + } + + // ================================================== Error Codes ================================================= + + const EALREADY_INITIALIZED: u64 = 1; +} diff --git a/examples/oft-adapter-aptos-move/test/evm/foundry/MyOFT.t.sol b/examples/oft-adapter-aptos-move/test/evm/foundry/MyOFT.t.sol new file mode 100644 index 000000000..2f8841555 --- /dev/null +++ b/examples/oft-adapter-aptos-move/test/evm/foundry/MyOFT.t.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +// Mock imports +import { MyOFT } from "../../../contracts/MyOFT.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"; + +// OFT imports +import { IOFT, SendParam, OFTReceipt } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; +import { MessagingFee, MessagingReceipt } from "@layerzerolabs/oft-evm/contracts/OFTCore.sol"; +import { OFTMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTMsgCodec.sol"; +import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.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 MyOFTTest is TestHelperOz5 { + using OptionsBuilder for bytes; + + uint32 private aEid = 1; + uint32 private bEid = 2; + + MyOFT private aOFT; + MyOFT private bOFT; + + address private userA = makeAddr("userA"); + address private userB = makeAddr("userB"); + 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); + + aOFT = MyOFT( + _deployOApp(type(MyOFT).creationCode, abi.encode("aOFT", "aOFT", address(endpoints[aEid]), address(this))) + ); + + bOFT = MyOFT( + _deployOApp(type(MyOFT).creationCode, abi.encode("bOFT", "bOFT", address(endpoints[bEid]), address(this))) + ); + + // config and wire the ofts + address[] memory ofts = new address[](2); + ofts[0] = address(aOFT); + ofts[1] = address(bOFT); + this.wireOApps(ofts); + + // mint tokens + deal(address(aOFT), userA, initialBalance); + deal(address(bOFT), userB, initialBalance); + } + + function test_constructor() public { + assertEq(aOFT.owner(), address(this)); + assertEq(bOFT.owner(), address(this)); + + assertEq(aOFT.balanceOf(userA), initialBalance); + assertEq(bOFT.balanceOf(userB), initialBalance); + + assertEq(aOFT.token(), address(aOFT)); + assertEq(bOFT.token(), address(bOFT)); + } + + function test_send_oft() public { + uint256 tokensToSend = 1 ether; + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + SendParam memory sendParam = SendParam( + bEid, + addressToBytes32(userB), + tokensToSend, + tokensToSend, + options, + "", + "" + ); + MessagingFee memory fee = aOFT.quoteSend(sendParam, false); + + assertEq(aOFT.balanceOf(userA), initialBalance); + assertEq(bOFT.balanceOf(userB), initialBalance); + + vm.prank(userA); + aOFT.send{ value: fee.nativeFee }(sendParam, fee, payable(address(this))); + verifyPackets(bEid, addressToBytes32(address(bOFT))); + + assertEq(aOFT.balanceOf(userA), initialBalance - tokensToSend); + assertEq(bOFT.balanceOf(userB), initialBalance + tokensToSend); + } +} diff --git a/examples/oft-adapter-aptos-move/test/evm/mocks/ERC20Mock.sol b/examples/oft-adapter-aptos-move/test/evm/mocks/ERC20Mock.sol new file mode 100644 index 000000000..6ce17e418 --- /dev/null +++ b/examples/oft-adapter-aptos-move/test/evm/mocks/ERC20Mock.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract ERC20Mock is ERC20 { + constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {} + + function mint(address _to, uint256 _amount) public { + _mint(_to, _amount); + } +} diff --git a/examples/oft-adapter-aptos-move/test/evm/mocks/OFTComposerMock.sol b/examples/oft-adapter-aptos-move/test/evm/mocks/OFTComposerMock.sol new file mode 100644 index 000000000..fdd5c2426 --- /dev/null +++ b/examples/oft-adapter-aptos-move/test/evm/mocks/OFTComposerMock.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { IOAppComposer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; + +contract OFTComposerMock is IOAppComposer { + // default empty values for testing a lzCompose received message + address public from; + bytes32 public guid; + bytes public message; + address public executor; + bytes public extraData; + + function lzCompose( + address _from, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata /*_extraData*/ + ) external payable { + from = _from; + guid = _guid; + message = _message; + executor = _executor; + extraData = _message; + } +} diff --git a/examples/oft-adapter-aptos-move/test/evm/mocks/OFTMock.sol b/examples/oft-adapter-aptos-move/test/evm/mocks/OFTMock.sol new file mode 100644 index 000000000..cef8770f5 --- /dev/null +++ b/examples/oft-adapter-aptos-move/test/evm/mocks/OFTMock.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol"; +import { SendParam } from "@layerzerolabs/oft-evm/contracts/OFTCore.sol"; + +contract OFTMock is OFT { + constructor( + string memory _name, + string memory _symbol, + address _lzEndpoint, + address _delegate + ) Ownable(_delegate) OFT(_name, _symbol, _lzEndpoint, _delegate) {} + + function mint(address _to, uint256 _amount) public { + _mint(_to, _amount); + } + + // @dev expose internal functions for testing purposes + function debit( + uint256 _amountToSendLD, + uint256 _minAmountToCreditLD, + uint32 _dstEid + ) public returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { + return _debit(msg.sender, _amountToSendLD, _minAmountToCreditLD, _dstEid); + } + + function debitView( + uint256 _amountToSendLD, + uint256 _minAmountToCreditLD, + uint32 _dstEid + ) public view returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { + return _debitView(_amountToSendLD, _minAmountToCreditLD, _dstEid); + } + + function removeDust(uint256 _amountLD) public view returns (uint256 amountLD) { + return _removeDust(_amountLD); + } + + function toLD(uint64 _amountSD) public view returns (uint256 amountLD) { + return _toLD(_amountSD); + } + + function toSD(uint256 _amountLD) public view returns (uint64 amountSD) { + return _toSD(_amountLD); + } + + function credit(address _to, uint256 _amountToCreditLD, uint32 _srcEid) public returns (uint256 amountReceivedLD) { + return _credit(_to, _amountToCreditLD, _srcEid); + } + + function buildMsgAndOptions( + SendParam calldata _sendParam, + uint256 _amountToCreditLD + ) public view returns (bytes memory message, bytes memory options) { + return _buildMsgAndOptions(_sendParam, _amountToCreditLD); + } +} diff --git a/examples/oft-adapter-aptos-move/test/movement/internal_oapp/oapp_core_tests.move b/examples/oft-adapter-aptos-move/test/movement/internal_oapp/oapp_core_tests.move new file mode 100644 index 000000000..1f2f33f36 --- /dev/null +++ b/examples/oft-adapter-aptos-move/test/movement/internal_oapp/oapp_core_tests.move @@ -0,0 +1,213 @@ +#[test_only] +module oft::oapp_core_tests { + use std::account::create_account_for_test; + use std::account::create_signer_for_test; + use std::event::was_event_emitted; + use std::fungible_asset::{Self, FungibleAsset}; + use std::option; + use std::signer::address_of; + + use endpoint_v2::channels::packet_sent_event; + use endpoint_v2::messaging_receipt; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32::{Self, from_address, from_bytes32}; + use endpoint_v2_common::guid; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, initialize_native_token_for_test, + mint_native_token_for_test + }; + use endpoint_v2_common::packet_v1_codec; + use oft::oapp_core::{Self, pause_sending_set, set_pause_sending}; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core::{SEND, SEND_AND_CALL}; + + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32) { + // Test the send function + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + let oft_admin = &create_signer_for_test(@oft_admin); + initialize_native_token_for_test(); + oft::oapp_test_helper::init_oapp(); + oapp_core::set_peer(oft_admin, SRC_EID, from_bytes32(from_address(@1234))); + oapp_core::set_peer(oft_admin, DST_EID, from_bytes32(from_address(@4321))); + } + + #[test] + fun test_send_internal() { + setup(SRC_EID, DST_EID); + + let called_send = false; + let called_inspect = false; + assert!(!called_inspect && !called_send, 0); + + let native_fee = mint_native_token_for_test(100000); + let zro_fee = option::none(); + + // pause then unpause (to test to make sure unpause works) + set_pause_sending(&create_signer_for_test(@oft_admin), DST_EID, true); + assert!(was_event_emitted(&pause_sending_set(DST_EID, true)), 0); + + set_pause_sending(&create_signer_for_test(@oft_admin), DST_EID, false); + assert!(was_event_emitted(&pause_sending_set(DST_EID, false)), 0); + + let messaging_receipt = oapp_core::lz_send( + DST_EID, + b"oapp-message", + b"options", + &mut native_fee, + &mut zro_fee, + ); + + let expected_guid = guid::compute_guid( + 1, + SRC_EID, + bytes32::from_address(OAPP_ADDRESS()), + DST_EID, + bytes32::from_address(@4321), + ); + + assert!(messaging_receipt::get_guid(&messaging_receipt) == expected_guid, 0); + + // 0 fees in simple msglib + assert!(messaging_receipt::get_native_fee(&messaging_receipt) == 0, 1); + assert!(messaging_receipt::get_zro_fee(&messaging_receipt) == 0, 1); + // nothing removed + assert!(fungible_asset::amount(&native_fee) == 100000, 0); + + assert!(messaging_receipt::get_nonce(&messaging_receipt) == 1, 2); + + let packet = packet_v1_codec::new_packet_v1( + SRC_EID, + bytes32::from_address(OAPP_ADDRESS()), + DST_EID, + bytes32::from_address(@4321), + 1, + expected_guid, + b"oapp-message", + ); + assert!(was_event_emitted(&packet_sent_event( + packet, + b"options", + @simple_msglib, + )), 0); + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + } + + #[test] + #[expected_failure(abort_code = oft::oapp_core::ESEND_PAUSED)] + fun test_send_pause() { + setup(SRC_EID, DST_EID); + set_pause_sending(&create_signer_for_test(@oft_admin), DST_EID, true); + + let called_send = false; + let called_inspect = false; + assert!(!called_inspect && !called_send, 0); + + let native_fee = mint_native_token_for_test(100000); + let zro_fee = option::none(); + + assert!(was_event_emitted(&pause_sending_set(DST_EID, true)), 0); + + oapp_core::lz_send( + DST_EID, + b"oapp-message", + b"options", + &mut native_fee, + &mut zro_fee, + ); + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + } + + #[test] + fun test_quote_internal() { + setup(SRC_EID, DST_EID); + + let (native_fee, zro_fee) = oapp_core::lz_quote( + DST_EID, + b"oapp-message", + b"options", + false, + ); + assert!(native_fee == 0, 0); + assert!(zro_fee == 0, 1); + } + + #[test] + fun test_set_enforced_options() { + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + oapp_core::set_enforced_options(admin, SRC_EID, SEND(), x"0003aaaa"); + oapp_core::set_enforced_options(admin, DST_EID, SEND(), x"0003bbbc"); + oapp_core::set_enforced_options(admin, DST_EID, SEND(), x"000355"); + oapp_core::set_enforced_options(admin, DST_EID, SEND_AND_CALL(), x"000344"); + assert!(oapp_core::get_enforced_options(DST_EID, SEND()) == x"000355", 0); + assert!(oapp_core::get_enforced_options(DST_EID, SEND_AND_CALL()) == x"000344", 0); + assert!(oapp_core::get_enforced_options(SRC_EID, SEND()) == x"0003aaaa", 0); + } + + #[test] + fun test_combine_options() { + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + let enforced_options = x"0003aaaa"; + let options = x"0003bbbb"; + oapp_core::set_enforced_options(admin, DST_EID, SEND(), enforced_options); + // unrelated option below just to make sure it doesn't get overwritten + oapp_core::set_enforced_options(admin, DST_EID, SEND_AND_CALL(), x"0003235326"); + let combined = oapp_core::combine_options(DST_EID, SEND(), options); + assert!(combined == x"0003aaaabbbb", 0); + } + + #[test] + fun test_peers() { + // Test the send function + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + assert!(!oapp_core::has_peer(1111), 0); + oapp_core::set_peer(admin, 1111, from_bytes32(from_address(@1234))); + oapp_core::set_peer(admin, 2222, from_bytes32(from_address(@2345))); + assert!(oapp_core::has_peer(1111), 0); + assert!(oapp_core::has_peer(2222), 0); + assert!(oapp_core::get_peer_bytes32(1111) == from_address(@1234), 0); + assert!(oapp_core::get_peer_bytes32(2222) == from_address(@2345), 0); + } + + #[test] + fun test_delegate() { + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + let delegate = &create_signer_for_test(@2222); + oapp_core::set_delegate(admin, address_of(delegate)); + assert!(oapp_core::get_delegate() == address_of(delegate), 0); + + let delegate2 = &create_signer_for_test(@3333); + oapp_core::set_delegate(admin, address_of(delegate2)); + + oapp_core::skip(delegate2, SRC_EID, from_bytes32(from_address(@1234)), 1); + } +} diff --git a/examples/oft-adapter-aptos-move/test/movement/internal_oapp/oapp_test_helper.move b/examples/oft-adapter-aptos-move/test/movement/internal_oapp/oapp_test_helper.move new file mode 100644 index 000000000..9f0a827dc --- /dev/null +++ b/examples/oft-adapter-aptos-move/test/movement/internal_oapp/oapp_test_helper.move @@ -0,0 +1,12 @@ +#[test_only] +module oft::oapp_test_helper { + use oft::oapp_compose; + use oft::oapp_receive; + use oft::oapp_store; + + public fun init_oapp() { + oapp_store::init_module_for_test(); + oapp_receive::init_module_for_test(); + oapp_compose::init_module_for_test(); + } +} diff --git a/examples/oft-adapter-aptos-move/test/movement/internal_oft/oft_core_tests.move b/examples/oft-adapter-aptos-move/test/movement/internal_oft/oft_core_tests.move new file mode 100644 index 000000000..c0cd148be --- /dev/null +++ b/examples/oft-adapter-aptos-move/test/movement/internal_oft/oft_core_tests.move @@ -0,0 +1,522 @@ +#[test_only] +module oft::oft_core_tests { + use std::account::create_signer_for_test; + use std::event::was_event_emitted; + use std::fungible_asset; + use std::fungible_asset::FungibleAsset; + use std::option; + use std::string::utf8; + use std::vector; + + use endpoint_v2::endpoint; + use endpoint_v2::messaging_receipt; + use endpoint_v2::messaging_receipt::new_messaging_receipt_for_test; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32; + use endpoint_v2_common::bytes32::{from_address, from_bytes32}; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, initialize_native_token_for_test, + mint_native_token_for_test + }; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::packet_v1_codec::compute_payload_hash; + use endpoint_v2_common::zro_test_helpers::create_fa; + use oft::oapp_core; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core::{Self, oft_v1_compatibility_mode_set, SEND, SEND_AND_CALL, set_v1_compatibility_mode}; + use oft::oft_store; + use oft_common::oft_compose_msg_codec; + use oft_common::oft_msg_codec; + use oft_common::oft_v1_msg_codec::{Self, PT_SEND, PT_SEND_AND_CALL}; + + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32) { + // Test the send function + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + let oft_admin = &create_signer_for_test(@oft_admin); + initialize_native_token_for_test(); + let (_, metadata, _) = create_fa(b"ZRO"); + let local_decimals = fungible_asset::decimals(metadata); + oft::oapp_test_helper::init_oapp(); + + oft_store::init_module_for_test(); + oft_core::initialize(local_decimals, 6); + oapp_core::set_peer(oft_admin, SRC_EID, from_bytes32(from_address(@1234))); + oapp_core::set_peer(oft_admin, DST_EID, from_bytes32(from_address(@4321))); + } + + #[test] + fun test_send() { + setup(SRC_EID, DST_EID); + + let user_sender = @99; + let to = bytes32::from_address(@2001); + let native_fee = mint_native_token_for_test(100000); + let zro_fee = option::none(); + let compose_message = b"Hello"; + + let called_send = false; + let called_inspect = false; + assert!(!called_inspect && !called_send, 0); + + // Turn on and off v1 compatibility mode (to test turning off works) + set_v1_compatibility_mode(DST_EID, true); + set_v1_compatibility_mode(DST_EID, false); + + let (messaging_receipt, amount_sent_ld, amount_received_ld) = oft_core::send( + user_sender, + DST_EID, + to, + compose_message, + |message, options| { + called_send = true; + assert!(oft_msg_codec::has_compose(&message), 0); + assert!(oft_msg_codec::sender(&message) == from_address(user_sender), 1); + assert!(oft_msg_codec::send_to(&message) == to, 0); + assert!(oft_msg_codec::amount_sd(&message) == 40, 1); + assert!(options == b"options", 2); + + new_messaging_receipt_for_test( + from_address(@333), + 4, + 1111, + 2222, + ) + }, + |_unused| (5000, 4000), + |_amount_received_ld, _msg_type| b"options", + |message, options| { + called_inspect = true; + assert!(vector::length(message) > 0, 0); + assert!(*options == b"options", 0); + }, + ); + + assert!(called_send, 0); + assert!(called_inspect, 0); + + let (guid, nonce, native_fee_amount, zro_fee_amount) = messaging_receipt::unpack_messaging_receipt( + messaging_receipt, + ); + assert!(guid == from_address(@333), 0); + assert!(nonce == 4, 0); + assert!(native_fee_amount == 1111, 1); + assert!(zro_fee_amount == 2222, 2); + + assert!(amount_sent_ld == 5000, 3); + assert!(amount_received_ld == 4000, 4); + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + } + + #[test] + fun test_send_v1_compatibility() { + setup(SRC_EID, DST_EID); + + let user_sender = @99; + let to = bytes32::from_address(@2001); + let native_fee = mint_native_token_for_test(100000); + let zro_fee = option::none(); + + // Compose message not allowed in v1 compatibility mode + let compose_message = b""; + + let called_send = false; + let called_inspect = false; + assert!(!called_inspect && !called_send, 0); + + set_v1_compatibility_mode(DST_EID, true); + assert!(was_event_emitted(&oft_v1_compatibility_mode_set(DST_EID, true)), 1); + + let (messaging_receipt, amount_sent_ld, amount_received_ld) = oft_core::send( + user_sender, + DST_EID, + to, + compose_message, + |message, options| { + called_send = true; + assert!(!oft_v1_msg_codec::has_compose(&message), 0); + assert!(oft_v1_msg_codec::send_to(&message) == to, 0); + assert!(oft_v1_msg_codec::amount_sd(&message) == 40, 1); + assert!(options == b"options", 2); + + new_messaging_receipt_for_test( + from_address(@333), + 4, + 1111, + 2222, + ) + }, + |_unused| (5000, 4000), + |_amount_received_ld, _msg_type| b"options", + |message, options| { + called_inspect = true; + assert!(vector::length(message) > 0, 0); + assert!(*options == b"options", 0); + }, + ); + + assert!(called_send, 0); + assert!(called_inspect, 0); + + let (guid, nonce, native_fee_amount, zro_fee_amount) = messaging_receipt::unpack_messaging_receipt( + messaging_receipt, + ); + assert!(guid == from_address(@333), 0); + assert!(nonce == 4, 0); + assert!(native_fee_amount == 1111, 1); + assert!(zro_fee_amount == 2222, 2); + + assert!(amount_sent_ld == 5000, 3); + assert!(amount_received_ld == 4000, 4); + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + } + + #[test] + fun test_receive() { + setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let called_credit = false; + assert!(!called_credit, 1); + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"", + ); + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + ); + + oft_core::receive( + SRC_EID, + nonce, + guid, + message, + |_to, _index, _message| { + // should not be called + assert!(false, 0); + }, + |to_address, message_amount| { + called_credit = true; + + assert!(to_address == @0x2000, 0); + // Add 2 0s for (8 local decimals - 6 shared decimals) + assert!(message_amount == 12300, 1); + + 5000 + }, + ); + + assert!(called_credit, 2); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 5000, + )), 3); + } + + #[test] + fun test_receive_v1() { + setup(DST_EID, SRC_EID); + set_v1_compatibility_mode(SRC_EID, true); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let called_credit = false; + assert!(!called_credit, 1); + + let message = oft_v1_msg_codec::encode( + PT_SEND(), + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + 0, + b"", + ); + let sender = bytes32::from_address(@1234); + + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + ); + + oft_core::receive( + SRC_EID, + nonce, + guid, + message, + |_to, _index, _message| { + // should not be called + assert!(false, 0); + }, + |to_address, message_amount| { + called_credit = true; + + assert!(to_address == @0x2000, 0); + // Add 2 0s for (8 local decimals - 6 shared decimals) + assert!(message_amount == 12300, 1); + + 5000 + }, + ); + + assert!(called_credit, 2); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 5000, + )), 3); + } + + #[test] + fun test_receive_with_compose() { + setup(DST_EID, SRC_EID); + set_v1_compatibility_mode(SRC_EID, true); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let called_credit = 0; + assert!(called_credit == 0, 1); + + + let message = oft_v1_msg_codec::encode( + PT_SEND_AND_CALL(), + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + 50, + b"Hello", + ); + + // Composer must be registered + let to_address_account = &create_signer_for_test(@0x2000); + endpoint::register_composer(to_address_account, utf8(b"oft")); + + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + ); + + let called_compose = 0; + assert!(called_compose == 0, 0); + + oft_core::receive( + SRC_EID, + nonce, + guid, + message, + |to, index, message| { + called_compose = called_compose + 1; + assert!(to == @0x2000, 0); + assert!(index == 0, 1); + assert!(oft_compose_msg_codec::compose_payload_message(&message) == b"Hello", 2); + }, + |to_address, message_amount| { + called_credit = called_credit + 1; + + assert!(to_address == @0x2000, 0); + // Add 2 0s for (8 local decimals - 6 shared decimals) + assert!(message_amount == 12300, 1); + 12300 // message_amount + }, + ); + + assert!(called_compose == 1, 0); + assert!(called_credit == 1, 2); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 12300, + )), 3); + } + + #[test] + fun test_receive_with_compose_v1() { + setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let called_credit = 0; + assert!(called_credit == 0, 1); + + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"Hello", + ); + + // Composer must be registered + let to_address_account = &create_signer_for_test(@0x2000); + endpoint::register_composer(to_address_account, utf8(b"oft")); + + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + ); + + let called_compose = 0; + assert!(called_compose == 0, 0); + + oft_core::receive( + SRC_EID, + nonce, + guid, + message, + |to, index, message| { + called_compose = called_compose + 1; + assert!(to == @0x2000, 0); + assert!(index == 0, 1); + assert!(oft_compose_msg_codec::compose_payload_message(&message) == b"Hello", 2); + }, + |to_address, message_amount| { + called_credit = called_credit + 1; + + assert!(to_address == @0x2000, 0); + // Add 2 0s for (8 local decimals - 6 shared decimals) + assert!(message_amount == 12300, 1); + 12300 // message_amount + }, + ); + + assert!(called_compose == 1, 0); + assert!(called_credit == 1, 2); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 12300, + )), 3); + } + + + #[test] + fun test_no_fee_debit_view() { + setup(SRC_EID, DST_EID); + + let (sent, received) = oft_core::no_fee_debit_view(123456789, 200); + assert!(sent == received, 0); + // dust removed (last 2 digits cleared) + assert!(sent == 123456700, 1); + } + + #[test] + #[expected_failure(abort_code = oft::oft_core::ESLIPPAGE_EXCEEDED)] + fun test_no_fee_debit_view_fails_if_post_dust_remove_less_than_min() { + setup(SRC_EID, DST_EID); + + oft_core::no_fee_debit_view(99, 20); + } + + #[test] + fun test_encode_oft_msg() { + setup(SRC_EID, DST_EID); + + let (encoded, message_type) = oft_core::encode_oft_msg( + @0x12345678, + 123, + bytes32::from_address(@0x2000), + b"Hello", + ); + + assert!(message_type == SEND_AND_CALL(), 0); + + let expected_encoded = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + // dust removed and SD + 1, + bytes32::from_address(@0x12345678), + b"Hello", + ); + + assert!(encoded == expected_encoded, 1); + + // without compose + let (encoded, message_type) = oft_core::encode_oft_msg( + @0x12345678, + 123, + bytes32::from_address(@0x2000), + b"", + ); + + assert!(message_type == SEND(), 2); + + let expected_encoded = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + // dust removed and SD + 1, + bytes32::from_address(@0x12345678), + b"", + ); + + assert!(encoded == expected_encoded, 1); + } +} \ No newline at end of file diff --git a/examples/oft-adapter-aptos-move/test/movement/internal_oft/oft_impl_config_tests.move b/examples/oft-adapter-aptos-move/test/movement/internal_oft/oft_impl_config_tests.move new file mode 100644 index 000000000..e4dc78a71 --- /dev/null +++ b/examples/oft-adapter-aptos-move/test/movement/internal_oft/oft_impl_config_tests.move @@ -0,0 +1,220 @@ +#[test_only] +module oft::oft_impl_config_tests { + use std::string::utf8; + use std::vector; + use aptos_framework::event::was_event_emitted; + + use oft::oapp_store; + use oft::oft_core; + use oft::oft_impl_config::{ + Self, + assert_not_blocklisted, + blocked_amount_redirected_event, + debit_view_with_possible_fee, + fee_details_with_possible_fee, + fee_bps, + has_rate_limit, + in_flight_at_timestamp, + rate_limit_capacity_at_timestamp, + rate_limit_config, + rate_limit_set_event, + rate_limit_updated_event, + redirect_to_admin_if_blocklisted, + release_rate_limit_capacity, + set_blocklist, set_fee_bps, try_consume_rate_limit_capacity_at_timestamp, unset_rate_limit, + }; + use oft::oft_store; + use oft_common::oft_fee_detail; + + const MAX_U64: u64 = 0xffffffffffffffff; + + fun setup() { + oapp_store::init_module_for_test(); + oft_store::init_module_for_test(); + oft_impl_config::init_module_for_test(); + oft_core::initialize(8, 6); + } + + #[test] + fun test_set_fee_bps() { + setup(); + + // Set the fee to 10% + let fee_bps = 1000; + set_fee_bps(fee_bps); + assert!(fee_bps() == fee_bps, 1); + + let (sent, received) = debit_view_with_possible_fee(1234, 1000); + // Send amount should include dust if there is a fee + assert!(sent == 1234, 1); + // Received amount should be 90% of the sent amount with dust removed: 1234 - 120 = 1114 => 1100 (dust removed) + assert!(received == 1100, 2); + + let fee_details = fee_details_with_possible_fee(1234, 1000); + assert!(vector::length(&fee_details) == 1, 1); + let (fee, is_reward) = oft_fee_detail::fee_amount_ld(vector::borrow(&fee_details, 0)); + assert!(fee == 134, 1); + assert!(is_reward == false, 2); + assert!(oft_fee_detail::description(vector::borrow(&fee_details, 0)) == utf8(b"OFT Fee"), 2); + + // Set the fee to 0% + let fee_bps = 0; + set_fee_bps(fee_bps); + + let (sent, received) = debit_view_with_possible_fee(1234, 1000); + // Sent and amount should be 100% of the amount with dust removed with no fee: 1234 => 1200 (dust removed) + assert!(sent == 1200, 3); + assert!(received == 1200, 4); + + // Expect no fee details if there is no fee + let fee_details = fee_details_with_possible_fee(1234, 1000); + assert!(vector::length(&fee_details) == 0, 1); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EINVALID_FEE)] + fun test_set_fee_bps_invalid() { + setup(); + + // Set the fee to 101% + let fee_bps = 10100; + set_fee_bps(fee_bps); + } + + #[test] + fun test_set_blocklist() { + setup(); + + assert!(oft_impl_config::is_blocklisted(@0x1234) == false, 1); + assert_not_blocklisted(@0x1234); + let redirected_address = redirect_to_admin_if_blocklisted(@0x1234, 1111); + assert!(redirected_address == @0x1234, 2); + + set_blocklist(@0x1234, true); + assert!(oft_impl_config::is_blocklisted(@0x1234), 1); + let redirected_address = redirect_to_admin_if_blocklisted(@0x1234, 1111); + assert!(redirected_address == @oft_admin, 2); + assert!(was_event_emitted(&blocked_amount_redirected_event(1111, @0x1234, @oft_admin)), 3); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EADDRESS_BLOCKED)] + fun test_assert_not_blocked() { + setup(); + + set_blocklist(@0x1234, true); + assert_not_blocklisted(@0x1234); + } + + #[test] + fun set_rate_limit() { + setup(); + + // No rate limit configured + assert!(has_rate_limit(30100) == false, 2); + let (limit, window) = rate_limit_config(30100); + assert!(limit == 0 && window == 0, 1); + assert!(in_flight_at_timestamp(30100, 10) == 0, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 10) == MAX_U64, 2); + + // Configure rate limit (200/second) + oft_impl_config::set_rate_limit_at_timestamp(30100, 20000, 1000, 100); + assert!(was_event_emitted(&rate_limit_set_event(30100, 20000, 1000)), 1); + assert!(has_rate_limit(30100) == true, 2); + assert!(has_rate_limit(30200) == false, 2); // Different eid + let (limit, window) = rate_limit_config(30100); + assert!(limit == 20000 && window == 1000, 1); + assert!(in_flight_at_timestamp(30100, 100) == 0, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 100) == 20000, 2); + + // 100 seconds later + assert!(in_flight_at_timestamp(30100, 200) == 0, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 200) == 20000, 2); + + // consume 10% of the capacity + try_consume_rate_limit_capacity_at_timestamp(30100, 2000, 200); + assert!(in_flight_at_timestamp(30100, 200) == 2000, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 200) == 18000, 2); + + // 10 seconds later: in flight should decline by 20000/1000s * 10s = 200 + assert!(in_flight_at_timestamp(30100, 210) == 1800, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 210) == 18200, 2); + + // 20 seconds later: in flight should decline by 20000/1000s * 20s = 400 + assert!(in_flight_at_timestamp(30100, 220) == 1600, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 220) == 18400, 2); + + // update rate limit (300/second) + oft_impl_config::set_rate_limit_at_timestamp(30100, 30000, 1000, 220); + assert!(was_event_emitted(&rate_limit_updated_event(30100, 30000, 1000)), 1); + // in flight shouldn't change, but capacity should be updated with the new limit in mind + assert!(in_flight_at_timestamp(30100, 220) == 1600, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 220) == 28400, 2); + + // 10 seconds later: in flight should decline by 30000/1000s * 10s = 300 + assert!(in_flight_at_timestamp(30100, 230) == 1300, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 230) == 28700, 2); + + // 10 seconds later: in flight should decline by 30000/1000s * 10s = 300 + assert!(in_flight_at_timestamp(30100, 240) == 1000, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 240) == 29000, 2); + + // 100 seconds later: in flight should decline fully (without overshooting): 30000/1000s * 100s = 3000 + assert!(in_flight_at_timestamp(30100, 300) == 0, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 300) == 30000, 2); + + // Consume again + try_consume_rate_limit_capacity_at_timestamp(30100, 2000, 300); + try_consume_rate_limit_capacity_at_timestamp(30100, 2000, 310); + // 2000 + (2000 - 30000/1000*10) = 3800 + assert!(in_flight_at_timestamp(30100, 310) == 3700, 1); + + // Unset rate limit + unset_rate_limit(30100); + assert!(has_rate_limit(30100) == false, 2); + let (limit, window) = rate_limit_config(30100); + assert!(limit == 0 && window == 0, 1); + assert!(in_flight_at_timestamp(30100, 320) == 0, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 320) == MAX_U64, 2); + } + + #[test] + fun set_rate_limit_net() { + setup(); + + // No rate limit configured + assert!(has_rate_limit(30100) == false, 2); + let (limit, window) = rate_limit_config(30100); + assert!(limit == 0 && window == 0, 1); + assert!(in_flight_at_timestamp(30100, 10) == 0, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 10) == MAX_U64, 2); + + // Configure rate limit (200/second) + oft_impl_config::set_rate_limit_at_timestamp(30100, 20000, 1000, 100); + assert!(has_rate_limit(30100) == true, 2); + + // consume 100% of the capacity + try_consume_rate_limit_capacity_at_timestamp(30100, 20000, 0); + assert!(in_flight_at_timestamp(30100, 0) == 20000, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 0) == 0, 2); + + // 50 seconds later: in flight should decline by 20000/1000s * 500s = 10000 + assert!(in_flight_at_timestamp(30100, 500) == 10000, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 500) == 10000, 2); + + // release most of remaining capacity + release_rate_limit_capacity(30100, 9000); + assert!(in_flight_at_timestamp(30100, 500) == 1000, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 500) == 19000, 2); + + // consume all of remaining capacity + try_consume_rate_limit_capacity_at_timestamp(30100, 19000, 500); + assert!(in_flight_at_timestamp(30100, 500) == 20000, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 500) == 0, 2); + + // release excess capacity (5x limit) - should not overshoot + release_rate_limit_capacity(30100, 100_000); + assert!(in_flight_at_timestamp(30100, 500) == 0, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 500) == 20000, 2); + } +} diff --git a/examples/oft-adapter-aptos-move/test/movement/oapp_receive_using_oft_fa_tests.move b/examples/oft-adapter-aptos-move/test/movement/oapp_receive_using_oft_fa_tests.move new file mode 100644 index 000000000..b6160c030 --- /dev/null +++ b/examples/oft-adapter-aptos-move/test/movement/oapp_receive_using_oft_fa_tests.move @@ -0,0 +1,165 @@ +#[test_only] +module oft::oapp_receive_using_oft_fa_tests { + use std::account::create_signer_for_test; + use std::event::was_event_emitted; + use std::string::utf8; + + use endpoint_v2::endpoint; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32::{Self, from_address, from_bytes32}; + use endpoint_v2_common::native_token_test_helpers::initialize_native_token_for_test; + use endpoint_v2_common::packet_v1_codec::{Self, compute_payload_hash}; + use oft::oapp_core; + use oft::oapp_receive; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core; + use oft::oft_impl; + use oft::oft_impl_config; + use oft::oft_store; + use oft_common::oft_compose_msg_codec; + use oft_common::oft_msg_codec; + + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32) { + // Test the send function + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + let oft_admin = &create_signer_for_test(@oft_admin); + initialize_native_token_for_test(); + oft::oapp_test_helper::init_oapp(); + + oft_store::init_module_for_test(); + oft_impl_config::init_module_for_test(); + oft_impl::init_module_for_test(); + oft_impl::initialize( + &create_signer_for_test(@oft_admin), + b"My Test Token", + b"MYT", + b"", + b"", + 6, + 8, + ); + oapp_core::set_peer(oft_admin, SRC_EID, from_bytes32(from_address(@1234))); + oapp_core::set_peer(oft_admin, DST_EID, from_bytes32(from_address(@4321))); + } + + #[test] + fun test_receive() { + setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"", + ); + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + ); + + + oapp_receive::lz_receive( + SRC_EID, + from_bytes32(sender), + nonce, + from_bytes32(guid), + message, + b"", + ); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 12300, + )), 3); + } + + #[test] + fun test_receive_with_compose() { + setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"Hello", + ); + + // Composer must be registered + let to_address_account = &create_signer_for_test(@0x2000); + endpoint::register_composer(to_address_account, utf8(b"composer")); + + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + ); + + + oapp_receive::lz_receive( + SRC_EID, + from_bytes32(sender), + nonce, + from_bytes32(guid), + message, + b"", + ); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 12300, + )), 3); + + let compose_message_part = oft_msg_codec::compose_payload(&message); + let expected_compose_message = oft_compose_msg_codec::encode( + nonce, + SRC_EID, + 12300, + compose_message_part, + ); + + // Compose Triggered to the same address + assert!(was_event_emitted(&endpoint_v2::messaging_composer::compose_sent_event( + OAPP_ADDRESS(), + @0x2000, + from_bytes32(guid), + 0, + expected_compose_message, + )), 0); + } +} \ No newline at end of file diff --git a/examples/oft-adapter-aptos-move/test/movement/oft_fa_tests.move b/examples/oft-adapter-aptos-move/test/movement/oft_fa_tests.move new file mode 100644 index 000000000..e33bce426 --- /dev/null +++ b/examples/oft-adapter-aptos-move/test/movement/oft_fa_tests.move @@ -0,0 +1,458 @@ +#[test_only] +module oft::oft_fa_tests { + use std::account::{create_account_for_test, create_signer_for_test}; + use std::event::was_event_emitted; + use std::fungible_asset::{Self, Metadata}; + use std::object::address_to_object; + use std::option; + use std::primary_fungible_store; + use std::string::utf8; + use std::timestamp; + use std::vector; + + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use oft::oapp_core; + use oft::oft_impl::{ + Self, fee_bps, fee_deposit_address, is_blocklisted, mint_tokens_for_test, set_fee_bps, set_fee_deposit_address, + }; + use oft::oft_impl_config; + use oft::oft_store; + use oft_common::oft_limit::new_unbounded_oft_limit; + + const MAXU64: u64 = 0xffffffffffffffff; + + const LOCAL_EID: u32 = 101; + + fun setup() { + setup_layerzero_for_test(@simple_msglib, LOCAL_EID, LOCAL_EID); + + oft::oapp_test_helper::init_oapp(); + + oft_store::init_module_for_test(); + oft_impl::init_module_for_test(); + oft_impl_config::init_module_for_test(); + oft_impl::initialize( + &create_signer_for_test(@oft_admin), + b"My Test Token", + b"MYT", + b"https://example.com/icon.png", + b"https://example.com/project", + 6, + 8, + ); + + assert!(fungible_asset::name(oft::oft::metadata()) == utf8(b"My Test Token"), 0); + assert!(fungible_asset::symbol(oft::oft::metadata()) == utf8(b"MYT"), 0); + assert!(fungible_asset::icon_uri(oft::oft::metadata()) == utf8(b"https://example.com/icon.png"), 0); + assert!(fungible_asset::project_uri(oft::oft::metadata()) == utf8(b"https://example.com/project"), 0); + assert!(fungible_asset::decimals(oft::oft::metadata()) == 8, 0); + } + + #[test] + fun test_debit() { + setup(); + + let dst_eid = 2u32; + // This configuration function (debit) is not resposible for handling dust, therefore the tested amount excludes + // the dust amount (last two digits) + let amount_ld = 123456700; + let min_amount_ld = 0u64; + + let fa = mint_tokens_for_test(amount_ld); + let (sent, received) = oft_impl::debit_fungible_asset( + @444, + &mut fa, + min_amount_ld, + dst_eid, + ); + + // amount sent and received should reflect the amount debited + assert!(sent == 123456700, 0); + assert!(received == 123456700, 0); + + // no remaining balance + let remaining_balance = fungible_asset::amount(&fa); + assert!(remaining_balance == 00, 0); + burn_token_for_test(fa); + } + + #[test] + fun test_credit() { + setup(); + + let amount_ld = 123456700; + let lz_receive_value = option::none(); + let src_eid = 12345; + + let to = @555; + create_account_for_test(to); + + // 0 balance before crediting + let balance = primary_fungible_store::balance(to, oft_impl::metadata()); + assert!(balance == 0, 0); + + let credited = oft_impl::credit( + to, + amount_ld, + src_eid, + lz_receive_value, + ); + // amount credited should reflect the amount credited + assert!(credited == 123456700, 0); + + // balance should appear in account + let balance = primary_fungible_store::balance(to, oft_impl::metadata()); + assert!(balance == 123456700, 0); + } + + #[test] + fun test_credit_with_extra_lz_receive_drop() { + setup(); + + let amount_ld = 123456700; + let lz_receive_value = option::some(mint_native_token_for_test(100)); + let src_eid = 12345; + + let to = @555; + create_account_for_test(to); + + // 0 balance before crediting + let balance = primary_fungible_store::balance(to, oft_impl::metadata()); + assert!(balance == 0, 0); + + oft_impl::credit( + to, + amount_ld, + src_eid, + lz_receive_value, + ); + + let native_token_metadata = address_to_object(@native_token_metadata_address); + assert!(primary_fungible_store::balance(@oft_admin, native_token_metadata) == 100, 1) + } + + #[test] + fun test_debit_view() { + setup(); + + // shouldn't take a fee + let (sent, received) = oft_impl::debit_view(123456700, 100, 2); + assert!(sent == 123456700, 0); + assert!(received == 123456700, 0); + } + + #[test] + #[expected_failure(abort_code = oft::oft_core::ESLIPPAGE_EXCEEDED)] + fun test_debit_view_fails_if_less_than_min() { + setup(); + + oft_impl::debit_view(32, 100, 2); + } + + #[test] + fun test_build_options() { + setup(); + let dst_eid = 103; + + let message_type = 2; + + let options = oft_impl::build_options( + message_type, + dst_eid, + // OKAY that it's not type 3 if no enforced options are set + x"1234", + @123, + 123324, + bytes32::from_address(@444), + x"8888", + x"34" + ); + // should pass through the options if none configured + assert!(options == x"1234", 0); + + let oft_admin = &create_signer_for_test(@oft_admin); + oapp_core::set_enforced_options( + oft_admin, + dst_eid, + message_type, + x"00037777" + ); + + let options = oft_impl::build_options( + message_type, + dst_eid, + x"00031234", + @123, + 123324, + bytes32::from_address(@444), + x"8888", + x"34" + ); + + // should append to configured options + assert!(options == x"000377771234", 0); + } + + #[test] + fun test_inspect_message() { + // doesn't do anything, just tests that it doesn't fail + oft_impl::inspect_message( + &x"1234", + &x"1234", + true, + ); + } + + #[test] + fun test_oft_limit_and_fees() { + setup(); + + timestamp::set_time_has_started_for_testing(&create_signer_for_test(@std)); + let (limit, fees) = oft_impl::oft_limit_and_fees( + 123, + x"1234", + 123, + 100, + x"1234", + x"1234", + x"1234" + ); + + // always unbounded and empty for this oft configuration + assert!(limit == new_unbounded_oft_limit(), 0); + assert!(vector::length(&fees) == 0, 0); + } + + #[test] + fun test_set_fee_bps() { + setup(); + + let oft_admin = &create_signer_for_test(@oft_admin); + let fee_bps = 500; // 5% + + set_fee_bps( + oft_admin, + fee_bps, + ); + + let fee_bps_result = fee_bps(); + assert!(fee_bps_result == fee_bps, 0); + + let (oft_limit, oft_fee_details) = oft_impl::oft_limit_and_fees( + 123, + x"1234", + 100_000_000, + 100, + x"1234", + x"1234", + x"1234" + ); + + // Check fee detail + assert!(vector::length(&oft_fee_details) == 1, 1); + let fee_detail = *vector::borrow(&oft_fee_details, 0); + let (fee_amount, is_reward) = oft_common::oft_fee_detail::fee_amount_ld(&fee_detail); + assert!(fee_amount == 5_000_000, 2); + assert!(is_reward == false, 3); + + // Check limit + assert!(oft_limit == new_unbounded_oft_limit(), 4); + + let deposit_address = @5555; + create_account_for_test(deposit_address); + set_fee_deposit_address( + oft_admin, + deposit_address, + ); + assert!(fee_deposit_address() == @5555, 1); + + // debit with fee + let dst_eid = 2u32; + // This configuration function (debit) is not resposible for handling dust, therefore the tested amount excludes + // the dust amount (last two digits) + let amount_ld = 123456700; + let min_amount_ld = 0u64; + + let fa = mint_tokens_for_test(amount_ld); + let (sent, received) = oft_impl::debit_fungible_asset( + @444, + &mut fa, + min_amount_ld, + dst_eid, + ); + + // amount sent and received should reflect the amount debited + assert!(sent == 123456700, 0); + // Any dust is also included in the fee + assert!(received == 117283800, 0); // 123456700 * 0.95 = 117283865 - remove dust -> 117283800 + + // no remaining balance + let remaining_balance = fungible_asset::amount(&fa); + assert!(remaining_balance == 00, 0); + burn_token_for_test(fa); + + // check that the fee was deposited + let fee_deposited = primary_fungible_store::balance(deposit_address, oft_impl::metadata()); + assert!(fee_deposited == 6172900, 0); // 123456700 - 117283800 = 6172900 + + // check the invariant that the total amount is conserved + assert!(received + fee_deposited == sent, 1); + } + + #[test] + fun test_set_blocklist_credit() { + setup(); + + let blocklisted_address = @0x1234; + assert!(is_blocklisted(blocklisted_address) == false, 0); + + let admin = &create_signer_for_test(@oft_admin); + oft_impl::set_blocklist( + admin, + blocklisted_address, + true, + ); + assert!(was_event_emitted(&oft_impl_config::blocklist_set_event(blocklisted_address, true)), 1); + assert!(is_blocklisted(blocklisted_address), 1); + + oft_impl::credit( + blocklisted_address, + 1234, + 12345, + option::none(), + ); + + assert!( + was_event_emitted(&oft_impl_config::blocked_amount_redirected_event(1234, blocklisted_address, @oft_admin)), + 2 + ); + + let admin_balance = primary_fungible_store::balance(@oft_admin, oft_impl::metadata()); + assert!(admin_balance == 1234, 3); + + oft_impl::set_blocklist( + admin, + blocklisted_address, + false, + ); + assert!(was_event_emitted(&oft_impl_config::blocklist_set_event(blocklisted_address, false)), 4); + assert!(is_blocklisted(blocklisted_address) == false, 5); + + oft_impl::credit( + blocklisted_address, + 1234, + 12345, + option::none(), + ); + + let to_balance = primary_fungible_store::balance(blocklisted_address, oft_impl::metadata()); + assert!(to_balance == 1234, 6); + + let admin_balance = primary_fungible_store::balance(@oft_admin, oft_impl::metadata()); + // unchanged + assert!(admin_balance == 1234, 7); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EADDRESS_BLOCKED)] + fun test_set_blocklist_debit() { + setup(); + + let blocklisted_address = @0x1234; + primary_fungible_store::deposit(blocklisted_address, oft_impl::mint_tokens_for_test(10_000)); + + assert!(is_blocklisted(blocklisted_address) == false, 0); + + let admin = &create_signer_for_test(@oft_admin); + oft_impl::set_blocklist( + admin, + blocklisted_address, + true, + ); + + let debit_tokens = mint_tokens_for_test(1234); + oft_impl::debit_fungible_asset( + blocklisted_address, + &mut debit_tokens, + 0, + 12345, + ); + burn_token_for_test(debit_tokens); + } + + #[test] + #[expected_failure(abort_code = 0x50003, location = std::fungible_asset)] + fun cannot_transfer_blocklisted_tokens() { + setup(); + + let blocklisted_address = @0x1234; + primary_fungible_store::deposit(blocklisted_address, oft_impl::mint_tokens_for_test(10_000)); + + assert!(is_blocklisted(blocklisted_address) == false, 0); + + let admin = &create_signer_for_test(@oft_admin); + oft_impl::set_blocklist( + admin, + blocklisted_address, + true, + ); + + primary_fungible_store::transfer( + &create_signer_for_test(blocklisted_address), + oft_impl::metadata(), + @9888, + 2000 + ); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EBLOCKLIST_DISABLED)] + fun test_disable_blocklist() { + setup(); + + let admin = &create_signer_for_test(@oft_admin); + oft_impl::irrevocably_disable_blocklist(admin); + assert!(was_event_emitted(&oft_impl_config::blocklisting_disabled_event()), 1); + + let blocklisted_address = @0x1234; + oft_impl::set_blocklist( + admin, + blocklisted_address, + true, + ); + } + + #[test] + fun test_rate_limit() { + setup(); + + let (limit, window) = oft_impl::rate_limit_config(30100); + assert!(limit == 0 && window == 0, 0); + + let admin = &create_signer_for_test(@oft_admin); + oft_impl::set_rate_limit(admin, 30100, 2500, 100); + assert!(was_event_emitted(&oft_impl_config::rate_limit_set_event(30100, 2500, 100)), 1); + + let (limit, window) = oft_impl::rate_limit_config(30100); + assert!(limit == 2500 && window == 100, 1); + + let (oft_limit, fee_detail) = oft_impl::oft_limit_and_fees( + 30100, + x"1234", + 123, + 100, + x"1234", + x"1234", + x"1234" + ); + + // no fee + assert!(vector::length(&fee_detail) == 0, 2); + + // rate limit + let (min_amount_ld, max_amount_ld) = oft_common::oft_limit::unpack_oft_limit(oft_limit); + assert!(min_amount_ld == 0 && max_amount_ld == 2500, 3); + } +} diff --git a/examples/oft-adapter-aptos-move/test/movement/oft_using_oft_fa_tests.move b/examples/oft-adapter-aptos-move/test/movement/oft_using_oft_fa_tests.move new file mode 100644 index 000000000..a1d4ee579 --- /dev/null +++ b/examples/oft-adapter-aptos-move/test/movement/oft_using_oft_fa_tests.move @@ -0,0 +1,305 @@ +// **Important** This module tests the behavior of OFT assuming that it is connected to OFT_FA. When connecting to +// a different template such as OFT_ADAPTER_FA or configuring the OFT module differently, the tests will no longer be +// valid. +#[test_only] +module oft::oft_using_oft_fa_tests { + use std::account::create_signer_for_test; + use std::fungible_asset; + use std::option; + use std::primary_fungible_store::{Self, balance}; + use std::signer::address_of; + use std::vector; + + use endpoint_v2::endpoint; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32::{Self, from_address, from_bytes32}; + use endpoint_v2_common::contract_identity::make_dynamic_call_ref_for_test; + use endpoint_v2_common::guid; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::packet_v1_codec::{Self, compute_payload}; + use oft::oapp_core::{set_pause_sending, set_peer}; + use oft::oapp_receive::lz_receive; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft::{ + debit_view, quote_oft, quote_send, remove_dust, send, send_withdraw, to_ld, to_sd, token, unpack_oft_receipt, + }; + use oft::oft_impl::{mint_tokens_for_test, set_fee_bps, set_rate_limit}; + use oft::oft_store; + use oft_common::oft_limit::{max_amount_ld, min_amount_ld}; + use oft_common::oft_msg_codec; + + const MAXU64: u64 = 0xffffffffffffffff; + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32) { + let oft_admin = &create_signer_for_test(@oft_admin); + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + oft::oapp_test_helper::init_oapp(); + + oft::oft_impl_config::init_module_for_test(); + oft_store::init_module_for_test(); + oft::oft_impl::init_module_for_test(); + oft::oft_impl::initialize( + oft_admin, + b"My Test Token", + b"MYT", + b"", + b"", + 6, + 8, + ); + + let remote_oapp = from_address(@2000); + set_peer(oft_admin, DST_EID, from_bytes32(remote_oapp)); + } + + #[test] + fun test_quote_oft() { + setup(SRC_EID, DST_EID); + + let receipient = from_address(@2000); + let amount_ld = 100u64 * 100_000_000; // 100 TOKEN + let compose_msg = vector[]; + let (limit, fees, amount_sent_ld, amount_received_ld) = quote_oft( + DST_EID, + from_bytes32(receipient), + amount_ld, + 0, + vector[], + compose_msg, + vector[] + ); + assert!(min_amount_ld(&limit) == 0, 0); + assert!(max_amount_ld(&limit) == MAXU64, 1); + assert!(vector::length(&fees) == 0, 2); + + assert!(amount_sent_ld == amount_ld, 3); + assert!(amount_received_ld == amount_ld, 3); + } + + #[test] + fun test_quote_send() { + setup(SRC_EID, DST_EID); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let (native_fee, zro_fee) = quote_send( + @1000, + DST_EID, + from_bytes32(from_address(@2000)), + amount, + amount, + vector[], + vector[], + vector[], + false, + ); + assert!(native_fee == 0, 0); + assert!(zro_fee == 0, 1); + } + + #[test] + fun test_send_fa() { + setup(SRC_EID, DST_EID); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_address(@5678); + let tokens = mint_tokens_for_test(amount); + + let native_fee = mint_native_token_for_test(10000000); + let zro_fee = option::none(); + send( + &make_dynamic_call_ref_for_test(address_of(alice), OAPP_ADDRESS(), b"send"), + DST_EID, + bob, + &mut tokens, + amount, + vector[], + vector[], + vector[], + &mut native_fee, + &mut zro_fee, + ); + assert!(fungible_asset::amount(&tokens) == 0, 1); // after send balance + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + burn_token_for_test(tokens); + } + + #[test] + fun test_send_fa_with_fee() { + setup(SRC_EID, DST_EID); + // 10% fee + set_fee_bps(&create_signer_for_test(@oft_admin), 1000); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_address(@5678); + let tokens = mint_tokens_for_test(amount); + + let native_fee = mint_native_token_for_test(10000000); + let zro_fee = option::none(); + let (_messaging_receipt, oft_receipt) = send( + &make_dynamic_call_ref_for_test(address_of(alice), OAPP_ADDRESS(), b"send"), + DST_EID, + bob, + &mut tokens, + 9_000_000, + vector[], + vector[], + vector[], + &mut native_fee, + &mut zro_fee, + ); + assert!(fungible_asset::amount(&tokens) == 0, 1); // after send balance + + let (sent, received) = unpack_oft_receipt(&oft_receipt); + assert!(sent == 10_000_000_000, 2); + assert!(received == 9_000_000_000, 2); + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + burn_token_for_test(tokens); + } + + #[test] + fun test_send() { + setup(SRC_EID, DST_EID); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + let tokens = mint_tokens_for_test(amount); + primary_fungible_store::deposit(address_of(alice), tokens); + assert!(balance(address_of(alice), oft::oft::metadata()) == amount, 0); // before send balance + + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + assert!(balance(address_of(alice), oft::oft::metadata()) == 0, 1); // after send balance + } + + #[test] + #[expected_failure(abort_code = oft::oapp_core::ESEND_PAUSED)] + fun test_send_paused() { + setup(SRC_EID, DST_EID); + set_pause_sending(&create_signer_for_test(@oft_admin), DST_EID, true); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + let tokens = mint_tokens_for_test(amount); + primary_fungible_store::deposit(address_of(alice), tokens); + assert!(balance(address_of(alice), oft::oft::metadata()) == amount, 0); // before send balance + + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EEXCEEDED_RATE_LIMIT)] + fun test_send_exceed_rate_limit() { + setup(SRC_EID, DST_EID); + set_rate_limit(&create_signer_for_test(@oft_admin), DST_EID, 19_000_000_000, 10); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + // 3x the required tokens + let tokens = mint_tokens_for_test(amount * 3); + primary_fungible_store::deposit(address_of(alice), tokens); + + // Succeeds: consumes 10_000_000_000 of 19_000_000_000 + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + // Fails (rate limit exceeded): consumes 20_000_000_000 of 19_000_000_000 + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + } + + #[test] + fun test_send_rate_limit_netted_by_receive() { + setup(SRC_EID, DST_EID); + set_rate_limit(&create_signer_for_test(@oft_admin), DST_EID, 19_000_000_000, 10); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + // 3x the required tokens + let tokens = mint_tokens_for_test(amount * 3); + primary_fungible_store::deposit(address_of(alice), tokens); + + // Succeeds: consumes 10_000_000_000 of 19_000_000_000 in flight + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + + // Receive 5_000_000_000: 5_000_000_000 of 19_000_000_000 in flight + let message = oft_msg_codec::encode( + from_address(@1234), + 5_000_000_000, + from_address(@0xffff), + b"", + ); + // Reverse the EIDs to simulate a receive from the remote OFT + let packet = packet_v1_codec::new_packet_v1( + DST_EID, + from_address(@2000), // remote OFT + SRC_EID, + from_address(@oft), + 1, + guid::compute_guid( + 1, + DST_EID, + from_address(@2000), + SRC_EID, + from_address(@oft), + ), + message, + ); + endpoint::verify( + @simple_msglib, + packet_raw::get_packet_bytes(packet_v1_codec::extract_header(&packet)), + from_bytes32(bytes32::keccak256(compute_payload( + packet_v1_codec::get_guid(&packet), + packet_v1_codec::get_message(&packet), + ))), + ); + lz_receive( + packet_v1_codec::get_src_eid(&packet), + from_bytes32(packet_v1_codec::get_sender(&packet)), + packet_v1_codec::get_nonce(&packet), + from_bytes32(packet_v1_codec::get_guid(&packet)), + message, + vector[], + ); + + + // Fails (rate limit exceeded): consumes 15_000_000_000 of 19_000_000_000 in flight + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + } + + #[test] + fun test_metadata_view_functions() { + setup(SRC_EID, DST_EID); + + // token is unknown at time of test - just calling to check it doesn't abort + token(); + + assert!(to_ld(100) == 10000, 0); + assert!(to_sd(100) == 1, 1); + assert!(remove_dust(123) == 100, 2); + + let (sent, received) = debit_view(1234, 0, DST_EID); + assert!(sent == 1200, 3); + assert!(received == 1200, 4); + } +} \ No newline at end of file diff --git a/examples/oft-adapter-aptos-move/tests/implementations/oft_adapter_fa_tests.move b/examples/oft-adapter-aptos-move/tests/implementations/oft_adapter_fa_tests.move new file mode 100644 index 000000000..a39af1cdf --- /dev/null +++ b/examples/oft-adapter-aptos-move/tests/implementations/oft_adapter_fa_tests.move @@ -0,0 +1,543 @@ +// **Important** These tests are only valid for the default configuration of OFT Adapter FA. If the configuration is +// changed, these tests will need to be updated to reflect those changes +#[test_only] +module oft::oft_adapter_fa_tests { + use std::account::{create_account_for_test, create_signer_for_test}; + use std::event::was_event_emitted; + use std::fungible_asset::{Self, Metadata}; + use std::fungible_asset::{mint, MintRef}; + use std::object::address_to_object; + use std::option; + use std::primary_fungible_store; + use std::timestamp; + use std::vector; + + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, initialize_native_token_for_test, + mint_native_token_for_test + }; + use endpoint_v2_common::zro_test_helpers::create_fa; + use oft::oapp_core; + use oft::oft_adapter_fa::{ + Self, + escrow_address, + fee_bps, + fee_deposit_address, + is_blocklisted, + set_fee_bps, + set_fee_deposit_address, + }; + use oft::oft_impl_config; + use oft::oft_store; + use oft_common::oft_limit::new_unbounded_oft_limit; + + const MAXU64: u64 = 0xffffffffffffffff; + const LOCAL_EID: u32 = 101; + + fun setup(): MintRef { + initialize_native_token_for_test(); + setup_layerzero_for_test(@simple_msglib, LOCAL_EID, LOCAL_EID); + + oft::oapp_test_helper::init_oapp(); + + oft_store::init_module_for_test(); + oft_adapter_fa::init_module_for_test(); + oft_impl_config::init_module_for_test(); + + // Generates a fungible asset with 8 decimals + let (fa, _, mint_ref) = create_fa(b"My Test Token"); + + oft_adapter_fa::initialize( + &create_signer_for_test(@oft_admin), + fa, + // Some of the tests expect that that shared decimals is 6. If this is changed, the tests will need to be + // updated (specifically the values need to be adjusted for dust) + 6, + ); + + mint_ref + } + + #[test] + fun test_debit() { + let mint_ref = setup(); + + let dst_eid = 2u32; + // This configuration function (debit) is not resposible for handling dust, therefore the tested amount excludes + // the dust amount (last two digits) + let amount_ld = 123456700; + let min_amount_ld = 0u64; + + let fa = mint(&mint_ref, amount_ld); + let (sent, received) = oft_adapter_fa::debit_fungible_asset( + @444, + &mut fa, + min_amount_ld, + dst_eid, + ); + + // amount sent and received should reflect the amount debited + assert!(sent == 123456700, 0); + assert!(received == 123456700, 0); + + // no remaining balance in debited account + let remaining_balance = fungible_asset::amount(&fa); + assert!(remaining_balance == 00, 0); + burn_token_for_test(fa); + + // escrow balance should increase to match + let balance = primary_fungible_store::balance(escrow_address(), oft_adapter_fa::metadata()); + assert!(balance == 123456700, 0); + } + + #[test] + fun test_credit() { + let mint_ref = setup(); + + let amount_ld = 123456700; + let lz_receive_value = option::none(); + let src_eid = 12345; + + // debit first to make sure account has balance + + let deposit = mint(&mint_ref, amount_ld); + oft_adapter_fa::debit_fungible_asset( + @444, + &mut deposit, + 0, + src_eid, + ); + burn_token_for_test(deposit); + + let balance = primary_fungible_store::balance(escrow_address(), oft_adapter_fa::metadata()); + assert!(balance == amount_ld, 0); + + let to = @555; + create_account_for_test(to); + + // 0 balance before crediting + let balance = primary_fungible_store::balance(to, oft_adapter_fa::metadata()); + assert!(balance == 0, 0); + + let credited = oft_adapter_fa::credit( + to, + amount_ld, + src_eid, + lz_receive_value, + ); + // amount credited should reflect the amount credited + assert!(credited == 123456700, 0); + + // balance should appear in recipient account + let balance = primary_fungible_store::balance(to, oft_adapter_fa::metadata()); + assert!(balance == 123456700, 0); + + // escrow balance should be back to 0 + let balance = primary_fungible_store::balance(escrow_address(), oft_adapter_fa::metadata()); + assert!(balance == 0, 0); + } + + #[test] + fun test_tvl() { + let mint_ref = setup(); + + let tvl = oft_adapter_fa::tvl(); + assert!(tvl == 0, 0); + + let eid = 2u32; + let amount_ld = 1000000000; + let alice = @555; + + // deposit some + let fa = mint(&mint_ref, amount_ld); + oft_adapter_fa::debit_fungible_asset( + alice, + &mut fa, + 0, + eid, + ); + + let tvl = oft_adapter_fa::tvl(); + assert!(tvl == 1000000000, 1); + + burn_token_for_test(fa); + + // withdraw some + let amount_ld = 600000000; + oft_adapter_fa::credit( + alice, + amount_ld, + eid, + option::none(), + ); + + let tvl = oft_adapter_fa::tvl(); + assert!(tvl == 400000000, 2); + } + + #[test] + #[expected_failure(abort_code = 0x10004, location = std::fungible_asset)] + fun test_credit_fails_if_insufficient_balance() { + setup(); + + let amount_ld = 123456700; + let lz_receive_value = option::none(); + let src_eid = 12345; + + let to = @555; + create_account_for_test(to); + + // 0 balance before crediting + let balance = primary_fungible_store::balance(to, oft_adapter_fa::metadata()); + assert!(balance == 0, 0); + + let credited = oft_adapter_fa::credit( + to, + amount_ld, + src_eid, + lz_receive_value, + ); + // amount credited should reflect the amount credited + assert!(credited == 123456700, 0); + + // balance should appear in account + let balance = primary_fungible_store::balance(to, oft_adapter_fa::metadata()); + assert!(balance == 123456700, 0); + } + + #[test] + fun test_credit_with_extra_lz_receive_drop() { + setup(); + + let amount_ld = 0; + let lz_receive_value = option::some(mint_native_token_for_test(100)); + let src_eid = 12345; + + let to = @555; + create_account_for_test(to); + + // 0 balance before crediting + let balance = primary_fungible_store::balance(to, oft_adapter_fa::metadata()); + assert!(balance == 0, 0); + + oft_adapter_fa::credit( + to, + amount_ld, + src_eid, + lz_receive_value, + ); + + let native_token_metadata = address_to_object(@native_token_metadata_address); + assert!(primary_fungible_store::balance(@oft_admin, native_token_metadata) == 100, 1) + } + + #[test] + fun test_debit_view() { + setup(); + + // shouldn't take a fee + let (sent, received) = oft_adapter_fa::debit_view(123456700, 100, 2); + assert!(sent == 123456700, 0); + assert!(received == 123456700, 0); + } + + #[test] + #[expected_failure(abort_code = oft::oft_core::ESLIPPAGE_EXCEEDED)] + fun test_debit_view_fails_if_less_than_min() { + setup(); + + oft_adapter_fa::debit_view(32, 100, 2); + } + + #[test] + fun test_build_options() { + setup(); + let dst_eid = 103; + + let message_type = 2; + + let options = oft_adapter_fa::build_options( + message_type, + dst_eid, + // OKAY that it's not type 3 if no enforced options are set + x"1234", + @123, + 123324, + bytes32::from_address(@444), + x"8888", + x"34" + ); + // should pass through the options if none configured + assert!(options == x"1234", 0); + + let oft_admin = &create_signer_for_test(@oft_admin); + oapp_core::set_enforced_options( + oft_admin, + dst_eid, + message_type, + x"00037777" + ); + + let options = oft_adapter_fa::build_options( + message_type, + dst_eid, + x"00031234", + @123, + 123324, + bytes32::from_address(@444), + x"8888", + x"34" + ); + + // should append to configured options + assert!(options == x"000377771234", 0); + } + + #[test] + fun test_inspect_message() { + initialize_native_token_for_test(); + // doesn't do anything, just tests that it doesn't fail + oft_adapter_fa::inspect_message( + &x"1234", + &x"1234", + true, + ); + } + + #[test] + fun test_oft_limit_and_fees() { + setup(); + + timestamp::set_time_has_started_for_testing(&create_signer_for_test(@std)); + initialize_native_token_for_test(); + let (limit, fees) = oft_adapter_fa::oft_limit_and_fees( + 123, + x"1234", + 123, + 100, + x"1234", + x"1234", + x"1234" + ); + + // always unbounded and empty for this oft configuration + assert!(limit == new_unbounded_oft_limit(), 0); + assert!(vector::length(&fees) == 0, 0); + } + + #[test] + fun test_set_fee_bps() { + let mint_ref = &setup(); + + let oft_admin = &create_signer_for_test(@oft_admin); + let fee_bps = 500; // 5% + + set_fee_bps( + oft_admin, + fee_bps, + ); + + let fee_bps_result = fee_bps(); + assert!(fee_bps_result == fee_bps, 0); + + let (oft_limit, oft_fee_details) = oft_adapter_fa::oft_limit_and_fees( + 123, + x"1234", + 100_000_000, + 100, + x"1234", + x"1234", + x"1234" + ); + + // Check fee detail + assert!(vector::length(&oft_fee_details) == 1, 1); + let fee_detail = *vector::borrow(&oft_fee_details, 0); + let (fee_amount, is_reward) = oft_common::oft_fee_detail::fee_amount_ld(&fee_detail); + assert!(fee_amount == 5_000_000, 2); + assert!(is_reward == false, 3); + + // Check limit + assert!(oft_limit == new_unbounded_oft_limit(), 4); + + let deposit_address = @5555; + create_account_for_test(deposit_address); + set_fee_deposit_address( + oft_admin, + deposit_address, + ); + assert!(fee_deposit_address() == @5555, 1); + + // debit with fee + let dst_eid = 2u32; + // This configuration function (debit) is not resposible for handling dust, therefore the tested amount excludes + // the dust amount (last two digits) + let amount_ld = 123456700; + let min_amount_ld = 0u64; + + let fa = fungible_asset::mint(mint_ref, amount_ld); + let (sent, received) = oft_adapter_fa::debit_fungible_asset( + @444, + &mut fa, + min_amount_ld, + dst_eid, + ); + + // amount sent and received should reflect the amount debited + assert!(sent == 123456700, 0); + // Any dust is also included in the fee + assert!(received == 117283800, 0); // 123456700 * 0.95 = 117283865 - remove dust -> 117283800 + + // no remaining balance + let remaining_balance = fungible_asset::amount(&fa); + assert!(remaining_balance == 00, 0); + burn_token_for_test(fa); + + // check that the fee was deposited + let fee_deposited = primary_fungible_store::balance(deposit_address, oft_adapter_fa::metadata()); + assert!(fee_deposited == 6172900, 0); // 123456700 - 117283800 = 6172900 + + // check the invariant that the total amount is conserved + assert!(received + fee_deposited == sent, 1); + } + + #[test] + fun test_set_blocklist_credit() { + let mint_ref = &setup(); + + let blocklisted_address = @0x1234; + assert!(is_blocklisted(blocklisted_address) == false, 0); + + let deposit_fa = fungible_asset::mint(mint_ref, 100_000); + oft_adapter_fa::debit_fungible_asset( + blocklisted_address, + &mut deposit_fa, + 0, + 12345, + ); + burn_token_for_test(deposit_fa); + + let admin = &create_signer_for_test(@oft_admin); + oft_adapter_fa::set_blocklist( + admin, + blocklisted_address, + true, + ); + assert!(was_event_emitted(&oft_impl_config::blocklist_set_event(blocklisted_address, true)), 1); + assert!(is_blocklisted(blocklisted_address), 1); + + oft_adapter_fa::credit( + blocklisted_address, + 1234, + 12345, + option::none(), + ); + + assert!( + was_event_emitted(&oft_impl_config::blocked_amount_redirected_event(1234, blocklisted_address, @oft_admin)), + 2 + ); + + let admin_balance = primary_fungible_store::balance(@oft_admin, oft_adapter_fa::metadata()); + assert!(admin_balance == 1234, 3); + + oft_adapter_fa::set_blocklist( + admin, + blocklisted_address, + false, + ); + assert!(was_event_emitted(&oft_impl_config::blocklist_set_event(blocklisted_address, false)), 4); + assert!(is_blocklisted(blocklisted_address) == false, 5); + + oft_adapter_fa::credit( + blocklisted_address, + 1234, + 12345, + option::none(), + ); + + let to_balance = primary_fungible_store::balance(blocklisted_address, oft_adapter_fa::metadata()); + assert!(to_balance == 1234, 6); + + let admin_balance = primary_fungible_store::balance(@oft_admin, oft_adapter_fa::metadata()); + // unchanged + assert!(admin_balance == 1234, 7); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EADDRESS_BLOCKED)] + fun test_set_blocklist_debit() { + let mint_ref = &setup(); + + let blocklisted_address = @0x1234; + primary_fungible_store::deposit(blocklisted_address, fungible_asset::mint(mint_ref, 10_000)); + + assert!(is_blocklisted(blocklisted_address) == false, 0); + + let admin = &create_signer_for_test(@oft_admin); + oft_adapter_fa::set_blocklist( + admin, + blocklisted_address, + true, + ); + + let debit_tokens = fungible_asset::mint(mint_ref, 1234); + oft_adapter_fa::debit_fungible_asset( + blocklisted_address, + &mut debit_tokens, + 0, + 12345, + ); + burn_token_for_test(debit_tokens); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EBLOCKLIST_DISABLED)] + fun test_disable_blocklist() { + setup(); + + let admin = &create_signer_for_test(@oft_admin); + oft_adapter_fa::irrevocably_disable_blocklist(admin); + assert!(was_event_emitted(&oft_impl_config::blocklisting_disabled_event()), 1); + + let blocklisted_address = @0x1234; + oft_adapter_fa::set_blocklist( + admin, + blocklisted_address, + true, + ); + } + + #[test] + fun test_rate_limit() { + setup(); + + let (limit, window) = oft_adapter_fa::rate_limit_config(30100); + assert!(limit == 0 && window == 0, 0); + + let admin = &create_signer_for_test(@oft_admin); + oft_adapter_fa::set_rate_limit(admin, 30100, 2500, 100); + assert!(was_event_emitted(&oft_impl_config::rate_limit_set_event(30100, 2500, 100)), 1); + + let (limit, window) = oft_adapter_fa::rate_limit_config(30100); + assert!(limit == 2500 && window == 100, 1); + + let (oft_limit, fee_detail) = oft_adapter_fa::oft_limit_and_fees( + 30100, + x"1234", + 123, + 100, + x"1234", + x"1234", + x"1234" + ); + + // no fee + assert!(vector::length(&fee_detail) == 0, 2); + + // rate limit + let (min_amount_ld, max_amount_ld) = oft_common::oft_limit::unpack_oft_limit(oft_limit); + assert!(min_amount_ld == 0 && max_amount_ld == 2500, 3); + } +} diff --git a/examples/oft-adapter-aptos-move/tests/oapp_receive_using_oft_adapter_fa_tests.move b/examples/oft-adapter-aptos-move/tests/oapp_receive_using_oft_adapter_fa_tests.move new file mode 100644 index 000000000..26e6ebccd --- /dev/null +++ b/examples/oft-adapter-aptos-move/tests/oapp_receive_using_oft_adapter_fa_tests.move @@ -0,0 +1,172 @@ +#[test_only] +module oft::oapp_receive_using_oft_adapter_fa_tests { + use std::account::create_signer_for_test; + use std::event::was_event_emitted; + use std::fungible_asset::{Self, MintRef}; + use std::primary_fungible_store; + use std::string::utf8; + + use endpoint_v2::endpoint; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32::{Self, from_address, from_bytes32}; + use endpoint_v2_common::native_token_test_helpers::initialize_native_token_for_test; + use endpoint_v2_common::packet_v1_codec::{Self, compute_payload_hash}; + use endpoint_v2_common::zro_test_helpers::create_fa; + use oft::oapp_core; + use oft::oapp_receive; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_adapter_fa; + use oft::oft_core; + use oft::oft_impl_config; + use oft::oft_store; + use oft_common::oft_compose_msg_codec; + use oft_common::oft_msg_codec; + + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32): MintRef { + // Test the send function + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + let oft_admin = &create_signer_for_test(@oft_admin); + initialize_native_token_for_test(); + oft::oapp_test_helper::init_oapp(); + + oft_store::init_module_for_test(); + oft_impl_config::init_module_for_test(); + oft_adapter_fa::init_module_for_test(); + + // Generates a fungible asset with 8 decimals + let (fa, _, mint_ref) = create_fa(b"My Test Token"); + oft_adapter_fa::initialize(oft_admin, fa, 6); + + oapp_core::set_peer(oft_admin, SRC_EID, from_bytes32(from_address(@1234))); + oapp_core::set_peer(oft_admin, DST_EID, from_bytes32(from_address(@4321))); + + mint_ref + } + + #[test] + fun test_receive() { + let mint_ref = setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"", + ); + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + b"" + ); + + let escrow = oft_adapter_fa::escrow_address(); + primary_fungible_store::deposit(escrow, fungible_asset::mint(&mint_ref, 20000)); + + oapp_receive::lz_receive( + SRC_EID, + from_bytes32(sender), + nonce, + from_bytes32(guid), + message, + b"", + ); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 12300, + )), 3); + } + + #[test] + fun test_receive_with_compose() { + let mint_ref = setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"Hello", + ); + + // Composer must be registered + let to_address_account = &create_signer_for_test(@0x2000); + endpoint::register_composer(to_address_account, utf8(b"composer")); + + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + b"" + ); + + let escrow = oft_adapter_fa::escrow_address(); + primary_fungible_store::deposit(escrow, fungible_asset::mint(&mint_ref, 20000)); + + oapp_receive::lz_receive( + SRC_EID, + from_bytes32(sender), + nonce, + from_bytes32(guid), + message, + b"", + ); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 12300, + )), 3); + + let compose_message_part = oft_msg_codec::compose_payload(&message); + let expected_compose_message = oft_compose_msg_codec::encode( + nonce, + SRC_EID, + 12300, + compose_message_part, + ); + + // Compose Triggered to the same address + assert!(was_event_emitted(&endpoint_v2::messaging_composer::compose_sent_event( + OAPP_ADDRESS(), + @0x2000, + from_bytes32(guid), + 0, + expected_compose_message, + )), 0); + } +} \ No newline at end of file diff --git a/examples/oft-adapter-aptos-move/tests/oft_using_oft_adapter_fa_tests.move b/examples/oft-adapter-aptos-move/tests/oft_using_oft_adapter_fa_tests.move new file mode 100644 index 000000000..ddf7b6c5d --- /dev/null +++ b/examples/oft-adapter-aptos-move/tests/oft_using_oft_adapter_fa_tests.move @@ -0,0 +1,338 @@ +#[test_only] +module oft::oft_using_oft_adapter_fa_tests { + use std::account::{create_account_for_test, create_signer_for_test}; + use std::fungible_asset; + use std::fungible_asset::MintRef; + use std::option; + use std::primary_fungible_store::{Self, balance}; + use std::signer::address_of; + use std::vector; + + use endpoint_v2::endpoint; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32::{Self, from_address, from_bytes32}; + use endpoint_v2_common::contract_identity::make_dynamic_call_ref_for_test; + use endpoint_v2_common::guid; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::packet_v1_codec::{Self, compute_payload}; + use endpoint_v2_common::zro_test_helpers::create_fa; + use oft::oapp_core::set_peer; + use oft::oapp_receive::lz_receive; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft::{ + debit_view, quote_oft, quote_send, remove_dust, send, send_withdraw, to_ld, to_sd, token, unpack_oft_receipt, + }; + use oft::oft_adapter_fa; + use oft::oft_adapter_fa::{set_fee_bps, set_rate_limit}; + use oft::oft_store; + use oft_common::oft_limit::{max_amount_ld, min_amount_ld}; + use oft_common::oft_msg_codec; + + const MAXU64: u64 = 0xffffffffffffffff; + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32): MintRef { + let oft_admin = &create_signer_for_test(@oft_admin); + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + oft::oapp_test_helper::init_oapp(); + + oft::oft_impl_config::init_module_for_test(); + oft_store::init_module_for_test(); + oft::oft_adapter_fa::init_module_for_test(); + + // Generates a fungible asset with 8 decimals + let (fa, _, mint_ref) = create_fa(b"My Test Token"); + oft_adapter_fa::initialize(oft_admin, fa, 6); + + let remote_oapp = from_address(@2000); + set_peer(oft_admin, DST_EID, from_bytes32(remote_oapp)); + + mint_ref + } + + #[test] + fun test_quote_oft() { + let _mint_ref = setup(SRC_EID, DST_EID); + + let receipient = from_address(@2000); + let amount_ld = 100u64 * 100_000_000; // 100 TOKEN + let compose_msg = vector[]; + let (limit, fees, amount_sent_ld, amount_received_ld) = quote_oft( + DST_EID, + from_bytes32(receipient), + amount_ld, + 0, + vector[], + compose_msg, + vector[] + ); + assert!(min_amount_ld(&limit) == 0, 0); + assert!(max_amount_ld(&limit) == MAXU64, 1); + assert!(vector::length(&fees) == 0, 2); + + assert!(amount_sent_ld == amount_ld, 3); + assert!(amount_received_ld == amount_ld, 3); + } + + #[test] + fun test_quote_send() { + let _mint_ref = setup(SRC_EID, DST_EID); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let (native_fee, zro_fee) = quote_send( + @1000, + DST_EID, + from_bytes32(from_address(@2000)), + amount, + amount, + vector[], + vector[], + vector[], + false, + ); + assert!(native_fee == 0, 0); + assert!(zro_fee == 0, 1); + } + + #[test] + fun test_send_fa() { + let mint_ref = setup(SRC_EID, DST_EID); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_address(@5678); + let tokens = fungible_asset::mint(&mint_ref, amount); + + let native_fee = mint_native_token_for_test(10000000); + let zro_fee = option::none(); + send( + &make_dynamic_call_ref_for_test(address_of(alice), OAPP_ADDRESS(), b"send"), + DST_EID, + bob, + &mut tokens, + amount, + vector[], + vector[], + vector[], + &mut native_fee, + &mut zro_fee, + ); + assert!(fungible_asset::amount(&tokens) == 0, 1); // after send balance + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + burn_token_for_test(tokens); + } + + #[test] + fun test_send_fa_with_fee() { + let mint_ref = setup(SRC_EID, DST_EID); + // 10% fee + set_fee_bps(&create_signer_for_test(@oft_admin), 1000); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_address(@5678); + let tokens = fungible_asset::mint(&mint_ref, amount); + + let native_fee = mint_native_token_for_test(10000000); + let zro_fee = option::none(); + let (_messaging_receipt, oft_receipt) = send( + &make_dynamic_call_ref_for_test(address_of(alice), OAPP_ADDRESS(), b"send"), + DST_EID, + bob, + &mut tokens, + 9_000_000, + vector[], + vector[], + vector[], + &mut native_fee, + &mut zro_fee, + ); + assert!(fungible_asset::amount(&tokens) == 0, 1); // after send balance + + let (sent, received) = unpack_oft_receipt(&oft_receipt); + assert!(sent == 10_000_000_000, 2); + assert!(received == 9_000_000_000, 2); + + // Fee transferred to admin + assert!(balance(@oft_admin, oft::oft::metadata()) == 1_000_000_000, 3); // fee balance + + // send again with different deposit address + create_account_for_test(@9999); + oft::oft_adapter_fa::set_fee_deposit_address(&create_signer_for_test(@oft_admin), @9999); + + burn_token_for_test(tokens); + + let tokens = fungible_asset::mint(&mint_ref, amount); + + let (_messaging_receipt, oft_receipt) = send( + &make_dynamic_call_ref_for_test(address_of(alice), OAPP_ADDRESS(), b"send"), + DST_EID, + bob, + &mut tokens, + 9_000_000, + vector[], + vector[], + vector[], + &mut native_fee, + &mut zro_fee, + ); + assert!(fungible_asset::amount(&tokens) == 0, 1); // after send balance + + let (sent, received) = unpack_oft_receipt(&oft_receipt); + assert!(sent == 10_000_000_000, 2); + assert!(received == 9_000_000_000, 2); + + // Admin balance unchanged + assert!(balance(@oft_admin, oft::oft::metadata()) == 1_000_000_000, 3); // fee balance + // Fee transferred to new deposit address + assert!(balance(@9999, oft::oft::metadata()) == 1_000_000_000, 3); // fee balance + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + burn_token_for_test(tokens); + } + + #[test] + fun test_send() { + let mint_ref = setup(SRC_EID, DST_EID); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + let tokens = fungible_asset::mint(&mint_ref, amount); + primary_fungible_store::deposit(address_of(alice), tokens); + assert!(balance(address_of(alice), oft::oft::metadata()) == amount, 0); // before send balance + + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + assert!(balance(address_of(alice), oft::oft::metadata()) == 0, 1); // after send balance + } + + #[test] + #[expected_failure(abort_code = oft::oft::EINSUFFICIENT_BALANCE)] + fun test_send_fails_if_insufficient_balance() { + let mint_ref = setup(SRC_EID, DST_EID); + + let amount = 100u64 * 100_000_000; // 100 TOKENS + let amount_balance = 50u64 * 100_000_000; // 50 TOKENS + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + let tokens = fungible_asset::mint(&mint_ref, amount_balance); + primary_fungible_store::deposit(address_of(alice), tokens); + assert!(balance(address_of(alice), oft::oft::metadata()) == amount_balance, 0); // before send balance + + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EEXCEEDED_RATE_LIMIT)] + fun test_send_exceed_rate_limit() { + let mint_ref = setup(SRC_EID, DST_EID); + set_rate_limit(&create_signer_for_test(@oft_admin), DST_EID, 19_000_000_000, 10); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + // 3x the required tokens + let tokens = fungible_asset::mint(&mint_ref, amount * 3); + primary_fungible_store::deposit(address_of(alice), tokens); + + // Succeeds: consumes 10_000_000_000 of 19_000_000_000 + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + // Fails (rate limit exceeded): consumes 20_000_000_000 of 19_000_000_000 + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + } + + #[test] + fun test_send_rate_limit_netted_by_receive() { + let mint_ref = setup(SRC_EID, DST_EID); + set_rate_limit(&create_signer_for_test(@oft_admin), DST_EID, 19_000_000_000, 10); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + // 3x the required tokens + let tokens = fungible_asset::mint(&mint_ref, amount * 3); + primary_fungible_store::deposit(address_of(alice), tokens); + + // Succeeds: consumes 10_000_000_000 of 19_000_000_000 in flight + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + + // Receive 5_000_000_000 (50_000_000 * 100 conversion factor) of 19_000_000_000 in flight + let message = oft_msg_codec::encode( + from_address(@1234), + 50_000_000, + from_address(@0xffff), + b"", + ); + // Reverse the EIDs to simulate a receive from the remote OFT + let packet = packet_v1_codec::new_packet_v1( + DST_EID, + from_address(@2000), // remote OFT + SRC_EID, + from_address(@oft), + 1, + guid::compute_guid( + 1, + DST_EID, + from_address(@2000), + SRC_EID, + from_address(@oft), + ), + message, + ); + endpoint::verify( + @simple_msglib, + packet_raw::get_packet_bytes(packet_v1_codec::extract_header(&packet)), + from_bytes32(bytes32::keccak256(compute_payload( + packet_v1_codec::get_guid(&packet), + packet_v1_codec::get_message(&packet), + ))), + b"" + ); + + + lz_receive( + packet_v1_codec::get_src_eid(&packet), + from_bytes32(packet_v1_codec::get_sender(&packet)), + packet_v1_codec::get_nonce(&packet), + from_bytes32(packet_v1_codec::get_guid(&packet)), + message, + vector[], + ); + + // Succeeds: consumes 15_000_000_000 of 19_000_000_000 in flight + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + } + + #[test] + fun test_metadata_view_functions() { + let _mint_ref = setup(SRC_EID, DST_EID); + + // token is unknown at time of test - just calling to check it doesn't abort + token(); + + assert!(to_ld(100) == 10000, 0); + assert!(to_sd(100) == 1, 1); + assert!(remove_dust(123) == 100, 2); + + let (sent, received) = debit_view(1234, 0, DST_EID); + assert!(sent == 1200, 3); + assert!(received == 1200, 4); + } +} \ No newline at end of file diff --git a/examples/oft-adapter-aptos-move/tests/shared_oapp/oapp_core_tests.move b/examples/oft-adapter-aptos-move/tests/shared_oapp/oapp_core_tests.move new file mode 100644 index 000000000..a8f21fdf1 --- /dev/null +++ b/examples/oft-adapter-aptos-move/tests/shared_oapp/oapp_core_tests.move @@ -0,0 +1,269 @@ +#[test_only] +module oft::oapp_core_tests { + use std::account::create_account_for_test; + use std::account::create_signer_for_test; + use std::event::was_event_emitted; + use std::fungible_asset::{Self, FungibleAsset}; + use std::option; + use std::primary_fungible_store; + use std::signer::address_of; + + use endpoint_v2::channels::packet_sent_event; + use endpoint_v2::messaging_receipt; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32::{Self, from_address, from_bytes32}; + use endpoint_v2_common::guid; + use endpoint_v2_common::native_token; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, initialize_native_token_for_test, + mint_native_token_for_test + }; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::universal_config; + use endpoint_v2_common::zro_test_helpers::create_fa; + use oft::oapp_core::{Self, withdraw_lz_fees}; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core::{SEND, SEND_AND_CALL}; + + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32) { + // Test the send function + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + let oft_admin = &create_signer_for_test(@oft_admin); + initialize_native_token_for_test(); + oft::oapp_test_helper::init_oapp(); + oapp_core::set_peer(oft_admin, SRC_EID, from_bytes32(from_address(@1234))); + oapp_core::set_peer(oft_admin, DST_EID, from_bytes32(from_address(@4321))); + } + + #[test] + fun test_send_internal() { + setup(SRC_EID, DST_EID); + + let called_send = false; + let called_inspect = false; + assert!(!called_inspect && !called_send, 0); + + let native_fee = mint_native_token_for_test(100000); + let zro_fee = option::none(); + + let messaging_receipt = oapp_core::lz_send( + DST_EID, + b"oapp-message", + b"options", + &mut native_fee, + &mut zro_fee, + ); + + let expected_guid = guid::compute_guid( + 1, + SRC_EID, + bytes32::from_address(OAPP_ADDRESS()), + DST_EID, + bytes32::from_address(@4321), + ); + + assert!(messaging_receipt::get_guid(&messaging_receipt) == expected_guid, 0); + + // 0 fees in simple msglib + assert!(messaging_receipt::get_native_fee(&messaging_receipt) == 0, 1); + assert!(messaging_receipt::get_zro_fee(&messaging_receipt) == 0, 1); + // nothing removed + assert!(fungible_asset::amount(&native_fee) == 100000, 0); + + assert!(messaging_receipt::get_nonce(&messaging_receipt) == 1, 2); + + let packet = packet_v1_codec::new_packet_v1( + SRC_EID, + bytes32::from_address(OAPP_ADDRESS()), + DST_EID, + bytes32::from_address(@4321), + 1, + expected_guid, + b"oapp-message", + ); + assert!(was_event_emitted(&packet_sent_event( + packet, + b"options", + @simple_msglib, + )), 0); + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + } + + #[test] + fun test_quote_internal() { + setup(SRC_EID, DST_EID); + + let (native_fee, zro_fee) = oapp_core::lz_quote( + DST_EID, + b"oapp-message", + b"options", + false, + ); + assert!(native_fee == 0, 0); + assert!(zro_fee == 0, 1); + } + + #[test] + fun test_set_enforced_options() { + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + oapp_core::set_enforced_options(admin, SRC_EID, SEND(), x"0003aaaa"); + oapp_core::set_enforced_options(admin, DST_EID, SEND(), x"0003bbbc"); + oapp_core::set_enforced_options(admin, DST_EID, SEND(), x"000355"); + oapp_core::set_enforced_options(admin, DST_EID, SEND_AND_CALL(), x"000344"); + assert!(oapp_core::get_enforced_options(DST_EID, SEND()) == x"000355", 0); + assert!(oapp_core::get_enforced_options(DST_EID, SEND_AND_CALL()) == x"000344", 0); + assert!(oapp_core::get_enforced_options(SRC_EID, SEND()) == x"0003aaaa", 0); + } + + #[test] + fun test_combine_options() { + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + let enforced_options = x"0003aaaa"; + let options = x"0003bbbb"; + oapp_core::set_enforced_options(admin, DST_EID, SEND(), enforced_options); + // unrelated option below just to make sure it doesn't get overwritten + oapp_core::set_enforced_options(admin, DST_EID, SEND_AND_CALL(), x"0003235326"); + let combined = oapp_core::combine_options(DST_EID, SEND(), options); + assert!(combined == x"0003aaaabbbb", 0); + } + + #[test] + fun test_peers() { + // Test the send function + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + assert!(!oapp_core::has_peer(1111), 0); + oapp_core::set_peer(admin, 1111, from_bytes32(from_address(@1234))); + oapp_core::set_peer(admin, 2222, from_bytes32(from_address(@2345))); + assert!(oapp_core::has_peer(1111), 0); + assert!(oapp_core::has_peer(2222), 0); + assert!(oapp_core::get_peer_bytes32(1111) == from_address(@1234), 0); + assert!(oapp_core::get_peer_bytes32(2222) == from_address(@2345), 0); + } + + #[test] + fun test_delegate() { + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + let delegate = &create_signer_for_test(@2222); + oapp_core::set_delegate(admin, address_of(delegate)); + assert!(oapp_core::get_delegate() == address_of(delegate), 0); + + let delegate2 = &create_signer_for_test(@3333); + oapp_core::set_delegate(admin, address_of(delegate2)); + + oapp_core::skip(delegate2, SRC_EID, from_bytes32(from_address(@1234)), 1); + } + + #[test] + fun test_withdraw_lz_fees() { + let native_token = mint_native_token_for_test(1000); + + let (zro_address, zro_metadata, mint_ref) = create_fa(b"ZRO"); + universal_config::init_module_for_test(100); + universal_config::set_zro_address(&create_signer_for_test(@layerzero_admin), zro_address); + + let zro_token = fungible_asset::mint(&mint_ref, 1000); + + primary_fungible_store::deposit(@0x1234, native_token); + primary_fungible_store::deposit(@0x1234, zro_token); + + let account = &create_signer_for_test(@0x1234); + + let (native_token, zro_token) = withdraw_lz_fees(account, 600, 550); + assert!(fungible_asset::amount(&native_token) == 600, 0); + assert!(fungible_asset::amount(option::borrow(&zro_token)) == 550, 0); + burn_token_for_test(native_token); + burn_token_for_test(option::extract(&mut zro_token)); + option::destroy_none(zro_token); + + assert!(native_token::balance(@0x1234) == 400, 0); + assert!(primary_fungible_store::balance(@0x1234, zro_metadata) == 450, 0); + } + + #[test] + fun test_withdraw_lz_fees_no_zro() { + let native_token = mint_native_token_for_test(1000); + primary_fungible_store::deposit(@0x1234, native_token); + + let account = &create_signer_for_test(@0x1234); + + let (native_token, zro_token) = withdraw_lz_fees(account, 600, 0); + assert!(fungible_asset::amount(&native_token) == 600, 0); + assert!(option::is_none(&zro_token), 0); + burn_token_for_test(native_token); + option::destroy_none(zro_token); + + assert!(native_token::balance(@0x1234) == 400, 0); + } + + #[test] + #[expected_failure(abort_code = oft::oapp_core::EINSUFFICIENT_NATIVE_TOKEN_BALANCE)] + fun test_withdraw_lz_fees_fails_with_insufficient_native_balance() { + let native_token = mint_native_token_for_test(100); + + let (zro_address, _, mint_ref) = create_fa(b"ZRO"); + universal_config::init_module_for_test(100); + universal_config::set_zro_address(&create_signer_for_test(@layerzero_admin), zro_address); + + let zro_token = fungible_asset::mint(&mint_ref, 1000); + + primary_fungible_store::deposit(@0x1234, native_token); + primary_fungible_store::deposit(@0x1234, zro_token); + + let account = &create_signer_for_test(@0x1234); + + let (native_token, zro_token) = withdraw_lz_fees(account, 600, 550); + burn_token_for_test(native_token); + burn_token_for_test(option::extract(&mut zro_token)); + option::destroy_none(zro_token); + } + + #[test] + #[expected_failure(abort_code = oft::oapp_core::EINSUFFICIENT_ZRO_BALANCE)] + fun test_withdraw_lz_fees_fails_with_insufficient_zro_balance() { + let native_token = mint_native_token_for_test(1000); + + let (zro_address, _, mint_ref) = create_fa(b"ZRO"); + universal_config::init_module_for_test(100); + universal_config::set_zro_address(&create_signer_for_test(@layerzero_admin), zro_address); + + let zro_token = fungible_asset::mint(&mint_ref, 100); + + primary_fungible_store::deposit(@0x1234, native_token); + primary_fungible_store::deposit(@0x1234, zro_token); + + let account = &create_signer_for_test(@0x1234); + + let (native_token, zro_token) = withdraw_lz_fees(account, 600, 550); + burn_token_for_test(native_token); + burn_token_for_test(option::extract(&mut zro_token)); + option::destroy_none(zro_token); + } +} diff --git a/examples/oft-adapter-aptos-move/tests/shared_oapp/oapp_test_helper.move b/examples/oft-adapter-aptos-move/tests/shared_oapp/oapp_test_helper.move new file mode 100644 index 000000000..9f0a827dc --- /dev/null +++ b/examples/oft-adapter-aptos-move/tests/shared_oapp/oapp_test_helper.move @@ -0,0 +1,12 @@ +#[test_only] +module oft::oapp_test_helper { + use oft::oapp_compose; + use oft::oapp_receive; + use oft::oapp_store; + + public fun init_oapp() { + oapp_store::init_module_for_test(); + oapp_receive::init_module_for_test(); + oapp_compose::init_module_for_test(); + } +} diff --git a/examples/oft-adapter-aptos-move/tests/shared_oft/oft_core_tests.move b/examples/oft-adapter-aptos-move/tests/shared_oft/oft_core_tests.move new file mode 100644 index 000000000..fc7f075a7 --- /dev/null +++ b/examples/oft-adapter-aptos-move/tests/shared_oft/oft_core_tests.move @@ -0,0 +1,310 @@ +#[test_only] +module oft::oft_core_tests { + use std::account::create_signer_for_test; + use std::event::was_event_emitted; + use std::fungible_asset; + use std::fungible_asset::FungibleAsset; + use std::option; + use std::string::utf8; + use std::vector; + + use endpoint_v2::endpoint; + use endpoint_v2::messaging_receipt; + use endpoint_v2::messaging_receipt::new_messaging_receipt_for_test; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32; + use endpoint_v2_common::bytes32::{from_address, from_bytes32}; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, initialize_native_token_for_test, + mint_native_token_for_test + }; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::packet_v1_codec::compute_payload_hash; + use endpoint_v2_common::zro_test_helpers::create_fa; + use oft::oapp_core; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core::{Self, SEND, SEND_AND_CALL}; + use oft::oft_store; + use oft_common::oft_compose_msg_codec; + use oft_common::oft_msg_codec; + + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32) { + // Test the send function + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + let oft_admin = &create_signer_for_test(@oft_admin); + initialize_native_token_for_test(); + let (_, metadata, _) = create_fa(b"ZRO"); + let local_decimals = fungible_asset::decimals(metadata); + oft::oapp_test_helper::init_oapp(); + + oft_store::init_module_for_test(); + oft_core::initialize(local_decimals, 6); + oapp_core::set_peer(oft_admin, SRC_EID, from_bytes32(from_address(@1234))); + oapp_core::set_peer(oft_admin, DST_EID, from_bytes32(from_address(@4321))); + } + + #[test] + fun test_send() { + setup(SRC_EID, DST_EID); + + let user_sender = @99; + let to = bytes32::from_address(@2001); + let native_fee = mint_native_token_for_test(100000); + let zro_fee = option::none(); + let compose_message = b"Hello"; + + let called_send = false; + let called_inspect = false; + assert!(!called_inspect && !called_send, 0); + + let (messaging_receipt, amount_sent_ld, amount_received_ld) = oft_core::send( + user_sender, + DST_EID, + to, + compose_message, + |message, options| { + called_send = true; + assert!(oft_msg_codec::has_compose(&message), 0); + assert!(oft_msg_codec::sender(&message) == from_address(user_sender), 1); + assert!(oft_msg_codec::send_to(&message) == to, 0); + assert!(oft_msg_codec::amount_sd(&message) == 40, 1); + assert!(options == b"options", 2); + + new_messaging_receipt_for_test( + from_address(@333), + 4, + 1111, + 2222, + ) + }, + |_unused| (5000, 4000), + |_amount_received_ld, _msg_type| b"options", + |message, options| { + called_inspect = true; + assert!(vector::length(message) > 0, 0); + assert!(*options == b"options", 0); + }, + ); + + assert!(called_send, 0); + assert!(called_inspect, 0); + + let (guid, nonce, native_fee_amount, zro_fee_amount) = messaging_receipt::unpack_messaging_receipt( + messaging_receipt, + ); + assert!(guid == from_address(@333), 0); + assert!(nonce == 4, 0); + assert!(native_fee_amount == 1111, 1); + assert!(zro_fee_amount == 2222, 2); + + assert!(amount_sent_ld == 5000, 3); + assert!(amount_received_ld == 4000, 4); + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + } + + #[test] + fun test_receive() { + setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let called_credit = false; + assert!(!called_credit, 1); + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"", + ); + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + b"" + ); + + oft_core::receive( + SRC_EID, + nonce, + guid, + message, + |_to, _index, _message| { + // should not be called + assert!(false, 0); + }, + |to_address, message_amount| { + called_credit = true; + + assert!(to_address == @0x2000, 0); + // Add 2 0s for (8 local decimals - 6 shared decimals) + assert!(message_amount == 12300, 1); + + 5000 + }, + ); + + assert!(called_credit, 2); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 5000, + )), 3); + } + + #[test] + fun test_receive_with_compose() { + setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let called_credit = 0; + assert!(called_credit == 0, 1); + + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"Hello", + ); + + // Composer must be registered + let to_address_account = &create_signer_for_test(@0x2000); + endpoint::register_composer(to_address_account, utf8(b"oft")); + + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + b"" + ); + + let called_compose = 0; + assert!(called_compose == 0, 0); + + oft_core::receive( + SRC_EID, + nonce, + guid, + message, + |to, index, message| { + called_compose = called_compose + 1; + assert!(to == @0x2000, 0); + assert!(index == 0, 1); + assert!(oft_compose_msg_codec::compose_payload_message(&message) == b"Hello", 2); + }, + |to_address, message_amount| { + called_credit = called_credit + 1; + + assert!(to_address == @0x2000, 0); + // Add 2 0s for (8 local decimals - 6 shared decimals) + assert!(message_amount == 12300, 1); + 12300 // message_amount + }, + ); + + assert!(called_compose == 1, 0); + assert!(called_credit == 1, 2); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 12300, + )), 3); + } + + + #[test] + fun test_no_fee_debit_view() { + setup(SRC_EID, DST_EID); + + let (sent, received) = oft_core::no_fee_debit_view(123456789, 200); + assert!(sent == received, 0); + // dust removed (last 2 digits cleared) + assert!(sent == 123456700, 1); + } + + #[test] + #[expected_failure(abort_code = oft::oft_core::ESLIPPAGE_EXCEEDED)] + fun test_no_fee_debit_view_fails_if_post_dust_remove_less_than_min() { + setup(SRC_EID, DST_EID); + + oft_core::no_fee_debit_view(99, 20); + } + + #[test] + fun test_encode_oft_msg() { + setup(SRC_EID, DST_EID); + + let (encoded, message_type) = oft_core::encode_oft_msg( + @0x12345678, + 123, + bytes32::from_address(@0x2000), + b"Hello", + ); + + assert!(message_type == SEND_AND_CALL(), 0); + + let expected_encoded = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + // dust removed and SD + 1, + bytes32::from_address(@0x12345678), + b"Hello", + ); + + assert!(encoded == expected_encoded, 1); + + // without compose + let (encoded, message_type) = oft_core::encode_oft_msg( + @0x12345678, + 123, + bytes32::from_address(@0x2000), + b"", + ); + + assert!(message_type == SEND(), 2); + + let expected_encoded = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + // dust removed and SD + 1, + bytes32::from_address(@0x12345678), + b"", + ); + + assert!(encoded == expected_encoded, 1); + } +} \ No newline at end of file diff --git a/examples/oft-adapter-aptos-move/tests/shared_oft/oft_impl_config_tests.move b/examples/oft-adapter-aptos-move/tests/shared_oft/oft_impl_config_tests.move new file mode 100644 index 000000000..888e67dca --- /dev/null +++ b/examples/oft-adapter-aptos-move/tests/shared_oft/oft_impl_config_tests.move @@ -0,0 +1,318 @@ +#[test_only] +module oft::oft_impl_config_tests { + use std::account::create_account_for_test; + use std::event::was_event_emitted; + use std::string::utf8; + use std::vector; + + use oft::oapp_store; + use oft::oft_core; + use oft::oft_impl_config::{ + Self, + assert_not_blocklisted, + blocked_amount_redirected_event, + blocklisting_disabled_event, + debit_view_with_possible_fee, + fee_bps, + fee_details_with_possible_fee, + has_rate_limit, + in_flight_at_time, + irrevocably_disable_blocklist, + is_blocklisted, + rate_limit_capacity_at_time, + rate_limit_config, + rate_limit_set_event, + rate_limit_updated_event, redirect_to_admin_if_blocklisted, release_rate_limit_capacity, set_blocklist, + set_fee_bps, try_consume_rate_limit_capacity_at_time, unset_rate_limit, + }; + use oft::oft_store; + use oft_common::oft_fee_detail; + + const MAX_U64: u64 = 0xffffffffffffffff; + + fun setup() { + oapp_store::init_module_for_test(); + oft_store::init_module_for_test(); + oft_impl_config::init_module_for_test(); + oft_core::initialize(8, 6); + } + + #[test] + fun test_set_fee_deposit_address() { + setup(); + + let deposit_address = @0x1234; + create_account_for_test(deposit_address); + + assert!(oft_impl_config::fee_deposit_address() == @oft_admin, 1); + oft_impl_config::set_fee_deposit_address(deposit_address); + assert!(oft_impl_config::fee_deposit_address() == deposit_address, 2); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EINVALID_DEPOSIT_ADDRESS)] + fun test_set_fee_deposit_address_invalid() { + setup(); + + let deposit_address = @0x1234; + oft_impl_config::set_fee_deposit_address(deposit_address); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::ESETTING_UNCHANGED)] + fun test_set_fee_deposit_address_unchanged() { + setup(); + + let deposit_address = @0x1234; + create_account_for_test(deposit_address); + + oft_impl_config::set_fee_deposit_address(deposit_address); + oft_impl_config::set_fee_deposit_address(deposit_address); + } + + #[test] + fun test_set_fee_bps() { + setup(); + + // Set the fee to 10% + let fee_bps = 1000; + set_fee_bps(fee_bps); + assert!(fee_bps() == fee_bps, 1); + + let (sent, received) = debit_view_with_possible_fee(1234, 1000); + // Send amount should include dust if there is a fee + assert!(sent == 1234, 1); + // Received amount should be 90% of the sent amount with dust removed: 1234 - 120 = 1114 => 1100 (dust removed) + assert!(received == 1100, 2); + + let fee_details = fee_details_with_possible_fee(1234, 1000); + assert!(vector::length(&fee_details) == 1, 1); + let (fee, is_reward) = oft_fee_detail::fee_amount_ld(vector::borrow(&fee_details, 0)); + assert!(fee == 134, 1); + assert!(is_reward == false, 2); + assert!(oft_fee_detail::description(vector::borrow(&fee_details, 0)) == utf8(b"OFT Fee"), 2); + + // Set the fee to 0% + let fee_bps = 0; + set_fee_bps(fee_bps); + + let (sent, received) = debit_view_with_possible_fee(1234, 1000); + // Sent and amount should be 100% of the amount with dust removed with no fee: 1234 => 1200 (dust removed) + assert!(sent == 1200, 3); + assert!(received == 1200, 4); + + // Expect no fee details if there is no fee + let fee_details = fee_details_with_possible_fee(1234, 1000); + assert!(vector::length(&fee_details) == 0, 1); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::ESETTING_UNCHANGED)] + fun test_set_fee_bps_fails_if_unchanged() { + setup(); + + // Set the fee to 10% + let fee_bps = 1000; + set_fee_bps(fee_bps); + set_fee_bps(fee_bps); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EINVALID_FEE)] + fun test_set_fee_bps_invalid() { + setup(); + + // Set the fee to 101% + let fee_bps = 10100; + set_fee_bps(fee_bps); + } + + #[test] + fun test_set_blocklist() { + setup(); + + assert!(oft_impl_config::is_blocklisted(@0x1234) == false, 1); + assert_not_blocklisted(@0x1234); + let redirected_address = redirect_to_admin_if_blocklisted(@0x1234, 1111); + assert!(redirected_address == @0x1234, 2); + + set_blocklist(@0x1234, true); + assert!(oft_impl_config::is_blocklisted(@0x1234), 1); + let redirected_address = redirect_to_admin_if_blocklisted(@0x1234, 1111); + assert!(redirected_address == @oft_admin, 2); + assert!(was_event_emitted(&blocked_amount_redirected_event(1111, @0x1234, @oft_admin)), 3); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::ESETTING_UNCHANGED)] + fun test_set_blocklist_fails_if_unchanged() { + setup(); + + set_blocklist(@0x1234, true); + set_blocklist(@0x1234, true); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::ESETTING_UNCHANGED)] + fun test_set_blocklist_fails_if_unchanged_2() { + setup(); + + set_blocklist(@0x1234, false); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EBLOCKLIST_DISABLED)] + fun test_cant_set_blocklist_if_disabled() { + setup(); + + assert!(oft_impl_config::is_blocklisted(@0x1234) == false, 1); + assert_not_blocklisted(@0x1234); + let redirected_address = redirect_to_admin_if_blocklisted(@0x1234, 1111); + assert!(redirected_address == @0x1234, 2); + + irrevocably_disable_blocklist(); + assert!(was_event_emitted(&blocklisting_disabled_event()), 1); + + assert!(is_blocklisted(@0x1234) == false, 1); + + // Should not be able to set blocklist + set_blocklist(@0x3333, true); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EADDRESS_BLOCKED)] + fun test_assert_not_blocked() { + setup(); + + set_blocklist(@0x1234, true); + assert_not_blocklisted(@0x1234); + } + + #[test] + fun set_rate_limit() { + setup(); + + // No rate limit configured + assert!(has_rate_limit(30100) == false, 2); + let (limit, window) = rate_limit_config(30100); + assert!(limit == 0 && window == 0, 1); + assert!(in_flight_at_time(30100, 10) == 0, 1); + assert!(rate_limit_capacity_at_time(30100, 10) == MAX_U64, 2); + + // Configure rate limit (200/second) + oft_impl_config::set_rate_limit_at_timestamp(30100, 20000, 1000, 100); + assert!(was_event_emitted(&rate_limit_set_event(30100, 20000, 1000)), 1); + assert!(has_rate_limit(30100) == true, 2); + assert!(has_rate_limit(30200) == false, 2); // Different eid + let (limit, window) = rate_limit_config(30100); + assert!(limit == 20000 && window == 1000, 1); + assert!(in_flight_at_time(30100, 100) == 0, 1); + assert!(rate_limit_capacity_at_time(30100, 100) == 20000, 2); + + // 100 seconds later + assert!(in_flight_at_time(30100, 200) == 0, 1); + assert!(rate_limit_capacity_at_time(30100, 200) == 20000, 2); + + // consume 10% of the capacity + try_consume_rate_limit_capacity_at_time(30100, 2000, 200); + assert!(in_flight_at_time(30100, 200) == 2000, 1); + assert!(rate_limit_capacity_at_time(30100, 200) == 18000, 2); + + // 10 seconds later: in flight should decline by 20000/1000s * 10s = 200 + assert!(in_flight_at_time(30100, 210) == 1800, 1); + assert!(rate_limit_capacity_at_time(30100, 210) == 18200, 2); + + // 20 seconds later: in flight should decline by 20000/1000s * 20s = 400 + assert!(in_flight_at_time(30100, 220) == 1600, 1); + assert!(rate_limit_capacity_at_time(30100, 220) == 18400, 2); + + // update rate limit (300/second) + oft_impl_config::set_rate_limit_at_timestamp(30100, 30000, 1000, 220); + assert!(was_event_emitted(&rate_limit_updated_event(30100, 30000, 1000)), 1); + // in flight shouldn't change, but capacity should be updated with the new limit in mind + assert!(in_flight_at_time(30100, 220) == 1600, 1); + assert!(rate_limit_capacity_at_time(30100, 220) == 28400, 2); + + // 10 seconds later: in flight should decline by 30000/1000s * 10s = 300 + assert!(in_flight_at_time(30100, 230) == 1300, 1); + assert!(rate_limit_capacity_at_time(30100, 230) == 28700, 2); + + // 10 seconds later: in flight should decline by 30000/1000s * 10s = 300 + assert!(in_flight_at_time(30100, 240) == 1000, 1); + assert!(rate_limit_capacity_at_time(30100, 240) == 29000, 2); + + // 100 seconds later: in flight should decline fully (without overshooting): 30000/1000s * 100s = 3000 + assert!(in_flight_at_time(30100, 300) == 0, 1); + assert!(rate_limit_capacity_at_time(30100, 300) == 30000, 2); + + // Consume again + try_consume_rate_limit_capacity_at_time(30100, 2000, 300); + try_consume_rate_limit_capacity_at_time(30100, 2000, 310); + // 2000 + (2000 - 30000/1000*10) = 3800 + assert!(in_flight_at_time(30100, 310) == 3700, 1); + + // Reduce rate limit to below the in flight + oft_impl_config::set_rate_limit_at_timestamp(30100, 500, 1000, 320); + + // Rate limit capacity cannot go below 0, and should not abort + assert!(rate_limit_capacity_at_time(30100, 320) == 0, 2); + + // Unset rate limit + unset_rate_limit(30100); + assert!(has_rate_limit(30100) == false, 2); + let (limit, window) = rate_limit_config(30100); + assert!(limit == 0 && window == 0, 1); + assert!(in_flight_at_time(30100, 320) == 0, 1); + assert!(rate_limit_capacity_at_time(30100, 320) == MAX_U64, 2); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::ESETTING_UNCHANGED)] + fun set_rate_limit_fails_if_unchanged() { + setup(); + + // Configure rate limit (200/second) + oft_impl_config::set_rate_limit_at_timestamp(30100, 20000, 1000, 100); + oft_impl_config::set_rate_limit_at_timestamp(30100, 20000, 1000, 200); + } + + #[test] + fun test_set_rate_limit_net() { + setup(); + + // No rate limit configured + assert!(has_rate_limit(30100) == false, 2); + let (limit, window) = rate_limit_config(30100); + assert!(limit == 0 && window == 0, 1); + assert!(in_flight_at_time(30100, 10) == 0, 1); + assert!(rate_limit_capacity_at_time(30100, 10) == MAX_U64, 2); + + // Configure rate limit (200/second) + oft_impl_config::set_rate_limit_at_timestamp(30100, 20000, 1000, 100); + assert!(has_rate_limit(30100) == true, 2); + + // consume 100% of the capacity + try_consume_rate_limit_capacity_at_time(30100, 20000, 0); + assert!(in_flight_at_time(30100, 0) == 20000, 1); + assert!(rate_limit_capacity_at_time(30100, 0) == 0, 2); + + // 50 seconds later: in flight should decline by 20000/1000s * 500s = 10000 + assert!(in_flight_at_time(30100, 500) == 10000, 1); + assert!(rate_limit_capacity_at_time(30100, 500) == 10000, 2); + + // release most of remaining capacity + release_rate_limit_capacity(30100, 9000); + assert!(in_flight_at_time(30100, 500) == 1000, 1); + assert!(rate_limit_capacity_at_time(30100, 500) == 19000, 2); + + // consume all of remaining capacity + try_consume_rate_limit_capacity_at_time(30100, 19000, 500); + assert!(in_flight_at_time(30100, 500) == 20000, 1); + assert!(rate_limit_capacity_at_time(30100, 500) == 0, 2); + + // release excess capacity (5x limit) - should not overshoot + release_rate_limit_capacity(30100, 100_000); + assert!(in_flight_at_time(30100, 500) == 0, 1); + assert!(rate_limit_capacity_at_time(30100, 500) == 20000, 2); + } +} diff --git a/examples/oft-adapter-aptos-move/tsconfig.json b/examples/oft-adapter-aptos-move/tsconfig.json new file mode 100644 index 000000000..9ba95395c --- /dev/null +++ b/examples/oft-adapter-aptos-move/tsconfig.json @@ -0,0 +1,19 @@ +{ + "exclude": ["node_modules"], + "include": [ + "deploy", + "tasks", + "test", + "hardhat.config.ts", + "deploy-move/MyMoveOFTFA.ts" + ], + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +} diff --git a/examples/oft-adapter-aptos-move/turbo.json b/examples/oft-adapter-aptos-move/turbo.json new file mode 100644 index 000000000..adaaffa58 --- /dev/null +++ b/examples/oft-adapter-aptos-move/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "pipeline": { + "build": { + "outputs": ["build*/**"] + } + } +} diff --git a/examples/oft-aptos-move/.env.example b/examples/oft-aptos-move/.env.example new file mode 100644 index 000000000..3e8b55588 --- /dev/null +++ b/examples/oft-aptos-move/.env.example @@ -0,0 +1,18 @@ +# .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- +# / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ +# `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' +# +# 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 +EVM_MNEMONIC= +EVM_PRIVATE_KEY= + +# Aptos config - this should be the same as the value in your .aptos/config.yaml file +APTOS_ACCOUNT_ADDRESS= \ No newline at end of file diff --git a/examples/oft-aptos-move/.eslintignore b/examples/oft-aptos-move/.eslintignore new file mode 100644 index 000000000..ee9f768fd --- /dev/null +++ b/examples/oft-aptos-move/.eslintignore @@ -0,0 +1,10 @@ +artifacts +cache +dist +node_modules +out +*.log +*.sol +*.yaml +*.lock +package-lock.json \ No newline at end of file diff --git a/examples/oft-aptos-move/.eslintrc.js b/examples/oft-aptos-move/.eslintrc.js new file mode 100644 index 000000000..876745330 --- /dev/null +++ b/examples/oft-aptos-move/.eslintrc.js @@ -0,0 +1,11 @@ +require('@rushstack/eslint-patch/modern-module-resolution'); + +module.exports = { + root: true, // Add this line to prevent ESLint from looking up the directory tree + 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', + }, +}; diff --git a/examples/oft-aptos-move/.gitignore b/examples/oft-aptos-move/.gitignore new file mode 100644 index 000000000..738da4dab --- /dev/null +++ b/examples/oft-aptos-move/.gitignore @@ -0,0 +1,33 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types +build/ + +# Hardhat files +cache +artifacts + + +# LayerZero specific files +.layerzero +transactions + +# foundry test compilation files +out + +# pnpm +pnpm-error.log + +# Editor and OS files +.DS_Store +.idea + +.aptos + +shell-scripts + +transactions/* +!transactions/.gitkeep diff --git a/examples/oft-aptos-move/.nvmrc b/examples/oft-aptos-move/.nvmrc new file mode 100644 index 000000000..b714151ef --- /dev/null +++ b/examples/oft-aptos-move/.nvmrc @@ -0,0 +1 @@ +v18.18.0 \ No newline at end of file diff --git a/examples/oft-aptos-move/.prettierignore b/examples/oft-aptos-move/.prettierignore new file mode 100644 index 000000000..6e8232f5a --- /dev/null +++ b/examples/oft-aptos-move/.prettierignore @@ -0,0 +1,10 @@ +artifacts/ +cache/ +dist/ +node_modules/ +out/ +*.log +*ignore +*.yaml +*.lock +package-lock.json \ No newline at end of file diff --git a/examples/oft-aptos-move/.prettierrc.js b/examples/oft-aptos-move/.prettierrc.js new file mode 100644 index 000000000..6f55b4019 --- /dev/null +++ b/examples/oft-aptos-move/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('@layerzerolabs/prettier-config-next'), +}; diff --git a/examples/oft-aptos-move/Move.toml b/examples/oft-aptos-move/Move.toml new file mode 100644 index 000000000..cacddf99c --- /dev/null +++ b/examples/oft-aptos-move/Move.toml @@ -0,0 +1,75 @@ +[package] +name = "oft" +version = "1.0.0" +authors = [] + +[addresses] +oft = "_" +oft_admin = "_" +oft_common = "_" +router_node_0 = "_" +simple_msglib = "_" +blocked_msglib = "_" +uln_302 = "_" +router_node_1 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +msglib_types = "_" +treasury = "_" +worker_peripherals = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +worker_common = "_" +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +executor_fee_lib_0 = "_" +dvn_fee_lib_0 = "_" +dvn = "_" +native_token_metadata_address = "0xa" +# For Initia: "0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9" + +[dev-addresses] +oft = "0x302814823" +oft_admin = "0x12321241" +oft_common = "0x30281482332" +router_node_0 = "0x10000f" +simple_msglib = "0x100011" +blocked_msglib = "0x100001" +uln_302 = "0x100013" +router_node_1 = "0x100010" +endpoint_v2_common = "0x100007" +endpoint_v2 = "0x100006" +layerzero_admin = "0x200001" +layerzero_treasury_admin = "0x200002" +msglib_types = "0x10000b" +treasury = "0x100012" +worker_peripherals = "0x3000" +price_feed_router_0 = "0x10000d" +price_feed_router_1 = "0x10000e" +price_feed_module_0 = "0x10000c" +worker_common = "0x100014" +executor_fee_lib_router_0 = "0x100009" +executor_fee_lib_router_1 = "0x10000a" +dvn_fee_lib_router_0 = "0x100004" +dvn_fee_lib_router_1 = "0x100005" +executor_fee_lib_0 = "0x100008" +dvn_fee_lib_0 = "0x100003" +dvn = "0x100002" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" + +[dependencies] +endpoint_v2_common = { git = "https://github.com/LayerZero-Labs/LayerZero-v2", rev = "aptos-v2-jan-6", subdir = "packages/layerzero-v2/aptos/contracts/endpoint_v2_common" } +endpoint_v2 = { git = "https://github.com/LayerZero-Labs/LayerZero-v2", rev = "aptos-v2-jan-6", subdir = "packages/layerzero-v2/aptos/contracts/endpoint_v2" } +oft_common = { git = "https://github.com/LayerZero-Labs/LayerZero-v2", rev = "aptos-v2-jan-6", subdir = "packages/layerzero-v2/aptos/contracts/oapps/oft_common" } + +[dev-dependencies] +simple_msglib = { git = "https://github.com/LayerZero-Labs/LayerZero-v2", rev = "aptos-v2-jan-6", subdir = "packages/layerzero-v2/aptos/contracts/msglib/libs/simple_msglib" } \ No newline at end of file diff --git a/examples/oft-aptos-move/README.md b/examples/oft-aptos-move/README.md new file mode 100644 index 000000000..87ba46a2c --- /dev/null +++ b/examples/oft-aptos-move/README.md @@ -0,0 +1,330 @@ +## Move-VM OFT Setup and Deployment + +### connecting to aptos via cli + +To install aptos cli, run the following command: + +``` +brew install aptos +``` + +If you need to generate a new key, run the following command: + +``` +aptos key generate --output-file my_key.pub +``` + +Then initialize the aptos cli and connect to the aptos network: + +``` +aptos init --network=testnet --private-key= +``` + +You can then verify that your initialization was successful by running the following command: + +``` +cat .aptos/config.yaml +``` + +If successful the config will be populated with the RPC links and your account private key, account address, and network. + +Note: Your private key is stored in the .aptos/config.yaml file and will be extracted from there. + +## Setup + +Create a `.env` file with the following variables: + +```bash +APTOS_ACCOUNT_ADDRESS= +EVM_PRIVATE_KEY= +``` + +Then run `source .env` in order for your values to be mapped to `$APTOS_ACCOUNT_ADDRESS` and `$EVM_PRIVATE_KEY` + +Note: aptos account address can be found in .aptos/config.yaml + +## Build and deploy + +Note: to overwrite previous deploy and build, you can use `--force-build true` for the build script and `--force-deploy true` for the deploy script. + +### Builds the contracts + +```bash +pnpm run lz:sdk:move:build --oapp-config move.layerzero.config.ts --named-addresses oft=$APTOS_ACCOUNT_ADDRESS,oft_admin=$APTOS_ACCOUNT_ADDRESS +``` + +### Checks for build, builds if not, then deploys the contracts, sets the delegate and initializes + +First modify deploy-move/OFTInitParams.ts and replace the oftMetadata with your desired values: + +```ts +const oftMetadata = { + token_name: "MyMoveOFT", + token_symbol: "MMOFT", + icon_uri: "", + project_uri: "", + sharedDecimals: 6, + localDecimals: 6, +}; +``` + +```bash +pnpm run lz:sdk:move:deploy --oapp-config move.layerzero.config.ts --address-name oft --named-addresses oft=$APTOS_ACCOUNT_ADDRESS,oft_admin=$APTOS_ACCOUNT_ADDRESS --move-deploy-script deploy-move/OFTInitParams.ts +``` + +## EVM Deployment + +```bash +npx hardhat lz:deploy +``` + +Select only the evm networks (DO NOT SELECT APTOS or MOVEMENT) + +## Init and Set Delegate + +Before running the wire command, first inside of move.layerzero.config.ts, set the delegate address to your account address. + +```ts + contracts: [ + { + contract: bscContract, + config: { + owner: 'YOUR_EVM_ACCOUNT_ADDRESS', + delegate: 'YOUR_EVM_ACCOUNT_ADDRESS', + }, + }, + { + contract: aptosContract, + config: { + delegate: 'YOUR_APTOS_ACCOUNT_ADDRESS', + owner: 'YOUR_APTOS_ACCOUNT_ADDRESS', + }, + }, + ], +``` + +Then run the following commands: + +```bash +pnpm run lz:sdk:move:init-fa --oapp-config move.layerzero.config.ts --move-deploy-script deploy-move/OFTInitParams.ts +``` + +```bash +pnpm run lz:sdk:move:set-delegate --oapp-config move.layerzero.config.ts +``` + +## Wire + +For EVM: +Ensure that in move.layerzero.config.ts, all of your evm contracts have the owner and delegate contract is specified. + +```ts + contracts: [ + { + contract: your_contract_name, + config: { + owner: 'YOUR_EVM_ACCOUNT_ADDRESS', + delegate: 'YOUR_EVM_ACCOUNT_ADDRESS', + }, + }, + ... + ] +``` + +Then run the wire command: +If you are wiring solana to move-vm, create a file in deployments/solana-mainnet/MyOFT.json (solana-testnet if you are using testnet) and add the following field: + +```json +{ + "address": +} +``` + +Commands: + +```bash +pnpm run lz:sdk:evm:wire --oapp-config move.layerzero.config.ts [--simulate true] [--mnemonic-index 0] +``` + +--simulate and --mnemonic-index are optional. +--mnemonic-index is the index of the mnemonic to use for the EVM account. If not specified, EVM_PRIVATE_KEY from .env is used. else the mnemonic is used along with the index. + +For Move-VM: + +```bash +pnpm run lz:sdk:move:wire --oapp-config move.layerzero.config.ts +``` + +## Set Fee + +```bash +pnpm run lz:sdk:move:set-fee --oapp-config move.layerzero.config.ts --fee-bps 1000 --to-eid number +``` + +## Set Rate Limit + +```bash +pnpm run lz:sdk:move:set-rate-limit --oapp-config move.layerzero.config.ts --rate-limit 10000 --window-seconds 60 --to-eid number +``` + +Rate limit limits how much is sent netted by the amount that is received. It is set on a per pathway basis. +For example if the rate limit from Aptos to EVM is 100 tokens you can send 100 tokens from Aptos to EVM, however if you receive 50 tokens from EVM to Aptos you are then able to send 150 tokens from Aptos to EVM. +Window is the number of seconds over which the capacity is restored. If the rate limit is 1000 and window is 10 seconds, then each second you get 100 (1000/10) capacity back. The units of the rate limit are the tokens in local decimals. + +## Unset Rate Limit + +```bash +pnpm run lz:sdk:move:unset-rate-limit --oapp-config move.layerzero.config.ts --to-eid number +``` + +## Permanently Disable Blocklist + +> ⚠️ **Warning**: This will permanently disable the blocklist for the OFT. It is for OFTs that want to demonstrate to their holders that they will never use blocklisting abilities. + +```bash +pnpm run lz:sdk:move:permanently-disable-blocklist +``` + +## Permanently Disable Freezing + +> ⚠️ **Warning**: This will permanently disable the freezing for the OFT. It is for OFTs that want to demonstrate to their holders that they will never use the freezing ability. + +```bash +pnpm run lz:sdk:move:permanently-disable-freezing +``` + +### Transferring Ownership of your Move OApp (OFT) + +There are three steps to transferring ownership of your Move OFT: + +1. Transfer the delegate to the new delegate +2. Transfer the OApp owner of the your to the new owner +3. Transfer the Move-VM object owner to the new owner + +To set the delegate, run the following command: +First ensure that the delegate is specified in the move.layerzero.config.ts file. + +```ts + contracts: [ + { + contract: your_contract_name, + config: { + delegate: 'YOUR_DESIRED_DELEGATE_ACCOUNT_ADDRESS', + }, + }, + ... + ] +``` + +Then run the following command: + +```bash +pnpm run lz:sdk:move:set-delegate --oapp-config move.layerzero.config.ts +``` + +To transfer the OApp owner, run the following command: + +```bash +pnpm run lz:sdk:move:transfer-oapp-owner --new-owner +``` + +To transfer the Move-VM object owner, run the following command: + +```bash +pnpm run lz:sdk:move:transfer-object-owner --new-owner +``` + +Note: The object owner has the upgrade authority for the Object. + +### Mint to Account on Move VM OFT: + +> ⚠️ **Warning**: This mint command is only for testing and experimentation purposes. Do not use in production. +> First add this function to oft/sources/internal_oft/oft_impl.move in order to expose minting functionality to our move sdk script: + +``` +public entry fun mint( + admin: &signer, + recipient: address, + amount: u64, +) acquires OftImpl { + assert_admin(address_of(admin)); + primary_fungible_store::mint(&store().mint_ref, recipient, amount); +} +``` + +Then run the following command to mint the move oft: + +```bash +pnpm run lz:sdk:move:mint-to-move-oft --amount-ld 1000000000000000000 --to-address +``` + +## Send from Move VM + +```bash +pnpm run lz:sdk:move:send-from-move-oft \ + --amount-ld \ + --min-amount-ld \ + --src-address \ + --to-address \ + --gas-limit \ + --dst-eid \ +``` + +## Send from EVM + +```bash +pnpm run lz:sdk:evm:send-evm \ + --oapp-config move.layerzero.config.ts \ + --src-eid \ + --dst-eid \ + --to \ + --amount \ + --min-amount +``` + +## Help + +```bash +pnpm run lz:sdk:help +``` + +### Verifying successful ownership transfer of your Move-VM OFT: + +Run the following command: + +```bash +aptos account list \ + --account \ + --url https://fullnode.testnet.aptoslabs.com \ + --query resources +``` + +Note: replace the url with your desired aptos fullnode url. + +Look for the following in the output: + +```json +{ + "0x1::object::ObjectCore": { + ... + "owner": "0x", + ... + } + ... +} +``` + +If the owner is your desired address, then the ownership transfer was successful. + +For verifying the admin look for the following in the output: + +```json + { + "::oapp_store::OAppStore": { + "admin": "0x", + ... + } + } +``` + +If the admin is your desired address, then the ownership transfer was successful. diff --git a/examples/oft-aptos-move/contracts/MyOFT.sol b/examples/oft-aptos-move/contracts/MyOFT.sol new file mode 100644 index 000000000..f8bc7b47f --- /dev/null +++ b/examples/oft-aptos-move/contracts/MyOFT.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol"; + +contract MyOFT is OFT { + constructor( + string memory _name, + string memory _symbol, + address _lzEndpoint, + address _delegate + ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {} +} diff --git a/examples/oft-aptos-move/contracts/mocks/MyOFTMock.sol b/examples/oft-aptos-move/contracts/mocks/MyOFTMock.sol new file mode 100644 index 000000000..3ebb888d4 --- /dev/null +++ b/examples/oft-aptos-move/contracts/mocks/MyOFTMock.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import { MyOFT } from "../MyOFT.sol"; + +// @dev WARNING: This is for testing purposes only +contract MyOFTMock is MyOFT { + constructor( + string memory _name, + string memory _symbol, + address _lzEndpoint, + address _delegate + ) MyOFT(_name, _symbol, _lzEndpoint, _delegate) {} + + function mint(address _to, uint256 _amount) public { + _mint(_to, _amount); + } +} diff --git a/examples/oft-aptos-move/deploy-move/OFTInitParams.ts b/examples/oft-aptos-move/deploy-move/OFTInitParams.ts new file mode 100644 index 000000000..8a42b149b --- /dev/null +++ b/examples/oft-aptos-move/deploy-move/OFTInitParams.ts @@ -0,0 +1,12 @@ +const contractName = 'oft' + +const oftMetadata = { + token_name: 'MyMoveVMOFT', + token_symbol: 'MMVMOFT', + icon_uri: '', + project_uri: '', + sharedDecimals: 6, + localDecimals: 6, +} + +export { contractName, oftMetadata } diff --git a/examples/oft-aptos-move/deploy/MyEVMOFT.ts b/examples/oft-aptos-move/deploy/MyEVMOFT.ts new file mode 100644 index 000000000..bcbdeb19b --- /dev/null +++ b/examples/oft-aptos-move/deploy/MyEVMOFT.ts @@ -0,0 +1,53 @@ +import assert from 'assert' + +import { type DeployFunction } from 'hardhat-deploy/types' + +const contractName = 'MyOFT' + +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: [ + 'MyOFT', // name + 'MOFT', // symbol + 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/oft-aptos-move/foundry.toml b/examples/oft-aptos-move/foundry.toml new file mode 100644 index 000000000..f76c3d4c3 --- /dev/null +++ b/examples/oft-aptos-move/foundry.toml @@ -0,0 +1,31 @@ +[profile.default] +solc-version = '0.8.22' +src = 'contracts' +out = 'out' +test = 'test/evm/foundry' +cache_path = 'cache/foundry' +verbosity = 3 +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 ou 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/oft-aptos-move/hardhat.config.ts b/examples/oft-aptos-move/hardhat.config.ts new file mode 100644 index 000000000..dac786566 --- /dev/null +++ b/examples/oft-aptos-move/hardhat.config.ts @@ -0,0 +1,87 @@ +// 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 'hardhat-contract-sizer' +import '@nomiclabs/hardhat-ethers' +import '@layerzerolabs/toolbox-hardhat' +import { HardhatUserConfig, HttpNetworkAccountsUserConfig } from 'hardhat/types' + +import { EndpointId } from '@layerzerolabs/lz-definitions' + +// 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.EVM_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: { + 'bsc-testnet': { + eid: EndpointId.BSC_V2_TESTNET, + url: process.env.RPC_URL_BSC || 'https://bsc-testnet.public.blastapi.io', + accounts, + }, + 'aptos-testnet': { + eid: EndpointId.APTOS_V2_TESTNET, + url: process.env.RPC_URL_APTOS_TESTNET || 'https://rpc.ankr.com/http/aptos_testnet/v1[1]', + accounts, + }, + 'eth-testnet': { + eid: EndpointId.ETHEREUM_V2_TESTNET, + url: process.env.RPC_URL_ETHEREUM_TESTNET || 'https://sepolia.infura.io/v3/', + accounts, + }, + 'solana-devnet': { + eid: EndpointId.SOLANA_V2_TESTNET, + url: process.env.RPC_URL_SOLANA_TESTNET || 'https://api.devnet.solana.com', + 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/oft-aptos-move/move.layerzero.config.ts b/examples/oft-aptos-move/move.layerzero.config.ts new file mode 100644 index 000000000..04f030ea4 --- /dev/null +++ b/examples/oft-aptos-move/move.layerzero.config.ts @@ -0,0 +1,156 @@ +import { EndpointId } from '@layerzerolabs/lz-definitions' +import { ExecutorOptionType } from '@layerzerolabs/lz-v2-utilities' + +import type { OAppOmniGraphHardhat, OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat' + +enum MsgType { + SEND = 1, + SEND_AND_CALL = 2, +} + +const bscContract: OmniPointHardhat = { + eid: EndpointId.BSC_V2_TESTNET, + contractName: 'MyOFT', +} + +const aptosContract: OmniPointHardhat = { + eid: EndpointId.APTOS_V2_TESTNET, + contractName: 'oft', +} + +const config: OAppOmniGraphHardhat = { + contracts: [ + { + contract: bscContract, + config: { + owner: '', + delegate: '', + }, + }, + { + contract: aptosContract, + config: { + delegate: '', + owner: '', + }, + }, + ], + connections: [ + { + from: aptosContract, + to: bscContract, + config: { + enforcedOptions: [ + { + msgType: MsgType.SEND, + optionType: ExecutorOptionType.LZ_RECEIVE, + gas: 80_000, // gas limit in wei for EndpointV2.lzReceive + value: 0, // msg.value in wei for EndpointV2.lzReceive + }, + { + msgType: MsgType.SEND_AND_CALL, + optionType: ExecutorOptionType.LZ_RECEIVE, + gas: 80_000, // gas limit in wei for EndpointV2.lzReceive + value: 0, // msg.value in wei for EndpointV2.lzReceive + }, + ], + sendLibrary: '0xcc1c03aed42e2841211865758b5efe93c0dde2cb7a2a5dc6cf25a4e33ad23690', + receiveLibraryConfig: { + // Required Receive Library Address on Aptos + receiveLibrary: '0xcc1c03aed42e2841211865758b5efe93c0dde2cb7a2a5dc6cf25a4e33ad23690', + // Optional Grace Period for Switching Receive Library Address on Aptos + gracePeriod: BigInt(0), + }, + // Optional Receive Library Timeout for when the Old Receive Library Address will no longer be valid on Aptos + // receiveLibraryTimeoutConfig: { + // lib: '0xbe533727aebe97132ec0a606d99e0ce137dbdf06286eb07d9e0f7154df1f3f10', + // expiry: BigInt(1000000000), + // }, + sendConfig: { + executorConfig: { + maxMessageSize: 10_000, + // The configured Executor address on Aptos + executor: '0x93353700091200ef9fdc536ce6a86182cc7e62da25f94356be9421c6310b9585', + }, + ulnConfig: { + // The number of block confirmations to wait on Aptos before emitting the message from the source chain. + confirmations: BigInt(260), + // The address of the DVNs you will pay to verify a sent message on the source chain. + // The destination tx will wait until ALL `requiredDVNs` verify the message. + requiredDVNs: ['0x756f8ab056688d22687740f4a9aeec3b361170b28d08b719e28c4d38eed1043e'], + // The address of the DVNs you will pay to verify a sent message on the source chain. + // The destination tx will wait until the configured threshold of `optionalDVNs` verify a message. + optionalDVNs: [], + // The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified. + optionalDVNThreshold: 0, + }, + }, + // Optional Receive Configuration + // @dev Controls how the `from` chain receives messages from the `to` chain. + receiveConfig: { + ulnConfig: { + // The number of block confirmations to expect from the `to` chain. + confirmations: BigInt(5), + // The address of the DVNs your `receiveConfig` expects to receive verifications from on the `from` chain. + // The `from` chain's OApp will wait until the configured threshold of `requiredDVNs` verify the message. + requiredDVNs: ['0x756f8ab056688d22687740f4a9aeec3b361170b28d08b719e28c4d38eed1043e'], + // The address of the `optionalDVNs` you expect to receive verifications from on the `from` chain. + // The destination tx will wait until the configured threshold of `optionalDVNs` verify the message. + optionalDVNs: [], + // The number of `optionalDVNs` that need to successfully verify the message for it to be considered Verified. + optionalDVNThreshold: 0, + }, + }, + }, + }, + { + from: bscContract, + to: aptosContract, + config: { + enforcedOptions: [ + { + msgType: MsgType.SEND, + optionType: ExecutorOptionType.LZ_RECEIVE, + gas: 200_000, // gas limit in wei for EndpointV2.lzReceive + value: 0, // msg.value in wei for EndpointV2.lzReceive + }, + { + msgType: MsgType.SEND_AND_CALL, + optionType: ExecutorOptionType.LZ_RECEIVE, + gas: 200_000, // gas limit in wei for EndpointV2.lzCompose + value: 0, // msg.value in wei for EndpointV2.lzCompose + }, + ], + sendLibrary: '0x55f16c442907e86D764AFdc2a07C2de3BdAc8BB7', + receiveLibraryConfig: { + receiveLibrary: '0x188d4bbCeD671A7aA2b5055937F79510A32e9683', + gracePeriod: BigInt(0), + }, + // receiveLibraryTimeoutConfig: { + // lib: '0x188d4bbCeD671A7aA2b5055937F79510A32e9683', + // expiry: BigInt(67323472), + // }, + sendConfig: { + executorConfig: { + maxMessageSize: 10_000, + executor: '0x31894b190a8bAbd9A067Ce59fde0BfCFD2B18470', + }, + ulnConfig: { + confirmations: BigInt(5), + requiredDVNs: ['0x0eE552262f7B562eFcED6DD4A7e2878AB897d405'], + optionalDVNThreshold: 0, + }, + }, + receiveConfig: { + ulnConfig: { + confirmations: BigInt(260), + requiredDVNs: ['0x0eE552262f7B562eFcED6DD4A7e2878AB897d405'], + optionalDVNThreshold: 0, + }, + }, + }, + }, + ], +} + +export default config diff --git a/examples/oft-aptos-move/package.json b/examples/oft-aptos-move/package.json new file mode 100644 index 000000000..5627b2e5d --- /dev/null +++ b/examples/oft-aptos-move/package.json @@ -0,0 +1,114 @@ +{ + "name": "@layerzerolabs/oft-aptos-move-example", + "version": "0.0.1", + "private": true, + "license": "MIT", + "scripts": { + "clean": "rm -rf artifacts cache out build", + "compile": "$npm_execpath run compile:forge && $npm_execpath run compile:hardhat && $npm_execpath run compile:aptos", + "compile:aptos": "aptos move compile --dev --skip-attribute-checks --included-artifacts=none --skip-fetch-latest-git-deps", + "compile:forge": "forge build", + "compile:hardhat": "hardhat compile", + "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'", + "lz:sdk:evm:quote-send-evm": "ts-node scripts/cli.ts --vm evm --op quote-send", + "lz:sdk:evm:send-evm": "ts-node scripts/cli.ts --vm evm --op send", + "lz:sdk:evm:wire": "ts-node scripts/cli.ts --vm evm --op wire", + "lz:sdk:help": "ts-node scripts/cli.ts --op help --filter all", + "lz:sdk:move:build": "ts-node scripts/cli.ts --vm move --op build", + "lz:sdk:move:deploy": "ts-node scripts/cli.ts --vm move --op deploy", + "lz:sdk:move:init-fa": "ts-node scripts/cli.ts --vm move --op init-fa", + "lz:sdk:move:mint-to-move-oft": "ts-node scripts/cli.ts --vm move --op mint-to-move-oft", + "lz:sdk:move:permanently-disable-blocklist": "ts-node scripts/cli.ts --vm move --op permanently-disable-blocklist", + "lz:sdk:move:permanently-disable-freezing": "ts-node scripts/cli.ts --vm move --op permanently-disable-freezing", + "lz:sdk:move:quote-send-move-oft": "ts-node scripts/cli.ts --vm move --op quote-send-move-oft", + "lz:sdk:move:send-from-move-oft": "ts-node scripts/cli.ts --vm move --op send-from-move-oft", + "lz:sdk:move:set-delegate": "ts-node scripts/cli.ts --vm move --op set-delegate", + "lz:sdk:move:set-fee": "ts-node scripts/cli.ts --vm move --op set-fee", + "lz:sdk:move:set-rate-limit": "ts-node scripts/cli.ts --vm move --op set-rate-limit", + "lz:sdk:move:transfer-oapp-owner": "ts-node scripts/cli.ts --vm move --op transfer-oapp-owner", + "lz:sdk:move:transfer-object-owner": "ts-node scripts/cli.ts --vm move --op transfer-object-owner", + "lz:sdk:move:unset-rate-limit": "ts-node scripts/cli.ts --vm move --op unset-rate-limit", + "lz:sdk:move:wire": "ts-node scripts/cli.ts --vm move --op wire", + "test": "$npm_execpath run test:forge && $npm_execpath run test:hardhat && $npm_execpath run test:aptos", + "test:aptos": "aptos move test --dev --skip-fetch-latest-git-deps", + "test:forge": "forge test", + "test:hardhat": "hardhat test" + }, + "resolutions": { + "ethers": "^5.7.2", + "hardhat-deploy": "^0.12.1" + }, + "devDependencies": { + "@aptos-labs/ts-sdk": "^1.33.1", + "@babel/core": "^7.23.9", + "@jest/globals": "^29.7.0", + "@layerzerolabs/devtools-extensible-cli": "^0.0.1", + "@layerzerolabs/devtools-move": "^0.0.1", + "@layerzerolabs/eslint-config-next": "~2.3.39", + "@layerzerolabs/lz-config-types": "^3.0.15", + "@layerzerolabs/lz-definitions": "^3.0.21", + "@layerzerolabs/lz-evm-messagelib-v2": "^3.0.12", + "@layerzerolabs/lz-evm-protocol-v2": "^3.0.12", + "@layerzerolabs/lz-evm-sdk-v2": "^3.0.27", + "@layerzerolabs/lz-evm-v1-0.7": "^3.0.12", + "@layerzerolabs/lz-movevm-sdk-v2": "^3.0.15", + "@layerzerolabs/lz-serdes": "^3.0.19", + "@layerzerolabs/lz-v2-utilities": "^3.0.12", + "@layerzerolabs/move-definitions": "^3.0.15", + "@layerzerolabs/oapp-evm": "^0.3.0", + "@layerzerolabs/oft-evm": "^3.0.0", + "@layerzerolabs/oft-move": "^0.0.1", + "@layerzerolabs/prettier-config-next": "^2.3.39", + "@layerzerolabs/solhint-config": "^3.0.12", + "@layerzerolabs/test-devtools-evm-foundry": "~5.1.0", + "@layerzerolabs/toolbox-foundry": "~0.1.9", + "@layerzerolabs/toolbox-hardhat": "~0.6.3", + "@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", + "@types/argparse": "^2.0.17", + "@types/chai": "^4.3.11", + "@types/jest": "^29.5.12", + "@types/mocha": "^10.0.6", + "@types/node": "~18.18.14", + "argparse": "^2.0.1", + "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", + "hardhat": "^2.22.10", + "hardhat-contract-sizer": "^2.10.0", + "hardhat-deploy": "^0.12.1", + "install": "^0.13.0", + "jest": "^29.7.0", + "mocha": "^10.2.0", + "prettier": "^3.2.5", + "solhint": "^4.1.1", + "solidity-bytes-utils": "^0.8.2", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "tsup": "^8.0.1", + "typescript": "^5.4.4", + "yaml": "^2.6.1" + }, + "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/oft-aptos-move/scripts/cli.ts b/examples/oft-aptos-move/scripts/cli.ts new file mode 100644 index 000000000..206574330 --- /dev/null +++ b/examples/oft-aptos-move/scripts/cli.ts @@ -0,0 +1,13 @@ +import { sdk } from '@layerzerolabs/devtools-extensible-cli/cli/AptosEVMCli' +import { attach_wire_evm, attach_wire_move } from '@layerzerolabs/devtools-move/cli/init' +import { attach_oft_move } from '@layerzerolabs/oft-move/cli/init' + +async function lzSdk() { + await attach_wire_move(sdk) + await attach_oft_move(sdk) + await attach_wire_evm(sdk) + + await sdk.execute() +} + +lzSdk() diff --git a/examples/oft-aptos-move/solhint.config.js b/examples/oft-aptos-move/solhint.config.js new file mode 100644 index 000000000..52efe629c --- /dev/null +++ b/examples/oft-aptos-move/solhint.config.js @@ -0,0 +1 @@ +module.exports = require('@layerzerolabs/solhint-config'); diff --git a/examples/oft-aptos-move/sources/oft_implementation/oft_fa.move b/examples/oft-aptos-move/sources/oft_implementation/oft_fa.move new file mode 100644 index 000000000..104d75c42 --- /dev/null +++ b/examples/oft-aptos-move/sources/oft_implementation/oft_fa.move @@ -0,0 +1,400 @@ +/// This is an implementation of a Fungible-Asset-standard OFT. +/// +/// This creates a FungibleAsset upon initialization and mints and burns tokens on receive and send respectively. +/// This can be modified to accept mint, burn, and metadata references of an existing FungibleAsset upon initialization +/// rather than creating a new FungibleAsset +module oft::oft_fa { + use std::coin::Coin; + use std::event::emit; + use std::fungible_asset::{Self, BurnRef, FungibleAsset, Metadata, MintRef, MutateMetadataRef, TransferRef}; + use std::object::{Self, Object}; + use std::object::{address_from_constructor_ref, address_to_object}; + use std::option::{Self, Option}; + use std::primary_fungible_store; + use std::signer::address_of; + use std::string::utf8; + + use endpoint_v2_common::bytes32::Bytes32; + use oft::oapp_core::{assert_admin, combine_options}; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core; + use oft::oft_impl_config::{ + Self, assert_not_blocklisted, debit_view_with_possible_fee, fee_details_with_possible_fee, + redirect_to_admin_if_blocklisted, release_rate_limit_capacity, try_consume_rate_limit_capacity, + }; + use oft_common::oft_fee_detail::OftFeeDetail; + use oft_common::oft_limit::{Self, OftLimit}; + + friend oft::oft; + friend oft::oapp_receive; + + #[test_only] + friend oft::oft_fa_tests; + + struct OftImpl has key { + metadata: Object, + mint_ref: MintRef, + burn_ref: BurnRef, + transfer_ref: TransferRef, + mutate_metadata_ref: MutateMetadataRef, + freeze_fungible_store_enabled: bool, + } + + // ================================================= OFT Handlers ================================================= + + /// The default *credit* behavior for a standard OFT is to mint the amount and transfer to the recipient + public(friend) fun credit( + to: address, + amount_ld: u64, + src_eid: u32, + lz_receive_value: Option, + ): u64 acquires OftImpl { + // Default implementation does not make special use of LZ Receive Value sent; just deposit to the OFT address + option::for_each(lz_receive_value, |fa| primary_fungible_store::deposit(@oft_admin, fa)); + + // Release rate limit capacity for the pathway (net inflow) + release_rate_limit_capacity(src_eid, amount_ld); + + // Mint the extracted amount to the recipient, or redirect to the admin if the recipient is blocklisted + primary_fungible_store::mint(&store().mint_ref, redirect_to_admin_if_blocklisted(to, amount_ld), amount_ld); + + amount_ld + } + + /// The default *debit* behavior for a standard OFT is to deduct the amount from the sender and burn the deducted + /// amount + /// @return (amount_sent_ld, amount_received_ld) + public(friend) fun debit_fungible_asset( + sender: address, + fa: &mut FungibleAsset, + min_amount_ld: u64, + dst_eid: u32, + ): (u64, u64) acquires OftImpl { + assert_not_blocklisted(sender); + assert_metadata(fa); + + // Calculate the exact send amount + let amount_ld = fungible_asset::amount(fa); + let (amount_sent_ld, amount_received_ld) = debit_view(amount_ld, min_amount_ld, dst_eid); + + // Consume rate limit capacity for the pathway (net outflow), based on the amount received on the other side + try_consume_rate_limit_capacity(dst_eid, amount_received_ld); + + // Extract the exact send amount from the provided fungible asset + let extracted_fa = fungible_asset::extract(fa, amount_sent_ld); + + // Extract the fee and deposit it to the fee deposit address + let fee_ld = (amount_sent_ld - amount_received_ld); + if (fee_ld > 0) { + let fee_fa = fungible_asset::extract(&mut extracted_fa, fee_ld); + primary_fungible_store::deposit(fee_deposit_address(), fee_fa); + }; + + // Burn the extracted amount + fungible_asset::burn(&store().burn_ref, extracted_fa); + + (amount_sent_ld, amount_received_ld) + } + + // Unused in this implementation + public(friend) fun debit_coin( + _sender: address, + _coin: &mut Coin, + _min_amount_ld: u64, + _dst_eid: u32, + ): (u64, u64) { + abort ENOT_IMPLEMENTED + } + + /// The default *debit_view* behavior for a standard OFT is to remove dust and use remainder as both the sent and + /// received amounts, reflecting that no additional fees are removed + public(friend) fun debit_view(amount_ld: u64, min_amount_ld: u64, _dst_eid: u32): (u64, u64) { + debit_view_with_possible_fee(amount_ld, min_amount_ld) + } + + /// Change this to override the Executor and DVN options of the OFT transmission + public(friend) fun build_options( + message_type: u16, + dst_eid: u32, + extra_options: vector, + _user_sender: address, + _amount_received_ld: u64, + _to: Bytes32, + _compose_msg: vector, + _oft_cmd: vector, + ): vector { + combine_options(dst_eid, message_type, extra_options) + } + + /// Implement this function to inspect the message and options before quoting and sending + public(friend) fun inspect_message( + _message: &vector, + _options: &vector, + _is_sending: bool, + ) {} + + /// Change this to override the OFT limit and fees provided when quoting. The fees should reflect the difference + /// between the amount sent and the amount received returned from debit() and debit_view() + public(friend) fun oft_limit_and_fees( + dst_eid: u32, + _to: vector, + amount_ld: u64, + min_amount_ld: u64, + _extra_options: vector, + _compose_msg: vector, + _oft_cmd: vector, + ): (OftLimit, vector) { + (rate_limited_oft_limit(dst_eid), fee_details_with_possible_fee(amount_ld, min_amount_ld)) + } + + // =========================================== Coin Deposit / Withdrawal ========================================== + + public(friend) fun send_standards_supported(): vector> { + vector[b"fungible_asset"] + } + + /// Deposit coin function abstracted from `oft.move` for cross-chain flexibility + public(friend) fun deposit_coin(_account: address, _coin: Coin) { + abort ENOT_IMPLEMENTED + } + + + /// Unused in this implementation + public(friend) fun withdraw_coin(_account: &signer, _amount_ld: u64): Coin { + abort ENOT_IMPLEMENTED + } + + // =================================================== Metadata =================================================== + + public(friend) fun metadata(): Object acquires OftImpl { + store().metadata + } + + fun assert_metadata(fa: &FungibleAsset) acquires OftImpl { + let fa_metadata = fungible_asset::metadata_from_asset(fa); + assert!(fa_metadata == metadata(), EWRONG_FA_METADATA); + } + + public(friend) fun balance(account: address): u64 acquires OftImpl { + primary_fungible_store::balance(account, metadata()) + } + + /// Present for compatibility only + struct PlaceholderCoin {} + + // ================================================= Configuration ================================================ + + /// Set the fee (in BPS) for outbound OFT sends + public entry fun set_fee_bps(admin: &signer, fee_bps: u64) { + assert_admin(address_of(admin)); + oft_impl_config::set_fee_bps(fee_bps); + } + + #[view] + /// Get the fee (in BPS) for outbound OFT sends + public fun fee_bps(): u64 { oft_impl_config::fee_bps() } + + /// Set the fee deposit address for outbound OFT sends + public entry fun set_fee_deposit_address(admin: &signer, fee_deposit_address: address) { + assert_admin(address_of(admin)); + oft_impl_config::set_fee_deposit_address(fee_deposit_address); + } + + #[view] + /// Get the fee deposit address for outbound OFT sends + public fun fee_deposit_address(): address { oft_impl_config::fee_deposit_address() } + + /// Permanently disable the ability to blocklist addresses + public entry fun irrevocably_disable_blocklist(admin: &signer) { + assert_admin(address_of(admin)); + oft_impl_config::irrevocably_disable_blocklist(); + } + + /// Set the blocklist status of a wallet address + /// If a wallet is blocklisted + /// - OFT sends from the wallet will be blocked + /// - OFT receives to the wallet will be be diverted to the admin + public entry fun set_blocklist(admin: &signer, wallet: address, block: bool) { + assert_admin(address_of(admin)); + oft_impl_config::set_blocklist(wallet, block); + } + + #[view] + /// Get the blocklist status of a wallet address + public fun is_blocklisted(wallet: address): bool { oft_impl_config::is_blocklisted(wallet) } + + /// Set the rate limit configuration for a given endpoint ID + /// The rate limit is the maximum amount of OFT that can be sent to the endpoint within a given window + /// The rate limit capacity recovers linearly at a rate of limit / window_seconds + /// *Important*: Setting the rate limit does not reset the current "in-flight" volume (in-flight refers to the + /// decayed rate limit consumption). This means that if the rate limit is set lower than the current in-flight + /// volume, the endpoint will not be able to receive OFT until the in-flight volume decays below the new rate limit. + /// In order to reset the in-flight volume, the rate limit must be unset and then set again. + public entry fun set_rate_limit(admin: &signer, eid: u32, limit: u64, window_seconds: u64) { + assert_admin(address_of(admin)); + oft_impl_config::set_rate_limit(eid, limit, window_seconds); + } + + /// Unset the rate limit + public entry fun unset_rate_limit(admin: &signer, eid: u32) { + assert_admin(address_of(admin)); + oft_impl_config::unset_rate_limit(eid); + } + + #[view] + /// Get the rate limit configuration for a given endpoint ID + /// @return (limit, window_seconds) + public fun rate_limit_config(eid: u32): (u64, u64) { oft_impl_config::rate_limit_config(eid) } + + #[view] + /// Get the amount of rate limit capacity currently consumed on this pathway + public fun rate_limit_in_flight(eid: u32): u64 { oft_impl_config::in_flight(eid) } + + #[view] + /// Get the rate limit capacity for a given endpoint ID + public fun rate_limit_capacity(eid: u32): u64 { oft_impl_config::rate_limit_capacity(eid) } + + /// Create an OftLimit that reflects the rate limit for a given endpoint ID + public fun rate_limited_oft_limit(eid: u32): OftLimit { + oft_limit::new_oft_limit(0, oft_impl_config::rate_limit_capacity(eid)) + } + + /// Permanently disable the ability to freeze a primary fungible store through the OFT + /// This will permanently prevent freezing of new accounts. It will not prevent unfreezing accounts, and existing + /// frozen accounts will remain frozen until unfrozen + public entry fun permanently_disable_fungible_store_freezing(admin: &signer) acquires OftImpl { + assert_admin(address_of(admin)); + store_mut().freeze_fungible_store_enabled = false; + emit(FungibleStoreFreezingPermanentlyDisabled {}); + } + + /// Set the frozen status of a primary fungible store + /// To freeze, account freezing must not have been disabled + public entry fun set_primary_fungible_store_frozen( + admin: &signer, + account: address, + frozen: bool + ) acquires OftImpl { + assert_admin(address_of(admin)); + assert!(frozen != primary_fungible_store::is_frozen(account, metadata()), ENO_CHANGE); + // If account freezing is disabled, do not allow freezing accounts, but allow unfreeze + assert!(!frozen || store().freeze_fungible_store_enabled, EFREEZE_FUNGIBLE_STORE_DISABLED); + primary_fungible_store::set_frozen_flag(&store().transfer_ref, account, frozen); + } + + #[view] + /// Get the frozen status of a primary fungible store + public fun is_primary_fungible_store_frozen(account: address): bool acquires OftImpl { + primary_fungible_store::is_frozen(account, metadata()) + } + + /// Set the frozen status of a fungible store + public entry fun set_fungible_store_frozen( + admin: &signer, + fa_store: Object, + frozen: bool + ) acquires OftImpl { + assert_admin(address_of(admin)); + assert!(frozen != fungible_asset::is_frozen(fa_store), ENO_CHANGE); + // If account freezing is disabled, do not allow freezing accounts, but allow unfreeze + assert!(!frozen || store().freeze_fungible_store_enabled, EFREEZE_FUNGIBLE_STORE_DISABLED); + fungible_asset::set_frozen_flag(&store().transfer_ref, fa_store, frozen); + } + + #[view] + /// Get the frozen status of a fungible store + public fun is_fungible_store_frozen(fa_store: Object): bool { + fungible_asset::is_frozen(fa_store) + } + + #[view] + /// Get the total supply of the Coin + public fun supply(): u128 acquires OftImpl { + let supply = fungible_asset::supply(metadata()); + option::destroy_with_default(supply, 0) + } + + // ================================================ Initialization ================================================ + + public entry fun initialize( + account: &signer, + token_name: vector, + symbol: vector, + icon_uri: vector, + project_uri: vector, + shared_decimals: u8, + local_decimals: u8, + ) acquires OftImpl { + assert_admin(address_of(account)); + fungible_asset::mutate_metadata( + &store().mutate_metadata_ref, + option::some(utf8(token_name)), + option::some(utf8(symbol)), + option::some(local_decimals), + option::some(utf8(icon_uri)), + option::some(utf8(project_uri)), + ); + + oft_core::initialize(local_decimals, shared_decimals); + } + + fun init_module(account: &signer) { + let constructor_ref = &object::create_named_object(account, b"oft_fa"); + // Create a fungible asset with empty paramters that be set to the correct values on initialize() + primary_fungible_store::create_primary_store_enabled_fungible_asset( + constructor_ref, + option::none(), + utf8(b""), + utf8(b""), + 8, + utf8(b""), + utf8(b""), + ); + + move_to(move account, OftImpl { + metadata: address_to_object(address_from_constructor_ref(constructor_ref)), + mint_ref: fungible_asset::generate_mint_ref(constructor_ref), + burn_ref: fungible_asset::generate_burn_ref(constructor_ref), + transfer_ref: fungible_asset::generate_transfer_ref(constructor_ref), + mutate_metadata_ref: fungible_asset::generate_mutate_metadata_ref(constructor_ref), + freeze_fungible_store_enabled: true, + }); + } + + #[test_only] + public fun init_module_for_test() { + init_module(&std::account::create_signer_for_test(OAPP_ADDRESS())); + } + + // =================================================== Helpers ==================================================== + + #[test_only] + public fun mint_tokens_for_test(amount_ld: u64): FungibleAsset acquires OftImpl { + fungible_asset::mint(&store().mint_ref, amount_ld) + } + + inline fun store(): &OftImpl { + borrow_global(OAPP_ADDRESS()) + } + + inline fun store_mut(): &mut OftImpl { + borrow_global_mut(OAPP_ADDRESS()) + } + + // ==================================================== Events ==================================================== + + #[event] + struct FungibleStoreFreezingPermanentlyDisabled has store, drop {} + + #[test_only] + public fun fungible_store_freezing_permanently_disabled_event(): FungibleStoreFreezingPermanentlyDisabled { + FungibleStoreFreezingPermanentlyDisabled {} + } + + // ================================================== Error Codes ================================================= + + const EFREEZE_FUNGIBLE_STORE_DISABLED: u64 = 1; + const ENO_CHANGE: u64 = 2; + const ENOT_IMPLEMENTED: u64 = 3; + const EWRONG_FA_METADATA: u64 = 4; +} \ No newline at end of file diff --git a/examples/oft-aptos-move/sources/shared_oapp/oapp_core.move b/examples/oft-aptos-move/sources/shared_oapp/oapp_core.move new file mode 100644 index 000000000..2c5c0a7fd --- /dev/null +++ b/examples/oft-aptos-move/sources/shared_oapp/oapp_core.move @@ -0,0 +1,408 @@ +/// This OApp Core module provides the common functionality for OApps to interact with the LayerZero Endpoint V2 module. +/// It gives the ability to send messages, set configurations, manage peers and delegates, and enforce options. +/// +/// This should generally not need to be edited by OApp developers, except to update friend declarations to reflect +/// the modules that depend on the friend functions called in this module. +module oft::oapp_core { + use std::event::emit; + use std::fungible_asset::FungibleAsset; + use std::option::{Self, Option}; + use std::primary_fungible_store; + use std::signer::address_of; + use std::string::String; + use std::vector; + + use endpoint_v2::endpoint::{Self, wrap_guid}; + use endpoint_v2::messaging_receipt::MessagingReceipt; + use endpoint_v2_common::bytes32::{Bytes32, from_bytes32, to_bytes32, ZEROS_32_BYTES}; + use endpoint_v2_common::native_token; + use endpoint_v2_common::serde; + use endpoint_v2_common::universal_config::get_zro_metadata; + use oft::oapp_store::{Self, OAPP_ADDRESS}; + + friend oft::oft; + + #[test_only] + friend oft::oapp_core_tests; + #[test_only] + friend oft::oft_core_tests; + + // ==================================================== Send =================================================== + + /// This handles sending a message to a remote OApp on the configured peer + public(friend) fun lz_send( + dst_eid: u32, + message: vector, + options: vector, + native_fee: &mut FungibleAsset, + zro_fee: &mut Option, + ): MessagingReceipt { + endpoint::send( + &oapp_store::call_ref(), + dst_eid, + get_peer_bytes32(dst_eid), + message, + options, + native_fee, + zro_fee, + ) + } + + #[view] + /// This provides a LayerZero quote for sending a message + public fun lz_quote( + dst_eid: u32, + message: vector, + options: vector, + pay_in_zro: bool, + ): (u64, u64) { + endpoint::quote(OAPP_ADDRESS(), dst_eid, get_peer_bytes32(dst_eid), message, options, pay_in_zro) + } + + // ==================================================== Compose =================================================== + + public(friend) fun lz_send_compose( + to: address, + index: u16, + guid: Bytes32, + message: vector, + ) { + endpoint::send_compose(&oapp_store::call_ref(), to, index, guid, message); + } + + // ================================================ Delegated Calls =============================================== + + /// Asserts that the delegated call is "authorized," (the assigned delegate) + /// "authorized" indicates a wallet has permission to act on behalf of the OApp in respect to endpoint calls, + /// for example, "set_send_library()" or "skip()," but this does not extend to calls that are internal to (stored + /// on) the OApp like "set_peer()," which is are "admin only" permissions + fun assert_authorized(account: address) { + assert!(account == oapp_store::get_delegate(), EUNAUTHORIZED); + } + + /// Set the OApp configuration for a Message Library + public entry fun set_config( + account: &signer, + msglib: address, + eid: u32, + config_type: u32, + config: vector, + ) { + assert_authorized(address_of(move account)); + endpoint::set_config(&oapp_store::call_ref(), msglib, eid, config_type, config) + } + + /// Set the Send Library for an OApp + public entry fun set_send_library( + account: &signer, + remote_eid: u32, + msglib: address, + ) { + assert_authorized(address_of(move account)); + endpoint::set_send_library(&oapp_store::call_ref(), remote_eid, msglib) + } + + /// Set the Receive Library for an OApp + public entry fun set_receive_library( + account: &signer, + remote_eid: u32, + msglib: address, + grace_period: u64, + ) { + assert_authorized(address_of(move account)); + endpoint::set_receive_library(&oapp_store::call_ref(), remote_eid, msglib, grace_period) + } + + /// Update the Receive Library Expiry for an OApp + public entry fun set_receive_library_timeout( + account: &signer, + remote_eid: u32, + msglib: address, + expiry: u64, + ) { + assert_authorized(address_of(move account)); + endpoint::set_receive_library_timeout(&oapp_store::call_ref(), remote_eid, msglib, expiry) + } + + /// Register a Receive Pathway for an OApp + public entry fun register_receive_pathway( + account: &signer, + src_eid: u32, + src_oapp: vector, + ) { + assert_authorized(address_of(move account)); + endpoint::register_receive_pathway(&oapp_store::call_ref(), src_eid, to_bytes32(src_oapp)) + } + + /// Clear an OApp message + public entry fun clear( + account: &signer, + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + message: vector, + ) { + assert_authorized(address_of(move account)); + endpoint::clear( + &oapp_store::call_ref(), + src_eid, + to_bytes32(sender), + nonce, + wrap_guid(to_bytes32(guid)), + message, + ) + } + + /// Skip an OApp message + public entry fun skip( + account: &signer, + src_eid: u32, + sender: vector, + nonce: u64, + ) { + assert_authorized(address_of(move account)); + endpoint::skip(&oapp_store::call_ref(), src_eid, to_bytes32(sender), nonce) + } + + /// Burn an OApp message + public entry fun burn( + account: &signer, + src_eid: u32, + sender: vector, + nonce: u64, + payload_hash: vector, + ) { + assert_authorized(address_of(move account)); + endpoint::burn(&oapp_store::call_ref(), src_eid, to_bytes32(sender), nonce, to_bytes32(payload_hash)) + } + + /// Nilify an OApp message + public entry fun nilify( + account: &signer, + src_eid: u32, + sender: vector, + nonce: u64, + payload_hash: vector, + ) { + assert_authorized(address_of(move account)); + endpoint::nilify(&oapp_store::call_ref(), src_eid, to_bytes32(sender), nonce, to_bytes32(payload_hash)) + } + + // =============================================== Enforced Options =============================================== + + #[view] + public fun get_enforced_options(eid: u32, msg_type: u16): vector { + oapp_store::get_enforced_options(eid, msg_type) + } + + public entry fun set_enforced_options( + account: &signer, + eid: u32, + msg_type: u16, + enforced_options: vector, + ) { + assert_admin(address_of(move account)); + assert_options_type_3(enforced_options); + oapp_store::set_enforced_options(eid, msg_type, enforced_options); + emit(EnforcedOptionSet { eid, msg_type, enforced_options }); + } + + #[view] + public fun combine_options(eid: u32, msg_type: u16, extra_options: vector): vector { + let enforced_options = oapp_store::get_enforced_options(eid, msg_type); + if (vector::is_empty(&enforced_options)) { return extra_options }; + if (vector::is_empty(&extra_options)) { return enforced_options }; + assert_options_type_3(extra_options); + vector::append(&mut enforced_options, serde::extract_bytes_until_end(&extra_options, &mut 2)); + enforced_options + } + + // ===================================================== Admin ==================================================== + + #[view] + /// Gets the admin address + public fun get_admin(): address { + oapp_store::get_admin() + } + + /// Change the admin of the OApp to another account + public entry fun transfer_admin(account: &signer, new_admin: address) { + let admin = address_of(move account); + assert_admin(admin); + assert!(std::account::exists_at(new_admin), EINVALID_ACCOUNT); + oapp_store::set_admin(new_admin); + emit(AdminTransferred { admin: new_admin }); + } + + /// Permanently renounce OApp admin rights. Once this is called the admin cannot be reinstated + public entry fun renounce_admin(account: &signer) { + let admin = address_of(move account); + assert_admin(admin); + oapp_store::set_admin(@0x0); + emit(AdminTransferred { admin: @0x0 }); + } + + /// Asserts that a user address is the OApp admin. This admin can make any configuration change that directly lives + /// on the OApp (like setting the peer), but it does not include permission to make configuration changes or act on + /// behalf of the OApp on the Endpoint, which requires "authorized" permission + public fun assert_admin(admin: address) { + assert!(admin == oapp_store::get_admin(), EUNAUTHORIZED); + } + + // ===================================================== Peers ==================================================== + + #[view] + public fun has_peer(eid: u32): bool { + oapp_store::has_peer(eid) + } + + #[view] + public fun get_peer(eid: u32): vector { + from_bytes32(get_peer_bytes32(eid)) + } + + public fun get_peer_bytes32(eid: u32): Bytes32 { + assert!(oapp_store::has_peer(eid), EUNCONFIGURED_PEER); + oapp_store::get_peer(eid) + } + + public entry fun set_peer(account: &signer, eid: u32, peer: vector) { + assert_admin(address_of(move account)); + // Automatically register the receive pathway when a peer is set + endpoint::register_receive_pathway(&oapp_store::call_ref(), eid, to_bytes32(peer)); + // Set the peer + let peer_bytes32 = to_bytes32(peer); + oapp_store::set_peer(eid, peer_bytes32); + emit(PeerSet { eid, peer }); + } + + public entry fun remove_peer(account: &signer, eid: u32) { + assert_admin(address_of(move account)); + assert!(oapp_store::has_peer(eid), EUNCONFIGURED_PEER); + oapp_store::remove_peer(eid); + emit(PeerSet { eid, peer: ZEROS_32_BYTES() }); + } + + // =================================================== Delegates ================================================== + + #[view] + public fun has_delegate(): bool { + oapp_store::get_delegate() != @0x0 + } + + #[view] + public fun get_delegate(): address { + oapp_store::get_delegate() + } + + /// Set the delegate address for the OApp - set to @0x0 to remove the delegate + public entry fun set_delegate(account: &signer, delegate: address) { + assert_admin(address_of(move account)); + oapp_store::set_delegate(delegate); + emit(DelegateSet { delegate }); + } + + // ==================================================== General =================================================== + + #[view] + public fun get_lz_receive_module_name(): String { + endpoint::get_lz_receive_module(oft::oapp_store::OAPP_ADDRESS()) + } + + #[view] + public fun get_lz_compose_module_name(): String { + endpoint::get_lz_compose_module(oft::oapp_store::OAPP_ADDRESS()) + } + + // ===================================================== Utils ==================================================== + + /// Utility function to withdraw the specified native and zro fees from the provided account + public fun withdraw_lz_fees( + account: &signer, + native_fee: u64, + zro_fee: u64, + ): (FungibleAsset, Option) { + assert!(native_token::balance(address_of(account)) >= native_fee, EINSUFFICIENT_NATIVE_TOKEN_BALANCE); + let native_fee_fa = native_token::withdraw(account, native_fee); + let zro_fee_fa = if (zro_fee > 0) { + let zro_metadata = get_zro_metadata(); + assert!( + primary_fungible_store::balance(address_of(account), zro_metadata) >= zro_fee, + EINSUFFICIENT_ZRO_BALANCE, + ); + option::some(primary_fungible_store::withdraw(account, zro_metadata, zro_fee)) + } else option::none(); + (native_fee_fa, zro_fee_fa) + } + + /// Utility function to refund the specified fees to the provided account address + public fun refund_fees(account: address, native_fee_fa: FungibleAsset, zro_fee_fa: Option) { + primary_fungible_store::deposit(account, native_fee_fa); + option::destroy(zro_fee_fa, |zro_fee_fa| primary_fungible_store::deposit(account, zro_fee_fa)); + } + + // ==================================================== Helpers =================================================== + + /// Assert that an option is a type 3 option (begins with 0x0003) + public fun assert_options_type_3(options: vector) { + assert!(vector::length(&options) >= 2, EINVALID_OPTIONS); + let options_type = serde::extract_u16(&options, &mut 0); + assert!(options_type == 3, EINVALID_OPTIONS); + } + + // ==================================================== Events ==================================================== + + #[event] + struct AdminTransferred has drop, store { + admin: address, + } + + #[event] + struct PeerSet has drop, store { + eid: u32, + // Peer address - all zeros (0x00*32) if unset + peer: vector, + } + + #[event] + struct DelegateSet has drop, store { + delegate: address, + } + + #[event] + struct EnforcedOptionSet has drop, store { + eid: u32, + msg_type: u16, + enforced_options: vector, + } + + #[test_only] + public fun admin_transferred_event(admin: address): AdminTransferred { + AdminTransferred { admin } + } + + #[test_only] + public fun peer_set_event(eid: u32, peer: vector): PeerSet { + PeerSet { eid, peer } + } + + #[test_only] + public fun delegate_set_event(delegate: address): DelegateSet { + DelegateSet { delegate } + } + + #[test_only] + public fun enforced_option_set_event(eid: u32, msg_type: u16, enforced_options: vector): EnforcedOptionSet { + EnforcedOptionSet { eid, msg_type, enforced_options } + } + + // ================================================== Error Codes ================================================= + + const EUNAUTHORIZED: u64 = 1; + const EUNCONFIGURED_PEER: u64 = 2; + const EINSUFFICIENT_NATIVE_TOKEN_BALANCE: u64 = 3; + const EINSUFFICIENT_ZRO_BALANCE: u64 = 4; + const EINVALID_OPTIONS: u64 = 5; + const EINVALID_ACCOUNT: u64 = 6; +} diff --git a/examples/oft-aptos-move/sources/shared_oapp/oapp_receive.move b/examples/oft-aptos-move/sources/shared_oapp/oapp_receive.move new file mode 100644 index 000000000..e3bf7862b --- /dev/null +++ b/examples/oft-aptos-move/sources/shared_oapp/oapp_receive.move @@ -0,0 +1,93 @@ +/// This is an internal module that receives the lz_receive call from the Executor. This in turn calls the handler in +/// `oapp_receive_handler::lz_receive_impl` which is implemented by the OApp developer. +/// +/// This module should generally not be modified by the OApp developer. +module oft::oapp_receive { + use std::fungible_asset::FungibleAsset; + use std::option::{Self, Option}; + use std::string::utf8; + use std::type_info::{module_name, type_of}; + + use endpoint_v2::endpoint::{Self, get_guid_from_wrapped, wrap_guid, WrappedGuid}; + use endpoint_v2_common::bytes32::to_bytes32; + use oft::oapp_core::get_peer_bytes32; + use oft::oapp_store; + use oft::oapp_store::is_native_token; + use oft::oft::{lz_receive_impl, next_nonce_impl}; + + /// LZ Receive function for self-execution + public entry fun lz_receive( + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + message: vector, + extra_data: vector, + ) { + lz_receive_with_value( + src_eid, + sender, + nonce, + wrap_guid(to_bytes32(guid)), + message, + extra_data, + option::none(), + ) + } + + /// LZ Receive function to be called by the Executor + /// This is able to be provided a receive value in the form of a FungibleAsset + /// For self-executing with a value, this should be called with a script + /// The WrappedGuid is used by the caller script to enforce that the LayerZero endpoint is called by the OApp + public fun lz_receive_with_value( + src_eid: u32, + sender: vector, + nonce: u64, + wrapped_guid: WrappedGuid, + message: vector, + extra_data: vector, + value: Option, + ) { + assert!(option::is_none(&value) || is_native_token(option::borrow(&value)), EINVALID_TOKEN); + let sender = to_bytes32(sender); + assert!(get_peer_bytes32(src_eid) == sender, ENOT_PEER); + + let guid = get_guid_from_wrapped(&wrapped_guid); + endpoint::clear(&oapp_store::call_ref(), src_eid, sender, nonce, wrapped_guid, message); + + lz_receive_impl( + src_eid, + sender, + nonce, + guid, + message, + extra_data, + value, + ); + } + + #[view] + /// Get the next nonce for the given pathway + public fun next_nonce(src_eid: u32, sender: vector): u64 { + next_nonce_impl(src_eid, to_bytes32(sender)) + } + + // ================================================ Initialization ================================================ + + fun init_module(account: &signer) { + let module_name = module_name(&type_of()); + endpoint::register_oapp(account, utf8(module_name)); + } + + struct LzReceiveModule {} + + #[test_only] + public fun init_module_for_test() { + init_module(&std::account::create_signer_for_test(oft::oapp_store::OAPP_ADDRESS())); + } + + // ================================================== Error Codes ================================================= + + const EINVALID_TOKEN: u64 = 1; + const ENOT_PEER: u64 = 2; +} diff --git a/examples/oft-aptos-move/sources/shared_oapp/oapp_store.move b/examples/oft-aptos-move/sources/shared_oapp/oapp_store.move new file mode 100644 index 000000000..14792db47 --- /dev/null +++ b/examples/oft-aptos-move/sources/shared_oapp/oapp_store.move @@ -0,0 +1,115 @@ +/// The Internal store module for the OApp Core and Receive modules. +/// +/// This module should generally not be modified by the OApp developer. +module oft::oapp_store { + use std::fungible_asset::{Self, FungibleAsset}; + use std::object::object_address; + use std::table::{Self, Table}; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::contract_identity::{Self, CallRef, ContractSigner, create_contract_signer, DynamicCallRef}; + + friend oft::oapp_core; + friend oft::oapp_receive; + friend oft::oft; + + // ************************************************* CONFIGURATION ************************************************* + + /// The address of the OApp + public inline fun OAPP_ADDRESS(): address { @oft } + + // *********************************************** END CONFIGURATION *********************************************** + + struct OAppStore has key { + contract_signer: ContractSigner, + admin: address, + peers: Table, + delegate: address, + enforced_options: Table>, + } + + /// Enforced Options are stored by the OApp by EID and a OApp-specific "message type" (u16) + struct EnforcedOptionsKey has store, copy, drop { eid: u32, msg_type: u16 } + + // =================================================== Call Ref =================================================== + + public(friend) fun call_ref(): CallRef acquires OAppStore { + contract_identity::make_call_ref(&store().contract_signer) + } + + public(friend) fun dynamic_call_ref(target: address, auth: vector): DynamicCallRef acquires OAppStore { + contract_identity::make_dynamic_call_ref(&store().contract_signer, target, auth) + } + + // =============================================== Enforced Options =============================================== + + public(friend) fun get_enforced_options(eid: u32, msg_type: u16): vector acquires OAppStore { + *table::borrow_with_default(&store().enforced_options, EnforcedOptionsKey { eid, msg_type }, &b"") + } + + public(friend) fun set_enforced_options(eid: u32, msg_type: u16, option: vector) acquires OAppStore { + table::upsert(&mut store_mut().enforced_options, EnforcedOptionsKey { eid, msg_type }, option) + } + + // ===================================================== Admin ==================================================== + + public(friend) fun get_admin(): address acquires OAppStore { store().admin } + + public(friend) fun set_admin(admin: address) acquires OAppStore { + store_mut().admin = admin; + } + + // ===================================================== Peers ==================================================== + + public(friend) fun has_peer(eid: u32): bool acquires OAppStore { + table::contains(&store().peers, eid) + } + + public(friend) fun get_peer(eid: u32): Bytes32 acquires OAppStore { + *table::borrow(&store().peers, eid) + } + + public(friend) fun set_peer(eid: u32, peer: Bytes32) acquires OAppStore { + table::upsert(&mut store_mut().peers, eid, peer) + } + + public(friend) fun remove_peer(eid: u32) acquires OAppStore { + table::remove(&mut store_mut().peers, eid); + } + + // =================================================== Delegate =================================================== + + public(friend) fun get_delegate(): address acquires OAppStore { store().delegate } + + public(friend) fun set_delegate(delegate: address) acquires OAppStore { + store_mut().delegate = delegate; + } + + // ===================================================== Misc ===================================================== + + /// Checks that a token is the native token + public(friend) fun is_native_token(token: &FungibleAsset): bool { + object_address(&fungible_asset::asset_metadata(token)) == @native_token_metadata_address + } + + inline fun store(): &OAppStore { borrow_global(OAPP_ADDRESS()) } + + inline fun store_mut(): &mut OAppStore { borrow_global_mut(OAPP_ADDRESS()) } + + // ================================================ Initialization ================================================ + + fun init_module(account: &signer) { + move_to(account, OAppStore { + contract_signer: create_contract_signer(account), + admin: @oft_admin, + peers: table::new(), + delegate: @0x0, + enforced_options: table::new(), + }); + } + + #[test_only] + public fun init_module_for_test() { + init_module(&std::account::create_signer_for_test(OAPP_ADDRESS())); + } +} diff --git a/examples/oft-aptos-move/sources/shared_oft/oft.move b/examples/oft-aptos-move/sources/shared_oft/oft.move new file mode 100644 index 000000000..c54fff645 --- /dev/null +++ b/examples/oft-aptos-move/sources/shared_oft/oft.move @@ -0,0 +1,418 @@ +/// This is the OFT interface that provides send, quote, and view functions for the OFT. +/// +/// The OFT developer should update the name of the implementation module in the configuration section of this module. +/// Other than that, this module generally does not need to be updated by the OFT developer. As much as possible, +/// customizations should be made in the OFT implementation module. +module oft::oft { + use std::coin::Coin; + use std::fungible_asset::{FungibleAsset, Metadata, metadata_from_asset}; + use std::object::{Object, object_address}; + use std::option::Option; + use std::primary_fungible_store; + use std::signer::address_of; + + use endpoint_v2::messaging_receipt::MessagingReceipt; + use endpoint_v2_common::bytes32::{Bytes32, to_bytes32}; + use endpoint_v2_common::contract_identity::{DynamicCallRef, get_dynamic_call_ref_caller}; + use oft::oapp_core::{Self, lz_quote, lz_send, lz_send_compose, refund_fees, withdraw_lz_fees}; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core; + use oft::oft_fa::{ + balance as balance_internal, + build_options, + credit, + debit_coin, + debit_fungible_asset, + debit_view as debit_view_internal, + deposit_coin, + inspect_message, + metadata as metadata_internal, + oft_limit_and_fees, + PlaceholderCoin, + send_standards_supported as send_standards_supported_internal, + withdraw_coin, + }; + use oft_common::oft_fee_detail::OftFeeDetail; + use oft_common::oft_limit::OftLimit; + + friend oft::oapp_receive; + + // ======================================== For FungibleAsset Enabled OFTs ======================================== + + /// This is called to send an amount in FungibleAsset to a recipient on another EID + public fun send( + call_ref: &DynamicCallRef, + dst_eid: u32, + to: Bytes32, + send_value: &mut FungibleAsset, + min_amount_ld: u64, + extra_options: vector, + compose_message: vector, + oft_cmd: vector, + native_fee: &mut FungibleAsset, + zro_fee: &mut Option, + ): (MessagingReceipt, OftReceipt) { + let sender = get_dynamic_call_ref_caller(call_ref, OAPP_ADDRESS(), b"send"); + send_internal( + sender, dst_eid, to, send_value, min_amount_ld, extra_options, compose_message, oft_cmd, native_fee, + zro_fee, + ) + } + + /// Send from an account to a recipient on another EID, deducting the fees and the amount to send from the sender's + /// account + public entry fun send_withdraw( + account: &signer, + dst_eid: u32, + to: vector, + amount_ld: u64, + min_amount_ld: u64, + extra_options: vector, + compose_message: vector, + oft_cmd: vector, + native_fee: u64, + zro_fee: u64, + ) { + // Withdraw the amount and fees from the account + assert!( + primary_fungible_store::balance(address_of(account), metadata()) >= amount_ld, + EINSUFFICIENT_BALANCE, + ); + let send_value = primary_fungible_store::withdraw(account, metadata(), amount_ld); + let (native_fee_fa, zro_fee_fa) = withdraw_lz_fees(account, native_fee, zro_fee); + let sender = address_of(move account); + + send_internal( + sender, dst_eid, to_bytes32(to), &mut send_value, min_amount_ld, extra_options, compose_message, oft_cmd, + &mut native_fee_fa, &mut zro_fee_fa, + ); + + // Return unused amounts and fees to the account + refund_fees(sender, native_fee_fa, zro_fee_fa); + primary_fungible_store::deposit(sender, send_value); + } + + fun send_internal( + sender: address, + dst_eid: u32, + to: Bytes32, + send_value: &mut FungibleAsset, + min_amount_ld: u64, + extra_options: vector, + compose_message: vector, + oft_cmd: vector, + native_fee: &mut FungibleAsset, + zro_fee: &mut Option, + ): (MessagingReceipt, OftReceipt) { + assert!(metadata_from_asset(send_value) == metadata(), EINVALID_METADATA); + let (messaging_receipt, amount_sent_ld, amount_received_ld) = oft_core::send( + sender, + dst_eid, + to, + compose_message, + |message, options| { + lz_send(dst_eid, message, options, native_fee, zro_fee) + }, + |_nothing| debit_fungible_asset(sender, send_value, min_amount_ld, dst_eid), + |amount_received_ld, message_type| build_options( + message_type, + dst_eid, + extra_options, + sender, + amount_received_ld, + to, + compose_message, + oft_cmd, + ), + |message, options| inspect_message(message, options, true), + ); + (messaging_receipt, OftReceipt { amount_sent_ld, amount_received_ld }) + } + + // ============================================= For Coin-enabled OFTs ============================================ + + /// This is called to send an amount in Coin to a recipient on another EID + public fun send_coin( + call_ref: &DynamicCallRef, + dst_eid: u32, + to: Bytes32, + send_value: &mut Coin, + min_amount_ld: u64, + extra_options: vector, + compose_message: vector, + oft_cmd: vector, + native_fee: &mut FungibleAsset, + zro_fee: &mut Option, + ): (MessagingReceipt, OftReceipt) { + let sender = get_dynamic_call_ref_caller(call_ref, OAPP_ADDRESS(), b"send_coin"); + send_coin_internal( + sender, dst_eid, to, send_value, min_amount_ld, extra_options, compose_message, oft_cmd, native_fee, + zro_fee, + ) + } + + /// Send from an amount to a recipient on another EID, deducting the fees and the amount from the sender's account + public entry fun send_withdraw_coin( + account: &signer, + dst_eid: u32, + to: vector, + amount_ld: u64, + min_amount_ld: u64, + extra_options: vector, + compose_message: vector, + oft_cmd: vector, + native_fee: u64, + zro_fee: u64, + ) { + // Withdraw the amount and fees from the account + let send_value = withdraw_coin(account, amount_ld); + let (native_fee_fa, zro_fee_fa) = withdraw_lz_fees(account, native_fee, zro_fee); + + let sender = address_of(move account); + + send_coin_internal( + sender, dst_eid, to_bytes32(to), &mut send_value, min_amount_ld, extra_options, compose_message, oft_cmd, + &mut native_fee_fa, &mut zro_fee_fa, + ); + + // Return unused amounts and fees back to the account + refund_fees(sender, native_fee_fa, zro_fee_fa); + deposit_coin(sender, send_value); + } + + /// This is called to send an amount in Coin to a recipient on another EID + fun send_coin_internal( + sender: address, + dst_eid: u32, + to: Bytes32, + send_value: &mut Coin, + min_amount_ld: u64, + extra_options: vector, + compose_message: vector, + oft_cmd: vector, + native_fee: &mut FungibleAsset, + zro_fee: &mut Option, + ): (MessagingReceipt, OftReceipt) { + let (messaging_receipt, amount_sent_ld, amount_received_ld) = oft_core::send( + sender, + dst_eid, + to, + compose_message, + |message, options| { + lz_send(dst_eid, message, options, native_fee, zro_fee) + }, + |_nothing| debit_coin(sender, send_value, min_amount_ld, dst_eid), + |amount_received_ld, message_type| build_options( + message_type, + dst_eid, + extra_options, + sender, + amount_received_ld, + to, + compose_message, + oft_cmd, + ), + |message, options| inspect_message(message, options, true), + ); + (messaging_receipt, OftReceipt { amount_sent_ld, amount_received_ld }) + } + + + // ===================================================== Quote ==================================================== + + #[view] + /// Quote the OFT for a particular send without sending + /// @return ( + /// oft_limit: The minimum and maximum limits that can be sent to the recipient + /// fees: The fees that will be applied to the amount sent + /// amount_sent_ld: The amount that would be debited from the sender in local decimals + /// amount_received_ld: The amount that would be received by the recipient in local decimals + /// ) + public fun quote_oft( + dst_eid: u32, + to: vector, + amount_ld: u64, + min_amount_ld: u64, + extra_options: vector, + compose_msg: vector, + oft_cmd: vector, + ): (OftLimit, vector, u64, u64) { + let (amount_sent_ld, amount_received_ld) = debit_view_internal(amount_ld, min_amount_ld, dst_eid); + + let (limit, fees) = oft_limit_and_fees( + dst_eid, + to, + amount_ld, + min_amount_ld, + extra_options, + compose_msg, + oft_cmd, + ); + (limit, fees, amount_sent_ld, amount_received_ld) + } + + #[view] + /// Quote the network fees for a particular send + /// @return (native_fee, zro_fee) + public fun quote_send( + user_sender: address, + dst_eid: u32, + to: vector, + amount_ld: u64, + min_amount_ld: u64, + extra_options: vector, + compose_message: vector, + oft_cmd: vector, + pay_in_zro: bool, + ): (u64, u64) { + oft_core::quote_send( + user_sender, + to, + compose_message, + |message, options| lz_quote(dst_eid, message, options, pay_in_zro), + |_nothing| debit_view_internal(amount_ld, min_amount_ld, dst_eid), + |amount_received_ld, message_type| build_options( + message_type, + dst_eid, + extra_options, + user_sender, + amount_received_ld, + to_bytes32(to), + compose_message, + oft_cmd, + ), + |message, options| inspect_message(message, options, false), + ) + } + + // ==================================================== Receive =================================================== + + public(friend) fun lz_receive_impl( + src_eid: u32, + _sender: Bytes32, + nonce: u64, + guid: Bytes32, + message: vector, + _extra_data: vector, + receive_value: Option, + ) { + oft_core::receive( + src_eid, + nonce, + guid, + message, + |to, index, message| lz_send_compose(to, index, guid, message), + |to, amount_ld| credit(to, amount_ld, src_eid, receive_value), + ); + } + + // ==================================================== Compose =================================================== + + public(friend) fun lz_compose_impl( + _from: address, + _guid: Bytes32, + _index: u16, + _message: vector, + _extra_data: vector, + _value: Option, + ) { + abort ECOMPOSE_NOT_IMPLEMENTED + } + + // =============================================== Ordered Execution ============================================== + + /// Provides the next nonce if executor options request ordered execution; returns 0 to indicate ordered execution + /// is disabled + public(friend) fun next_nonce_impl(_src_eid: u32, _sender: Bytes32): u64 { + 0 + } + + // ================================================== OFT Receipt ================================================= + + struct OftReceipt has drop, store { + amount_sent_ld: u64, + amount_received_ld: u64, + } + + public fun get_amount_sent_ld(receipt: &OftReceipt): u64 { receipt.amount_sent_ld } + + public fun get_amount_received_ld(receipt: &OftReceipt): u64 { receipt.amount_received_ld } + + public fun unpack_oft_receipt(receipt: &OftReceipt): (u64, u64) { + (receipt.amount_sent_ld, receipt.amount_received_ld) + } + + // ===================================================== View ===================================================== + + #[view] + public fun balance(account: address): u64 { + balance_internal(account) + } + + #[view] + /// The version of the OFT + /// @return (interface_id, protocol_version) + public fun oft_version(): (u64, u64) { + (1, 1) + } + + #[view] + public fun send_standards_supported(): vector> { + send_standards_supported_internal() + } + + #[view] + /// The address of the OFT token + public fun token(): address { + object_address(&metadata_internal()) + } + + #[view] + /// The metadata object of the OFT + public fun metadata(): Object { + metadata_internal() + } + + #[view] + public fun debit_view(amount_ld: u64, min_amount_ld: u64, _dst_eid: u32): (u64, u64) { + debit_view_internal(amount_ld, min_amount_ld, _dst_eid) + } + + #[view] + public fun to_ld(amount_sd: u64): u64 { oft_core::to_ld(amount_sd) } + + #[view] + public fun to_sd(amount_ld: u64): u64 { oft_core::to_sd(amount_ld) } + + #[view] + public fun remove_dust(amount_ld: u64): u64 { oft_core::remove_dust(amount_ld) } + + #[view] + public fun shared_decimals(): u8 { oft_core::shared_decimals() } + + #[view] + public fun decimal_conversion_rate(): u64 { oft_core::decimal_conversion_rate() } + + #[view] + /// Encode an OFT message + /// @return (message, message_type) + public fun encode_oft_msg( + sender: address, + amount_ld: u64, + to: vector, + compose_msg: vector, + ): (vector, u16) { + oft_core::encode_oft_msg(sender, amount_ld, to_bytes32(to), compose_msg) + } + + #[view] + public fun get_peer(eid: u32): vector { + oapp_core::get_peer(eid) + } + + // ================================================== Error Codes ================================================= + + const ECOMPOSE_NOT_IMPLEMENTED: u64 = 1; + const EINSUFFICIENT_BALANCE: u64 = 2; + const EINVALID_METADATA: u64 = 3; +} diff --git a/examples/oft-aptos-move/sources/shared_oft/oft_core.move b/examples/oft-aptos-move/sources/shared_oft/oft_core.move new file mode 100644 index 000000000..fad231564 --- /dev/null +++ b/examples/oft-aptos-move/sources/shared_oft/oft_core.move @@ -0,0 +1,293 @@ +/// This provides core OFT functionality. +/// +/// This module should generally not be modified by the OFT developer except to correct the friend declarations to +/// match the modules that are actually used. +module oft::oft_core { + use std::event::emit; + use std::math64::pow; + use std::vector; + + use endpoint_v2::messaging_receipt::{get_guid, MessagingReceipt}; + use endpoint_v2_common::bytes32::{Self, Bytes32, from_bytes32, to_bytes32}; + use oft::oft_store; + use oft_common::oft_compose_msg_codec; + use oft_common::oft_msg_codec; + + friend oft::oft; + friend oft::oft_impl_config; + friend oft::oapp_receive; + #[test_only] + friend oft::oft_core_tests; + #[test_only] + friend oft::oft_impl_config_tests; + + friend oft::oft_fa; + #[test_only] + friend oft::oft_fa_tests; + #[test_only] + friend oft::oapp_receive_using_oft_fa_tests; + + // ===================================================== OFT Core ================================================= + + /// Send a message to a destination endpoint using the provided send implementation and debit behavior. + /// + /// @param user_sender: The address of the user sending the message + /// @param dst_eid: The destination endpoint ID + /// @param to: The destination wallet address + /// @param compose_message: The compose message to be sent + /// @param send_impl: A function to send the message + /// |message, options| MessagingReceipt + /// @param debit: A function to debit the user account (unused field included to prevent IDE error) + /// |_unused_field| (sent_amount_ld, received_amount_ld) + /// @param build_options: A function to build the options for the message + /// |received_amount_ld, msg_type| options + /// @param inspect: A function to inspect the message and options before sending + /// |message, options| () + /// @return (messaging_receipt, amount_sent_ld, amount_received_ld) + public(friend) inline fun send( + user_sender: address, + dst_eid: u32, + to: Bytes32, + compose_payload: vector, + send_impl: |vector, vector| MessagingReceipt, + debit: |bool /*unused*/| (u64, u64), + build_options: |u64, u16| vector, + inspect: |&vector, &vector|, + ): (MessagingReceipt, u64, u64) { + let (amount_sent_ld, amount_received_ld) = debit(true /*unused*/); + + let msg_type = if (vector::length(&compose_payload) > 0) { SEND_AND_CALL() } else { SEND() }; + let options = build_options(amount_received_ld, msg_type); + + // Construct message and options + let (message, _) = encode_oft_msg(user_sender, amount_received_ld, to, compose_payload); + + // Hook to inspect the message and options before sending + inspect(&message, &options); + + // Send by endpoint + let messaging_receipt = send_impl(message, options); + + emit_oft_sent( + from_bytes32(get_guid(&messaging_receipt)), + dst_eid, + user_sender, + amount_sent_ld, + amount_received_ld, + ); + + (messaging_receipt, amount_sent_ld, amount_received_ld) + } + + /// Handle a received packet + /// @param src_eid: The source endpoint ID + /// @param nonce: The nonce of the message + /// @param guid: The GUID of the message + /// @param message: The message received + /// @param send_compose: A function to send the compose message + /// |to_address, index, message| () + /// @param credit: A function to credit the user account + /// |to_address, amount_received_ld| credited_amount_ld + public(friend) inline fun receive( + src_eid: u32, + nonce: u64, + guid: Bytes32, + message: vector, + send_compose: |address, u16, vector| (), + credit: |address, u64| u64, + ) { + // Decode the message using the OFT v2 codec + let to_address = bytes32::to_address(oft_msg_codec::send_to(&message)); + let message_amount_ld = to_ld(oft_msg_codec::amount_sd(&message)); + let has_compose = oft_msg_codec::has_compose(&message); + + // Credit the user account + let amount_received_ld = credit(to_address, message_amount_ld); + + // Send compose payload if present + if (has_compose) { + let compose_payload = oft_msg_codec::compose_payload(&message); + let compose_message = oft_compose_msg_codec::encode( + nonce, + src_eid, + amount_received_ld, + compose_payload, + ); + // In the default implementation the compose index is always 0; send_compose accepts the index parameter + // for extensibility + send_compose(to_address, 0, compose_message); + }; + emit_oft_received(from_bytes32(guid), src_eid, to_address, amount_received_ld); + } + + /// Get a quote for the network fee for sending a message to a destination endpoint + /// @param user_sender: The address of the user sending the message + /// @param to: The destination wallet address + /// @param compose_message: The compose message to be included + /// @param quote_impl: A function to get a quote from the LayerZero endpoint + /// @param debit_view: A function that provides the debit amounts (unused param only included to prevent Move IDE error) + /// |_unused_field| (sent_amount_ld, received_amount_ld) + /// @param build_options: A function to build the options for the message + /// |received_amount_ld, msg_type| options + /// @param inspect: A function to inspect the message and options before quoting + /// |message, options| () + /// @return (native_fee, lz_fee) + public(friend) inline fun quote_send( + user_sender: address, + to: vector, + compose_message: vector, + quote_impl: |vector, vector| (u64, u64), + debit_view: |bool /*unused*/| (u64, u64), + build_options: |u64, u16| vector, + inspect: |&vector, &vector|, + ): (u64, u64) { + let (_, amount_received_ld) = debit_view(true /*unused*/); + let (message, msg_type) = encode_oft_msg(user_sender, amount_received_ld, to_bytes32(to), compose_message); + let options = build_options(amount_received_ld, msg_type); + + // Hook to inspect the message and options before sending + inspect(&message, &options); + + quote_impl(message, options) + } + + + // ===================================================== Utils ==================================================== + + /// This is a debit view implementation that can be used by the OFT to compute the amount to be sent and received + /// with no fees + public(friend) fun no_fee_debit_view(amount_ld: u64, min_amount_ld: u64): (u64, u64) { + let amount_sent_ld = remove_dust(amount_ld); + let amount_received_ld = amount_sent_ld; + assert!(amount_received_ld >= min_amount_ld, ESLIPPAGE_EXCEEDED); + (amount_sent_ld, amount_received_ld) + } + + /// Encode an OFT message + public(friend) fun encode_oft_msg( + sender: address, + amount_ld: u64, + to: Bytes32, + compose_payload: vector, + ): (vector, u16) { + let encoded_msg = oft_msg_codec::encode( + to, + to_sd(amount_ld), + bytes32::from_address(sender), + compose_payload, + ); + let msg_type = if (!vector::is_empty(&compose_payload)) { SEND_AND_CALL() } else { SEND() }; + + (encoded_msg, msg_type) + } + + // =================================================== Viewable =================================================== + + /// Convert an amount from shared decimals to local decimals + public(friend) fun to_ld(amount_sd: u64): u64 { + amount_sd * oft_store::decimal_conversion_rate() + } + + /// Convert an amount from local decimals to shared decimals + public(friend) fun to_sd(amount_ld: u64): u64 { + amount_ld / oft_store::decimal_conversion_rate() + } + + /// Calculate an amount in local decimals minus the dust + public(friend) fun remove_dust(amount_ld: u64): u64 { + let decimal_conversion_rate = oft_store::decimal_conversion_rate(); + (amount_ld / decimal_conversion_rate) * decimal_conversion_rate + } + + /// Get the shared decimals for the OFT + public(friend) fun shared_decimals(): u8 { oft_store::shared_decimals() } + + /// Get the decimal conversion rate + /// This is the multiplier to convert a shared decimals to a local decimals representation + public(friend) fun decimal_conversion_rate(): u64 { oft_store::decimal_conversion_rate() } + + // ===================================================== Store ==================================================== + + public(friend) fun initialize(local_decimals: u8, shared_decimals: u8) { + assert!(shared_decimals <= local_decimals, EINVALID_LOCAL_DECIMALS); + let decimal_conversion_rate = pow(10, ((local_decimals - shared_decimals) as u64)); + oft_store::initialize(shared_decimals, decimal_conversion_rate); + } + + // ==================================================== Events ==================================================== + + #[event] + struct OftReceived has store, drop { + guid: vector, + src_eid: u32, + to_address: address, + amount_received_ld: u64, + } + + #[event] + struct OftSent has store, drop { + // GUID of the OFT message + guid: vector, + // Destination Endpoint ID + dst_eid: u32, + // Address of the sender on the src chain + from_address: address, + // Amount of tokens sent in local decimals + amount_sent_ld: u64, + // Amount of tokens received in local decimals + amount_received_ld: u64 + } + + public(friend) fun emit_oft_received( + guid: vector, + src_eid: u32, + to_address: address, + amount_received_ld: u64, + ) { + emit(OftReceived { guid, src_eid, to_address, amount_received_ld }); + } + + public(friend) fun emit_oft_sent( + guid: vector, + dst_eid: u32, + from_address: address, + amount_sent_ld: u64, + amount_received_ld: u64, + ) { + emit(OftSent { guid, dst_eid, from_address, amount_sent_ld, amount_received_ld }); + } + + #[test_only] + public fun oft_received_event( + guid: vector, + src_eid: u32, + to_address: address, + amount_received_ld: u64, + ): OftReceived { + OftReceived { guid, src_eid, to_address, amount_received_ld } + } + + #[test_only] + public fun oft_sent_event( + guid: vector, + dst_eid: u32, + from_address: address, + amount_sent_ld: u64, + amount_received_ld: u64, + ): OftSent { + OftSent { guid, dst_eid, from_address, amount_sent_ld, amount_received_ld } + } + + // =============================================== Shared Constants =============================================== + + // Message type for a message that does not contain a compose message + public inline fun SEND(): u16 { 1 } + + // Message type for a message that contains a compose message + public inline fun SEND_AND_CALL(): u16 { 2 } + + // ================================================== Error Codes ================================================= + + const EINVALID_LOCAL_DECIMALS: u64 = 1; + const ESLIPPAGE_EXCEEDED: u64 = 2; +} diff --git a/examples/oft-aptos-move/sources/shared_oft/oft_impl_config.move b/examples/oft-aptos-move/sources/shared_oft/oft_impl_config.move new file mode 100644 index 000000000..331983f97 --- /dev/null +++ b/examples/oft-aptos-move/sources/shared_oft/oft_impl_config.move @@ -0,0 +1,444 @@ +module oft::oft_impl_config { + use std::event::emit; + use std::math64::min; + use std::string::utf8; + use std::table::{Self, Table}; + use std::timestamp; + + use oft::oapp_core::get_admin; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core::{no_fee_debit_view, remove_dust}; + use oft_common::oft_fee_detail::{new_oft_fee_detail, OftFeeDetail}; + + #[test_only] + friend oft::oft_impl_config_tests; + + friend oft::oft_fa; + + struct Config has key { + fee_bps: u64, + fee_deposit_address: address, + blocklist_enabled: bool, + blocklist: Table, + rate_limit_by_eid: Table, + } + + const MAX_U64: u64 = 0xffffffffffffffff; + + // =============================================== Fee Configuration ============================================== + + // The maximum fee that can be set is 100% + const MAX_FEE_BPS: u64 = 10_000; + + /// Set the fee deposit address + /// This is where OFT fees collected are deposited + public(friend) fun set_fee_deposit_address(fee_deposit_address: address) acquires Config { + // The fee deposit address must exist as an account to prevent revert for Coin deposits + assert!(std::account::exists_at(fee_deposit_address), EINVALID_DEPOSIT_ADDRESS); + assert!(store().fee_deposit_address != fee_deposit_address, ESETTING_UNCHANGED); + store_mut().fee_deposit_address = fee_deposit_address; + emit(FeeDepositAddressSet { fee_deposit_address }); + } + + /// Get the fee deposit address + public(friend) fun fee_deposit_address(): address acquires Config { store().fee_deposit_address } + + /// Set the fee for the OFT + public(friend) fun set_fee_bps(fee_bps: u64) acquires Config { + assert!(fee_bps <= MAX_FEE_BPS, EINVALID_FEE); + assert!(fee_bps != store().fee_bps, ESETTING_UNCHANGED); + store_mut().fee_bps = fee_bps; + emit(FeeSet { fee_bps }); + } + + /// Get the fee for the OFT + public(friend) fun fee_bps(): u64 acquires Config { store().fee_bps } + + /// Calculate the amount sent and received after applying the fee + /// If there is a zero fee, untransferable dust is to be left in user's wallet + /// If there is a non-zero fee, untransferable dust is to be consumed as a fee + /// This is consistent with EVM fee-enabled OFTs vs non-fee OFTs (we match fee = 0 with non-fee OFT behavior) + public(friend) fun debit_view_with_possible_fee( + amount_ld: u64, + min_amount_ld: u64, + ): (u64, u64) acquires Config { + let fee_bps = store().fee_bps; + if (fee_bps == 0) { + // If there is no fee, the amount sent and received is simply the amount provided minus dust, which is left + // in the wallet + no_fee_debit_view(amount_ld, min_amount_ld) + } else { + // The amount sent is the amount provided. The excess dust is consumed as a "fee" even if the dust could be + // left in the wallet in order to provide a more predictable experience for the user + let amount_sent_ld = amount_ld; + + // Calculate the preliminary fee based on the amount provided; this may increase when dust is added to it. + // The actual fee is the amount sent - amount received, which is fee + dust removed + let preliminary_fee = ((((amount_ld as u128) * (fee_bps as u128)) / 10_000) as u64); + + // Compute the received amount first, which is the amount after fee and dust removal + let amount_received_ld = remove_dust(amount_ld - preliminary_fee); + + // Ensure the amount received is greater than the minimum amount + assert!(amount_received_ld >= min_amount_ld, ESLIPPAGE_EXCEEDED); + + (amount_sent_ld, amount_received_ld) + } + } + + /// Specify the fee details based the configured fee and the amount sent + public(friend) fun fee_details_with_possible_fee( + amount_ld: u64, + min_amount_ld: u64 + ): vector acquires Config { + let (amount_sent_ld, amount_received_ld) = debit_view_with_possible_fee(amount_ld, min_amount_ld); + let fee = amount_sent_ld - amount_received_ld; + if (fee != 0) { + vector[new_oft_fee_detail(fee, false, utf8(b"OFT Fee"))] + } else { + vector[] + } + } + + // ============================================ Blocklist Configuration =========================================== + + /// Permanently disable the ability to blocklist accounts + /// This will also effectively restore any previously blocked accounts to unblocked status (is_blocklisted() will + /// return false for all accounts) + /// This is a one-way operation, once this is called, the blocklist capability cannot be restored + public(friend) fun irrevocably_disable_blocklist() acquires Config { + assert!(store().blocklist_enabled, EBLOCKLIST_ALREADY_DISABLED); + store_mut().blocklist_enabled = false; + emit(BlocklistingDisabled {}) + } + + /// Check if the blocklisting capability is enabled + public(friend) fun can_blocklist(): bool acquires Config { + store().blocklist_enabled + } + + /// Add or remove an account from the blocklist + public(friend) fun set_blocklist(wallet: address, blocklist: bool) acquires Config { + assert!(store().blocklist_enabled, EBLOCKLIST_DISABLED); + assert!(is_blocklisted(wallet) != blocklist, ESETTING_UNCHANGED); + if (blocklist) { + table::upsert(&mut store_mut().blocklist, wallet, true) + } else { + table::remove(&mut store_mut().blocklist, wallet); + }; + emit(BlocklistSet { wallet, blocked: blocklist }); + } + + /// Check if an account is blocked + public(friend) fun is_blocklisted(wallet: address): bool acquires Config { + store().blocklist_enabled && *table::borrow_with_default(&store().blocklist, wallet, &false) + } + + /// Revert if an account is blocked + public(friend) fun assert_not_blocklisted(wallet: address) acquires Config { + assert!(!is_blocklisted(wallet), EADDRESS_BLOCKED); + } + + /// Provide the admin address and emit a BlockedAmountRedirected event if an account is blocked + /// This is to be used in conjunction with a deposit(to) call and provides the admin address instead of the + /// recipient address if recipient is blocklisted. This also emits a message to alert that blocklisted funds have + /// been received + public(friend) fun redirect_to_admin_if_blocklisted(recipient: address, amount_ld: u64): address acquires Config { + if (!is_blocklisted(recipient)) { + recipient + } else { + emit(BlockedAmountRedirected { + amount_ld, + blocked_address: recipient, + redirected_to: get_admin(), + }); + get_admin() + } + } + + // ======================================= Sending Rate Limit Configuration ======================================= + + struct RateLimit has store, drop, copy { + limit: u64, + window_seconds: u64, + in_flight_on_last_update: u64, + last_update: u64, + } + + /// Set the rate limit (local_decimals) and the window (seconds) at the current timestamp + /// The capacity of the rate limit increased by limit_ld/window_s until it reaches the limit and stays there + public(friend) fun set_rate_limit(dst_eid: u32, limit: u64, window_seconds: u64) acquires Config { + set_rate_limit_at_timestamp(dst_eid, limit, window_seconds, timestamp::now_seconds()); + } + + /// Set or update the rate limit for a given EID at a specified timestamp + public(friend) fun set_rate_limit_at_timestamp( + dst_eid: u32, + limit: u64, + window_seconds: u64, + timestamp: u64 + ) acquires Config { + assert!(window_seconds > 0, EINVALID_WINDOW); + + // If the rate limit is already set, checkpoint the in-flight amount before updating the rate limit. + if (has_rate_limit(dst_eid)) { + let (prior_limit, prior_window_seconds) = rate_limit_config(dst_eid); + assert!(limit != prior_limit || window_seconds != prior_window_seconds, ESETTING_UNCHANGED); + + // Checkpoint the in-flight amount before updating the rate settings. If this is not saved, it could change + // the in-flight calculation amount retroactively + checkpoint_rate_limit_in_flight(dst_eid, timestamp); + + let rate_limit_store = table::borrow_mut(&mut store_mut().rate_limit_by_eid, dst_eid); + rate_limit_store.limit = limit; + rate_limit_store.window_seconds = window_seconds; + emit(RateLimitUpdated { dst_eid, limit, window_seconds }); + } else { + table::upsert(&mut store_mut().rate_limit_by_eid, dst_eid, RateLimit { + limit, + window_seconds, + in_flight_on_last_update: 0, + last_update: timestamp, + }); + emit(RateLimitSet { dst_eid, limit, window_seconds }); + }; + } + + /// Unset the rate limit for a given EID + public(friend) fun unset_rate_limit(eid: u32) acquires Config { + assert!(table::contains(&store().rate_limit_by_eid, eid), ESETTING_UNCHANGED); + table::remove(&mut store_mut().rate_limit_by_eid, eid); + emit(RateLimitUnset { eid }); + } + + /// Checkpoint the in-flight amount for a given EID for the provided timestamp. + /// This should whenever there is a change in rate limit or before consuming rate limit capacity + public(friend) fun checkpoint_rate_limit_in_flight(eid: u32, timestamp: u64) acquires Config { + let inflight = in_flight_at_time(eid, timestamp); + let rate_limit = table::borrow_mut(&mut store_mut().rate_limit_by_eid, eid); + rate_limit.in_flight_on_last_update = inflight; + rate_limit.last_update = timestamp; + } + + + /// Check if a rate limit is set for a given EID + public(friend) fun has_rate_limit(eid: u32): bool acquires Config { + table::contains(&store().rate_limit_by_eid, eid) + } + + /// Get the rate limit and window (in seconds) for a given EID + public(friend) fun rate_limit_config(eid: u32): (u64, u64) acquires Config { + if (!has_rate_limit(eid)) { + (0, 0) + } else { + let rate_limit = *table::borrow(&store().rate_limit_by_eid, eid); + (rate_limit.limit, rate_limit.window_seconds) + } + } + + /// Get the in-flight amount for a given EID at present + public(friend) fun in_flight(eid: u32): u64 acquires Config { + in_flight_at_time(eid, timestamp::now_seconds()) + } + + /// Get the in-flight amount for a given EID. The in-flight count is the amount of the rate limit that has been + /// consumed linearly decayed to the provided timestamp + public(friend) fun in_flight_at_time(eid: u32, timestamp: u64): u64 acquires Config { + if (!has_rate_limit(eid)) { + 0 + } else { + let rate_limit = *table::borrow(&store().rate_limit_by_eid, eid); + if (timestamp > rate_limit.last_update) { + // If the timestamp is greater than the last update, calculate the decayed in-flight amount + let elapsed = min(timestamp - rate_limit.last_update, rate_limit.window_seconds); + let decay = ((((elapsed as u128) * (rate_limit.limit as u128)) / (rate_limit.window_seconds as u128)) as u64); + + // Ensure the decayed in-flight amount is not negative + if (decay < rate_limit.in_flight_on_last_update) { + rate_limit.in_flight_on_last_update - decay + } else { + 0 + } + } else { + // If not, return the unaltered in-flight amount at the last checkpoint + rate_limit.in_flight_on_last_update + } + } + } + + /// Calculate the spare rate limit capacity for a given EID at present + public(friend) fun rate_limit_capacity(eid: u32): u64 acquires Config { + rate_limit_capacity_at_time(eid, timestamp::now_seconds()) + } + + /// Calculate the spare rate limit capacity for a given EID at the proviced timestamp + public(friend) fun rate_limit_capacity_at_time(eid: u32, timestamp: u64): u64 acquires Config { + if (!has_rate_limit(eid)) { + return MAX_U64 + }; + let rate_limit = *table::borrow(&store().rate_limit_by_eid, eid); + if (rate_limit.limit > in_flight_at_time(eid, timestamp)) { + rate_limit.limit - in_flight_at_time(eid, timestamp) + } else { + 0 + } + } + + /// Consume rate limit capacity for a given EID or abort if the capacity is exceeded + public(friend) fun try_consume_rate_limit_capacity(eid: u32, amount: u64) acquires Config { + if (!has_rate_limit(eid)) return; + try_consume_rate_limit_capacity_at_time(eid, amount, timestamp::now_seconds()); + } + + /// Consume rate limit capacity for a given EID or abort if the capacity is exceeded at a provided timestamp + public(friend) fun try_consume_rate_limit_capacity_at_time( + eid: u32, + amount: u64, + timestamp: u64 + ) acquires Config { + checkpoint_rate_limit_in_flight(eid, timestamp); + let rate_limit = table::borrow_mut(&mut store_mut().rate_limit_by_eid, eid); + assert!(rate_limit.in_flight_on_last_update + amount <= rate_limit.limit, EEXCEEDED_RATE_LIMIT); + rate_limit.in_flight_on_last_update = rate_limit.in_flight_on_last_update + amount; + } + + /// Release rate limit capacity for a given EID + /// This is used to when wanting to rate limit by net inflow - outflow + /// This will release the capacity back to the rate limit up to the limit itself + public(friend) fun release_rate_limit_capacity(eid: u32, amount: u64) acquires Config { + if (!has_rate_limit(eid)) return; + + let rate_limit = table::borrow_mut(&mut store_mut().rate_limit_by_eid, eid); + if (amount >= rate_limit.in_flight_on_last_update) { + rate_limit.in_flight_on_last_update = 0; + } else { + rate_limit.in_flight_on_last_update = rate_limit.in_flight_on_last_update - amount; + } + } + + // ==================================================== Helpers =================================================== + + inline fun store(): &Config { borrow_global(OAPP_ADDRESS()) } + + inline fun store_mut(): &mut Config { borrow_global_mut(OAPP_ADDRESS()) } + + // ==================================================== Events ==================================================== + + #[event] + struct FeeDepositAddressSet has drop, store { + fee_deposit_address: address, + } + + #[event] + struct FeeSet has drop, store { + fee_bps: u64, + } + + #[event] + struct BlocklistingDisabled has drop, store {} + + #[event] + struct BlocklistSet has drop, store { + wallet: address, + blocked: bool, + } + + #[event] + struct BlockedAmountRedirected has drop, store { + amount_ld: u64, + blocked_address: address, + redirected_to: address, + } + + #[event] + struct RateLimitSet has drop, store { + dst_eid: u32, + limit: u64, + window_seconds: u64, + } + + #[event] + struct RateLimitUpdated has drop, store { + dst_eid: u32, + limit: u64, + window_seconds: u64, + } + + #[event] + struct RateLimitUnset has drop, store { + eid: u32, + } + + #[test_only] + public fun fee_deposit_address_set_event(fee_deposit_address: address): FeeDepositAddressSet { + FeeDepositAddressSet { fee_deposit_address } + } + + #[test_only] + public fun fee_set_event(fee_bps: u64): FeeSet { + FeeSet { fee_bps } + } + + #[test_only] + public fun blocklisting_disabled_event(): BlocklistingDisabled { + BlocklistingDisabled {} + } + + #[test_only] + public fun blocklist_set_event(wallet: address, blocked: bool): BlocklistSet { + BlocklistSet { wallet, blocked } + } + + #[test_only] + public fun blocked_amount_redirected_event( + amount_ld: u64, + blocked_address: address, + redirected_to: address + ): BlockedAmountRedirected { + BlockedAmountRedirected { amount_ld, blocked_address, redirected_to } + } + + #[test_only] + public fun rate_limit_set_event(dst_eid: u32, limit: u64, window_seconds: u64): RateLimitSet { + RateLimitSet { dst_eid, limit, window_seconds } + } + + #[test_only] + public fun rate_limit_updated_event(dst_eid: u32, limit: u64, window_seconds: u64): RateLimitUpdated { + RateLimitUpdated { dst_eid, limit, window_seconds } + } + + #[test_only] + public fun rate_limit_unset_event(eid: u32): RateLimitUnset { + RateLimitUnset { eid } + } + + // ================================================ Initialization ================================================ + + fun init_module(account: &signer) { + move_to(move account, Config { + fee_bps: 0, + fee_deposit_address: @oft_admin, + blocklist_enabled: true, + blocklist: table::new(), + rate_limit_by_eid: table::new(), + }); + } + + #[test_only] + public fun init_module_for_test() { + init_module(&std::account::create_signer_for_test(OAPP_ADDRESS())); + } + + // ================================================== Error Codes ================================================= + + const EADDRESS_BLOCKED: u64 = 1; + const EBLOCKLIST_ALREADY_DISABLED: u64 = 2; + const EBLOCKLIST_DISABLED: u64 = 3; + const EEXCEEDED_RATE_LIMIT: u64 = 4; + const EINVALID_DEPOSIT_ADDRESS: u64 = 5; + const EINVALID_FEE: u64 = 6; + const EINVALID_WINDOW: u64 = 7; + const ESETTING_UNCHANGED: u64 = 8; + const ESLIPPAGE_EXCEEDED: u64 = 9; +} diff --git a/examples/oft-aptos-move/sources/shared_oft/oft_store.move b/examples/oft-aptos-move/sources/shared_oft/oft_store.move new file mode 100644 index 000000000..57ccc9715 --- /dev/null +++ b/examples/oft-aptos-move/sources/shared_oft/oft_store.move @@ -0,0 +1,58 @@ +/// This module contains the general/internal store of the OFT OApp. +/// +/// This module should generally not be modified by the OFT/OApp developer. OFT specific data should be stored in the +/// implementation module. +module oft::oft_store { + use oft::oapp_store::OAPP_ADDRESS; + + friend oft::oft_core; + + /// The OFT configuration that is general to all OFT implementations + struct OftStore has key { + shared_decimals: u8, + decimal_conversion_rate: u64, + } + + /// Get the decimal conversion rate of the OFT, this is the multiplier to convert a shared decimals to a local + /// decimals representation + public(friend) fun decimal_conversion_rate(): u64 acquires OftStore { + store().decimal_conversion_rate + } + + /// Get the shared decimals of the OFT, this is the number of decimals that are preserved on wire transmission + public(friend) fun shared_decimals(): u8 acquires OftStore { + store().shared_decimals + } + + // ==================================================== Helpers =================================================== + + inline fun store(): &OftStore { borrow_global(OAPP_ADDRESS()) } + + inline fun store_mut(): &mut OftStore { borrow_global_mut(OAPP_ADDRESS()) } + + // ================================================ Initialization ================================================ + + public(friend) fun initialize(shared_decimals: u8, decimal_conversion_rate: u64) acquires OftStore { + // Prevent re-initialization; the caller computes the decimal conversion rate, which cannot be 0 + assert!(store().decimal_conversion_rate == 0, EALREADY_INITIALIZED); + + store_mut().shared_decimals = shared_decimals; + store_mut().decimal_conversion_rate = decimal_conversion_rate; + } + + fun init_module(account: &signer) { + move_to(account, OftStore { + shared_decimals: 0, + decimal_conversion_rate: 0, + }) + } + + #[test_only] + public fun init_module_for_test() { + init_module(&std::account::create_signer_for_test(OAPP_ADDRESS())); + } + + // ================================================== Error Codes ================================================= + + const EALREADY_INITIALIZED: u64 = 1; +} diff --git a/examples/oft-aptos-move/test/evm/foundry/MyOFT.t.sol b/examples/oft-aptos-move/test/evm/foundry/MyOFT.t.sol new file mode 100644 index 000000000..2f8841555 --- /dev/null +++ b/examples/oft-aptos-move/test/evm/foundry/MyOFT.t.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +// Mock imports +import { MyOFT } from "../../../contracts/MyOFT.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"; + +// OFT imports +import { IOFT, SendParam, OFTReceipt } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; +import { MessagingFee, MessagingReceipt } from "@layerzerolabs/oft-evm/contracts/OFTCore.sol"; +import { OFTMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTMsgCodec.sol"; +import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.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 MyOFTTest is TestHelperOz5 { + using OptionsBuilder for bytes; + + uint32 private aEid = 1; + uint32 private bEid = 2; + + MyOFT private aOFT; + MyOFT private bOFT; + + address private userA = makeAddr("userA"); + address private userB = makeAddr("userB"); + 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); + + aOFT = MyOFT( + _deployOApp(type(MyOFT).creationCode, abi.encode("aOFT", "aOFT", address(endpoints[aEid]), address(this))) + ); + + bOFT = MyOFT( + _deployOApp(type(MyOFT).creationCode, abi.encode("bOFT", "bOFT", address(endpoints[bEid]), address(this))) + ); + + // config and wire the ofts + address[] memory ofts = new address[](2); + ofts[0] = address(aOFT); + ofts[1] = address(bOFT); + this.wireOApps(ofts); + + // mint tokens + deal(address(aOFT), userA, initialBalance); + deal(address(bOFT), userB, initialBalance); + } + + function test_constructor() public { + assertEq(aOFT.owner(), address(this)); + assertEq(bOFT.owner(), address(this)); + + assertEq(aOFT.balanceOf(userA), initialBalance); + assertEq(bOFT.balanceOf(userB), initialBalance); + + assertEq(aOFT.token(), address(aOFT)); + assertEq(bOFT.token(), address(bOFT)); + } + + function test_send_oft() public { + uint256 tokensToSend = 1 ether; + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + SendParam memory sendParam = SendParam( + bEid, + addressToBytes32(userB), + tokensToSend, + tokensToSend, + options, + "", + "" + ); + MessagingFee memory fee = aOFT.quoteSend(sendParam, false); + + assertEq(aOFT.balanceOf(userA), initialBalance); + assertEq(bOFT.balanceOf(userB), initialBalance); + + vm.prank(userA); + aOFT.send{ value: fee.nativeFee }(sendParam, fee, payable(address(this))); + verifyPackets(bEid, addressToBytes32(address(bOFT))); + + assertEq(aOFT.balanceOf(userA), initialBalance - tokensToSend); + assertEq(bOFT.balanceOf(userB), initialBalance + tokensToSend); + } +} diff --git a/examples/oft-aptos-move/test/evm/mocks/ERC20Mock.sol b/examples/oft-aptos-move/test/evm/mocks/ERC20Mock.sol new file mode 100644 index 000000000..6ce17e418 --- /dev/null +++ b/examples/oft-aptos-move/test/evm/mocks/ERC20Mock.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract ERC20Mock is ERC20 { + constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {} + + function mint(address _to, uint256 _amount) public { + _mint(_to, _amount); + } +} diff --git a/examples/oft-aptos-move/test/evm/mocks/OFTComposerMock.sol b/examples/oft-aptos-move/test/evm/mocks/OFTComposerMock.sol new file mode 100644 index 000000000..fdd5c2426 --- /dev/null +++ b/examples/oft-aptos-move/test/evm/mocks/OFTComposerMock.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { IOAppComposer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; + +contract OFTComposerMock is IOAppComposer { + // default empty values for testing a lzCompose received message + address public from; + bytes32 public guid; + bytes public message; + address public executor; + bytes public extraData; + + function lzCompose( + address _from, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata /*_extraData*/ + ) external payable { + from = _from; + guid = _guid; + message = _message; + executor = _executor; + extraData = _message; + } +} diff --git a/examples/oft-aptos-move/test/evm/mocks/OFTMock.sol b/examples/oft-aptos-move/test/evm/mocks/OFTMock.sol new file mode 100644 index 000000000..cef8770f5 --- /dev/null +++ b/examples/oft-aptos-move/test/evm/mocks/OFTMock.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol"; +import { SendParam } from "@layerzerolabs/oft-evm/contracts/OFTCore.sol"; + +contract OFTMock is OFT { + constructor( + string memory _name, + string memory _symbol, + address _lzEndpoint, + address _delegate + ) Ownable(_delegate) OFT(_name, _symbol, _lzEndpoint, _delegate) {} + + function mint(address _to, uint256 _amount) public { + _mint(_to, _amount); + } + + // @dev expose internal functions for testing purposes + function debit( + uint256 _amountToSendLD, + uint256 _minAmountToCreditLD, + uint32 _dstEid + ) public returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { + return _debit(msg.sender, _amountToSendLD, _minAmountToCreditLD, _dstEid); + } + + function debitView( + uint256 _amountToSendLD, + uint256 _minAmountToCreditLD, + uint32 _dstEid + ) public view returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { + return _debitView(_amountToSendLD, _minAmountToCreditLD, _dstEid); + } + + function removeDust(uint256 _amountLD) public view returns (uint256 amountLD) { + return _removeDust(_amountLD); + } + + function toLD(uint64 _amountSD) public view returns (uint256 amountLD) { + return _toLD(_amountSD); + } + + function toSD(uint256 _amountLD) public view returns (uint64 amountSD) { + return _toSD(_amountLD); + } + + function credit(address _to, uint256 _amountToCreditLD, uint32 _srcEid) public returns (uint256 amountReceivedLD) { + return _credit(_to, _amountToCreditLD, _srcEid); + } + + function buildMsgAndOptions( + SendParam calldata _sendParam, + uint256 _amountToCreditLD + ) public view returns (bytes memory message, bytes memory options) { + return _buildMsgAndOptions(_sendParam, _amountToCreditLD); + } +} diff --git a/examples/oft-aptos-move/test/movement/internal_oapp/oapp_core_tests.move b/examples/oft-aptos-move/test/movement/internal_oapp/oapp_core_tests.move new file mode 100644 index 000000000..1f2f33f36 --- /dev/null +++ b/examples/oft-aptos-move/test/movement/internal_oapp/oapp_core_tests.move @@ -0,0 +1,213 @@ +#[test_only] +module oft::oapp_core_tests { + use std::account::create_account_for_test; + use std::account::create_signer_for_test; + use std::event::was_event_emitted; + use std::fungible_asset::{Self, FungibleAsset}; + use std::option; + use std::signer::address_of; + + use endpoint_v2::channels::packet_sent_event; + use endpoint_v2::messaging_receipt; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32::{Self, from_address, from_bytes32}; + use endpoint_v2_common::guid; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, initialize_native_token_for_test, + mint_native_token_for_test + }; + use endpoint_v2_common::packet_v1_codec; + use oft::oapp_core::{Self, pause_sending_set, set_pause_sending}; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core::{SEND, SEND_AND_CALL}; + + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32) { + // Test the send function + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + let oft_admin = &create_signer_for_test(@oft_admin); + initialize_native_token_for_test(); + oft::oapp_test_helper::init_oapp(); + oapp_core::set_peer(oft_admin, SRC_EID, from_bytes32(from_address(@1234))); + oapp_core::set_peer(oft_admin, DST_EID, from_bytes32(from_address(@4321))); + } + + #[test] + fun test_send_internal() { + setup(SRC_EID, DST_EID); + + let called_send = false; + let called_inspect = false; + assert!(!called_inspect && !called_send, 0); + + let native_fee = mint_native_token_for_test(100000); + let zro_fee = option::none(); + + // pause then unpause (to test to make sure unpause works) + set_pause_sending(&create_signer_for_test(@oft_admin), DST_EID, true); + assert!(was_event_emitted(&pause_sending_set(DST_EID, true)), 0); + + set_pause_sending(&create_signer_for_test(@oft_admin), DST_EID, false); + assert!(was_event_emitted(&pause_sending_set(DST_EID, false)), 0); + + let messaging_receipt = oapp_core::lz_send( + DST_EID, + b"oapp-message", + b"options", + &mut native_fee, + &mut zro_fee, + ); + + let expected_guid = guid::compute_guid( + 1, + SRC_EID, + bytes32::from_address(OAPP_ADDRESS()), + DST_EID, + bytes32::from_address(@4321), + ); + + assert!(messaging_receipt::get_guid(&messaging_receipt) == expected_guid, 0); + + // 0 fees in simple msglib + assert!(messaging_receipt::get_native_fee(&messaging_receipt) == 0, 1); + assert!(messaging_receipt::get_zro_fee(&messaging_receipt) == 0, 1); + // nothing removed + assert!(fungible_asset::amount(&native_fee) == 100000, 0); + + assert!(messaging_receipt::get_nonce(&messaging_receipt) == 1, 2); + + let packet = packet_v1_codec::new_packet_v1( + SRC_EID, + bytes32::from_address(OAPP_ADDRESS()), + DST_EID, + bytes32::from_address(@4321), + 1, + expected_guid, + b"oapp-message", + ); + assert!(was_event_emitted(&packet_sent_event( + packet, + b"options", + @simple_msglib, + )), 0); + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + } + + #[test] + #[expected_failure(abort_code = oft::oapp_core::ESEND_PAUSED)] + fun test_send_pause() { + setup(SRC_EID, DST_EID); + set_pause_sending(&create_signer_for_test(@oft_admin), DST_EID, true); + + let called_send = false; + let called_inspect = false; + assert!(!called_inspect && !called_send, 0); + + let native_fee = mint_native_token_for_test(100000); + let zro_fee = option::none(); + + assert!(was_event_emitted(&pause_sending_set(DST_EID, true)), 0); + + oapp_core::lz_send( + DST_EID, + b"oapp-message", + b"options", + &mut native_fee, + &mut zro_fee, + ); + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + } + + #[test] + fun test_quote_internal() { + setup(SRC_EID, DST_EID); + + let (native_fee, zro_fee) = oapp_core::lz_quote( + DST_EID, + b"oapp-message", + b"options", + false, + ); + assert!(native_fee == 0, 0); + assert!(zro_fee == 0, 1); + } + + #[test] + fun test_set_enforced_options() { + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + oapp_core::set_enforced_options(admin, SRC_EID, SEND(), x"0003aaaa"); + oapp_core::set_enforced_options(admin, DST_EID, SEND(), x"0003bbbc"); + oapp_core::set_enforced_options(admin, DST_EID, SEND(), x"000355"); + oapp_core::set_enforced_options(admin, DST_EID, SEND_AND_CALL(), x"000344"); + assert!(oapp_core::get_enforced_options(DST_EID, SEND()) == x"000355", 0); + assert!(oapp_core::get_enforced_options(DST_EID, SEND_AND_CALL()) == x"000344", 0); + assert!(oapp_core::get_enforced_options(SRC_EID, SEND()) == x"0003aaaa", 0); + } + + #[test] + fun test_combine_options() { + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + let enforced_options = x"0003aaaa"; + let options = x"0003bbbb"; + oapp_core::set_enforced_options(admin, DST_EID, SEND(), enforced_options); + // unrelated option below just to make sure it doesn't get overwritten + oapp_core::set_enforced_options(admin, DST_EID, SEND_AND_CALL(), x"0003235326"); + let combined = oapp_core::combine_options(DST_EID, SEND(), options); + assert!(combined == x"0003aaaabbbb", 0); + } + + #[test] + fun test_peers() { + // Test the send function + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + assert!(!oapp_core::has_peer(1111), 0); + oapp_core::set_peer(admin, 1111, from_bytes32(from_address(@1234))); + oapp_core::set_peer(admin, 2222, from_bytes32(from_address(@2345))); + assert!(oapp_core::has_peer(1111), 0); + assert!(oapp_core::has_peer(2222), 0); + assert!(oapp_core::get_peer_bytes32(1111) == from_address(@1234), 0); + assert!(oapp_core::get_peer_bytes32(2222) == from_address(@2345), 0); + } + + #[test] + fun test_delegate() { + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + let delegate = &create_signer_for_test(@2222); + oapp_core::set_delegate(admin, address_of(delegate)); + assert!(oapp_core::get_delegate() == address_of(delegate), 0); + + let delegate2 = &create_signer_for_test(@3333); + oapp_core::set_delegate(admin, address_of(delegate2)); + + oapp_core::skip(delegate2, SRC_EID, from_bytes32(from_address(@1234)), 1); + } +} diff --git a/examples/oft-aptos-move/test/movement/internal_oapp/oapp_test_helper.move b/examples/oft-aptos-move/test/movement/internal_oapp/oapp_test_helper.move new file mode 100644 index 000000000..9f0a827dc --- /dev/null +++ b/examples/oft-aptos-move/test/movement/internal_oapp/oapp_test_helper.move @@ -0,0 +1,12 @@ +#[test_only] +module oft::oapp_test_helper { + use oft::oapp_compose; + use oft::oapp_receive; + use oft::oapp_store; + + public fun init_oapp() { + oapp_store::init_module_for_test(); + oapp_receive::init_module_for_test(); + oapp_compose::init_module_for_test(); + } +} diff --git a/examples/oft-aptos-move/test/movement/internal_oft/oft_core_tests.move b/examples/oft-aptos-move/test/movement/internal_oft/oft_core_tests.move new file mode 100644 index 000000000..c0cd148be --- /dev/null +++ b/examples/oft-aptos-move/test/movement/internal_oft/oft_core_tests.move @@ -0,0 +1,522 @@ +#[test_only] +module oft::oft_core_tests { + use std::account::create_signer_for_test; + use std::event::was_event_emitted; + use std::fungible_asset; + use std::fungible_asset::FungibleAsset; + use std::option; + use std::string::utf8; + use std::vector; + + use endpoint_v2::endpoint; + use endpoint_v2::messaging_receipt; + use endpoint_v2::messaging_receipt::new_messaging_receipt_for_test; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32; + use endpoint_v2_common::bytes32::{from_address, from_bytes32}; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, initialize_native_token_for_test, + mint_native_token_for_test + }; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::packet_v1_codec::compute_payload_hash; + use endpoint_v2_common::zro_test_helpers::create_fa; + use oft::oapp_core; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core::{Self, oft_v1_compatibility_mode_set, SEND, SEND_AND_CALL, set_v1_compatibility_mode}; + use oft::oft_store; + use oft_common::oft_compose_msg_codec; + use oft_common::oft_msg_codec; + use oft_common::oft_v1_msg_codec::{Self, PT_SEND, PT_SEND_AND_CALL}; + + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32) { + // Test the send function + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + let oft_admin = &create_signer_for_test(@oft_admin); + initialize_native_token_for_test(); + let (_, metadata, _) = create_fa(b"ZRO"); + let local_decimals = fungible_asset::decimals(metadata); + oft::oapp_test_helper::init_oapp(); + + oft_store::init_module_for_test(); + oft_core::initialize(local_decimals, 6); + oapp_core::set_peer(oft_admin, SRC_EID, from_bytes32(from_address(@1234))); + oapp_core::set_peer(oft_admin, DST_EID, from_bytes32(from_address(@4321))); + } + + #[test] + fun test_send() { + setup(SRC_EID, DST_EID); + + let user_sender = @99; + let to = bytes32::from_address(@2001); + let native_fee = mint_native_token_for_test(100000); + let zro_fee = option::none(); + let compose_message = b"Hello"; + + let called_send = false; + let called_inspect = false; + assert!(!called_inspect && !called_send, 0); + + // Turn on and off v1 compatibility mode (to test turning off works) + set_v1_compatibility_mode(DST_EID, true); + set_v1_compatibility_mode(DST_EID, false); + + let (messaging_receipt, amount_sent_ld, amount_received_ld) = oft_core::send( + user_sender, + DST_EID, + to, + compose_message, + |message, options| { + called_send = true; + assert!(oft_msg_codec::has_compose(&message), 0); + assert!(oft_msg_codec::sender(&message) == from_address(user_sender), 1); + assert!(oft_msg_codec::send_to(&message) == to, 0); + assert!(oft_msg_codec::amount_sd(&message) == 40, 1); + assert!(options == b"options", 2); + + new_messaging_receipt_for_test( + from_address(@333), + 4, + 1111, + 2222, + ) + }, + |_unused| (5000, 4000), + |_amount_received_ld, _msg_type| b"options", + |message, options| { + called_inspect = true; + assert!(vector::length(message) > 0, 0); + assert!(*options == b"options", 0); + }, + ); + + assert!(called_send, 0); + assert!(called_inspect, 0); + + let (guid, nonce, native_fee_amount, zro_fee_amount) = messaging_receipt::unpack_messaging_receipt( + messaging_receipt, + ); + assert!(guid == from_address(@333), 0); + assert!(nonce == 4, 0); + assert!(native_fee_amount == 1111, 1); + assert!(zro_fee_amount == 2222, 2); + + assert!(amount_sent_ld == 5000, 3); + assert!(amount_received_ld == 4000, 4); + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + } + + #[test] + fun test_send_v1_compatibility() { + setup(SRC_EID, DST_EID); + + let user_sender = @99; + let to = bytes32::from_address(@2001); + let native_fee = mint_native_token_for_test(100000); + let zro_fee = option::none(); + + // Compose message not allowed in v1 compatibility mode + let compose_message = b""; + + let called_send = false; + let called_inspect = false; + assert!(!called_inspect && !called_send, 0); + + set_v1_compatibility_mode(DST_EID, true); + assert!(was_event_emitted(&oft_v1_compatibility_mode_set(DST_EID, true)), 1); + + let (messaging_receipt, amount_sent_ld, amount_received_ld) = oft_core::send( + user_sender, + DST_EID, + to, + compose_message, + |message, options| { + called_send = true; + assert!(!oft_v1_msg_codec::has_compose(&message), 0); + assert!(oft_v1_msg_codec::send_to(&message) == to, 0); + assert!(oft_v1_msg_codec::amount_sd(&message) == 40, 1); + assert!(options == b"options", 2); + + new_messaging_receipt_for_test( + from_address(@333), + 4, + 1111, + 2222, + ) + }, + |_unused| (5000, 4000), + |_amount_received_ld, _msg_type| b"options", + |message, options| { + called_inspect = true; + assert!(vector::length(message) > 0, 0); + assert!(*options == b"options", 0); + }, + ); + + assert!(called_send, 0); + assert!(called_inspect, 0); + + let (guid, nonce, native_fee_amount, zro_fee_amount) = messaging_receipt::unpack_messaging_receipt( + messaging_receipt, + ); + assert!(guid == from_address(@333), 0); + assert!(nonce == 4, 0); + assert!(native_fee_amount == 1111, 1); + assert!(zro_fee_amount == 2222, 2); + + assert!(amount_sent_ld == 5000, 3); + assert!(amount_received_ld == 4000, 4); + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + } + + #[test] + fun test_receive() { + setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let called_credit = false; + assert!(!called_credit, 1); + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"", + ); + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + ); + + oft_core::receive( + SRC_EID, + nonce, + guid, + message, + |_to, _index, _message| { + // should not be called + assert!(false, 0); + }, + |to_address, message_amount| { + called_credit = true; + + assert!(to_address == @0x2000, 0); + // Add 2 0s for (8 local decimals - 6 shared decimals) + assert!(message_amount == 12300, 1); + + 5000 + }, + ); + + assert!(called_credit, 2); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 5000, + )), 3); + } + + #[test] + fun test_receive_v1() { + setup(DST_EID, SRC_EID); + set_v1_compatibility_mode(SRC_EID, true); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let called_credit = false; + assert!(!called_credit, 1); + + let message = oft_v1_msg_codec::encode( + PT_SEND(), + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + 0, + b"", + ); + let sender = bytes32::from_address(@1234); + + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + ); + + oft_core::receive( + SRC_EID, + nonce, + guid, + message, + |_to, _index, _message| { + // should not be called + assert!(false, 0); + }, + |to_address, message_amount| { + called_credit = true; + + assert!(to_address == @0x2000, 0); + // Add 2 0s for (8 local decimals - 6 shared decimals) + assert!(message_amount == 12300, 1); + + 5000 + }, + ); + + assert!(called_credit, 2); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 5000, + )), 3); + } + + #[test] + fun test_receive_with_compose() { + setup(DST_EID, SRC_EID); + set_v1_compatibility_mode(SRC_EID, true); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let called_credit = 0; + assert!(called_credit == 0, 1); + + + let message = oft_v1_msg_codec::encode( + PT_SEND_AND_CALL(), + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + 50, + b"Hello", + ); + + // Composer must be registered + let to_address_account = &create_signer_for_test(@0x2000); + endpoint::register_composer(to_address_account, utf8(b"oft")); + + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + ); + + let called_compose = 0; + assert!(called_compose == 0, 0); + + oft_core::receive( + SRC_EID, + nonce, + guid, + message, + |to, index, message| { + called_compose = called_compose + 1; + assert!(to == @0x2000, 0); + assert!(index == 0, 1); + assert!(oft_compose_msg_codec::compose_payload_message(&message) == b"Hello", 2); + }, + |to_address, message_amount| { + called_credit = called_credit + 1; + + assert!(to_address == @0x2000, 0); + // Add 2 0s for (8 local decimals - 6 shared decimals) + assert!(message_amount == 12300, 1); + 12300 // message_amount + }, + ); + + assert!(called_compose == 1, 0); + assert!(called_credit == 1, 2); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 12300, + )), 3); + } + + #[test] + fun test_receive_with_compose_v1() { + setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let called_credit = 0; + assert!(called_credit == 0, 1); + + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"Hello", + ); + + // Composer must be registered + let to_address_account = &create_signer_for_test(@0x2000); + endpoint::register_composer(to_address_account, utf8(b"oft")); + + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + ); + + let called_compose = 0; + assert!(called_compose == 0, 0); + + oft_core::receive( + SRC_EID, + nonce, + guid, + message, + |to, index, message| { + called_compose = called_compose + 1; + assert!(to == @0x2000, 0); + assert!(index == 0, 1); + assert!(oft_compose_msg_codec::compose_payload_message(&message) == b"Hello", 2); + }, + |to_address, message_amount| { + called_credit = called_credit + 1; + + assert!(to_address == @0x2000, 0); + // Add 2 0s for (8 local decimals - 6 shared decimals) + assert!(message_amount == 12300, 1); + 12300 // message_amount + }, + ); + + assert!(called_compose == 1, 0); + assert!(called_credit == 1, 2); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 12300, + )), 3); + } + + + #[test] + fun test_no_fee_debit_view() { + setup(SRC_EID, DST_EID); + + let (sent, received) = oft_core::no_fee_debit_view(123456789, 200); + assert!(sent == received, 0); + // dust removed (last 2 digits cleared) + assert!(sent == 123456700, 1); + } + + #[test] + #[expected_failure(abort_code = oft::oft_core::ESLIPPAGE_EXCEEDED)] + fun test_no_fee_debit_view_fails_if_post_dust_remove_less_than_min() { + setup(SRC_EID, DST_EID); + + oft_core::no_fee_debit_view(99, 20); + } + + #[test] + fun test_encode_oft_msg() { + setup(SRC_EID, DST_EID); + + let (encoded, message_type) = oft_core::encode_oft_msg( + @0x12345678, + 123, + bytes32::from_address(@0x2000), + b"Hello", + ); + + assert!(message_type == SEND_AND_CALL(), 0); + + let expected_encoded = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + // dust removed and SD + 1, + bytes32::from_address(@0x12345678), + b"Hello", + ); + + assert!(encoded == expected_encoded, 1); + + // without compose + let (encoded, message_type) = oft_core::encode_oft_msg( + @0x12345678, + 123, + bytes32::from_address(@0x2000), + b"", + ); + + assert!(message_type == SEND(), 2); + + let expected_encoded = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + // dust removed and SD + 1, + bytes32::from_address(@0x12345678), + b"", + ); + + assert!(encoded == expected_encoded, 1); + } +} \ No newline at end of file diff --git a/examples/oft-aptos-move/test/movement/internal_oft/oft_impl_config_tests.move b/examples/oft-aptos-move/test/movement/internal_oft/oft_impl_config_tests.move new file mode 100644 index 000000000..e4dc78a71 --- /dev/null +++ b/examples/oft-aptos-move/test/movement/internal_oft/oft_impl_config_tests.move @@ -0,0 +1,220 @@ +#[test_only] +module oft::oft_impl_config_tests { + use std::string::utf8; + use std::vector; + use aptos_framework::event::was_event_emitted; + + use oft::oapp_store; + use oft::oft_core; + use oft::oft_impl_config::{ + Self, + assert_not_blocklisted, + blocked_amount_redirected_event, + debit_view_with_possible_fee, + fee_details_with_possible_fee, + fee_bps, + has_rate_limit, + in_flight_at_timestamp, + rate_limit_capacity_at_timestamp, + rate_limit_config, + rate_limit_set_event, + rate_limit_updated_event, + redirect_to_admin_if_blocklisted, + release_rate_limit_capacity, + set_blocklist, set_fee_bps, try_consume_rate_limit_capacity_at_timestamp, unset_rate_limit, + }; + use oft::oft_store; + use oft_common::oft_fee_detail; + + const MAX_U64: u64 = 0xffffffffffffffff; + + fun setup() { + oapp_store::init_module_for_test(); + oft_store::init_module_for_test(); + oft_impl_config::init_module_for_test(); + oft_core::initialize(8, 6); + } + + #[test] + fun test_set_fee_bps() { + setup(); + + // Set the fee to 10% + let fee_bps = 1000; + set_fee_bps(fee_bps); + assert!(fee_bps() == fee_bps, 1); + + let (sent, received) = debit_view_with_possible_fee(1234, 1000); + // Send amount should include dust if there is a fee + assert!(sent == 1234, 1); + // Received amount should be 90% of the sent amount with dust removed: 1234 - 120 = 1114 => 1100 (dust removed) + assert!(received == 1100, 2); + + let fee_details = fee_details_with_possible_fee(1234, 1000); + assert!(vector::length(&fee_details) == 1, 1); + let (fee, is_reward) = oft_fee_detail::fee_amount_ld(vector::borrow(&fee_details, 0)); + assert!(fee == 134, 1); + assert!(is_reward == false, 2); + assert!(oft_fee_detail::description(vector::borrow(&fee_details, 0)) == utf8(b"OFT Fee"), 2); + + // Set the fee to 0% + let fee_bps = 0; + set_fee_bps(fee_bps); + + let (sent, received) = debit_view_with_possible_fee(1234, 1000); + // Sent and amount should be 100% of the amount with dust removed with no fee: 1234 => 1200 (dust removed) + assert!(sent == 1200, 3); + assert!(received == 1200, 4); + + // Expect no fee details if there is no fee + let fee_details = fee_details_with_possible_fee(1234, 1000); + assert!(vector::length(&fee_details) == 0, 1); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EINVALID_FEE)] + fun test_set_fee_bps_invalid() { + setup(); + + // Set the fee to 101% + let fee_bps = 10100; + set_fee_bps(fee_bps); + } + + #[test] + fun test_set_blocklist() { + setup(); + + assert!(oft_impl_config::is_blocklisted(@0x1234) == false, 1); + assert_not_blocklisted(@0x1234); + let redirected_address = redirect_to_admin_if_blocklisted(@0x1234, 1111); + assert!(redirected_address == @0x1234, 2); + + set_blocklist(@0x1234, true); + assert!(oft_impl_config::is_blocklisted(@0x1234), 1); + let redirected_address = redirect_to_admin_if_blocklisted(@0x1234, 1111); + assert!(redirected_address == @oft_admin, 2); + assert!(was_event_emitted(&blocked_amount_redirected_event(1111, @0x1234, @oft_admin)), 3); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EADDRESS_BLOCKED)] + fun test_assert_not_blocked() { + setup(); + + set_blocklist(@0x1234, true); + assert_not_blocklisted(@0x1234); + } + + #[test] + fun set_rate_limit() { + setup(); + + // No rate limit configured + assert!(has_rate_limit(30100) == false, 2); + let (limit, window) = rate_limit_config(30100); + assert!(limit == 0 && window == 0, 1); + assert!(in_flight_at_timestamp(30100, 10) == 0, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 10) == MAX_U64, 2); + + // Configure rate limit (200/second) + oft_impl_config::set_rate_limit_at_timestamp(30100, 20000, 1000, 100); + assert!(was_event_emitted(&rate_limit_set_event(30100, 20000, 1000)), 1); + assert!(has_rate_limit(30100) == true, 2); + assert!(has_rate_limit(30200) == false, 2); // Different eid + let (limit, window) = rate_limit_config(30100); + assert!(limit == 20000 && window == 1000, 1); + assert!(in_flight_at_timestamp(30100, 100) == 0, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 100) == 20000, 2); + + // 100 seconds later + assert!(in_flight_at_timestamp(30100, 200) == 0, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 200) == 20000, 2); + + // consume 10% of the capacity + try_consume_rate_limit_capacity_at_timestamp(30100, 2000, 200); + assert!(in_flight_at_timestamp(30100, 200) == 2000, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 200) == 18000, 2); + + // 10 seconds later: in flight should decline by 20000/1000s * 10s = 200 + assert!(in_flight_at_timestamp(30100, 210) == 1800, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 210) == 18200, 2); + + // 20 seconds later: in flight should decline by 20000/1000s * 20s = 400 + assert!(in_flight_at_timestamp(30100, 220) == 1600, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 220) == 18400, 2); + + // update rate limit (300/second) + oft_impl_config::set_rate_limit_at_timestamp(30100, 30000, 1000, 220); + assert!(was_event_emitted(&rate_limit_updated_event(30100, 30000, 1000)), 1); + // in flight shouldn't change, but capacity should be updated with the new limit in mind + assert!(in_flight_at_timestamp(30100, 220) == 1600, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 220) == 28400, 2); + + // 10 seconds later: in flight should decline by 30000/1000s * 10s = 300 + assert!(in_flight_at_timestamp(30100, 230) == 1300, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 230) == 28700, 2); + + // 10 seconds later: in flight should decline by 30000/1000s * 10s = 300 + assert!(in_flight_at_timestamp(30100, 240) == 1000, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 240) == 29000, 2); + + // 100 seconds later: in flight should decline fully (without overshooting): 30000/1000s * 100s = 3000 + assert!(in_flight_at_timestamp(30100, 300) == 0, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 300) == 30000, 2); + + // Consume again + try_consume_rate_limit_capacity_at_timestamp(30100, 2000, 300); + try_consume_rate_limit_capacity_at_timestamp(30100, 2000, 310); + // 2000 + (2000 - 30000/1000*10) = 3800 + assert!(in_flight_at_timestamp(30100, 310) == 3700, 1); + + // Unset rate limit + unset_rate_limit(30100); + assert!(has_rate_limit(30100) == false, 2); + let (limit, window) = rate_limit_config(30100); + assert!(limit == 0 && window == 0, 1); + assert!(in_flight_at_timestamp(30100, 320) == 0, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 320) == MAX_U64, 2); + } + + #[test] + fun set_rate_limit_net() { + setup(); + + // No rate limit configured + assert!(has_rate_limit(30100) == false, 2); + let (limit, window) = rate_limit_config(30100); + assert!(limit == 0 && window == 0, 1); + assert!(in_flight_at_timestamp(30100, 10) == 0, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 10) == MAX_U64, 2); + + // Configure rate limit (200/second) + oft_impl_config::set_rate_limit_at_timestamp(30100, 20000, 1000, 100); + assert!(has_rate_limit(30100) == true, 2); + + // consume 100% of the capacity + try_consume_rate_limit_capacity_at_timestamp(30100, 20000, 0); + assert!(in_flight_at_timestamp(30100, 0) == 20000, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 0) == 0, 2); + + // 50 seconds later: in flight should decline by 20000/1000s * 500s = 10000 + assert!(in_flight_at_timestamp(30100, 500) == 10000, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 500) == 10000, 2); + + // release most of remaining capacity + release_rate_limit_capacity(30100, 9000); + assert!(in_flight_at_timestamp(30100, 500) == 1000, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 500) == 19000, 2); + + // consume all of remaining capacity + try_consume_rate_limit_capacity_at_timestamp(30100, 19000, 500); + assert!(in_flight_at_timestamp(30100, 500) == 20000, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 500) == 0, 2); + + // release excess capacity (5x limit) - should not overshoot + release_rate_limit_capacity(30100, 100_000); + assert!(in_flight_at_timestamp(30100, 500) == 0, 1); + assert!(rate_limit_capacity_at_timestamp(30100, 500) == 20000, 2); + } +} diff --git a/examples/oft-aptos-move/test/movement/oapp_receive_using_oft_fa_tests.move b/examples/oft-aptos-move/test/movement/oapp_receive_using_oft_fa_tests.move new file mode 100644 index 000000000..b6160c030 --- /dev/null +++ b/examples/oft-aptos-move/test/movement/oapp_receive_using_oft_fa_tests.move @@ -0,0 +1,165 @@ +#[test_only] +module oft::oapp_receive_using_oft_fa_tests { + use std::account::create_signer_for_test; + use std::event::was_event_emitted; + use std::string::utf8; + + use endpoint_v2::endpoint; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32::{Self, from_address, from_bytes32}; + use endpoint_v2_common::native_token_test_helpers::initialize_native_token_for_test; + use endpoint_v2_common::packet_v1_codec::{Self, compute_payload_hash}; + use oft::oapp_core; + use oft::oapp_receive; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core; + use oft::oft_impl; + use oft::oft_impl_config; + use oft::oft_store; + use oft_common::oft_compose_msg_codec; + use oft_common::oft_msg_codec; + + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32) { + // Test the send function + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + let oft_admin = &create_signer_for_test(@oft_admin); + initialize_native_token_for_test(); + oft::oapp_test_helper::init_oapp(); + + oft_store::init_module_for_test(); + oft_impl_config::init_module_for_test(); + oft_impl::init_module_for_test(); + oft_impl::initialize( + &create_signer_for_test(@oft_admin), + b"My Test Token", + b"MYT", + b"", + b"", + 6, + 8, + ); + oapp_core::set_peer(oft_admin, SRC_EID, from_bytes32(from_address(@1234))); + oapp_core::set_peer(oft_admin, DST_EID, from_bytes32(from_address(@4321))); + } + + #[test] + fun test_receive() { + setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"", + ); + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + ); + + + oapp_receive::lz_receive( + SRC_EID, + from_bytes32(sender), + nonce, + from_bytes32(guid), + message, + b"", + ); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 12300, + )), 3); + } + + #[test] + fun test_receive_with_compose() { + setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"Hello", + ); + + // Composer must be registered + let to_address_account = &create_signer_for_test(@0x2000); + endpoint::register_composer(to_address_account, utf8(b"composer")); + + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + ); + + + oapp_receive::lz_receive( + SRC_EID, + from_bytes32(sender), + nonce, + from_bytes32(guid), + message, + b"", + ); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 12300, + )), 3); + + let compose_message_part = oft_msg_codec::compose_payload(&message); + let expected_compose_message = oft_compose_msg_codec::encode( + nonce, + SRC_EID, + 12300, + compose_message_part, + ); + + // Compose Triggered to the same address + assert!(was_event_emitted(&endpoint_v2::messaging_composer::compose_sent_event( + OAPP_ADDRESS(), + @0x2000, + from_bytes32(guid), + 0, + expected_compose_message, + )), 0); + } +} \ No newline at end of file diff --git a/examples/oft-aptos-move/test/movement/oft_fa_tests.move b/examples/oft-aptos-move/test/movement/oft_fa_tests.move new file mode 100644 index 000000000..e33bce426 --- /dev/null +++ b/examples/oft-aptos-move/test/movement/oft_fa_tests.move @@ -0,0 +1,458 @@ +#[test_only] +module oft::oft_fa_tests { + use std::account::{create_account_for_test, create_signer_for_test}; + use std::event::was_event_emitted; + use std::fungible_asset::{Self, Metadata}; + use std::object::address_to_object; + use std::option; + use std::primary_fungible_store; + use std::string::utf8; + use std::timestamp; + use std::vector; + + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use oft::oapp_core; + use oft::oft_impl::{ + Self, fee_bps, fee_deposit_address, is_blocklisted, mint_tokens_for_test, set_fee_bps, set_fee_deposit_address, + }; + use oft::oft_impl_config; + use oft::oft_store; + use oft_common::oft_limit::new_unbounded_oft_limit; + + const MAXU64: u64 = 0xffffffffffffffff; + + const LOCAL_EID: u32 = 101; + + fun setup() { + setup_layerzero_for_test(@simple_msglib, LOCAL_EID, LOCAL_EID); + + oft::oapp_test_helper::init_oapp(); + + oft_store::init_module_for_test(); + oft_impl::init_module_for_test(); + oft_impl_config::init_module_for_test(); + oft_impl::initialize( + &create_signer_for_test(@oft_admin), + b"My Test Token", + b"MYT", + b"https://example.com/icon.png", + b"https://example.com/project", + 6, + 8, + ); + + assert!(fungible_asset::name(oft::oft::metadata()) == utf8(b"My Test Token"), 0); + assert!(fungible_asset::symbol(oft::oft::metadata()) == utf8(b"MYT"), 0); + assert!(fungible_asset::icon_uri(oft::oft::metadata()) == utf8(b"https://example.com/icon.png"), 0); + assert!(fungible_asset::project_uri(oft::oft::metadata()) == utf8(b"https://example.com/project"), 0); + assert!(fungible_asset::decimals(oft::oft::metadata()) == 8, 0); + } + + #[test] + fun test_debit() { + setup(); + + let dst_eid = 2u32; + // This configuration function (debit) is not resposible for handling dust, therefore the tested amount excludes + // the dust amount (last two digits) + let amount_ld = 123456700; + let min_amount_ld = 0u64; + + let fa = mint_tokens_for_test(amount_ld); + let (sent, received) = oft_impl::debit_fungible_asset( + @444, + &mut fa, + min_amount_ld, + dst_eid, + ); + + // amount sent and received should reflect the amount debited + assert!(sent == 123456700, 0); + assert!(received == 123456700, 0); + + // no remaining balance + let remaining_balance = fungible_asset::amount(&fa); + assert!(remaining_balance == 00, 0); + burn_token_for_test(fa); + } + + #[test] + fun test_credit() { + setup(); + + let amount_ld = 123456700; + let lz_receive_value = option::none(); + let src_eid = 12345; + + let to = @555; + create_account_for_test(to); + + // 0 balance before crediting + let balance = primary_fungible_store::balance(to, oft_impl::metadata()); + assert!(balance == 0, 0); + + let credited = oft_impl::credit( + to, + amount_ld, + src_eid, + lz_receive_value, + ); + // amount credited should reflect the amount credited + assert!(credited == 123456700, 0); + + // balance should appear in account + let balance = primary_fungible_store::balance(to, oft_impl::metadata()); + assert!(balance == 123456700, 0); + } + + #[test] + fun test_credit_with_extra_lz_receive_drop() { + setup(); + + let amount_ld = 123456700; + let lz_receive_value = option::some(mint_native_token_for_test(100)); + let src_eid = 12345; + + let to = @555; + create_account_for_test(to); + + // 0 balance before crediting + let balance = primary_fungible_store::balance(to, oft_impl::metadata()); + assert!(balance == 0, 0); + + oft_impl::credit( + to, + amount_ld, + src_eid, + lz_receive_value, + ); + + let native_token_metadata = address_to_object(@native_token_metadata_address); + assert!(primary_fungible_store::balance(@oft_admin, native_token_metadata) == 100, 1) + } + + #[test] + fun test_debit_view() { + setup(); + + // shouldn't take a fee + let (sent, received) = oft_impl::debit_view(123456700, 100, 2); + assert!(sent == 123456700, 0); + assert!(received == 123456700, 0); + } + + #[test] + #[expected_failure(abort_code = oft::oft_core::ESLIPPAGE_EXCEEDED)] + fun test_debit_view_fails_if_less_than_min() { + setup(); + + oft_impl::debit_view(32, 100, 2); + } + + #[test] + fun test_build_options() { + setup(); + let dst_eid = 103; + + let message_type = 2; + + let options = oft_impl::build_options( + message_type, + dst_eid, + // OKAY that it's not type 3 if no enforced options are set + x"1234", + @123, + 123324, + bytes32::from_address(@444), + x"8888", + x"34" + ); + // should pass through the options if none configured + assert!(options == x"1234", 0); + + let oft_admin = &create_signer_for_test(@oft_admin); + oapp_core::set_enforced_options( + oft_admin, + dst_eid, + message_type, + x"00037777" + ); + + let options = oft_impl::build_options( + message_type, + dst_eid, + x"00031234", + @123, + 123324, + bytes32::from_address(@444), + x"8888", + x"34" + ); + + // should append to configured options + assert!(options == x"000377771234", 0); + } + + #[test] + fun test_inspect_message() { + // doesn't do anything, just tests that it doesn't fail + oft_impl::inspect_message( + &x"1234", + &x"1234", + true, + ); + } + + #[test] + fun test_oft_limit_and_fees() { + setup(); + + timestamp::set_time_has_started_for_testing(&create_signer_for_test(@std)); + let (limit, fees) = oft_impl::oft_limit_and_fees( + 123, + x"1234", + 123, + 100, + x"1234", + x"1234", + x"1234" + ); + + // always unbounded and empty for this oft configuration + assert!(limit == new_unbounded_oft_limit(), 0); + assert!(vector::length(&fees) == 0, 0); + } + + #[test] + fun test_set_fee_bps() { + setup(); + + let oft_admin = &create_signer_for_test(@oft_admin); + let fee_bps = 500; // 5% + + set_fee_bps( + oft_admin, + fee_bps, + ); + + let fee_bps_result = fee_bps(); + assert!(fee_bps_result == fee_bps, 0); + + let (oft_limit, oft_fee_details) = oft_impl::oft_limit_and_fees( + 123, + x"1234", + 100_000_000, + 100, + x"1234", + x"1234", + x"1234" + ); + + // Check fee detail + assert!(vector::length(&oft_fee_details) == 1, 1); + let fee_detail = *vector::borrow(&oft_fee_details, 0); + let (fee_amount, is_reward) = oft_common::oft_fee_detail::fee_amount_ld(&fee_detail); + assert!(fee_amount == 5_000_000, 2); + assert!(is_reward == false, 3); + + // Check limit + assert!(oft_limit == new_unbounded_oft_limit(), 4); + + let deposit_address = @5555; + create_account_for_test(deposit_address); + set_fee_deposit_address( + oft_admin, + deposit_address, + ); + assert!(fee_deposit_address() == @5555, 1); + + // debit with fee + let dst_eid = 2u32; + // This configuration function (debit) is not resposible for handling dust, therefore the tested amount excludes + // the dust amount (last two digits) + let amount_ld = 123456700; + let min_amount_ld = 0u64; + + let fa = mint_tokens_for_test(amount_ld); + let (sent, received) = oft_impl::debit_fungible_asset( + @444, + &mut fa, + min_amount_ld, + dst_eid, + ); + + // amount sent and received should reflect the amount debited + assert!(sent == 123456700, 0); + // Any dust is also included in the fee + assert!(received == 117283800, 0); // 123456700 * 0.95 = 117283865 - remove dust -> 117283800 + + // no remaining balance + let remaining_balance = fungible_asset::amount(&fa); + assert!(remaining_balance == 00, 0); + burn_token_for_test(fa); + + // check that the fee was deposited + let fee_deposited = primary_fungible_store::balance(deposit_address, oft_impl::metadata()); + assert!(fee_deposited == 6172900, 0); // 123456700 - 117283800 = 6172900 + + // check the invariant that the total amount is conserved + assert!(received + fee_deposited == sent, 1); + } + + #[test] + fun test_set_blocklist_credit() { + setup(); + + let blocklisted_address = @0x1234; + assert!(is_blocklisted(blocklisted_address) == false, 0); + + let admin = &create_signer_for_test(@oft_admin); + oft_impl::set_blocklist( + admin, + blocklisted_address, + true, + ); + assert!(was_event_emitted(&oft_impl_config::blocklist_set_event(blocklisted_address, true)), 1); + assert!(is_blocklisted(blocklisted_address), 1); + + oft_impl::credit( + blocklisted_address, + 1234, + 12345, + option::none(), + ); + + assert!( + was_event_emitted(&oft_impl_config::blocked_amount_redirected_event(1234, blocklisted_address, @oft_admin)), + 2 + ); + + let admin_balance = primary_fungible_store::balance(@oft_admin, oft_impl::metadata()); + assert!(admin_balance == 1234, 3); + + oft_impl::set_blocklist( + admin, + blocklisted_address, + false, + ); + assert!(was_event_emitted(&oft_impl_config::blocklist_set_event(blocklisted_address, false)), 4); + assert!(is_blocklisted(blocklisted_address) == false, 5); + + oft_impl::credit( + blocklisted_address, + 1234, + 12345, + option::none(), + ); + + let to_balance = primary_fungible_store::balance(blocklisted_address, oft_impl::metadata()); + assert!(to_balance == 1234, 6); + + let admin_balance = primary_fungible_store::balance(@oft_admin, oft_impl::metadata()); + // unchanged + assert!(admin_balance == 1234, 7); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EADDRESS_BLOCKED)] + fun test_set_blocklist_debit() { + setup(); + + let blocklisted_address = @0x1234; + primary_fungible_store::deposit(blocklisted_address, oft_impl::mint_tokens_for_test(10_000)); + + assert!(is_blocklisted(blocklisted_address) == false, 0); + + let admin = &create_signer_for_test(@oft_admin); + oft_impl::set_blocklist( + admin, + blocklisted_address, + true, + ); + + let debit_tokens = mint_tokens_for_test(1234); + oft_impl::debit_fungible_asset( + blocklisted_address, + &mut debit_tokens, + 0, + 12345, + ); + burn_token_for_test(debit_tokens); + } + + #[test] + #[expected_failure(abort_code = 0x50003, location = std::fungible_asset)] + fun cannot_transfer_blocklisted_tokens() { + setup(); + + let blocklisted_address = @0x1234; + primary_fungible_store::deposit(blocklisted_address, oft_impl::mint_tokens_for_test(10_000)); + + assert!(is_blocklisted(blocklisted_address) == false, 0); + + let admin = &create_signer_for_test(@oft_admin); + oft_impl::set_blocklist( + admin, + blocklisted_address, + true, + ); + + primary_fungible_store::transfer( + &create_signer_for_test(blocklisted_address), + oft_impl::metadata(), + @9888, + 2000 + ); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EBLOCKLIST_DISABLED)] + fun test_disable_blocklist() { + setup(); + + let admin = &create_signer_for_test(@oft_admin); + oft_impl::irrevocably_disable_blocklist(admin); + assert!(was_event_emitted(&oft_impl_config::blocklisting_disabled_event()), 1); + + let blocklisted_address = @0x1234; + oft_impl::set_blocklist( + admin, + blocklisted_address, + true, + ); + } + + #[test] + fun test_rate_limit() { + setup(); + + let (limit, window) = oft_impl::rate_limit_config(30100); + assert!(limit == 0 && window == 0, 0); + + let admin = &create_signer_for_test(@oft_admin); + oft_impl::set_rate_limit(admin, 30100, 2500, 100); + assert!(was_event_emitted(&oft_impl_config::rate_limit_set_event(30100, 2500, 100)), 1); + + let (limit, window) = oft_impl::rate_limit_config(30100); + assert!(limit == 2500 && window == 100, 1); + + let (oft_limit, fee_detail) = oft_impl::oft_limit_and_fees( + 30100, + x"1234", + 123, + 100, + x"1234", + x"1234", + x"1234" + ); + + // no fee + assert!(vector::length(&fee_detail) == 0, 2); + + // rate limit + let (min_amount_ld, max_amount_ld) = oft_common::oft_limit::unpack_oft_limit(oft_limit); + assert!(min_amount_ld == 0 && max_amount_ld == 2500, 3); + } +} diff --git a/examples/oft-aptos-move/test/movement/oft_using_oft_fa_tests.move b/examples/oft-aptos-move/test/movement/oft_using_oft_fa_tests.move new file mode 100644 index 000000000..a1d4ee579 --- /dev/null +++ b/examples/oft-aptos-move/test/movement/oft_using_oft_fa_tests.move @@ -0,0 +1,305 @@ +// **Important** This module tests the behavior of OFT assuming that it is connected to OFT_FA. When connecting to +// a different template such as OFT_ADAPTER_FA or configuring the OFT module differently, the tests will no longer be +// valid. +#[test_only] +module oft::oft_using_oft_fa_tests { + use std::account::create_signer_for_test; + use std::fungible_asset; + use std::option; + use std::primary_fungible_store::{Self, balance}; + use std::signer::address_of; + use std::vector; + + use endpoint_v2::endpoint; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32::{Self, from_address, from_bytes32}; + use endpoint_v2_common::contract_identity::make_dynamic_call_ref_for_test; + use endpoint_v2_common::guid; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::packet_v1_codec::{Self, compute_payload}; + use oft::oapp_core::{set_pause_sending, set_peer}; + use oft::oapp_receive::lz_receive; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft::{ + debit_view, quote_oft, quote_send, remove_dust, send, send_withdraw, to_ld, to_sd, token, unpack_oft_receipt, + }; + use oft::oft_impl::{mint_tokens_for_test, set_fee_bps, set_rate_limit}; + use oft::oft_store; + use oft_common::oft_limit::{max_amount_ld, min_amount_ld}; + use oft_common::oft_msg_codec; + + const MAXU64: u64 = 0xffffffffffffffff; + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32) { + let oft_admin = &create_signer_for_test(@oft_admin); + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + oft::oapp_test_helper::init_oapp(); + + oft::oft_impl_config::init_module_for_test(); + oft_store::init_module_for_test(); + oft::oft_impl::init_module_for_test(); + oft::oft_impl::initialize( + oft_admin, + b"My Test Token", + b"MYT", + b"", + b"", + 6, + 8, + ); + + let remote_oapp = from_address(@2000); + set_peer(oft_admin, DST_EID, from_bytes32(remote_oapp)); + } + + #[test] + fun test_quote_oft() { + setup(SRC_EID, DST_EID); + + let receipient = from_address(@2000); + let amount_ld = 100u64 * 100_000_000; // 100 TOKEN + let compose_msg = vector[]; + let (limit, fees, amount_sent_ld, amount_received_ld) = quote_oft( + DST_EID, + from_bytes32(receipient), + amount_ld, + 0, + vector[], + compose_msg, + vector[] + ); + assert!(min_amount_ld(&limit) == 0, 0); + assert!(max_amount_ld(&limit) == MAXU64, 1); + assert!(vector::length(&fees) == 0, 2); + + assert!(amount_sent_ld == amount_ld, 3); + assert!(amount_received_ld == amount_ld, 3); + } + + #[test] + fun test_quote_send() { + setup(SRC_EID, DST_EID); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let (native_fee, zro_fee) = quote_send( + @1000, + DST_EID, + from_bytes32(from_address(@2000)), + amount, + amount, + vector[], + vector[], + vector[], + false, + ); + assert!(native_fee == 0, 0); + assert!(zro_fee == 0, 1); + } + + #[test] + fun test_send_fa() { + setup(SRC_EID, DST_EID); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_address(@5678); + let tokens = mint_tokens_for_test(amount); + + let native_fee = mint_native_token_for_test(10000000); + let zro_fee = option::none(); + send( + &make_dynamic_call_ref_for_test(address_of(alice), OAPP_ADDRESS(), b"send"), + DST_EID, + bob, + &mut tokens, + amount, + vector[], + vector[], + vector[], + &mut native_fee, + &mut zro_fee, + ); + assert!(fungible_asset::amount(&tokens) == 0, 1); // after send balance + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + burn_token_for_test(tokens); + } + + #[test] + fun test_send_fa_with_fee() { + setup(SRC_EID, DST_EID); + // 10% fee + set_fee_bps(&create_signer_for_test(@oft_admin), 1000); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_address(@5678); + let tokens = mint_tokens_for_test(amount); + + let native_fee = mint_native_token_for_test(10000000); + let zro_fee = option::none(); + let (_messaging_receipt, oft_receipt) = send( + &make_dynamic_call_ref_for_test(address_of(alice), OAPP_ADDRESS(), b"send"), + DST_EID, + bob, + &mut tokens, + 9_000_000, + vector[], + vector[], + vector[], + &mut native_fee, + &mut zro_fee, + ); + assert!(fungible_asset::amount(&tokens) == 0, 1); // after send balance + + let (sent, received) = unpack_oft_receipt(&oft_receipt); + assert!(sent == 10_000_000_000, 2); + assert!(received == 9_000_000_000, 2); + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + burn_token_for_test(tokens); + } + + #[test] + fun test_send() { + setup(SRC_EID, DST_EID); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + let tokens = mint_tokens_for_test(amount); + primary_fungible_store::deposit(address_of(alice), tokens); + assert!(balance(address_of(alice), oft::oft::metadata()) == amount, 0); // before send balance + + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + assert!(balance(address_of(alice), oft::oft::metadata()) == 0, 1); // after send balance + } + + #[test] + #[expected_failure(abort_code = oft::oapp_core::ESEND_PAUSED)] + fun test_send_paused() { + setup(SRC_EID, DST_EID); + set_pause_sending(&create_signer_for_test(@oft_admin), DST_EID, true); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + let tokens = mint_tokens_for_test(amount); + primary_fungible_store::deposit(address_of(alice), tokens); + assert!(balance(address_of(alice), oft::oft::metadata()) == amount, 0); // before send balance + + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EEXCEEDED_RATE_LIMIT)] + fun test_send_exceed_rate_limit() { + setup(SRC_EID, DST_EID); + set_rate_limit(&create_signer_for_test(@oft_admin), DST_EID, 19_000_000_000, 10); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + // 3x the required tokens + let tokens = mint_tokens_for_test(amount * 3); + primary_fungible_store::deposit(address_of(alice), tokens); + + // Succeeds: consumes 10_000_000_000 of 19_000_000_000 + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + // Fails (rate limit exceeded): consumes 20_000_000_000 of 19_000_000_000 + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + } + + #[test] + fun test_send_rate_limit_netted_by_receive() { + setup(SRC_EID, DST_EID); + set_rate_limit(&create_signer_for_test(@oft_admin), DST_EID, 19_000_000_000, 10); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + // 3x the required tokens + let tokens = mint_tokens_for_test(amount * 3); + primary_fungible_store::deposit(address_of(alice), tokens); + + // Succeeds: consumes 10_000_000_000 of 19_000_000_000 in flight + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + + // Receive 5_000_000_000: 5_000_000_000 of 19_000_000_000 in flight + let message = oft_msg_codec::encode( + from_address(@1234), + 5_000_000_000, + from_address(@0xffff), + b"", + ); + // Reverse the EIDs to simulate a receive from the remote OFT + let packet = packet_v1_codec::new_packet_v1( + DST_EID, + from_address(@2000), // remote OFT + SRC_EID, + from_address(@oft), + 1, + guid::compute_guid( + 1, + DST_EID, + from_address(@2000), + SRC_EID, + from_address(@oft), + ), + message, + ); + endpoint::verify( + @simple_msglib, + packet_raw::get_packet_bytes(packet_v1_codec::extract_header(&packet)), + from_bytes32(bytes32::keccak256(compute_payload( + packet_v1_codec::get_guid(&packet), + packet_v1_codec::get_message(&packet), + ))), + ); + lz_receive( + packet_v1_codec::get_src_eid(&packet), + from_bytes32(packet_v1_codec::get_sender(&packet)), + packet_v1_codec::get_nonce(&packet), + from_bytes32(packet_v1_codec::get_guid(&packet)), + message, + vector[], + ); + + + // Fails (rate limit exceeded): consumes 15_000_000_000 of 19_000_000_000 in flight + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + } + + #[test] + fun test_metadata_view_functions() { + setup(SRC_EID, DST_EID); + + // token is unknown at time of test - just calling to check it doesn't abort + token(); + + assert!(to_ld(100) == 10000, 0); + assert!(to_sd(100) == 1, 1); + assert!(remove_dust(123) == 100, 2); + + let (sent, received) = debit_view(1234, 0, DST_EID); + assert!(sent == 1200, 3); + assert!(received == 1200, 4); + } +} \ No newline at end of file diff --git a/examples/oft-aptos-move/tests/implementations/oft_fa_tests.move b/examples/oft-aptos-move/tests/implementations/oft_fa_tests.move new file mode 100644 index 000000000..99341edf5 --- /dev/null +++ b/examples/oft-aptos-move/tests/implementations/oft_fa_tests.move @@ -0,0 +1,583 @@ +#[test_only] +module oft::oft_fa_tests { + use std::account::{create_account_for_test, create_signer_for_test}; + use std::event::was_event_emitted; + use std::fungible_asset::{Self, Metadata}; + use std::object::address_to_object; + use std::option; + use std::primary_fungible_store; + use std::string::utf8; + use std::timestamp; + use std::vector; + + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use oft::oapp_core; + use oft::oft_fa::{ + Self, fee_bps, fee_deposit_address, is_blocklisted, mint_tokens_for_test, set_fee_bps, set_fee_deposit_address, + }; + use oft::oft_impl_config; + use oft::oft_store; + use oft_common::oft_limit::new_unbounded_oft_limit; + + const MAXU64: u64 = 0xffffffffffffffff; + + const LOCAL_EID: u32 = 101; + + fun setup() { + setup_layerzero_for_test(@simple_msglib, LOCAL_EID, LOCAL_EID); + + oft::oapp_test_helper::init_oapp(); + + oft_store::init_module_for_test(); + oft_fa::init_module_for_test(); + oft_impl_config::init_module_for_test(); + oft_fa::initialize( + &create_signer_for_test(@oft_admin), + b"My Test Token", + b"MYT", + b"https://example.com/icon.png", + b"https://example.com/project", + 6, + 8, + ); + + assert!(fungible_asset::name(oft::oft::metadata()) == utf8(b"My Test Token"), 0); + assert!(fungible_asset::symbol(oft::oft::metadata()) == utf8(b"MYT"), 0); + assert!(fungible_asset::icon_uri(oft::oft::metadata()) == utf8(b"https://example.com/icon.png"), 0); + assert!(fungible_asset::project_uri(oft::oft::metadata()) == utf8(b"https://example.com/project"), 0); + assert!(fungible_asset::decimals(oft::oft::metadata()) == 8, 0); + } + + #[test] + fun test_debit() { + setup(); + + let dst_eid = 2u32; + // This configuration function (debit) is not resposible for handling dust, therefore the tested amount excludes + // the dust amount (last two digits) + let amount_ld = 123456700; + let min_amount_ld = 0u64; + + let fa = mint_tokens_for_test(amount_ld); + let (sent, received) = oft_fa::debit_fungible_asset( + @444, + &mut fa, + min_amount_ld, + dst_eid, + ); + + // amount sent and received should reflect the amount debited + assert!(sent == 123456700, 0); + assert!(received == 123456700, 0); + + // no remaining balance + let remaining_balance = fungible_asset::amount(&fa); + assert!(remaining_balance == 00, 0); + burn_token_for_test(fa); + } + + #[test] + fun test_credit() { + setup(); + + let amount_ld = 123456700; + let lz_receive_value = option::none(); + let src_eid = 12345; + + let to = @555; + create_account_for_test(to); + + // 0 balance before crediting + let balance = primary_fungible_store::balance(to, oft_fa::metadata()); + assert!(balance == 0, 0); + + let credited = oft_fa::credit( + to, + amount_ld, + src_eid, + lz_receive_value, + ); + // amount credited should reflect the amount credited + assert!(credited == 123456700, 0); + + // balance should appear in account + let balance = primary_fungible_store::balance(to, oft_fa::metadata()); + assert!(balance == 123456700, 0); + } + + #[test] + fun test_supply() { + setup(); + + let supply = oft_fa::supply(); + assert!(supply == 0, 0); + + let amount_ld = 1000000000; + let lz_receive_value = option::none(); + let eid = 12345; + + let alice = @555; + let alice_signer = create_account_for_test(alice); + // will mint 1_000_000_000 tokens + oft_fa::credit( + alice, + amount_ld, + eid, + lz_receive_value, + ); + + let supply = oft_fa::supply(); + assert!(supply == 1000000000, 1); + + let amount_ld = 600000000; + let fa = primary_fungible_store::withdraw(&alice_signer, oft_fa::metadata(), amount_ld); + // will burn 600_000_000 tokens + oft_fa::debit_fungible_asset( + alice, + &mut fa, + 0, + eid, + ); + + let supply = oft_fa::supply(); + assert!(supply == 400000000, 2); + + burn_token_for_test(fa); + } + + #[test] + fun test_credit_with_extra_lz_receive_drop() { + setup(); + + let amount_ld = 123456700; + let lz_receive_value = option::some(mint_native_token_for_test(100)); + let src_eid = 12345; + + let to = @555; + create_account_for_test(to); + + // 0 balance before crediting + let balance = primary_fungible_store::balance(to, oft_fa::metadata()); + assert!(balance == 0, 0); + + oft_fa::credit( + to, + amount_ld, + src_eid, + lz_receive_value, + ); + + let native_token_metadata = address_to_object(@native_token_metadata_address); + assert!(primary_fungible_store::balance(@oft_admin, native_token_metadata) == 100, 1) + } + + #[test] + fun test_debit_view() { + setup(); + + // shouldn't take a fee + let (sent, received) = oft_fa::debit_view(123456700, 100, 2); + assert!(sent == 123456700, 0); + assert!(received == 123456700, 0); + } + + #[test] + #[expected_failure(abort_code = oft::oft_core::ESLIPPAGE_EXCEEDED)] + fun test_debit_view_fails_if_less_than_min() { + setup(); + + oft_fa::debit_view(32, 100, 2); + } + + #[test] + fun test_build_options() { + setup(); + let dst_eid = 103; + + let message_type = 2; + + let options = oft_fa::build_options( + message_type, + dst_eid, + // OKAY that it's not type 3 if no enforced options are set + x"1234", + @123, + 123324, + bytes32::from_address(@444), + x"8888", + x"34" + ); + // should pass through the options if none configured + assert!(options == x"1234", 0); + + let oft_admin = &create_signer_for_test(@oft_admin); + oapp_core::set_enforced_options( + oft_admin, + dst_eid, + message_type, + x"00037777" + ); + + let options = oft_fa::build_options( + message_type, + dst_eid, + x"00031234", + @123, + 123324, + bytes32::from_address(@444), + x"8888", + x"34" + ); + + // should append to configured options + assert!(options == x"000377771234", 0); + } + + #[test] + fun test_inspect_message() { + // doesn't do anything, just tests that it doesn't fail + oft_fa::inspect_message( + &x"1234", + &x"1234", + true, + ); + } + + #[test] + fun test_oft_limit_and_fees() { + setup(); + + timestamp::set_time_has_started_for_testing(&create_signer_for_test(@std)); + let (limit, fees) = oft_fa::oft_limit_and_fees( + 123, + x"1234", + 123, + 100, + x"1234", + x"1234", + x"1234" + ); + + // always unbounded and empty for this oft configuration + assert!(limit == new_unbounded_oft_limit(), 0); + assert!(vector::length(&fees) == 0, 0); + } + + #[test] + fun test_set_fee_bps() { + setup(); + + let oft_admin = &create_signer_for_test(@oft_admin); + let fee_bps = 500; // 5% + + set_fee_bps( + oft_admin, + fee_bps, + ); + + let fee_bps_result = fee_bps(); + assert!(fee_bps_result == fee_bps, 0); + + let (oft_limit, oft_fee_details) = oft_fa::oft_limit_and_fees( + 123, + x"1234", + 100_000_000, + 100, + x"1234", + x"1234", + x"1234" + ); + + // Check fee detail + assert!(vector::length(&oft_fee_details) == 1, 1); + let fee_detail = *vector::borrow(&oft_fee_details, 0); + let (fee_amount, is_reward) = oft_common::oft_fee_detail::fee_amount_ld(&fee_detail); + assert!(fee_amount == 5_000_000, 2); + assert!(is_reward == false, 3); + + // Check limit + assert!(oft_limit == new_unbounded_oft_limit(), 4); + + let deposit_address = @5555; + create_account_for_test(deposit_address); + set_fee_deposit_address( + oft_admin, + deposit_address, + ); + assert!(fee_deposit_address() == @5555, 1); + + // debit with fee + let dst_eid = 2u32; + // This configuration function (debit) is not resposible for handling dust, therefore the tested amount excludes + // the dust amount (last two digits) + let amount_ld = 123456700; + let min_amount_ld = 0u64; + + let fa = mint_tokens_for_test(amount_ld); + let (sent, received) = oft_fa::debit_fungible_asset( + @444, + &mut fa, + min_amount_ld, + dst_eid, + ); + + // amount sent and received should reflect the amount debited + assert!(sent == 123456700, 0); + // Any dust is also included in the fee + assert!(received == 117283800, 0); // 123456700 * 0.95 = 117283865 - remove dust -> 117283800 + + // no remaining balance + let remaining_balance = fungible_asset::amount(&fa); + assert!(remaining_balance == 00, 0); + burn_token_for_test(fa); + + // check that the fee was deposited + let fee_deposited = primary_fungible_store::balance(deposit_address, oft_fa::metadata()); + assert!(fee_deposited == 6172900, 0); // 123456700 - 117283800 = 6172900 + + // check the invariant that the total amount is conserved + assert!(received + fee_deposited == sent, 1); + } + + #[test] + fun test_set_blocklist_credit() { + setup(); + + let blocklisted_address = @0x1234; + assert!(is_blocklisted(blocklisted_address) == false, 0); + + let admin = &create_signer_for_test(@oft_admin); + oft_fa::set_blocklist( + admin, + blocklisted_address, + true, + ); + assert!(was_event_emitted(&oft_impl_config::blocklist_set_event(blocklisted_address, true)), 1); + assert!(is_blocklisted(blocklisted_address), 1); + + oft_fa::credit( + blocklisted_address, + 1234, + 12345, + option::none(), + ); + + assert!( + was_event_emitted(&oft_impl_config::blocked_amount_redirected_event(1234, blocklisted_address, @oft_admin)), + 2 + ); + + let admin_balance = primary_fungible_store::balance(@oft_admin, oft_fa::metadata()); + assert!(admin_balance == 1234, 3); + + oft_fa::set_blocklist( + admin, + blocklisted_address, + false, + ); + assert!(was_event_emitted(&oft_impl_config::blocklist_set_event(blocklisted_address, false)), 4); + assert!(is_blocklisted(blocklisted_address) == false, 5); + + oft_fa::credit( + blocklisted_address, + 1234, + 12345, + option::none(), + ); + + let to_balance = primary_fungible_store::balance(blocklisted_address, oft_fa::metadata()); + assert!(to_balance == 1234, 6); + + let admin_balance = primary_fungible_store::balance(@oft_admin, oft_fa::metadata()); + // unchanged + assert!(admin_balance == 1234, 7); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EADDRESS_BLOCKED)] + fun test_set_blocklist_debit() { + setup(); + + let blocklisted_address = @0x1234; + primary_fungible_store::deposit(blocklisted_address, oft_fa::mint_tokens_for_test(10_000)); + + assert!(is_blocklisted(blocklisted_address) == false, 0); + + let admin = &create_signer_for_test(@oft_admin); + oft_fa::set_blocklist( + admin, + blocklisted_address, + true, + ); + + let debit_tokens = mint_tokens_for_test(1234); + oft_fa::debit_fungible_asset( + blocklisted_address, + &mut debit_tokens, + 0, + 12345, + ); + burn_token_for_test(debit_tokens); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EBLOCKLIST_DISABLED)] + fun test_disable_blocklist() { + setup(); + + let admin = &create_signer_for_test(@oft_admin); + oft_fa::irrevocably_disable_blocklist(admin); + assert!(was_event_emitted(&oft_impl_config::blocklisting_disabled_event()), 1); + + let blocklisted_address = @0x1234; + oft_fa::set_blocklist( + admin, + blocklisted_address, + true, + ); + } + + #[test] + fun test_rate_limit() { + setup(); + + let (limit, window) = oft_fa::rate_limit_config(30100); + assert!(limit == 0 && window == 0, 0); + + let admin = &create_signer_for_test(@oft_admin); + oft_fa::set_rate_limit(admin, 30100, 2500, 100); + assert!(was_event_emitted(&oft_impl_config::rate_limit_set_event(30100, 2500, 100)), 1); + + let (limit, window) = oft_fa::rate_limit_config(30100); + assert!(limit == 2500 && window == 100, 1); + + let (oft_limit, fee_detail) = oft_fa::oft_limit_and_fees( + 30100, + x"1234", + 123, + 100, + x"1234", + x"1234", + x"1234" + ); + + // no fee + assert!(vector::length(&fee_detail) == 0, 2); + + // rate limit + let (min_amount_ld, max_amount_ld) = oft_common::oft_limit::unpack_oft_limit(oft_limit); + assert!(min_amount_ld == 0 && max_amount_ld == 2500, 3); + } + + #[test] + #[expected_failure(abort_code = 0x50003, location = std::fungible_asset)] + fun test_freeze_primary_fungible_store() { + setup(); + create_account_for_test(@0x1234); + let admin = &create_account_for_test(@oft_admin); + + // Set up initial state + primary_fungible_store::deposit(@0x1234, oft_fa::mint_tokens_for_test(100)); + assert!(primary_fungible_store::balance(@0x1234, oft::oft::metadata()) == 100, 1); + + // Freeze the store + assert!(!oft_fa::is_primary_fungible_store_frozen(@0x1234), 2); + oft_fa::set_primary_fungible_store_frozen(admin, @0x1234, true); + assert!(oft_fa::is_primary_fungible_store_frozen(@0x1234), 3); + + // Try to deposit tokens - should fail + let tokens = oft_fa::mint_tokens_for_test(50); + primary_fungible_store::deposit(@0x1234, tokens); + } + + #[test] + #[expected_failure(abort_code = 0x50003, location = std::fungible_asset)] + fun test_freeze_fungible_store() { + setup(); + create_account_for_test(@0x1234); + let admin = &create_account_for_test(@oft_admin); + + // Set up initial state + let store = primary_fungible_store::ensure_primary_store_exists(@0x1234, oft::oft::metadata()); + fungible_asset::deposit(store, oft_fa::mint_tokens_for_test(100)); + + // Freeze the store + oft_fa::set_fungible_store_frozen(admin, store, true); + assert!(oft_fa::is_primary_fungible_store_frozen(@0x1234), 0); + + // Try to deposit tokens - should fail + let tokens = oft_fa::mint_tokens_for_test(50); + primary_fungible_store::deposit(@0x1234, tokens); + } + + #[test] + #[expected_failure(abort_code = oft::oft_fa::EFREEZE_FUNGIBLE_STORE_DISABLED)] + fun test_freeze_primary_store_should_fail_when_disabled() { + setup(); + create_account_for_test(@0x1234); + let admin = &create_account_for_test(@oft_admin); + oft_fa::permanently_disable_fungible_store_freezing(admin); + assert!(was_event_emitted(&oft_fa::fungible_store_freezing_permanently_disabled_event()), 1); + oft_fa::set_primary_fungible_store_frozen(admin, @0x1234, true); + } + + #[test] + #[expected_failure(abort_code = oft::oft_fa::EFREEZE_FUNGIBLE_STORE_DISABLED)] + fun test_freeze_fungible_store_should_fail_when_disabled() { + setup(); + create_account_for_test(@0x1234); + let admin = &create_account_for_test(@oft_admin); + oft_fa::permanently_disable_fungible_store_freezing(admin); + assert!(was_event_emitted(&oft_fa::fungible_store_freezing_permanently_disabled_event()), 1); + let store = primary_fungible_store::ensure_primary_store_exists(@0x1234, oft::oft::metadata()); + oft_fa::set_fungible_store_frozen(admin, store, true); + } + + #[test] + fun test_unfreeze_primary_fungible_store_should_work_even_if_freeze_disabled() { + setup(); + create_account_for_test(@0x1234); + let admin = &create_account_for_test(@oft_admin); + + // Set up initial state + primary_fungible_store::deposit(@0x1234, oft_fa::mint_tokens_for_test(100)); + assert!(primary_fungible_store::balance(@0x1234, oft::oft::metadata()) == 100, 1); + + // Freeze the store + assert!(!oft_fa::is_primary_fungible_store_frozen(@0x1234), 2); + oft_fa::set_primary_fungible_store_frozen(admin, @0x1234, true); + assert!(oft_fa::is_primary_fungible_store_frozen(@0x1234), 3); + + oft_fa::permanently_disable_fungible_store_freezing(admin); + + // Unfreeze the store + oft_fa::set_primary_fungible_store_frozen(admin, @0x1234, false); + assert!(!oft_fa::is_primary_fungible_store_frozen(@0x1234), 4); + + // Try to deposit tokens - should succeed + let tokens = oft_fa::mint_tokens_for_test(50); + primary_fungible_store::deposit(@0x1234, tokens); + assert!(primary_fungible_store::balance(@0x1234, oft::oft::metadata()) == 150, 5); + } + + #[test] + fun test_unfreeze_store_should_work_even_if_freeze_disabled() { + setup(); + create_account_for_test(@0x1234); + let admin = &create_account_for_test(@oft_admin); + + let store = primary_fungible_store::ensure_primary_store_exists(@0x1234, oft::oft::metadata()); + fungible_asset::deposit(store, oft_fa::mint_tokens_for_test(100)); + + oft_fa::set_fungible_store_frozen(admin, store, true); + oft_fa::permanently_disable_fungible_store_freezing(admin); + oft_fa::set_fungible_store_frozen(admin, store, false); + + // Try to deposit tokens - should succeed + let tokens = oft_fa::mint_tokens_for_test(50); + primary_fungible_store::deposit(@0x1234, tokens); + assert!(primary_fungible_store::balance(@0x1234, oft::oft::metadata()) == 150, 5); + } +} diff --git a/examples/oft-aptos-move/tests/oapp_receive_using_oft_fa_tests.move b/examples/oft-aptos-move/tests/oapp_receive_using_oft_fa_tests.move new file mode 100644 index 000000000..a775c181e --- /dev/null +++ b/examples/oft-aptos-move/tests/oapp_receive_using_oft_fa_tests.move @@ -0,0 +1,167 @@ +#[test_only] +module oft::oapp_receive_using_oft_fa_tests { + use std::account::create_signer_for_test; + use std::event::was_event_emitted; + use std::string::utf8; + + use endpoint_v2::endpoint; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32::{Self, from_address, from_bytes32}; + use endpoint_v2_common::native_token_test_helpers::initialize_native_token_for_test; + use endpoint_v2_common::packet_v1_codec::{Self, compute_payload_hash}; + use oft::oapp_core; + use oft::oapp_receive; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core; + use oft::oft_fa; + use oft::oft_impl_config; + use oft::oft_store; + use oft_common::oft_compose_msg_codec; + use oft_common::oft_msg_codec; + + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32) { + // Test the send function + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + let oft_admin = &create_signer_for_test(@oft_admin); + initialize_native_token_for_test(); + oft::oapp_test_helper::init_oapp(); + + oft_store::init_module_for_test(); + oft_impl_config::init_module_for_test(); + oft_fa::init_module_for_test(); + oft_fa::initialize( + &create_signer_for_test(@oft_admin), + b"My Test Token", + b"MYT", + b"", + b"", + 6, + 8, + ); + oapp_core::set_peer(oft_admin, SRC_EID, from_bytes32(from_address(@1234))); + oapp_core::set_peer(oft_admin, DST_EID, from_bytes32(from_address(@4321))); + } + + #[test] + fun test_receive() { + setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"", + ); + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + b"" + ); + + + oapp_receive::lz_receive( + SRC_EID, + from_bytes32(sender), + nonce, + from_bytes32(guid), + message, + b"", + ); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 12300, + )), 3); + } + + #[test] + fun test_receive_with_compose() { + setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"Hello", + ); + + // Composer must be registered + let to_address_account = &create_signer_for_test(@0x2000); + endpoint::register_composer(to_address_account, utf8(b"composer")); + + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + b"" + ); + + + oapp_receive::lz_receive( + SRC_EID, + from_bytes32(sender), + nonce, + from_bytes32(guid), + message, + b"", + ); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 12300, + )), 3); + + let compose_message_part = oft_msg_codec::compose_payload(&message); + let expected_compose_message = oft_compose_msg_codec::encode( + nonce, + SRC_EID, + 12300, + compose_message_part, + ); + + // Compose Triggered to the same address + assert!(was_event_emitted(&endpoint_v2::messaging_composer::compose_sent_event( + OAPP_ADDRESS(), + @0x2000, + from_bytes32(guid), + 0, + expected_compose_message, + )), 0); + } +} \ No newline at end of file diff --git a/examples/oft-aptos-move/tests/oft_using_oft_fa_tests.move b/examples/oft-aptos-move/tests/oft_using_oft_fa_tests.move new file mode 100644 index 000000000..73f850a6f --- /dev/null +++ b/examples/oft-aptos-move/tests/oft_using_oft_fa_tests.move @@ -0,0 +1,339 @@ +// **Important** This module tests the behavior of OFT assuming that it is connected to OFT_FA. When connecting to +// a different template such as OFT_ADAPTER_FA or configuring the OFT module differently, the tests will no longer be +// valid. +#[test_only] +module oft::oft_using_oft_fa_tests { + use std::account::{create_account_for_test, create_signer_for_test}; + use std::fungible_asset; + use std::option; + use std::primary_fungible_store::{Self, balance}; + use std::signer::address_of; + use std::vector; + + use endpoint_v2::endpoint; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32::{Self, from_address, from_bytes32}; + use endpoint_v2_common::contract_identity::make_dynamic_call_ref_for_test; + use endpoint_v2_common::guid; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::packet_v1_codec::{Self, compute_payload}; + use oft::oapp_core::set_peer; + use oft::oapp_receive::lz_receive; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft::{ + debit_view, quote_oft, quote_send, remove_dust, send, send_withdraw, to_ld, to_sd, token, unpack_oft_receipt, + }; + use oft::oft_fa::{mint_tokens_for_test, set_fee_bps, set_rate_limit}; + use oft::oft_store; + use oft_common::oft_limit::{max_amount_ld, min_amount_ld}; + use oft_common::oft_msg_codec; + + const MAXU64: u64 = 0xffffffffffffffff; + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32) { + let oft_admin = &create_signer_for_test(@oft_admin); + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + oft::oapp_test_helper::init_oapp(); + + oft::oft_impl_config::init_module_for_test(); + oft_store::init_module_for_test(); + oft::oft_fa::init_module_for_test(); + oft::oft_fa::initialize( + oft_admin, + b"My Test Token", + b"MYT", + b"", + b"", + 6, + 8, + ); + + let remote_oapp = from_address(@2000); + set_peer(oft_admin, DST_EID, from_bytes32(remote_oapp)); + } + + #[test] + fun test_quote_oft() { + setup(SRC_EID, DST_EID); + + let receipient = from_address(@2000); + let amount_ld = 100u64 * 100_000_000; // 100 TOKEN + let compose_msg = vector[]; + let (limit, fees, amount_sent_ld, amount_received_ld) = quote_oft( + DST_EID, + from_bytes32(receipient), + amount_ld, + 0, + vector[], + compose_msg, + vector[] + ); + assert!(min_amount_ld(&limit) == 0, 0); + assert!(max_amount_ld(&limit) == MAXU64, 1); + assert!(vector::length(&fees) == 0, 2); + + assert!(amount_sent_ld == amount_ld, 3); + assert!(amount_received_ld == amount_ld, 3); + } + + #[test] + fun test_quote_send() { + setup(SRC_EID, DST_EID); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let (native_fee, zro_fee) = quote_send( + @1000, + DST_EID, + from_bytes32(from_address(@2000)), + amount, + amount, + vector[], + vector[], + vector[], + false, + ); + assert!(native_fee == 0, 0); + assert!(zro_fee == 0, 1); + } + + #[test] + fun test_send_fa() { + setup(SRC_EID, DST_EID); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_address(@5678); + let tokens = mint_tokens_for_test(amount); + + let native_fee = mint_native_token_for_test(10000000); + let zro_fee = option::none(); + send( + &make_dynamic_call_ref_for_test(address_of(alice), OAPP_ADDRESS(), b"send"), + DST_EID, + bob, + &mut tokens, + amount, + vector[], + vector[], + vector[], + &mut native_fee, + &mut zro_fee, + ); + assert!(fungible_asset::amount(&tokens) == 0, 1); // after send balance + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + burn_token_for_test(tokens); + } + + #[test] + fun test_send_fa_with_fee() { + setup(SRC_EID, DST_EID); + // 10% fee + set_fee_bps(&create_signer_for_test(@oft_admin), 1000); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_address(@5678); + let tokens = mint_tokens_for_test(amount); + + let native_fee = mint_native_token_for_test(10000000); + let zro_fee = option::none(); + let (_messaging_receipt, oft_receipt) = send( + &make_dynamic_call_ref_for_test(address_of(alice), OAPP_ADDRESS(), b"send"), + DST_EID, + bob, + &mut tokens, + 9_000_000, + vector[], + vector[], + vector[], + &mut native_fee, + &mut zro_fee, + ); + assert!(fungible_asset::amount(&tokens) == 0, 1); // after send balance + + let (sent, received) = unpack_oft_receipt(&oft_receipt); + assert!(sent == 10_000_000_000, 2); + assert!(received == 9_000_000_000, 2); + + // Fee transferred to admin + assert!(balance(@oft_admin, oft::oft::metadata()) == 1_000_000_000, 3); // fee balance + + // send again with different deposit address + create_account_for_test(@9999); + oft::oft_fa::set_fee_deposit_address(&create_signer_for_test(@oft_admin), @9999); + + burn_token_for_test(tokens); + + let tokens = mint_tokens_for_test(amount); + + let (_messaging_receipt, oft_receipt) = send( + &make_dynamic_call_ref_for_test(address_of(alice), OAPP_ADDRESS(), b"send"), + DST_EID, + bob, + &mut tokens, + 9_000_000, + vector[], + vector[], + vector[], + &mut native_fee, + &mut zro_fee, + ); + assert!(fungible_asset::amount(&tokens) == 0, 1); // after send balance + + let (sent, received) = unpack_oft_receipt(&oft_receipt); + assert!(sent == 10_000_000_000, 2); + assert!(received == 9_000_000_000, 2); + + // Admin balance unchanged + assert!(balance(@oft_admin, oft::oft::metadata()) == 1_000_000_000, 3); // fee balance + // Fee transferred to new deposit address + assert!(balance(@9999, oft::oft::metadata()) == 1_000_000_000, 3); // fee balance + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + burn_token_for_test(tokens); + } + + #[test] + fun test_send() { + setup(SRC_EID, DST_EID); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + let tokens = mint_tokens_for_test(amount); + primary_fungible_store::deposit(address_of(alice), tokens); + assert!(balance(address_of(alice), oft::oft::metadata()) == amount, 0); // before send balance + + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + assert!(balance(address_of(alice), oft::oft::metadata()) == 0, 1); // after send balance + } + + #[test] + #[expected_failure(abort_code = oft::oft::EINSUFFICIENT_BALANCE)] + fun test_send_fails_if_insufficient_balance() { + setup(SRC_EID, DST_EID); + + let amount = 100u64 * 100_000_000; // 100 TOKENS + let amount_balance = 50u64 * 100_000_000; // 50 TOKENS + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + let tokens = mint_tokens_for_test(amount_balance); + primary_fungible_store::deposit(address_of(alice), tokens); + assert!(balance(address_of(alice), oft::oft::metadata()) == amount_balance, 0); // before send balance + + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EEXCEEDED_RATE_LIMIT)] + fun test_send_exceed_rate_limit() { + setup(SRC_EID, DST_EID); + set_rate_limit(&create_signer_for_test(@oft_admin), DST_EID, 19_000_000_000, 10); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + // 3x the required tokens + let tokens = mint_tokens_for_test(amount * 3); + primary_fungible_store::deposit(address_of(alice), tokens); + + // Succeeds: consumes 10_000_000_000 of 19_000_000_000 + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + // Fails (rate limit exceeded): consumes 20_000_000_000 of 19_000_000_000 + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + } + + #[test] + fun test_send_rate_limit_netted_by_receive() { + setup(SRC_EID, DST_EID); + set_rate_limit(&create_signer_for_test(@oft_admin), DST_EID, 19_000_000_000, 10); + + let amount = 100u64 * 100_000_000; // 100 TOKEN + let alice = &create_signer_for_test(@1234); + let fa = mint_native_token_for_test(100_000_000); // mint 1 APT to alice + primary_fungible_store::deposit(address_of(alice), fa); + let bob = from_bytes32(from_address(@5678)); + // 3x the required tokens + let tokens = mint_tokens_for_test(amount * 3); + primary_fungible_store::deposit(address_of(alice), tokens); + + // Succeeds: consumes 10_000_000_000 of 19_000_000_000 in flight + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + + // Receive 5_000_000_000: 5_000_000_000 of 19_000_000_000 in flight + let message = oft_msg_codec::encode( + from_address(@1234), + 5_000_000_000, + from_address(@0xffff), + b"", + ); + // Reverse the EIDs to simulate a receive from the remote OFT + let packet = packet_v1_codec::new_packet_v1( + DST_EID, + from_address(@2000), // remote OFT + SRC_EID, + from_address(@oft), + 1, + guid::compute_guid( + 1, + DST_EID, + from_address(@2000), + SRC_EID, + from_address(@oft), + ), + message, + ); + endpoint::verify( + @simple_msglib, + packet_raw::get_packet_bytes(packet_v1_codec::extract_header(&packet)), + from_bytes32(bytes32::keccak256(compute_payload( + packet_v1_codec::get_guid(&packet), + packet_v1_codec::get_message(&packet), + ))), + b"" + ); + lz_receive( + packet_v1_codec::get_src_eid(&packet), + from_bytes32(packet_v1_codec::get_sender(&packet)), + packet_v1_codec::get_nonce(&packet), + from_bytes32(packet_v1_codec::get_guid(&packet)), + message, + vector[], + ); + + // Succeeds: consumes 15_000_000_000 of 19_000_000_000 in flight + send_withdraw(alice, DST_EID, bob, amount, amount, vector[], vector[], vector[], 0, 0); + } + + #[test] + fun test_metadata_view_functions() { + setup(SRC_EID, DST_EID); + + // token is unknown at time of test - just calling to check it doesn't abort + token(); + + assert!(to_ld(100) == 10000, 0); + assert!(to_sd(100) == 1, 1); + assert!(remove_dust(123) == 100, 2); + + let (sent, received) = debit_view(1234, 0, DST_EID); + assert!(sent == 1200, 3); + assert!(received == 1200, 4); + } +} \ No newline at end of file diff --git a/examples/oft-aptos-move/tests/shared_oapp/oapp_core_tests.move b/examples/oft-aptos-move/tests/shared_oapp/oapp_core_tests.move new file mode 100644 index 000000000..a8f21fdf1 --- /dev/null +++ b/examples/oft-aptos-move/tests/shared_oapp/oapp_core_tests.move @@ -0,0 +1,269 @@ +#[test_only] +module oft::oapp_core_tests { + use std::account::create_account_for_test; + use std::account::create_signer_for_test; + use std::event::was_event_emitted; + use std::fungible_asset::{Self, FungibleAsset}; + use std::option; + use std::primary_fungible_store; + use std::signer::address_of; + + use endpoint_v2::channels::packet_sent_event; + use endpoint_v2::messaging_receipt; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32::{Self, from_address, from_bytes32}; + use endpoint_v2_common::guid; + use endpoint_v2_common::native_token; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, initialize_native_token_for_test, + mint_native_token_for_test + }; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::universal_config; + use endpoint_v2_common::zro_test_helpers::create_fa; + use oft::oapp_core::{Self, withdraw_lz_fees}; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core::{SEND, SEND_AND_CALL}; + + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32) { + // Test the send function + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + let oft_admin = &create_signer_for_test(@oft_admin); + initialize_native_token_for_test(); + oft::oapp_test_helper::init_oapp(); + oapp_core::set_peer(oft_admin, SRC_EID, from_bytes32(from_address(@1234))); + oapp_core::set_peer(oft_admin, DST_EID, from_bytes32(from_address(@4321))); + } + + #[test] + fun test_send_internal() { + setup(SRC_EID, DST_EID); + + let called_send = false; + let called_inspect = false; + assert!(!called_inspect && !called_send, 0); + + let native_fee = mint_native_token_for_test(100000); + let zro_fee = option::none(); + + let messaging_receipt = oapp_core::lz_send( + DST_EID, + b"oapp-message", + b"options", + &mut native_fee, + &mut zro_fee, + ); + + let expected_guid = guid::compute_guid( + 1, + SRC_EID, + bytes32::from_address(OAPP_ADDRESS()), + DST_EID, + bytes32::from_address(@4321), + ); + + assert!(messaging_receipt::get_guid(&messaging_receipt) == expected_guid, 0); + + // 0 fees in simple msglib + assert!(messaging_receipt::get_native_fee(&messaging_receipt) == 0, 1); + assert!(messaging_receipt::get_zro_fee(&messaging_receipt) == 0, 1); + // nothing removed + assert!(fungible_asset::amount(&native_fee) == 100000, 0); + + assert!(messaging_receipt::get_nonce(&messaging_receipt) == 1, 2); + + let packet = packet_v1_codec::new_packet_v1( + SRC_EID, + bytes32::from_address(OAPP_ADDRESS()), + DST_EID, + bytes32::from_address(@4321), + 1, + expected_guid, + b"oapp-message", + ); + assert!(was_event_emitted(&packet_sent_event( + packet, + b"options", + @simple_msglib, + )), 0); + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + } + + #[test] + fun test_quote_internal() { + setup(SRC_EID, DST_EID); + + let (native_fee, zro_fee) = oapp_core::lz_quote( + DST_EID, + b"oapp-message", + b"options", + false, + ); + assert!(native_fee == 0, 0); + assert!(zro_fee == 0, 1); + } + + #[test] + fun test_set_enforced_options() { + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + oapp_core::set_enforced_options(admin, SRC_EID, SEND(), x"0003aaaa"); + oapp_core::set_enforced_options(admin, DST_EID, SEND(), x"0003bbbc"); + oapp_core::set_enforced_options(admin, DST_EID, SEND(), x"000355"); + oapp_core::set_enforced_options(admin, DST_EID, SEND_AND_CALL(), x"000344"); + assert!(oapp_core::get_enforced_options(DST_EID, SEND()) == x"000355", 0); + assert!(oapp_core::get_enforced_options(DST_EID, SEND_AND_CALL()) == x"000344", 0); + assert!(oapp_core::get_enforced_options(SRC_EID, SEND()) == x"0003aaaa", 0); + } + + #[test] + fun test_combine_options() { + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + let enforced_options = x"0003aaaa"; + let options = x"0003bbbb"; + oapp_core::set_enforced_options(admin, DST_EID, SEND(), enforced_options); + // unrelated option below just to make sure it doesn't get overwritten + oapp_core::set_enforced_options(admin, DST_EID, SEND_AND_CALL(), x"0003235326"); + let combined = oapp_core::combine_options(DST_EID, SEND(), options); + assert!(combined == x"0003aaaabbbb", 0); + } + + #[test] + fun test_peers() { + // Test the send function + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + assert!(!oapp_core::has_peer(1111), 0); + oapp_core::set_peer(admin, 1111, from_bytes32(from_address(@1234))); + oapp_core::set_peer(admin, 2222, from_bytes32(from_address(@2345))); + assert!(oapp_core::has_peer(1111), 0); + assert!(oapp_core::has_peer(2222), 0); + assert!(oapp_core::get_peer_bytes32(1111) == from_address(@1234), 0); + assert!(oapp_core::get_peer_bytes32(2222) == from_address(@2345), 0); + } + + #[test] + fun test_delegate() { + setup(SRC_EID, DST_EID); + + // setup admin + let oft_admin = &create_signer_for_test(@oft_admin); + let admin = &create_account_for_test(@1111); + oapp_core::transfer_admin(oft_admin, address_of(admin)); + + let delegate = &create_signer_for_test(@2222); + oapp_core::set_delegate(admin, address_of(delegate)); + assert!(oapp_core::get_delegate() == address_of(delegate), 0); + + let delegate2 = &create_signer_for_test(@3333); + oapp_core::set_delegate(admin, address_of(delegate2)); + + oapp_core::skip(delegate2, SRC_EID, from_bytes32(from_address(@1234)), 1); + } + + #[test] + fun test_withdraw_lz_fees() { + let native_token = mint_native_token_for_test(1000); + + let (zro_address, zro_metadata, mint_ref) = create_fa(b"ZRO"); + universal_config::init_module_for_test(100); + universal_config::set_zro_address(&create_signer_for_test(@layerzero_admin), zro_address); + + let zro_token = fungible_asset::mint(&mint_ref, 1000); + + primary_fungible_store::deposit(@0x1234, native_token); + primary_fungible_store::deposit(@0x1234, zro_token); + + let account = &create_signer_for_test(@0x1234); + + let (native_token, zro_token) = withdraw_lz_fees(account, 600, 550); + assert!(fungible_asset::amount(&native_token) == 600, 0); + assert!(fungible_asset::amount(option::borrow(&zro_token)) == 550, 0); + burn_token_for_test(native_token); + burn_token_for_test(option::extract(&mut zro_token)); + option::destroy_none(zro_token); + + assert!(native_token::balance(@0x1234) == 400, 0); + assert!(primary_fungible_store::balance(@0x1234, zro_metadata) == 450, 0); + } + + #[test] + fun test_withdraw_lz_fees_no_zro() { + let native_token = mint_native_token_for_test(1000); + primary_fungible_store::deposit(@0x1234, native_token); + + let account = &create_signer_for_test(@0x1234); + + let (native_token, zro_token) = withdraw_lz_fees(account, 600, 0); + assert!(fungible_asset::amount(&native_token) == 600, 0); + assert!(option::is_none(&zro_token), 0); + burn_token_for_test(native_token); + option::destroy_none(zro_token); + + assert!(native_token::balance(@0x1234) == 400, 0); + } + + #[test] + #[expected_failure(abort_code = oft::oapp_core::EINSUFFICIENT_NATIVE_TOKEN_BALANCE)] + fun test_withdraw_lz_fees_fails_with_insufficient_native_balance() { + let native_token = mint_native_token_for_test(100); + + let (zro_address, _, mint_ref) = create_fa(b"ZRO"); + universal_config::init_module_for_test(100); + universal_config::set_zro_address(&create_signer_for_test(@layerzero_admin), zro_address); + + let zro_token = fungible_asset::mint(&mint_ref, 1000); + + primary_fungible_store::deposit(@0x1234, native_token); + primary_fungible_store::deposit(@0x1234, zro_token); + + let account = &create_signer_for_test(@0x1234); + + let (native_token, zro_token) = withdraw_lz_fees(account, 600, 550); + burn_token_for_test(native_token); + burn_token_for_test(option::extract(&mut zro_token)); + option::destroy_none(zro_token); + } + + #[test] + #[expected_failure(abort_code = oft::oapp_core::EINSUFFICIENT_ZRO_BALANCE)] + fun test_withdraw_lz_fees_fails_with_insufficient_zro_balance() { + let native_token = mint_native_token_for_test(1000); + + let (zro_address, _, mint_ref) = create_fa(b"ZRO"); + universal_config::init_module_for_test(100); + universal_config::set_zro_address(&create_signer_for_test(@layerzero_admin), zro_address); + + let zro_token = fungible_asset::mint(&mint_ref, 100); + + primary_fungible_store::deposit(@0x1234, native_token); + primary_fungible_store::deposit(@0x1234, zro_token); + + let account = &create_signer_for_test(@0x1234); + + let (native_token, zro_token) = withdraw_lz_fees(account, 600, 550); + burn_token_for_test(native_token); + burn_token_for_test(option::extract(&mut zro_token)); + option::destroy_none(zro_token); + } +} diff --git a/examples/oft-aptos-move/tests/shared_oapp/oapp_test_helper.move b/examples/oft-aptos-move/tests/shared_oapp/oapp_test_helper.move new file mode 100644 index 000000000..b7512235c --- /dev/null +++ b/examples/oft-aptos-move/tests/shared_oapp/oapp_test_helper.move @@ -0,0 +1,10 @@ +#[test_only] +module oft::oapp_test_helper { + use oft::oapp_receive; + use oft::oapp_store; + + public fun init_oapp() { + oapp_store::init_module_for_test(); + oapp_receive::init_module_for_test(); + } +} diff --git a/examples/oft-aptos-move/tests/shared_oft/oft_core_tests.move b/examples/oft-aptos-move/tests/shared_oft/oft_core_tests.move new file mode 100644 index 000000000..fc7f075a7 --- /dev/null +++ b/examples/oft-aptos-move/tests/shared_oft/oft_core_tests.move @@ -0,0 +1,310 @@ +#[test_only] +module oft::oft_core_tests { + use std::account::create_signer_for_test; + use std::event::was_event_emitted; + use std::fungible_asset; + use std::fungible_asset::FungibleAsset; + use std::option; + use std::string::utf8; + use std::vector; + + use endpoint_v2::endpoint; + use endpoint_v2::messaging_receipt; + use endpoint_v2::messaging_receipt::new_messaging_receipt_for_test; + use endpoint_v2::test_helpers::setup_layerzero_for_test; + use endpoint_v2_common::bytes32; + use endpoint_v2_common::bytes32::{from_address, from_bytes32}; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, initialize_native_token_for_test, + mint_native_token_for_test + }; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::packet_v1_codec::compute_payload_hash; + use endpoint_v2_common::zro_test_helpers::create_fa; + use oft::oapp_core; + use oft::oapp_store::OAPP_ADDRESS; + use oft::oft_core::{Self, SEND, SEND_AND_CALL}; + use oft::oft_store; + use oft_common::oft_compose_msg_codec; + use oft_common::oft_msg_codec; + + const SRC_EID: u32 = 101; + const DST_EID: u32 = 201; + + fun setup(local_eid: u32, remote_eid: u32) { + // Test the send function + setup_layerzero_for_test(@simple_msglib, local_eid, remote_eid); + let oft_admin = &create_signer_for_test(@oft_admin); + initialize_native_token_for_test(); + let (_, metadata, _) = create_fa(b"ZRO"); + let local_decimals = fungible_asset::decimals(metadata); + oft::oapp_test_helper::init_oapp(); + + oft_store::init_module_for_test(); + oft_core::initialize(local_decimals, 6); + oapp_core::set_peer(oft_admin, SRC_EID, from_bytes32(from_address(@1234))); + oapp_core::set_peer(oft_admin, DST_EID, from_bytes32(from_address(@4321))); + } + + #[test] + fun test_send() { + setup(SRC_EID, DST_EID); + + let user_sender = @99; + let to = bytes32::from_address(@2001); + let native_fee = mint_native_token_for_test(100000); + let zro_fee = option::none(); + let compose_message = b"Hello"; + + let called_send = false; + let called_inspect = false; + assert!(!called_inspect && !called_send, 0); + + let (messaging_receipt, amount_sent_ld, amount_received_ld) = oft_core::send( + user_sender, + DST_EID, + to, + compose_message, + |message, options| { + called_send = true; + assert!(oft_msg_codec::has_compose(&message), 0); + assert!(oft_msg_codec::sender(&message) == from_address(user_sender), 1); + assert!(oft_msg_codec::send_to(&message) == to, 0); + assert!(oft_msg_codec::amount_sd(&message) == 40, 1); + assert!(options == b"options", 2); + + new_messaging_receipt_for_test( + from_address(@333), + 4, + 1111, + 2222, + ) + }, + |_unused| (5000, 4000), + |_amount_received_ld, _msg_type| b"options", + |message, options| { + called_inspect = true; + assert!(vector::length(message) > 0, 0); + assert!(*options == b"options", 0); + }, + ); + + assert!(called_send, 0); + assert!(called_inspect, 0); + + let (guid, nonce, native_fee_amount, zro_fee_amount) = messaging_receipt::unpack_messaging_receipt( + messaging_receipt, + ); + assert!(guid == from_address(@333), 0); + assert!(nonce == 4, 0); + assert!(native_fee_amount == 1111, 1); + assert!(zro_fee_amount == 2222, 2); + + assert!(amount_sent_ld == 5000, 3); + assert!(amount_received_ld == 4000, 4); + + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + } + + #[test] + fun test_receive() { + setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let called_credit = false; + assert!(!called_credit, 1); + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"", + ); + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + b"" + ); + + oft_core::receive( + SRC_EID, + nonce, + guid, + message, + |_to, _index, _message| { + // should not be called + assert!(false, 0); + }, + |to_address, message_amount| { + called_credit = true; + + assert!(to_address == @0x2000, 0); + // Add 2 0s for (8 local decimals - 6 shared decimals) + assert!(message_amount == 12300, 1); + + 5000 + }, + ); + + assert!(called_credit, 2); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 5000, + )), 3); + } + + #[test] + fun test_receive_with_compose() { + setup(DST_EID, SRC_EID); + + let called_inspect = false; + assert!(!called_inspect, 0); + + let nonce = 1; + let guid = bytes32::from_address(@23498213432414324); + + let called_credit = 0; + assert!(called_credit == 0, 1); + + + let message = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + 123, + bytes32::from_address(@0x3000), + b"Hello", + ); + + // Composer must be registered + let to_address_account = &create_signer_for_test(@0x2000); + endpoint::register_composer(to_address_account, utf8(b"oft")); + + let sender = bytes32::from_address(@1234); + + endpoint::verify( + @simple_msglib, + packet_v1_codec::new_packet_v1_header_only_bytes( + SRC_EID, + sender, + DST_EID, + bytes32::from_address(OAPP_ADDRESS()), + nonce, + ), + bytes32::from_bytes32(compute_payload_hash(guid, message)), + b"" + ); + + let called_compose = 0; + assert!(called_compose == 0, 0); + + oft_core::receive( + SRC_EID, + nonce, + guid, + message, + |to, index, message| { + called_compose = called_compose + 1; + assert!(to == @0x2000, 0); + assert!(index == 0, 1); + assert!(oft_compose_msg_codec::compose_payload_message(&message) == b"Hello", 2); + }, + |to_address, message_amount| { + called_credit = called_credit + 1; + + assert!(to_address == @0x2000, 0); + // Add 2 0s for (8 local decimals - 6 shared decimals) + assert!(message_amount == 12300, 1); + 12300 // message_amount + }, + ); + + assert!(called_compose == 1, 0); + assert!(called_credit == 1, 2); + + assert!(was_event_emitted(&oft_core::oft_received_event( + from_bytes32(guid), + SRC_EID, + @0x2000, + 12300, + )), 3); + } + + + #[test] + fun test_no_fee_debit_view() { + setup(SRC_EID, DST_EID); + + let (sent, received) = oft_core::no_fee_debit_view(123456789, 200); + assert!(sent == received, 0); + // dust removed (last 2 digits cleared) + assert!(sent == 123456700, 1); + } + + #[test] + #[expected_failure(abort_code = oft::oft_core::ESLIPPAGE_EXCEEDED)] + fun test_no_fee_debit_view_fails_if_post_dust_remove_less_than_min() { + setup(SRC_EID, DST_EID); + + oft_core::no_fee_debit_view(99, 20); + } + + #[test] + fun test_encode_oft_msg() { + setup(SRC_EID, DST_EID); + + let (encoded, message_type) = oft_core::encode_oft_msg( + @0x12345678, + 123, + bytes32::from_address(@0x2000), + b"Hello", + ); + + assert!(message_type == SEND_AND_CALL(), 0); + + let expected_encoded = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + // dust removed and SD + 1, + bytes32::from_address(@0x12345678), + b"Hello", + ); + + assert!(encoded == expected_encoded, 1); + + // without compose + let (encoded, message_type) = oft_core::encode_oft_msg( + @0x12345678, + 123, + bytes32::from_address(@0x2000), + b"", + ); + + assert!(message_type == SEND(), 2); + + let expected_encoded = oft_msg_codec::encode( + bytes32::from_address(@0x2000), + // dust removed and SD + 1, + bytes32::from_address(@0x12345678), + b"", + ); + + assert!(encoded == expected_encoded, 1); + } +} \ No newline at end of file diff --git a/examples/oft-aptos-move/tests/shared_oft/oft_impl_config_tests.move b/examples/oft-aptos-move/tests/shared_oft/oft_impl_config_tests.move new file mode 100644 index 000000000..888e67dca --- /dev/null +++ b/examples/oft-aptos-move/tests/shared_oft/oft_impl_config_tests.move @@ -0,0 +1,318 @@ +#[test_only] +module oft::oft_impl_config_tests { + use std::account::create_account_for_test; + use std::event::was_event_emitted; + use std::string::utf8; + use std::vector; + + use oft::oapp_store; + use oft::oft_core; + use oft::oft_impl_config::{ + Self, + assert_not_blocklisted, + blocked_amount_redirected_event, + blocklisting_disabled_event, + debit_view_with_possible_fee, + fee_bps, + fee_details_with_possible_fee, + has_rate_limit, + in_flight_at_time, + irrevocably_disable_blocklist, + is_blocklisted, + rate_limit_capacity_at_time, + rate_limit_config, + rate_limit_set_event, + rate_limit_updated_event, redirect_to_admin_if_blocklisted, release_rate_limit_capacity, set_blocklist, + set_fee_bps, try_consume_rate_limit_capacity_at_time, unset_rate_limit, + }; + use oft::oft_store; + use oft_common::oft_fee_detail; + + const MAX_U64: u64 = 0xffffffffffffffff; + + fun setup() { + oapp_store::init_module_for_test(); + oft_store::init_module_for_test(); + oft_impl_config::init_module_for_test(); + oft_core::initialize(8, 6); + } + + #[test] + fun test_set_fee_deposit_address() { + setup(); + + let deposit_address = @0x1234; + create_account_for_test(deposit_address); + + assert!(oft_impl_config::fee_deposit_address() == @oft_admin, 1); + oft_impl_config::set_fee_deposit_address(deposit_address); + assert!(oft_impl_config::fee_deposit_address() == deposit_address, 2); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EINVALID_DEPOSIT_ADDRESS)] + fun test_set_fee_deposit_address_invalid() { + setup(); + + let deposit_address = @0x1234; + oft_impl_config::set_fee_deposit_address(deposit_address); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::ESETTING_UNCHANGED)] + fun test_set_fee_deposit_address_unchanged() { + setup(); + + let deposit_address = @0x1234; + create_account_for_test(deposit_address); + + oft_impl_config::set_fee_deposit_address(deposit_address); + oft_impl_config::set_fee_deposit_address(deposit_address); + } + + #[test] + fun test_set_fee_bps() { + setup(); + + // Set the fee to 10% + let fee_bps = 1000; + set_fee_bps(fee_bps); + assert!(fee_bps() == fee_bps, 1); + + let (sent, received) = debit_view_with_possible_fee(1234, 1000); + // Send amount should include dust if there is a fee + assert!(sent == 1234, 1); + // Received amount should be 90% of the sent amount with dust removed: 1234 - 120 = 1114 => 1100 (dust removed) + assert!(received == 1100, 2); + + let fee_details = fee_details_with_possible_fee(1234, 1000); + assert!(vector::length(&fee_details) == 1, 1); + let (fee, is_reward) = oft_fee_detail::fee_amount_ld(vector::borrow(&fee_details, 0)); + assert!(fee == 134, 1); + assert!(is_reward == false, 2); + assert!(oft_fee_detail::description(vector::borrow(&fee_details, 0)) == utf8(b"OFT Fee"), 2); + + // Set the fee to 0% + let fee_bps = 0; + set_fee_bps(fee_bps); + + let (sent, received) = debit_view_with_possible_fee(1234, 1000); + // Sent and amount should be 100% of the amount with dust removed with no fee: 1234 => 1200 (dust removed) + assert!(sent == 1200, 3); + assert!(received == 1200, 4); + + // Expect no fee details if there is no fee + let fee_details = fee_details_with_possible_fee(1234, 1000); + assert!(vector::length(&fee_details) == 0, 1); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::ESETTING_UNCHANGED)] + fun test_set_fee_bps_fails_if_unchanged() { + setup(); + + // Set the fee to 10% + let fee_bps = 1000; + set_fee_bps(fee_bps); + set_fee_bps(fee_bps); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EINVALID_FEE)] + fun test_set_fee_bps_invalid() { + setup(); + + // Set the fee to 101% + let fee_bps = 10100; + set_fee_bps(fee_bps); + } + + #[test] + fun test_set_blocklist() { + setup(); + + assert!(oft_impl_config::is_blocklisted(@0x1234) == false, 1); + assert_not_blocklisted(@0x1234); + let redirected_address = redirect_to_admin_if_blocklisted(@0x1234, 1111); + assert!(redirected_address == @0x1234, 2); + + set_blocklist(@0x1234, true); + assert!(oft_impl_config::is_blocklisted(@0x1234), 1); + let redirected_address = redirect_to_admin_if_blocklisted(@0x1234, 1111); + assert!(redirected_address == @oft_admin, 2); + assert!(was_event_emitted(&blocked_amount_redirected_event(1111, @0x1234, @oft_admin)), 3); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::ESETTING_UNCHANGED)] + fun test_set_blocklist_fails_if_unchanged() { + setup(); + + set_blocklist(@0x1234, true); + set_blocklist(@0x1234, true); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::ESETTING_UNCHANGED)] + fun test_set_blocklist_fails_if_unchanged_2() { + setup(); + + set_blocklist(@0x1234, false); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EBLOCKLIST_DISABLED)] + fun test_cant_set_blocklist_if_disabled() { + setup(); + + assert!(oft_impl_config::is_blocklisted(@0x1234) == false, 1); + assert_not_blocklisted(@0x1234); + let redirected_address = redirect_to_admin_if_blocklisted(@0x1234, 1111); + assert!(redirected_address == @0x1234, 2); + + irrevocably_disable_blocklist(); + assert!(was_event_emitted(&blocklisting_disabled_event()), 1); + + assert!(is_blocklisted(@0x1234) == false, 1); + + // Should not be able to set blocklist + set_blocklist(@0x3333, true); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::EADDRESS_BLOCKED)] + fun test_assert_not_blocked() { + setup(); + + set_blocklist(@0x1234, true); + assert_not_blocklisted(@0x1234); + } + + #[test] + fun set_rate_limit() { + setup(); + + // No rate limit configured + assert!(has_rate_limit(30100) == false, 2); + let (limit, window) = rate_limit_config(30100); + assert!(limit == 0 && window == 0, 1); + assert!(in_flight_at_time(30100, 10) == 0, 1); + assert!(rate_limit_capacity_at_time(30100, 10) == MAX_U64, 2); + + // Configure rate limit (200/second) + oft_impl_config::set_rate_limit_at_timestamp(30100, 20000, 1000, 100); + assert!(was_event_emitted(&rate_limit_set_event(30100, 20000, 1000)), 1); + assert!(has_rate_limit(30100) == true, 2); + assert!(has_rate_limit(30200) == false, 2); // Different eid + let (limit, window) = rate_limit_config(30100); + assert!(limit == 20000 && window == 1000, 1); + assert!(in_flight_at_time(30100, 100) == 0, 1); + assert!(rate_limit_capacity_at_time(30100, 100) == 20000, 2); + + // 100 seconds later + assert!(in_flight_at_time(30100, 200) == 0, 1); + assert!(rate_limit_capacity_at_time(30100, 200) == 20000, 2); + + // consume 10% of the capacity + try_consume_rate_limit_capacity_at_time(30100, 2000, 200); + assert!(in_flight_at_time(30100, 200) == 2000, 1); + assert!(rate_limit_capacity_at_time(30100, 200) == 18000, 2); + + // 10 seconds later: in flight should decline by 20000/1000s * 10s = 200 + assert!(in_flight_at_time(30100, 210) == 1800, 1); + assert!(rate_limit_capacity_at_time(30100, 210) == 18200, 2); + + // 20 seconds later: in flight should decline by 20000/1000s * 20s = 400 + assert!(in_flight_at_time(30100, 220) == 1600, 1); + assert!(rate_limit_capacity_at_time(30100, 220) == 18400, 2); + + // update rate limit (300/second) + oft_impl_config::set_rate_limit_at_timestamp(30100, 30000, 1000, 220); + assert!(was_event_emitted(&rate_limit_updated_event(30100, 30000, 1000)), 1); + // in flight shouldn't change, but capacity should be updated with the new limit in mind + assert!(in_flight_at_time(30100, 220) == 1600, 1); + assert!(rate_limit_capacity_at_time(30100, 220) == 28400, 2); + + // 10 seconds later: in flight should decline by 30000/1000s * 10s = 300 + assert!(in_flight_at_time(30100, 230) == 1300, 1); + assert!(rate_limit_capacity_at_time(30100, 230) == 28700, 2); + + // 10 seconds later: in flight should decline by 30000/1000s * 10s = 300 + assert!(in_flight_at_time(30100, 240) == 1000, 1); + assert!(rate_limit_capacity_at_time(30100, 240) == 29000, 2); + + // 100 seconds later: in flight should decline fully (without overshooting): 30000/1000s * 100s = 3000 + assert!(in_flight_at_time(30100, 300) == 0, 1); + assert!(rate_limit_capacity_at_time(30100, 300) == 30000, 2); + + // Consume again + try_consume_rate_limit_capacity_at_time(30100, 2000, 300); + try_consume_rate_limit_capacity_at_time(30100, 2000, 310); + // 2000 + (2000 - 30000/1000*10) = 3800 + assert!(in_flight_at_time(30100, 310) == 3700, 1); + + // Reduce rate limit to below the in flight + oft_impl_config::set_rate_limit_at_timestamp(30100, 500, 1000, 320); + + // Rate limit capacity cannot go below 0, and should not abort + assert!(rate_limit_capacity_at_time(30100, 320) == 0, 2); + + // Unset rate limit + unset_rate_limit(30100); + assert!(has_rate_limit(30100) == false, 2); + let (limit, window) = rate_limit_config(30100); + assert!(limit == 0 && window == 0, 1); + assert!(in_flight_at_time(30100, 320) == 0, 1); + assert!(rate_limit_capacity_at_time(30100, 320) == MAX_U64, 2); + } + + #[test] + #[expected_failure(abort_code = oft::oft_impl_config::ESETTING_UNCHANGED)] + fun set_rate_limit_fails_if_unchanged() { + setup(); + + // Configure rate limit (200/second) + oft_impl_config::set_rate_limit_at_timestamp(30100, 20000, 1000, 100); + oft_impl_config::set_rate_limit_at_timestamp(30100, 20000, 1000, 200); + } + + #[test] + fun test_set_rate_limit_net() { + setup(); + + // No rate limit configured + assert!(has_rate_limit(30100) == false, 2); + let (limit, window) = rate_limit_config(30100); + assert!(limit == 0 && window == 0, 1); + assert!(in_flight_at_time(30100, 10) == 0, 1); + assert!(rate_limit_capacity_at_time(30100, 10) == MAX_U64, 2); + + // Configure rate limit (200/second) + oft_impl_config::set_rate_limit_at_timestamp(30100, 20000, 1000, 100); + assert!(has_rate_limit(30100) == true, 2); + + // consume 100% of the capacity + try_consume_rate_limit_capacity_at_time(30100, 20000, 0); + assert!(in_flight_at_time(30100, 0) == 20000, 1); + assert!(rate_limit_capacity_at_time(30100, 0) == 0, 2); + + // 50 seconds later: in flight should decline by 20000/1000s * 500s = 10000 + assert!(in_flight_at_time(30100, 500) == 10000, 1); + assert!(rate_limit_capacity_at_time(30100, 500) == 10000, 2); + + // release most of remaining capacity + release_rate_limit_capacity(30100, 9000); + assert!(in_flight_at_time(30100, 500) == 1000, 1); + assert!(rate_limit_capacity_at_time(30100, 500) == 19000, 2); + + // consume all of remaining capacity + try_consume_rate_limit_capacity_at_time(30100, 19000, 500); + assert!(in_flight_at_time(30100, 500) == 20000, 1); + assert!(rate_limit_capacity_at_time(30100, 500) == 0, 2); + + // release excess capacity (5x limit) - should not overshoot + release_rate_limit_capacity(30100, 100_000); + assert!(in_flight_at_time(30100, 500) == 0, 1); + assert!(rate_limit_capacity_at_time(30100, 500) == 20000, 2); + } +} diff --git a/examples/oft-aptos-move/tsconfig.json b/examples/oft-aptos-move/tsconfig.json new file mode 100644 index 000000000..9ba95395c --- /dev/null +++ b/examples/oft-aptos-move/tsconfig.json @@ -0,0 +1,19 @@ +{ + "exclude": ["node_modules"], + "include": [ + "deploy", + "tasks", + "test", + "hardhat.config.ts", + "deploy-move/MyMoveOFTFA.ts" + ], + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +} diff --git a/examples/oft-aptos-move/turbo.json b/examples/oft-aptos-move/turbo.json new file mode 100644 index 000000000..adaaffa58 --- /dev/null +++ b/examples/oft-aptos-move/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "pipeline": { + "build": { + "outputs": ["build*/**"] + } + } +} diff --git a/packages/create-lz-oapp/src/config.ts b/packages/create-lz-oapp/src/config.ts index 5fde2c4d8..40911030c 100644 --- a/packages/create-lz-oapp/src/config.ts +++ b/packages/create-lz-oapp/src/config.ts @@ -111,6 +111,26 @@ export const getExamples = (): Example[] => { }, ] : []), + + // The Aptos examples are feature flagged for the time being + ...(process.env.LZ_ENABLE_EXPERIMENTAL_MOVE_VM_EXAMPLES + ? [ + { + id: 'oft-move', + label: 'OFT (Aptos Fungible Asset)', + repository, + directory: 'examples/oft-aptos', + ref, + }, + { + id: 'oft-adapter-move', + label: 'OFT Adapter (Aptos Fungible Asset)', + repository, + directory: 'examples/oft-adapter-aptos', + ref, + }, + ] + : []), ] } diff --git a/packages/devtools-extensible-cli/.eslintignore b/packages/devtools-extensible-cli/.eslintignore new file mode 100644 index 000000000..ee9f768fd --- /dev/null +++ b/packages/devtools-extensible-cli/.eslintignore @@ -0,0 +1,10 @@ +artifacts +cache +dist +node_modules +out +*.log +*.sol +*.yaml +*.lock +package-lock.json \ No newline at end of file diff --git a/packages/devtools-extensible-cli/.nvmrc b/packages/devtools-extensible-cli/.nvmrc new file mode 100644 index 000000000..b714151ef --- /dev/null +++ b/packages/devtools-extensible-cli/.nvmrc @@ -0,0 +1 @@ +v18.18.0 \ No newline at end of file diff --git a/packages/devtools-extensible-cli/README.md b/packages/devtools-extensible-cli/README.md new file mode 100644 index 000000000..848bc7c85 --- /dev/null +++ b/packages/devtools-extensible-cli/README.md @@ -0,0 +1,36 @@ +# Devtools-Extensible-CLI README + +This is a CLI for LayerZero Devtools - which is extensible by defining new operations. + +Operations are of the class Type `INewOperation` and are defined in the `types/index.d.ts` file. + +When you go to type your new operation, the args will have `-` instead of `_`. +For example, `oapp_config` will be `--oapp-config` on the command line. + +```ts +import { build as buildMove } from '../../tasks/move/build' +import { INewOperation } from './NewOperation' + +class MoveBuildOperation implements INewOperation { + // the vm to use + vm = 'move' + // the name of this operation + operation = 'build' + // the required arguments for the operation + reqArgs = ['oapp_config', 'named_addresses', 'move_deploy_script'] + // arguments that you want to create in addition to the pre-defined ones + addArgs = [] + + // the implementation of the operation + async impl(args: any): Promise { + await buildMove(args) + } +} + +const NewOperation = new MoveBuildOperation() +export { NewOperation } +``` + +You can attach new operations to this via +1. Paths `await sdk.extendOperationFromPath('./operations/move-build')` - example: `packages/devtools-movement/operations/init.ts` +2. Providing an implementation of the `INewOperation` interface `await sdk.extendOperation(NewOperation)` - example: `examples/oft-aptos-move/scripts/cli.ts` diff --git a/packages/devtools-extensible-cli/cli/AptosEVMCli.ts b/packages/devtools-extensible-cli/cli/AptosEVMCli.ts new file mode 100644 index 000000000..3ee683108 --- /dev/null +++ b/packages/devtools-extensible-cli/cli/AptosEVMCli.ts @@ -0,0 +1,94 @@ +import { INewOperation, Operation } from './types/NewOperation' +import { ArgumentParser } from 'argparse' +import { initOperation } from './operations/init' + +class AptosEVMCLI { + parser: ArgumentParser + args: any + operations: Operation + rootDir: string + constructor(rootDir: string = process.cwd()) { + this.parser = new ArgumentParser({ + description: 'A simple CLI tool built with argparse in TypeScript', + }) + + this.operations = {} + this.rootDir = rootDir + } + + validateArgs(args: string[]) { + let isValid = true + const missingArgs: string[] = [] + for (const arg of args) { + if (!this.args[arg]) { + missingArgs.push(`--${arg.replace('_', '-')}=`) + isValid = false + } + } + if (!isValid) { + throw new Error(`The following args are required: ${missingArgs.join(', ')}\n\n`) + } + return isValid + } + + async extendOperationFromPath(path: string) { + const operation = await import(path) + const NewOperation = operation.NewOperation + await this.extendOperation(NewOperation) + } + + async extendOperation(NewOperation: INewOperation) { + if (!this.operations[NewOperation.vm]) { + this.operations[NewOperation.vm] = {} + } + if (!this.operations[NewOperation.vm][NewOperation.operation]) { + this.operations[NewOperation.vm][NewOperation.operation] = { + func: NewOperation.impl, + requiredArgs: NewOperation.reqArgs || [], + description: NewOperation.description, + } + } + + if (NewOperation.addArgs) { + for (const arg of NewOperation.addArgs) { + try { + this.parser.add_argument(arg.name, arg.arg) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + // Handles the case when the same argument is added more than once + if (e.message.includes('conflicting option string')) { + continue + } + throw new Error('Error adding argument: \n' + e.message) + } + } + } + } + + async execute() { + this.args = this.parser.parse_args() + this.args.rootDir = this.rootDir + this.args.operations = this.operations + + if (this.args.op === 'help') { + this.args.vm = '*' + } else if (!this.args.vm) { + throw new Error('--vm is required') + } + + const vm = this.args.vm + const op = this.args.op + + const exec_op = this.operations[vm][op] + if (!exec_op) { + throw new Error(`Operation ${op} is not valid for ${vm}`) + } + if (this.validateArgs(exec_op.requiredArgs)) { + await exec_op.func(this.args) + } + } +} + +const sdk = new AptosEVMCLI() +sdk.extendOperation(initOperation) +export { AptosEVMCLI, sdk } diff --git a/packages/devtools-extensible-cli/cli/index.ts b/packages/devtools-extensible-cli/cli/index.ts new file mode 100644 index 000000000..62746b850 --- /dev/null +++ b/packages/devtools-extensible-cli/cli/index.ts @@ -0,0 +1 @@ +export * from './AptosEVMCli' diff --git a/packages/devtools-extensible-cli/cli/operations/init.ts b/packages/devtools-extensible-cli/cli/operations/init.ts new file mode 100644 index 000000000..323e82c73 --- /dev/null +++ b/packages/devtools-extensible-cli/cli/operations/init.ts @@ -0,0 +1,89 @@ +import { INewOperation } from '../types/NewOperation' + +class InitOperation implements INewOperation { + vm = '*' + operation = 'help' + description = 'lists all the operations for a given vm' + reqArgs = ['filter'] + addArgs = [ + { + name: '--vm', + arg: { + help: 'vm to perform operation on', + required: false, + }, + }, + { + name: '--op', + arg: { + help: 'operation to perform', + required: false, + }, + }, + { + name: '--filter', + arg: { + help: 'filter flags', + required: false, + }, + }, + { + name: '--oapp-config', + arg: { + help: 'path to the layerzeroconfig file', + required: false, + }, + }, + { + name: '--move-deploy-script', + arg: { + help: 'path to the move deploy script', + required: false, + }, + }, + { + name: '--named-addresses', + arg: { + help: 'deployer account address based on your config', + required: false, + }, + }, + { + name: '--force-build', + arg: { + help: 'Force aptos build even if contracts already built', + required: false, + }, + }, + { + name: '--force-deploy', + arg: { + help: 'Force aptos deploy even if deployment already exists', + required: false, + }, + }, + ] + + async impl(args: any): Promise { + console.log('\n Operation Help:\n') + const ops = args.operations + const op_names = Object.keys(ops) + + for (const op_name of op_names) { + const vm_op = ops[op_name] + const op_functions = Object.keys(vm_op) + for (const op_function of op_functions) { + let reqArgs = vm_op[op_function].requiredArgs.join(' --') + if (reqArgs.length > 0) { + reqArgs = '--' + reqArgs + } + const reqArgsFormatted = reqArgs.replace(/_/g, '-') + console.log('--op', op_function, reqArgsFormatted) + console.log('\tDescription:', vm_op[op_function].description, '\n') + } + } + } +} + +const initOperation = new InitOperation() +export { initOperation } diff --git a/packages/devtools-extensible-cli/cli/types/NewOperation.ts b/packages/devtools-extensible-cli/cli/types/NewOperation.ts new file mode 100644 index 000000000..f1409f7cb --- /dev/null +++ b/packages/devtools-extensible-cli/cli/types/NewOperation.ts @@ -0,0 +1,26 @@ +import { ArgumentOptions } from 'argparse' + +type NewArg = { + name: string + arg: ArgumentOptions +} + +export type Operation = { + [key: string]: { + [key: string]: { + func: (...args: any[]) => Promise + requiredArgs: string[] + description: string + } + } +} + +export interface INewOperation { + vm: string + operation: string + description: string + reqArgs?: string[] + addArgs?: NewArg[] + + impl: (args: any) => Promise +} diff --git a/packages/devtools-extensible-cli/package.json b/packages/devtools-extensible-cli/package.json new file mode 100644 index 000000000..8c0388b44 --- /dev/null +++ b/packages/devtools-extensible-cli/package.json @@ -0,0 +1,24 @@ +{ + "name": "@layerzerolabs/devtools-extensible-cli", + "version": "0.0.1", + "private": true, + "license": "MIT", + "types": "types/index.d.ts", + "scripts": { + "prebuild": "tsc -noEmit", + "build": "$npm_execpath tsup --clean", + "clean": "rm -rf dist", + "dev": "$npm_execpath tsup --watch", + "lint": "$npm_execpath eslint '**/*.{js,ts,json}'", + "lint:fix": "eslint --fix '**/*.{js,ts,json}'" + }, + "devDependencies": { + "@types/argparse": "^2.0.17", + "argparse": "^2.0.1", + "tsup": "^8.0.1", + "typescript": "^5.4.4" + }, + "engines": { + "node": ">=18.16.0" + } +} diff --git a/packages/devtools-extensible-cli/tsconfig.json b/packages/devtools-extensible-cli/tsconfig.json new file mode 100644 index 000000000..c641ff073 --- /dev/null +++ b/packages/devtools-extensible-cli/tsconfig.json @@ -0,0 +1,13 @@ +{ + "exclude": ["node_modules"], + "include": ["cli", "types"], + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +} diff --git a/packages/devtools-extensible-cli/tsup.config.ts b/packages/devtools-extensible-cli/tsup.config.ts new file mode 100644 index 000000000..6cdf115d7 --- /dev/null +++ b/packages/devtools-extensible-cli/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['cli/index.ts'], + outDir: './dist', + clean: true, + dts: true, + sourcemap: true, + splitting: false, + treeshake: true, + format: ['esm', 'cjs'], +}) diff --git a/packages/devtools-extensible-cli/turbo.json b/packages/devtools-extensible-cli/turbo.json new file mode 100644 index 000000000..adaaffa58 --- /dev/null +++ b/packages/devtools-extensible-cli/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "pipeline": { + "build": { + "outputs": ["build*/**"] + } + } +} diff --git a/packages/devtools-extensible-cli/types/index.d.ts b/packages/devtools-extensible-cli/types/index.d.ts new file mode 100644 index 000000000..3baa9426e --- /dev/null +++ b/packages/devtools-extensible-cli/types/index.d.ts @@ -0,0 +1,4 @@ +import { AptosEVMCLI } from '../cli/AptosEVMCli' +import { INewOperation } from '../cli/types/NewOperation' + +export { AptosEVMCLI, INewOperation } diff --git a/packages/devtools-move/.eslintignore b/packages/devtools-move/.eslintignore new file mode 100644 index 000000000..ee9f768fd --- /dev/null +++ b/packages/devtools-move/.eslintignore @@ -0,0 +1,10 @@ +artifacts +cache +dist +node_modules +out +*.log +*.sol +*.yaml +*.lock +package-lock.json \ No newline at end of file diff --git a/packages/devtools-move/.nvmrc b/packages/devtools-move/.nvmrc new file mode 100644 index 000000000..b714151ef --- /dev/null +++ b/packages/devtools-move/.nvmrc @@ -0,0 +1 @@ +v18.18.0 \ No newline at end of file diff --git a/packages/devtools-move/README.md b/packages/devtools-move/README.md new file mode 100644 index 000000000..42aee75c0 --- /dev/null +++ b/packages/devtools-move/README.md @@ -0,0 +1,5 @@ +# Devtools-Movement README + +1. Supports movement and evm chain side of the layerzero-sdk +2. Uses existing `hardhat.config.ts` and `layerzero.config.ts` +3. Extensible for OFTs and OApps \ No newline at end of file diff --git a/packages/devtools-move/cli/init.ts b/packages/devtools-move/cli/init.ts new file mode 100644 index 000000000..7251871c3 --- /dev/null +++ b/packages/devtools-move/cli/init.ts @@ -0,0 +1,17 @@ +import { AptosEVMCLI } from '@layerzerolabs/devtools-extensible-cli/cli/AptosEVMCli' +import path from 'path' + +export async function attach_wire_move(sdk: AptosEVMCLI) { + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-build')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-deploy')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-wire')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-set-delegate')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/transfer-oapp-owner')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/transfer-object-owner')) +} + +export async function attach_wire_evm(sdk: AptosEVMCLI) { + await sdk.extendOperationFromPath(path.join(__dirname, './operations/evm-wire')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/evm-quote-send')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/evm-send')) +} diff --git a/packages/devtools-move/cli/operations/evm-quote-send.ts b/packages/devtools-move/cli/operations/evm-quote-send.ts new file mode 100644 index 000000000..4c4cff422 --- /dev/null +++ b/packages/devtools-move/cli/operations/evm-quote-send.ts @@ -0,0 +1,105 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' +import { createEvmOmniContracts, readPrivateKey } from '../../tasks/evm/wire-evm' +import { ethers } from 'ethers' + +class EVMQuoteSendOperation implements INewOperation { + vm = 'evm' + operation = 'quote-send' + description = 'Computes the quote for sending an OFT message' + reqArgs = ['oapp_config', 'src_eid', 'dst_eid', 'to', 'amount', 'min_amount'] + addArgs = [ + { + name: '--src-eid', + arg: { + help: 'The source endpoint ID', + required: false, + }, + }, + { + name: '--dst-eid', + arg: { + help: 'The destination endpoint ID', + required: false, + }, + }, + { + name: '--to', + arg: { + help: 'The address to send the message to', + required: false, + }, + }, + { + name: '--amount', + arg: { + help: 'The amount to send', + required: false, + }, + }, + { + name: '--min-amount', + arg: { + help: 'The minimum amount to send', + required: false, + }, + }, + ] + + async impl(args: any): Promise { + const fee = await quoteSendEvm(args) + const nativeFee = fee.nativeFee.toString() + const lzTokenFee = fee.lzTokenFee.toString() + console.log('nativeFee :', nativeFee, 'wei', `[${nativeFee[0]}e${nativeFee.length - 2}]`) + console.log('lzTokenFee :', lzTokenFee, 'wei') + } +} + +const NewOperation = new EVMQuoteSendOperation() +export { NewOperation } + +async function quoteSendEvm(args: any): Promise { + const srcEid = args.src_eid + const dstEid = args.dst_eid + const to = args.to + const amount = args.amount + const minAmount = args.min_amount + + const privateKey = readPrivateKey(args) + + const omniContracts = await createEvmOmniContracts(args, privateKey) + + let oft: ethers.Contract + if (omniContracts[srcEid.toString()]) { + oft = omniContracts[srcEid.toString()].contract.oapp + } else { + throw new Error(`No OApp found for endpoint ID ${srcEid}`) + } + + const sendParam: SendParam = { + dstEid: dstEid, + to: to, + amountLD: amount, + minAmountLD: minAmount, + extraOptions: '0x', + composeMsg: '0x', + oftCmd: '0x', + } + + const fee: MessagingFee = await oft.quoteSend(sendParam, false) + return fee +} + +type SendParam = { + dstEid: number + to: string + amountLD: number + minAmountLD: number + extraOptions: string + composeMsg: string + oftCmd: string +} + +type MessagingFee = { + nativeFee: bigint + lzTokenFee: bigint +} diff --git a/packages/devtools-move/cli/operations/evm-send.ts b/packages/devtools-move/cli/operations/evm-send.ts new file mode 100644 index 000000000..f846f3959 --- /dev/null +++ b/packages/devtools-move/cli/operations/evm-send.ts @@ -0,0 +1,132 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' +import { createEvmOmniContracts, readPrivateKey } from '../../tasks/evm/wire-evm' +import { ethers } from 'ethers' + +class EVMQuoteSendOperation implements INewOperation { + vm = 'evm' + operation = 'send' + description = 'Sends an OFT message' + reqArgs = ['oapp_config', 'src_eid', 'dst_eid', 'to', 'amount', 'min_amount'] + addArgs = [ + { + name: '--src-eid', + arg: { + help: 'The source endpoint ID', + required: false, + }, + }, + { + name: '--dst-eid', + arg: { + help: 'The destination endpoint ID', + required: false, + }, + }, + { + name: '--to', + arg: { + help: 'The address to send the message to', + required: false, + }, + }, + { + name: '--amount', + arg: { + help: 'The amount to send', + required: false, + }, + }, + { + name: '--min-amount', + arg: { + help: 'The minimum amount to send', + required: false, + }, + }, + { + name: '--refund-address', + arg: { + help: 'The address to refund the gas fee to', + required: false, + }, + }, + ] + + async impl(args: any): Promise { + await sendOFT(args) + } +} + +const NewOperation = new EVMQuoteSendOperation() +export { NewOperation } + +async function sendOFT(args: any): Promise { + const srcEid = args.src_eid + const dstEid = args.dst_eid + const to = args.to + const amount = args.amount + const minAmount = args.min_amount + + const privateKey = readPrivateKey(args) + const omniContracts = await createEvmOmniContracts(args, privateKey) + let oft: ethers.Contract + const contract = omniContracts[srcEid.toString()] + if (contract?.contract?.oapp) { + oft = contract.contract.oapp + } else { + throw new Error(`No OApp found for endpoint ID ${srcEid}`) + } + const refundAddress = args.refund_address || (await oft.signer.getAddress()) + + console.log(`\n🚀 Sending ${amount} units`) + console.log(`\t📝 Using OFT at address: ${oft.address}`) + console.log(`\t👤 From account: ${await oft.signer.getAddress()}`) + console.log(`\t🎯 To account: ${to}`) + console.log(`\t🌐 dstEid: ${dstEid}`) + console.log(`\t🔍 Min amount: ${minAmount}`) + + const sendParam: SendParam = { + dstEid: dstEid, + to: to, + amountLD: amount, + minAmountLD: minAmount, + extraOptions: '0x', + composeMsg: '0x', + oftCmd: '0x', + } + + const fee: MessagingFee = await oft.quoteSend(sendParam, false) + + console.log('\n💰 Quote received:') + console.log('\t🏦 Native fee:', fee.nativeFee.toString()) + console.log('\t🪙 LZ token fee:', fee.lzTokenFee.toString()) + + const tx = await oft.send(sendParam, fee, refundAddress, { + value: fee.nativeFee, + }) + + console.log('\n📨 Transaction sent:') + console.log('\t🔑 Hash:', tx.hash) + console.log('\t🔍 LayerZero Explorer:', `https://layerzeroscan.com/tx/${tx.hash}`) + console.log('\t📤 From:', tx.from) + console.log('\t📥 To:', tx.to) + console.log('\t💵 Value:', ethers.utils.formatEther(tx.value), 'ETH') + console.log('\t⛽ Gas limit:', tx.gasLimit.toString()) + + return tx +} + +type SendParam = { + dstEid: number + to: string + amountLD: number + minAmountLD: number + extraOptions: string + composeMsg: string + oftCmd: string +} + +type MessagingFee = { + nativeFee: bigint + lzTokenFee: bigint +} diff --git a/packages/devtools-move/cli/operations/evm-wire.ts b/packages/devtools-move/cli/operations/evm-wire.ts new file mode 100644 index 000000000..0f55e1fa7 --- /dev/null +++ b/packages/devtools-move/cli/operations/evm-wire.ts @@ -0,0 +1,34 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' +import { wireEvm } from '../../tasks/evm/wire-evm' + +class EVMWireOperation implements INewOperation { + vm = 'evm' + operation = 'wire' + description = 'Wire EVM contracts' + reqArgs = ['oapp_config', 'simulate'] + addArgs = [ + { + name: '--simulate', + arg: { + help: 'Whether to simulate the transaction', + required: false, + default: 'true', + }, + }, + { + name: '--mnemonic-index', + arg: { + help: 'EVM mnemonic index', + required: false, + default: '-1', + }, + }, + ] + + async impl(args: any): Promise { + await wireEvm(args) + } +} + +const NewOperation = new EVMWireOperation() +export { NewOperation } diff --git a/packages/devtools-move/cli/operations/move-build.ts b/packages/devtools-move/cli/operations/move-build.ts new file mode 100644 index 000000000..df91fac6e --- /dev/null +++ b/packages/devtools-move/cli/operations/move-build.ts @@ -0,0 +1,16 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' +import { build as buildMove } from '../../tasks/move/build' + +class MoveBuildOperation implements INewOperation { + vm = 'move' + operation = 'build' + description = 'Build Aptos Move contracts' + reqArgs = ['oapp_config', 'named_addresses'] + + async impl(args: any): Promise { + await buildMove(args) + } +} + +const NewOperation = new MoveBuildOperation() +export { NewOperation } diff --git a/packages/devtools-move/cli/operations/move-deploy.ts b/packages/devtools-move/cli/operations/move-deploy.ts new file mode 100644 index 000000000..9f09d5fdf --- /dev/null +++ b/packages/devtools-move/cli/operations/move-deploy.ts @@ -0,0 +1,31 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' + +import { build as buildMove } from '../../tasks/move/build' +import { deploy as deployMove } from '../../tasks/move/deploy' +import { setDelegate } from '../../tasks/move/setDelegate' + +class MoveDeployOperation implements INewOperation { + vm = 'move' + operation = 'deploy' + description = 'Deploy Aptos Move contracts' + reqArgs = ['oapp_config', 'address_name', 'named_addresses', 'move_deploy_script'] + + addArgs = [ + { + name: '--address-name', + arg: { + help: 'address name', + required: false, + }, + }, + ] + + async impl(args: any): Promise { + await buildMove(args) + await deployMove(args.oapp_config, args.named_addresses, args.force_deploy, args.address_name) + await setDelegate(args, true) + } +} + +const NewOperation = new MoveDeployOperation() +export { NewOperation } diff --git a/packages/devtools-move/cli/operations/move-set-delegate.ts b/packages/devtools-move/cli/operations/move-set-delegate.ts new file mode 100644 index 000000000..836d76ae2 --- /dev/null +++ b/packages/devtools-move/cli/operations/move-set-delegate.ts @@ -0,0 +1,16 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' +import { setDelegate } from '../../tasks/move/setDelegate' + +class MoveDeployOperation implements INewOperation { + vm = 'move' + operation = 'set-delegate' + description = 'Set Aptos Move delegate' + reqArgs = ['oapp_config'] + + async impl(args: any): Promise { + await setDelegate(args) + } +} + +const NewOperation = new MoveDeployOperation() +export { NewOperation } diff --git a/packages/devtools-move/cli/operations/move-wire.ts b/packages/devtools-move/cli/operations/move-wire.ts new file mode 100644 index 000000000..b17219bdd --- /dev/null +++ b/packages/devtools-move/cli/operations/move-wire.ts @@ -0,0 +1,16 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' +import { wireMove } from '../../tasks/move/wireMove' + +class MoveWireOperation implements INewOperation { + vm = 'move' + operation = 'wire' + description = 'Wire Aptos Move contracts' + reqArgs = ['oapp_config'] + + async impl(args: any): Promise { + await wireMove(args) + } +} + +const NewOperation = new MoveWireOperation() +export { NewOperation } diff --git a/packages/devtools-move/cli/operations/transfer-oapp-owner.ts b/packages/devtools-move/cli/operations/transfer-oapp-owner.ts new file mode 100644 index 000000000..871590b70 --- /dev/null +++ b/packages/devtools-move/cli/operations/transfer-oapp-owner.ts @@ -0,0 +1,26 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' +import { transferOAppOwner } from '../../tasks/move/transferOwnerOapp' + +class MoveTransferOwnerOperation implements INewOperation { + vm = 'move' + operation = 'transfer-oapp-owner' + description = 'Transfer Aptos Move OApp Ownership' + reqArgs = ['new_owner'] + + addArgs = [ + { + name: '--new-owner', + arg: { + help: 'new owner', + required: false, + }, + }, + ] + + async impl(args: any): Promise { + await transferOAppOwner(args.new_owner) + } +} + +const NewOperation = new MoveTransferOwnerOperation() +export { NewOperation } diff --git a/packages/devtools-move/cli/operations/transfer-object-owner.ts b/packages/devtools-move/cli/operations/transfer-object-owner.ts new file mode 100644 index 000000000..f80de1e94 --- /dev/null +++ b/packages/devtools-move/cli/operations/transfer-object-owner.ts @@ -0,0 +1,16 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' +import { transferObjectOwner } from '../../tasks/move/transferObjectOwner' + +class MoveTransferObjectOwnerOperation implements INewOperation { + vm = 'move' + operation = 'transfer-object-owner' + description = 'Transfer Aptos Move OApp Ownership' + reqArgs = ['new_owner'] + + async impl(args: any): Promise { + await transferObjectOwner(args.new_owner) + } +} + +const NewOperation = new MoveTransferObjectOwnerOperation() +export { NewOperation } diff --git a/packages/devtools-move/jest.config.js b/packages/devtools-move/jest.config.js new file mode 100644 index 000000000..81cb23111 --- /dev/null +++ b/packages/devtools-move/jest.config.js @@ -0,0 +1,9 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + testMatch: ['**/*.test.ts'], +}; diff --git a/packages/devtools-move/jest/baseXtoBytes32.test.ts b/packages/devtools-move/jest/baseXtoBytes32.test.ts new file mode 100644 index 000000000..1fa95f813 --- /dev/null +++ b/packages/devtools-move/jest/baseXtoBytes32.test.ts @@ -0,0 +1,26 @@ +import { EndpointId } from '@layerzerolabs/lz-definitions' +import { expect } from 'chai' +import { basexToBytes32 } from '../tasks/shared/basexToBytes32' + +describe('should convert base-x addresses (eth, aptos, solana, etc) to bytes32', () => { + it('should convert solana base-x address to bytes32', () => { + const basexAddress = 'Efvf2QfcPAJc8XCd1MV1MN1JLNDZRKj2ZbJ3xg6pAf7' + const eid = EndpointId.SOLANA_V2_MAINNET.toString() + const bytes32 = basexToBytes32(basexAddress, eid) + expect(bytes32).to.equal('0x038090321ef8b2bbe6d072ded5e3e9ad8d5608e1b04db7d2e9777e39231af2ae') + }) + + it('should convert aptos base-x address to bytes32', () => { + const basexAddress = '0x000000000000000000000000177b58ddda0c81424227ee473e4132044a0dc871' + const eid = EndpointId.APTOS_V2_MAINNET.toString() + const bytes32 = basexToBytes32(basexAddress, eid) + expect(bytes32).to.equal('0x000000000000000000000000177b58ddda0c81424227ee473e4132044a0dc871') + }) + + it('should convert ethereum base-x address to bytes32', () => { + const basexAddress = '0x177B58Ddda0C81424227Ee473e4132044a0DC871' + const eid = EndpointId.ETHEREUM_V2_MAINNET.toString() + const bytes32 = basexToBytes32(basexAddress, eid) + expect(bytes32).to.equal('0x000000000000000000000000177b58ddda0c81424227ee473e4132044a0dc871') + }) +}) diff --git a/packages/devtools-move/package.json b/packages/devtools-move/package.json new file mode 100644 index 000000000..89a1efdaa --- /dev/null +++ b/packages/devtools-move/package.json @@ -0,0 +1,48 @@ +{ + "name": "@layerzerolabs/devtools-move", + "version": "0.0.1", + "private": true, + "license": "MIT", + "types": "types/index.d.ts", + "scripts": { + "prebuild": "tsc -noEmit", + "build": "$npm_execpath tsup --clean", + "clean": "rm -rf dist", + "dev": "$npm_execpath tsup --watch", + "lint": "$npm_execpath eslint '**/*.{js,ts,json}'", + "lint:fix": "eslint --fix '**/*.{js,ts,json}'", + "test": "$npm_execpath jest" + }, + "dependencies": { + "@types/chai": "^4.3.11", + "chai": "^4.4.1", + "jest": "^29.7.0", + "ts-jest": "^29.2.5" + }, + "devDependencies": { + "@aptos-labs/ts-sdk": "^1.33.1", + "@layerzerolabs/devtools-extensible-cli": "^0.0.1", + "@layerzerolabs/lz-definitions": "^3.0.40", + "@layerzerolabs/lz-evm-sdk-v2": "^3.0.27", + "@layerzerolabs/lz-serdes": "^3.0.19", + "@layerzerolabs/lz-v2-utilities": "^3.0.12", + "@layerzerolabs/toolbox-hardhat": "~0.6.3", + "@types/argparse": "^2.0.17", + "@types/jest": "^29.5.12", + "@types/node": "~18.18.14", + "argparse": "^2.0.1", + "base-x": "^5.0.0", + "bs58": "^6.0.0", + "depcheck": "^1.4.7", + "dotenv": "^16.4.7", + "ethers": "^5.7.2", + "hardhat": "^2.22.10", + "ts-node": "^10.9.2", + "tsup": "^8.0.1", + "typescript": "^5.4.4", + "yaml": "^2.6.1" + }, + "engines": { + "node": ">=18.16.0" + } +} diff --git a/packages/devtools-move/sdk/endpoint.ts b/packages/devtools-move/sdk/endpoint.ts new file mode 100644 index 000000000..1fa1609ac --- /dev/null +++ b/packages/devtools-move/sdk/endpoint.ts @@ -0,0 +1,119 @@ +import { Aptos } from '@aptos-labs/ts-sdk' + +import { EndpointId, getNetworkForChainId } from '@layerzerolabs/lz-definitions' + +interface LibraryTimeoutResponse { + expiry: bigint + lib: string +} + +export class Endpoint { + private aptos: Aptos + private endpoint_address: string + constructor(aptos: Aptos, endpoint_address: string) { + this.aptos = aptos + this.endpoint_address = endpoint_address + } + + async getDefaultSendLibrary(eid: EndpointId): Promise { + const result = await this.aptos.view({ + payload: { + function: `${this.endpoint_address}::endpoint::get_default_send_library`, + functionArguments: [eid], + }, + }) + + return result[0] as string + } + + async getSendLibrary(oftAddress: string, dstEid: number): Promise<[string, boolean]> { + try { + const result = await this.aptos.view({ + payload: { + function: `${this.endpoint_address}::endpoint::get_effective_send_library`, + functionArguments: [oftAddress, dstEid], + }, + }) + return [result[0] as string, result[1] as boolean] + } catch (error) { + const toNetwork = getNetworkForChainId(dstEid) + throw new Error( + `Failed to get send library. Network: ${toNetwork.chainName}-${toNetwork.env} might not be supported.` + ) + } + } + + async getReceiveLibrary(oftAddress: string, dstEid: number): Promise<[string, boolean]> { + const result = await this.aptos.view({ + payload: { + function: `${this.endpoint_address}::endpoint::get_effective_receive_library`, + functionArguments: [oftAddress, dstEid], + }, + }) + return [result[0] as string, result[1] as boolean] + } + + async getDefaultReceiveLibraryTimeout(eid: EndpointId): Promise { + const result = await this.aptos.view({ + payload: { + function: `${this.endpoint_address}::endpoint::get_default_receive_library_timeout`, + functionArguments: [eid], + }, + }) + return { + expiry: BigInt(result[0]?.toString() ?? '0'), + lib: result[1] as string, + } + } + + async getReceiveLibraryTimeout(oftAddress: string, dstEid: number): Promise { + try { + const result = await this.aptos.view({ + payload: { + function: `${this.endpoint_address}::endpoint::get_receive_library_timeout`, + functionArguments: [oftAddress, dstEid], + }, + }) + + // result is an array where [0] is expiry and [1] is lib address + return { + expiry: BigInt(result[0]?.toString() ?? '0'), + lib: result[1] as string, + } + } catch (error) { + // if the timeout is not set, it will throw a VM error, so we should return a value that will always produce a diff + return { expiry: BigInt(-1), lib: '' } + } + } + + async getDefaultReceiveLibrary(eid: EndpointId): Promise { + const result = await this.aptos.view({ + payload: { + function: `${this.endpoint_address}::endpoint::get_default_receive_library`, + functionArguments: [eid], + }, + }) + return result[0] as string + } + + async getConfig( + oAppAddress: string, + msgLibAddress: string, + eid: EndpointId, + configType: number + ): Promise { + try { + const result = await this.aptos.view({ + payload: { + function: `${this.endpoint_address}::endpoint::get_config`, + functionArguments: [oAppAddress, msgLibAddress, eid, configType], + }, + }) + return result[0] as Uint8Array + } catch (error) { + throw new Error( + `Failed to get config for Message Library: ${msgLibAddress} on ${getNetworkForChainId(eid).chainName}. Please ensure that the Message Library exists.` + ) + } + } +} diff --git a/packages/devtools-move/sdk/moveVMConnectionBuilder.ts b/packages/devtools-move/sdk/moveVMConnectionBuilder.ts new file mode 100644 index 000000000..a7ee1176d --- /dev/null +++ b/packages/devtools-move/sdk/moveVMConnectionBuilder.ts @@ -0,0 +1,47 @@ +// todo: implement connection builder that reads form yaml and fills in config with movement stuff if needed +// todo replace all mentions of aptos with move-vm instead + +import { Aptos, AptosConfig, Network } from '@aptos-labs/ts-sdk' + +const CHAIN_MOVEMENT = 'movement' +const CHAIN_APTOS = 'aptos' + +const MOVEMENT_INDEXER_URLS = { + [Network.TESTNET]: 'https://indexer.testnet.porto.movementnetwork.xyz/v1/graphql', + [Network.MAINNET]: 'N/A', + [Network.DEVNET]: 'N/A', + [Network.LOCAL]: 'N/A', + [Network.CUSTOM]: 'N/A', +} + +export function getConnection(chain: string, network: Network, fullnode: string, faucet?: string): Aptos { + if (chain === CHAIN_MOVEMENT) { + const indexer = getMovementIndexerUrl(network) + return new Aptos( + new AptosConfig({ + network: Network.CUSTOM, + fullnode: fullnode, + faucet: faucet ?? undefined, + indexer: indexer, + }) + ) + } else { + return new Aptos(new AptosConfig({ network: network })) + } +} + +export function getChain(fullnode: string): string { + if (fullnode.toLowerCase().includes(CHAIN_MOVEMENT)) { + return CHAIN_MOVEMENT + } else { + return CHAIN_APTOS + } +} + +function getMovementIndexerUrl(network: Network): string { + const indexerUrl = MOVEMENT_INDEXER_URLS[network] + if (indexerUrl !== 'N/A') { + return indexerUrl + } + throw new Error('Invalid network') +} diff --git a/packages/devtools-move/sdk/msgLib.ts b/packages/devtools-move/sdk/msgLib.ts new file mode 100644 index 000000000..fec987e2b --- /dev/null +++ b/packages/devtools-move/sdk/msgLib.ts @@ -0,0 +1,122 @@ +import { ExecutorConfig } from '../tasks/move/utils' +import { Aptos } from '@aptos-labs/ts-sdk' + +import { EndpointId } from '@layerzerolabs/lz-definitions' + +interface MoveVectorResponse { + vec: UlnConfig[] +} + +interface UlnConfig { + confirmations: bigint + optional_dvn_threshold: number + optional_dvns: string[] + required_dvns: string[] + use_default_for_confirmations: boolean + use_default_for_optional_dvns: boolean + use_default_for_required_dvns: boolean +} + +interface ExecutorConfigResponse { + executor_address: string + max_message_size: number +} + +interface MoveVectorExecutorResponse { + vec: ExecutorConfigResponse[] +} + +const DEFAULT_ULN_CONFIG: UlnConfig = { + confirmations: BigInt(0), + optional_dvn_threshold: 0, + optional_dvns: [], + required_dvns: [], + use_default_for_confirmations: true, + use_default_for_optional_dvns: true, + use_default_for_required_dvns: true, +} + +const DEFAULT_EXECUTOR_CONFIG: ExecutorConfig = { + executor_address: '', + max_message_size: -1, +} + +export class MsgLib { + private aptos: Aptos + private msgLibAddress: string + constructor(aptos: Aptos, msgLibAddress: string) { + this.aptos = aptos + this.msgLibAddress = msgLibAddress + } + + async get_default_uln_send_config(eid: EndpointId): Promise { + try { + const result = await this.aptos.view({ + payload: { + function: `${this.msgLibAddress}::msglib::get_default_uln_send_config`, + functionArguments: [eid], + }, + }) + const rawConfig = (result[0] as MoveVectorResponse)?.vec[0] + if (!rawConfig) { + return DEFAULT_ULN_CONFIG + } + return { + ...rawConfig, + confirmations: BigInt(rawConfig.confirmations), + optional_dvns: rawConfig.optional_dvns.map(String), + required_dvns: rawConfig.required_dvns.map(String), + optional_dvn_threshold: Number(rawConfig.optional_dvn_threshold), + } + } catch (error) { + return DEFAULT_ULN_CONFIG + } + } + + async get_default_uln_receive_config(eid: EndpointId): Promise { + try { + const result = await this.aptos.view({ + payload: { + function: `${this.msgLibAddress}::msglib::get_default_uln_receive_config`, + functionArguments: [eid], + }, + }) + const rawConfig = (result[0] as MoveVectorResponse)?.vec[0] + if (!rawConfig) { + return DEFAULT_ULN_CONFIG + } + return { + ...rawConfig, + confirmations: BigInt(rawConfig.confirmations), + optional_dvns: rawConfig.optional_dvns.map(String), + required_dvns: rawConfig.required_dvns.map(String), + optional_dvn_threshold: Number(rawConfig.optional_dvn_threshold), + } + } catch (error) { + return DEFAULT_ULN_CONFIG + } + } + + async get_default_executor_config(eid: EndpointId): Promise { + try { + const result = await this.aptos.view({ + payload: { + function: `${this.msgLibAddress}::msglib::get_default_executor_config`, + functionArguments: [eid], + }, + }) + const rawConfig = (result[0] as MoveVectorExecutorResponse)?.vec[0] + if (!rawConfig) { + // In the case where we don't find a default executor config, we return an empty config + return DEFAULT_EXECUTOR_CONFIG + } + return { + executor_address: String(rawConfig.executor_address), + max_message_size: Number(rawConfig.max_message_size), + } + } catch (error) { + // In the case where we don't find a executor config, we return an empty config + return DEFAULT_EXECUTOR_CONFIG + } + } +} diff --git a/packages/devtools-move/sdk/oft.ts b/packages/devtools-move/sdk/oft.ts new file mode 100644 index 000000000..d95ffa08e --- /dev/null +++ b/packages/devtools-move/sdk/oft.ts @@ -0,0 +1,367 @@ +import { + Account, + Aptos, + Ed25519PrivateKey, + InputGenerateTransactionPayloadData, + PrivateKey, + PrivateKeyVariants, + SimpleTransaction, +} from '@aptos-labs/ts-sdk' + +import { EndpointId } from '@layerzerolabs/lz-definitions' + +import { hexAddrToAptosBytesAddr } from './utils' + +export enum OFTType { + OFT_FA = 'oft_fa', + OFT_ADAPTER_FA = 'oft_adapter_fa', + OFT_COIN = 'oft_coin', + OFT_ADAPTER_COIN = 'oft_adapter_coin', +} + +export class OFT { + public moveVMConnection: Aptos + private private_key: string + private signer_account: Account + public oft_address: string + + constructor(moveVMConnection: Aptos, oft_address: string, account_address: string, private_key: string) { + this.moveVMConnection = moveVMConnection + this.oft_address = oft_address + this.private_key = PrivateKey.formatPrivateKey(private_key, PrivateKeyVariants.Ed25519) + this.signer_account = Account.fromPrivateKey({ + privateKey: new Ed25519PrivateKey(this.private_key), + address: account_address, + }) + } + + initializeOFTFAPayload( + token_name: string, + symbol: string, + icon_uri: string, + project_uri: string, + shared_decimals: number, + local_decimals: number + ): InputGenerateTransactionPayloadData { + const encoder = new TextEncoder() + return { + function: `${this.oft_address}::oft_fa::initialize`, + functionArguments: [ + encoder.encode(token_name), + encoder.encode(symbol), + encoder.encode(icon_uri), + encoder.encode(project_uri), + shared_decimals, + local_decimals, + ], + } + } + + initializeAdapterFAPayload( + tokenMetadataAddress: string, + sharedDecimals: number + ): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::oft_adapter_fa::initialize`, + functionArguments: [tokenMetadataAddress, sharedDecimals], + } + } + + createSetRateLimitTx( + eid: EndpointId, + limit: number | bigint, + window_seconds: number | bigint, + oftType: OFTType + ): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::${oftType}::set_rate_limit`, + functionArguments: [eid, limit, window_seconds], + } + } + + createUnsetRateLimitTx(eid: EndpointId, oftType: OFTType): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::${oftType}::unset_rate_limit`, + functionArguments: [eid], + } + } + + // returns (limit, window_seconds) + async getRateLimitConfig(eid: EndpointId, oftType: OFTType): Promise<[bigint, bigint]> { + const result = await this.moveVMConnection.view({ + payload: { + function: `${this.oft_address}::${oftType}::rate_limit_config`, + functionArguments: [eid], + }, + }) + const limit = typeof result[0] === 'string' ? BigInt(result[0]) : (result[0] as bigint) + const window = typeof result[1] === 'string' ? BigInt(result[1]) : (result[1] as bigint) + return [limit, window] + } + + createSetFeeBpsTx(fee_bps: number | bigint, oftType: OFTType): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::${oftType}::set_fee_bps`, + functionArguments: [fee_bps], + } + } + + async getFeeBps(oftType: OFTType): Promise { + const result = await this.moveVMConnection.view({ + payload: { + function: `${this.oft_address}::${oftType}::fee_bps`, + functionArguments: [], + }, + }) + const feeBps = result[0] + return typeof feeBps === 'string' ? BigInt(feeBps) : (feeBps as bigint) + } + + mintPayload(recipient: string, amount: number | bigint): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::oft_fa::mint`, + functionArguments: [recipient, amount], + } + } + + async getBalance(account: string): Promise { + const result = await this.moveVMConnection.view({ + payload: { + function: `${this.oft_address}::oft::balance`, + functionArguments: [account], + }, + }) + + return result[0] as number + } + async quoteSend( + userSender: string, + dst_eid: number, + to: Uint8Array, + amount_ld: number | bigint, + min_amount_ld: number | bigint, + extra_options: Uint8Array, + compose_message: Uint8Array, + oft_cmd: Uint8Array, + pay_in_zro: boolean + ): Promise<[number, number]> { + const result = await this.moveVMConnection.view({ + payload: { + function: `${this.oft_address}::oft::quote_send`, + functionArguments: [ + userSender, + dst_eid, + to, + amount_ld, + min_amount_ld, + extra_options, + compose_message, + oft_cmd, + pay_in_zro, + ], + }, + }) + + return [result[0] as number, result[1] as number] + } + + // Calls send withdraw on the oft + sendPayload( + dst_eid: number, + to: Uint8Array, + amount_ld: number | bigint, + min_amount_ld: number | bigint, + extra_options: Uint8Array, + compose_message: Uint8Array, + oft_cmd: Uint8Array, + native_fee: number | bigint, + zro_fee: number | bigint + ): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::oft::send_withdraw`, + functionArguments: [ + dst_eid, + to, + amount_ld, + min_amount_ld, + extra_options, + compose_message, + oft_cmd, + native_fee, + zro_fee, + ], + } + } + + setPeerPayload(eid: EndpointId, peerAddress: string): InputGenerateTransactionPayloadData { + const peerAddressAsBytes = hexAddrToAptosBytesAddr(peerAddress) + return { + function: `${this.oft_address}::oapp_core::set_peer`, + functionArguments: [eid, peerAddressAsBytes], + } + } + + setDelegatePayload(delegateAddress: string): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::oapp_core::set_delegate`, + functionArguments: [delegateAddress], + } + } + + async getDelegate(): Promise { + const result = await this.moveVMConnection.view({ + payload: { + function: `${this.oft_address}::oapp_core::get_delegate`, + functionArguments: [], + }, + }) + + return result[0] as string + } + + async getAdmin(): Promise { + const result = await this.moveVMConnection.view({ + payload: { + function: `${this.oft_address}::oapp_core::get_admin`, + functionArguments: [], + }, + }) + + return result[0] as string + } + + transferAdminPayload(adminAddress: string): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::oapp_core::transfer_admin`, + functionArguments: [adminAddress], + } + } + + transferObjectPayload(object_address: string, new_owner_address: string): InputGenerateTransactionPayloadData { + return { + function: '0x1::object::transfer', + typeArguments: [`0x1::object::ObjectCore`], + functionArguments: [object_address, new_owner_address], + } + } + + renounceAdminPayload(): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::oapp_core::renounce_admin`, + functionArguments: [], + } + } + + async getPeer(eid: EndpointId): Promise { + const result = await this.moveVMConnection.view({ + payload: { + function: `${this.oft_address}::oapp_core::get_peer`, + functionArguments: [eid], + }, + }) + + return result[0] as string + } + + async hasPeer(eid: EndpointId): Promise { + const result = await this.moveVMConnection.view({ + payload: { + function: `${this.oft_address}::oapp_core::has_peer`, + functionArguments: [eid], + }, + }) + + return result[0] as boolean + } + + setEnforcedOptionsPayload( + eid: number, + msgType: number, + enforcedOptions: Uint8Array + ): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::oapp_core::set_enforced_options`, + functionArguments: [eid, msgType, enforcedOptions], + } + } + + async getEnforcedOptions(eid: number, msgType: number): Promise { + const result = await this.moveVMConnection.view({ + payload: { + function: `${this.oft_address}::oapp_core::get_enforced_options`, + functionArguments: [eid, msgType], + }, + }) + + return result[0] as string + } + + setSendLibraryPayload(remoteEid: number, msglibAddress: string): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::oapp_core::set_send_library`, + functionArguments: [remoteEid, msglibAddress], + } + } + + setReceiveLibraryPayload( + remoteEid: number, + msglibAddress: string, + gracePeriod: number + ): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::oapp_core::set_receive_library`, + functionArguments: [remoteEid, msglibAddress, gracePeriod], + } + } + + setReceiveLibraryTimeoutPayload( + remoteEid: number, + msglibAddress: string, + expiry: number + ): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::oapp_core::set_receive_library_timeout`, + functionArguments: [remoteEid, msglibAddress, expiry], + } + } + + setConfigPayload( + msgLibAddress: string, + eid: number, + configType: number, + config: Uint8Array + ): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::oapp_core::set_config`, + functionArguments: [msgLibAddress, eid, configType, config], + } + } + + irrevocablyDisableBlocklistPayload(oftType: OFTType): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::${oftType}::irrevocably_disable_blocklist`, + functionArguments: [], + } + } + + permanentlyDisableFungibleStoreFreezingPayload(): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::oft_fa::permanently_disable_fungible_store_freezing`, + functionArguments: [], + } + } + + async signSubmitAndWaitForTx(transaction: SimpleTransaction) { + const signedTransaction = await this.moveVMConnection.signAndSubmitTransaction({ + signer: this.signer_account, + transaction: transaction, + }) + + const executedTransaction = await this.moveVMConnection.waitForTransaction({ + transactionHash: signedTransaction.hash, + }) + console.log('Transaction executed.') + + return executedTransaction + } +} diff --git a/packages/devtools-move/sdk/utils.ts b/packages/devtools-move/sdk/utils.ts new file mode 100644 index 000000000..58bd3f3d9 --- /dev/null +++ b/packages/devtools-move/sdk/utils.ts @@ -0,0 +1,15 @@ +/** + * Converts a hexadecimal address string to a 32-byte Uint8Array format used by Aptos + * @param address - The hex address string to convert, optionally starting with '0x'. Can be null/undefined. + * @returns A 32-byte Uint8Array with the address right-aligned (padded with zeros on the left) + * + * If the input is null/undefined, returns an empty 32-byte array. + * Otherwise, removes '0x' prefix if present, converts hex string to bytes, + * and right-aligns the result in a 32-byte array. + */ +export function hexAddrToAptosBytesAddr(address: string | null | undefined): Uint8Array { + const bytes = address ? Buffer.from(address.replace('0x', ''), 'hex') : new Uint8Array(0) + const bytes32 = new Uint8Array(32) + bytes32.set(bytes, 32 - bytes.length) + return bytes32 +} diff --git a/packages/devtools-move/tasks/evm/utils/anvilForkNode.ts b/packages/devtools-move/tasks/evm/utils/anvilForkNode.ts new file mode 100644 index 000000000..cd2f82b02 --- /dev/null +++ b/packages/devtools-move/tasks/evm/utils/anvilForkNode.ts @@ -0,0 +1,91 @@ +import { spawn } from 'child_process' +import net from 'net' + +import type { AnvilNode, eid } from './types' + +/** + * @description Class to manage Anvil fork nodes + * @param forkUrls - Array of URLs to fork from + * @param eids - Array of EIDs to associate with the fork URLs + * @param startingPort - Starting port number for the Anvil fork nodes (default: 8545) + * @dev Used to create anvil forks to run simulations on before sending them on-chain + * @returns AnvilForkNode instance + */ +class AnvilForkNode { + public nodes: AnvilNode[] = [] + // Maps EIDs to RPC URLs + public forkUrlsMap: Record = {} + + constructor( + private eidRpcMap: Record, + private startingPort = 8545 + ) {} + + // Start the Anvil fork nodes at incremental ports + public async startNodes(): Promise> { + // const numPorts = Object.keys(this.eidRpcMap).length + // await this.checkFreePorts(this.startingPort, numPorts) + + for (const [eid, rpcUrl] of Object.entries(this.eidRpcMap)) { + console.log(`Starting Anvil node on port ${this.startingPort} using fork URL ${rpcUrl}...`) + const node = await this.startAnvilForkNode(rpcUrl, this.startingPort.toString()) + this.nodes.push(node) + this.forkUrlsMap[eid] = node.rpcUrl + this.startingPort++ + } + + return this.forkUrlsMap + } + + // Method to start an individual Anvil fork node + private startAnvilForkNode(forkUrl: string, port: string): Promise { + return new Promise((resolve) => { + const anvilNode = spawn('anvil', ['--fork-url', forkUrl, '--port', port], { + stdio: 'ignore', // Silence output so you don't see the logs + }) + + // Wait for the node to start + setTimeout(() => { + resolve({ + process: anvilNode, + rpcUrl: `http://127.0.0.1:${port}`, + }) + }, 1000) + }) + } + + private async checkFreePorts(startingPort: number, numPorts: number): Promise { + for (let i = 0; i < numPorts; i++) { + const port = startingPort + i + const isFree = await this.checkFreePort(port) + if (!isFree) { + throw new Error(`Port ${port} is not free - please kill running process or change the starting port`) + } + } + return true + } + + private async checkFreePort(port: number): Promise { + return new Promise((resolve) => { + const server = net.createServer() + server.on('error', () => resolve(false)) + server.listen(port, () => resolve(true)) + server.close() + }) + } + // Method to stop all running Anvil fork nodes + public killNodes(): void { + for (const node of this.nodes) { + console.log(`Stopping Anvil node at ${node.rpcUrl}`) + node.process.kill() + } + this.nodes = [] + } + + // Method to get the RPC URLs of the running Anvil fork nodes + public getRpcMap(): Record { + return this.forkUrlsMap + } +} + +export default AnvilForkNode diff --git a/packages/devtools-move/tasks/evm/utils/libraryConfigUtils.ts b/packages/devtools-move/tasks/evm/utils/libraryConfigUtils.ts new file mode 100644 index 000000000..79827c7e6 --- /dev/null +++ b/packages/devtools-move/tasks/evm/utils/libraryConfigUtils.ts @@ -0,0 +1,137 @@ +import { Contract, PopulatedTransaction, utils, constants } from 'ethers' + +import { Uln302ExecutorConfig, Uln302UlnUserConfig } from '@layerzerolabs/toolbox-hardhat' + +import { returnChecksum, returnChecksums } from './types' + +import type { SetConfigParam, address, eid } from './types' + +const DEFAULT_CONFIG_MESSAGE = `${constants.AddressZero} - DEFAULT` + +export async function getConfig( + epv2Contract: Contract, + evmAddress: address, + libraryAddress: address, + aptosEid: eid, + configType: number, + isSendConfig: boolean +): Promise { + let isDefault: boolean = false + + if (isSendConfig) { + isDefault = await epv2Contract.isDefaultSendLibrary(evmAddress, aptosEid) + } else { + const receiveConfig = await epv2Contract.getReceiveLibrary(evmAddress, aptosEid) + isDefault = receiveConfig.isDefault + } + + if (isDefault) { + return DEFAULT_CONFIG_MESSAGE + } + + const config = await epv2Contract.getConfig(evmAddress, libraryAddress, aptosEid, configType) + + return config +} + +export function setConfig( + epv2Contract: Contract, + evmAddress: address, + libraryAddress: address, + config: SetConfigParam[] +): Promise { + return epv2Contract.populateTransaction.setConfig(evmAddress, libraryAddress, config) +} + +export function buildConfig( + ulnConfig: Uln302UlnUserConfig, + executorConfig: Uln302ExecutorConfig | null = null +): { executorConfigBytes: string; ulnConfigBytes: string } { + if (!ulnConfig.optionalDVNs) { + ulnConfig.optionalDVNs = [] + } + const _optionalDVNs = returnChecksums(ulnConfig.optionalDVNs) + const _requiredDVNs = returnChecksums(ulnConfig.requiredDVNs) + + const ulnConfigBytes = utils.defaultAbiCoder.encode( + [ + 'tuple(uint64 confirmations, uint8 requiredDVNCount, uint8 optionalDVNCount, uint8 optionalDVNThreshold, address[] requiredDVNs, address[] optionalDVNs)', + ], + [ + { + confirmations: ulnConfig.confirmations, + requiredDVNCount: _requiredDVNs.length, + optionalDVNCount: _optionalDVNs.length, + optionalDVNThreshold: ulnConfig.optionalDVNThreshold, + requiredDVNs: _requiredDVNs.sort(), + optionalDVNs: _optionalDVNs.sort(), + }, + ] + ) + + if (executorConfig !== null) { + const _executor = returnChecksum(executorConfig.executor) + const executorConfigBytes = utils.defaultAbiCoder.encode( + ['uint32', 'address'], + [executorConfig.maxMessageSize, _executor] + ) + + return { executorConfigBytes, ulnConfigBytes } + } + return { executorConfigBytes: '0x', ulnConfigBytes } +} + +export function decodeConfig(configParam: SetConfigParam[]) { + const decodedConfig: { + executorConfig: Uln302ExecutorConfig + ulnConfig: Uln302UlnUserConfig + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } = {} as any + try { + for (const { configType, config } of configParam) { + if (configType === 1) { + if (config === DEFAULT_CONFIG_MESSAGE) { + decodedConfig.executorConfig = { + maxMessageSize: 0, + executor: constants.AddressZero, + } + continue + } + + const decoded = utils.defaultAbiCoder.decode(['uint32', 'address'], config) + decodedConfig.executorConfig = { + maxMessageSize: decoded[0], + executor: decoded[1], + } + } else if (configType === 2) { + if (config === DEFAULT_CONFIG_MESSAGE) { + decodedConfig.ulnConfig = { + confirmations: BigInt(0), + requiredDVNs: [], + optionalDVNs: [], + optionalDVNThreshold: 0, + } + continue + } + + const decoded = utils.defaultAbiCoder.decode( + [ + 'tuple(uint64 confirmations, uint8 requiredDVNCount, uint8 optionalDVNCount, uint8 optionalDVNThreshold, address[] requiredDVNs, address[] optionalDVNs)', + ], + config + ) + + decodedConfig.ulnConfig = { + confirmations: BigInt(decoded[0]['confirmations']), + requiredDVNs: decoded[0]['requiredDVNs'], + optionalDVNs: decoded[0]['optionalDVNs'], + optionalDVNThreshold: decoded[0]['optionalDVNThreshold'], + } + } + } + } catch (e) { + throw new Error('unable to decode config') + } + + return decodedConfig +} diff --git a/packages/devtools-move/tasks/evm/utils/types.ts b/packages/devtools-move/tasks/evm/utils/types.ts new file mode 100644 index 000000000..4ca926438 --- /dev/null +++ b/packages/devtools-move/tasks/evm/utils/types.ts @@ -0,0 +1,116 @@ +import { ChildProcess } from 'child_process' + +import { PopulatedTransaction, ethers, providers } from 'ethers' + +import type { OAppEdgeConfig, OAppNodeConfig } from '@layerzerolabs/toolbox-hardhat' + +export type TxTypes = + | 'setPeer' + | 'setDelegate' + | 'setEnforcedOptions' + | 'setSendLibrary' + | 'setReceiveLibrary' + | 'setReceiveLibraryTimeout' + | 'sendConfig' + | 'receiveConfig' + +export type eid = string +type EidTx = { + toEid: eid + populatedTx: PopulatedTransaction +} +export type EidTxMap = Record +export type address = string + +type PeerOApp = { + eid: eid + address: address +} + +export type ContractMetadata = { + address: { + oapp: address + epv2: address + } + contract: { + oapp: ethers.Contract + epv2: ethers.Contract + } + peers: PeerOApp[] + provider: ethers.providers.JsonRpcProvider + configAccount: OAppNodeConfig + configOapp: OAppEdgeConfig | undefined +} + +export type AccountData = { + [eid: string]: { + gasPrice: ethers.BigNumber + nonce: number + signer: ethers.Wallet + } +} +//[TxTypes][eid] = PopulatedTransaction +export type TxEidMapping = Record + +//[fromEid as number] = ContractMetadata +export type OmniContractMetadataMapping = Record + +export type TxPool = { + from_eid: eid + raw: ethers.PopulatedTransaction + response: Promise | undefined +} + +export type TxReceipt = { + src_eid: string + dst_eid: string + src_from: string + src_to: string + tx_hash: string | undefined + data: string +} +export type TxReceiptJson = Record // txType -> txReceipt + +export type enforcedOptionParam = { + eid: eid + msgType: number + options: string +} + +export type RecvLibParam = { + lib: address + isDefault: boolean +} + +export type RecvLibraryTimeoutConfig = { + lib: address + expiry: bigint +} + +export type SetConfigParam = { + eid: eid + configType: number + config: string +} + +export type AnvilNode = { process: ChildProcess; rpcUrl: string } + +export function returnChecksums(addresses: string[]): string[] { + const checksumAddresses: string[] = [] + for (const address of addresses) { + try { + checksumAddresses.push(returnChecksum(address)) + } catch (error) { + console.error(`Invalid address: ${address}. Error: ${error}`) + } + } + return checksumAddresses +} + +export function returnChecksum(address: string): string { + try { + return ethers.utils.getAddress(address) + } catch (error) { + throw new Error(`Invalid address: ${address}. Error: ${error}`) + } +} diff --git a/packages/devtools-move/tasks/evm/utils/validateOmnicontracts.ts b/packages/devtools-move/tasks/evm/utils/validateOmnicontracts.ts new file mode 100644 index 000000000..fe882cf5c --- /dev/null +++ b/packages/devtools-move/tasks/evm/utils/validateOmnicontracts.ts @@ -0,0 +1,94 @@ +import { ethers } from 'ethers' + +import type { OmniContractMetadataMapping } from './types' +import { getNetworkForChainId } from '@layerzerolabs/lz-definitions' + +export async function validateOmniContractsOrTerminate(omniContracts: OmniContractMetadataMapping) { + let shouldNotTerminate = true + shouldNotTerminate = shouldNotTerminate && (await validateRpcUrl(omniContracts)) + shouldNotTerminate = shouldNotTerminate && (await validateEidSupport(omniContracts)) + + if (!shouldNotTerminate) { + process.exit(1) + } + return shouldNotTerminate +} + +export async function validateRpcUrl(omniContracts: OmniContractMetadataMapping): Promise { + let shouldNotTerminate = true + const badRpcUrls: Record = {} + + for (const [eid, { provider }] of Object.entries(omniContracts)) { + let blockNumber = 0 + try { + blockNumber = await getBlockNumber(provider) + } catch (error) { + blockNumber = 0 + } + + if (blockNumber === 0) { + badRpcUrls[eid] = provider.connection.url + } + } + + if (Object.keys(badRpcUrls).length > 0) { + console.error( + `The following EIDs have an invalid RPC URL (block number returned by them is 0):\n${Object.entries( + badRpcUrls + ) + .map(([eid, url]) => `EID: ${eid}\t RPC URL: ${url}`) + .join('\n')}` + ) + shouldNotTerminate = false + } + + return shouldNotTerminate +} + +export async function validateEidSupport(omniContracts: OmniContractMetadataMapping): Promise { + let shouldNotTerminate = true + const unsupportedEids: Record = {} + + for (const [eid, { contract, peers }] of Object.entries(omniContracts)) { + const epv2 = contract.epv2 + + for (const peer of peers) { + const peerEid = peer.eid + + let isSupported = false + try { + isSupported = await epv2.isSupportedEid(peerEid) + } catch (error) { + if (!unsupportedEids[eid]) { + unsupportedEids[eid] = [] + } + isSupported = false + } + + if (!isSupported) { + unsupportedEids[eid].push(peerEid) + } + } + } + + if (Object.keys(unsupportedEids).length > 0) { + console.error( + 'The following EIDs are not supported by the EPV2 contract (endpointAddress::isSupportedEid(u32))' + ) + for (const [eid, badEids] of Object.entries(unsupportedEids)) { + const badNetworks = badEids.map((eid) => getNetworkForChainId(parseInt(eid))) + const badNetworkNames = badNetworks.map((network) => `${network.chainName}-${network.env}`) + const network = getNetworkForChainId(parseInt(eid)) + console.error( + `${network.chainName}-${network.env}\t EndpointV2: ${omniContracts[eid].contract.epv2.address}\t Unsupported networks: ${badNetworkNames.join(', ')}` + ) + } + shouldNotTerminate = false + } + + return shouldNotTerminate +} + +export async function getBlockNumber(provider: ethers.providers.JsonRpcProvider): Promise { + return await provider.getBlockNumber() +} diff --git a/packages/devtools-move/tasks/evm/wire-evm.ts b/packages/devtools-move/tasks/evm/wire-evm.ts new file mode 100644 index 000000000..4c6e5d223 --- /dev/null +++ b/packages/devtools-move/tasks/evm/wire-evm.ts @@ -0,0 +1,181 @@ +import { Contract, ethers } from 'ethers' + +import { getDeploymentAddressAndAbi } from '@layerzerolabs/lz-evm-sdk-v2' +import { getNetworkForChainId, ChainType, endpointIdToChainType } from '@layerzerolabs/lz-definitions' + +import { createSetDelegateTransactions } from './wire/setDelegate' +import { createSetEnforcedOptionsTransactions } from './wire/setEnforcedOptions' +import { createSetPeerTransactions } from './wire/setPeer' +import { createSetReceiveConfigTransactions } from './wire/setReceiveConfig' +import { createSetReceiveLibraryTransactions } from './wire/setReceiveLibrary' +import { createSetSendConfigTransactions } from './wire/setSendConfig' +import { createSetSendLibraryTransactions } from './wire/setSendLibrary' +import { executeTransactions } from './wire/transactionExecutor' +import { createSetReceiveLibraryTimeoutTransactions } from './wire/setReceiveLibraryTimeout' + +import AnvilForkNode from './utils/anvilForkNode' +import { createEidToNetworkMapping, getConfigConnectionsFromChainType, getHHAccountConfig } from '../shared/utils' +import { basexToBytes32 } from '../shared/basexToBytes32' +import type { OmniContractMetadataMapping, TxEidMapping } from './utils/types' +import { validateOmniContractsOrTerminate } from './utils/validateOmnicontracts' + +import fs from 'fs' +import path from 'path' +import dotenv from 'dotenv' + +/** + * @description Handles wiring of EVM contracts with the Aptos OApp + * @dev Creates ethers's populated transactions for the various transaction types (setPeer, setDelegate, setEnforcedOptions, setSendLibrary, setReceiveLibrary, setReceiveLibraryTimeout). It then simulates them on a forked network before executing + */ +export async function createEvmOmniContracts(args: any, privateKey: string, chainType: ChainType = ChainType.EVM) { + const globalConfigPath = path.resolve(path.join(args.rootDir, args.oapp_config)) + const connectionsToWire = await getConfigConnectionsFromChainType('from', chainType, globalConfigPath) + const accountConfigs = await getHHAccountConfig(globalConfigPath) + const networks = await createEidToNetworkMapping('networkName') + const rpcUrls = await createEidToNetworkMapping('url') + + // Indexed by the eid it contains information about the contract, provider, and configuration of the account and oapp. + const omniContracts: OmniContractMetadataMapping = {} + + /* + * Looping through the connections we build out the omniContracts and TxTypeEidMapping by reading from the deployment files. + * omniContracts contains ethers Contract objects for the OApp and EndpointV2 contracts. + */ + for (const conn of connectionsToWire) { + const fromEid = conn.from.eid + const toEid = conn.to.eid.toString() + const fromNetwork = networks[fromEid] + const toNetwork = networks[toEid] + const configOapp = conn?.config + + const provider = new ethers.providers.JsonRpcProvider(rpcUrls[fromEid]) + const signer = new ethers.Wallet(privateKey, provider) + + const OAppDeploymentPath = path.resolve(`deployments/${fromNetwork}/${conn.from.contractName}.json`) + const OAppDeploymentData = JSON.parse(fs.readFileSync(OAppDeploymentPath, 'utf8')) + + const WireOAppDeploymentPath = path.resolve(`deployments/${toNetwork}/${conn.to.contractName}.json`) + const WireOAppDeploymentData = JSON.parse(fs.readFileSync(WireOAppDeploymentPath, 'utf8')) + + const lzFromNetwork = getNetworkForChainId(fromEid) + const lzFromNetworkString = `${lzFromNetwork.chainName}-${lzFromNetwork.env}` + const EndpointV2DeploymentData = getDeploymentAddressAndAbi(lzFromNetworkString, 'EndpointV2') + + const { address: oappAddress, abi: oappAbi } = OAppDeploymentData + const { address: epv2Address, abi: epv2Abi } = EndpointV2DeploymentData + + const OAppContract = new Contract(oappAddress, oappAbi, signer) + const EPV2Contract = new Contract(epv2Address, epv2Abi, signer) + + const currPeers = omniContracts[fromEid]?.peers ?? [] + + let peerAddress + + switch (endpointIdToChainType(parseInt(toEid))) { + case ChainType.SOLANA: + peerAddress = WireOAppDeploymentData.oftStore + break + default: // EVM and Aptos + peerAddress = WireOAppDeploymentData.address + } + + const peer = { eid: toEid, address: basexToBytes32(peerAddress, toEid) } + currPeers.push(peer) + + omniContracts[fromEid] = { + address: { + oapp: oappAddress, + epv2: epv2Address, + }, + contract: { + oapp: OAppContract, + epv2: EPV2Contract, + }, + peers: currPeers, + provider: provider, + configAccount: accountConfigs[fromEid], + configOapp: configOapp, + } + } + return omniContracts +} + +export function readPrivateKey(args: any) { + const envPath = path.resolve(path.join(args.rootDir, '.env')) + const env = dotenv.config({ path: envPath }) + if (!env.parsed || env.error?.message !== undefined) { + console.error('Failed to load .env file.') + process.exit(1) + } + + let privateKey + const evmMnemonicIndex = parseInt(args.mnemonic_index) + if (evmMnemonicIndex < 0) { + privateKey = env.parsed.EVM_PRIVATE_KEY + } else { + const mnemonic = env.parsed.EVM_MNEMONIC + console.log('Using mnemonic:', mnemonic) + console.log('Using mnemonic index:', evmMnemonicIndex) + privateKey = ethers.Wallet.fromMnemonic(mnemonic, `m/44'/60'/0'/0/${evmMnemonicIndex}`).privateKey + } + + if (!privateKey) { + console.error('EVM_PRIVATE_KEY is not set in .env file') + process.exit(1) + } + + return privateKey +} + +async function wireEvm(args: any) { + const privateKey = readPrivateKey(args) + + const omniContracts = await createEvmOmniContracts(args, privateKey, ChainType.EVM) + await validateOmniContractsOrTerminate(omniContracts) + + // Build a Transaction mapping for each type of transaction. It is further indexed by the eid. + const TxTypeEidMapping: TxEidMapping = { + setPeer: {}, + setDelegate: {}, + setEnforcedOptions: {}, + setSendLibrary: {}, + setReceiveLibrary: {}, + setReceiveLibraryTimeout: {}, + sendConfig: {}, + receiveConfig: {}, + } + + TxTypeEidMapping.setPeer = await createSetPeerTransactions(omniContracts) + TxTypeEidMapping.setDelegate = await createSetDelegateTransactions(omniContracts) + TxTypeEidMapping.setEnforcedOptions = await createSetEnforcedOptionsTransactions(omniContracts) + TxTypeEidMapping.setSendLibrary = await createSetSendLibraryTransactions(omniContracts) + TxTypeEidMapping.setReceiveLibrary = await createSetReceiveLibraryTransactions(omniContracts) + TxTypeEidMapping.sendConfig = await createSetSendConfigTransactions(omniContracts) + TxTypeEidMapping.receiveConfig = await createSetReceiveConfigTransactions(omniContracts) + TxTypeEidMapping.setReceiveLibraryTimeout = await createSetReceiveLibraryTimeoutTransactions(omniContracts) + + // @todo Clean this up or move to utils + const rpcUrlSelfMap: { [eid: string]: string } = {} + for (const [eid, eidData] of Object.entries(omniContracts)) { + rpcUrlSelfMap[eid] = eidData.provider.connection.url + } + + let anvilForkNode: AnvilForkNode | null = null + try { + anvilForkNode = new AnvilForkNode(rpcUrlSelfMap, 8545) + + if (args.simulate === 'true') { + const forkRpcMap = await anvilForkNode.startNodes() + await executeTransactions(omniContracts, TxTypeEidMapping, forkRpcMap, 'dry-run', privateKey, args) + } else { + console.warn('--simulate set to false\n Skipping simulation and going directly to broadcast') + } + await executeTransactions(omniContracts, TxTypeEidMapping, rpcUrlSelfMap, 'broadcast', privateKey, args) + } catch (error) { + throw new Error(`Failed to wire EVM contracts: ${error}`) + } finally { + anvilForkNode?.killNodes() + } +} + +export { wireEvm } diff --git a/packages/devtools-move/tasks/evm/wire/setDelegate.ts b/packages/devtools-move/tasks/evm/wire/setDelegate.ts new file mode 100644 index 000000000..ee9f668c9 --- /dev/null +++ b/packages/devtools-move/tasks/evm/wire/setDelegate.ts @@ -0,0 +1,54 @@ +import { Contract, utils } from 'ethers' + +import { diffPrinter } from '../../shared/utils' + +import { createDiffMessage, printAlreadySet, printNotSet, logPathwayHeader } from '../../shared/messageBuilder' + +import type { OmniContractMetadataMapping, EidTxMap } from '../utils/types' + +/** + * @notice Sets delegate for a contract. + * @dev Fetches the current delegate from EndpointV2 + * @dev Sets the new delegate on the OApp + * @returns EidTxMap + */ +export async function createSetDelegateTransactions(eidDataMapping: OmniContractMetadataMapping): Promise { + const txTypePool: EidTxMap = {} + logPathwayHeader('setDelegate') + for (const [eid, { peers, address, contract, configAccount }] of Object.entries(eidDataMapping)) { + const peerToEid = peers[0].eid + const currDelegate = await getDelegate(contract.epv2, address.oapp) + + if (!configAccount?.delegate) { + printNotSet('delegate - not found in config', Number(eid), Number(eid)) + continue + } + + const newDelegate = utils.getAddress(configAccount.delegate) + + if (currDelegate === newDelegate) { + printAlreadySet('delegate', Number(eid), Number(eid)) + continue + } + + const diffMessage = createDiffMessage('delegate', Number(eid), Number(eid)) + diffPrinter(diffMessage, { delegate: currDelegate }, { delegate: newDelegate }) + + const tx = await contract.oapp.populateTransaction.setDelegate(newDelegate) + + txTypePool[eid] = txTypePool[eid] ?? [] + txTypePool[eid].push({ + toEid: peerToEid, + populatedTx: tx, + }) + } + + return txTypePool +} + +export async function getDelegate(epv2Contract: Contract, oappAddress: string) { + const delegate = await epv2Contract.delegates(oappAddress) + const delegateAddress = utils.getAddress(delegate) + + return delegateAddress +} diff --git a/packages/devtools-move/tasks/evm/wire/setEnforcedOptions.ts b/packages/devtools-move/tasks/evm/wire/setEnforcedOptions.ts new file mode 100644 index 000000000..149d43018 --- /dev/null +++ b/packages/devtools-move/tasks/evm/wire/setEnforcedOptions.ts @@ -0,0 +1,185 @@ +import { Contract } from 'ethers' + +import { + ComposeOption, + ExecutorLzReceiveOption, + ExecutorNativeDropOption, + ExecutorOptionType, + Options, +} from '@layerzerolabs/lz-v2-utilities' + +import { createDiffMessage, printAlreadySet, printNotSet, logPathwayHeader } from '../../shared/messageBuilder' + +import { diffPrinter } from '../../shared/utils' + +import type { OmniContractMetadataMapping, EidTxMap, enforcedOptionParam } from '../utils/types' + +type optionTypes = boolean | ExecutorLzReceiveOption | ExecutorNativeDropOption | ComposeOption | undefined + +/** + * @notice Sets EnforcedOptions for a contract. + * @dev Fetches the current enforcedOptions from Oapp + * @dev Sets the new enforcedOptions on the Oapp + * @returns EidTxMap + */ +export async function createSetEnforcedOptionsTransactions( + eidDataMapping: OmniContractMetadataMapping +): Promise { + const txTypePool: EidTxMap = {} + logPathwayHeader('setEnforcedOptions') + for (const [eid, { peers, contract, configOapp }] of Object.entries(eidDataMapping)) { + for (const peer of peers) { + const { eid: peerToEid } = peer + if (!configOapp?.enforcedOptions) { + printNotSet('enforced options - not found in config', Number(eid), Number(peerToEid)) + continue + } + const toEnforcedOptions = configOapp.enforcedOptions + const thisEnforcedOptionBuilder: Record = {} + + // Iterate over toEnforcedOptions and reduce by msgType + for (const currEnforcedOption of toEnforcedOptions) { + if (!thisEnforcedOptionBuilder[currEnforcedOption.msgType]) { + thisEnforcedOptionBuilder[currEnforcedOption.msgType] = Options.newOptions() + } + + thisEnforcedOptionBuilder[currEnforcedOption.msgType] = reduceOptionsByMsgType( + thisEnforcedOptionBuilder[currEnforcedOption.msgType], + currEnforcedOption + ) + } + + // Extract the msgTypes + const msgTypes = Object.keys(thisEnforcedOptionBuilder).map((msgType) => Number(msgType)) + + // Populate the arguments for the transaction function call + const enforcedOptionParams: enforcedOptionParam[] = [] + + const diffcurrOptions: Record = {} + const diffnewOptions: Record = {} + + for (const msgType of msgTypes) { + const currOptions = await getEnforcedOption(contract.oapp, peerToEid, msgType) + const newOptions = thisEnforcedOptionBuilder[msgType].toHex() + + if (currOptions === newOptions) { + printAlreadySet(`enforced options - msgType ${msgType}`, Number(eid), Number(peerToEid)) + } else { + diffcurrOptions[msgType] = currOptions + diffnewOptions[msgType] = newOptions + + enforcedOptionParams.push({ + eid: peerToEid, + msgType: msgType, + options: newOptions, + }) + + const currOptionsDecoded: Record = {} + const newOptionsDecoded: Record = {} + const optionTypeCount = Object.keys(ExecutorOptionType).length / 2 + + for (let i = 1; i <= optionTypeCount; i++) { + const currOption = decodeOptionsByMsgType(currOptions, i) + const newOption = decodeOptionsByMsgType(newOptions, i) + + if (typeof currOption === 'object' && typeof newOption === 'object') { + const newOptionKeys = Object.keys(newOption) + + if (newOptionKeys.length > 0) { + const optionTypeName = ExecutorOptionType[i] + currOptionsDecoded[`${optionTypeName}`] = currOption + newOptionsDecoded[`${optionTypeName}`] = newOption + } + } + } + + diffPrinter( + createDiffMessage('enforced options', Number(eid), Number(peerToEid)), + currOptionsDecoded, + newOptionsDecoded + ) + const tx = await contract.oapp.populateTransaction.setEnforcedOptions(enforcedOptionParams) + + txTypePool[eid] = txTypePool[eid] ?? [] + txTypePool[eid].push({ + toEid: peerToEid, + populatedTx: tx, + }) + } + } + } + } + + return txTypePool +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function reduceOptionsByMsgType(baseOptions: Options, addOption: any): Options { + const optionType = addOption.optionType + switch (optionType) { + case ExecutorOptionType.LZ_RECEIVE: + baseOptions.addExecutorLzReceiveOption(addOption.gas, addOption.value) + break + case ExecutorOptionType.NATIVE_DROP: + baseOptions.addExecutorNativeDropOption(addOption.amount, addOption.receiver) + break + case ExecutorOptionType.COMPOSE: + baseOptions.addExecutorComposeOption(Number(addOption.gas), addOption.value) + break + case ExecutorOptionType.ORDERED: + baseOptions.addExecutorOrderedExecutionOption() + break + case ExecutorOptionType.LZ_READ: + baseOptions.addExecutorLzReadOption(addOption.gas, addOption.value) + break + default: + throw new Error(`Unknown option type: ${optionType}`) + } + + return baseOptions +} + +function decodeOptionsByMsgType(baseOption: string, msgType: number): optionTypes { + // Handle empty/zero cases + if (!baseOption || baseOption === '0x' || baseOption === '0x0') { + switch (msgType) { + case ExecutorOptionType.LZ_RECEIVE: + return { gas: BigInt(0), value: BigInt(0) } + case ExecutorOptionType.NATIVE_DROP: + return [ + { amount: BigInt(0), receiver: '0x0000000000000000000000000000000000000000' }, + ] as ExecutorNativeDropOption + case ExecutorOptionType.COMPOSE: + return [{ index: 0, gas: BigInt(0), value: BigInt(0) }] as ComposeOption + case ExecutorOptionType.ORDERED: + return false + case ExecutorOptionType.LZ_READ: + return { gas: BigInt(0), value: BigInt(0) } + default: + throw new Error(`Unknown option type: ${msgType}`) + } + } + + const options = Options.fromOptions(baseOption) + + switch (msgType) { + case ExecutorOptionType.LZ_RECEIVE: + return options.decodeExecutorLzReceiveOption() + case ExecutorOptionType.NATIVE_DROP: + return options.decodeExecutorNativeDropOption() + case ExecutorOptionType.COMPOSE: + return options.decodeExecutorComposeOption() + case ExecutorOptionType.ORDERED: + return options.decodeExecutorOrderedExecutionOption() + case ExecutorOptionType.LZ_READ: + return options.decodeExecutorLzReadOption() + default: + throw new Error(`Unknown option type: ${msgType}`) + } +} + +export async function getEnforcedOption(oappContract: Contract, eid: string, msgTypes: number): Promise { + const options = await oappContract.enforcedOptions(eid, msgTypes) + + return options +} diff --git a/packages/devtools-move/tasks/evm/wire/setPeer.ts b/packages/devtools-move/tasks/evm/wire/setPeer.ts new file mode 100644 index 000000000..3f6a2fed9 --- /dev/null +++ b/packages/devtools-move/tasks/evm/wire/setPeer.ts @@ -0,0 +1,46 @@ +import { Contract } from 'ethers' + +import { diffPrinter } from '../../shared/utils' + +import { createDiffMessage, printAlreadySet, logPathwayHeader } from '../../shared/messageBuilder' +import type { EidTxMap, OmniContractMetadataMapping } from '../utils/types' + +/** + * @notice Sets peer information for connections to wire. + * @dev Fetches the current peer from OApp + * @dev Sets the new peer on the OApp + * @returns EidTxMap + */ +export async function createSetPeerTransactions(eidDataMappings: OmniContractMetadataMapping): Promise { + const txTypePool: EidTxMap = {} + logPathwayHeader('setPeer') + + for (const [toEid, { peers, contract }] of Object.entries(eidDataMappings)) { + for (const peer of peers) { + const { eid: peerToEid, address: peerAddressBytes32 } = peer + const currPeer = await getPeer(contract.oapp, peerToEid) + + if (currPeer === peerAddressBytes32) { + printAlreadySet('peer', Number(toEid), Number(peerToEid)) + continue + } + + const diffMessage = createDiffMessage('peer', Number(toEid), Number(peerToEid)) + diffPrinter(diffMessage, { 'base-32': currPeer }, { 'base-32': peerAddressBytes32 }) + + const tx = await contract.oapp.populateTransaction.setPeer(peerToEid, peerAddressBytes32) + + txTypePool[toEid] = txTypePool[toEid] ?? [] + txTypePool[toEid].push({ + toEid: peerToEid, + populatedTx: tx, + }) + } + } + + return txTypePool +} + +export async function getPeer(contract: Contract, eid: string) { + return await contract.peers(eid) +} diff --git a/packages/devtools-move/tasks/evm/wire/setReceiveConfig.ts b/packages/devtools-move/tasks/evm/wire/setReceiveConfig.ts new file mode 100644 index 000000000..7f19d6d7a --- /dev/null +++ b/packages/devtools-move/tasks/evm/wire/setReceiveConfig.ts @@ -0,0 +1,106 @@ +import { diffPrinter } from '../../shared/utils' + +import { parseReceiveLibrary } from './setReceiveLibrary' + +import { buildConfig, decodeConfig, getConfig, setConfig } from '../utils/libraryConfigUtils' +import { createDiffMessage, printAlreadySet, printNotSet, logPathwayHeader } from '../../shared/messageBuilder' + +import type { OmniContractMetadataMapping, EidTxMap, SetConfigParam } from '../utils/types' + +/** + * @returns EidTxMap + */ +export async function createSetReceiveConfigTransactions( + eidDataMapping: OmniContractMetadataMapping +): Promise { + const txTypePool: EidTxMap = {} + logPathwayHeader('setReceiveConfig') + for (const [eid, { peers, address, contract, configOapp }] of Object.entries(eidDataMapping)) { + for (const peer of peers) { + const { eid: peerToEid } = peer + if (!configOapp?.receiveConfig?.ulnConfig) { + printNotSet('receive config - not found in config', Number(eid), Number(peerToEid)) + continue + } + + const ulnConfig = configOapp.receiveConfig.ulnConfig + + const currReceiveLibrary = await parseReceiveLibrary( + configOapp?.receiveLibraryConfig, + contract.epv2, + address.oapp, + peerToEid + ) + + const currReceiveConfig = { + ulnConfigBytes: '', + } + + currReceiveConfig.ulnConfigBytes = await getConfig( + contract.epv2, + address.oapp, + currReceiveLibrary.currReceiveLibrary, + peerToEid, + 2, + false + ) + + const newReceiveConfig = buildConfig(ulnConfig) + + let diffFromOptions: string | undefined + let diffToOptions: string | undefined + + const setFromConfigParam: SetConfigParam[] = [] + const setToConfigParam: SetConfigParam[] = [] + + if (currReceiveConfig.ulnConfigBytes === newReceiveConfig.ulnConfigBytes) { + printAlreadySet('receive config - uln', Number(eid), Number(peerToEid)) + } else { + diffFromOptions = currReceiveConfig.ulnConfigBytes + diffToOptions = newReceiveConfig.ulnConfigBytes + + setFromConfigParam.push({ + eid: peerToEid, + configType: 2, + config: diffFromOptions, + }) + + setToConfigParam.push({ + eid: peerToEid, + configType: 2, + config: diffToOptions, + }) + } + + if (setFromConfigParam.length === 0) { + continue + } + + const decodedSetFromConfigParam = decodeConfig(setFromConfigParam) + const decodedSetToConfigParam = decodeConfig(setToConfigParam) + + if (decodedSetFromConfigParam && decodedSetToConfigParam) { + diffPrinter( + createDiffMessage('receive config', Number(eid), Number(peerToEid)), + decodedSetFromConfigParam, + decodedSetToConfigParam + ) + } + + const tx = await setConfig( + contract.epv2, + address.oapp, + currReceiveLibrary.newReceiveLibrary, + setToConfigParam + ) + + txTypePool[eid] = txTypePool[eid] ?? [] + txTypePool[eid].push({ + toEid: peerToEid, + populatedTx: tx, + }) + } + } + + return txTypePool +} diff --git a/packages/devtools-move/tasks/evm/wire/setReceiveLibrary.ts b/packages/devtools-move/tasks/evm/wire/setReceiveLibrary.ts new file mode 100644 index 000000000..6597853ed --- /dev/null +++ b/packages/devtools-move/tasks/evm/wire/setReceiveLibrary.ts @@ -0,0 +1,124 @@ +import { Contract, utils, constants } from 'ethers' + +import { diffPrinter } from '../../shared/utils' +import { createDiffMessage, printAlreadySet, printNotSet, logPathwayHeader } from '../../shared/messageBuilder' +import type { OmniContractMetadataMapping, EidTxMap, RecvLibParam, address, eid } from '../utils/types' +import type { OAppEdgeConfig } from '@layerzerolabs/toolbox-hardhat' + +const error_LZ_DefaultReceiveLibUnavailable = '0x78e84d0' + +/** + * @notice Generates setReceiveLibrary transaction per Eid's OFT. + * @dev Fetches the current receiveLibrary from EndpointV2 + * @dev Sets the new receiveLibrary on the EndpointV2. + * @dev The zero address != current default receive library + * @dev - Zero Address is an abstraction to a variable receive library configurable by LZ. + * @dev - The "value" of the current default receiveLibrary is a fixed value that is invariant on LZ changing the default receive library. + * @returns EidTxMap + */ +export async function createSetReceiveLibraryTransactions( + eidDataMapping: OmniContractMetadataMapping +): Promise { + const txTypePool: EidTxMap = {} + logPathwayHeader('setReceiveLibrary') + + for (const [eid, { peers, address, contract, configOapp }] of Object.entries(eidDataMapping)) { + for (const peer of peers) { + const { eid: peerToEid } = peer + if (configOapp?.receiveLibraryConfig === undefined) { + printNotSet('receive library - not found in config', Number(eid), Number(peerToEid)) + continue + } + + const { currReceiveLibrary, newReceiveLibrary } = await parseReceiveLibrary( + configOapp.receiveLibraryConfig, + contract.epv2, + address.oapp, + peerToEid + ) + + if (newReceiveLibrary === '') { + printNotSet('receive library - set to null', Number(eid), Number(peerToEid)) + continue + } + + if (currReceiveLibrary === newReceiveLibrary) { + printAlreadySet('receive library', Number(eid), Number(peerToEid)) + continue + } + const receiveLibraryGracePeriod = configOapp.receiveLibraryConfig.gracePeriod + + diffPrinter( + createDiffMessage('receive library', Number(eid), Number(peerToEid)), + { receiveLibrary: currReceiveLibrary }, + { receiveLibrary: newReceiveLibrary } + ) + + const tx = await contract.epv2.populateTransaction.setReceiveLibrary( + address.oapp, + peerToEid, + newReceiveLibrary, + receiveLibraryGracePeriod + ) + + txTypePool[eid] = txTypePool[eid] ?? [] + txTypePool[eid].push({ + toEid: peerToEid, + populatedTx: tx, + }) + } + } + + return txTypePool +} + +export async function parseReceiveLibrary( + receiveLib: OAppEdgeConfig['receiveLibraryConfig'] | undefined, + epv2: Contract, + oappAddress: address, + eid: eid +): Promise<{ currReceiveLibrary: string; newReceiveLibrary: string }> { + if (receiveLib === undefined || receiveLib.receiveLibrary === undefined) { + const currReceiveLibrary = await getDefaultReceiveLibrary(epv2, oappAddress, eid) + console.log('currReceiveLibrary', currReceiveLibrary) + + return { + currReceiveLibrary, + newReceiveLibrary: '', + } + } + + const currReceiveLibrary = await getReceiveLibrary(epv2, oappAddress, eid) + + const newReceiveLibrary = utils.getAddress(receiveLib.receiveLibrary) + + return { currReceiveLibrary, newReceiveLibrary } +} + +export async function getReceiveLibrary(epv2Contract: Contract, evmAddress: string, aptosEid: eid): Promise { + const recvLibParam: RecvLibParam = await epv2Contract.getReceiveLibrary(evmAddress, aptosEid) + if (recvLibParam.isDefault) { + return constants.AddressZero + } + + const recvLib = recvLibParam.lib + if (recvLib === error_LZ_DefaultReceiveLibUnavailable) { + return constants.AddressZero + } + + const recvLibAddress = utils.getAddress(recvLib) + + return recvLibAddress +} + +export async function getDefaultReceiveLibrary( + epv2Contract: Contract, + evmAddress: string, + aptosEid: eid +): Promise { + const recvLib = await epv2Contract.getDefaultReceiveLibrary(evmAddress, aptosEid) + + const recvLibAddress = utils.getAddress(recvLib) + + return recvLibAddress +} diff --git a/packages/devtools-move/tasks/evm/wire/setReceiveLibraryTimeout.ts b/packages/devtools-move/tasks/evm/wire/setReceiveLibraryTimeout.ts new file mode 100644 index 000000000..5613e1bec --- /dev/null +++ b/packages/devtools-move/tasks/evm/wire/setReceiveLibraryTimeout.ts @@ -0,0 +1,87 @@ +import { Contract, utils, constants } from 'ethers' + +import { diffPrinter } from '../../shared/utils' +import { createDiffMessage, printAlreadySet, printNotSet, logPathwayHeader } from '../../shared/messageBuilder' +import type { OmniContractMetadataMapping, EidTxMap, RecvLibraryTimeoutConfig, eid } from '../utils/types' + +/** + * @notice Generates setReceiveLibraryTimeout transaction per Eid's OFT. + * @dev Fetches the current receiveLibraryTimeout from EndpointV2 + * @dev Sets the new receiveLibraryTimeout on the EndpointV2. + * @returns EidTxMap + */ +export async function createSetReceiveLibraryTimeoutTransactions( + eidDataMapping: OmniContractMetadataMapping +): Promise { + const txTypePool: EidTxMap = {} + logPathwayHeader('setReceiveLibraryTimeout') + + for (const [eid, { peers, address, contract, configOapp }] of Object.entries(eidDataMapping)) { + for (const peer of peers) { + const { eid: peerToEid } = peer + + if (configOapp?.receiveLibraryTimeoutConfig === undefined) { + printNotSet('receive library timeout - not found in config', Number(eid), Number(peerToEid)) + continue + } + + const defaultReceiveLibrary = await contract.epv2.getReceiveLibrary(contract.epv2.address, peerToEid) + if (defaultReceiveLibrary.isDefault) { + console.log('Can not set receive library timout to default library') + continue + } + + const currReceiveLibraryParam = await getReceiveLibraryTimeout(contract.epv2, address.oapp, peerToEid) + const currReceiveLibrary = currReceiveLibraryParam.lib + const currReceiveLibraryExpiry = Number(currReceiveLibraryParam.expiry) + + const newReceiveLibrary = utils.getAddress(configOapp.receiveLibraryTimeoutConfig.lib) + const newReceiveLibraryExpiry = Number(configOapp.receiveLibraryTimeoutConfig.expiry) + + if (currReceiveLibrary === newReceiveLibrary && currReceiveLibraryExpiry === newReceiveLibraryExpiry) { + printAlreadySet('receive library timeout', Number(eid), Number(peerToEid)) + continue + } + + diffPrinter( + createDiffMessage('receive library timeout', Number(eid), Number(peerToEid)), + { lib: currReceiveLibrary, expiry: currReceiveLibraryExpiry }, + { lib: newReceiveLibrary, expiry: newReceiveLibraryExpiry } + ) + + const tx = await contract.epv2.populateTransaction.setReceiveLibraryTimeout( + address.oapp, + peerToEid, + newReceiveLibrary, + newReceiveLibraryExpiry + ) + + txTypePool[eid] = txTypePool[eid] ?? [] + txTypePool[eid].push({ + toEid: peerToEid, + populatedTx: tx, + }) + } + } + + return txTypePool +} + +export async function getReceiveLibraryTimeout( + epv2Contract: Contract, + evmAddress: string, + aptosEid: eid +): Promise { + const recvLibTimeoutParam: RecvLibraryTimeoutConfig = await epv2Contract.receiveLibraryTimeout(evmAddress, aptosEid) + + const recvLib = recvLibTimeoutParam.lib + + if (recvLib === constants.AddressZero) { + return { + lib: constants.AddressZero, + expiry: BigInt(0), + } + } + + return recvLibTimeoutParam +} diff --git a/packages/devtools-move/tasks/evm/wire/setSendConfig.ts b/packages/devtools-move/tasks/evm/wire/setSendConfig.ts new file mode 100644 index 000000000..e1b3fe32b --- /dev/null +++ b/packages/devtools-move/tasks/evm/wire/setSendConfig.ts @@ -0,0 +1,130 @@ +import { diffPrinter } from '../../shared/utils' +import { buildConfig, decodeConfig, getConfig, setConfig } from '../utils/libraryConfigUtils' + +import { parseSendLibrary } from './setSendLibrary' + +import { createDiffMessage, printAlreadySet, printNotSet, logPathwayHeader } from '../../shared/messageBuilder' + +import type { OmniContractMetadataMapping, EidTxMap, SetConfigParam } from '../utils/types' + +/** + * @author Shankar + * @returns EidTxMap + */ +export async function createSetSendConfigTransactions(eidDataMapping: OmniContractMetadataMapping): Promise { + const txTypePool: EidTxMap = {} + logPathwayHeader('setSendConfig') + for (const [eid, { peers, address, contract, configOapp }] of Object.entries(eidDataMapping)) { + for (const peer of peers) { + const { eid: peerToEid } = peer + if (configOapp?.sendConfig?.ulnConfig === undefined) { + printNotSet('send config - not found in config', Number(eid), Number(peerToEid)) + continue + } + const ulnConfig = configOapp.sendConfig.ulnConfig + const executorConfig = configOapp.sendConfig.executorConfig + + const currSendLibrary = await parseSendLibrary( + configOapp?.sendLibrary, + contract.epv2, + address.oapp, + peerToEid + ) + + const currSendConfig = { + executorConfigBytes: '', + ulnConfigBytes: '', + } + + currSendConfig.executorConfigBytes = await getConfig( + contract.epv2, + address.oapp, + currSendLibrary.currSendLibrary, + peerToEid, + 1, + true + ) + + currSendConfig.ulnConfigBytes = await getConfig( + contract.epv2, + address.oapp, + currSendLibrary.currSendLibrary, + peerToEid, + 2, + true + ) + + const newSendConfig = buildConfig(ulnConfig, executorConfig) + + const diffFromOptions: Record = {} + const diffToOptions: Record = {} + const setConfigParam: SetConfigParam[] = [] + + if (currSendConfig.executorConfigBytes === newSendConfig.executorConfigBytes) { + printAlreadySet('send config - executor', Number(eid), Number(peerToEid)) + } else { + diffFromOptions[1] = currSendConfig.executorConfigBytes + diffToOptions[1] = newSendConfig.executorConfigBytes + + setConfigParam.push({ + eid: peerToEid, + configType: 1, + config: newSendConfig.executorConfigBytes, + }) + } + + if (currSendConfig.ulnConfigBytes === newSendConfig.ulnConfigBytes) { + printAlreadySet('send config - uln', Number(eid), Number(peerToEid)) + } else { + diffFromOptions[2] = currSendConfig.ulnConfigBytes + diffToOptions[2] = newSendConfig.ulnConfigBytes + + setConfigParam.push({ + eid: peerToEid, + configType: 2, + config: newSendConfig.ulnConfigBytes, + }) + } + + if (setConfigParam.length === 0) { + continue + } + + const currSendConfigParam: SetConfigParam[] = [] + currSendConfigParam.push({ + eid: peerToEid, + configType: 1, + config: currSendConfig.executorConfigBytes, + }) + currSendConfigParam.push({ + eid: peerToEid, + configType: 2, + config: currSendConfig.ulnConfigBytes, + }) + + const decodedCurrSendConfigParam = decodeConfig(currSendConfigParam) + const decodedSetConfigParam = decodeConfig(setConfigParam) + + // they are null because of some reason fix this and it should just work + if (decodedCurrSendConfigParam && decodedSetConfigParam) { + if (decodedCurrSendConfigParam !== decodedSetConfigParam) { + diffPrinter( + createDiffMessage('send config', Number(eid), Number(peerToEid)), + decodedCurrSendConfigParam, + decodedSetConfigParam + ) + } + } + + const tx = await setConfig(contract.epv2, address.oapp, currSendLibrary.newSendLibrary, setConfigParam) + + txTypePool[eid] = txTypePool[eid] ?? [] + txTypePool[eid].push({ + toEid: peerToEid, + populatedTx: tx, + }) + } + } + + return txTypePool +} diff --git a/packages/devtools-move/tasks/evm/wire/setSendLibrary.ts b/packages/devtools-move/tasks/evm/wire/setSendLibrary.ts new file mode 100644 index 000000000..8984d004f --- /dev/null +++ b/packages/devtools-move/tasks/evm/wire/setSendLibrary.ts @@ -0,0 +1,106 @@ +import { Contract, utils, constants } from 'ethers' + +import { diffPrinter } from '../../shared/utils' + +import type { OmniContractMetadataMapping, EidTxMap, address, eid } from '../utils/types' +import type { OAppEdgeConfig } from '@layerzerolabs/toolbox-hardhat' +import { createDiffMessage, printAlreadySet, printNotSet, logPathwayHeader } from '../../shared/messageBuilder' +const error_LZ_DefaultSendLibUnavailable = '0x6c1ccdb5' + +/** + * @notice Generates setSendLibrary transaction per Eid's OFT. + * @dev Fetches the current sendLibrary from EndpointV2 + * @dev Sets the new sendLibrary on the EndpointV2. + * @dev The zero address != current default send library + * @dev - Zero Address is an abstraction to a variable send library configurable by LZ. + * @dev - The "value" of the current default send library is a fixed value that is invariant on LZ changing the default send library. + * @returns EidTxMap + */ +export async function createSetSendLibraryTransactions(eidDataMapping: OmniContractMetadataMapping): Promise { + const txTypePool: EidTxMap = {} + logPathwayHeader('setSendLibrary') + + for (const [eid, { peers, address, contract, configOapp }] of Object.entries(eidDataMapping)) { + for (const peer of peers) { + const { eid: peerToEid } = peer + if (!configOapp?.sendLibrary) { + printNotSet('send library - not found in config', Number(eid), Number(peerToEid)) + continue + } + + const { currSendLibrary, newSendLibrary } = await parseSendLibrary( + configOapp.sendLibrary, + contract.epv2, + address.oapp, + peerToEid + ) + + if (currSendLibrary === newSendLibrary) { + printAlreadySet('send library', Number(eid), Number(peerToEid)) + continue + } + + diffPrinter( + createDiffMessage('send library', Number(eid), Number(peerToEid)), + { sendLibrary: currSendLibrary }, + { sendLibrary: newSendLibrary } + ) + + const tx = await contract.epv2.populateTransaction.setSendLibrary(address.oapp, peerToEid, newSendLibrary) + + txTypePool[eid] = txTypePool[eid] ?? [] + txTypePool[eid].push({ + toEid: peerToEid, + populatedTx: tx, + }) + } + } + + return txTypePool +} + +export async function parseSendLibrary( + sendLib: OAppEdgeConfig['sendLibrary'] | undefined, + epv2: Contract, + oappAddress: address, + eid: eid +): Promise<{ currSendLibrary: string; newSendLibrary: string }> { + if (sendLib === undefined) { + const currSendLibrary = await getDefaultSendLibrary(epv2, oappAddress, eid) + return { + currSendLibrary, + newSendLibrary: '', + } + } + + const currSendLibrary = await getSendLibrary(epv2, oappAddress, eid) + + const newSendLibrary = utils.getAddress(sendLib) + + return { currSendLibrary, newSendLibrary } +} + +export async function getSendLibrary(epv2Contract: Contract, evmAddress: string, apnewSEid: eid): Promise { + const isDefault = await epv2Contract.isDefaultSendLibrary(evmAddress, apnewSEid) + if (isDefault) { + return constants.AddressZero + } + + const sendLib = await epv2Contract.getSendLibrary(evmAddress, apnewSEid) + if (sendLib === error_LZ_DefaultSendLibUnavailable) { + return constants.AddressZero + } + return utils.getAddress(sendLib) +} + +export async function getDefaultSendLibrary( + epv2Contract: Contract, + evmAddress: string, + apnewSEid: eid +): Promise { + const sendLib = await epv2Contract.getDefaultSendLibrary(evmAddress, apnewSEid) + + const sendLibAddress = utils.getAddress(sendLib) + + return sendLibAddress +} diff --git a/packages/devtools-move/tasks/evm/wire/transactionExecutor.ts b/packages/devtools-move/tasks/evm/wire/transactionExecutor.ts new file mode 100644 index 000000000..ee68b31c9 --- /dev/null +++ b/packages/devtools-move/tasks/evm/wire/transactionExecutor.ts @@ -0,0 +1,170 @@ +import { exit } from 'process' + +import { Contract, ethers, providers } from 'ethers' + +import { promptForConfirmation } from '../../shared/utils' + +import type { AccountData, OmniContractMetadataMapping, TxPool, TxReceiptJson, TxEidMapping, eid } from '../utils/types' +import { getNetworkForChainId } from '@layerzerolabs/lz-definitions' +import path from 'path' +import fs from 'fs' + +/** + * @notice Simulates transactions on the blockchains + * - Fetches the nonce and balance for each chain + * - Estimates gas for each transaction and updates the balance + * - Checks if the user's balance is sufficient to submit all the transactions + * - Simulates transactions on-chain and catches any errors before submitting the transaction + */ +export async function executeTransactions( + eidMetaData: OmniContractMetadataMapping, + TxTypeEidMapping: TxEidMapping, + rpcUrlsMap: Record, + simulation = 'dry-run', + privateKey: string, + args: any +) { + const rootDir = args.rootDir + const oappConfig = args.oapp_config + + const num_chains = Object.entries(eidMetaData).length + let totalTransactions = 0 + + Object.entries(TxTypeEidMapping).forEach(([_key, eidMapping]) => { + Object.entries(eidMapping).forEach(([_eid, txArray]) => { + totalTransactions += txArray.length + }) + }) + + if (totalTransactions == 0) { + console.log('✨ No transactions to submit') + return + } + + console.log(`\n📦 Transaction Summary:`) + console.log(` • Total chains: ${num_chains}`) + console.log(` • Total transactions: ${totalTransactions}`) + console.log(` • Mode: ${simulation === 'dry-run' ? 'SIMULATION (dry-run)' : 'EXECUTION (broadcast)'}`) + + const flag = await promptForConfirmation(totalTransactions) + if (!flag) { + console.log('Operation cancelled.') + exit(0) + } + + // Populate simulation account data - does not need to have an address for each eid because the same deployer accunt is used for all chains + const accountEidMap: AccountData = {} + + const tx_pool: Record> = {} + const tx_pool_receipt: Promise[] = [] + + for (const [eid, _eidData] of Object.entries(eidMetaData)) { + /* + * Create a new provider using the URL from rpcUrlsMap + * This is required because: + * - forkUrl != rpcUrl => fork mode + * - forkUrl = rpcUrl => mainnet mode + * + * The mapping helps keep track of the different chains and their respective providers + * We also create a signer object using the private key and provider + */ + const newProvider = new providers.JsonRpcProvider(rpcUrlsMap[eid]) + const signer = new ethers.Wallet(privateKey, newProvider) + + accountEidMap[eid] = { + nonce: await signer.getTransactionCount(), + gasPrice: await signer.getGasPrice(), + signer: signer, + } + + const network = getNetworkForChainId(Number(eid)) + console.log( + ` • Balance on ${network.chainName}-${network.env} for ${signer.address}: ${ethers.utils.formatEther( + await newProvider.getBalance(signer.address) + )} ETH` + ) + } + + console.log('\n🔄 Processing transactions...') + let processedTx = 0 + for (const [txType, EidTxsMapping] of Object.entries(TxTypeEidMapping)) { + if (tx_pool[txType] === undefined) { + tx_pool[txType] = {} + } + for (const [eid, TxPool] of Object.entries(EidTxsMapping)) { + for (const { toEid, populatedTx } of TxPool) { + processedTx++ + const progress = `[${processedTx}/${totalTransactions}]` + const network = getNetworkForChainId(Number(eid)) + console.log( + ` ${progress} Submitting transaction on chain ${network.chainName}-${network.env} (${txType})...` + ) + + const provider: providers.JsonRpcProvider = new providers.JsonRpcProvider(rpcUrlsMap[eid]) + const signer = accountEidMap[eid].signer + + populatedTx.gasLimit = await provider.estimateGas(populatedTx) + populatedTx.gasPrice = accountEidMap[eid].gasPrice + populatedTx.nonce = accountEidMap[eid].nonce++ + + let sendTx: Promise | undefined = undefined + if (simulation === 'broadcast') { + sendTx = signer.sendTransaction(populatedTx) + tx_pool_receipt.push(sendTx) + } + + if (tx_pool[txType][toEid.toString()] === undefined) { + tx_pool[txType][toEid.toString()] = { + from_eid: eid, + raw: populatedTx, + response: sendTx, + } + } + } + } + } + + const folderPath = path.join(rootDir, 'transactions', oappConfig, simulation) + fs.mkdirSync(folderPath, { recursive: true }) + + const runId = fs.readdirSync(folderPath).length + 1 + + const filePath = path.join(folderPath, `${runId}.json`) + const txReceiptJson: TxReceiptJson = {} + + for (const [txType, eidTxsMapping] of Object.entries(tx_pool)) { + if (txReceiptJson[txType] === undefined) { + txReceiptJson[txType] = [] + } + for (const [to_eid, txPool] of Object.entries(eidTxsMapping)) { + let txHash = undefined + if (txPool.response) { + txHash = (await txPool.response)?.hash + } + txReceiptJson[txType].push({ + src_eid: txPool.from_eid, + dst_eid: to_eid, + src_from: txPool.raw?.from ?? '', + src_to: txPool.raw?.to ?? '', + tx_hash: txHash, + data: txPool.raw?.data ?? '', + }) + } + } + fs.writeFileSync(filePath, JSON.stringify(txReceiptJson, null, 2)) + + console.log( + `\n✅ Successfully ${simulation === 'dry-run' ? 'simulated' : 'executed'} ${totalTransactions} transactions` + ) + console.log('Transactions have been saved to ', filePath) +} + +export function getContractForTxType(oappContract: Contract, epv2Contract: Contract, txType: string) { + const oappTxTypes = ['setPeer', 'setDelegate', 'setEnforcedOptions'] + + if (oappTxTypes.includes(txType)) { + return oappContract + } + + return epv2Contract +} diff --git a/packages/devtools-move/tasks/move/build.ts b/packages/devtools-move/tasks/move/build.ts new file mode 100644 index 000000000..53cedc7bb --- /dev/null +++ b/packages/devtools-move/tasks/move/build.ts @@ -0,0 +1,138 @@ +import { spawn } from 'child_process' + +import { getLzNetworkStage, parseYaml } from './utils/aptosNetworkParser' +import { getNamedAddresses } from './utils/config' +import fs from 'fs' +import path from 'path' + +let stdErr = '' + +/** + * @notice Main function to build an OFT + * @dev Wraps the aptos move build command + * @returns Promise + */ +async function buildMovementContracts(named_addresses: string, configPath: string) { + const aptosYamlConfig = await parseYaml(configPath) + const network = aptosYamlConfig.network + const lzNetworkStage = getLzNetworkStage(network) + + // Get additional named addresses and combine with provided ones + const additionalAddresses = getNamedAddresses(lzNetworkStage) + const namedAddresses = named_addresses ? `${named_addresses},${additionalAddresses}` : additionalAddresses + + const cmd = 'aptos' + const args = ['move', 'build', `--named-addresses=${namedAddresses}`] + + return new Promise((resolve, reject) => { + const childProcess = spawn(cmd, args, { + stdio: ['inherit', 'pipe', 'pipe'], // Inherit stdin, pipe stdout and stderr + }) + + // Capture stdout which contains our deployed address + childProcess.stdout?.on('data', (data) => { + const dataStr = data.toString() + process.stdout.write(`${dataStr}`) + }) + + // Capture stderr (this is actually NOT the error output but the interactive prompt) + childProcess.stderr?.on('data', (data) => { + const dataStr = data.toString() + stdErr += dataStr + process.stderr.write(`${dataStr}`) + }) + + // Handle process close + childProcess.on('close', (code) => { + if (code === 0) { + resolve() + } else { + console.error(`Command failed with code ${code}`) + console.error('Captured stderr:', stdErr) + reject(new Error(`Process exited with code ${code}`)) + } + }) + + // Handle errors + childProcess.on('error', (err) => { + console.error('Error spawning the process:', err) + reject(err) + }) + }) +} + +// Add this new helper function +function compareVersions(installed: string, required: string): boolean { + const installedParts = installed.split('.').map(Number) + const requiredParts = required.split('.').map(Number) + + for (let i = 0; i < 3; i++) { + if (installedParts[i] > requiredParts[i]) { + return true + } + if (installedParts[i] < requiredParts[i]) { + return false + } + } + return true // Equal versions +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function build(args: any, contractName: string = 'oft') { + const buildPath = path.join(process.cwd(), 'build', contractName) + const configPath = args.configPath + const aptosYamlConfig = await parseYaml(configPath) + const accountAddress = aptosYamlConfig.account_address + + try { + const version = await getAptosVersion() + console.log('🚀 aptos version is:', version) + const MIN_VERSION = '6.0.1' + + if (!compareVersions(version, MIN_VERSION)) { + console.error(`❌ aptos version too old. Required: ${MIN_VERSION} or newer, Found: ${version}`) + return + } + console.log('🚀 aptos version is compatible') + } catch (error) { + console.error('🚨 Failed to check aptos version:', error) + return + } + + if (!fs.existsSync(buildPath) || args.force_build === 'true') { + if (!args.named_addresses) { + console.error( + `Missing --named-addresses flag! - usage based on your aptos config:\n --named-addresses oft=${accountAddress},oft_admin=${accountAddress}` + ) + return + } + console.log('Building contracts\n') + await buildMovementContracts(args.named_addresses, configPath) + } else { + console.log('Skipping build - built modules already exist at: ', buildPath) + } +} + +async function getAptosVersion(): Promise { + return new Promise((resolve, reject) => { + const childProcess = spawn('aptos', ['--version']) + let stdout = '' + + childProcess.stdout?.on('data', (data) => { + stdout += data.toString() + }) + + childProcess.on('close', (code) => { + if (code === 0) { + const versionMatch = stdout.match(/aptos (\d+\.\d+\.\d+)/) + versionMatch ? resolve(versionMatch[1]) : reject(new Error('Could not parse version')) + } else { + reject(new Error(`aptos --version exited with code ${code}`)) + } + }) + + childProcess.on('error', reject) + }) +} + +export { build } diff --git a/packages/devtools-move/tasks/move/deploy.ts b/packages/devtools-move/tasks/move/deploy.ts new file mode 100644 index 000000000..ca93c2e5e --- /dev/null +++ b/packages/devtools-move/tasks/move/deploy.ts @@ -0,0 +1,152 @@ +import { spawn } from 'child_process' +import { assert } from 'console' +import fs from 'fs' + +import { Network } from '@aptos-labs/ts-sdk' + +import { deploymentFile } from '../shared/types' + +import { getLzNetworkStage, parseYaml } from './utils/aptosNetworkParser' +import { getNamedAddresses } from './utils/config' +import path from 'path' + +let stdOut = '' +let stdErr = '' + +/** + * @notice Main function to deploy an OFT + * @dev This function deploys an OFT and creates a deployment file in the deployments directory + * @dev Wraps the aptos move create-object-and-publish-package command + * @returns Promise + */ +async function deployMovementContracts(address_name: string, named_addresses: string, configPath: string) { + const aptosYamlConfig = await parseYaml(configPath) + const networkStage = aptosYamlConfig.network + const lzNetworkStage = getLzNetworkStage(networkStage) + const network = getNetworkFromConfig(aptosYamlConfig) + + const additionalAddresses = getNamedAddresses(lzNetworkStage) + const namedAddresses = named_addresses ? `${named_addresses},${additionalAddresses}` : additionalAddresses + + const cmd = 'aptos' + const args = [ + 'move', + 'create-object-and-publish-package', + `--address-name=${address_name}`, + `--named-addresses=${namedAddresses}`, + ] + + return new Promise((resolve, reject) => { + const childProcess = spawn(cmd, args, { + stdio: ['inherit', 'pipe', 'pipe'], // Inherit stdin, pipe stdout and stderr + }) + + // Capture stdout which contains our deployed address + childProcess.stdout?.on('data', (data) => { + const dataStr = data.toString() + stdOut += dataStr + process.stdout.write(`${dataStr}`) + }) + + // Capture stderr (this is actually NOT the error output but the interactive prompt) + childProcess.stderr?.on('data', (data) => { + const dataStr = data.toString() + stdErr += dataStr + process.stderr.write(`${dataStr}`) + }) + + // Handle process close + childProcess.on('close', (code) => { + if (code === 0) { + const addresses = stdOut.match(/0x[0-9a-fA-F]{64}/g)! + assert(addresses[0] == addresses[1], 'Addresses do not match') + createDeployment(addresses[0], address_name, network, lzNetworkStage) + + resolve() + } else { + console.error(`Command failed with code ${code}`) + console.error('Captured stderr:', stdErr) + reject(new Error(`Process exited with code ${code}`)) + } + }) + + // Handle errors + childProcess.on('error', (err) => { + console.error('Error spawning the process:', err) + reject(err) + }) + }) +} + +function getNetworkFromConfig(yamlConfig: { + account_address: string + private_key: string + network: Network + fullnode: string + faucet?: string +}): string { + if (yamlConfig.fullnode.toLowerCase().includes('movement')) { + return 'movement' + } else { + return 'aptos' + } +} + +async function createDeployment(deployedAddress: string, file_name: string, network: string, lzNetworkStage: string) { + fs.mkdirSync('deployments', { recursive: true }) + const aptosDir = `deployments/${network}-${lzNetworkStage}` + fs.mkdirSync(aptosDir, { recursive: true }) + + const deployment: deploymentFile = { + address: deployedAddress, + abi: [], + transactionHash: '', + receipt: {}, + args: [], + numDeployments: 1, + solcInputHash: '', + metadata: '', + bytecode: '', + deployedBytecode: '', + devdoc: {}, + storageLayout: {}, + } + + fs.writeFileSync(path.join(aptosDir, `${file_name}.json`), JSON.stringify(deployment, null, 2)) + console.log('\n✅ Deployment successful ✅') + console.log(`📝 Successfully created deployment file at: ${aptosDir}/${file_name}.json`) +} + +async function checkIfDeploymentExists(network: string, lzNetworkStage: string, contractName: string) { + const aptosDir = path.join(process.cwd(), 'deployments', `${network}-${lzNetworkStage}`) + return fs.existsSync(path.join(aptosDir, `${contractName}.json`)) +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function deploy( + configPath: string, + namedAddresses: string, + forceDeploy: boolean = false, + contractName: string = 'oft' +) { + const aptosYamlConfig = await parseYaml(configPath) + const networkStage = aptosYamlConfig.network + const lzNetworkStage = getLzNetworkStage(networkStage) + const network = getNetworkFromConfig(aptosYamlConfig) + + const deploymentExists = await checkIfDeploymentExists(network, lzNetworkStage, contractName) + + if (deploymentExists) { + if (forceDeploy) { + console.log(`Follow the prompts to complete the deployment ${contractName}`) + await deployMovementContracts(contractName, namedAddresses, configPath) + } else { + console.log('Skipping deploy - deployment already exists') + } + } else { + console.warn('You are in force deploy mode:') + console.log(`Follow the prompts to complete the deployment ${contractName}`) + await deployMovementContracts(contractName, namedAddresses, configPath) + } +} +export { deploy } diff --git a/packages/devtools-move/tasks/move/setDelegate.ts b/packages/devtools-move/tasks/move/setDelegate.ts new file mode 100644 index 000000000..e6620ca65 --- /dev/null +++ b/packages/devtools-move/tasks/move/setDelegate.ts @@ -0,0 +1,36 @@ +import path from 'path' +import { getChain, getConnection } from '../../sdk/moveVMConnectionBuilder' +import { OFT } from '../../sdk/oft' + +import { getEidFromMoveNetwork, getLzNetworkStage, parseYaml } from './utils/aptosNetworkParser' +import { setDelegate } from './utils/moveVMOftConfigOps' +import { getDelegateFromLzConfig, getMoveVMOftAddress, sendAllTxs } from './utils/utils' + +async function executeSetDelegate(args: any, useAccountAddress: boolean = false) { + const configPath = args.oapp_config + const lzConfigPath = path.resolve(path.join(process.cwd(), configPath)) + const lzConfigFile = await import(lzConfigPath) + const lzConfig = lzConfigFile.default + + const { account_address, private_key, network, fullnode, faucet } = await parseYaml() + + const chain = getChain(fullnode) + const aptos = getConnection(chain, network, fullnode, faucet) + + const lzNetworkStage = getLzNetworkStage(network) + const oftAddress = getMoveVMOftAddress(chain, lzNetworkStage) + + console.log(`\n🔧 Setting ${chain}-${lzNetworkStage} OFT Delegate`) + console.log(`\tFor: ${oftAddress}\n`) + + const oft = new OFT(aptos, oftAddress, account_address, private_key) + + const eid = getEidFromMoveNetwork(chain, network) + const delegate = useAccountAddress ? account_address : getDelegateFromLzConfig(eid, lzConfig) + + const setDelegatePayload = await setDelegate(oft, delegate, eid) + + sendAllTxs(aptos, oft, account_address, [setDelegatePayload]) +} + +export { executeSetDelegate as setDelegate } diff --git a/packages/devtools-move/tasks/move/transferObjectOwner.ts b/packages/devtools-move/tasks/move/transferObjectOwner.ts new file mode 100644 index 000000000..e63f55539 --- /dev/null +++ b/packages/devtools-move/tasks/move/transferObjectOwner.ts @@ -0,0 +1,30 @@ +import { OFT } from '../../sdk/oft' + +import { getLzNetworkStage, parseYaml } from './utils/aptosNetworkParser' +import { createTransferObjectOwnerPayload } from './utils/moveVMOftConfigOps' +import { getMoveVMOftAddress, sendAllTxs } from './utils/utils' +import { getChain } from '../../sdk/moveVMConnectionBuilder' +import { getConnection } from '../../sdk/moveVMConnectionBuilder' + +async function transferObjectOwner(newOwner: string) { + const { account_address, private_key, network, fullnode, faucet } = await parseYaml() + + const chain = getChain(fullnode) + const aptos = getConnection(chain, network, fullnode, faucet) + + const lzNetworkStage = getLzNetworkStage(network) + const oftAddress = getMoveVMOftAddress(chain, lzNetworkStage) + console.log(`\n👑 Transferring ${chain}-${lzNetworkStage} Object Ownership`) + console.log(`\tFor Object at: ${oftAddress}\n`) + console.log(`\tNew Owner: ${newOwner}\n`) + + const oft = new OFT(aptos, oftAddress, account_address, private_key) + + const transferOwnerPayload = createTransferObjectOwnerPayload(oftAddress, newOwner) + + const payloads = [transferOwnerPayload] + + sendAllTxs(aptos, oft, account_address, payloads) +} + +export { transferObjectOwner } diff --git a/packages/devtools-move/tasks/move/transferOwnerOapp.ts b/packages/devtools-move/tasks/move/transferOwnerOapp.ts new file mode 100644 index 000000000..70a8e9482 --- /dev/null +++ b/packages/devtools-move/tasks/move/transferOwnerOapp.ts @@ -0,0 +1,32 @@ +import { OFT } from '../../sdk/oft' + +import { getEidFromMoveNetwork, getLzNetworkStage, parseYaml } from './utils/aptosNetworkParser' +import { createTransferOwnerOAppPayload } from './utils/moveVMOftConfigOps' +import { getMoveVMOftAddress, sendAllTxs } from './utils/utils' +import { getChain } from '../../sdk/moveVMConnectionBuilder' +import { getConnection } from '../../sdk/moveVMConnectionBuilder' + +async function transferOAppOwner(newOwner: string) { + const { account_address, private_key, network, fullnode, faucet } = await parseYaml() + + const chain = getChain(fullnode) + const aptos = getConnection(chain, network, fullnode, faucet) + + const lzNetworkStage = getLzNetworkStage(network) + const oftAddress = getMoveVMOftAddress(chain, lzNetworkStage) + console.log(`\n👑 Transferring ${chain}-${lzNetworkStage} OApp Ownership`) + console.log(`\tFor OApp at: ${oftAddress}\n`) + console.log(`\tNew Owner: ${newOwner}\n`) + + const oft = new OFT(aptos, oftAddress, account_address, private_key) + + const eid = getEidFromMoveNetwork(chain, network) + + const transferOwnerPayload = await createTransferOwnerOAppPayload(oft, newOwner, eid) + + const payloads = [transferOwnerPayload] + + sendAllTxs(aptos, oft, account_address, payloads) +} + +export { transferOAppOwner } diff --git a/packages/devtools-move/tasks/move/utils/aptosNetworkParser.ts b/packages/devtools-move/tasks/move/utils/aptosNetworkParser.ts new file mode 100644 index 000000000..d9bda656c --- /dev/null +++ b/packages/devtools-move/tasks/move/utils/aptosNetworkParser.ts @@ -0,0 +1,59 @@ +import { Network as AptosNetworkStage } from '@aptos-labs/ts-sdk' + +import { EndpointId, Stage } from '@layerzerolabs/lz-definitions' + +import { loadAptosYamlConfig } from './config' + +export function getEidFromMoveNetwork(chain: string, networkStage: AptosNetworkStage): number { + if (chain === 'aptos') { + if (networkStage === AptosNetworkStage.MAINNET || networkStage.toLowerCase() === 'mainnet') { + return EndpointId.APTOS_V2_MAINNET + } else if (networkStage === AptosNetworkStage.TESTNET || networkStage.toLowerCase() === 'testnet') { + return EndpointId.APTOS_V2_TESTNET + } else { + throw new Error(`Unsupported network stage for ${chain}: ${networkStage}`) + } + } else if (chain === 'movement') { + if (networkStage === AptosNetworkStage.TESTNET || networkStage.toLowerCase() === 'testnet') { + return EndpointId.MOVEMENT_V2_TESTNET + } else { + throw new Error(`Unsupported network stage for ${chain}: ${networkStage}`) + } + } else { + throw new Error(`Unsupported chain: ${chain}`) + } +} + +export function getLzNetworkStage(network: AptosNetworkStage): Stage { + if (network === AptosNetworkStage.MAINNET) { + return Stage.MAINNET + } else if (network === AptosNetworkStage.TESTNET) { + return Stage.TESTNET + } else if (network === AptosNetworkStage.CUSTOM) { + return Stage.SANDBOX + } else { + throw new Error(`Unsupported network: ${network}`) + } +} + +export async function parseYaml(_rootDir: string = process.cwd()): Promise<{ + account_address: string + private_key: string + network: AptosNetworkStage + fullnode: string + faucet?: string +}> { + const aptosYamlConfig = await loadAptosYamlConfig() + + let account_address = aptosYamlConfig.profiles.default.account + const private_key = aptosYamlConfig.profiles.default.private_key + const network = aptosYamlConfig.profiles.default.network.toLowerCase() as AptosNetworkStage + const fullnode = aptosYamlConfig.profiles.default.rest_url + const faucet = aptosYamlConfig.profiles.default.faucet_url ?? undefined + + if (!account_address.startsWith('0x')) { + account_address = '0x' + account_address + } + + return { account_address, private_key, network, fullnode, faucet } +} diff --git a/packages/devtools-move/tasks/move/utils/config.ts b/packages/devtools-move/tasks/move/utils/config.ts new file mode 100644 index 000000000..4e956fcdf --- /dev/null +++ b/packages/devtools-move/tasks/move/utils/config.ts @@ -0,0 +1,41 @@ +import fs from 'fs' +import path from 'path' + +import { Account, Ed25519PrivateKey } from '@aptos-labs/ts-sdk' +import YAML from 'yaml' + +type AptosYamlConfig = { + profiles: { + default: { + network: string + private_key: string + public_key: string + account: string + rest_url: string + faucet_url?: string + } + } +} + +export async function loadAptosYamlConfig(_rootDir: string = process.cwd()): Promise { + const file = fs.readFileSync(path.resolve(path.join(_rootDir, '.aptos/config.yaml')), 'utf8') + const config = YAML.parse(file) as AptosYamlConfig + return config +} + +export function createAccountFromPrivateKey(privateKey: string, account_address: string): Account { + return Account.fromPrivateKey({ + privateKey: new Ed25519PrivateKey(privateKey), + address: account_address, + }) +} + +export function getNamedAddresses(networkType: string): string { + const addressesPath = path.join(__dirname, './deploymentAddresses.json') + const addresses = JSON.parse(fs.readFileSync(addressesPath, 'utf8')) + const networkAddresses = addresses[`${networkType}-addresses`] + + return Object.entries(networkAddresses) + .map(([name, addr]) => `${name}=${addr}`) + .join(',') +} diff --git a/packages/devtools-move/tasks/move/utils/deploymentAddresses.json b/packages/devtools-move/tasks/move/utils/deploymentAddresses.json new file mode 100644 index 000000000..363a73671 --- /dev/null +++ b/packages/devtools-move/tasks/move/utils/deploymentAddresses.json @@ -0,0 +1,80 @@ +{ + "sandbox-addresses": { + "oft_common": "0x49eec680042ba13d54b2343f1551aadfb3dbf549da4f5a3ab55dd49316c1732f", + "router_node_0": "0xde8f887ac62c3ddad6de4df32e30977f0327c8cfeca56e5981dec1f111e189c1", + "simple_msglib": "0xbb29964fac328dc2cd1dbf03982e51fad9de67e2a525c35d6e52cde4b66e7997", + "blocked_msglib": "0xfe58a1557dd30f5f69df9ebd0466f514840fba81a4c6fbcd536f088fe9e58100", + "uln_302": "0x3f2714ef2d63f1128f45e4a3d31b354c1c940ccdb38aca697c9797ef95e7a09f", + "router_node_1": "0x3dc5c01fde9a92724de233adc32878638ddc98b2178282d137ba2bafb1f4b935", + "endpoint_v2_common": "0xf8ce9f0030cb1fa5fb48f481d9b2da3909cd922992e7085e76a196653a707bbf", + "endpoint_v2": "0x824f76b2794de0a0bf25384f2fde4db5936712e6c5c45cf2c3f9ef92e75709c", + "layerzero_admin": "0x75de231a1ea32a14ed5be6a52475b16d7d8eeac35d378afd4c361979c0ae1531", + "layerzero_treasury_admin": "0x75de231a1ea32a14ed5be6a52475b16d7d8eeac35d378afd4c361979c0ae1531", + "msglib_types": "0x147b2ca35d8d6101a49690b3c108e67b174665d13b0fbba515e3b99d48b9fe42", + "treasury": "0xd83cb5c494daec692964a3599c3b36d4bd618dc54e7bcb2bbe444a7f9732f740", + "worker_peripherals": "0x75de231a1ea32a14ed5be6a52475b16d7d8eeac35d378afd4c361979c0ae1531", + "price_feed_router_0": "0x86c53b1187769b354e59b44111d90ab74f3162240d3f35ea1ca451281137cf2c", + "price_feed_router_1": "0xd78078a798d87940a9783ea4b8b09eb47117f36f23f57fe31a96faeb775046c4", + "price_feed_module_0": "0x894473c4f48d05a65d5bfb106ab91bc0881c7a1c7e9c66fbea2859c2ba9bff83", + "worker_common": "0x9d8a2cc4cd5563028107b792fd3c7f4068064405ef1bc4fce1cbc3af916032e8", + "executor_fee_lib_router_0": "0x551bd1f03eaba95f5aae59c0f382acf74e051b3d0890d63574069e45fce0118e", + "executor_fee_lib_router_1": "0xf1265e9c0ae38b49f099355587290d830ab46484df87d95cbad66c4ee3af8967", + "dvn_fee_lib_router_0": "0xc09c085aafdb39e4aa66f3311f3d293fe67ee88dab2aec772df9c2f0a1d60abd", + "dvn_fee_lib_router_1": "0xd9df2854349456b32d482d25db0b3917964f6fbae9737b3b4b62bf6d0a27ed51", + "executor_fee_lib_0": "0xff72f529a579e922e0edba30f384b4c9c86d05020cbd2401a23fe708f29c6664", + "dvn_fee_lib_0": "0xf63f6b01ea6688189e09e2be035bef512049288ff1d307c2470a913f3831330f", + "dvn": "0x1f79b324153abe0ca18a279822f3b561acbaabb4d68d47ed3639b5a53e4d3470" + }, + "testnet-addresses": { + "oft_common": "0xa4c83e48703381fc23d03e3cb89088dbd363c51427fe52f352ce1668b4d146cd", + "router_node_0": "0x7a19f544a6db990a1bfa6d84d0f53f918cd41765ec5b0d67abbdb78455f1d43a", + "simple_msglib": "0x98ab4d5f6f5ae0d3bc9d9785cfd63244fffc4652bf54f8971cb7035589a256eb", + "blocked_msglib": "0xb79b041ff861c2ec1e67138501270d978a35bd28817565a6752bfc469fa62c06", + "uln_302": "0xcc1c03aed42e2841211865758b5efe93c0dde2cb7a2a5dc6cf25a4e33ad23690", + "router_node_1": "0x8c65dab3f69a5c35eefed1e181f68890021797156dc1842b191b2ece7bda909a", + "endpoint_v2_common": "0x3bc8cbd74c2e1929c287a0063206fbb126314976146934bae12283f6120e99e9", + "endpoint_v2": "0x7f03103b83c51c8b09be1751a797a65ac6e755f72947ecdecffc203d32d816c6", + "layerzero_admin": "0xb1f42e295868a61b2d78836f1199324c8964c84a54e8ff8f72c5a6594d600d07", + "layerzero_treasury_admin": "0xb1f42e295868a61b2d78836f1199324c8964c84a54e8ff8f72c5a6594d600d07", + "msglib_types": "0xd34d78d10b19757dd6bc007f7c2d07f6848c2eebfcc63b8eab95991751196df8", + "treasury": "0x3a9902a21eabd3552edfc104cb4a6ce1ac4fe5af6aa24a56037969e1a0db3d93", + "worker_peripherals": "0xb1f42e295868a61b2d78836f1199324c8964c84a54e8ff8f72c5a6594d600d07", + "price_feed_router_0": "0xe7067908019da66726a41d09dde09bc5520a91089edd4a649ed36a01b4613b67", + "price_feed_router_1": "0xbcb5f986fbffd251b26936d95d5c52761703ca1cdb3f3f3b71b5e7dd0a303813", + "price_feed_module_0": "0xa762d65f42c852e0a7f6240ec6441694ea1b4786392cdfa5351dee8364c868fc", + "worker_common": "0xfe0b685e4cc9e77d91d008ef4161de68f7d7646c3bf67079fd4c2f0356631be8", + "executor_fee_lib_router_0": "0xfb62a7ea757acc3b5a5f3e19794b6a5c9f6fd56c6e3fb392aac3d9d275ea4bca", + "executor_fee_lib_router_1": "0x8824962f90a61eae9f0c2e1abaf90dac63929bb6ec0cd6c54ac1b4c9df295e29", + "dvn_fee_lib_router_0": "0x4e27bce08903acad46c4cfd35229589cc8e6975a0f9beec0e8a9aa2a1b12574b", + "dvn_fee_lib_router_1": "0x5248f0a5e6f1629e7e12dfb5d87a2b1ee3adf3f3cbf0c7a2b667053f336c985e", + "executor_fee_lib_0": "0x4123c50265067995272e998193638aaf876d75454b2aba50d55d950b2236ff4e", + "dvn_fee_lib_0": "0x9b77c6ad73d3e642f4c59ff191e1b460a7f4a16a67558edba1a744b4d6a88127", + "dvn": "0x756f8ab056688d22687740f4a9aeec3b361170b28d08b719e28c4d38eed1043e" + }, + "mainnet-addresses": { + "oft_common": "0xcf4e1eb4b32b84266f27efe35539a9a3b7a3ec822299d8eb828ca32e581aa72c", + "router_node_0": "0x6de27e5aa7dbee0fc32af2a92b8aa0b96e0033026ade8f22e4692cd8603220e9", + "simple_msglib": "0x52d5c6f8dcb20ed8ace8dbaa7cc09a98eb1dbec0f184720795310c031ace5111", + "blocked_msglib": "0x3ca0d187f1938cf9776a0aa821487a650fc7bb2ab1c1d241ba319192aae4afc6", + "uln_302": "0xc33752e0220faf79e45385dd73fb28d681dcd9f1569a1480725507c1f3c3aba9", + "router_node_1": "0x2ae54c38567f217c42b255016a38ccd68b67eb276a6cc3ebad609935fe3cc70c", + "endpoint_v2_common": "0xe1dc2a62b445403bea0dbd73df8cee03b3ead0a06b003e72e401c030a810a133", + "endpoint_v2": "0xe60045e20fc2c99e869c1c34a65b9291c020cd12a0d37a00a53ac1348af4f43c", + "msglib_types": "0xa3fac5ed887625dd1d4371a60c7bfd5869e8ce5c3c5783fb8898dc0128365c31", + "treasury": "0x77c941e60b8e2c8d784de2ee456fd497283edfe1e15704c99a192ff795fc38b7", + "worker_common": "0x1bffc83ec332cb9de738e8f0c27dd2230ee57bdbc71473047fcfe8bfaa21fab7", + "executor_fee_lib_router_0": "0xfb941d4e28fc08b94fe53c9043e392d6405a16475bccbfee5222d588cef5b709", + "executor_fee_lib_router_1": "0xf8ed27afba36de5693de4c9ea654ee73de7b0e2ac7c43a54d36bc155a944d9d1", + "dvn_fee_lib_router_0": "0x707f09a7db866c4be5d2ee7c4ffcfe38e1b893f8d757712fe224fa19da881c93", + "dvn_fee_lib_router_1": "0x31dcc84f4bfffff09648cb9ae6d84261ddb7c04646d1f2f6c38bf6d7551a0831", + "executor_fee_lib_0": "0xbbb5d80871b10c4a7c10b9bbc636fdca4faa05feb3b03dc27e3018a7bfcbd8cb", + "dvn_fee_lib_0": "0x349c43bc506cbbe7b754b164867bd1751763410b6458a798c25bb6f3c3e9e487", + "dvn": "0xdf8f0a53b20f1656f998504b81259698d126523a31bdbbae45ba1e8a3078d8da", + "price_feed_router_0": "0x969722e6e181bb17165c17492c037514cd213a0ce9830a59724190e13c011136", + "price_feed_router_1": "0x3808a699d1a14d25de813a4e0bbcde7a8ce8d27ccc9055aee8070d28172faced", + "price_feed_module_0": "0xad0f7141f626c07db99a7fe5b864fde080bc4966c144d88f6f14ac4af391f30", + "layerzero_admin": "0x19f1c63510f3ea8b8cd467ebe663897371919c185218d2859927f5a357b0bcae", + "layerzero_treasury_admin": "0x19f1c63510f3ea8b8cd467ebe663897371919c185218d2859927f5a357b0bcae", + "worker_peripherals": "0x19f1c63510f3ea8b8cd467ebe663897371919c185218d2859927f5a357b0bcae" + } +} diff --git a/packages/devtools-move/tasks/move/utils/index.ts b/packages/devtools-move/tasks/move/utils/index.ts new file mode 100644 index 000000000..fc451524e --- /dev/null +++ b/packages/devtools-move/tasks/move/utils/index.ts @@ -0,0 +1,222 @@ +import { Deserializer, Serializer } from '@layerzerolabs/lz-serdes' +import { addressToBytes32, trim0x } from '@layerzerolabs/lz-v2-utilities' + +/** + * Interface representing the limits. + */ +export interface OftLimit { + /** + * The minimum amount in LD (local decimals). + */ + min_amount_ld: bigint + + /** + * The maximum amount in LD (local decimals). + */ + max_amount_ld: bigint +} + +/** + * Interface representing the fee details. + */ +export interface OftFeeDetail { + /** + * The fee amount in LD (local decimals). + */ + fee_amount_ld: bigint + + /** + * Indicates if the fee is a reward. + */ + is_reward: boolean + + /** + * The description of the fee. + */ + description: string +} + +/** + * Enum representing the message types. + * Refer to aptos_contracts/bridge_remote/sources/internal_woft/woft_core.move + */ +export enum MessageType { + /** + * Message type for a message that does not contain a compose message + */ + SEND = 1, + /** + * Message type for a message that contains a compose message + */ + SEND_AND_CALL = 2, +} + +/** + * Enum representing the config types. + * Refer to packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/configuration.move (in monorepo) + */ +export enum ConfigType { + CONFIG_TYPE_EXECUTOR = 1, + CONFIG_TYPE_SEND_ULN = 2, + CONFIG_TYPE_RECV_ULN = 3, +} + +/** + * Interface representing the Byte32. + * Refer to packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/bytes32.move (in monorepo) + */ +export interface Byte32 { + bytes: Uint8Array +} + +/** + * Interface representing the ULN configuration. + * Refer to packages/layerzero-v2/aptos/contracts/msglib/msglib_types/sources/configs_uln.move (in monorepo) + */ +export interface UlnConfig { + /** + * The number of confirmations. + */ + confirmations: bigint + /** + * The optional DVN threshold. + */ + optional_dvn_threshold: number + /** + * The required DVNs. + */ + required_dvns: string[] + /** + * The optional DVNs. + */ + optional_dvns: string[] + /** + * Whether to use the default for confirmations. + */ + use_default_for_confirmations: boolean + /** + * Whether to use the default for required DVNs. + */ + use_default_for_required_dvns: boolean + /** + * Whether to use the default for optional DVNs. + */ + use_default_for_optional_dvns: boolean +} + +export const UlnConfig = { + isUlnConfig(obj: any): obj is UlnConfig { + return ( + typeof obj === 'object' && + obj !== null && + typeof obj.confirmations === 'bigint' && + typeof obj.optional_dvn_threshold === 'number' && + Array.isArray(obj.required_dvns) && + obj.required_dvns.every((item: any) => typeof item === 'string') && + Array.isArray(obj.optional_dvns) && + obj.optional_dvns.every((item: any) => typeof item === 'string') && + typeof obj.use_default_for_confirmations === 'boolean' && + typeof obj.use_default_for_required_dvns === 'boolean' && + typeof obj.use_default_for_optional_dvns === 'boolean' + ) + }, + + serialize(obj: UlnConfig): Uint8Array { + const serializer = new Serializer(false) + serializer.serializeU64(obj.confirmations) + serializer.serializeU8(obj.optional_dvn_threshold) + const requiredDVNCount = obj.required_dvns.length + serializer.serializeU8(requiredDVNCount) + for (const item of obj.required_dvns) { + serializer.serializeFixedBytes(addressToBytes32(item)) + } + const optionalDVNCount = obj.optional_dvns.length + serializer.serializeU8(optionalDVNCount) + for (const item of obj.optional_dvns) { + serializer.serializeFixedBytes(addressToBytes32(item)) + } + serializer.serializeBool(obj.use_default_for_confirmations) + serializer.serializeBool(obj.use_default_for_required_dvns) + serializer.serializeBool(obj.use_default_for_optional_dvns) + return serializer.getBytes() + }, + + deserialize(data: string | Uint8Array): UlnConfig { + if (typeof data === 'string') { + data = Uint8Array.from(Buffer.from(trim0x(data), 'hex')) + } + const deserializer = new Deserializer(data, false) + const confirmations = deserializer.deserializeU64() + const optional_dvn_threshold = deserializer.deserializeU8() + const requiredDVNCount = deserializer.deserializeU8() + const required_dvns: string[] = [] + for (let i = 0; i < requiredDVNCount; i++) { + required_dvns.push('0x' + Buffer.from(deserializer.deserializeFixedBytes(32)).toString('hex')) + } + const optionalDVNCount = deserializer.deserializeU8() + const optional_dvns: string[] = [] + for (let i = 0; i < optionalDVNCount; i++) { + optional_dvns.push('0x' + Buffer.from(deserializer.deserializeFixedBytes(32)).toString('hex')) + } + const use_default_for_confirmations = deserializer.deserializeBool() + const use_default_for_required_dvns = deserializer.deserializeBool() + const use_default_for_optional_dvns = deserializer.deserializeBool() + return { + confirmations, + optional_dvn_threshold, + required_dvns, + optional_dvns, + use_default_for_confirmations, + use_default_for_required_dvns, + use_default_for_optional_dvns, + } + }, +} + +/** + * Interface representing the executor configuration. + * Refer to packages/layerzero-v2/aptos/contracts/msglib/msglib_types/sources/configs_executor.move (in monorepo) + */ +export interface ExecutorConfig { + /** + * The maximum message size. + */ + max_message_size: number + /** + * The executor address. + */ + executor_address: string +} + +export const ExecutorConfig = { + isExecutorConfig(obj: any): obj is ExecutorConfig { + return ( + typeof obj === 'object' && + obj !== null && + typeof obj.max_message_size === 'number' && + typeof obj.executor_address === 'string' + ) + }, + + serialize(obj: ExecutorConfig): Uint8Array { + const serializer = new Serializer(false) + + serializer.serializeU32(obj.max_message_size) + + const addressBytes = addressToBytes32(obj.executor_address) + serializer.serializeFixedBytes(addressBytes) + + const finalBytes = serializer.getBytes() + return finalBytes + }, + + deserialize(data: string | Uint8Array): ExecutorConfig { + if (typeof data === 'string') { + data = Uint8Array.from(Buffer.from(trim0x(data), 'hex')) + } + const deserializer = new Deserializer(data, false) + const max_message_size = deserializer.deserializeU32() + const executor_address = Buffer.from(deserializer.deserializeFixedBytes(32)).toString('hex') + return { max_message_size, executor_address } + }, +} diff --git a/packages/devtools-move/tasks/move/utils/moveVMOftConfigOps.ts b/packages/devtools-move/tasks/move/utils/moveVMOftConfigOps.ts new file mode 100644 index 000000000..40d8efe52 --- /dev/null +++ b/packages/devtools-move/tasks/move/utils/moveVMOftConfigOps.ts @@ -0,0 +1,804 @@ +import * as fs from 'fs' +import * as path from 'path' + +import { InputGenerateTransactionPayloadData } from '@aptos-labs/ts-sdk' + +import { + ChainType, + EndpointId, + Stage, + endpointIdToChainType, + endpointIdToStage, + getNetworkForChainId, +} from '@layerzerolabs/lz-definitions' +import { ExecutorOptionType, Options } from '@layerzerolabs/lz-v2-utilities' + +import { Endpoint } from '../../../sdk/endpoint' +import { MsgLib } from '../../../sdk/msgLib' +import { OFT, OFTType } from '../../../sdk/oft' +import { createEidToNetworkMapping, diffPrinter } from '../../shared/utils' + +import { createSerializableUlnConfig } from './ulnConfigBuilder' + +import { ExecutorConfig, UlnConfig } from '.' + +import type { OAppOmniGraphHardhat, Uln302ExecutorConfig } from '@layerzerolabs/toolbox-hardhat' +import { decodeSolanaAddress } from '../../shared/basexToBytes32' + +export type TransactionPayload = { + payload: InputGenerateTransactionPayloadData + description: string + eid?: EndpointId +} + +// Configuration Types as used in Aptos Message Libraries +enum ConfigType { + EXECUTOR = 1, + SEND_ULN = 2, + RECV_ULN = 3, +} + +const configTypeToNameMap = { + [ConfigType.SEND_ULN]: 'Send', + [ConfigType.RECV_ULN]: 'Receive', + [ConfigType.EXECUTOR]: 'Executor', +} + +export async function createTransferOwnerOAppPayload( + oft: OFT, + newOwner: string, + eid: EndpointId +): Promise { + const currOwner = await oft.getAdmin() + if (currOwner == newOwner) { + console.log(`✅ Owner already set to ${newOwner}\n`) + return null + } else { + diffPrinter(`Set Owner for OApp at ${oft.oft_address}`, { address: currOwner }, { address: newOwner }) + const tx = oft.transferAdminPayload(newOwner) + return { payload: tx, description: 'Transfer Owner', eid: eid } + } +} + +export function createTransferObjectOwnerPayload(objectAddress: string, toAddress: string): TransactionPayload { + return { + payload: { + function: '0x1::object::transfer_call', + functionArguments: [objectAddress, toAddress], + }, + description: `Transfer object ${objectAddress} to ${toAddress}`, + } +} + +export async function setDelegate(oft: OFT, delegate: string, eid: EndpointId): Promise { + const currDelegate = await oft.getDelegate() + if (currDelegate == delegate) { + console.log(`✅ Delegate already set to ${delegate}\n`) + return null + } else { + diffPrinter( + `Set Delegate for Aptos OFT at ${oft.oft_address}`, + { address: currDelegate }, + { address: delegate } + ) + + const tx = oft.setDelegatePayload(delegate) + return { payload: tx, description: 'Set Delegate', eid: eid } + } +} + +export function toAptosAddress(address: string, eid: string): string { + if (!address) { + return '0x' + '0'.repeat(64) + } + + const chainType = endpointIdToChainType(Number(eid)) + + // Handle Solana addresses by decoding base58 first + if (chainType === ChainType.SOLANA) { + address = decodeSolanaAddress(address) + } + + address = address.toLowerCase() + const hex = address.replace('0x', '') + // Ensure the hex string is exactly 64 chars by padding or truncating + const paddedHex = hex.length > 64 ? hex.slice(-64) : hex.padStart(64, '0') + return '0x' + paddedHex +} + +export async function createSetPeerTx( + oft: OFT, + connection: OAppOmniGraphHardhat['connections'][number] +): Promise { + const eidToNetworkMapping = await createEidToNetworkMapping() + + if (!connection.to.contractName) { + printNotSet('peer', connection) + return null + } + const networkName = eidToNetworkMapping[connection.to.eid] + validateNetwork(networkName, connection) + const contractAddress = getContractAddress(networkName, connection.to.contractName) + const newPeer = toAptosAddress(contractAddress, connection.to.eid.toString()) + const currentPeerHex = await getCurrentPeer(oft, connection.to.eid as EndpointId) + + if (currentPeerHex === newPeer) { + printAlreadySet('peer', connection) + return null + } else { + const diffMessage = createDiffMessage('peer', connection) + diffPrinter(diffMessage, { address: currentPeerHex }, { address: newPeer }) + + const payload = oft.setPeerPayload(connection.to.eid as EndpointId, newPeer) + return { + payload: payload, + description: buildTransactionDescription('Set Peer', connection), + eid: connection.to.eid as EndpointId, + } + } +} + +function validateNetwork(networkName: string, entry: OAppOmniGraphHardhat['connections'][number]) { + if (!networkName) { + const toNetwork = getNetworkForChainId(entry.to.eid) + throw new Error(`Network not found in Hardhat config for ${toNetwork.chainName}-${toNetwork.env}`) + } +} + +export async function createSetReceiveLibraryTimeoutTx( + oft: OFT, + endpoint: Endpoint, + connection: OAppOmniGraphHardhat['connections'][number] +): Promise { + if (!connection.config?.receiveLibraryTimeoutConfig) { + printNotSet('Receive library timeout', connection) + return null + } + + const currentTimeout = await endpoint.getReceiveLibraryTimeout(oft.oft_address, connection.to.eid) + const defaultTimeout = await endpoint.getDefaultReceiveLibraryTimeout(connection.to.eid as EndpointId) + + // If current matches default, we should set the new value regardless + const isUsingDefault = currentTimeout.expiry === defaultTimeout.expiry && currentTimeout.lib === defaultTimeout.lib + + if (currentTimeout.expiry === BigInt(connection.config.receiveLibraryTimeoutConfig.expiry) && !isUsingDefault) { + printAlreadySet('Receive library timeout', connection) + return null + } else { + let diffMessage = createDiffMessage(`receive library timeout`, connection) + if (isUsingDefault) { + diffMessage += ' (currently set to defaults)' + } + + diffPrinter( + diffMessage, + { timeout: currentTimeout.expiry }, + { timeout: connection.config.receiveLibraryTimeoutConfig.expiry } + ) + + const tx = oft.setReceiveLibraryTimeoutPayload( + connection.to.eid, + connection.config.receiveLibraryTimeoutConfig.lib, + Number(connection.config.receiveLibraryTimeoutConfig.expiry) + ) + return { + payload: tx, + description: buildTransactionDescription('Set Receive Library Timeout', connection), + eid: connection.to.eid as EndpointId, + } + } +} + +export async function createSetReceiveLibraryTx( + oft: OFT, + endpoint: Endpoint, + connection: OAppOmniGraphHardhat['connections'][number] +): Promise { + if (!connection.config?.receiveLibraryConfig?.receiveLibrary) { + printNotSet('Receive library', connection) + return null + } + const currentReceiveLibrary = await endpoint.getReceiveLibrary(oft.oft_address, connection.to.eid) + const currentReceiveLibraryAddress = currentReceiveLibrary[0] + const isFallbackToDefault = currentReceiveLibrary[1] + + // if unset, fallbackToDefault will be true and the receive library should be set regardless of the current value + if ( + currentReceiveLibraryAddress === connection.config.receiveLibraryConfig.receiveLibrary && + !isFallbackToDefault + ) { + printAlreadySet('Receive library', connection) + return null + } else { + let diffMessage = createDiffMessage('receive library', connection) + if (isFallbackToDefault) { + diffMessage += ' (currently set to defaults)' + } + diffPrinter( + diffMessage, + { address: currentReceiveLibraryAddress }, + { address: connection.config.receiveLibraryConfig.receiveLibrary } + ) + const tx = await oft.setReceiveLibraryPayload( + connection.to.eid, + connection.config.receiveLibraryConfig.receiveLibrary, + Number(connection.config.receiveLibraryConfig.gracePeriod || 0) + ) + return { + payload: tx, + description: buildTransactionDescription('Set Receive Library', connection), + eid: connection.to.eid as EndpointId, + } + } +} + +export async function createSetSendLibraryTx( + oft: OFT, + endpoint: Endpoint, + connection: OAppOmniGraphHardhat['connections'][number] +) { + if (!connection.config?.sendLibrary) { + printNotSet('Send library', connection) + return null + } + const currentSendLibrary = await endpoint.getSendLibrary(oft.oft_address, connection.to.eid) + const currentSendLibraryAddress = currentSendLibrary[0] + const isFallbackToDefault = currentSendLibrary[1] + + // if unset, fallbackToDefault will be true and the receive library should be set regardless of the current value + if (currentSendLibraryAddress === connection.config.sendLibrary && !isFallbackToDefault) { + printAlreadySet('Send library', connection) + return null + } else { + let diffMessage = createDiffMessage('send library', connection) + if (isFallbackToDefault) { + diffMessage += ' (currently set to defaults)' + } + diffPrinter(diffMessage, { address: currentSendLibraryAddress }, { address: connection.config.sendLibrary }) + const tx = oft.setSendLibraryPayload(connection.to.eid, connection.config.sendLibrary) + return { + payload: tx, + description: buildTransactionDescription('Set Send Library', connection), + eid: connection.to.eid as EndpointId, + } + } +} + +export async function createSetSendConfigTx( + oft: OFT, + endpoint: Endpoint, + connection: OAppOmniGraphHardhat['connections'][number] +): Promise { + if (!connection.config?.sendConfig) { + printNotSet('Send config', connection) + return null + } + if (!connection.config.sendConfig.ulnConfig) { + printNotSet('Send config', connection) + return null + } + const newUlnConfig = createSerializableUlnConfig( + connection.config.sendConfig.ulnConfig, + connection.to, + connection.from + ) + + const currentSendLibrary = await endpoint.getSendLibrary(oft.oft_address, connection.to.eid) + const currentSendLibraryAddress = currentSendLibrary[0] + + const msgLib = new MsgLib(oft.moveVMConnection, currentSendLibraryAddress) + const defaultUlnConfig = await msgLib.get_default_uln_send_config(connection.to.eid as EndpointId) + const newSettingEqualsDefault = checkUlnConfigEqualsDefault(newUlnConfig, defaultUlnConfig) + + const currHexSerializedUlnConfig = await endpoint.getConfig( + oft.oft_address, + currentSendLibraryAddress, + connection.to.eid as EndpointId, + ConfigType.SEND_ULN + ) + const currUlnConfig = UlnConfig.deserialize(currHexSerializedUlnConfig) + + await checkNewConfig( + new MsgLib(oft.moveVMConnection, currentSendLibraryAddress), + newUlnConfig, + connection, + ConfigType.SEND_ULN + ) + + const serializedCurrentConfig = UlnConfig.serialize(currUlnConfig) + const newSerializedUlnConfig = UlnConfig.serialize(newUlnConfig) + + if (Buffer.from(serializedCurrentConfig).equals(Buffer.from(newSerializedUlnConfig)) && !newSettingEqualsDefault) { + printAlreadySet('Send config', connection) + return null + } else { + let diffMessage = createDiffMessage('send config', connection) + // Add a note if the current config is set to defaults + const currSettingEqualsDefault = checkUlnConfigEqualsDefault(currUlnConfig, defaultUlnConfig) + if (currSettingEqualsDefault) { + diffMessage += ' (currently set to defaults)' + } + + diffPrinter(diffMessage, currUlnConfig, newUlnConfig) + + const sendLibAddress = connection.config?.sendLibrary ?? currentSendLibraryAddress + + const tx = oft.setConfigPayload(sendLibAddress, connection.to.eid, ConfigType.SEND_ULN, newSerializedUlnConfig) + return { + payload: tx, + description: buildTransactionDescription('Set Send Config', connection), + eid: connection.to.eid as EndpointId, + } + } +} + +function checkUlnConfigEqualsDefault(ulnConfig: UlnConfig, defaultUlnConfig: UlnConfig): boolean { + return ( + ulnConfig.confirmations === defaultUlnConfig.confirmations && + ulnConfig.optional_dvn_threshold === defaultUlnConfig.optional_dvn_threshold && + JSON.stringify(ulnConfig.optional_dvns.sort()) === JSON.stringify(defaultUlnConfig.optional_dvns.sort()) && + JSON.stringify(ulnConfig.required_dvns.sort()) === JSON.stringify(defaultUlnConfig.required_dvns.sort()) && + ulnConfig.use_default_for_confirmations === defaultUlnConfig.use_default_for_confirmations && + ulnConfig.use_default_for_required_dvns === defaultUlnConfig.use_default_for_required_dvns && + ulnConfig.use_default_for_optional_dvns === defaultUlnConfig.use_default_for_optional_dvns + ) +} + +export async function createSetReceiveConfigTx( + oft: OFT, + endpoint: Endpoint, + connection: OAppOmniGraphHardhat['connections'][number] +): Promise { + if (!connection.config?.receiveConfig) { + printNotSet('Receive config', connection) + return null + } + if (!connection.config.receiveConfig.ulnConfig) { + printNotSet('Receive ULN config', connection) + return null + } + const newUlnConfig = createSerializableUlnConfig( + connection.config.receiveConfig.ulnConfig, + connection.to, + connection.from + ) + + const currentReceiveLibrary = await endpoint.getReceiveLibrary(oft.oft_address, connection.to.eid) + const currentReceiveLibraryAddress = currentReceiveLibrary[0] + + const msgLib = new MsgLib(oft.moveVMConnection, currentReceiveLibraryAddress) + const defaultUlnConfig = await msgLib.get_default_uln_receive_config(connection.to.eid as EndpointId) + const newSettingEqualsDefault = checkUlnConfigEqualsDefault(newUlnConfig, defaultUlnConfig) + + const currHexSerializedUlnConfig = await endpoint.getConfig( + oft.oft_address, + currentReceiveLibraryAddress, + connection.to.eid as EndpointId, + ConfigType.RECV_ULN + ) + + const currUlnConfig = UlnConfig.deserialize(currHexSerializedUlnConfig) + + await checkNewConfig( + new MsgLib(oft.moveVMConnection, currentReceiveLibraryAddress), + newUlnConfig, + connection, + ConfigType.RECV_ULN + ) + + const serializedCurrentConfig = UlnConfig.serialize(currUlnConfig) + const newSerializedUlnConfig = UlnConfig.serialize(newUlnConfig) + + if (Buffer.from(serializedCurrentConfig).equals(Buffer.from(newSerializedUlnConfig)) && !newSettingEqualsDefault) { + printAlreadySet('Receive config', connection) + return null + } else { + let diffMessage = createDiffMessage('receive config', connection) + // Add a note if the current config is set to defaults + const currSettingEqualsDefault = checkUlnConfigEqualsDefault(currUlnConfig, defaultUlnConfig) + if (currSettingEqualsDefault) { + diffMessage += ' (currently set to defaults)' + } + + diffPrinter(diffMessage, currUlnConfig, newUlnConfig) + + const receiveLibAddress = + connection.config?.receiveLibraryConfig?.receiveLibrary ?? currentReceiveLibraryAddress + + const tx = oft.setConfigPayload( + receiveLibAddress, + connection.to.eid, + ConfigType.RECV_ULN, + newSerializedUlnConfig + ) + return { + payload: tx, + description: buildTransactionDescription('Set Receive Config', connection), + eid: connection.to.eid as EndpointId, + } + } +} + +export async function checkExecutorConfigEqualsDefault( + msgLib: MsgLib, + newExecutorConfig: ExecutorConfig, + eid: EndpointId +): Promise { + const defaultExecutorConfig = await msgLib.get_default_executor_config(eid) + return ( + newExecutorConfig.executor_address === defaultExecutorConfig.executor_address && + newExecutorConfig.max_message_size === defaultExecutorConfig.max_message_size + ) +} + +export async function createSetExecutorConfigTx( + oft: OFT, + endpoint: Endpoint, + connection: OAppOmniGraphHardhat['connections'][number] +): Promise { + if (!connection.config?.sendConfig?.executorConfig) { + printNotSet('Executor config', connection) + return null + } + const newExecutorConfig = createSerializableExecutorConfig(connection.config.sendConfig.executorConfig) + const currentSendLibrary = await endpoint.getSendLibrary(oft.oft_address, connection.to.eid) + const currentSendLibraryAddress = currentSendLibrary[0] + + const currHexSerializedExecutorConfig = await endpoint.getConfig( + oft.oft_address, + currentSendLibraryAddress, + connection.to.eid as EndpointId, + ConfigType.EXECUTOR + ) + + const msgLib = new MsgLib(oft.moveVMConnection, currentSendLibraryAddress) + const newSettingEqualsDefault = await checkExecutorConfigEqualsDefault( + msgLib, + newExecutorConfig, + connection.to.eid as EndpointId + ) + + const currExecutorConfig = ExecutorConfig.deserialize(currHexSerializedExecutorConfig) + currExecutorConfig.executor_address = '0x' + currExecutorConfig.executor_address + // We need to re-serialize the current config to compare it with the new config to ensure same format + const serializedCurrentConfig = ExecutorConfig.serialize(currExecutorConfig) + + const newSerializedExecutorConfig = ExecutorConfig.serialize(newExecutorConfig) + + if ( + Buffer.from(serializedCurrentConfig).equals(Buffer.from(newSerializedExecutorConfig)) && + !newSettingEqualsDefault + ) { + printAlreadySet('Executor config', connection) + return null + } else { + let diffMessage = createDiffMessage('executor config', connection) + // Add a note if the current config is set to defaults + const currSettingEqualsDefault = await checkExecutorConfigEqualsDefault( + msgLib, + currExecutorConfig, + connection.to.eid as EndpointId + ) + if (currSettingEqualsDefault) { + diffMessage += ' (currently set to defaults)' + } + + diffPrinter(diffMessage, currExecutorConfig, newExecutorConfig) + + const sendLibrary = connection.config.sendLibrary ?? currentSendLibraryAddress + + const tx = oft.setConfigPayload( + sendLibrary, + connection.to.eid, + ConfigType.EXECUTOR, + newSerializedExecutorConfig + ) + return { + payload: tx, + description: buildTransactionDescription('setExecutorConfig', connection), + eid: connection.to.eid as EndpointId, + } + } +} + +export async function createSetRateLimitTx( + oft: OFT, + rateLimit: bigint, + window_seconds: bigint, + eid: EndpointId, + oftType: OFTType +): Promise { + const [currentLimit, currentWindow] = await oft.getRateLimitConfig(eid, oftType) + const toNetwork = getNetworkForChainId(eid) + + if (currentLimit === rateLimit && currentWindow === window_seconds) { + console.log(`✅ Rate limit already set for ${toNetwork.chainName}-${toNetwork.env}`) + return null + } else { + diffPrinter( + `Set rate limit for ${toNetwork.chainName}-${toNetwork.env}`, + { limit: currentLimit, window: currentWindow }, + { limit: rateLimit, window: window_seconds } + ) + + const tx = oft.createSetRateLimitTx(eid, rateLimit, window_seconds, oftType) + return { + payload: tx, + description: `Set rate limit for ${toNetwork.chainName}-${toNetwork.env}`, + eid: eid, + } + } +} + +export async function createUnsetRateLimitTx( + oft: OFT, + eid: EndpointId, + oftType: OFTType +): Promise { + const tx = oft.createUnsetRateLimitTx(eid, oftType) + const toNetwork = getNetworkForChainId(eid) + return { + payload: tx, + description: `Unset rate limit for ${toNetwork.chainName}-${toNetwork.env}`, + eid: eid, + } +} + +export async function createSetFeeBpsTx( + oft: OFT, + fee_bps: bigint, + eid: EndpointId, + oftType: OFTType +): Promise { + const currentFeeBps = await oft.getFeeBps(oftType) + const toNetwork = getNetworkForChainId(eid) + + if (currentFeeBps === fee_bps) { + console.log(`✅ Fee BPS already set for ${toNetwork.chainName}-${toNetwork.env}`) + return null + } else { + diffPrinter( + `Set fee BPS for ${toNetwork.chainName}-${toNetwork.env}`, + { fee_bps: currentFeeBps }, + { fee_bps: fee_bps } + ) + + const tx = oft.createSetFeeBpsTx(fee_bps, oftType) + return { + payload: tx, + description: `Set fee BPS for ${toNetwork.chainName}-${toNetwork.env}`, + eid: eid, + } + } +} + +// getPeer errors if there is no peer set, so we need to check if there is a peer before calling getPeer +async function getCurrentPeer(oft: OFT, eid: EndpointId): Promise { + const hasPeer = await oft.hasPeer(eid) + return hasPeer ? await oft.getPeer(eid) : '' +} + +function getContractAddress(networkName: string, contractName: string | null | undefined) { + const deploymentPath = path.join(process.cwd(), `/deployments/${networkName}/${contractName}.json`) + + try { + const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8')) + return deployment.address + } catch (error) { + throw new Error(`Failed to read deployment file for network ${networkName}: ${error}\n`) + } +} + +function addOptions(enforcedOption: any, options: Options) { + if (enforcedOption.optionType === ExecutorOptionType.LZ_RECEIVE) { + options.addExecutorLzReceiveOption(enforcedOption.gas, enforcedOption.value) + } else if (enforcedOption.optionType === ExecutorOptionType.NATIVE_DROP) { + options.addExecutorNativeDropOption(enforcedOption.amount, enforcedOption.receiver) + } else if (enforcedOption.optionType === ExecutorOptionType.COMPOSE) { + options.addExecutorComposeOption(enforcedOption.gas, enforcedOption.value) + } else if (enforcedOption.optionType === ExecutorOptionType.ORDERED) { + options.addExecutorOrderedExecutionOption() + } else if (enforcedOption.optionType === ExecutorOptionType.LZ_READ) { + options.addExecutorLzReadOption(enforcedOption.gas, enforcedOption.value) + } +} + +export async function createSetEnforcedOptionsTxs( + oft: OFT, + connection: OAppOmniGraphHardhat['connections'][number] +): Promise { + const txs: TransactionPayload[] = [] + if (!connection.config?.enforcedOptions) { + printNotSet('Enforced options', connection) + return [] + } + + const msgTypes = [1, 2] + for (const msgType of msgTypes) { + const options = getOptionsByMsgType(connection, msgType) + const tx = await createTxFromOptions(options, connection, oft, msgType) + if (tx) { + txs.push({ + payload: tx, + description: buildTransactionDescription('Set Enforced Options', connection), + eid: connection.to.eid as EndpointId, + }) + } + } + return txs +} + +function getOptionsByMsgType(entry: OAppOmniGraphHardhat['connections'][number], msgType: number): any[] { + const options = [] + for (const enforcedOption of entry.config?.enforcedOptions ?? []) { + if (enforcedOption.msgType === msgType) { + options.push(enforcedOption) + } + } + return options +} + +async function createTxFromOptions( + options: Options[], + entry: OAppOmniGraphHardhat['connections'][number], + oft: OFT, + msgType: number +): Promise { + const newOptions = Options.newOptions() + for (const enforcedOption of options) { + addOptions(enforcedOption, newOptions) + } + const currentOptionsHex = ensureOptionsCompatible(await oft.getEnforcedOptions(entry.to.eid, msgType)) + + if (newOptions.toHex() === currentOptionsHex) { + printAlreadySet('Enforced options', entry) + return null + } else { + decodeOptionsAndPrintDiff(entry, currentOptionsHex, newOptions.toHex(), msgType) + const tx = oft.setEnforcedOptionsPayload(entry.to.eid, msgType, newOptions.toBytes()) + return tx + } +} + +function decodeOptionsAndPrintDiff( + entry: OAppOmniGraphHardhat['connections'][number], + currentOptionsHex: string, + newOptionsHex: string, + msgType: number +) { + const currentOptionsString = Options.fromOptions(currentOptionsHex) + const newOptionsString = Options.fromOptions(newOptionsHex) + + // Default empty values based on type definitions + const emptyDefaults = { + LzReceiveOption: { gas: undefined, value: undefined }, + NativeDropOption: [] as { amount: bigint; receiver: string }[], + ComposeOption: [] as { index: number; gas: bigint; value: bigint }[], + LzReadOption: { gas: undefined, dataSize: undefined, value: undefined }, + OrderedExecutionOption: undefined, // Assuming boolean based on context + } + + const currentOptions = { + LzReceiveOption: currentOptionsString.decodeExecutorLzReceiveOption() ?? emptyDefaults.LzReceiveOption, + NativeDropOption: currentOptionsString.decodeExecutorNativeDropOption() ?? emptyDefaults.NativeDropOption, + ComposeOption: currentOptionsString.decodeExecutorComposeOption() ?? emptyDefaults.ComposeOption, + LzReadOption: currentOptionsString.decodeExecutorLzReadOption() ?? emptyDefaults.LzReadOption, + OrderedExecutionOption: + currentOptionsString.decodeExecutorOrderedExecutionOption() ?? emptyDefaults.OrderedExecutionOption, + } + + const newOptions = { + LzReceiveOption: newOptionsString.decodeExecutorLzReceiveOption() ?? emptyDefaults.LzReceiveOption, + NativeDropOption: newOptionsString.decodeExecutorNativeDropOption() ?? emptyDefaults.NativeDropOption, + ComposeOption: newOptionsString.decodeExecutorComposeOption() ?? emptyDefaults.ComposeOption, + LzReadOption: newOptionsString.decodeExecutorLzReadOption() ?? emptyDefaults.LzReadOption, + OrderedExecutionOption: + newOptionsString.decodeExecutorOrderedExecutionOption() ?? emptyDefaults.OrderedExecutionOption, + } + const diffMessage = createDiffMessage(`enforced options with message type: ${msgType}`, entry) + diffPrinter(diffMessage, currentOptions, newOptions) +} + +// Default options return from aptos are 0x, however the options decoder expects 0x00 +function ensureOptionsCompatible(optionsHex: string): string { + if (optionsHex === '0x') { + return '0x00' + } + return optionsHex +} + +async function checkNewConfig( + msgLib: MsgLib, + newUlnConfig: UlnConfig, + entry: OAppOmniGraphHardhat['connections'][number], + configType: ConfigType +) { + // Check if the new config has less DVNs than the default one and warn if it does + if ( + newUlnConfig.required_dvns.length + newUlnConfig.optional_dvns.length < 2 && + endpointIdToStage(entry.from.eid) === Stage.MAINNET + ) { + console.log(createWarningMessage(configType, entry)) + console.log(`\tConfig has less than 2 DVNs.\n\tWe strongly recommend setting at least 2 DVNs for mainnet.\n`) + } + + // Check if the new config has less confirmations than the default one and warn if it does + if (configType === ConfigType.RECV_ULN) { + const defaultReceiveConfig = await msgLib.get_default_uln_receive_config(entry.to.eid as EndpointId) + const defaultConfirmations = defaultReceiveConfig.confirmations + if (newUlnConfig.confirmations < defaultConfirmations) { + console.log(createWarningMessage(configType, entry)) + console.log( + `\tConfig has less than ${defaultConfirmations} block confirmations.\n\tWe recommend setting at least ${defaultConfirmations} block confirmations.\n` + ) + } + } else if (configType === ConfigType.SEND_ULN) { + const defaultSendConfig = await msgLib.get_default_uln_send_config(entry.to.eid as EndpointId) + const defaultConfirmations = defaultSendConfig.confirmations + if (newUlnConfig.confirmations < defaultConfirmations) { + console.log(createWarningMessage(configType, entry)) + console.log( + `\tConfig has less than ${defaultConfirmations} block confirmations.\n\tWe recommend setting at least ${defaultConfirmations} block confirmations.\n` + ) + } + } +} + +export function createIrrevocablyDisableBlocklistPayload(oft: OFT, oftType: OFTType): TransactionPayload { + const payload = oft.irrevocablyDisableBlocklistPayload(oftType) + return { + payload: payload, + description: `Irrevocably Disable Blocklist`, + } +} + +export function createPermanentlyDisableFungibleStoreFreezingPayload(oft: OFT): TransactionPayload { + const payload = oft.permanentlyDisableFungibleStoreFreezingPayload() + return { + payload: payload, + description: `Permanently Disable Freezing`, + } +} + +function createSerializableExecutorConfig(executorConfig: Uln302ExecutorConfig): ExecutorConfig { + return { + max_message_size: executorConfig.maxMessageSize, + + executor_address: executorConfig.executor, + } +} + +function createDiffMessage(elementDesc: string, entry: OAppOmniGraphHardhat['connections'][number]) { + const toNetwork = getNetworkForChainId(entry.to.eid) + const fromNetwork = getNetworkForChainId(entry.from.eid) + return `Set ${elementDesc} for pathway ${entry.from.contractName} on ${fromNetwork.chainName}-${fromNetwork.env} → ${toNetwork.chainName}-${toNetwork.env}` +} + +function printAlreadySet(configObject: string, entry: OAppOmniGraphHardhat['connections'][number]) { + const toNetwork = getNetworkForChainId(entry.to.eid) + const fromNetwork = getNetworkForChainId(entry.from.eid) + console.log( + `✅ ${configObject} already set or pathway ${fromNetwork.chainName}-${fromNetwork.env} → ${toNetwork.chainName}-${toNetwork.env}\n` + ) +} + +function printNotSet(configObject: string, entry: OAppOmniGraphHardhat['connections'][number]) { + const toNetwork = getNetworkForChainId(entry.to.eid) + const fromNetwork = getNetworkForChainId(entry.from.eid) + console.log( + `No ${configObject} specified for pathway ${fromNetwork.chainName}-${fromNetwork.env} → ${toNetwork.chainName}-${toNetwork.env}\n` + ) +} + +function createWarningMessage(configType: ConfigType, entry: OAppOmniGraphHardhat['connections'][number]) { + const fromNetwork = getNetworkForChainId(entry.from.eid) + const toNetwork = getNetworkForChainId(entry.to.eid) + return `⚠️ WARN: ${configTypeToNameMap[configType]} config for ${fromNetwork.chainName}-${fromNetwork.env} → ${toNetwork.chainName}-${toNetwork.env}` +} + +function buildTransactionDescription(action: string, connections: OAppOmniGraphHardhat['connections'][number]): string { + const fromNetwork = getNetworkForChainId(connections.from.eid) + const toNetwork = getNetworkForChainId(connections.to.eid) + + return `${action} from ${fromNetwork.chainName}-${fromNetwork.env} → ${toNetwork.chainName}-${toNetwork.env}` +} diff --git a/packages/devtools-move/tasks/move/utils/ulnConfigBuilder.ts b/packages/devtools-move/tasks/move/utils/ulnConfigBuilder.ts new file mode 100644 index 000000000..0ce9ebec4 --- /dev/null +++ b/packages/devtools-move/tasks/move/utils/ulnConfigBuilder.ts @@ -0,0 +1,67 @@ +import type { OmniPointHardhat, Uln302UlnUserConfig } from '@layerzerolabs/toolbox-hardhat' + +interface SerializableUlnConfig { + confirmations: bigint + optional_dvn_threshold: number + optional_dvns: string[] + required_dvns: string[] + use_default_for_confirmations: boolean + use_default_for_required_dvns: boolean + use_default_for_optional_dvns: boolean +} + +export function createSerializableUlnConfig( + ulnConfig: Uln302UlnUserConfig, + to: OmniPointHardhat, + from: OmniPointHardhat +): SerializableUlnConfig { + // Validate required fields + if (ulnConfig.requiredDVNs === undefined) { + throw new Error( + `requiredDVNs must be specified in ULN configuration for ${from.contractName}(${from.eid}) -> ${to.contractName}(${to.eid})\nIf you intend to use defaults for requiredDVNs, set requiredDVNs to an empty array.` + ) + } + // TODO: ensure behaviour is correct with krak + // if (ulnConfig.optionalDVNs === undefined) { + // throw new Error( + // `optionalDVNs must be specified in ULN configuration for ${from.contractName}(${from.eid}) -> ${to.contractName}(${to.eid})\nIf you intend to use defaults for optionalDVNs, set optionalDVNs to an empty array.` + // ) + // } + if (ulnConfig.optionalDVNThreshold === undefined) { + throw new Error( + `optionalDVNThreshold must be specified in ULN configuration for ${from.contractName}(${from.eid}) -> ${to.contractName}(${to.eid})` + ) + } + if (ulnConfig.confirmations === undefined) { + throw new Error( + `confirmations must be specified in ULN configuration for ${from.contractName}(${from.eid}) -> ${to.contractName}(${to.eid})\nIf you wish to use defaults for confirmations, set confirmations to 0.` + ) + } + + const confirmations = ulnConfig.confirmations + const optionalDVNThreshold = ulnConfig.optionalDVNThreshold + const requiredDVNs = ulnConfig.requiredDVNs + const optionalDVNs = ulnConfig.optionalDVNs + + // Use defaults when confirmations are set to 0 + const useDefaultForConfirmations = confirmations === BigInt(0) + // Use defaults for required DVNs when array is empty + const useDefaultForRequiredDVNs = !requiredDVNs.length + // Use defaults for optional DVNs when array is empty + let useDefaultForOptionalDVNs = false + if (optionalDVNs === undefined) { + useDefaultForOptionalDVNs = true + } else { + useDefaultForOptionalDVNs = !optionalDVNs.length + } + + return { + confirmations, + optional_dvn_threshold: optionalDVNThreshold, + optional_dvns: optionalDVNs ?? [], + required_dvns: requiredDVNs, + use_default_for_confirmations: useDefaultForConfirmations, + use_default_for_required_dvns: useDefaultForRequiredDVNs, + use_default_for_optional_dvns: useDefaultForOptionalDVNs, + } satisfies SerializableUlnConfig +} diff --git a/packages/devtools-move/tasks/move/utils/utils.ts b/packages/devtools-move/tasks/move/utils/utils.ts new file mode 100644 index 000000000..80811450e --- /dev/null +++ b/packages/devtools-move/tasks/move/utils/utils.ts @@ -0,0 +1,172 @@ +import * as fs from 'fs' +import * as readline from 'readline' + +import { Aptos } from '@aptos-labs/ts-sdk' + +import { EndpointId, Stage } from '@layerzerolabs/lz-definitions' + +import { OFT } from '../../../sdk/oft' + +import { TransactionPayload } from './moveVMOftConfigOps' + +import type { OAppOmniGraphHardhat } from '@layerzerolabs/toolbox-hardhat' +import path from 'path' + +export function getDelegateFromLzConfig(eid: EndpointId, config: OAppOmniGraphHardhat): string { + validateConfigHasDelegate(config, eid) + let delegate = '' + for (const conn of config.contracts) { + if (conn.contract.eid == eid) { + delegate = conn.config?.delegate ?? '' + delegate = delegate.startsWith('0x') ? delegate : `0x${delegate}` + } + } + return delegate +} + +export function getOwnerFromLzConfig(eid: EndpointId, config: OAppOmniGraphHardhat): string { + validateConfigHasOwner(config, eid) + let owner = '' + for (const conn of config.contracts) { + if (conn.contract.eid == eid) { + owner = conn.config?.owner ?? '' + owner = owner.startsWith('0x') ? owner : `0x${owner}` + } + } + return owner +} + +export function validateConfigHasDelegate(config: OAppOmniGraphHardhat, eid: EndpointId) { + const aptosConfig = config.contracts.find((c: any) => c.contract.eid === eid) + + if (!aptosConfig || !aptosConfig.config || !aptosConfig.config.delegate) { + console.log(` +[LayerZero Config] Update Required +-------------------------------- +Current Configuration: +• delegate: ${aptosConfig?.config?.delegate || 'not found'} + +Please update layerzero config with your Aptos delegate address +{ + contract: , + config: { + delegate: '', + } +} +`) + throw new Error('Please update your Aptos configuration with valid delegate address') + } +} + +function validateConfigHasOwner(config: OAppOmniGraphHardhat, eid: EndpointId) { + const aptosConfig = config.contracts.find((c: any) => c.contract.eid === eid) + + if (!aptosConfig || !aptosConfig.config || !aptosConfig.config.owner) { + console.log(` +[LayerZero Config] Update Required +-------------------------------- +Current Configuration: +• owner: ${aptosConfig?.config?.owner || 'not found'} + +Please update layerzero config with your Aptos owner address +{ + contract: , + config: { + owner: '', + } +} +`) + throw new Error('Please update your Aptos configuration with valid owner address') + } +} + +export function getOwner(config: OAppOmniGraphHardhat, eid: EndpointId) { + for (const conn of config.contracts) { + if (conn.contract.eid == eid) { + const owner = conn.config?.owner ?? '' + return owner.startsWith('0x') ? owner : `0x${owner}` + } + } +} + +export function getMoveVMOftAddress(network: string, stage: Stage, rootDir: string = process.cwd()) { + const deploymentPath = path.join(rootDir, `deployments/${network}-${stage}/oft.json`) + const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8')) + return deployment.address +} + +export async function sendAllTxs( + aptos: Aptos, + oft: OFT, + account_address: string, + payloads: (TransactionPayload | null)[] +) { + const cleanedPayloads = pruneNulls(payloads) + + if (cleanedPayloads.length == 0) { + console.log('✨ No transactions to send.') + return + } + if (await promptForConfirmation(cleanedPayloads.length)) { + console.log('\n📦 Transaction Summary:') + console.log(` • Total transactions: ${cleanedPayloads.length}`) + + for (let i = 0; i < cleanedPayloads.length; i++) { + const progress = `[${i + 1}/${cleanedPayloads.length}]` + console.log(`🔄 ${progress} Processing transaction ${i}: ${cleanedPayloads[i].description}...`) + + const trans = await aptos.transaction.build.simple({ + sender: account_address, + data: cleanedPayloads[i].payload, + }) + const result = await oft.signSubmitAndWaitForTx(trans) + + console.log(` 📎 Transaction hash: ${result.hash}`) + console.log(` 🔍 Explorer: https://explorer.aptoslabs.com/txn/${result.hash}?network=testnet`) + } + + console.log('🎉 Transaction Summary:') + console.log(` • ${cleanedPayloads.length} transactions processed successfully`) + } else { + console.log('Operation cancelled.') + process.exit(0) + } +} + +function pruneNulls(payloads: (TransactionPayload | null)[]): TransactionPayload[] { + return payloads.filter((payload): payload is TransactionPayload => payload !== null && payload.payload !== null) +} + +async function promptForConfirmation(txCount: number): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + + const answer = await new Promise((resolve) => { + rl.question( + `\nReview the ${txCount} transaction(s) above carefully.\nWould you like to proceed with execution? (yes/no): `, + resolve + ) + }) + + rl.close() + return ['yes', 'y'].includes(answer.toLowerCase().trim()) +} + +export async function sendInitTransaction( + moveVMConnection: Aptos, + oft: OFT, + account_address: string, + payloads: TransactionPayload[] +) { + try { + await sendAllTxs(moveVMConnection, oft, account_address, payloads) + } catch (error: any) { + if (error.message?.includes('EALREADY_INITIALIZED')) { + console.log('\n✅ OFT already initialized. No values changed.\n') + } else { + console.error('Error:', error) + } + } +} diff --git a/packages/devtools-move/tasks/move/wireMove.ts b/packages/devtools-move/tasks/move/wireMove.ts new file mode 100644 index 000000000..0c1d9f06c --- /dev/null +++ b/packages/devtools-move/tasks/move/wireMove.ts @@ -0,0 +1,128 @@ +import { OAppOmniGraphHardhat } from '@layerzerolabs/toolbox-hardhat' +import { Endpoint } from '../../sdk/endpoint' +import { getChain, getConnection } from '../../sdk/moveVMConnectionBuilder' +import { OFT } from '../../sdk/oft' +import { getConfigConnections } from '../shared/utils' + +import { getEidFromMoveNetwork, getLzNetworkStage, parseYaml } from './utils/aptosNetworkParser' +import { getNamedAddresses } from './utils/config' +import * as oftConfig from './utils/moveVMOftConfigOps' +import { TransactionPayload } from './utils/moveVMOftConfigOps' +import { getMoveVMOftAddress, sendAllTxs } from './utils/utils' +import path from 'path' +import { getNetworkForChainId } from '@layerzerolabs/lz-definitions' + +async function wireMove(args: any) { + const { account_address, private_key, network, fullnode, faucet } = await parseYaml(args.rootDir) + const fullConfigPath = path.join(args.rootDir, args.oapp_config) + const chain = getChain(fullnode) + + const moveVMConnection = getConnection(chain, network, fullnode, faucet) + + const lzNetworkStage = getLzNetworkStage(network) + + const moveVMOftAddress = getMoveVMOftAddress(chain, lzNetworkStage) + const namedAddresses = getNamedAddresses(lzNetworkStage) + const endpointAddress = getEndpointAddressFromNamedAddresses(namedAddresses) + + console.log(`\n🔌 Wiring ${chain}-${lzNetworkStage} OFT`) + console.log(`\tAddress: ${moveVMOftAddress}\n`) + + const oftSDK = new OFT(moveVMConnection, moveVMOftAddress, account_address, private_key) + const moveVMEndpoint = new Endpoint(moveVMConnection, endpointAddress) + + const currDelegate = await oftSDK.getDelegate() + validateDelegate(currDelegate, account_address) + + const moveVMEndpointID = getEidFromMoveNetwork(chain, network) + const connectionsFromMoveToAny = await getConfigConnections('from', moveVMEndpointID, fullConfigPath) + + const txs = await createWiringTxs(oftSDK, moveVMEndpoint, connectionsFromMoveToAny) + await sendAllTxs(moveVMConnection, oftSDK, account_address, txs) +} + +async function createWiringTxs( + oft: OFT, + endpoint: Endpoint, + connectionConfigs: OAppOmniGraphHardhat['connections'] +): Promise { + const txs: TransactionPayload[] = [] + + for (const connection of connectionConfigs) { + logPathwayHeader(connection) + + const setPeerTx = await oftConfig.createSetPeerTx(oft, connection) + if (setPeerTx) { + txs.push(setPeerTx) + } + + const setEnforcedOptionsTxs = await oftConfig.createSetEnforcedOptionsTxs(oft, connection) + if (setEnforcedOptionsTxs.length > 0) { + txs.push(...setEnforcedOptionsTxs) + } + + const setSendLibraryTx = await oftConfig.createSetSendLibraryTx(oft, endpoint, connection) + if (setSendLibraryTx) { + txs.push(setSendLibraryTx) + } + + const setReceiveLibraryTx = await oftConfig.createSetReceiveLibraryTx(oft, endpoint, connection) + if (setReceiveLibraryTx) { + txs.push(setReceiveLibraryTx) + } + + const setReceiveLibraryTimeoutTx = await oftConfig.createSetReceiveLibraryTimeoutTx(oft, endpoint, connection) + if (setReceiveLibraryTimeoutTx) { + txs.push(setReceiveLibraryTimeoutTx) + } + + const setSendConfigTx = await oftConfig.createSetSendConfigTx(oft, endpoint, connection) + if (setSendConfigTx) { + txs.push(setSendConfigTx) + } + + const setExecutorConfigTx = await oftConfig.createSetExecutorConfigTx(oft, endpoint, connection) + if (setExecutorConfigTx) { + txs.push(setExecutorConfigTx) + } + + const setReceiveConfigTx = await oftConfig.createSetReceiveConfigTx(oft, endpoint, connection) + if (setReceiveConfigTx) { + txs.push(setReceiveConfigTx) + } + } + return txs +} + +function logPathwayHeader(connection: OAppOmniGraphHardhat['connections'][number]) { + const fromNetwork = getNetworkForChainId(connection.from.eid) + const toNetwork = getNetworkForChainId(connection.to.eid) + + const pathwayString = `🔄 Building wire transactions for pathway: ${fromNetwork.chainName}-${fromNetwork.env} → ${toNetwork.chainName}-${toNetwork.env} 🔄` + const borderLine = '━'.repeat(pathwayString.length) + + console.log(borderLine) + console.log(pathwayString) + console.log(`${borderLine}\n`) +} + +function validateDelegate(currDelegate: string, account_address: string) { + if (currDelegate != account_address) { + throw new Error( + `Delegate must be set to account address of the transaction sender for wiring.\n\tCurrent delegate: ${currDelegate}, expected: ${account_address}\n\n` + ) + } +} +function getEndpointAddressFromNamedAddresses(namedAddresses: string): string { + const addresses = namedAddresses.split(',') + const endpointEntry = addresses.find((addr) => addr.startsWith('endpoint_v2=')) + const endpointAddress = endpointEntry?.split('=')[1] + + if (!endpointAddress) { + throw new Error('Endpoint address not found in named addresses configuration') + } + + return endpointAddress +} + +export { wireMove } diff --git a/packages/devtools-move/tasks/shared/basexToBytes32.ts b/packages/devtools-move/tasks/shared/basexToBytes32.ts new file mode 100644 index 000000000..b23b713f5 --- /dev/null +++ b/packages/devtools-move/tasks/shared/basexToBytes32.ts @@ -0,0 +1,30 @@ +import basex from 'base-x' +import { ethers } from 'ethers' +import { ChainType, endpointIdToChainType } from '@layerzerolabs/lz-definitions' + +const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + +export function basexToBytes32(basexAddress: string, eid: string): string { + const chainType = endpointIdToChainType(Number(eid)) + + switch (chainType) { + case ChainType.EVM: { + const addressBytes = ethers.utils.zeroPad(basexAddress, 32) + return `0x${Buffer.from(addressBytes).toString('hex')}` + } + case ChainType.APTOS: { + return basexAddress + } + case ChainType.SOLANA: { + return decodeSolanaAddress(basexAddress) + } + default: { + throw new Error('Invalid chain type') + } + } +} + +export function decodeSolanaAddress(address: string): string { + const addressBytes = basex(BASE58).decode(address) + return '0x' + Buffer.from(addressBytes).toString('hex') +} diff --git a/packages/devtools-move/tasks/shared/messageBuilder.ts b/packages/devtools-move/tasks/shared/messageBuilder.ts new file mode 100644 index 000000000..9213e3fc4 --- /dev/null +++ b/packages/devtools-move/tasks/shared/messageBuilder.ts @@ -0,0 +1,57 @@ +import { getNetworkForChainId } from '@layerzerolabs/lz-definitions' + +enum ConfigType { + EXECUTOR = 1, + SEND_ULN = 2, + RECV_ULN = 3, +} + +const configTypeToNameMap = { + [ConfigType.SEND_ULN]: 'Send', + [ConfigType.RECV_ULN]: 'Receive', + [ConfigType.EXECUTOR]: 'Executor', +} + +export function createDiffMessage(elementDesc: string, fromEid: number, toEid: number) { + const toNetwork = getNetworkForChainId(toEid) + const fromNetwork = getNetworkForChainId(fromEid) + return `Set ${elementDesc} for pathway ${fromNetwork.chainName}-${fromNetwork.env} → ${toNetwork.chainName}-${toNetwork.env}` +} + +export function printAlreadySet(configObject: string, fromEid: number, toEid: number) { + const toNetwork = getNetworkForChainId(toEid) + const fromNetwork = getNetworkForChainId(fromEid) + console.log( + `✅ ${configObject} already set or pathway ${fromNetwork.chainName}-${fromNetwork.env} → ${toNetwork.chainName}-${toNetwork.env}\n` + ) +} + +export function printNotSet(configObject: string, fromEid: number, toEid: number) { + const toNetwork = getNetworkForChainId(toEid) + const fromNetwork = getNetworkForChainId(fromEid) + console.log( + `No ${configObject} specified for pathway ${fromNetwork.chainName}-${fromNetwork.env} → ${toNetwork.chainName}-${toNetwork.env}\n` + ) +} + +export function createWarningMessage(configType: ConfigType, fromEid: number, toEid: number) { + const fromNetwork = getNetworkForChainId(fromEid) + const toNetwork = getNetworkForChainId(toEid) + return `⚠️ WARN: ${configTypeToNameMap[configType]} config for ${fromNetwork.chainName}-${fromNetwork.env} → ${toNetwork.chainName}-${toNetwork.env}` +} + +export function buildTransactionDescription(action: string, fromEid: number, toEid: number): string { + const fromNetwork = getNetworkForChainId(fromEid) + const toNetwork = getNetworkForChainId(toEid) + + return `${action} from ${fromNetwork.chainName}-${fromNetwork.env} → ${toNetwork.chainName}-${toNetwork.env}` +} + +export function logPathwayHeader(op: string) { + const pathwayString = `🔄 Building ${op} transactions 🔄` + const borderLine = '━'.repeat(pathwayString.length) + + console.log(borderLine) + console.log(pathwayString) + console.log(`${borderLine}\n`) +} diff --git a/packages/devtools-move/tasks/shared/types.ts b/packages/devtools-move/tasks/shared/types.ts new file mode 100644 index 000000000..58380f2c5 --- /dev/null +++ b/packages/devtools-move/tasks/shared/types.ts @@ -0,0 +1,14 @@ +export type deploymentFile = { + address: string + abi: [] + transactionHash: '' + receipt: object + args: [] + numDeployments: 1 + solcInputHash: '' + metadata: '' + bytecode: '' + deployedBytecode: '' + devdoc: object + storageLayout: object +} diff --git a/packages/devtools-move/tasks/shared/utils.ts b/packages/devtools-move/tasks/shared/utils.ts new file mode 100644 index 000000000..e0e9e1c65 --- /dev/null +++ b/packages/devtools-move/tasks/shared/utils.ts @@ -0,0 +1,255 @@ +import * as readline from 'readline' + +import type { + OAppEdgeConfig, + OAppNodeConfig, + OAppOmniGraphHardhat, + OmniEdgeHardhat, +} from '@layerzerolabs/toolbox-hardhat' +import { ChainType, endpointIdToChainType } from '@layerzerolabs/lz-definitions' +import path from 'path' +import 'hardhat/register' + +/* + * Parses hardhat.config.ts and returns a mapping of EID to network name or URL. + */ +export async function createEidToNetworkMapping(_value = 'networkName'): Promise> { + const hardhatConfigPath = path.resolve(`${process.cwd()}/hardhat.config.ts`) + const hardhatConfigFile = await import(hardhatConfigPath) + const hardhatConfig = hardhatConfigFile.default + + if (!hardhatConfig.networks) { + throw new Error('No networks found in hardhat config') + } + + const networks = hardhatConfig.networks + const eidNetworkNameMapping: Record = {} + for (const [networkName, networkConfig] of Object.entries(networks)) { + if (networkName === 'hardhat') { + continue + } + const netConfig = networkConfig as { eid: number; url: string } + if (!netConfig['eid']) { + throw new Error(`EID not found for network: ${networkName}`) + } + if (_value == 'networkName') { + eidNetworkNameMapping[netConfig.eid.toString()] = networkName + } else { + const configValue = netConfig.url + if (configValue !== undefined) { + eidNetworkNameMapping[netConfig.eid.toString()] = configValue + } + } + } + return eidNetworkNameMapping +} + +export async function getConfigConnectionsFromChainType( + key: keyof OmniEdgeHardhat, + chainType: ChainType, + configPath: string +): Promise { + const configFile = await import(configPath) + const config = configFile.default + if (!config.connections) { + throw new Error('No connections found in config') + } + + const conns = config.connections + const connections: OAppOmniGraphHardhat['connections'] = [] + + for (const conn of conns) { + if (key == 'to' && endpointIdToChainType(conn.to.eid) == chainType) { + connections.push(conn) + } else if (key == 'from' && endpointIdToChainType(conn.from.eid) == chainType) { + connections.push(conn) + } + } + + return connections +} + +export async function getConfigConnections( + _key: keyof OmniEdgeHardhat, + _eid: number, + _configPath: string +): Promise { + const configFile = await import(_configPath) + const config = configFile.default + if (!config.connections) { + throw new Error('No connections found in config') + } + + const conns = config.connections + const connections: OAppOmniGraphHardhat['connections'] = [] + + for (const conn of conns) { + if (_key == 'to' && conn.to.eid == _eid) { + connections.push(conn) + } else if (_key == 'from' && conn.from.eid == _eid) { + connections.push(conn) + } + } + + return connections +} + +export function getConfigConnectionsFromConfigConnections( + conns: OAppOmniGraphHardhat['connections'], + _key: keyof OmniEdgeHardhat, + _eid: number +): OAppOmniGraphHardhat['connections'] { + const connections: OAppOmniGraphHardhat['connections'] = [] + + for (const conn of conns) { + if (conn[_key] == _eid) { + connections.push(conn) + } + } + + return connections +} + +export async function getHHAccountConfig(_configPath: string): Promise> { + const configPath = path.resolve(_configPath) + const configFile = await import(configPath) + const config = configFile.default + if (!config.contracts) { + throw new Error('No contracts found in config') + } + + const conns = config.contracts + + const configs: Record = {} + + for (const conn of conns) { + if (conn.config) { + configs[conn.contract.eid] = conn.config + } else { + configs[conn.contract.eid] = {} + } + } + + return configs +} + +// @todo instead of constant width, change it to dynamic width based on terminal width. +export function diffPrinter(logObject: string, from: object, to: object) { + const keyWidth = 30 // Fixed width for Key column + const valueWidth = 72 // Fixed width for From and To columns + const tableWidth = keyWidth + (valueWidth + 6) * 2 // Total table width (including separators) + + // Flatten nested objects/arrays into dot notation + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const flatten = (obj: any, prefix = ''): Record => { + return Object.keys(obj).reduce((acc: Record, key: string) => { + const value = obj[key] + const newKey = prefix ? `${prefix}.${key}` : key + + if (Array.isArray(value)) { + // Special case for empty arrays + if (value.length === 0) { + acc[newKey] = '[]' + } else { + // Handle arrays by creating indexed keys + value.forEach((item, index) => { + if (typeof item === 'object' && item !== null) { + Object.assign(acc, flatten(item, `${newKey}[${index}]`)) + } else { + acc[`${newKey}[${index}]`] = String(item) + } + }) + } + } else if (typeof value === 'object' && value !== null) { + // Recursively flatten nested objects + Object.assign(acc, flatten(value, newKey)) + } else { + // Base case: primitive values + acc[newKey] = String(value ?? '') + } + return acc + }, {}) + } + + const flatFrom = flatten(from) + const flatTo = flatten(to) + + // Remove empty array entries when the other side has array elements + Object.keys({ ...flatFrom, ...flatTo }).forEach((key) => { + if ( + (flatFrom[key] === '[]' && Object.keys(flatTo).some((k) => k.startsWith(`${key}[`))) || + (flatTo[key] === '[]' && Object.keys(flatFrom).some((k) => k.startsWith(`${key}[`))) + ) { + delete flatFrom[key] + delete flatTo[key] + } + }) + + const pad = (str: string, width: number) => str.padEnd(width, ' ') + + const wrapText = (text: string, width: number) => { + const lines = [] + let remaining = text + while (remaining.length > width) { + lines.push(remaining.slice(0, width)) + remaining = remaining.slice(width) + } + lines.push(remaining) // Add remaining text + return lines + } + + const allKeys = Array.from(new Set([...Object.keys(flatFrom), ...Object.keys(flatTo)])) + + const orangeVertical = '\x1b[33m' + `|` + '\x1b[0m' + + const header = `${orangeVertical} ${pad('Key', keyWidth)} | ${pad('Current', valueWidth)} | ${pad('New', valueWidth)} ${orangeVertical}` + const separator = + `${orangeVertical}-${'-'.repeat(keyWidth)}-|-` + + `${'-'.repeat(valueWidth)}-|-` + + `${'-'.repeat(valueWidth)}-${orangeVertical}` + + const rows = allKeys.flatMap((key) => { + const fromValue = flatFrom[key] !== undefined ? String(flatFrom[key]) : '' + const toValue = flatTo[key] !== undefined ? String(flatTo[key]) : '' + + const fromLines = wrapText(fromValue, valueWidth) + const toLines = wrapText(toValue, valueWidth) + const lineCount = Math.max(fromLines.length, toLines.length) + + // Create rows for each line of wrapped text + const rowLines = [] + for (let i = 0; i < lineCount; i++) { + const keyText = i === 0 ? pad(key, keyWidth) : pad('', keyWidth) // Only display key on the first row + const fromText = pad(fromLines[i] || '', valueWidth) + const toText = pad(toLines[i] || '', valueWidth) + rowLines.push(`${orangeVertical} ${keyText} | ${fromText} | ${toText} ${orangeVertical}`) + } + return rowLines + }) + + // Print the table + const orangeLine = '\x1b[33m' + ` ${'-'.repeat(tableWidth - 2)} ` + '\x1b[0m' + console.log(logObject) + console.log(orangeLine) + console.log(` ${header} `) + console.log(` ${separator} `) + rows.forEach((row) => console.log(` ${row} `)) + console.log(orangeLine, '\n') +} + +export async function promptForConfirmation(txCount: number): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + + const answer = await new Promise((resolve) => { + rl.question( + `\nReview the ${txCount} transaction(s) above carefully.\nWould you like to proceed with execution? (yes/no): `, + resolve + ) + }) + + rl.close() + return ['yes', 'y'].includes(answer.toLowerCase().trim()) +} diff --git a/packages/devtools-move/tsconfig.json b/packages/devtools-move/tsconfig.json new file mode 100644 index 000000000..027ad0f3f --- /dev/null +++ b/packages/devtools-move/tsconfig.json @@ -0,0 +1,13 @@ +{ + "exclude": ["node_modules"], + "include": ["deploy", "tasks", "test", "hardhat.config.ts"], + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +} diff --git a/packages/devtools-move/tsup.config.ts b/packages/devtools-move/tsup.config.ts new file mode 100644 index 000000000..8f03c2dc4 --- /dev/null +++ b/packages/devtools-move/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['types/index.d.ts'], + outDir: './dist', + clean: true, + dts: true, + sourcemap: true, + splitting: false, + treeshake: true, + format: ['esm', 'cjs'], +}) diff --git a/packages/devtools-move/turbo.json b/packages/devtools-move/turbo.json new file mode 100644 index 000000000..adaaffa58 --- /dev/null +++ b/packages/devtools-move/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "pipeline": { + "build": { + "outputs": ["build*/**"] + } + } +} diff --git a/packages/devtools-move/types/index.d.ts b/packages/devtools-move/types/index.d.ts new file mode 100644 index 000000000..e462fd3ab --- /dev/null +++ b/packages/devtools-move/types/index.d.ts @@ -0,0 +1,8 @@ +import { build } from '../tasks/move/build' +import { deploy } from '../tasks/move/deploy' +import { setDelegate } from '../tasks/move/setDelegate' + +import { wireMove } from '../tasks/move/wireMove' +import { wireEvm } from '../tasks/evm/wire-evm' + +export { build, deploy, setDelegate, wireMove, wireEvm } diff --git a/packages/oft-move/.eslintignore b/packages/oft-move/.eslintignore new file mode 100644 index 000000000..ee9f768fd --- /dev/null +++ b/packages/oft-move/.eslintignore @@ -0,0 +1,10 @@ +artifacts +cache +dist +node_modules +out +*.log +*.sol +*.yaml +*.lock +package-lock.json \ No newline at end of file diff --git a/packages/oft-move/.gitignore b/packages/oft-move/.gitignore new file mode 100644 index 000000000..30c5e26fd --- /dev/null +++ b/packages/oft-move/.gitignore @@ -0,0 +1,23 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types +build/ + +# Hardhat files +cache +artifacts + +# LayerZero specific files +.layerzero + +# pnpm +pnpm-error.log + +# Editor and OS files +.DS_Store +.idea + +.aptos \ No newline at end of file diff --git a/packages/oft-move/.nvmrc b/packages/oft-move/.nvmrc new file mode 100644 index 000000000..b714151ef --- /dev/null +++ b/packages/oft-move/.nvmrc @@ -0,0 +1 @@ +v18.18.0 \ No newline at end of file diff --git a/packages/oft-move/.prettierignore b/packages/oft-move/.prettierignore new file mode 100644 index 000000000..6e8232f5a --- /dev/null +++ b/packages/oft-move/.prettierignore @@ -0,0 +1,10 @@ +artifacts/ +cache/ +dist/ +node_modules/ +out/ +*.log +*ignore +*.yaml +*.lock +package-lock.json \ No newline at end of file diff --git a/packages/oft-move/.prettierrc.js b/packages/oft-move/.prettierrc.js new file mode 100644 index 000000000..6f55b4019 --- /dev/null +++ b/packages/oft-move/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('@layerzerolabs/prettier-config-next'), +}; diff --git a/packages/oft-move/Move.toml b/packages/oft-move/Move.toml new file mode 100644 index 000000000..6855698b2 --- /dev/null +++ b/packages/oft-move/Move.toml @@ -0,0 +1,75 @@ +[package] +name = "oft-movement" +version = "0.0.1" +authors = [] + +[addresses] +oft = "_" +oft_admin = "_" +oft_common = "_" +router_node_0 = "_" +simple_msglib = "_" +blocked_msglib = "_" +uln_302 = "_" +router_node_1 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +msglib_types = "_" +treasury = "_" +worker_peripherals = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +worker_common = "_" +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +executor_fee_lib_0 = "_" +dvn_fee_lib_0 = "_" +dvn = "_" +native_token_metadata_address = "0xa" +# For Initia: "0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9" + +[dev-addresses] +oft = "0x302814823" +oft_admin = "0x12321241" +oft_common = "0x30281482332" +router_node_0 = "0x10000f" +simple_msglib = "0x100011" +blocked_msglib = "0x100001" +uln_302 = "0x100013" +router_node_1 = "0x100010" +endpoint_v2_common = "0x100007" +endpoint_v2 = "0x100006" +layerzero_admin = "0x200001" +layerzero_treasury_admin = "0x200002" +msglib_types = "0x10000b" +treasury = "0x100012" +worker_peripherals = "0x3000" +price_feed_router_0 = "0x10000d" +price_feed_router_1 = "0x10000e" +price_feed_module_0 = "0x10000c" +worker_common = "0x100014" +executor_fee_lib_router_0 = "0x100009" +executor_fee_lib_router_1 = "0x10000a" +dvn_fee_lib_router_0 = "0x100004" +dvn_fee_lib_router_1 = "0x100005" +executor_fee_lib_0 = "0x100008" +dvn_fee_lib_0 = "0x100003" +dvn = "0x100002" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-core.git" +rev = "mainnet" +subdir = "aptos-move/framework/aptos-framework" + +[dependencies] +endpoint_v2_common = { git = "https://github.com/LayerZero-Labs/LayerZero-v2", rev = "aptos-v2", subdir = "packages/layerzero-v2/aptos/contracts/endpoint_v2_common" } +endpoint_v2 = { git = "https://github.com/LayerZero-Labs/LayerZero-v2", rev = "aptos-v2", subdir = "packages/layerzero-v2/aptos/contracts/endpoint_v2" } +oft_common = { git = "https://github.com/LayerZero-Labs/LayerZero-v2", rev = "aptos-v2", subdir = "packages/layerzero-v2/aptos/contracts/oapps/oft_common" } + +[dev-dependencies] +simple_msglib = { git = "https://github.com/LayerZero-Labs/LayerZero-v2", rev = "aptos-v2", subdir = "packages/layerzero-v2/aptos/contracts/msglib/libs/simple_msglib" } \ No newline at end of file diff --git a/packages/oft-move/README.md b/packages/oft-move/README.md new file mode 100644 index 000000000..fba025392 --- /dev/null +++ b/packages/oft-move/README.md @@ -0,0 +1,346 @@ +# Move CLI Reviewer Guide + +I'm putting together a guide of the CLI where I trace through running one of the commands and explain what files are doing to illustrate the general high-level architecture of the CLI. Hopefully this makes code review slightly more approachable. + +`examples/oft-aptos-move` is the example for deploying an OFT on Aptos (and later Movement). + +Inside of `examples/oft-aptos-move/`, go into package.json. Here we have a list of the commands that we can run. + +You can also run `pnpm run lz:sdk:help` inside of `examples/oft-aptos-move/` to see the list of commands. + +For this high-level overview, we will be drilling down into the wire command. + +The wire command code can be found in the `packages/devtools-move/tasks/move/wire.ts` file. + +The wire command is defined in `packages/devtools-move/cli/operations/move-wire.ts` +`cli/operations/`. This is where we define all of the CLI operations that we want a user to be able to run. It specifiies the command, the arguments, and the description. The implementation is stored elsewhere. + +Here is the code for the wire operation. As you can see it just defines what args are required, and passes those in to wireMove, where the actual logic is defined. +```ts +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' +import { wireMove } from '../../tasks/move/wire' + +class MoveWireOperation implements INewOperation { + vm = 'move' // This is the VM we are working with + operation = 'wire' // This is the new name of the operation + description = 'Wire Aptos Move contracts' // This is the description of the operation that is displayed when help is called + reqArgs = ['oapp_config'] // This is the list of args that are required for the operation for wire its just the oapp_config path + + async impl(args: any): Promise { + await wireMove(args) // call the wireMove function with the args + } +} + +const NewOperation = new MoveWireOperation() +export { NewOperation } +``` + +The wire operation is registered in: `packages/devtools-move/cli/init.ts` +`init.ts` is the location that the CLI will look to find what operations are available. +```ts +import { AptosEVMCLI } from '@layerzerolabs/devtools-extensible-cli/cli/AptosEVMCli' +import path from 'path' + +export async function attach_wire_move(sdk: AptosEVMCLI) { + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-build')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-deploy')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-wire')) // Here is the wire operation + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-set-delegate')) +} + +export async function attach_wire_evm(sdk: AptosEVMCLI) { + await sdk.extendOperationFromPath(path.join(__dirname, './operations/evm-wire')) +} +``` + +The above two files are all that is required to register an operation. + +Now we can move on to the wire command internals where we interface with the OFT SDK. + +The wire command is defined in `packages/devtools-move/tasks/move/wireMove.ts`. `tasks/move/` is where the scripts that interact with move VM are kept. + +```ts +async function wireMove(args: any) { + // Here is where we parse the user info .yaml file inside of examples/oft-aptos-move/.aptos/config.yaml + // This .yaml is created when the user runs aptos init and enters their private key + const { account_address, private_key, network, fullnode, faucet } = await parseYaml(args.rootDir) + const fullConfigPath = path.join(args.rootDir, args.oapp_config) + const chain = getChain(fullnode) + + const moveVMConnection = getConnection(chain, network, fullnode, faucet) + + const lzNetworkStage = getLzNetworkStage(network) + const moveVMOftAddress = getMoveVMOftAddress(lzNetworkStage) + const namedAddresses = getNamedAddresses(lzNetworkStage) + const endpointAddress = getEndpointAddressFromNamedAddresses(namedAddresses) + + console.log(`\n🔌 Wiring ${chain}-${lzNetworkStage} OFT`) + console.log(`\tAddress: ${moveVMOftAddress}\n`) + + const oftSDK = new OFT(moveVMConnection, moveVMOftAddress, account_address, private_key) + const moveVMEndpoint = new Endpoint(moveVMConnection, endpointAddress) + + const currDelegate = await oftSDK.getDelegate() + validateDelegate(currDelegate, account_address) + + const moveVMEndpointID = getEidFromMoveNetwork(chain, network) + // Here is where we parse the config file to get the connections + const connectionsFromMoveToAny = await + getConfigConnections('from', moveVMEndpointID, fullConfigPath) + // Here is where we create and send the payloads + const txs = await createWiringTxs(oftSDK, moveVMEndpoint, connectionsFromMoveToAny) + await sendAllTxs(moveVMConnection, oftSDK, account_address, txs) +} +``` + +There two main things we need to look at in the wire command: +1. creating the wiring transactions +2. sending the transactions + +The payloads are created by methods in `packages/devtools-move/tasks/move/utils/moveVMOftConfigOps.ts` + +This is the crux of the move VM side of this entire project. If there is any file to review, this would be the one. + +Let's drill down on the function (within wireMove.ts) `createSetReceiveLibraryTxs`. The other functions all follow a similar pattern. +The pattern consists of three steps: +1. Check for no setting - verify if the configuration value exists in the config file +2. Get the current setting - retrieve the current value from the blockchain +3. Compare and act: + - If the current value matches the config, print "already set" message + - If different, print the diff and create a transaction to update the value +```ts +export async function createSetReceiveLibraryTimeoutTx( + oft: OFT, + endpoint: Endpoint, + connection: OAppOmniGraphHardhat['connections'][number] +): Promise { + // 1. Check for no config setting + if (!connection.config?.receiveLibraryTimeoutConfig) { + printNotSet('Receive library timeout', connection) + return null + } + // 2. Get the current setting + const currentTimeout = await endpoint.getReceiveLibraryTimeout(oft.oft_address, connection.to.eid) + const currentTimeoutAsBigInt = BigInt(currentTimeout.expiry) + + // 3. Compare and act + if (currentTimeoutAsBigInt === BigInt(connection.config.receiveLibraryTimeoutConfig.expiry)) { + printAlreadySet('Receive library timeout', connection) + return null + } else { + let currTimeoutDisplay = '' + currentTimeoutAsBigInt + if (currentTimeoutAsBigInt === BigInt(-1)) { + currTimeoutDisplay = 'unset' + } + // Printing the diff + const diffMessage = createDiffMessage(`receive library timeout`, connection) + diffPrinter( + diffMessage, + { timeout: currTimeoutDisplay }, + { timeout: connection.config.receiveLibraryTimeoutConfig.expiry } + ) + // Create the transaction + const tx = oft.setReceiveLibraryTimeoutPayload( + connection.to.eid, + connection.config.receiveLibraryTimeoutConfig.lib, + Number(connection.config.receiveLibraryTimeoutConfig.expiry) + ) + // Return the transaction with the description and eid + return { + payload: tx, + description: buildTransactionDescription('Set Receive Library Timeout', connection), + eid: connection.to.eid, + } + } +} +``` + +The methods of `moveVMOftConfigOps.ts` call our MoveVM SDKs enpoint.ts, msgLib.ts, and oft.ts. These are all very simple and straightforward. The getters call blockchain view functions, but the setters return payloads to later be executed. + +For example, the `setReceiveLibraryTimeoutPayload` method returns a payload to be executed on the MoveVM. It calls the `set_receive_library_timeout` function on the `oapp_core` module, at the address of the OFT, that is retrieved from `examples/oft-aptos-move/deployments/aptos-testnet/oft.json`. +```ts +setReceiveLibraryTimeoutPayload( + remoteEid: number, + msglibAddress: string, + expiry: number +): InputGenerateTransactionPayloadData { + return { + function: `${this.oft_address}::oapp_core::set_receive_library_timeout`, + functionArguments: [remoteEid, msglibAddress, expiry], + } +} +``` + +After doing a similar process for all of the other config values, we now have a list of transactions in `wire.ts` and are ready to execute them. +```ts +const txs = await createWiringTxs(oft, moveVMEndpoint, connectionsFromMoveToAny) +await sendAllTxs(moveVMConnection, oft, account_address, txs) +``` +Then in sendAllTxs, we loop through the transactions, and one by one submit and build them: +```ts +const trans = await aptos.transaction.build.simple({ + sender: account_address, + data: cleanedPayloads[i].payload, +}) +await oft.signSubmitAndWaitForTx(trans) +``` + +The output of wire looks like this: + +```ts +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🔄 Building wire transactions for pathway: aptos-testnet → bsc-testnet 🔄 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Set peer for pathway oft on aptos-testnet → bsc-testnet + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + | Key | Current | New | + |--------------------------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------| + | address | | 0x000000000000000000000000ab88bad042336dcc4550182c12be17f0cc8bb7c5 | + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +Set enforced options with message type: 1 for pathway oft on aptos-testnet → bsc-testnet + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + | Key | Current | New | + |--------------------------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------| + | LzReceiveOption.gas | | 80000 | + | LzReceiveOption.value | | 0 | + | NativeDropOption | [] | [] | + | ComposeOption | [] | [] | + | LzReadOption.gas | | | + | LzReadOption.dataSize | | | + | LzReadOption.value | | | + | OrderedExecutionOption | false | false | + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +Set enforced options with message type: 2 for pathway oft on aptos-testnet → bsc-testnet + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + | Key | Current | New | + |--------------------------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------| + | LzReceiveOption.gas | | 80000 | + | LzReceiveOption.value | | 0 | + | NativeDropOption | [] | [] | + | ComposeOption | [] | [] | + | LzReadOption.gas | | | + | LzReadOption.dataSize | | | + | LzReadOption.value | | | + | OrderedExecutionOption | false | false | + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +Set send library for pathway oft on aptos-testnet → bsc-testnet + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + | Key | Current | New | + |--------------------------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------| + | address | default: 0xbe533727aebe97132ec0a606d99e0ce137dbdf06286eb07d9e0f7154df1f3 | 0xbe533727aebe97132ec0a606d99e0ce137dbdf06286eb07d9e0f7154df1f3f10 | + | | f10 | | + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +Set receive library for pathway oft on aptos-testnet → bsc-testnet + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + | Key | Current | New | + |--------------------------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------| + | address | default: 0xbe533727aebe97132ec0a606d99e0ce137dbdf06286eb07d9e0f7154df1f3 | 0xbe533727aebe97132ec0a606d99e0ce137dbdf06286eb07d9e0f7154df1f3f10 | + | | f10 | | + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +Set receive library timeout for pathway oft on aptos-testnet → bsc-testnet + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + | Key | Current | New | + |--------------------------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------| + | timeout | unset | 1000000000 | + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +Set send config for pathway oft on aptos-testnet → bsc-testnet + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + | Key | Current | New | + |--------------------------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------| + | confirmations | 260 | 260 | + | optional_dvn_threshold | 0 | 0 | + | required_dvns[0] | 0xd6f420483a90c7db5ce2ec12e8acfc2bfb7b93829c9e6a3b0760bca330be64dd | 0xd6f420483a90c7db5ce2ec12e8acfc2bfb7b93829c9e6a3b0760bca330be64dd | + | optional_dvns | [] | [] | + | use_default_for_confirmations | false | false | + | use_default_for_required_dvns | false | false | + | use_default_for_optional_dvns | false | true | + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +Set executor config for pathway oft on aptos-testnet → bsc-testnet + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + | Key | Current | New | + |--------------------------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------| + | max_message_size | 10000 | 10000 | + | executor_address | Default:0xeb514e8d337485dd9ce7492f70128ef5aaa8c34023866e261a24ffa3d61a68 | 0xeb514e8d337485dd9ce7492f70128ef5aaa8c34023866e261a24ffa3d61a686d | + | | 6d | | + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +Set receive config for pathway oft on aptos-testnet → bsc-testnet + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + | Key | Current | New | + |--------------------------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------| + | confirmations | 5 | 5 | + | optional_dvn_threshold | 0 | 0 | + | required_dvns[0] | 0xd6f420483a90c7db5ce2ec12e8acfc2bfb7b93829c9e6a3b0760bca330be64dd | 0xd6f420483a90c7db5ce2ec12e8acfc2bfb7b93829c9e6a3b0760bca330be64dd | + | optional_dvns | [] | [] | + | use_default_for_confirmations | false | false | + | use_default_for_required_dvns | false | false | + | use_default_for_optional_dvns | false | true | + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + +Review the 12 transaction(s) above carefully. +Would you like to proceed with execution? (yes/no): y +y + +📦 Transaction Summary: + • Total transactions: 12 +🔄 [1/12] Processing transaction 0: Set Peer from aptos-testnet → ethereum-testnet... +Transaction executed. +✅ [1/12] Transaction 0 completed + +🔄 [2/12] Processing transaction 1: Set Enforced Options from aptos-testnet → ethereum-testnet... +Transaction executed. +✅ [2/12] Transaction 1 completed + +🔄 [3/12] Processing transaction 2: Set Enforced Options from aptos-testnet → ethereum-testnet... +Transaction executed. +✅ [3/12] Transaction 2 completed + +🔄 [4/12] Processing transaction 3: Set Peer from aptos-testnet → bsc-testnet... +Transaction executed. +✅ [4/12] Transaction 3 completed + +🔄 [5/12] Processing transaction 4: Set Enforced Options from aptos-testnet → bsc-testnet... +Transaction executed. +✅ [5/12] Transaction 4 completed + +🔄 [6/12] Processing transaction 5: Set Enforced Options from aptos-testnet → bsc-testnet... +Transaction executed. +✅ [6/12] Transaction 5 completed + +🔄 [7/12] Processing transaction 6: Set Send Library from aptos-testnet → bsc-testnet... +Transaction executed. +✅ [7/12] Transaction 6 completed + +🔄 [8/12] Processing transaction 7: Set Receive Library from aptos-testnet → bsc-testnet... +Transaction executed. +✅ [8/12] Transaction 7 completed + +🔄 [9/12] Processing transaction 8: Set Receive Library Timeout from aptos-testnet → bsc-testnet... +Transaction executed. +✅ [9/12] Transaction 8 completed + +🔄 [10/12] Processing transaction 9: Set Send Config from aptos-testnet → bsc-testnet... +Transaction executed. +✅ [10/12] Transaction 9 completed + +🔄 [11/12] Processing transaction 10: setExecutorConfig from aptos-testnet → bsc-testnet... +Transaction executed. +✅ [11/12] Transaction 10 completed + +🔄 [12/12] Processing transaction 11: Set Receive Config from aptos-testnet → bsc-testnet... +Transaction executed. +✅ [12/12] Transaction 11 completed + +🎉 Transaction Summary: + • 12 transactions processed successfully +🔌 Running second Move-VM wire... +``` \ No newline at end of file diff --git a/packages/oft-move/cli/init.ts b/packages/oft-move/cli/init.ts new file mode 100644 index 000000000..418b2164c --- /dev/null +++ b/packages/oft-move/cli/init.ts @@ -0,0 +1,19 @@ +import { AptosEVMCLI } from '@layerzerolabs/devtools-extensible-cli/cli/AptosEVMCli' +import path from 'path' + +export async function attach_oft_move(sdk: AptosEVMCLI) { + await sdk.extendOperationFromPath(path.join(__dirname, './operations/init-move-oft-fa')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/quote-send-move-oft')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/init-move-oft-fa-adapter')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-oft-set-fee')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-oft-set-rate-limit')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-oft-unset-rate-limit')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-oft-disable-blocklist')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-oft-disable-freezing')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/send-from-move-oft')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/mint-to-move-oft')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-oft-adapter-disable-blocklist')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-oft-adapter-set-fee')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-oft-adapter-set-rate-limit')) + await sdk.extendOperationFromPath(path.join(__dirname, './operations/move-oft-adapter-unset-rate-limit')) +} diff --git a/packages/oft-move/cli/operations/init-move-oft-fa-adapter.ts b/packages/oft-move/cli/operations/init-move-oft-fa-adapter.ts new file mode 100644 index 000000000..c4d359679 --- /dev/null +++ b/packages/oft-move/cli/operations/init-move-oft-fa-adapter.ts @@ -0,0 +1,26 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' +import path from 'path' + +import { initOFTAdapterFA } from '../../tasks/initOFTAdapterFA' + +class InitOFTFAAdapter implements INewOperation { + vm = 'move' + operation = 'init-fa-adapter' + description = 'Initialize an OFT Adapter with FA' + reqArgs = ['move_deploy_script'] + + async impl(args: any): Promise { + const fullPathOFTConfig = path.resolve(path.join(args.rootDir, args.move_deploy_script)) + const oftConfig = await import(fullPathOFTConfig) + + const oftMetadata = oftConfig.oftMetadata + if (!oftMetadata) { + throw new Error(`${fullPathOFTConfig} does not contain an oftMetadata object`) + } + + await initOFTAdapterFA(oftMetadata.move_vm_fa_address, oftMetadata.shared_decimals) + } +} + +const NewOperation = new InitOFTFAAdapter() +export { NewOperation } diff --git a/packages/oft-move/cli/operations/init-move-oft-fa.ts b/packages/oft-move/cli/operations/init-move-oft-fa.ts new file mode 100644 index 000000000..5bc085336 --- /dev/null +++ b/packages/oft-move/cli/operations/init-move-oft-fa.ts @@ -0,0 +1,33 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' +import path from 'path' + +import { initOFTFA } from '../../tasks/initOFTFA' + +class InitOFTFA implements INewOperation { + vm = 'move' + operation = 'init-fa' + description = 'Initialize an OFT with FA' + reqArgs = ['move_deploy_script'] + + async impl(args: any): Promise { + const fullPathOFTConfig = path.resolve(path.join(args.rootDir, args.move_deploy_script)) + const oftConfig = await import(fullPathOFTConfig) + + const oftMetadata = oftConfig.oftMetadata + if (!oftMetadata) { + throw new Error(`${fullPathOFTConfig} does not contain an oftMetadata object`) + } + + await initOFTFA( + oftMetadata.token_name, + oftMetadata.token_symbol, + oftMetadata.icon_uri, + oftMetadata.project_uri, + oftMetadata.sharedDecimals, + oftMetadata.localDecimals + ) + } +} + +const NewOperation = new InitOFTFA() +export { NewOperation } diff --git a/packages/oft-move/cli/operations/mint-to-move-oft.ts b/packages/oft-move/cli/operations/mint-to-move-oft.ts new file mode 100644 index 000000000..6a2e1bcc4 --- /dev/null +++ b/packages/oft-move/cli/operations/mint-to-move-oft.ts @@ -0,0 +1,17 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' + +import mintToMoveVM from '../../tasks/mintToMoveVM' + +class MintToMoveOFT implements INewOperation { + vm = 'move' + operation = 'mint-to-move-oft' + description = 'Mint tokens to a Move OFT' + reqArgs = ['amount_ld', 'to_address'] + + async impl(args: any): Promise { + await mintToMoveVM(args.amount_ld, args.to_address) + } +} + +const NewOperation = new MintToMoveOFT() +export { NewOperation } diff --git a/packages/oft-move/cli/operations/move-oft-adapter-disable-blocklist.ts b/packages/oft-move/cli/operations/move-oft-adapter-disable-blocklist.ts new file mode 100644 index 000000000..5a6bf6134 --- /dev/null +++ b/packages/oft-move/cli/operations/move-oft-adapter-disable-blocklist.ts @@ -0,0 +1,18 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' + +import { irrevocablyDisableBlocklist } from '../../tasks/irrevocablyDisableBlocklist' +import { OFTType } from '@layerzerolabs/devtools-move/sdk/oft' + +class AdapterIrrevocablyDisableBlocklist implements INewOperation { + vm = 'move' + operation = 'adapter-permanently-disable-blocklist' + description = 'Permanently disable the ability to blocklist wallets for your OFT Adapter' + reqArgs = [] + + async impl(): Promise { + await irrevocablyDisableBlocklist(OFTType.OFT_ADAPTER_FA) + } +} + +const NewOperation = new AdapterIrrevocablyDisableBlocklist() +export { NewOperation } diff --git a/packages/oft-move/cli/operations/move-oft-adapter-set-fee.ts b/packages/oft-move/cli/operations/move-oft-adapter-set-fee.ts new file mode 100644 index 000000000..b3117e76d --- /dev/null +++ b/packages/oft-move/cli/operations/move-oft-adapter-set-fee.ts @@ -0,0 +1,35 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' + +import { setFee } from '../../tasks/setFee' +import { OFTType } from '@layerzerolabs/devtools-move/sdk/oft' + +class AdapterSetFee implements INewOperation { + vm = 'move' + operation = 'adapter-set-fee' + description = 'Set the fee BPS for an OFT Adapter' + reqArgs = ['fee_bps', 'to_eid'] + + addArgs = [ + { + name: '--to-eid', + arg: { + help: 'destination endpoint id', + required: false, + }, + }, + { + name: '--fee-bps', + arg: { + help: 'fee BPS', + required: false, + }, + }, + ] + + async impl(args: any): Promise { + await setFee(BigInt(args.fee_bps), args.to_eid, OFTType.OFT_ADAPTER_FA) + } +} + +const NewOperation = new AdapterSetFee() +export { NewOperation } diff --git a/packages/oft-move/cli/operations/move-oft-adapter-set-rate-limit.ts b/packages/oft-move/cli/operations/move-oft-adapter-set-rate-limit.ts new file mode 100644 index 000000000..69dbe0e53 --- /dev/null +++ b/packages/oft-move/cli/operations/move-oft-adapter-set-rate-limit.ts @@ -0,0 +1,43 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' + +import { setRateLimit } from '../../tasks/setRateLimit' +import { OFTType } from '@layerzerolabs/devtools-move/sdk/oft' + +class AdapterSetRateLimit implements INewOperation { + vm = 'move' + operation = 'adapter-set-rate-limit' + description = ` +Set the rate limit configuration for a given endpoint ID +The rate limit is the maximum amount of OFT that can be sent to the endpoint within a given window +The rate limit capacity recovers linearly at a rate of limit / window_seconds +*Important*: Setting the rate limit does not reset the current "in-flight" volume (in-flight refers to the decayed rate limit consumption). +This means that if the rate limit is set lower than the current in-flight volume, +the endpoint will not be able to receive OFT until the in-flight volume decays below the new rate limit. +In order to reset the in-flight volume, the rate limit must be unset and then set again.` + + reqArgs = ['rate_limit', 'window_seconds', 'to_eid'] + + addArgs = [ + { + name: '--window-seconds', + arg: { + help: 'window seconds', + required: false, + }, + }, + { + name: '--rate-limit', + arg: { + help: 'rate limit in', + required: false, + }, + }, + ] + + async impl(args: any): Promise { + await setRateLimit(BigInt(args.rate_limit), BigInt(args.window_seconds), args.to_eid, OFTType.OFT_ADAPTER_FA) + } +} + +const NewOperation = new AdapterSetRateLimit() +export { NewOperation } diff --git a/packages/oft-move/cli/operations/move-oft-adapter-unset-rate-limit.ts b/packages/oft-move/cli/operations/move-oft-adapter-unset-rate-limit.ts new file mode 100644 index 000000000..00d1ece20 --- /dev/null +++ b/packages/oft-move/cli/operations/move-oft-adapter-unset-rate-limit.ts @@ -0,0 +1,18 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' + +import { unsetRateLimit } from '../../tasks/unSetRateLimit' +import { OFTType } from '@layerzerolabs/devtools-move/sdk/oft' + +class AdapterUnsetRateLimit implements INewOperation { + vm = 'move' + operation = 'adapter-unset-rate-limit' + description = 'Unset the rate limit configuration for a given endpoint ID' + reqArgs = ['to_eid'] + + async impl(args: any): Promise { + await unsetRateLimit(args.to_eid, OFTType.OFT_ADAPTER_FA) + } +} + +const NewOperation = new AdapterUnsetRateLimit() +export { NewOperation } diff --git a/packages/oft-move/cli/operations/move-oft-disable-blocklist.ts b/packages/oft-move/cli/operations/move-oft-disable-blocklist.ts new file mode 100644 index 000000000..5dd99bfca --- /dev/null +++ b/packages/oft-move/cli/operations/move-oft-disable-blocklist.ts @@ -0,0 +1,18 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' + +import { irrevocablyDisableBlocklist } from '../../tasks/irrevocablyDisableBlocklist' +import { OFTType } from '@layerzerolabs/devtools-move/sdk/oft' + +class IrrevocablyDisableBlocklist implements INewOperation { + vm = 'move' + operation = 'permanently-disable-blocklist' + description = 'Permanently disable the ability to blocklist wallets for your OFT' + reqArgs = [] + + async impl(): Promise { + await irrevocablyDisableBlocklist(OFTType.OFT_FA) + } +} + +const NewOperation = new IrrevocablyDisableBlocklist() +export { NewOperation } diff --git a/packages/oft-move/cli/operations/move-oft-disable-freezing.ts b/packages/oft-move/cli/operations/move-oft-disable-freezing.ts new file mode 100644 index 000000000..ba9e1cec6 --- /dev/null +++ b/packages/oft-move/cli/operations/move-oft-disable-freezing.ts @@ -0,0 +1,21 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' + +import { permanentlyDisableFreezing } from '../../tasks/permanentlyDisableFreezing' + +class IrrevocablyDisableFreezing implements INewOperation { + vm = 'move' + operation = 'permanently-disable-freezing' + description = ` +Permanently disable the ability to freeze a primary fungible store through the OFT +This will permanently prevent freezing of new accounts. It will not prevent unfreezing accounts, and existing +frozen accounts will remain frozen until unfrozen` + + reqArgs = [] + + async impl(): Promise { + await permanentlyDisableFreezing() + } +} + +const NewOperation = new IrrevocablyDisableFreezing() +export { NewOperation } diff --git a/packages/oft-move/cli/operations/move-oft-set-fee.ts b/packages/oft-move/cli/operations/move-oft-set-fee.ts new file mode 100644 index 000000000..bde7e5e5b --- /dev/null +++ b/packages/oft-move/cli/operations/move-oft-set-fee.ts @@ -0,0 +1,35 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' + +import { setFee } from '../../tasks/setFee' +import { OFTType } from '@layerzerolabs/devtools-move/sdk/oft' + +class SetFee implements INewOperation { + vm = 'move' + operation = 'set-fee' + description = 'Set the fee BPS for an OFT' + reqArgs = ['fee_bps', 'to_eid'] + + addArgs = [ + { + name: '--to-eid', + arg: { + help: 'destination endpoint id', + required: false, + }, + }, + { + name: '--fee-bps', + arg: { + help: 'fee BPS', + required: false, + }, + }, + ] + + async impl(args: any): Promise { + await setFee(BigInt(args.fee_bps), args.to_eid, OFTType.OFT_FA) + } +} + +const NewOperation = new SetFee() +export { NewOperation } diff --git a/packages/oft-move/cli/operations/move-oft-set-rate-limit.ts b/packages/oft-move/cli/operations/move-oft-set-rate-limit.ts new file mode 100644 index 000000000..285c89d74 --- /dev/null +++ b/packages/oft-move/cli/operations/move-oft-set-rate-limit.ts @@ -0,0 +1,43 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' + +import { setRateLimit } from '../../tasks/setRateLimit' +import { OFTType } from '@layerzerolabs/devtools-move/sdk/oft' + +class SetFee implements INewOperation { + vm = 'move' + operation = 'set-rate-limit' + description = ` +Set the rate limit configuration for a given endpoint ID +The rate limit is the maximum amount of OFT that can be sent to the endpoint within a given window +The rate limit capacity recovers linearly at a rate of limit / window_seconds +*Important*: Setting the rate limit does not reset the current "in-flight" volume (in-flight refers to the decayed rate limit consumption). +This means that if the rate limit is set lower than the current in-flight volume, +the endpoint will not be able to receive OFT until the in-flight volume decays below the new rate limit. +In order to reset the in-flight volume, the rate limit must be unset and then set again.` + + reqArgs = ['rate_limit', 'window_seconds', 'to_eid'] + + addArgs = [ + { + name: '--window-seconds', + arg: { + help: 'window seconds', + required: false, + }, + }, + { + name: '--rate-limit', + arg: { + help: 'rate limit in', + required: false, + }, + }, + ] + + async impl(args: any): Promise { + await setRateLimit(BigInt(args.rate_limit), BigInt(args.window_seconds), args.to_eid, OFTType.OFT_FA) + } +} + +const NewOperation = new SetFee() +export { NewOperation } diff --git a/packages/oft-move/cli/operations/move-oft-unset-rate-limit.ts b/packages/oft-move/cli/operations/move-oft-unset-rate-limit.ts new file mode 100644 index 000000000..dcc9ed5fc --- /dev/null +++ b/packages/oft-move/cli/operations/move-oft-unset-rate-limit.ts @@ -0,0 +1,18 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' + +import { unsetRateLimit } from '../../tasks/unSetRateLimit' +import { OFTType } from '@layerzerolabs/devtools-move/sdk/oft' + +class SetFee implements INewOperation { + vm = 'move' + operation = 'unset-rate-limit' + description = 'Unset the rate limit configuration for a given endpoint ID' + reqArgs = ['to_eid'] + + async impl(args: any): Promise { + await unsetRateLimit(args.to_eid, OFTType.OFT_FA) + } +} + +const NewOperation = new SetFee() +export { NewOperation } diff --git a/packages/oft-move/cli/operations/quote-send-move-oft.ts b/packages/oft-move/cli/operations/quote-send-move-oft.ts new file mode 100644 index 000000000..acb74b684 --- /dev/null +++ b/packages/oft-move/cli/operations/quote-send-move-oft.ts @@ -0,0 +1,17 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' + +import { quoteSendOFT } from '../../tasks/quoteSendOFT' + +class QuoteSendOFT implements INewOperation { + vm = 'move' + operation = 'quote-send-move-oft' + description = 'Call the Quote send method of a move OFT' + reqArgs = ['amount_ld', 'min_amount_ld', 'to_address', 'gas_limit', 'dst_eid'] + + async impl(args: any): Promise { + await quoteSendOFT(args.amount_ld, args.min_amount_ld, args.to_address, args.gas_limit, args.dst_eid) + } +} + +const NewOperation = new QuoteSendOFT() +export { NewOperation } diff --git a/packages/oft-move/cli/operations/send-from-move-oft.ts b/packages/oft-move/cli/operations/send-from-move-oft.ts new file mode 100644 index 000000000..25934dc7d --- /dev/null +++ b/packages/oft-move/cli/operations/send-from-move-oft.ts @@ -0,0 +1,70 @@ +import { INewOperation } from '@layerzerolabs/devtools-extensible-cli' + +import { sendFromMoveVm } from '../../tasks/sendFromMoveVm' + +class SendFromMoveOFT implements INewOperation { + vm = 'move' + operation = 'send-from-move-oft' + description = + 'Call the send_withdraw method of a move OFT, and send the tokens from the source address to the destination address.' + reqArgs = ['amount_ld', 'min_amount_ld', 'src_address', 'to_address', 'gas_limit', 'dst_eid'] + + addArgs = [ + { + name: '--src-address', + arg: { + help: 'source address', + required: false, + }, + }, + { + name: '--dst-eid', + arg: { + help: 'destination endpoint id', + required: false, + }, + }, + { + name: '--gas-limit', + arg: { + help: 'gas limit', + required: false, + }, + }, + { + name: '--min-amount-ld', + arg: { + help: 'minimum amount to receive', + required: false, + }, + }, + { + name: '--to-address', + arg: { + help: 'to address', + required: false, + }, + }, + { + name: '--amount-ld', + arg: { + help: 'amount to send', + required: false, + }, + }, + ] + + async impl(args: any): Promise { + await sendFromMoveVm( + BigInt(args.amount_ld), + BigInt(args.min_amount_ld), + args.to_address, + BigInt(args.gas_limit), + args.dst_eid, + args.src_address + ) + } +} + +const NewOperation = new SendFromMoveOFT() +export { NewOperation } diff --git a/packages/oft-move/package.json b/packages/oft-move/package.json new file mode 100644 index 000000000..b73584a2e --- /dev/null +++ b/packages/oft-move/package.json @@ -0,0 +1,33 @@ +{ + "name": "@layerzerolabs/oft-move", + "version": "0.0.1", + "private": true, + "license": "MIT", + "types": "types/index.d.ts", + "scripts": { + "prebuild": "tsc -noEmit", + "build": "$npm_execpath tsup --clean", + "clean": "rm -rf artifacts cache out build", + "dev": "$npm_execpath tsup --watch", + "lint": "$npm_execpath eslint '**/*.{js,ts,json}'", + "lint:fix": "eslint --fix '**/*.{js,ts,json}'" + }, + "devDependencies": { + "@aptos-labs/ts-sdk": "^1.33.1", + "@layerzerolabs/devtools-extensible-cli": "^0.0.1", + "@layerzerolabs/devtools-move": "^0.0.1", + "@layerzerolabs/lz-definitions": "^3.0.21", + "@layerzerolabs/lz-v2-utilities": "^3.0.12", + "@layerzerolabs/prettier-config-next": "^2.3.39", + "argparse": "^2.0.1", + "depcheck": "^1.4.7", + "eslint": "^8.55.0", + "hardhat": "^2.22.10", + "ts-node": "^10.9.2", + "tsup": "^8.0.1", + "typescript": "^5.4.4" + }, + "engines": { + "node": ">=18.16.0" + } +} diff --git a/packages/oft-move/tasks/initOFTAdapterFA.ts b/packages/oft-move/tasks/initOFTAdapterFA.ts new file mode 100644 index 000000000..a932b4fb2 --- /dev/null +++ b/packages/oft-move/tasks/initOFTAdapterFA.ts @@ -0,0 +1,36 @@ +import { getChain, getConnection } from '@layerzerolabs/devtools-move/sdk/moveVMConnectionBuilder' +import { OFT } from '@layerzerolabs/devtools-move/sdk/oft' + +import { + getEidFromMoveNetwork, + getLzNetworkStage, + parseYaml, +} from '@layerzerolabs/devtools-move/tasks/move/utils/aptosNetworkParser' +import { getMoveVMOftAddress, sendInitTransaction } from '@layerzerolabs/devtools-move/tasks/move/utils/utils' + +async function initOFTAdapterFA(move_vm_fa_address: string, shared_decimals: number) { + const { account_address, private_key, network, fullnode, faucet } = await parseYaml() + console.log(`Using aptos network ${network}`) + + const chain = getChain(fullnode) + const lzNetworkStage = getLzNetworkStage(network) + const oftAdapterAddress = getMoveVMOftAddress(chain, lzNetworkStage) + + console.log(`\n⚡ Initializing OFT Adapter`) + console.log(` Address: ${oftAdapterAddress}\n`) + + console.log(`Shared Decimals: ${shared_decimals}`) + console.log(`${chain} FA Address: ${move_vm_fa_address}`) + + const moveVMConnection = getConnection(chain, network, fullnode, faucet) + const oft = new OFT(moveVMConnection, oftAdapterAddress, account_address, private_key) + + const initializePayload = oft.initializeAdapterFAPayload(move_vm_fa_address, shared_decimals) + + const eid = getEidFromMoveNetwork(chain, network) + const payloads = [{ payload: initializePayload, description: `Initialize ${chain} OFT`, eid }] + + sendInitTransaction(moveVMConnection, oft, account_address, payloads) +} + +export { initOFTAdapterFA } diff --git a/packages/oft-move/tasks/initOFTFA.ts b/packages/oft-move/tasks/initOFTFA.ts new file mode 100644 index 000000000..0a4fd4c43 --- /dev/null +++ b/packages/oft-move/tasks/initOFTFA.ts @@ -0,0 +1,55 @@ +import { getChain, getConnection } from '@layerzerolabs/devtools-move/sdk/moveVMConnectionBuilder' +import { OFT } from '@layerzerolabs/devtools-move/sdk/oft' + +import { + getEidFromMoveNetwork, + getLzNetworkStage, + parseYaml, +} from '@layerzerolabs/devtools-move/tasks/move/utils/aptosNetworkParser' +import { getMoveVMOftAddress, sendInitTransaction } from '@layerzerolabs/devtools-move/tasks/move/utils/utils' + +async function initOFTFA( + token_name: string, + token_symbol: string, + icon_uri: string, + project_uri: string, + shared_decimals: number, + local_decimals: number +) { + const { account_address, private_key, network, fullnode, faucet } = await parseYaml() + console.log(`Using aptos network ${network}`) + + const lzNetworkStage = getLzNetworkStage(network) + const chain = getChain(fullnode) + const aptosOftAddress = getMoveVMOftAddress(chain, lzNetworkStage) + + console.log(`\n⚡ Initializing ${chain}-${lzNetworkStage} OFT`) + console.log(` Address: ${aptosOftAddress}\n`) + + console.log(`Setting the following parameters:`) + console.log(`\tToken Name: ${token_name}`) + console.log(`\tToken Symbol: ${token_symbol}`) + console.log(`\tIcon URI: ${icon_uri}`) + console.log(`\tProject URI: ${project_uri}`) + console.log(`\tShared Decimals: ${shared_decimals}`) + console.log(`\tLocal Decimals: ${local_decimals}`) + + const moveVMConnection = getConnection(chain, network, fullnode, faucet) + const oft = new OFT(moveVMConnection, aptosOftAddress, account_address, private_key) + + const initializePayload = oft.initializeOFTFAPayload( + token_name, + token_symbol, + icon_uri, + project_uri, + shared_decimals, + local_decimals + ) + + const eid = getEidFromMoveNetwork(chain, network) + const payloads = [{ payload: initializePayload, description: 'Initialize Aptos OFT', eid }] + + sendInitTransaction(moveVMConnection, oft, account_address, payloads) +} + +export { initOFTFA } diff --git a/packages/oft-move/tasks/irrevocablyDisableBlocklist.ts b/packages/oft-move/tasks/irrevocablyDisableBlocklist.ts new file mode 100644 index 000000000..1cea3ba19 --- /dev/null +++ b/packages/oft-move/tasks/irrevocablyDisableBlocklist.ts @@ -0,0 +1,30 @@ +import { getChain, getConnection } from '@layerzerolabs/devtools-move/sdk/moveVMConnectionBuilder' +import { OFT, OFTType } from '@layerzerolabs/devtools-move/sdk/oft' + +import { getLzNetworkStage, parseYaml } from '@layerzerolabs/devtools-move/tasks/move/utils/aptosNetworkParser' +import { getMoveVMOftAddress, sendAllTxs } from '@layerzerolabs/devtools-move/tasks/move/utils/utils' +import { createIrrevocablyDisableBlocklistPayload } from '@layerzerolabs/devtools-move/tasks/move/utils/moveVMOftConfigOps' + +async function irrevocablyDisableBlocklist(oftType: OFTType) { + const { account_address, private_key, network, fullnode, faucet } = await parseYaml() + + const chain = getChain(fullnode) + const aptos = getConnection(chain, network, fullnode, faucet) + + const lzNetworkStage = getLzNetworkStage(network) + const oftAddress = getMoveVMOftAddress(chain, lzNetworkStage) + + console.log(`\n🔧 Irrevocably Disabling Blocklist for ${chain}-${lzNetworkStage} OFT`) + console.log(`\tFor: ${oftAddress}\n`) + console.log( + `\t\x1b[33m Warning: This action is irreversible and will permanently disable blocklisting ability.\x1b[0m` + ) + + const oft = new OFT(aptos, oftAddress, account_address, private_key) + + const payload = createIrrevocablyDisableBlocklistPayload(oft, oftType) + + sendAllTxs(aptos, oft, account_address, [payload]) +} + +export { irrevocablyDisableBlocklist } diff --git a/packages/oft-move/tasks/mintToMoveVM.ts b/packages/oft-move/tasks/mintToMoveVM.ts new file mode 100644 index 000000000..4ab06775b --- /dev/null +++ b/packages/oft-move/tasks/mintToMoveVM.ts @@ -0,0 +1,41 @@ +import { Aptos, AptosConfig } from '@aptos-labs/ts-sdk' +import { getChain } from '@layerzerolabs/devtools-move/sdk/moveVMConnectionBuilder' +import { OFT } from '@layerzerolabs/devtools-move/sdk/oft' + +import { + getEidFromMoveNetwork, + getLzNetworkStage, + parseYaml, +} from '@layerzerolabs/devtools-move/tasks/move/utils/aptosNetworkParser' +import { TransactionPayload } from '@layerzerolabs/devtools-move/tasks/move/utils/moveVMOftConfigOps' +import { getMoveVMOftAddress, sendAllTxs } from '@layerzerolabs/devtools-move/tasks/move/utils/utils' + +async function mintToMoveVM(amountLd: number, toAddress: string) { + const { account_address, private_key, network, fullnode } = await parseYaml() + console.log(`Using aptos network ${network}`) + + const chain = getChain(fullnode) + const aptosConfig = new AptosConfig({ network: network }) + const aptos = new Aptos(aptosConfig) + + const lzNetworkStage = getLzNetworkStage(network) + const aptosOftAddress = getMoveVMOftAddress(chain, lzNetworkStage) + + console.log(`\n🪙 Minting ${chain}-${lzNetworkStage} OFT ✨`) + console.log(`\tAddress: ${aptosOftAddress}`) + console.log(`\tAmount: ${amountLd}`) + console.log(`\tTo: ${toAddress}`) + + const oft = new OFT(aptos, aptosOftAddress, account_address, private_key) + const mintPayload = oft.mintPayload(toAddress, amountLd) + const eid = getEidFromMoveNetwork(chain, network) + + const transactionPayload: TransactionPayload = { + payload: mintPayload, + description: `Mint ${chain}-${lzNetworkStage} OFT`, + eid: eid, + } + sendAllTxs(aptos, oft, account_address, [transactionPayload]) +} + +export default mintToMoveVM diff --git a/packages/oft-move/tasks/permanentlyDisableFreezing.ts b/packages/oft-move/tasks/permanentlyDisableFreezing.ts new file mode 100644 index 000000000..0102040cb --- /dev/null +++ b/packages/oft-move/tasks/permanentlyDisableFreezing.ts @@ -0,0 +1,28 @@ +import { getChain, getConnection } from '@layerzerolabs/devtools-move/sdk/moveVMConnectionBuilder' +import { OFT } from '@layerzerolabs/devtools-move/sdk/oft' + +import { getLzNetworkStage, parseYaml } from '@layerzerolabs/devtools-move/tasks/move/utils/aptosNetworkParser' +import { getMoveVMOftAddress, sendAllTxs } from '@layerzerolabs/devtools-move/tasks/move/utils/utils' +import { createPermanentlyDisableFungibleStoreFreezingPayload } from '@layerzerolabs/devtools-move/tasks/move/utils/moveVMOftConfigOps' + +async function permanentlyDisableFreezing() { + const { account_address, private_key, network, fullnode, faucet } = await parseYaml() + + const chain = getChain(fullnode) + const aptos = getConnection(chain, network, fullnode, faucet) + + const lzNetworkStage = getLzNetworkStage(network) + const oftAddress = getMoveVMOftAddress(chain, lzNetworkStage) + + console.log(`\n🔧 Permanently Disabling Freezing for ${chain}-${lzNetworkStage} OFT`) + console.log(`\tFor: ${oftAddress}\n`) + console.log(`\t\x1b[33m Warning: This action is irreversible and will permanently disable freezing.\x1b[0m`) + + const oft = new OFT(aptos, oftAddress, account_address, private_key) + + const payload = createPermanentlyDisableFungibleStoreFreezingPayload(oft) + + sendAllTxs(aptos, oft, account_address, [payload]) +} + +export { permanentlyDisableFreezing } diff --git a/packages/oft-move/tasks/quoteSendOFT.ts b/packages/oft-move/tasks/quoteSendOFT.ts new file mode 100644 index 000000000..a8079e678 --- /dev/null +++ b/packages/oft-move/tasks/quoteSendOFT.ts @@ -0,0 +1,66 @@ +import { Aptos, AptosConfig } from '@aptos-labs/ts-sdk' + +import { EndpointId } from '@layerzerolabs/lz-definitions' +import { Options } from '@layerzerolabs/lz-v2-utilities' + +import { OFT } from '@layerzerolabs/devtools-move/sdk/oft' +import { hexAddrToAptosBytesAddr } from '@layerzerolabs/devtools-move/sdk/utils' + +import { getLzNetworkStage, parseYaml } from '@layerzerolabs/devtools-move/tasks/move/utils/aptosNetworkParser' +import { getMoveVMOftAddress } from '@layerzerolabs/devtools-move/tasks/move/utils/utils' +import { toAptosAddress } from '@layerzerolabs/devtools-move/tasks/move/utils/moveVMOftConfigOps' +import { getChain } from '@layerzerolabs/devtools-move/sdk/moveVMConnectionBuilder' + +async function quoteSendOFT( + amountLd: number, + minAmountLd: number, + toAddress: string, + gasLimit: number, + dstEid: EndpointId +) { + const { account_address, private_key, network, fullnode } = await parseYaml() + console.log(`Using aptos network ${network}`) + + const aptosConfig = new AptosConfig({ network: network }) + const aptos = new Aptos(aptosConfig) + + const lzNetworkStage = getLzNetworkStage(network) + const chain = getChain(fullnode) + const aptosOftAddress = getMoveVMOftAddress(chain, lzNetworkStage) + + const oft = new OFT(aptos, aptosOftAddress, account_address, private_key) + + // Pad EVM address to 64 chars and convert Solana address to Aptos address + toAddress = toAptosAddress(toAddress, dstEid.toString()) + const toAddressBytes = hexAddrToAptosBytesAddr(toAddress) + const options = Options.newOptions().addExecutorLzReceiveOption(BigInt(gasLimit)) + + console.log(`Attempting to quote send ${amountLd} units`) + console.log(`Using OFT at address: ${aptosOftAddress}`) + console.log(`From account: ${account_address}`) + console.log(`To account: ${toAddress}`) + console.log(`dstEid: ${dstEid}`) + + const extra_options = options.toBytes() + const compose_message = new Uint8Array([]) + const oft_cmd = new Uint8Array([]) + + const [nativeFee, zroFee] = await oft.quoteSend( + account_address, + dstEid, + toAddressBytes, + amountLd, + minAmountLd, + extra_options, + compose_message, + oft_cmd, + false // pay_in_zro: false to pay in native tokens + ) + + console.log('\nQuote received:') + console.log('- Native fee:', nativeFee) + console.log('- ZRO fee:', zroFee) + console.log('If the above fees are acceptable, the wiring is confirmed to be successful.') +} + +export { quoteSendOFT } diff --git a/packages/oft-move/tasks/sendFromMoveVm.ts b/packages/oft-move/tasks/sendFromMoveVm.ts new file mode 100644 index 000000000..8791c089c --- /dev/null +++ b/packages/oft-move/tasks/sendFromMoveVm.ts @@ -0,0 +1,91 @@ +import { Aptos, AptosConfig } from '@aptos-labs/ts-sdk' + +import { EndpointId } from '@layerzerolabs/lz-definitions' +import { Options } from '@layerzerolabs/lz-v2-utilities' + +import { OFT } from '@layerzerolabs/devtools-move/sdk/oft' +import { hexAddrToAptosBytesAddr } from '@layerzerolabs/devtools-move/sdk/utils' + +import { getLzNetworkStage, parseYaml } from '@layerzerolabs/devtools-move/tasks/move/utils/aptosNetworkParser' +import { getMoveVMOftAddress, sendAllTxs } from '@layerzerolabs/devtools-move/tasks/move/utils/utils' +import { toAptosAddress } from '@layerzerolabs/devtools-move/tasks/move/utils/moveVMOftConfigOps' +import { getChain } from '@layerzerolabs/devtools-move/sdk/moveVMConnectionBuilder' + +async function sendFromMoveVm( + amountLd: bigint, + minAmountLd: bigint, + toAddress: string, + gasLimit: bigint, + dstEid: EndpointId, + srcAddress: string +) { + const { account_address, private_key, network, fullnode } = await parseYaml() + console.log(`Using aptos network ${network}`) + + const aptosConfig = new AptosConfig({ network: network }) + const aptos = new Aptos(aptosConfig) + + const lzNetworkStage = getLzNetworkStage(network) + const chain = getChain(fullnode) + const aptosOftAddress = getMoveVMOftAddress(chain, lzNetworkStage) + + const oft = new OFT(aptos, aptosOftAddress, account_address, private_key) + + // Pad EVM address to 64 chars and convert Solana address to Aptos address + toAddress = toAptosAddress(toAddress, dstEid.toString()) + const toAddressBytes = hexAddrToAptosBytesAddr(toAddress) + const options = Options.newOptions().addExecutorLzReceiveOption(BigInt(gasLimit)) + + console.log(`Sending ${amountLd} units`) + console.log(`\tUsing OFT at address: ${aptosOftAddress}`) + console.log(`\tFrom account: ${srcAddress}`) + console.log(`\tTo account: ${toAddress}`) + console.log(`\tdstEid: ${dstEid}`) + console.log(`\tsrcAddress: ${srcAddress}`) + + const extra_options = options.toBytes() + const compose_message = new Uint8Array([]) + const oft_cmd = new Uint8Array([]) + + const [nativeFee, zroFee] = await oft.quoteSend( + srcAddress, + dstEid, + toAddressBytes, + amountLd, + minAmountLd, + extra_options, + compose_message, + oft_cmd, + false + ) + + console.log('\nQuote received:') + console.log('- Native fee:', nativeFee) + console.log('- ZRO fee:', zroFee) + + const sendPayload = oft.sendPayload( + dstEid, + toAddressBytes, + amountLd, + minAmountLd, + extra_options, + compose_message, + oft_cmd, + nativeFee, + 0 + ) + + const payloads = [{ payload: sendPayload, description: 'Send Aptos OFT', eid: dstEid }] + await sendAllTxs(aptos, oft, srcAddress, payloads) + + // Check the balance again + const balance = await aptos.view({ + payload: { + function: `${aptosOftAddress}::oft::balance`, + functionArguments: [srcAddress], + }, + }) + console.log('New balance:', balance) +} + +export { sendFromMoveVm } diff --git a/packages/oft-move/tasks/setFee.ts b/packages/oft-move/tasks/setFee.ts new file mode 100644 index 000000000..3fc5e4340 --- /dev/null +++ b/packages/oft-move/tasks/setFee.ts @@ -0,0 +1,33 @@ +import { EndpointId, getNetworkForChainId } from '@layerzerolabs/lz-definitions' + +import { getChain, getConnection } from '@layerzerolabs/devtools-move/sdk/moveVMConnectionBuilder' +import { OFT, OFTType } from '@layerzerolabs/devtools-move/sdk/oft' + +import { getLzNetworkStage, parseYaml } from '@layerzerolabs/devtools-move/tasks/move/utils/aptosNetworkParser' +import { getMoveVMOftAddress, sendAllTxs } from '@layerzerolabs/devtools-move/tasks/move/utils/utils' +import { createSetFeeBpsTx } from '@layerzerolabs/devtools-move/tasks/move/utils/moveVMOftConfigOps' + +async function setFee(feeBps: bigint, toEid: EndpointId, oftType: OFTType) { + const { account_address, private_key, network, fullnode, faucet } = await parseYaml() + + const chain = getChain(fullnode) + const aptos = getConnection(chain, network, fullnode, faucet) + + const lzNetworkStage = getLzNetworkStage(network) + const oftAddress = getMoveVMOftAddress(chain, lzNetworkStage) + + console.log(`\n🔧 Setting ${chain}-${lzNetworkStage} OFT Fee BPS`) + console.log(`\tFor: ${oftAddress}\n`) + + const toNetwork = getNetworkForChainId(toEid) + console.log(`\tPathway: ${chain}-${lzNetworkStage} -> ${toNetwork.chainName}-${toNetwork.env}\n`) + console.log(`\tFee BPS: ${feeBps}\n`) + + const oft = new OFT(aptos, oftAddress, account_address, private_key) + + const setFeeBpsPayload = await createSetFeeBpsTx(oft, feeBps, toEid, oftType) + + sendAllTxs(aptos, oft, account_address, [setFeeBpsPayload]) +} + +export { setFee } diff --git a/packages/oft-move/tasks/setRateLimit.ts b/packages/oft-move/tasks/setRateLimit.ts new file mode 100644 index 000000000..6213642d5 --- /dev/null +++ b/packages/oft-move/tasks/setRateLimit.ts @@ -0,0 +1,34 @@ +import { EndpointId, getNetworkForChainId } from '@layerzerolabs/lz-definitions' + +import { getChain, getConnection } from '@layerzerolabs/devtools-move/sdk/moveVMConnectionBuilder' +import { OFT, OFTType } from '@layerzerolabs/devtools-move/sdk/oft' + +import { getLzNetworkStage, parseYaml } from '@layerzerolabs/devtools-move/tasks/move/utils/aptosNetworkParser' +import { createSetRateLimitTx } from '@layerzerolabs/devtools-move/tasks/move/utils/moveVMOftConfigOps' +import { getMoveVMOftAddress, sendAllTxs } from '@layerzerolabs/devtools-move/tasks/move/utils/utils' + +async function setRateLimit(rateLimit: bigint, windowSeconds: bigint, toEid: EndpointId, oftType: OFTType) { + const { account_address, private_key, network, fullnode, faucet } = await parseYaml() + + const chain = getChain(fullnode) + const aptos = getConnection(chain, network, fullnode, faucet) + + const lzNetworkStage = getLzNetworkStage(network) + const oftAddress = getMoveVMOftAddress(chain, lzNetworkStage) + + console.log(`\n🔧 Setting ${chain}-${lzNetworkStage} OFT Rate Limit`) + console.log(`\tFor: ${oftAddress}\n`) + + const oft = new OFT(aptos, oftAddress, account_address, private_key) + + const toNetwork = getNetworkForChainId(toEid) + console.log(`\tPathway: ${chain}-${lzNetworkStage} -> ${toNetwork.chainName}-${toNetwork.env}\n`) + console.log(`\tRate Limit: ${rateLimit}`) + console.log(`\tWindow: ${windowSeconds} seconds\n`) + + const setRateLimitPayload = await createSetRateLimitTx(oft, rateLimit, windowSeconds, toEid, oftType) + + sendAllTxs(aptos, oft, account_address, [setRateLimitPayload]) +} + +export { setRateLimit } diff --git a/packages/oft-move/tasks/unSetRateLimit.ts b/packages/oft-move/tasks/unSetRateLimit.ts new file mode 100644 index 000000000..54a81cf5d --- /dev/null +++ b/packages/oft-move/tasks/unSetRateLimit.ts @@ -0,0 +1,36 @@ +import { EndpointId, getNetworkForChainId } from '@layerzerolabs/lz-definitions' + +import { getChain, getConnection } from '@layerzerolabs/devtools-move/sdk/moveVMConnectionBuilder' +import { OFT, OFTType } from '@layerzerolabs/devtools-move/sdk/oft' + +import { getLzNetworkStage, parseYaml } from '@layerzerolabs/devtools-move/tasks/move/utils/aptosNetworkParser' +import { createUnsetRateLimitTx } from '@layerzerolabs/devtools-move/tasks/move/utils/moveVMOftConfigOps' +import { getMoveVMOftAddress, sendAllTxs } from '@layerzerolabs/devtools-move/tasks/move/utils/utils' + +async function unsetRateLimit(toEid: EndpointId, oftType: OFTType) { + const { account_address, private_key, network, fullnode, faucet } = await parseYaml() + + const chain = getChain(fullnode) + const aptos = getConnection(chain, network, fullnode, faucet) + + const lzNetworkStage = getLzNetworkStage(network) + const oftAddress = getMoveVMOftAddress(chain, lzNetworkStage) + + console.log(`\n🔧 Unsetting ${chain}-${lzNetworkStage} OFT Rate Limit`) + console.log(`\tFor: ${oftAddress}\n`) + + const oft = new OFT(aptos, oftAddress, account_address, private_key) + + const toNetwork = getNetworkForChainId(toEid) + console.log(`\tPathway: ${chain}-${lzNetworkStage} -> ${toNetwork.chainName}-${toNetwork.env}\n`) + + const [currentLimit, currentWindow] = await oft.getRateLimitConfig(toEid, oftType) + console.log(`\tCurrent Rate Limit: ${currentLimit}`) + console.log(`\tCurrent Window: ${currentWindow} seconds\n`) + + const unsetRateLimitPayload = await createUnsetRateLimitTx(oft, toEid, oftType) + + sendAllTxs(aptos, oft, account_address, [unsetRateLimitPayload]) +} + +export { unsetRateLimit } diff --git a/packages/oft-move/tsconfig.json b/packages/oft-move/tsconfig.json new file mode 100644 index 000000000..fa385a5a5 --- /dev/null +++ b/packages/oft-move/tsconfig.json @@ -0,0 +1,17 @@ +{ + "exclude": ["node_modules"], + "include": ["deploy", "tasks", "test", "hardhat.config.ts"], + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "paths": { + "@/utils/*": ["./tasks/shared/*"], + "@/tasks/*": ["./tasks/*"] + } + } +} diff --git a/packages/oft-move/tsup.config.ts b/packages/oft-move/tsup.config.ts new file mode 100644 index 000000000..8f03c2dc4 --- /dev/null +++ b/packages/oft-move/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['types/index.d.ts'], + outDir: './dist', + clean: true, + dts: true, + sourcemap: true, + splitting: false, + treeshake: true, + format: ['esm', 'cjs'], +}) diff --git a/packages/oft-move/turbo.json b/packages/oft-move/turbo.json new file mode 100644 index 000000000..adaaffa58 --- /dev/null +++ b/packages/oft-move/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "pipeline": { + "build": { + "outputs": ["build*/**"] + } + } +} diff --git a/packages/oft-move/types/index.d.ts b/packages/oft-move/types/index.d.ts new file mode 100644 index 000000000..ec9ed6c41 --- /dev/null +++ b/packages/oft-move/types/index.d.ts @@ -0,0 +1,3 @@ +import { initOFTFA } from '../tasks/initOFTFA' + +export { initOFTFA } diff --git a/packages/toolbox-foundry/src/forge-std b/packages/toolbox-foundry/src/forge-std index 051fb65fc..ae570fec0 160000 --- a/packages/toolbox-foundry/src/forge-std +++ b/packages/toolbox-foundry/src/forge-std @@ -1 +1 @@ -Subproject commit 051fb65fc46013dff64e46ca410445d63ea5620a +Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79e7a9ff4..68cabed2c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -756,6 +756,345 @@ importers: specifier: ^5.4.4 version: 5.5.3 + examples/oft-adapter-aptos-move: + devDependencies: + '@aptos-labs/ts-sdk': + specifier: ^1.33.1 + version: 1.33.2 + '@babel/core': + specifier: ^7.23.9 + version: 7.23.9 + '@jest/globals': + specifier: ^29.7.0 + version: 29.7.0 + '@layerzerolabs/devtools-evm-hardhat': + specifier: ^2.0.3 + version: link:../../packages/devtools-evm-hardhat + '@layerzerolabs/devtools-extensible-cli': + specifier: ^0.0.1 + version: link:../../packages/devtools-extensible-cli + '@layerzerolabs/devtools-move': + specifier: ^0.0.1 + version: link:../../packages/devtools-move + '@layerzerolabs/eslint-config-next': + specifier: ~2.3.39 + version: 2.3.44(typescript@5.5.3) + '@layerzerolabs/lz-config-types': + specifier: ^3.0.15 + version: 3.0.59(ethers@5.7.2) + '@layerzerolabs/lz-definitions': + specifier: ^3.0.21 + version: 3.0.59 + '@layerzerolabs/lz-evm-messagelib-v2': + specifier: ^3.0.12 + version: 3.0.12(@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.12)(@layerzerolabs/lz-evm-v1-0.7@3.0.15)(@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.12 + version: 3.0.12(@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-sdk-v2': + specifier: ^3.0.27 + version: 3.0.59 + '@layerzerolabs/lz-evm-v1-0.7': + specifier: ^3.0.12 + version: 3.0.15(@openzeppelin/contracts-upgradeable@5.1.0)(@openzeppelin/contracts@5.1.0)(hardhat-deploy@0.12.4) + '@layerzerolabs/lz-movevm-sdk-v2': + specifier: ^3.0.15 + version: 3.0.59(typescript@5.5.3) + '@layerzerolabs/lz-serdes': + specifier: ^3.0.19 + version: 3.0.38 + '@layerzerolabs/lz-v2-utilities': + specifier: ^3.0.12 + version: 3.0.38 + '@layerzerolabs/move-definitions': + specifier: ^3.0.15 + version: 3.0.59 + '@layerzerolabs/oapp-evm': + specifier: ^0.3.0 + version: link:../../packages/oapp-evm + '@layerzerolabs/oft-evm': + specifier: ^3.0.0 + version: link:../../packages/oft-evm + '@layerzerolabs/oft-move': + specifier: ^0.0.1 + version: link:../../packages/oft-move + '@layerzerolabs/prettier-config-next': + specifier: ^2.3.39 + version: 2.3.44 + '@layerzerolabs/solhint-config': + specifier: ^3.0.12 + version: 3.0.12(typescript@5.5.3) + '@layerzerolabs/test-devtools-evm-foundry': + specifier: ~5.1.0 + version: 5.1.0(@layerzerolabs/lz-evm-messagelib-v2@3.0.12)(@layerzerolabs/lz-evm-protocol-v2@3.0.12)(@layerzerolabs/lz-evm-v1-0.7@3.0.15)(@layerzerolabs/oapp-evm@packages+oapp-evm)(@layerzerolabs/oft-evm@packages+oft-evm)(@openzeppelin/contracts-upgradeable@5.1.0)(@openzeppelin/contracts@5.1.0) + '@layerzerolabs/toolbox-foundry': + specifier: ~0.1.9 + version: link:../../packages/toolbox-foundry + '@layerzerolabs/toolbox-hardhat': + specifier: ~0.6.3 + version: link:../../packages/toolbox-hardhat + '@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 + '@types/argparse': + specifier: ^2.0.17 + version: 2.0.17 + '@types/chai': + specifier: ^4.3.11 + version: 4.3.20 + '@types/jest': + specifier: ^29.5.12 + version: 29.5.12 + '@types/mocha': + specifier: ^10.0.6 + version: 10.0.6 + '@types/node': + specifier: ~18.18.14 + version: 18.18.14 + argparse: + specifier: ^2.0.1 + version: 2.0.1 + 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 + 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 + install: + specifier: ^0.13.0 + version: 0.13.0 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + mocha: + specifier: ^10.2.0 + version: 10.2.0 + 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-jest: + specifier: ^29.2.5 + version: 29.2.5(@babel/core@7.23.9)(esbuild@0.19.11)(jest@29.7.0)(typescript@5.5.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + tsup: + specifier: ^8.0.1 + version: 8.0.1(@swc/core@1.4.0)(ts-node@10.9.2)(typescript@5.5.3) + typescript: + specifier: ^5.4.4 + version: 5.5.3 + yaml: + specifier: ^2.6.1 + version: 2.7.0 + + examples/oft-aptos-move: + devDependencies: + '@aptos-labs/ts-sdk': + specifier: ^1.33.1 + version: 1.33.2 + '@babel/core': + specifier: ^7.23.9 + version: 7.23.9 + '@jest/globals': + specifier: ^29.7.0 + version: 29.7.0 + '@layerzerolabs/devtools-extensible-cli': + specifier: ^0.0.1 + version: link:../../packages/devtools-extensible-cli + '@layerzerolabs/devtools-move': + specifier: ^0.0.1 + version: link:../../packages/devtools-move + '@layerzerolabs/eslint-config-next': + specifier: ~2.3.39 + version: 2.3.44(typescript@5.5.3) + '@layerzerolabs/lz-config-types': + specifier: ^3.0.15 + version: 3.0.59(ethers@5.7.2) + '@layerzerolabs/lz-definitions': + specifier: ^3.0.21 + version: 3.0.59 + '@layerzerolabs/lz-evm-messagelib-v2': + specifier: ^3.0.12 + version: 3.0.12(@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.12)(@layerzerolabs/lz-evm-v1-0.7@3.0.15)(@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.12 + version: 3.0.12(@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-sdk-v2': + specifier: ^3.0.27 + version: 3.0.59 + '@layerzerolabs/lz-evm-v1-0.7': + specifier: ^3.0.12 + version: 3.0.15(@openzeppelin/contracts-upgradeable@5.1.0)(@openzeppelin/contracts@5.1.0)(hardhat-deploy@0.12.4) + '@layerzerolabs/lz-movevm-sdk-v2': + specifier: ^3.0.15 + version: 3.0.59(typescript@5.5.3) + '@layerzerolabs/lz-serdes': + specifier: ^3.0.19 + version: 3.0.38 + '@layerzerolabs/lz-v2-utilities': + specifier: ^3.0.12 + version: 3.0.38 + '@layerzerolabs/move-definitions': + specifier: ^3.0.15 + version: 3.0.59 + '@layerzerolabs/oapp-evm': + specifier: ^0.3.0 + version: link:../../packages/oapp-evm + '@layerzerolabs/oft-evm': + specifier: ^3.0.0 + version: link:../../packages/oft-evm + '@layerzerolabs/oft-move': + specifier: ^0.0.1 + version: link:../../packages/oft-move + '@layerzerolabs/prettier-config-next': + specifier: ^2.3.39 + version: 2.3.44 + '@layerzerolabs/solhint-config': + specifier: ^3.0.12 + version: 3.0.12(typescript@5.5.3) + '@layerzerolabs/test-devtools-evm-foundry': + specifier: ~5.1.0 + version: 5.1.0(@layerzerolabs/lz-evm-messagelib-v2@3.0.12)(@layerzerolabs/lz-evm-protocol-v2@3.0.12)(@layerzerolabs/lz-evm-v1-0.7@3.0.15)(@layerzerolabs/oapp-evm@packages+oapp-evm)(@layerzerolabs/oft-evm@packages+oft-evm)(@openzeppelin/contracts-upgradeable@5.1.0)(@openzeppelin/contracts@5.1.0) + '@layerzerolabs/toolbox-foundry': + specifier: ~0.1.9 + version: link:../../packages/toolbox-foundry + '@layerzerolabs/toolbox-hardhat': + specifier: ~0.6.3 + version: link:../../packages/toolbox-hardhat + '@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 + '@types/argparse': + specifier: ^2.0.17 + version: 2.0.17 + '@types/chai': + specifier: ^4.3.11 + version: 4.3.20 + '@types/jest': + specifier: ^29.5.12 + version: 29.5.12 + '@types/mocha': + specifier: ^10.0.6 + version: 10.0.6 + '@types/node': + specifier: ~18.18.14 + version: 18.18.14 + argparse: + specifier: ^2.0.1 + version: 2.0.1 + 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 + 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 + install: + specifier: ^0.13.0 + version: 0.13.0 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + mocha: + specifier: ^10.2.0 + version: 10.2.0 + 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-jest: + specifier: ^29.2.5 + version: 29.2.5(@babel/core@7.23.9)(esbuild@0.19.11)(jest@29.7.0)(typescript@5.5.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + tsup: + specifier: ^8.0.1 + version: 8.0.1(@swc/core@1.4.0)(ts-node@10.9.2)(typescript@5.5.3) + typescript: + specifier: ^5.4.4 + version: 5.5.3 + yaml: + specifier: ^2.6.1 + version: 2.7.0 + examples/oft-solana: devDependencies: '@coral-xyz/anchor': @@ -1956,36 +2295,130 @@ importers: '@types/jest': specifier: ^29.5.12 version: 29.5.12 - fast-check: - specifier: ^3.15.1 - version: 3.15.1 - fp-ts: - specifier: ^2.16.2 - version: 2.16.2 + fast-check: + specifier: ^3.15.1 + version: 3.15.1 + fp-ts: + specifier: ^2.16.2 + version: 2.16.2 + hardhat: + specifier: ^2.22.10 + version: 2.22.12(ts-node@10.9.2)(typescript@5.5.3) + hardhat-deploy: + specifier: ^0.12.1 + version: 0.12.1 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + sinon: + specifier: ^17.0.1 + version: 17.0.1 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + tslib: + specifier: ~2.6.2 + version: 2.6.2 + tsup: + specifier: ~8.0.1 + version: 8.0.1(@swc/core@1.4.0)(ts-node@10.9.2)(typescript@5.5.3) + typescript: + specifier: ^5.4.4 + version: 5.5.3 + + packages/devtools-extensible-cli: + devDependencies: + '@types/argparse': + specifier: ^2.0.17 + version: 2.0.17 + argparse: + specifier: ^2.0.1 + version: 2.0.1 + tsup: + specifier: ^8.0.1 + version: 8.0.1(@swc/core@1.4.0)(typescript@5.5.3) + typescript: + specifier: ^5.4.4 + version: 5.5.3 + + packages/devtools-move: + dependencies: + '@types/chai': + specifier: ^4.3.11 + version: 4.3.20 + chai: + specifier: ^4.4.1 + version: 4.5.0 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + ts-jest: + specifier: ^29.2.5 + version: 29.2.5(@babel/core@7.23.9)(esbuild@0.19.11)(jest@29.7.0)(typescript@5.5.3) + devDependencies: + '@aptos-labs/ts-sdk': + specifier: ^1.33.1 + version: 1.33.2 + '@layerzerolabs/devtools-extensible-cli': + specifier: ^0.0.1 + version: link:../devtools-extensible-cli + '@layerzerolabs/lz-definitions': + specifier: ^3.0.40 + version: 3.0.59 + '@layerzerolabs/lz-evm-sdk-v2': + specifier: ^3.0.27 + version: 3.0.59 + '@layerzerolabs/lz-serdes': + specifier: ^3.0.19 + version: 3.0.38 + '@layerzerolabs/lz-v2-utilities': + specifier: ^3.0.12 + version: 3.0.38 + '@layerzerolabs/toolbox-hardhat': + specifier: ~0.6.3 + version: link:../toolbox-hardhat + '@types/argparse': + specifier: ^2.0.17 + version: 2.0.17 + '@types/jest': + specifier: ^29.5.12 + version: 29.5.12 + '@types/node': + specifier: ~18.18.14 + version: 18.18.14 + argparse: + specifier: ^2.0.1 + version: 2.0.1 + base-x: + specifier: ^5.0.0 + version: 5.0.0 + bs58: + specifier: ^6.0.0 + version: 6.0.0 + depcheck: + specifier: ^1.4.7 + version: 1.4.7 + dotenv: + specifier: ^16.4.7 + version: 16.4.7 + ethers: + specifier: ^5.7.2 + version: 5.7.2 hardhat: specifier: ^2.22.10 version: 2.22.12(ts-node@10.9.2)(typescript@5.5.3) - hardhat-deploy: - specifier: ^0.12.1 - version: 0.12.1 - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) - sinon: - specifier: ^17.0.1 - version: 17.0.1 ts-node: specifier: ^10.9.2 version: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) - tslib: - specifier: ~2.6.2 - version: 2.6.2 tsup: - specifier: ~8.0.1 + specifier: ^8.0.1 version: 8.0.1(@swc/core@1.4.0)(ts-node@10.9.2)(typescript@5.5.3) typescript: specifier: ^5.4.4 version: 5.5.3 + yaml: + specifier: ^2.6.1 + version: 2.7.0 packages/devtools-solana: dependencies: @@ -2459,6 +2892,48 @@ importers: specifier: ^5.0.5 version: 5.0.9 + packages/oft-move: + devDependencies: + '@aptos-labs/ts-sdk': + specifier: ^1.33.1 + version: 1.33.2 + '@layerzerolabs/devtools-extensible-cli': + specifier: ^0.0.1 + version: link:../devtools-extensible-cli + '@layerzerolabs/devtools-move': + specifier: ^0.0.1 + version: link:../devtools-move + '@layerzerolabs/lz-definitions': + specifier: ^3.0.21 + version: 3.0.59 + '@layerzerolabs/lz-v2-utilities': + specifier: ^3.0.12 + version: 3.0.38 + '@layerzerolabs/prettier-config-next': + specifier: ^2.3.39 + version: 2.3.44 + argparse: + specifier: ^2.0.1 + version: 2.0.1 + depcheck: + specifier: ^1.4.7 + version: 1.4.7 + eslint: + specifier: ^8.55.0 + version: 8.57.1 + hardhat: + specifier: ^2.22.10 + version: 2.22.12(ts-node@10.9.2)(typescript@5.5.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + tsup: + specifier: ^8.0.1 + version: 8.0.1(@swc/core@1.4.0)(ts-node@10.9.2)(typescript@5.5.3) + typescript: + specifier: ^5.4.4 + version: 5.5.3 + packages/omnicounter-devtools: devDependencies: '@layerzerolabs/devtools': @@ -4249,6 +4724,12 @@ packages: dependencies: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.22 + + /@aptos-labs/aptos-cli@1.0.2: + resolution: {integrity: sha512-PYPsd0Kk3ynkxNfe3S4fanI3DiUICCoh4ibQderbvjPFL5A0oK6F4lPEO2t0MDsQySTk2t4vh99Xjy6Bd9y+aQ==} + hasBin: true + dependencies: + commander: 12.1.0 dev: true /@aptos-labs/aptos-client@0.1.1: @@ -4261,6 +4742,25 @@ packages: - debug dev: true + /@aptos-labs/ts-sdk@1.33.2: + resolution: {integrity: sha512-nbro7x9HudBDLngOW8IpRCAysb6si1kE0F4pUfDesRHzRcqivnQzv8O5ePZma7jkTgpjGNx6gdEBXKI6YcJbww==} + engines: {node: '>=20.0.0'} + dependencies: + '@aptos-labs/aptos-cli': 1.0.2 + '@aptos-labs/aptos-client': 0.1.1 + '@noble/curves': 1.8.0 + '@noble/hashes': 1.7.0 + '@scure/bip32': 1.6.2 + '@scure/bip39': 1.5.4 + eventemitter3: 5.0.1 + form-data: 4.0.1 + js-base64: 3.7.7 + jwt-decode: 4.0.0 + poseidon-lite: 0.2.1 + transitivePeerDependencies: + - debug + dev: true + /@aws-crypto/sha256-js@1.2.2: resolution: {integrity: sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==} dependencies: @@ -4302,12 +4802,10 @@ packages: dependencies: '@babel/highlight': 7.23.4 chalk: 2.4.2 - dev: true /@babel/compat-data@7.23.5: resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} engines: {node: '>=6.9.0'} - dev: true /@babel/core@7.23.9: resolution: {integrity: sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==} @@ -4330,7 +4828,6 @@ packages: semver: 6.3.1 transitivePeerDependencies: - supports-color - dev: true /@babel/generator@7.23.6: resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} @@ -4340,7 +4837,6 @@ packages: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.22 jsesc: 2.5.2 - dev: true /@babel/helper-compilation-targets@7.23.6: resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} @@ -4351,12 +4847,10 @@ packages: browserslist: 4.22.2 lru-cache: 5.1.1 semver: 6.3.1 - dev: true /@babel/helper-environment-visitor@7.22.20: resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-function-name@7.23.0: resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} @@ -4364,21 +4858,18 @@ packages: dependencies: '@babel/template': 7.23.9 '@babel/types': 7.23.9 - dev: true /@babel/helper-hoist-variables@7.22.5: resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.23.9 - dev: true /@babel/helper-module-imports@7.22.15: resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.23.9 - dev: true /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} @@ -4392,41 +4883,44 @@ packages: '@babel/helper-simple-access': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 '@babel/helper-validator-identifier': 7.22.20 - dev: true /@babel/helper-plugin-utils@7.22.5: resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-simple-access@7.22.5: resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.23.9 - dev: true /@babel/helper-split-export-declaration@7.22.6: resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.23.9 - dev: true /@babel/helper-string-parser@7.23.4: resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} engines: {node: '>=6.9.0'} + + /@babel/helper-string-parser@7.25.9: + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} dev: true /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} + + /@babel/helper-validator-identifier@7.25.9: + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} dev: true /@babel/helper-validator-option@7.23.5: resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} engines: {node: '>=6.9.0'} - dev: true /@babel/helpers@7.23.9: resolution: {integrity: sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==} @@ -4437,7 +4931,6 @@ packages: '@babel/types': 7.23.9 transitivePeerDependencies: - supports-color - dev: true /@babel/highlight@7.23.4: resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} @@ -4446,7 +4939,6 @@ packages: '@babel/helper-validator-identifier': 7.22.20 chalk: 2.4.2 js-tokens: 4.0.0 - dev: true /@babel/parser@7.23.9: resolution: {integrity: sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==} @@ -4454,6 +4946,13 @@ packages: hasBin: true dependencies: '@babel/types': 7.23.9 + + /@babel/parser@7.26.7: + resolution: {integrity: sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.26.7 dev: true /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.9): @@ -4463,7 +4962,6 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.9): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} @@ -4472,7 +4970,6 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.9): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} @@ -4481,7 +4978,6 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.9): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} @@ -4490,7 +4986,6 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.9): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} @@ -4499,7 +4994,6 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} @@ -4509,7 +5003,6 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.9): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} @@ -4518,7 +5011,6 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.9): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} @@ -4527,7 +5019,6 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.9): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} @@ -4536,7 +5027,6 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.9): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} @@ -4545,7 +5035,6 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.9): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} @@ -4554,7 +5043,6 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.9): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} @@ -4563,7 +5051,6 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.9): resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} @@ -4573,7 +5060,6 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==} @@ -4583,7 +5069,6 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/runtime@7.23.8: resolution: {integrity: sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==} @@ -4606,7 +5091,6 @@ packages: '@babel/code-frame': 7.23.5 '@babel/parser': 7.23.9 '@babel/types': 7.23.9 - dev: true /@babel/traverse@7.23.9: resolution: {integrity: sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==} @@ -4624,7 +5108,6 @@ packages: globals: 11.12.0 transitivePeerDependencies: - supports-color - dev: true /@babel/types@7.23.9: resolution: {integrity: sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==} @@ -4633,6 +5116,13 @@ packages: '@babel/helper-string-parser': 7.23.4 '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 + + /@babel/types@7.26.7: + resolution: {integrity: sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 dev: true /@balena/dockerignore@1.0.2: @@ -4641,7 +5131,6 @@ packages: /@bcoe/v8-coverage@0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - dev: true /@bitcoinerlab/secp256k1@1.2.0: resolution: {integrity: sha512-jeujZSzb3JOZfmJYI0ph1PVpCRV5oaexCgy+RvCXV8XlY+XFB/2n3WOcvBsKLsOw78KYgnQrQWb2HrKE4be88Q==} @@ -4962,7 +5451,6 @@ packages: cpu: [ppc64] os: [aix] requiresBuild: true - dev: true optional: true /@esbuild/aix-ppc64@0.20.2: @@ -4980,7 +5468,6 @@ packages: cpu: [arm64] os: [android] requiresBuild: true - dev: true optional: true /@esbuild/android-arm64@0.20.2: @@ -4998,7 +5485,6 @@ packages: cpu: [arm] os: [android] requiresBuild: true - dev: true optional: true /@esbuild/android-arm@0.20.2: @@ -5016,7 +5502,6 @@ packages: cpu: [x64] os: [android] requiresBuild: true - dev: true optional: true /@esbuild/android-x64@0.20.2: @@ -5034,7 +5519,6 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: true optional: true /@esbuild/darwin-arm64@0.20.2: @@ -5052,7 +5536,6 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: true optional: true /@esbuild/darwin-x64@0.20.2: @@ -5070,7 +5553,6 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true - dev: true optional: true /@esbuild/freebsd-arm64@0.20.2: @@ -5088,7 +5570,6 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true - dev: true optional: true /@esbuild/freebsd-x64@0.20.2: @@ -5106,7 +5587,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-arm64@0.20.2: @@ -5124,7 +5604,6 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-arm@0.20.2: @@ -5142,7 +5621,6 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-ia32@0.20.2: @@ -5160,7 +5638,6 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-loong64@0.20.2: @@ -5178,7 +5655,6 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-mips64el@0.20.2: @@ -5196,7 +5672,6 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-ppc64@0.20.2: @@ -5214,7 +5689,6 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-riscv64@0.20.2: @@ -5232,7 +5706,6 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-s390x@0.20.2: @@ -5250,7 +5723,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-x64@0.20.2: @@ -5268,7 +5740,6 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true - dev: true optional: true /@esbuild/netbsd-x64@0.20.2: @@ -5286,7 +5757,6 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true - dev: true optional: true /@esbuild/openbsd-x64@0.20.2: @@ -5304,7 +5774,6 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true - dev: true optional: true /@esbuild/sunos-x64@0.20.2: @@ -5322,7 +5791,6 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: true optional: true /@esbuild/win32-arm64@0.20.2: @@ -5340,7 +5808,6 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true - dev: true optional: true /@esbuild/win32-ia32@0.20.2: @@ -5358,7 +5825,6 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: true optional: true /@esbuild/win32-x64@0.20.2: @@ -6142,12 +6608,10 @@ packages: get-package-type: 0.1.0 js-yaml: 3.14.1 resolve-from: 5.0.0 - dev: true /@istanbuljs/schema@0.1.3: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} - dev: true /@jest/console@29.7.0: resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} @@ -6159,7 +6623,6 @@ packages: jest-message-util: 29.7.0 jest-util: 29.7.0 slash: 3.0.0 - dev: true /@jest/core@29.7.0(ts-node@10.9.2): resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} @@ -6202,7 +6665,6 @@ packages: - babel-plugin-macros - supports-color - ts-node - dev: true /@jest/create-cache-key-function@29.7.0: resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==} @@ -6219,14 +6681,12 @@ packages: '@jest/types': 29.6.3 '@types/node': 18.18.14 jest-mock: 29.7.0 - dev: true /@jest/expect-utils@29.7.0: resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: jest-get-type: 29.6.3 - dev: true /@jest/expect@29.7.0: resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} @@ -6236,7 +6696,6 @@ packages: jest-snapshot: 29.7.0 transitivePeerDependencies: - supports-color - dev: true /@jest/fake-timers@29.7.0: resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} @@ -6248,7 +6707,6 @@ packages: jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 - dev: true /@jest/globals@29.7.0: resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} @@ -6260,7 +6718,6 @@ packages: jest-mock: 29.7.0 transitivePeerDependencies: - supports-color - dev: true /@jest/reporters@29.7.0: resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} @@ -6297,14 +6754,12 @@ packages: v8-to-istanbul: 9.2.0 transitivePeerDependencies: - supports-color - dev: true /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@sinclair/typebox': 0.27.8 - dev: true /@jest/source-map@29.6.3: resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} @@ -6313,7 +6768,6 @@ packages: '@jridgewell/trace-mapping': 0.3.22 callsites: 3.1.0 graceful-fs: 4.2.11 - dev: true /@jest/test-result@29.7.0: resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} @@ -6323,7 +6777,6 @@ packages: '@jest/types': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 collect-v8-coverage: 1.0.2 - dev: true /@jest/test-sequencer@29.7.0: resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} @@ -6333,7 +6786,6 @@ packages: graceful-fs: 4.2.11 jest-haste-map: 29.7.0 slash: 3.0.0 - dev: true /@jest/transform@29.7.0: resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} @@ -6356,7 +6808,6 @@ packages: write-file-atomic: 4.0.2 transitivePeerDependencies: - supports-color - dev: true /@jest/types@29.6.3: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} @@ -6368,7 +6819,6 @@ packages: '@types/node': 18.18.14 '@types/yargs': 17.0.32 chalk: 4.1.2 - dev: true /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} @@ -6377,7 +6827,6 @@ packages: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/trace-mapping': 0.3.22 - dev: true /@jridgewell/resolve-uri@3.1.1: resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} @@ -6386,17 +6835,19 @@ packages: /@jridgewell/set-array@1.1.2: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} - dev: true /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + /@jridgewell/sourcemap-codec@1.5.0: + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + dev: true + /@jridgewell/trace-mapping@0.3.22: resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 - dev: true /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -6442,18 +6893,26 @@ packages: - bufferutil - utf-8-validate - /@layerzerolabs/lz-core@3.0.0: - resolution: {integrity: sha512-GEsIq4OOjWU8gzT2dMxlyN/OpHBH/EQLlX3cG66nRkebjqjscL57bYO+K+QyvCRigFjvhZBj5YOUDcJRUsI1Gw==} + /@layerzerolabs/evm-sdks-core@3.0.59: + resolution: {integrity: sha512-LLzPakU4UVnyakSEi4VpztY5v0eLlcQnrl/aoHjdKjDOzqOlEaoQbb7BZJSmTyNbaVc/5qzgUhyL9aXuYuaBxg==} dependencies: - '@layerzerolabs/lz-foundation': 3.0.0 - '@noble/ed25519': 1.7.3 - '@noble/hashes': 1.4.0 - '@noble/secp256k1': 1.7.1 - tiny-invariant: 1.3.3 + ethers: 5.7.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + + /@layerzerolabs/lz-config-types@3.0.59(ethers@5.7.2): + resolution: {integrity: sha512-V8hcvabSMG/fCA1pt+mkumVwcT1/uWeH4jFqIcC+Elisw4e6C+ALsWBWKRkiOVzi8yFkKaddE9fPrI3iK46vsQ==} + dependencies: + '@layerzerolabs/lz-core': 3.0.59 + '@layerzerolabs/lz-definitions': 3.0.59 + '@safe-global/protocol-kit': 1.3.0(ethers@5.7.2) transitivePeerDependencies: - bufferutil - - debug - encoding + - ethers + - supports-color - utf-8-validate dev: true @@ -6461,12 +6920,16 @@ packages: resolution: {integrity: sha512-4RJzCGkZqwvLiuJp3Eh0b7y23QJRB7aObWGf1aoQYMELo++8ZikFGXdXkt+rH2/uGM9qIqFtdPUKZXppPEUAWA==} dev: true + /@layerzerolabs/lz-core@3.0.59: + resolution: {integrity: sha512-tnhW7bpxAV0GwIN384c9cBS+izSb0nmHVEM10z4FWBg+gFKxM+RZstTxeGWziH57cZWpzUjX1nUZrAGM4VSePg==} + dev: true + /@layerzerolabs/lz-corekit-solana@3.0.0: resolution: {integrity: sha512-vJtDiS7QM77BN/VuZJsM/xH6ZJmUY3vDlHOmRMHsjHf5sfXqMafNL4+FKCRIijPVPGR0DCJARiWrRtgZchv9+w==} dependencies: '@ethersproject/bytes': 5.7.0 - '@layerzerolabs/lz-core': 3.0.0 - '@noble/hashes': 1.4.0 + '@layerzerolabs/lz-core': 3.0.38 + '@noble/hashes': 1.7.0 '@noble/secp256k1': 1.7.1 '@solana/web3.js': 1.95.8 bip39: 3.1.0 @@ -6475,7 +6938,6 @@ packages: tiny-invariant: 1.3.3 transitivePeerDependencies: - bufferutil - - debug - encoding - utf-8-validate dev: true @@ -6793,7 +7255,7 @@ packages: '@layerzerolabs/lz-evm-v1-0.7': 3.0.12(@openzeppelin/contracts-upgradeable@5.1.0)(@openzeppelin/contracts@5.0.2)(hardhat-deploy@0.12.1) '@openzeppelin/contracts': 5.0.2 abi-decoder: 2.4.0 - dotenv: 16.4.5 + dotenv: 16.4.7 ethers: 5.7.2 hardhat-deploy: 0.12.1 solidity-bytes-utils: 0.8.2 @@ -6935,6 +7397,16 @@ packages: - bufferutil - utf-8-validate + /@layerzerolabs/lz-evm-sdk-v2@3.0.59: + resolution: {integrity: sha512-toiiCmk5xEmGN9v1jfd6A/FyXCkOKqVxy/d/j4TrHK5aSIngj+HHkO43JaSxjloiC20bPehFCSR7rutj4QlCrA==} + dependencies: + '@layerzerolabs/evm-sdks-core': 3.0.59 + ethers: 5.7.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + /@layerzerolabs/lz-evm-v1-0.7@3.0.12(@openzeppelin/contracts-upgradeable@4.9.5)(@openzeppelin/contracts@4.9.5)(hardhat-deploy@0.12.4): resolution: {integrity: sha512-7q8NRDwQ8vt+jpj7Pt8H/Q83v8lwa9RLgH2SXva15RQTC5L+ARoEACek9Pk0qLJrA/tKkpiNcn6MxwTJx+he7g==} peerDependencies: @@ -7055,30 +7527,31 @@ packages: hardhat-deploy: 0.12.4 dev: true - /@layerzerolabs/lz-foundation@3.0.0: - resolution: {integrity: sha512-vYuuZAfIaRQUEGaKnV4dCOiFTENhfc2w+Fcnza/T7+O/JIKL2p/GqdxcC21HqrMNdLGMmSWnUqLlou+eCk3ROQ==} + /@layerzerolabs/lz-foundation@3.0.38(typescript@5.5.3): + resolution: {integrity: sha512-0WDrlJ+DZ3GGxKPgcConoxQdTB7icpoOYjeTcwmBtJcUnXIE7G3vxzKUAKsUccGegFdgHMEj1K4D+3NPZbNQ2A==} dependencies: '@layerzerolabs/lz-definitions': 3.0.59 - '@layerzerolabs/lz-utilities': 3.0.0 + '@layerzerolabs/lz-utilities': 3.0.38(typescript@5.5.3) '@noble/ed25519': 1.7.3 - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.7.0 '@noble/secp256k1': 1.7.1 + '@scure/base': 1.2.1 transitivePeerDependencies: - bufferutil - debug - encoding + - typescript - utf-8-validate dev: true - /@layerzerolabs/lz-foundation@3.0.38(typescript@5.5.3): - resolution: {integrity: sha512-0WDrlJ+DZ3GGxKPgcConoxQdTB7icpoOYjeTcwmBtJcUnXIE7G3vxzKUAKsUccGegFdgHMEj1K4D+3NPZbNQ2A==} + /@layerzerolabs/lz-movevm-sdk-v2@3.0.59(typescript@5.5.3): + resolution: {integrity: sha512-qYP5R5nlmO/siy3q68MH3nDnvb0ORzae3KSo2BD3cctNLiEpCjBei+8OcWMo9qZpJDMovNlqtTEaHt1CJmKxBQ==} dependencies: '@layerzerolabs/lz-definitions': 3.0.59 - '@layerzerolabs/lz-utilities': 3.0.38(typescript@5.5.3) - '@noble/ed25519': 1.7.3 - '@noble/hashes': 1.4.0 - '@noble/secp256k1': 1.7.1 - '@scure/base': 1.2.1 + '@layerzerolabs/lz-serdes': 3.0.59 + '@layerzerolabs/lz-utilities': 3.0.59(typescript@5.5.3) + '@layerzerolabs/lz-v2-utilities': 3.0.59 + '@layerzerolabs/move-definitions': 3.0.59 transitivePeerDependencies: - bufferutil - debug @@ -7091,6 +7564,10 @@ packages: resolution: {integrity: sha512-4tUIqlXta70NX+ESISgL22jQXDbEaZsrjErCDbPnpz3byhBU6yynD/baRZiQhiJZ14sowzWZpyiMjYrZhjI4Dg==} dev: true + /@layerzerolabs/lz-serdes@3.0.59: + resolution: {integrity: sha512-frkC1hcuPObhAYlIOmf8zkDWGiqsi+a6cf/+ilxd4SNO66qrMbi0lYNtW1Y62QNSLzYJIshkreOa+f2I6n6PGA==} + dev: true + /@layerzerolabs/lz-solana-sdk-v2@3.0.0(fastestsmallesttextencoderdecoder@1.0.22): resolution: {integrity: sha512-sPvLXeQUO9QLpjOuWE7V+V8yfoI4E/NBYsH9lO2aPx0LYkQa+88ACgPq43B/zFROUD8238WuSb+doGrn3PKtJQ==} dependencies: @@ -7103,7 +7580,7 @@ packages: '@ethersproject/solidity': 5.7.0 '@layerzerolabs/lz-corekit-solana': 3.0.0 '@layerzerolabs/lz-definitions': 3.0.59 - '@layerzerolabs/lz-v2-utilities': 3.0.12 + '@layerzerolabs/lz-v2-utilities': 3.0.38 '@metaplex-foundation/beet': 0.7.2 '@metaplex-foundation/beet-solana': 0.4.1 '@metaplex-foundation/solita': 0.20.1 @@ -7115,7 +7592,6 @@ packages: tiny-invariant: 1.3.3 transitivePeerDependencies: - bufferutil - - debug - encoding - fastestsmallesttextencoderdecoder - supports-color @@ -7163,33 +7639,33 @@ packages: - utf-8-validate dev: true - /@layerzerolabs/lz-utilities@3.0.0: - resolution: {integrity: sha512-vlQXc4BdkNNMrQT0Zp4X20GLss6xwd3rzD0kYI6W3JnhCLrUo03FEZLbliTf6t2F3IgcQf80KyOQKRewMoU+ow==} + /@layerzerolabs/lz-utilities@3.0.38(typescript@5.5.3): + resolution: {integrity: sha512-USVXIpuSUWa2wCra1E+i3WhO9TF61TkXbh+Ey28gYsj1EIephRyWoJRxm6JV8pHSRhRxWEN/zaHxk/JT2ElX2w==} dependencies: '@ethersproject/bytes': 5.7.0 - '@ethersproject/providers': 5.7.2 + '@initia/initia.js': 0.2.24(typescript@5.5.3) '@layerzerolabs/lz-definitions': 3.0.59 - '@noble/hashes': 1.4.0 - '@noble/secp256k1': 1.7.1 '@solana/web3.js': 1.95.8 + '@ton/core': 0.59.0(@ton/crypto@3.3.0) + '@ton/crypto': 3.3.0 + '@ton/ton': 15.1.0(@ton/core@0.59.0)(@ton/crypto@3.3.0) aptos: 1.21.0 bip39: 3.1.0 ed25519-hd-key: 1.3.0 ethers: 5.7.2 find-up: 5.0.0 - glob: 10.3.10 - tree-kill: 1.2.2 + memoizee: 0.4.17 winston: 3.11.0 - zx: 8.1.8 transitivePeerDependencies: - bufferutil - debug - encoding + - typescript - utf-8-validate dev: true - /@layerzerolabs/lz-utilities@3.0.38(typescript@5.5.3): - resolution: {integrity: sha512-USVXIpuSUWa2wCra1E+i3WhO9TF61TkXbh+Ey28gYsj1EIephRyWoJRxm6JV8pHSRhRxWEN/zaHxk/JT2ElX2w==} + /@layerzerolabs/lz-utilities@3.0.59(typescript@5.5.3): + resolution: {integrity: sha512-1TdB+FUpVIkUppioVuZneDLONVlY/aDRcE7MlC1jEmQIpKA9IgPk0y8wejgEDSRtaljnVjo/g945ZPwjPyPmdw==} dependencies: '@ethersproject/bytes': 5.7.0 '@initia/initia.js': 0.2.24(typescript@5.5.3) @@ -7202,7 +7678,6 @@ packages: bip39: 3.1.0 ed25519-hd-key: 1.3.0 ethers: 5.7.2 - find-up: 5.0.0 memoizee: 0.4.17 winston: 3.11.0 transitivePeerDependencies: @@ -7238,12 +7713,31 @@ packages: tiny-invariant: 1.3.3 dev: true + /@layerzerolabs/lz-v2-utilities@3.0.59: + resolution: {integrity: sha512-/g92XaltJSubYGiFY3Ibr8ImmrmjjSsOLa96+yCJqk+Urp7Q0dgwAfDWV3TCwpUJqL1QCQjlE1GdaqgqGMqzDw==} + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/solidity': 5.7.0 + bs58: 5.0.0 + tiny-invariant: 1.3.3 + dev: true + + /@layerzerolabs/move-definitions@3.0.59: + resolution: {integrity: sha512-6cuvePiKpZ7OOgZEbX7keE0bQM3+HGzr7suqfb1LSYwfpcZKYICnB5fnbwXyC1Wa3qUx+/9ZlnhckKvUoUiwAA==} + dependencies: + '@layerzerolabs/lz-definitions': 3.0.59 + dev: true + /@layerzerolabs/oft-v2-solana-sdk@3.0.0(fastestsmallesttextencoderdecoder@1.0.22): resolution: {integrity: sha512-fiamhnEafuf83B45mDwj17rvp1BLFUmYGJEtzjp+nkKV6ZVwQwdtCXocq/5Kh8BbflEXmvpkHP+KxCHremFw6g==} dependencies: '@ethersproject/bytes': 5.7.0 '@layerzerolabs/lz-solana-sdk-v2': 3.0.0(fastestsmallesttextencoderdecoder@1.0.22) - '@layerzerolabs/lz-v2-utilities': 3.0.12 + '@layerzerolabs/lz-v2-utilities': 3.0.38 '@metaplex-foundation/beet': 0.7.2 '@metaplex-foundation/beet-solana': 0.4.1 '@metaplex-foundation/umi': 0.9.2 @@ -7252,10 +7746,9 @@ packages: '@metaplex-foundation/umi-web3js-adapters': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.8) '@solana/web3.js': 1.95.8 bn.js: 5.2.1 - dotenv: 16.4.5 + dotenv: 16.4.7 transitivePeerDependencies: - bufferutil - - debug - encoding - fastestsmallesttextencoderdecoder - supports-color @@ -7277,7 +7770,7 @@ packages: '@metaplex-foundation/umi-web3js-adapters': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.8) '@solana/web3.js': 1.95.8 bn.js: 5.2.1 - dotenv: 16.4.5 + dotenv: 16.4.7 transitivePeerDependencies: - bufferutil - debug @@ -7304,6 +7797,26 @@ packages: - typescript dev: true + /@layerzerolabs/test-devtools-evm-foundry@5.1.0(@layerzerolabs/lz-evm-messagelib-v2@3.0.12)(@layerzerolabs/lz-evm-protocol-v2@3.0.12)(@layerzerolabs/lz-evm-v1-0.7@3.0.15)(@layerzerolabs/oapp-evm@packages+oapp-evm)(@layerzerolabs/oft-evm@packages+oft-evm)(@openzeppelin/contracts-upgradeable@5.1.0)(@openzeppelin/contracts@5.1.0): + resolution: {integrity: sha512-LDfBMeb5VQRMRXadpNgUe5Knib5FdCQ24S2o+kWMS/qPS6ZbPGe7fnhFPhcifautuhMS8i9Rk7cMjoAxRGFdfQ==} + peerDependencies: + '@layerzerolabs/lz-evm-messagelib-v2': ^3.0.12 + '@layerzerolabs/lz-evm-protocol-v2': ^3.0.12 + '@layerzerolabs/lz-evm-v1-0.7': ^3.0.12 + '@layerzerolabs/oapp-evm': ^0.3.0 + '@layerzerolabs/oft-evm': ^3.0.0 + '@openzeppelin/contracts': ^4.9.5 || ^5.0.0 + '@openzeppelin/contracts-upgradeable': ^4.9.5 || ^5.0.0 + dependencies: + '@layerzerolabs/lz-evm-messagelib-v2': 3.0.12(@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.12)(@layerzerolabs/lz-evm-v1-0.7@3.0.15)(@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': 3.0.12(@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': 3.0.15(@openzeppelin/contracts-upgradeable@5.1.0)(@openzeppelin/contracts@5.1.0)(hardhat-deploy@0.12.4) + '@layerzerolabs/oapp-evm': link:packages/oapp-evm + '@layerzerolabs/oft-evm': link:packages/oft-evm + '@openzeppelin/contracts': 5.1.0 + '@openzeppelin/contracts-upgradeable': 5.1.0(@openzeppelin/contracts@5.1.0) + dev: true + /@ledgerhq/devices@8.4.4: resolution: {integrity: sha512-sz/ryhe/R687RHtevIE9RlKaV8kkKykUV4k29e7GAVwzHX1gqG+O75cu1NCJUHLbp3eABV5FdvZejqRUlLis9A==} dependencies: @@ -7427,7 +7940,7 @@ packages: fs-extra: 11.2.0 hardhat: 2.22.12(ts-node@10.9.2)(typescript@5.5.3) proper-lockfile: 4.1.2 - semver: 7.6.2 + semver: 7.6.3 sinon: 18.0.1 sinon-chai: 3.7.0(chai@4.5.0)(sinon@18.0.1) undici: 6.20.1 @@ -7562,7 +8075,7 @@ packages: resolution: {integrity: sha512-m0wkRBEQB/8krwMwKBvFugufZtYwMXiGHud2cTDAv+aGXK4M90y0Hx67/wpu+AqqoQfdV8VM9YezUOHKD+Z5kA==} dependencies: debug: 4.3.7 - semver: 7.6.2 + semver: 7.6.3 text-table: 0.2.0 toml: 3.0.0 transitivePeerDependencies: @@ -7627,7 +8140,7 @@ packages: dependencies: '@metaplex-foundation/umi': 0.9.2 '@metaplex-foundation/umi-web3js-adapters': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.8) - '@noble/curves': 1.4.2 + '@noble/curves': 1.8.0 '@solana/web3.js': 1.95.8 dev: true @@ -7767,6 +8280,13 @@ packages: '@noble/hashes': 1.7.0 dev: true + /@noble/curves@1.8.1: + resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} + engines: {node: ^14.21.3 || >=16} + dependencies: + '@noble/hashes': 1.7.1 + dev: true + /@noble/ed25519@1.7.3: resolution: {integrity: sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==} dev: true @@ -7788,6 +8308,11 @@ packages: engines: {node: ^14.21.3 || >=16} dev: true + /@noble/hashes@1.7.1: + resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} + engines: {node: ^14.21.3 || >=16} + dev: true + /@noble/secp256k1@1.7.1: resolution: {integrity: sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==} @@ -8520,7 +9045,6 @@ packages: - ethers - supports-color - utf-8-validate - dev: false /@safe-global/safe-core-sdk-types@2.3.0: resolution: {integrity: sha512-dU0KkDV1KJNf11ajbUjWiSi4ygdyWfhk1M50lTJWUdCn1/2Bsb/hICM8LoEk6DCoFumxaoCet02SmYakXsW2CA==} @@ -8546,6 +9070,10 @@ packages: resolution: {integrity: sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ==} dev: true + /@scure/base@1.2.4: + resolution: {integrity: sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==} + dev: true + /@scure/bip32@1.1.5: resolution: {integrity: sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==} dependencies: @@ -8560,6 +9088,14 @@ packages: '@noble/hashes': 1.3.3 '@scure/base': 1.1.5 + /@scure/bip32@1.6.2: + resolution: {integrity: sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==} + dependencies: + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/base': 1.2.4 + dev: true + /@scure/bip39@1.1.1: resolution: {integrity: sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==} dependencies: @@ -8579,6 +9115,13 @@ packages: '@noble/hashes': 1.3.3 '@scure/base': 1.1.5 + /@scure/bip39@1.5.4: + resolution: {integrity: sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==} + dependencies: + '@noble/hashes': 1.7.1 + '@scure/base': 1.2.4 + dev: true + /@sentry/core@5.30.0: resolution: {integrity: sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==} engines: {node: '>=6'} @@ -8644,7 +9187,6 @@ packages: /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - dev: true /@sindresorhus/is@4.6.0: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} @@ -8665,13 +9207,11 @@ packages: resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} dependencies: type-detect: 4.0.8 - dev: true /@sinonjs/fake-timers@10.3.0: resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} dependencies: '@sinonjs/commons': 3.0.1 - dev: true /@sinonjs/fake-timers@11.2.2: resolution: {integrity: sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==} @@ -8714,7 +9254,7 @@ packages: '@solana/spl-token': 0.4.8(@solana/web3.js@1.95.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) '@solana/web3.js': 1.95.8 bs58: 6.0.0 - dotenv: 16.4.5 + dotenv: 16.4.7 transitivePeerDependencies: - bufferutil - encoding @@ -9259,6 +9799,10 @@ packages: resolution: {integrity: sha512-/2B0nQF4UdupuxeKTJA2+Rj1D+uDemo6P4kMwKCpbfpnzeVaWSELTsAw4Lxn3VJD6APtRrZOCuYo+4nHUQfTfg==} dev: true + /@types/argparse@2.0.17: + resolution: {integrity: sha512-fueJssTf+4dW4HODshEGkIZbkLKHzgu1FvCI4cTc/MKum/534Euo3SrN+ilq8xgyHnOjtmg33/hee8iXLRg1XA==} + dev: true + /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: @@ -9267,26 +9811,22 @@ packages: '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.5 - dev: true /@types/babel__generator@7.6.8: resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} dependencies: '@babel/types': 7.23.9 - dev: true /@types/babel__template@7.4.4: resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} dependencies: '@babel/parser': 7.23.9 '@babel/types': 7.23.9 - dev: true /@types/babel__traverse@7.20.5: resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} dependencies: '@babel/types': 7.23.9 - dev: true /@types/bn.js@4.11.6: resolution: {integrity: sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==} @@ -9318,7 +9858,6 @@ packages: /@types/chai@4.3.20: resolution: {integrity: sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==} - dev: true /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -9330,20 +9869,10 @@ packages: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true - /@types/fs-extra@11.0.4: - resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} - requiresBuild: true - dependencies: - '@types/jsonfile': 6.1.4 - '@types/node': 18.18.14 - dev: true - optional: true - /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: '@types/node': 18.18.14 - dev: true /@types/http-cache-semantics@4.0.4: resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} @@ -9356,19 +9885,16 @@ packages: /@types/istanbul-lib-coverage@2.0.6: resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - dev: true /@types/istanbul-lib-report@3.0.3: resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} dependencies: '@types/istanbul-lib-coverage': 2.0.6 - dev: true /@types/istanbul-reports@3.0.4: resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} dependencies: '@types/istanbul-lib-report': 3.0.3 - dev: true /@types/jest@29.5.12: resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} @@ -9385,14 +9911,6 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true - /@types/jsonfile@6.1.4: - resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} - requiresBuild: true - dependencies: - '@types/node': 18.18.14 - dev: true - optional: true - /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: @@ -9413,6 +9931,10 @@ packages: /@types/lru-cache@5.1.1: resolution: {integrity: sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==} + /@types/minimatch@3.0.5: + resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + dev: true + /@types/minimist@1.2.5: resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} dev: true @@ -9446,18 +9968,14 @@ packages: dependencies: undici-types: 5.26.5 - /@types/node@22.9.0: - resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} - requiresBuild: true - dependencies: - undici-types: 6.19.8 - dev: true - optional: true - /@types/normalize-package-data@2.4.4: resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} dev: true + /@types/parse-json@4.0.2: + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + dev: true + /@types/pbkdf2@3.1.2: resolution: {integrity: sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==} dependencies: @@ -9536,7 +10054,6 @@ packages: /@types/stack-utils@2.0.3: resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - dev: true /@types/tinycolor2@1.4.6: resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} @@ -9566,13 +10083,11 @@ packages: /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - dev: true /@types/yargs@17.0.32: resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} dependencies: '@types/yargs-parser': 21.0.3 - dev: true /@types/yoga-layout@1.9.2: resolution: {integrity: sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==} @@ -9687,7 +10202,7 @@ packages: debug: 4.3.7 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.6.2 + semver: 7.6.3 tsutils: 3.21.0(typescript@5.5.3) typescript: 5.5.3 transitivePeerDependencies: @@ -9709,7 +10224,7 @@ packages: globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.2 + semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.5.3) typescript: 5.5.3 transitivePeerDependencies: @@ -9730,7 +10245,7 @@ packages: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.5.3) eslint: 8.57.0 eslint-scope: 5.1.1 - semver: 7.6.2 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -9750,7 +10265,7 @@ packages: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.5.3) eslint: 8.57.1 eslint-scope: 5.1.1 - semver: 7.6.2 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -9821,6 +10336,48 @@ packages: base64-sol: 1.0.1 dev: true + /@vue/compiler-core@3.5.13: + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + dependencies: + '@babel/parser': 7.26.7 + '@vue/shared': 3.5.13 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + dev: true + + /@vue/compiler-dom@3.5.13: + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + dependencies: + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 + dev: true + + /@vue/compiler-sfc@3.5.13: + resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + dependencies: + '@babel/parser': 7.26.7 + '@vue/compiler-core': 3.5.13 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.1 + source-map-js: 1.2.1 + dev: true + + /@vue/compiler-ssr@3.5.13: + resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/shared': 3.5.13 + dev: true + + /@vue/shared@3.5.13: + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + dev: true + /JSONStream@1.3.2: resolution: {integrity: sha512-mn0KSip7N4e0UDPZHnqDsHECo5uGQrixQKnAskOM1BIB8hd7QKbd6il8IPRPudPHOeHiECoCFqhyMaRO9+nWyA==} hasBin: true @@ -9877,7 +10434,6 @@ packages: dependencies: mime-types: 2.1.35 negotiator: 0.6.3 - dev: false /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -10008,7 +10564,6 @@ packages: /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - dev: true /ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} @@ -10061,7 +10616,6 @@ packages: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: sprintf-js: 1.0.3 - dev: true /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -10088,9 +10642,13 @@ packages: is-array-buffer: 3.0.2 dev: true + /array-differ@3.0.0: + resolution: {integrity: sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==} + engines: {node: '>=8'} + dev: true + /array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - dev: false /array-includes@3.1.7: resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} @@ -10167,6 +10725,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + dev: true + /asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} dependencies: @@ -10188,7 +10751,6 @@ packages: /assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - dev: true /ast-metadata-inferer@0.8.0: resolution: {integrity: sha512-jOMKcHht9LxYIEQu+RVd22vtgrPaVCtDRQ/16IGmurdzxvYbDd5ynxjnyrzLnieG96eTcAyaoj/wN/4/1FyyeA==} @@ -10212,7 +10774,6 @@ packages: /async-limiter@1.0.1: resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} - dev: false /async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} @@ -10321,7 +10882,6 @@ packages: slash: 3.0.0 transitivePeerDependencies: - supports-color - dev: true /babel-plugin-istanbul@6.1.1: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} @@ -10334,7 +10894,6 @@ packages: test-exclude: 6.0.0 transitivePeerDependencies: - supports-color - dev: true /babel-plugin-jest-hoist@29.6.3: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} @@ -10344,7 +10903,6 @@ packages: '@babel/types': 7.23.9 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.5 - dev: true /babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.9): resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} @@ -10364,7 +10922,6 @@ packages: '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.9) '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.9) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.9) - dev: true /babel-preset-jest@29.6.3(@babel/core@7.23.9): resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} @@ -10375,7 +10932,6 @@ packages: '@babel/core': 7.23.9 babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.9) - dev: true /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -10442,7 +10998,7 @@ packages: resolution: {integrity: sha512-5hVFGrdCnF8GB1Lj2eEo4PRE7+jp+3xBLnfNjydivOkMvKmUKeJ9GG8uOy8prmWl3Oh154uzgfudR1FRkNBudA==} engines: {node: '>=18.0.0'} dependencies: - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.7.0 '@scure/base': 1.2.1 uint8array-tools: 0.0.8 valibot: 0.37.0(typescript@5.5.3) @@ -10463,7 +11019,7 @@ packages: /bip39@3.1.0: resolution: {integrity: sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==} dependencies: - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.7.0 dev: true /bl@1.2.3: @@ -10514,7 +11070,6 @@ packages: unpipe: 1.0.0 transitivePeerDependencies: - supports-color - dev: false /body-parser@1.20.2: resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} @@ -10534,7 +11089,6 @@ packages: unpipe: 1.0.0 transitivePeerDependencies: - supports-color - dev: false /borsh@0.7.0: resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} @@ -10609,7 +11163,12 @@ packages: electron-to-chromium: 1.4.641 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.22.2) - dev: true + + /bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + dependencies: + fast-json-stable-stringify: 2.1.0 /bs58@4.0.1: resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} @@ -10636,7 +11195,7 @@ packages: /bs58check@4.0.0: resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} dependencies: - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.7.0 bs58: 6.0.0 dev: true @@ -10644,7 +11203,6 @@ packages: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} dependencies: node-int64: 0.4.0 - dev: true /buffer-alloc-unsafe@1.1.0: resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} @@ -10671,7 +11229,6 @@ packages: /buffer-to-arraybuffer@0.0.5: resolution: {integrity: sha512-3dthu5CYiVB1DEJp61FtApNnNndTckcqe4pFcLdvHtrpG+kcyekCJKg4MRiDcFW7A6AODnXB9U4dwQiCW5kzJQ==} - dev: false /buffer-xor@1.0.3: resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} @@ -10768,7 +11325,6 @@ packages: /cacheable-lookup@6.1.0: resolution: {integrity: sha512-KJ/Dmo1lDDhmW2XDPMo+9oiy/CeqosPguPCrgcVzKyZrL6pM1gU2GmPY/xo6OQPTUaA/c0kwHuywB4E6nmT9ww==} engines: {node: '>=10.6.0'} - dev: false /cacheable-lookup@7.0.0: resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} @@ -10817,10 +11373,13 @@ packages: get-intrinsic: 1.2.4 set-function-length: 1.2.1 + /callsite@1.0.0: + resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} + dev: true + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - dev: true /camelcase-keys@6.2.2: resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} @@ -10839,7 +11398,6 @@ packages: /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - dev: true /camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} @@ -10847,7 +11405,6 @@ packages: /caniuse-lite@1.0.30001579: resolution: {integrity: sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==} - dev: true /caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -10890,7 +11447,6 @@ packages: loupe: 2.3.7 pathval: 1.1.1 type-detect: 4.1.0 - dev: true /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -10915,7 +11471,6 @@ packages: /char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} - dev: true /chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} @@ -10925,7 +11480,6 @@ packages: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} dependencies: get-func-name: 2.0.2 - dev: true /chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} @@ -10976,7 +11530,6 @@ packages: /ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} - dev: true /cids@0.7.5: resolution: {integrity: sha512-zT7mPeghoWAu+ppn8+BS1tQ5qGmbMfB4AregnQjA/qHY3GC1m1ptI9GkWNlgeu38r7CuRdXB47uY2XgAYt6QVA==} @@ -10988,7 +11541,6 @@ packages: multibase: 0.6.1 multicodec: 1.0.4 multihashes: 0.4.21 - dev: false /cipher-base@1.0.4: resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} @@ -10998,11 +11550,9 @@ packages: /cjs-module-lexer@1.2.3: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} - dev: true /class-is@1.1.0: resolution: {integrity: sha512-rhjH9AG1fvabIDoGRVH587413LPjTZgmDF9fOFCbFJQV4yuocX1mHxxvXI4g3cGwbVY9wAYIoKlg1N79frJKQw==} - dev: false /clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} @@ -11084,7 +11634,6 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - dev: true /clone-response@1.0.3: resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} @@ -11099,7 +11648,6 @@ packages: /co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - dev: true /code-block-writer@12.0.0: resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==} @@ -11118,7 +11666,6 @@ packages: /collect-v8-coverage@1.0.2: resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} - dev: true /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -11268,7 +11815,6 @@ packages: engines: {node: '>= 0.6'} dependencies: safe-buffer: 5.2.1 - dev: false /content-hash@2.5.2: resolution: {integrity: sha512-FvIQKy0S1JaWV10sMsA7TRx8bpU+pqPkhbsfvOJAdjRXvYxEckAwQWGwtRjiaJfh+E0DvcWUGqcdjwMGFjsSdw==} @@ -11276,16 +11822,13 @@ packages: cids: 0.7.5 multicodec: 0.5.7 multihashes: 0.4.21 - dev: false /content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - dev: false /convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - dev: true /convert-to-spaces@1.0.2: resolution: {integrity: sha512-cj09EBuObp9gZNQCzc7hByQyrs6jVGE+o9kSJmeUoj+GiPiJvi5LYqEH/Hmme4+MTLHM+Ejtq+FChpjjEnsPdQ==} @@ -11293,7 +11836,6 @@ packages: /cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - dev: false /cookie@0.4.2: resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} @@ -11302,7 +11844,6 @@ packages: /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} - dev: false /core-js-pure@3.39.0: resolution: {integrity: sha512-7fEcWwKI4rJinnK+wLTezeg2smbFFdSBP6E2kQZNbnzM2s1rpKQ6aaRteZSSg7FLU3P0HGGVo/gbpfanU36urg==} @@ -11318,7 +11859,17 @@ packages: dependencies: object-assign: 4.1.1 vary: 1.1.2 - dev: false + + /cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: true /cosmiconfig@8.3.6(typescript@5.5.3): resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} @@ -11387,7 +11938,6 @@ packages: - babel-plugin-macros - supports-color - ts-node - dev: true /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -11422,7 +11972,6 @@ packages: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - dev: true /cross-spawn@7.0.5: resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} @@ -11554,14 +12103,12 @@ packages: /decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} - dev: false /decompress-response@3.3.0: resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} engines: {node: '>=4'} dependencies: mimic-response: 1.0.1 - dev: false /decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} @@ -11576,7 +12123,6 @@ packages: peerDependenciesMeta: babel-plugin-macros: optional: true - dev: true /deep-eql@4.1.3: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} @@ -11590,7 +12136,6 @@ packages: engines: {node: '>=6'} dependencies: type-detect: 4.1.0 - dev: true /deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} @@ -11604,7 +12149,6 @@ packages: /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - dev: true /defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -11659,14 +12203,54 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + /depcheck@1.4.7: + resolution: {integrity: sha512-1lklS/bV5chOxwNKA/2XUUk/hPORp8zihZsXflr8x0kLwmcZ9Y9BsS6Hs3ssvA+2wUVbG0U2Ciqvm1SokNjPkA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@babel/parser': 7.23.9 + '@babel/traverse': 7.23.9 + '@vue/compiler-sfc': 3.5.13 + callsite: 1.0.0 + camelcase: 6.3.0 + cosmiconfig: 7.1.0 + debug: 4.3.7 + deps-regex: 0.2.0 + findup-sync: 5.0.0 + ignore: 5.3.2 + is-core-module: 2.13.1 + js-yaml: 3.14.1 + json5: 2.2.3 + lodash: 4.17.21 + minimatch: 7.4.6 + multimatch: 5.0.0 + please-upgrade-node: 3.2.0 + readdirp: 3.6.0 + require-package-name: 2.0.1 + resolve: 1.22.8 + resolve-from: 5.0.0 + semver: 7.6.3 + yargs: 16.2.0 + transitivePeerDependencies: + - supports-color + dev: true + /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + /deps-regex@0.2.0: + resolution: {integrity: sha512-PwuBojGMQAYbWkMXOY9Pd/NWCDNHVH12pnS7WHqZkTSeMESe4hwnKKRp0yR87g37113x4JPbo/oIvXY+s/f56Q==} + dev: true + /destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dev: false + + /detect-file@1.0.0: + resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} + engines: {node: '>=0.10.0'} + dev: true /detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} @@ -11681,7 +12265,6 @@ packages: /detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} - dev: true /detect-newline@4.0.1: resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==} @@ -11691,7 +12274,6 @@ packages: /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true /diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} @@ -11780,7 +12362,6 @@ packages: /dom-walk@0.1.2: resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} - dev: false /dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} @@ -11804,6 +12385,11 @@ packages: engines: {node: '>=12'} dev: true + /dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + dev: true + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true @@ -11823,11 +12409,16 @@ packages: /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - dev: false + + /ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + jake: 10.9.2 /electron-to-chromium@1.4.641: resolution: {integrity: sha512-JetAF3M5Lr9hwzDe3oMmWFOydlclqt2loEljxc0AAP5NYM170sSW+F5/cn5ROBfjx5LdmzeeAgWnyAU9cjPhmA==} - dev: true /elliptic@6.5.4: resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} @@ -11850,7 +12441,6 @@ packages: inherits: 2.0.4 minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 - dev: true /emittery@0.10.0: resolution: {integrity: sha512-AGvFfs+d0JKCJQ4o01ASQLGPmSCxgfU9RFXvzPvZdjKK8oscynksuJhWrSTSw7j7Ep/sZct5b5ZhYCi8S/t0HQ==} @@ -11860,7 +12450,6 @@ packages: /emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} - dev: true /emoji-regex@10.3.0: resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} @@ -11883,7 +12472,6 @@ packages: /encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} - dev: false /encoding-down@6.3.0: resolution: {integrity: sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==} @@ -11923,6 +12511,11 @@ packages: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: true + /env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -11938,7 +12531,6 @@ packages: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: is-arrayish: 0.2.1 - dev: true /es-abstract@1.22.3: resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} @@ -12111,7 +12703,6 @@ packages: '@esbuild/win32-arm64': 0.19.11 '@esbuild/win32-ia32': 0.19.11 '@esbuild/win32-x64': 0.19.11 - dev: true /esbuild@0.20.2: resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} @@ -12150,7 +12741,6 @@ packages: /escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - dev: false /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} @@ -12315,7 +12905,7 @@ packages: eslint: 8.57.1 find-up: 5.0.0 lodash.memoize: 4.1.2 - semver: 7.6.2 + semver: 7.6.3 dev: true /eslint-plugin-es-x@7.5.0(eslint@8.57.1): @@ -12692,7 +13282,6 @@ packages: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - dev: true /esquery@1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} @@ -12725,6 +13314,10 @@ packages: engines: {node: '>=4.0'} dev: true + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -12733,7 +13326,6 @@ packages: /etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - dev: false /eth-ens-namehash@2.0.8: resolution: {integrity: sha512-VWEI1+KJfz4Km//dadyvBBoBeSQ0MHTXPvr8UIXiLW6IanxvAV+DmlZAijZwAyggqGUfwQBeHf7tc9wzc1piSw==} @@ -12745,7 +13337,7 @@ packages: resolution: {integrity: sha512-bfttrr3/7gG4E02HoWTDUcDDslN003OlOoBxk9virpAZQ1ja/jDgwkWB8QfJF7ojuEowrqy+lzp9VcJG7/k5bQ==} dependencies: bn.js: 4.12.0 - elliptic: 6.5.4 + elliptic: 6.6.1 nano-json-stream-parser: 0.1.2 servify: 0.1.12 ws: 3.3.3 @@ -12754,7 +13346,6 @@ packages: - bufferutil - supports-color - utf-8-validate - dev: false /eth-lib@0.2.8: resolution: {integrity: sha512-ArJ7x1WcWOlSpzdoTBX8vkwlkSQ85CjjifSZtV4co64vWxSV8geWfPI9x4SVYu3DSxnX4yWFVTtGL+j9DUFLNw==} @@ -12762,7 +13353,6 @@ packages: bn.js: 4.12.0 elliptic: 6.5.4 xhr-request-promise: 0.1.3 - dev: false /ethereum-bloom-filters@1.0.10: resolution: {integrity: sha512-rxJ5OFN3RwjQxDcFP2Z5+Q9ho4eIdEmSc2ht0fCu8Se9nbXjZ7/031uXoUYJ87KHCOdVeiUuwSnoS7hmYAGVHA==} @@ -12961,13 +13551,12 @@ packages: onetime: 5.1.2 signal-exit: 3.0.7 strip-final-newline: 2.0.0 - dev: true /execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.5 get-stream: 8.0.1 human-signals: 5.0.0 is-stream: 3.0.0 @@ -12981,6 +13570,12 @@ packages: /exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} + + /expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + dependencies: + homedir-polyfill: 1.0.3 dev: true /expect@29.7.0: @@ -12992,7 +13587,6 @@ packages: jest-matcher-utils: 29.7.0 jest-message-util: 29.7.0 jest-util: 29.7.0 - dev: true /exponential-backoff@3.1.1: resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} @@ -13034,7 +13628,6 @@ packages: vary: 1.1.2 transitivePeerDependencies: - supports-color - dev: false /ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} @@ -13127,7 +13720,6 @@ packages: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} dependencies: bser: 2.1.1 - dev: true /fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} @@ -13150,6 +13742,11 @@ packages: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} dev: true + /filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + dependencies: + minimatch: 5.1.6 + /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -13169,7 +13766,6 @@ packages: unpipe: 1.0.0 transitivePeerDependencies: - supports-color - dev: false /find-process@1.4.7: resolution: {integrity: sha512-/U4CYp1214Xrp3u3Fqr9yNynUrr5Le4y0SsJh2lMDDSbpwYSz3M2SMWQC+wqcx79cN8PQtHQIL8KnuY9M66fdg==} @@ -13209,7 +13805,6 @@ packages: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 - dev: true /find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} @@ -13225,6 +13820,16 @@ packages: pkg-dir: 4.2.0 dev: true + /findup-sync@5.0.0: + resolution: {integrity: sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==} + engines: {node: '>= 10.13.0'} + dependencies: + detect-file: 1.0.0 + is-glob: 4.0.3 + micromatch: 4.0.5 + resolve-dir: 1.0.1 + dev: true + /flat-cache@3.2.0: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} @@ -13319,7 +13924,6 @@ packages: /form-data-encoder@1.7.1: resolution: {integrity: sha512-EFRDrsMm/kyqbTQocNvRXMLjc7Es2Vk+IQFx/YW7hkUH1eBl4J1fqiP34l74Yt0pFLCNpc06fkbVk00008mzjg==} - dev: false /form-data-encoder@2.1.4: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} @@ -13354,7 +13958,6 @@ packages: /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - dev: false /fp-ts@1.19.3: resolution: {integrity: sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==} @@ -13365,7 +13968,6 @@ packages: /fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} - dev: false /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -13405,7 +14007,6 @@ packages: graceful-fs: 4.2.11 jsonfile: 4.0.0 universalify: 0.1.2 - dev: false /fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} @@ -13428,7 +14029,6 @@ packages: resolution: {integrity: sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==} dependencies: minipass: 2.9.0 - dev: false /fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} @@ -13492,7 +14092,6 @@ packages: /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} - dev: true /get-caller-file@1.0.3: resolution: {integrity: sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==} @@ -13509,7 +14108,6 @@ packages: /get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - dev: true /get-intrinsic@1.2.2: resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} @@ -13532,7 +14130,6 @@ packages: /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} - dev: true /get-stdin@9.0.0: resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} @@ -13654,7 +14251,6 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true /glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} @@ -13667,17 +14263,35 @@ packages: once: 1.4.0 dev: true + /global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + dev: true + + /global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + dev: true + /global@4.4.0: resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} dependencies: min-document: 2.19.0 process: 0.11.10 - dev: false /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} - dev: true /globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} @@ -13766,7 +14380,6 @@ packages: lowercase-keys: 3.0.0 p-cancelable: 3.0.0 responselike: 2.0.1 - dev: false /got@12.6.1: resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} @@ -14097,13 +14710,19 @@ packages: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + /homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + dependencies: + parse-passwd: 1.0.0 + dev: true + /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true /html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - dev: true /http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} @@ -14169,7 +14788,6 @@ packages: /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - dev: true /human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} @@ -14244,7 +14862,6 @@ packages: dependencies: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 - dev: true /imul@1.0.1: resolution: {integrity: sha512-WFAgfwPLAjU66EKt6vRdTlKj4nAgIDQzh29JonLa4Bqtl6D8JrIMvWjCnx7xEjVNmP3U0fM5o8ZObk7d0f62bA==} @@ -14254,7 +14871,6 @@ packages: /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - dev: true /indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} @@ -14374,6 +14990,11 @@ packages: - bufferutil - utf-8-validate + /install@0.13.0: + resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==} + engines: {node: '>= 0.10'} + dev: true + /internal-slot@1.0.6: resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} engines: {node: '>= 0.4'} @@ -14402,7 +15023,6 @@ packages: /ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - dev: false /is-arguments@1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} @@ -14421,7 +15041,6 @@ packages: /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: true /is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} @@ -14474,7 +15093,6 @@ packages: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: hasown: 2.0.0 - dev: true /is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} @@ -14518,12 +15136,10 @@ packages: /is-function@1.0.2: resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==} - dev: false /is-generator-fn@2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} - dev: true /is-generator-function@1.0.10: resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} @@ -14697,7 +15313,6 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true /isexe@3.1.1: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} @@ -14727,7 +15342,6 @@ packages: /istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} - dev: true /istanbul-lib-instrument@5.2.1: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} @@ -14740,7 +15354,6 @@ packages: semver: 6.3.1 transitivePeerDependencies: - supports-color - dev: true /istanbul-lib-instrument@6.0.1: resolution: {integrity: sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==} @@ -14750,10 +15363,9 @@ packages: '@babel/parser': 7.23.9 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.6.2 + semver: 7.6.3 transitivePeerDependencies: - supports-color - dev: true /istanbul-lib-report@3.0.1: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} @@ -14762,7 +15374,6 @@ packages: istanbul-lib-coverage: 3.2.2 make-dir: 4.0.0 supports-color: 7.2.0 - dev: true /istanbul-lib-source-maps@4.0.1: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} @@ -14773,7 +15384,6 @@ packages: source-map: 0.6.1 transitivePeerDependencies: - supports-color - dev: true /istanbul-reports@3.1.6: resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} @@ -14781,7 +15391,6 @@ packages: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - dev: true /iterator.prototype@1.1.2: resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} @@ -14811,6 +15420,16 @@ packages: '@pkgjs/parseargs': 0.11.0 dev: true + /jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + async: 3.2.5 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + /jayson@4.1.1: resolution: {integrity: sha512-5ZWm4Q/0DHPyeMfAsrwViwUS2DMVsQgWh8bEEIVTkfb3DzHZ2L3G5WUnF+AKmGjjM9r1uAv73SaqC1/U4RL45w==} engines: {node: '>=8'} @@ -14840,7 +15459,6 @@ packages: execa: 5.1.1 jest-util: 29.7.0 p-limit: 3.1.0 - dev: true /jest-circus@29.7.0: resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} @@ -14869,7 +15487,6 @@ packages: transitivePeerDependencies: - babel-plugin-macros - supports-color - dev: true /jest-cli@29.7.0(@types/node@18.18.14)(ts-node@10.9.2): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} @@ -14897,7 +15514,6 @@ packages: - babel-plugin-macros - supports-color - ts-node - dev: true /jest-config@29.7.0(@types/node@18.18.14)(ts-node@10.9.2): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} @@ -14938,7 +15554,6 @@ packages: transitivePeerDependencies: - babel-plugin-macros - supports-color - dev: true /jest-diff@29.7.0: resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} @@ -14948,14 +15563,12 @@ packages: diff-sequences: 29.6.3 jest-get-type: 29.6.3 pretty-format: 29.7.0 - dev: true /jest-docblock@29.7.0: resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: detect-newline: 3.1.0 - dev: true /jest-each@29.7.0: resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} @@ -14966,7 +15579,6 @@ packages: jest-get-type: 29.6.3 jest-util: 29.7.0 pretty-format: 29.7.0 - dev: true /jest-environment-node@29.7.0: resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} @@ -14978,7 +15590,6 @@ packages: '@types/node': 18.18.14 jest-mock: 29.7.0 jest-util: 29.7.0 - dev: true /jest-extended@4.0.2(jest@29.7.0): resolution: {integrity: sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==} @@ -14997,7 +15608,6 @@ packages: /jest-get-type@29.6.3: resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true /jest-haste-map@29.7.0: resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} @@ -15016,7 +15626,6 @@ packages: walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 - dev: true /jest-leak-detector@29.7.0: resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} @@ -15024,7 +15633,6 @@ packages: dependencies: jest-get-type: 29.6.3 pretty-format: 29.7.0 - dev: true /jest-matcher-utils@29.7.0: resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} @@ -15034,7 +15642,6 @@ packages: jest-diff: 29.7.0 jest-get-type: 29.6.3 pretty-format: 29.7.0 - dev: true /jest-message-util@29.7.0: resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} @@ -15049,7 +15656,6 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 - dev: true /jest-mock@29.7.0: resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} @@ -15058,7 +15664,6 @@ packages: '@jest/types': 29.6.3 '@types/node': 18.18.14 jest-util: 29.7.0 - dev: true /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} @@ -15070,12 +15675,10 @@ packages: optional: true dependencies: jest-resolve: 29.7.0 - dev: true /jest-regex-util@29.6.3: resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true /jest-resolve-dependencies@29.7.0: resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} @@ -15085,7 +15688,6 @@ packages: jest-snapshot: 29.7.0 transitivePeerDependencies: - supports-color - dev: true /jest-resolve@29.7.0: resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} @@ -15100,7 +15702,6 @@ packages: resolve: 1.22.8 resolve.exports: 2.0.2 slash: 3.0.0 - dev: true /jest-runner@29.7.0: resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} @@ -15129,7 +15730,6 @@ packages: source-map-support: 0.5.13 transitivePeerDependencies: - supports-color - dev: true /jest-runtime@29.7.0: resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} @@ -15159,7 +15759,6 @@ packages: strip-bom: 4.0.0 transitivePeerDependencies: - supports-color - dev: true /jest-snapshot@29.7.0: resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} @@ -15184,10 +15783,9 @@ packages: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.6.2 + semver: 7.6.3 transitivePeerDependencies: - supports-color - dev: true /jest-util@29.7.0: resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} @@ -15199,7 +15797,6 @@ packages: ci-info: 3.9.0 graceful-fs: 4.2.11 picomatch: 2.3.1 - dev: true /jest-validate@29.7.0: resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} @@ -15211,7 +15808,6 @@ packages: jest-get-type: 29.6.3 leven: 3.1.0 pretty-format: 29.7.0 - dev: true /jest-watcher@29.7.0: resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} @@ -15225,7 +15821,6 @@ packages: emittery: 0.13.1 jest-util: 29.7.0 string-length: 4.0.2 - dev: true /jest-worker@29.7.0: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} @@ -15235,7 +15830,6 @@ packages: jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - dev: true /jest@29.7.0(@types/node@18.18.14)(ts-node@10.9.2): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} @@ -15256,13 +15850,16 @@ packages: - babel-plugin-macros - supports-color - ts-node - dev: true /joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} dev: true + /js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + dev: true + /js-cookie@2.2.1: resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} dev: true @@ -15286,7 +15883,6 @@ packages: dependencies: argparse: 1.0.10 esprima: 4.0.1 - dev: true /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} @@ -15306,7 +15902,6 @@ packages: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} hasBin: true - dev: true /json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} @@ -15319,7 +15914,6 @@ packages: /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: true /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -15352,7 +15946,6 @@ packages: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true - dev: true /jsonc-parser@3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} @@ -15408,6 +16001,11 @@ packages: resolution: {integrity: sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==} dev: true + /jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + dev: true + /keccak256@1.0.6: resolution: {integrity: sha512-8GLiM01PkdJVGUhR1e6M/AvWnSqYS0HaERI+K/QtStGDGlSTx2B1zTqZk4Zlqu5TxHJNTxWAdP9Y+WI50OApUw==} dependencies: @@ -15556,7 +16154,6 @@ packages: /leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} - dev: true /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} @@ -15573,7 +16170,6 @@ packages: /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: true /lint-staged@15.2.2: resolution: {integrity: sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==} @@ -15644,7 +16240,6 @@ packages: engines: {node: '>=8'} dependencies: p-locate: 4.1.0 - dev: true /locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} @@ -15673,7 +16268,6 @@ packages: /lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - dev: true /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -15736,7 +16330,6 @@ packages: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} dependencies: get-func-name: 2.0.2 - dev: true /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -15773,7 +16366,6 @@ packages: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: yallist: 3.1.1 - dev: true /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} @@ -15794,12 +16386,17 @@ packages: resolution: {integrity: sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==} dev: true + /magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + dev: true + /make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} dependencies: - semver: 7.6.2 - dev: true + semver: 7.6.3 /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -15808,7 +16405,6 @@ packages: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} dependencies: tmpl: 1.0.5 - dev: true /map-age-cleaner@0.1.3: resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} @@ -15845,7 +16441,6 @@ packages: /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} - dev: false /mem@8.1.1: resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==} @@ -15905,11 +16500,9 @@ packages: /merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} - dev: false /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: true /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} @@ -15930,7 +16523,6 @@ packages: /methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} - dev: false /micro-ftch@0.3.1: resolution: {integrity: sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==} @@ -15945,7 +16537,6 @@ packages: dependencies: braces: 3.0.2 picomatch: 2.3.1 - dev: true /miller-rabin@4.0.1: resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} @@ -15969,7 +16560,6 @@ packages: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} hasBin: true - dev: false /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} @@ -16001,7 +16591,6 @@ packages: resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} dependencies: dom-walk: 0.1.2 - dev: false /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} @@ -16037,6 +16626,12 @@ packages: engines: {node: '>=10'} dependencies: brace-expansion: 2.0.1 + + /minimatch@7.4.6: + resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 dev: true /minimatch@9.0.5: @@ -16063,7 +16658,6 @@ packages: dependencies: safe-buffer: 5.2.1 yallist: 3.1.1 - dev: false /minipass@3.3.6: resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} @@ -16091,7 +16685,6 @@ packages: resolution: {integrity: sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==} dependencies: minipass: 2.9.0 - dev: false /minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} @@ -16116,7 +16709,6 @@ packages: deprecated: This package is broken and no longer maintained. 'mkdirp' itself supports promises now, please switch to that. dependencies: mkdirp: 1.0.4 - dev: false /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} @@ -16169,7 +16761,6 @@ packages: /mock-fs@4.14.0: resolution: {integrity: sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw==} - dev: false /mri@1.1.6: resolution: {integrity: sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ==} @@ -16191,7 +16782,6 @@ packages: dependencies: base-x: 3.0.9 buffer: 5.7.1 - dev: false /multibase@0.7.0: resolution: {integrity: sha512-TW8q03O0f6PNFTQDvh3xxH03c8CjGaaYrjkl9UQPG6rz53TQzzxJVCIWVjzcbN/Q5Y53Zd0IBQBMVktVgNx4Fg==} @@ -16199,14 +16789,12 @@ packages: dependencies: base-x: 3.0.9 buffer: 5.7.1 - dev: false /multicodec@0.5.7: resolution: {integrity: sha512-PscoRxm3f+88fAtELwUnZxGDkduE2HD9Q6GHUOywQLjOGT/HAdhjLDYNZ1e7VR0s0TP0EwZ16LNUTFpoBGivOA==} deprecated: This module has been superseded by the multiformats module dependencies: varint: 5.0.2 - dev: false /multicodec@1.0.4: resolution: {integrity: sha512-NDd7FeS3QamVtbgfvu5h7fd1IlbaC4EQ0/pgU4zqE2vdHCmBGsUa0TiM8/TdSeG6BMPC92OOCf8F1ocE/Wkrrg==} @@ -16214,7 +16802,6 @@ packages: dependencies: buffer: 5.7.1 varint: 5.0.2 - dev: false /multihashes@0.4.21: resolution: {integrity: sha512-uVSvmeCWf36pU2nB4/1kzYZjsXD9vofZKpgudqkceYY5g2aZZXJ5r9lxuzoRLl1OAp28XljXsEJ/X/85ZsKmKw==} @@ -16222,7 +16809,17 @@ packages: buffer: 5.7.1 multibase: 0.7.0 varint: 5.0.2 - dev: false + + /multimatch@5.0.0: + resolution: {integrity: sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==} + engines: {node: '>=10'} + dependencies: + '@types/minimatch': 3.0.5 + array-differ: 3.0.0 + array-union: 2.1.0 + arrify: 2.0.1 + minimatch: 3.1.2 + dev: true /murmur-128@0.2.1: resolution: {integrity: sha512-WseEgiRkI6aMFBbj8Cg9yBj/y+OdipwVC7zUo3W2W1JAJITwouUOtpqsmGSg67EQmwwSyod7hsVsWY5LsrfQVg==} @@ -16248,21 +16845,24 @@ packages: /nano-json-stream-parser@0.1.2: resolution: {integrity: sha512-9MqxMH/BSJC7dnLsEMPyfN5Dvoo49IsPFYMcHw3Bcfc2kN0lpHRBSzlMSVx4HGyJ7s9B31CyBTVehWJoQ8Ctew==} - dev: false /nanoid@3.3.3: resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true /negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} - dev: false /next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} @@ -16329,11 +16929,9 @@ packages: /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - dev: true /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - dev: true /nofilter@3.1.0: resolution: {integrity: sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==} @@ -16367,7 +16965,6 @@ packages: engines: {node: '>=8'} dependencies: path-key: 3.1.1 - dev: true /npm-run-path@5.2.0: resolution: {integrity: sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==} @@ -16481,7 +17078,6 @@ packages: engines: {node: '>= 0.8'} dependencies: ee-first: 1.1.1 - dev: false /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -16581,7 +17177,6 @@ packages: engines: {node: '>=8'} dependencies: p-limit: 2.3.0 - dev: true /p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} @@ -16641,7 +17236,7 @@ packages: got: 12.6.1 registry-auth-token: 5.0.2 registry-url: 6.0.1 - semver: 7.6.2 + semver: 7.6.3 dev: true /pako@2.1.0: @@ -16657,7 +17252,6 @@ packages: /parse-headers@2.0.5: resolution: {integrity: sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==} - dev: false /parse-json@2.2.0: resolution: {integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==} @@ -16674,12 +17268,15 @@ packages: error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + + /parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} dev: true /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - dev: false /patch-console@1.0.0: resolution: {integrity: sha512-nxl9nrnLQmh64iTzMfyylSlRozL7kAXIaxw1fVcLYdyhNkJCRUzirRZTikXGJsg+hc4fqpneTK6iU2H1Q8THSA==} @@ -16711,7 +17308,6 @@ packages: /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - dev: true /path-key@4.0.0: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} @@ -16739,7 +17335,6 @@ packages: /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} - dev: false /path-to-regexp@6.2.1: resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} @@ -16766,7 +17361,6 @@ packages: /pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - dev: true /pbkdf2@3.1.2: resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} @@ -16783,6 +17377,9 @@ packages: /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + /picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} dev: true /picomatch@2.3.1: @@ -16820,13 +17417,17 @@ packages: /pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} - dev: true /pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} dependencies: find-up: 4.1.0 + + /please-upgrade-node@3.2.0: + resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==} + dependencies: + semver-compare: 1.0.0 dev: true /pluralize@8.0.0: @@ -16834,6 +17435,10 @@ packages: engines: {node: '>=4'} dev: true + /poseidon-lite@0.2.1: + resolution: {integrity: sha512-xIr+G6HeYfOhCuswdqcFpSX47SPhm0EpisWJ6h7fHlWwaVIvH3dLnejpatrtw6Xc6HaLrpq05y7VRfvDmDGIog==} + dev: true + /postcss-load-config@4.0.2(ts-node@10.9.2): resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} engines: {node: '>= 14'} @@ -16848,7 +17453,16 @@ packages: dependencies: lilconfig: 3.0.0 ts-node: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) - yaml: 2.3.4 + yaml: 2.7.0 + dev: true + + /postcss@8.5.1: + resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 dev: true /preferred-pm@3.1.2: @@ -16894,7 +17508,7 @@ packages: dependencies: '@solidity-parser/parser': 0.17.0 prettier: 3.2.5 - semver: 7.6.2 + semver: 7.6.3 solidity-comments-extractor: 0.0.8 dev: true @@ -16917,7 +17531,6 @@ packages: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 react-is: 18.2.0 - dev: true /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -16926,7 +17539,6 @@ packages: /process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - dev: false /prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} @@ -16979,7 +17591,6 @@ packages: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 - dev: false /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -17022,7 +17633,6 @@ packages: /pure-rand@6.0.4: resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} - dev: true /pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} @@ -17033,7 +17643,6 @@ packages: engines: {node: '>=0.6'} dependencies: side-channel: 1.0.6 - dev: false /qs@6.11.2: resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} @@ -17074,7 +17683,6 @@ packages: decode-uri-component: 0.2.2 object-assign: 4.1.1 strict-uri-encode: 1.1.0 - dev: false /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -17097,7 +17705,6 @@ packages: /range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - dev: false /raw-body@2.5.1: resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} @@ -17107,7 +17714,6 @@ packages: http-errors: 2.0.0 iconv-lite: 0.4.24 unpipe: 1.0.0 - dev: false /raw-body@2.5.2: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} @@ -17142,7 +17748,6 @@ packages: /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - dev: true /react-reconciler@0.26.2(react@17.0.2): resolution: {integrity: sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q==} @@ -17351,6 +17956,10 @@ packages: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} dev: true + /require-package-name@2.0.1: + resolution: {integrity: sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==} + dev: true + /resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -17359,6 +17968,13 @@ packages: engines: {node: '>=8'} dependencies: resolve-from: 5.0.0 + + /resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 dev: true /resolve-from@4.0.0: @@ -17369,7 +17985,6 @@ packages: /resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - dev: true /resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -17378,7 +17993,6 @@ packages: /resolve.exports@2.0.2: resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} engines: {node: '>=10'} - dev: true /resolve@1.17.0: resolution: {integrity: sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==} @@ -17392,7 +18006,6 @@ packages: is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true /resolve@2.0.0-next.5: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} @@ -17623,6 +18236,10 @@ packages: engines: {node: '>=4.1'} dev: true + /semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + dev: true + /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -17645,7 +18262,6 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: false /semver@7.6.2: resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} @@ -17656,7 +18272,6 @@ packages: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} hasBin: true - dev: true /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -17677,7 +18292,6 @@ packages: statuses: 2.0.1 transitivePeerDependencies: - supports-color - dev: false /serialize-javascript@6.0.0: resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} @@ -17694,7 +18308,6 @@ packages: send: 0.18.0 transitivePeerDependencies: - supports-color - dev: false /servify@0.1.12: resolution: {integrity: sha512-/xE6GvsKKqyo1BAY+KxOWXcLpPsUUyji7Qg3bVD7hh1eRze5bR1uYiuDA/k3Gof1s9BTzQZEJK8sNcNGFIzeWw==} @@ -17707,7 +18320,6 @@ packages: xhr: 2.6.0 transitivePeerDependencies: - supports-color - dev: false /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -17768,7 +18380,6 @@ packages: engines: {node: '>=8'} dependencies: shebang-regex: 3.0.0 - dev: true /shebang-regex@1.0.0: resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} @@ -17778,7 +18389,6 @@ packages: /shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - dev: true /shell-quote@1.8.1: resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} @@ -17820,7 +18430,6 @@ packages: /simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - dev: false /simple-get@2.8.2: resolution: {integrity: sha512-Ijd/rV5o+mSBBs4F/x9oDPtTx9Zb6X9brmnXvMW4J7IR15ngi9q5xxqWBKU744jTZiaXtxaPL7uHG6vtN8kUkw==} @@ -17828,7 +18437,6 @@ packages: decompress-response: 3.3.0 once: 1.4.0 simple-concat: 1.0.1 - dev: false /simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} @@ -17883,7 +18491,6 @@ packages: /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - dev: true /slash@4.0.0: resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} @@ -18002,7 +18609,7 @@ packages: latest-version: 7.0.0 lodash: 4.17.21 pluralize: 8.0.0 - semver: 7.6.2 + semver: 7.6.3 strip-ansi: 6.0.1 table: 6.8.2 text-table: 0.2.0 @@ -18041,16 +18648,20 @@ packages: git-hooks-list: 3.1.0 globby: 13.2.2 is-plain-obj: 4.1.0 - semver: 7.6.2 + semver: 7.6.3 sort-object-keys: 1.1.3 dev: true + /source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + dev: true + /source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} dependencies: buffer-from: 1.1.2 source-map: 0.6.1 - dev: true /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -18113,7 +18724,6 @@ packages: /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: true /ssh2@1.16.0: resolution: {integrity: sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==} @@ -18170,7 +18780,6 @@ packages: /strict-uri-encode@1.1.0: resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==} engines: {node: '>=0.10.0'} - dev: false /string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} @@ -18187,7 +18796,6 @@ packages: dependencies: char-regex: 1.0.2 strip-ansi: 6.0.1 - dev: true /string-similarity@4.0.4: resolution: {integrity: sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==} @@ -18318,12 +18926,10 @@ packages: /strip-bom@4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} - dev: true /strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - dev: true /strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} @@ -18396,7 +19002,6 @@ packages: /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - dev: true /swarm-js@0.1.42: resolution: {integrity: sha512-BV7c/dVlA3R6ya1lMlSSNPLYrntt0LUq4YMgy3iwpCIc6rZnS5W2wUoctarZ5pXlpKtxDDf9hNziEkcfrxdhqQ==} @@ -18416,7 +19021,6 @@ packages: - bufferutil - supports-color - utf-8-validate - dev: false /symbol.inspect@1.0.1: resolution: {integrity: sha512-YQSL4duoHmLhsTD1Pw8RW6TZ5MaTX5rXJnqacJottr2P2LZBF/Yvrc3ku4NUpMOm8aM0KOCqM+UAkMA5HWQCzQ==} @@ -18515,7 +19119,6 @@ packages: mkdirp: 0.5.6 safe-buffer: 5.2.1 yallist: 3.1.1 - dev: false /tar@6.2.0: resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==} @@ -18544,7 +19147,6 @@ packages: '@istanbuljs/schema': 0.1.3 glob: 7.2.3 minimatch: 3.1.2 - dev: true /testrpc@0.0.1: resolution: {integrity: sha512-afH1hO+SQ/VPlmaLUFj2636QMeDvPCeQMc/9RBMW0IfjNe9gFD9Ra3ShqYkB7py0do1ZcCna/9acHyzTJ+GcNA==} @@ -18600,7 +19202,6 @@ packages: /timed-out@4.0.1: resolution: {integrity: sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==} engines: {node: '>=0.10.0'} - dev: false /timers-ext@0.1.8: resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} @@ -18637,7 +19238,6 @@ packages: /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - dev: true /to-buffer@1.1.1: resolution: {integrity: sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==} @@ -18646,7 +19246,6 @@ packages: /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} - dev: true /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -18763,6 +19362,44 @@ packages: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true + /ts-jest@29.2.5(@babel/core@7.23.9)(esbuild@0.19.11)(jest@29.7.0)(typescript@5.5.3): + resolution: {integrity: sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.23.9 + bs-logger: 0.2.6 + ejs: 3.1.10 + esbuild: 0.19.11 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.6.3 + typescript: 5.5.3 + yargs-parser: 21.1.1 + /ts-morph@21.0.1: resolution: {integrity: sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==} dependencies: @@ -19011,12 +19648,10 @@ packages: /type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} - dev: true /type-detect@4.1.0: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} - dev: true /type-fest@0.12.0: resolution: {integrity: sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==} @@ -19065,7 +19700,6 @@ packages: dependencies: media-typer: 0.3.0 mime-types: 2.1.35 - dev: false /type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} @@ -19160,7 +19794,6 @@ packages: /ultron@1.1.1: resolution: {integrity: sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==} - dev: false /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -19174,12 +19807,6 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - /undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - requiresBuild: true - dev: true - optional: true - /undici@5.28.2: resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==} engines: {node: '>=14.0'} @@ -19224,7 +19851,6 @@ packages: browserslist: 4.22.2 escalade: 3.1.1 picocolors: 1.0.0 - dev: true /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -19233,7 +19859,6 @@ packages: /url-set-query@1.0.0: resolution: {integrity: sha512-3AChu4NiXquPfeckE5R5cGdiHCMWJx1dwCWOmWIL4KHAziJNOFIYJlpGFeKDvwLPHovZRCxK3cYlwzqI9Vp+Gg==} - dev: false /url@0.11.4: resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} @@ -19277,7 +19902,6 @@ packages: /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - dev: false /uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} @@ -19291,7 +19915,6 @@ packages: /uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true - dev: false /v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -19303,7 +19926,6 @@ packages: '@jridgewell/trace-mapping': 0.3.22 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - dev: true /valibot@0.37.0(typescript@5.5.3): resolution: {integrity: sha512-FQz52I8RXgFgOHym3XHYSREbNtkgSjF9prvMFH1nBsRyfL6SfCzoT1GuSDTlbsuPubM7/6Kbw0ZMQb8A+V+VsQ==} @@ -19325,12 +19947,10 @@ packages: /varint@5.0.2: resolution: {integrity: sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==} - dev: false /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - dev: false /verror@1.10.0: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} @@ -19344,7 +19964,6 @@ packages: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: makeerror: 1.0.12 - dev: true /wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -19364,7 +19983,6 @@ packages: - bufferutil - supports-color - utf-8-validate - dev: false /web3-core-helpers@1.10.4: resolution: {integrity: sha512-r+L5ylA17JlD1vwS8rjhWr0qg7zVoVMDvWhajWA5r5+USdh91jRUYosp19Kd1m2vE034v7Dfqe1xYRoH2zvG0g==} @@ -19448,7 +20066,6 @@ packages: transitivePeerDependencies: - encoding - supports-color - dev: false /web3-eth-contract@1.10.4: resolution: {integrity: sha512-Q8PfolOJ4eV9TvnTj1TGdZ4RarpSLmHnUnzVxZ/6/NiTfe4maJz99R0ISgwZkntLhLRtw0C7LRJuklzGYCNN3A==} @@ -19465,7 +20082,6 @@ packages: transitivePeerDependencies: - encoding - supports-color - dev: false /web3-eth-ens@1.10.4: resolution: {integrity: sha512-LLrvxuFeVooRVZ9e5T6OWKVflHPFgrVjJ/jtisRWcmI7KN/b64+D/wJzXqgmp6CNsMQcE7rpmf4CQmJCrTdsgg==} @@ -19482,7 +20098,6 @@ packages: transitivePeerDependencies: - encoding - supports-color - dev: false /web3-eth-iban@1.10.4: resolution: {integrity: sha512-0gE5iNmOkmtBmbKH2aTodeompnNE8jEyvwFJ6s/AF6jkw9ky9Op9cqfzS56AYAbrqEFuClsqB/AoRves7LDELw==} @@ -19504,7 +20119,6 @@ packages: transitivePeerDependencies: - encoding - supports-color - dev: false /web3-eth@1.10.4: resolution: {integrity: sha512-Sql2kYKmgt+T/cgvg7b9ce24uLS7xbFrxE4kuuor1zSCGrjhTJ5rRNG8gTJUkAJGKJc7KgnWmgW+cOfMBPUDSA==} @@ -19525,7 +20139,6 @@ packages: transitivePeerDependencies: - encoding - supports-color - dev: false /web3-net@1.10.4: resolution: {integrity: sha512-mKINnhOOnZ4koA+yV2OT5s5ztVjIx7IY9a03w6s+yao/BUn+Luuty0/keNemZxTr1E8Ehvtn28vbOtW7Ids+Ow==} @@ -19537,7 +20150,6 @@ packages: transitivePeerDependencies: - encoding - supports-color - dev: false /web3-providers-http@1.10.4: resolution: {integrity: sha512-m2P5Idc8hdiO0l60O6DSCPw0kw64Zgi0pMjbEFRmxKIck2Py57RQMu4bxvkxJwkF06SlGaEQF8rFZBmuX7aagQ==} @@ -19579,7 +20191,6 @@ packages: transitivePeerDependencies: - encoding - supports-color - dev: false /web3-utils@1.10.4: resolution: {integrity: sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A==} @@ -19611,7 +20222,6 @@ packages: - encoding - supports-color - utf-8-validate - dev: false /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -19723,7 +20333,6 @@ packages: hasBin: true dependencies: isexe: 2.0.0 - dev: true /which@4.0.0: resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} @@ -19842,7 +20451,6 @@ packages: dependencies: imurmurhash: 0.1.4 signal-exit: 3.0.7 - dev: true /ws@3.3.3: resolution: {integrity: sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==} @@ -19858,7 +20466,6 @@ packages: async-limiter: 1.0.1 safe-buffer: 5.1.2 ultron: 1.1.1 - dev: false /ws@7.4.6: resolution: {integrity: sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==} @@ -19916,7 +20523,6 @@ packages: resolution: {integrity: sha512-YUBytBsuwgitWtdRzXDDkWAXzhdGB8bYm0sSzMPZT7Z2MBjMSTHFsyCT1yCRATY+XC69DUrQraRAEgcoCRaIPg==} dependencies: xhr-request: 1.1.0 - dev: false /xhr-request@1.1.0: resolution: {integrity: sha512-Y7qzEaR3FDtL3fP30k9wO/e+FBnBByZeybKOhASsGP30NIkRAAkKD/sCnLvgEfAIEC1rcmK7YG8f4oEnIrrWzA==} @@ -19928,7 +20534,6 @@ packages: timed-out: 4.0.1 url-set-query: 1.0.0 xhr: 2.6.0 - dev: false /xhr@2.6.0: resolution: {integrity: sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==} @@ -19937,7 +20542,6 @@ packages: is-function: 1.0.2 parse-headers: 2.0.5 xtend: 4.0.2 - dev: false /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} @@ -19969,11 +20573,22 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: true + /yaml@2.3.4: resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} engines: {node: '>= 14'} dev: true + /yaml@2.7.0: + resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} + engines: {node: '>= 14'} + hasBin: true + dev: true + /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -19996,7 +20611,6 @@ packages: /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - dev: true /yargs-unparser@2.0.0: resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} @@ -20047,7 +20661,6 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 - dev: true /yargs@4.8.1: resolution: {integrity: sha512-LqodLrnIDM3IFT+Hf/5sxBnEGECrfdC1uIbgZeJmESCSo4HoCAaKEus8MylXHAkdacGc0ye+Qa+dpkuom8uVYA==} @@ -20115,20 +20728,10 @@ packages: ethers: ^5.7.2 dependencies: ethers: 5.7.2 - dev: false /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} - /zx@8.1.8: - resolution: {integrity: sha512-m8s48skYQ8EcRz9KXfc7rZCjqlZevOGiNxq5tNhDiGnhOvXKRGxVr+ajUma9B6zxMdHGSSbnjV/R/r7Ue2xd+A==} - engines: {node: '>= 12.17.0'} - hasBin: true - optionalDependencies: - '@types/fs-extra': 11.0.4 - '@types/node': 22.9.0 - dev: true - github.com/LayerZero-Labs/es5-ext/7e360296a7e27176e240bc063b32f5ada55f9aa6: resolution: {tarball: https://codeload.github.com/LayerZero-Labs/es5-ext/tar.gz/7e360296a7e27176e240bc063b32f5ada55f9aa6} name: es5-ext diff --git a/tests-user/tests/create-lz-oapp.bats b/tests-user/tests/create-lz-oapp.bats index 10a99d460..9e70b24f9 100644 --- a/tests-user/tests/create-lz-oapp.bats +++ b/tests-user/tests/create-lz-oapp.bats @@ -189,6 +189,24 @@ teardown() { pnpm test } +@test "should work with pnpm & oft aptos example in CI mode" { + local DESTINATION="$PROJECTS_DIRECTORY/pnpm-oft-aptos" + + LZ_ENABLE_EXPERIMENTAL_MOVE_VM_EXAMPLES=1 npx --yes create-lz-oapp --ci --example oft-aptos-move --destination $DESTINATION --package-manager pnpm + cd "$DESTINATION" + pnpm compile + pnpm test +} + +@test "should work with pnpm & oft adapter aptos example in CI mode" { + local DESTINATION="$PROJECTS_DIRECTORY/pnpm-oft-adapter-aptos" + + LZ_ENABLE_EXPERIMENTAL_MOVE_VM_EXAMPLES=1 npx --yes create-lz-oapp --ci --example oft-adapter-aptos-move --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" @@ -273,6 +291,24 @@ teardown() { yarn test } +@test "should work with yarn & oft aptos example in CI mode" { + local DESTINATION="$PROJECTS_DIRECTORY/yarn-oft-aptos" + + YARN_CACHE_FOLDER="/tmp/.yarn-cache-oft-aptos" LZ_ENABLE_EXPERIMENTAL_MOVE_VM_EXAMPLES=1 npx --yes create-lz-oapp --ci --example oft-aptos --destination $DESTINATION --package-manager yarn + cd "$DESTINATION" + yarn compile + yarn test +} + +@test "should work with yarn & oft adapter aptos example in CI mode" { + local DESTINATION="$PROJECTS_DIRECTORY/yarn-oft-adapter-aptos" + + YARN_CACHE_FOLDER="/tmp/.yarn-cache-oft-adapter-aptos" LZ_ENABLE_EXPERIMENTAL_MOVE_VM_EXAMPLES=1 npx --yes create-lz-oapp --ci --example oft-adapter-aptos --destination $DESTINATION --package-manager yarn + cd "$DESTINATION" + yarn compile + yarn test +} + @test "should work with yarn & oapp read example in CI mode" { local DESTINATION="$PROJECTS_DIRECTORY/yarn-oapp-read" @@ -357,6 +393,24 @@ teardown() { npm run test } +@test "should work with npm & oft aptos example in CI mode" { + local DESTINATION="$PROJECTS_DIRECTORY/npm-oft-aptos" + + LZ_ENABLE_EXPERIMENTAL_MOVE_VM_EXAMPLES=1 npx --yes create-lz-oapp --ci --example oft-aptos --destination $DESTINATION --package-manager npm + cd "$DESTINATION" + npm run compile + npm run test +} + +@test "should work with npm & oft adapter aptos example in CI mode" { + local DESTINATION="$PROJECTS_DIRECTORY/npm-oft-adapter-aptos" + + LZ_ENABLE_EXPERIMENTAL_MOVE_VM_EXAMPLES=1 npx --yes create-lz-oapp --ci --example oft-adapter-aptos --destination $DESTINATION --package-manager npm + cd "$DESTINATION" + npm run compile + npm run test +} + @test "should work with npm & oapp read example in CI mode" { local DESTINATION="$PROJECTS_DIRECTORY/npm-oapp-read" diff --git a/turbo.json b/turbo.json index 214c855c2..3d39ce53f 100644 --- a/turbo.json +++ b/turbo.json @@ -46,7 +46,6 @@ "globalPassThroughEnv": [ "LZ_DEVTOOLS_ENABLE_DEPLOY_LOGGING", "LZ_DEVTOOLS_ENABLE_SOLANA_TESTS", - "LZ_ENABLE_EXPERIMENTAL_BATCHED_SEND", "LZ_ENABLE_EXPERIMENTAL_BATCHED_WAIT", "LZ_ENABLE_EXPERIMENTAL_PARALLEL_EXECUTION", @@ -59,7 +58,7 @@ "LZ_ENABLE_MINTBURN_EXAMPLE", "LZ_ENABLE_UPGRADEABLE_EXAMPLE", "LZ_METADATA_URL", - + "LZ_ENABLE_EXPERIMENTAL_MOVE_VM_EXAMPLES", "LAYERZERO_EXAMPLES_REPOSITORY_URL", "LAYERZERO_EXAMPLES_REPOSITORY_REF", "MNEMONIC", @@ -69,7 +68,6 @@ "NETWORK_URL_TANGO", "NETWORK_URL_TON", "CI", - "RPC_URL_SOLANA_MAINNET", "RPC_URL_SOLANA_TESTNET" ]