diff --git a/examples/lzapp-migration/.eslintrc.js b/examples/lzapp-migration/.eslintrc.js index a3f2f6a1f0..bd7363593f 100644 --- a/examples/lzapp-migration/.eslintrc.js +++ b/examples/lzapp-migration/.eslintrc.js @@ -1,14 +1,11 @@ +require('@rushstack/eslint-patch/modern-module-resolution'); + module.exports = { root: true, extends: ['@layerzerolabs/eslint-config-next/recommended'], - settings: { - 'import/resolver': { - typescript: { - project: './tsconfig.json', - }, - }, - }, rules: { + // @layerzerolabs/eslint-config-next defines rules for turborepo-based projects + // that are not relevant for this particular project 'turbo/no-undeclared-env-vars': 'off', 'import/no-unresolved': 'warn', }, diff --git a/examples/lzapp-migration/README.md b/examples/lzapp-migration/README.md index bcbe473ff5..f819c63cfb 100644 --- a/examples/lzapp-migration/README.md +++ b/examples/lzapp-migration/README.md @@ -249,14 +249,6 @@ pnpm hardhat lz:oft:solana:create --eid 40168 --program-id :information_source: You can also specify `--amount ` to have the OFT minted to your deployer address upon token creation. -#### For OFTAdapter: - -```bash -pnpm hardhat lz:oft-adapter:solana:create --eid 40168 --program-id --mint --token-program -``` - -:information_source: You can use OFT Adapter if you want to use an existing token on Solana. For OFT Adapter, tokens will be locked when sending to other chains and unlocked when receiving from other chains. - #### For OFT Mint-And-Burn Adapter (MABA): ```bash diff --git a/examples/lzapp-migration/package.json b/examples/lzapp-migration/package.json index a0c52eab06..db58f1a838 100644 --- a/examples/lzapp-migration/package.json +++ b/examples/lzapp-migration/package.json @@ -15,6 +15,7 @@ "lint:sol": "solhint 'contracts/**/*.sol'", "test": "$npm_execpath run test:hardhat", "test:anchor": "anchor test", + "test:forge": "forge test", "test:hardhat": "hardhat test" }, "resolutions": { diff --git a/examples/lzapp-migration/tasks/common/wire.ts b/examples/lzapp-migration/tasks/common/wire.ts index ed00cb366e..49b033abd5 100644 --- a/examples/lzapp-migration/tasks/common/wire.ts +++ b/examples/lzapp-migration/tasks/common/wire.ts @@ -104,8 +104,8 @@ task(TASK_LZ_OAPP_WIRE) // // - // construct the user's keypair via the SOLANA_PRIVATE_KEY env var - const keypair = useWeb3Js().web3JsKeypair + // construct the user's keypair via SOLANA_PRIVATE_KEY or other supported methods + const keypair = (await useWeb3Js()).web3JsKeypair const userAccount = keypair.publicKey const solanaDeployment = getSolanaDeployment(args.solanaEid) diff --git a/examples/lzapp-migration/tasks/index.ts b/examples/lzapp-migration/tasks/index.ts index e795337181..338d2d9b92 100644 --- a/examples/lzapp-migration/tasks/index.ts +++ b/examples/lzapp-migration/tasks/index.ts @@ -9,4 +9,10 @@ import './evm/v1/setMinDstGas' import './solana/createOFT' import './solana/sendSolana' import './solana/debug' +import './solana/retryPayload' +import './solana/setAuthority' +import './solana/updateMetadata' +import './solana/setUpdateAuthority' +import './solana/getPrioFees' +import './solana/base58' import './solana/initConfig' diff --git a/examples/lzapp-migration/tasks/solana/index.ts b/examples/lzapp-migration/tasks/solana/index.ts index 1715288300..017ac3f18e 100644 --- a/examples/lzapp-migration/tasks/solana/index.ts +++ b/examples/lzapp-migration/tasks/solana/index.ts @@ -24,13 +24,12 @@ import { import { createUmi } from '@metaplex-foundation/umi-bundle-defaults' import { createWeb3JsEddsa } from '@metaplex-foundation/umi-eddsa-web3js' import { toWeb3JsInstruction, toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters' -import { AddressLookupTableAccount, Connection, Keypair } from '@solana/web3.js' +import { AddressLookupTableAccount, Connection } from '@solana/web3.js' import { getSimulationComputeUnits } from '@solana-developers/helpers' -import bs58 from 'bs58' import { backOff } from 'exponential-backoff' import { formatEid } from '@layerzerolabs/devtools' -import { getPrioritizationFees } from '@layerzerolabs/devtools-solana' +import { getPrioritizationFees, getSolanaKeypair } from '@layerzerolabs/devtools-solana' import { promptToContinue } from '@layerzerolabs/io-devtools' import { EndpointId, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' import { OftPDA } from '@layerzerolabs/oft-v2-solana-sdk' @@ -42,29 +41,16 @@ const LOOKUP_TABLE_ADDRESS: Partial> = { [EndpointId.SOLANA_V2_TESTNET]: publicKey('9thqPdbR27A1yLWw2spwJLySemiGMXxPnEvfmXVk4KuK'), } -const getFromEnv = (key: string): string => { - const value = process.env[key] - if (!value) { - throw new Error(`${key} is not defined in the environment variables.`) - } - return value -} - -/** - * Extracts the SOLANA_PRIVATE_KEY from the environment. This is purposely not exported for encapsulation purposes. - */ -const getSolanaPrivateKeyFromEnv = () => getFromEnv('SOLANA_PRIVATE_KEY') - /** * Derive common connection and UMI objects for a given endpoint ID. * @param eid {EndpointId} */ export const deriveConnection = async (eid: EndpointId, readOnly = false) => { - const privateKey = readOnly ? bs58.encode(Keypair.generate().secretKey) : getSolanaPrivateKeyFromEnv() + const keypair = await getSolanaKeypair(readOnly) const connectionFactory = createSolanaConnectionFactory() const connection = await connectionFactory(eid) const umi = createUmi(connection.rpcEndpoint).use(mplToolbox()) - const umiWalletKeyPair = umi.eddsa.createKeypairFromSecretKey(bs58.decode(privateKey)) + const umiWalletKeyPair = umi.eddsa.createKeypairFromSecretKey(keypair.secretKey) const umiWalletSigner = createSignerFromKeypair(umi, umiWalletKeyPair) umi.use(signerIdentity(umiWalletSigner)) return { @@ -75,10 +61,8 @@ export const deriveConnection = async (eid: EndpointId, readOnly = false) => { } } -export const useWeb3Js = () => { - const privateKey = getSolanaPrivateKeyFromEnv() - const secretKeyBytes = bs58.decode(privateKey) - const keypair = Keypair.fromSecretKey(secretKeyBytes) +export const useWeb3Js = async () => { + const keypair = await getSolanaKeypair() return { web3JsKeypair: keypair, } diff --git a/examples/lzapp-migration/tasks/solana/retryPayload.ts b/examples/lzapp-migration/tasks/solana/retryPayload.ts index 0035d0248f..82774e484b 100644 --- a/examples/lzapp-migration/tasks/solana/retryPayload.ts +++ b/examples/lzapp-migration/tasks/solana/retryPayload.ts @@ -1,7 +1,6 @@ import { web3 } from '@coral-xyz/anchor' import { toWeb3JsKeypair } from '@metaplex-foundation/umi-web3js-adapters' -import { ComputeBudgetProgram, Keypair, sendAndConfirmTransaction } from '@solana/web3.js' -import bs58 from 'bs58' +import { ComputeBudgetProgram, sendAndConfirmTransaction } from '@solana/web3.js' import { task } from 'hardhat/config' import { makeBytes32 } from '@layerzerolabs/devtools' @@ -48,10 +47,6 @@ task('lz:oft:solana:retry-payload', 'Retry a stored payload on Solana') lamports, withPriorityFee, }: Args) => { - if (!process.env.SOLANA_PRIVATE_KEY) { - throw new Error('SOLANA_PRIVATE_KEY is not defined in the environment variables.') - } - const { connection, umiWalletKeyPair } = await deriveConnection(dstEid) const signer = toWeb3JsKeypair(umiWalletKeyPair) const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash() @@ -89,10 +84,9 @@ task('lz:oft:solana:retry-payload', 'Retry a stored payload on Solana') tx.add(instruction) tx.recentBlockhash = blockhash - const keypair = Keypair.fromSecretKey(bs58.decode(process.env.SOLANA_PRIVATE_KEY)) - tx.sign(keypair) + tx.sign(signer) - const signature = await sendAndConfirmTransaction(connection, tx, [keypair], { skipPreflight: true }) + const signature = await sendAndConfirmTransaction(connection, tx, [signer], { skipPreflight: true }) console.log( `View Solana transaction here: ${getExplorerTxLink(signature.toString(), dstEid == EndpointId.SOLANA_V2_TESTNET)}` ) diff --git a/examples/lzapp-migration/tasks/solana/setInboundRateLimit.ts b/examples/lzapp-migration/tasks/solana/setInboundRateLimit.ts deleted file mode 100644 index bccfdbebe4..0000000000 --- a/examples/lzapp-migration/tasks/solana/setInboundRateLimit.ts +++ /dev/null @@ -1,80 +0,0 @@ -import assert from 'assert' - -import { mplToolbox } from '@metaplex-foundation/mpl-toolbox' -import { createSignerFromKeypair, publicKey, signerIdentity } from '@metaplex-foundation/umi' -import { createUmi } from '@metaplex-foundation/umi-bundle-defaults' -import { fromWeb3JsKeypair, toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters' -import { Keypair, PublicKey, sendAndConfirmTransaction } from '@solana/web3.js' -import bs58 from 'bs58' -import { task } from 'hardhat/config' - -import { types } from '@layerzerolabs/devtools-evm-hardhat' -import { deserializeTransactionMessage } from '@layerzerolabs/devtools-solana' -import { EndpointId } from '@layerzerolabs/lz-definitions' -import { OftPDA, oft202 } from '@layerzerolabs/oft-v2-solana-sdk' -import { createOFTFactory } from '@layerzerolabs/ua-devtools-solana' - -import { createSolanaConnectionFactory } from '../common/utils' - -interface Args { - mint: string - eid: EndpointId - srcEid: EndpointId - programId: string - oftStore: string - capacity: bigint - refillPerSecond: bigint -} - -task( - 'lz:oft:solana:inbound-rate-limit', - "Sets the Solana and EVM rate limits from './scripts/solana/utils/constants.ts'" -) - .addParam('mint', 'The OFT token mint public key') - .addParam('programId', 'The OFT Program id') - .addParam('eid', 'Solana mainnet or testnet', undefined, types.eid) - .addParam('srcEid', 'The source endpoint ID', undefined, types.eid) - .addParam('oftStore', 'The OFTStore account') - .addParam('capacity', 'The capacity of the rate limit', undefined, types.bigint) - .addParam('refillPerSecond', 'The refill rate of the rate limit', undefined, types.bigint) - .setAction(async (taskArgs: Args, hre) => { - const privateKey = process.env.SOLANA_PRIVATE_KEY - assert(!!privateKey, 'SOLANA_PRIVATE_KEY is not defined in the environment variables.') - - const keypair = Keypair.fromSecretKey(bs58.decode(privateKey)) - const umiKeypair = fromWeb3JsKeypair(keypair) - - const connectionFactory = createSolanaConnectionFactory() - const connection = await connectionFactory(taskArgs.eid) - - const umi = createUmi(connection.rpcEndpoint).use(mplToolbox()) - const umiWalletSigner = createSignerFromKeypair(umi, umiKeypair) - umi.use(signerIdentity(umiWalletSigner)) - - const solanaSdkFactory = createOFTFactory( - () => toWeb3JsPublicKey(umiWalletSigner.publicKey), - () => new PublicKey(taskArgs.programId), - connectionFactory - ) - const sdk = await solanaSdkFactory({ - address: new PublicKey(taskArgs.oftStore).toBase58(), - eid: taskArgs.eid, - }) - const solanaRateLimits = { - capacity: taskArgs.capacity, - refillPerSecond: taskArgs.refillPerSecond, - } - try { - const tx = deserializeTransactionMessage( - (await sdk.setInboundRateLimit(taskArgs.srcEid, solanaRateLimits)).data - ) - tx.sign(keypair) - const txId = await sendAndConfirmTransaction(connection, tx, [keypair]) - console.log(`Transaction successful with ID: ${txId}`) - const [peer] = new OftPDA(publicKey(taskArgs.programId)).peer(publicKey(taskArgs.oftStore), taskArgs.srcEid) - const peerInfo = await oft202.accounts.fetchPeerConfig({ rpc: umi.rpc }, peer) - console.dir({ peerInfo }, { depth: null }) - } catch (error) { - console.error(`setInboundRateLimit failed:`, error) - } - }) diff --git a/examples/lzapp-migration/tasks/solana/setOutboundRateLimit.ts b/examples/lzapp-migration/tasks/solana/setOutboundRateLimit.ts deleted file mode 100644 index c02cc3635c..0000000000 --- a/examples/lzapp-migration/tasks/solana/setOutboundRateLimit.ts +++ /dev/null @@ -1,81 +0,0 @@ -import assert from 'assert' - -import { mplToolbox } from '@metaplex-foundation/mpl-toolbox' -import { createSignerFromKeypair, publicKey, signerIdentity } from '@metaplex-foundation/umi' -import { createUmi } from '@metaplex-foundation/umi-bundle-defaults' -import { fromWeb3JsKeypair, toWeb3JsKeypair, toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters' -import { Keypair, PublicKey, sendAndConfirmTransaction } from '@solana/web3.js' -import bs58 from 'bs58' -import { task } from 'hardhat/config' - -import { types } from '@layerzerolabs/devtools-evm-hardhat' -import { deserializeTransactionMessage } from '@layerzerolabs/devtools-solana' -import { EndpointId } from '@layerzerolabs/lz-definitions' -import { OftPDA, oft202 } from '@layerzerolabs/oft-v2-solana-sdk' -import { createOFTFactory } from '@layerzerolabs/ua-devtools-solana' - -import { createSolanaConnectionFactory } from '../common/utils' - -interface Args { - mint: string - eid: EndpointId - dstEid: EndpointId - programId: string - oftStore: string - capacity: bigint - refillPerSecond: bigint -} - -task( - 'lz:oft:solana:outbound-rate-limit', - "Sets the Solana and EVM rate limits from './scripts/solana/utils/constants.ts'" -) - .addParam('mint', 'The OFT token mint public key') - .addParam('programId', 'The OFT Program id') - .addParam('eid', 'Solana mainnet or testnet', undefined, types.eid) - .addParam('dstEid', 'The destination endpoint ID', undefined, types.eid) - .addParam('oftStore', 'The OFTStore account') - .addParam('capacity', 'The capacity of the rate limit', undefined, types.bigint) - .addParam('refillPerSecond', 'The refill rate of the rate limit', undefined, types.bigint) - .setAction(async (taskArgs: Args, hre) => { - const privateKey = process.env.SOLANA_PRIVATE_KEY - assert(!!privateKey, 'SOLANA_PRIVATE_KEY is not defined in the environment variables.') - - const keypair = Keypair.fromSecretKey(bs58.decode(privateKey)) - const umiKeypair = fromWeb3JsKeypair(keypair) - const connectionFactory = createSolanaConnectionFactory() - const connection = await connectionFactory(taskArgs.eid) - const umi = createUmi(connection.rpcEndpoint).use(mplToolbox()) - const umiWalletSigner = createSignerFromKeypair(umi, umiKeypair) - const web3WalletKeyPair = toWeb3JsKeypair(umiKeypair) - umi.use(signerIdentity(umiWalletSigner)) - - const solanaSdkFactory = createOFTFactory( - () => toWeb3JsPublicKey(umiWalletSigner.publicKey), - () => new PublicKey(taskArgs.programId), - connectionFactory - ) - - const sdk = await solanaSdkFactory({ - address: new PublicKey(taskArgs.oftStore).toBase58(), - eid: taskArgs.eid, - }) - const solanaRateLimits = { - capacity: taskArgs.capacity, - refillPerSecond: taskArgs.refillPerSecond, - } - // for (const peer of graph.connections.filter((connection) => connection.vector.from.eid === solanaEid)) { - try { - const tx = deserializeTransactionMessage( - (await sdk.setOutboundRateLimit(EndpointId.SEPOLIA_V2_TESTNET, solanaRateLimits)).data - ) - tx.sign(keypair) - const txId = await sendAndConfirmTransaction(connection, tx, [keypair]) - console.log(`Transaction successful with ID: ${txId}`) - const [peer] = new OftPDA(publicKey(taskArgs.programId)).peer(publicKey(taskArgs.oftStore), taskArgs.dstEid) - const peerInfo = await oft202.accounts.fetchPeerConfig({ rpc: umi.rpc }, peer) - console.dir({ peerInfo }, { depth: null }) - } catch (error) { - console.error(`setOutboundRateLimit failed:`, error) - } - }) diff --git a/examples/lzapp-migration/tasks/solana/setUpdateAuthority.ts b/examples/lzapp-migration/tasks/solana/setUpdateAuthority.ts new file mode 100644 index 0000000000..a042469de3 --- /dev/null +++ b/examples/lzapp-migration/tasks/solana/setUpdateAuthority.ts @@ -0,0 +1,99 @@ +import { fetchMetadataFromSeeds, updateV1 } from '@metaplex-foundation/mpl-token-metadata' +import { publicKey } from '@metaplex-foundation/umi' +import { SystemProgram } from '@solana/web3.js' +import bs58 from 'bs58' +import { task } from 'hardhat/config' + +import { types as devtoolsTypes } from '@layerzerolabs/devtools-evm-hardhat' +import { promptToContinue } from '@layerzerolabs/io-devtools' +import { EndpointId } from '@layerzerolabs/lz-definitions' + +import { deriveConnection, getExplorerTxLink } from '.' + +interface Args { + mint: string + newUpdateAuthority?: string + renounceUpdateAuthority?: boolean + eid: EndpointId +} + +// sets the update authority via Metaplex +task('lz:oft:solana:set-update-authority', 'Updates the metaplex update authority of the SPL Token') + .addParam('eid', 'Solana mainnet (30168) or testnet (40168)', undefined, devtoolsTypes.eid) + .addParam('mint', 'The Token mint public key', undefined, devtoolsTypes.string) + .addOptionalParam('newUpdateAuthority', 'The new update authority', undefined, devtoolsTypes.string) + .addOptionalParam('renounceUpdateAuthority', 'Renounce update authority', false, devtoolsTypes.boolean) + .setAction( + async ({ eid, mint: mintStr, newUpdateAuthority: newUpdateAuthorityStr, renounceUpdateAuthority }: Args) => { + // if not renouncing, must provide new update authority + if (!renounceUpdateAuthority && !newUpdateAuthorityStr) { + throw new Error( + 'Either specify the new update authority via --new-update-authority or renounce via --renounce-update-authority true' + ) + } + + // if renouncing, must not provide new update authority + if (renounceUpdateAuthority && newUpdateAuthorityStr) { + throw new Error('Cannot provide new update authority if renouncing') + } + + /* + * On why the update authority is set to SystemProgram.programId ("11111111111111111111111111111111") when renouncing: + * The Metaplex Token Metadata program defines the update_authority strictly as a Pubkey: + * https://github.com/metaplex-foundation/mpl-token-metadata/blob/23aee718e723578ee5df411f045184e0ac9a9e63/programs/token-metadata/program/src/state/metadata.rs#L73 + * Hence, the value must always be a Pubkey + * To renounce the update authority, we can to set its value to SystemProgram ID ("11111111111111111111111111111111") + * This is done on top of setting `isMutable` to false + */ + + const updateAuthority = renounceUpdateAuthority + ? publicKey(SystemProgram.programId) + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + publicKey(newUpdateAuthorityStr!) // we already checked that this is defined + + const { umi, umiWalletSigner } = await deriveConnection(eid) + + const mint = publicKey(mintStr) + const initialMetadata = await fetchMetadataFromSeeds(umi, { mint }) + + if (initialMetadata.updateAuthority === SystemProgram.programId.toString()) { + console.log('\nThe update authority has already been renounced\n') + return + } + + if (initialMetadata.updateAuthority !== umiWalletSigner.publicKey.toString()) { + throw new Error('Only the update authority can update the metadata') + } + + console.log(`\nMint Address: ${mintStr}\n`) + console.log(`\nCurrent update authority: ${initialMetadata.updateAuthority}\n`) + console.log(`\nNew update authority: ${updateAuthority.toString()}\n`) + + if (renounceUpdateAuthority) { + const doContinue = await promptToContinue( + 'You have chosen `--renounce-update-authority true`. This means that the Update Authority will be immediately renounced. This is irreversible. Continue?' + ) + if (!doContinue) { + return + } + } + + const isMutable = renounceUpdateAuthority ? false : initialMetadata.isMutable + + // Verify that isMutable is true when not renouncing, can't be too safe. + if (!renounceUpdateAuthority && !isMutable) { + throw new Error('When not renouncing, `isMutable` must be true') + } + + const txn = await updateV1(umi, { + mint, + newUpdateAuthority: updateAuthority, + authority: umiWalletSigner, + isMutable: renounceUpdateAuthority ? false : isMutable, + }).sendAndConfirm(umi) + + const isTestnet = eid == EndpointId.SOLANA_V2_TESTNET + + console.log(`Txn link: ${getExplorerTxLink(bs58.encode(txn.signature), isTestnet)}`) + } + )