Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/oft-solana-test-updates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@layerzerolabs/oft-solana-example": patch
---

Add OFT Solana test coverage for peer/config validation and fee withdrawal guards, plus a got shim for test runs.
1 change: 1 addition & 0 deletions examples/oft-solana/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ node_modules
coverage
coverage.json
target
test-ledger
typechain
typechain-types

Expand Down
2 changes: 1 addition & 1 deletion examples/oft-solana/Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ cluster = "Localnet"
wallet = "./junk-id.json"

[scripts]
test = "npx jest test/anchor"
test = "pnpm test:anchor"
19 changes: 12 additions & 7 deletions examples/oft-solana/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
"lint:js": "eslint '**/*.{js,ts,json}' && prettier --check .",
"lint:sol": "solhint 'contracts/**/*.sol'",
"test": "$npm_execpath run test:forge && $npm_execpath run test:hardhat",
"test:anchor": "anchor test",
"test:anchor": "$npm_execpath run test:generate-features && OFT_ID=9UovNrJD8pQyBLheeHNayuG1wJSEAoxkmM14vw5gcsTT anchor build --no-idl && $npm_execpath exec ts-mocha -b -p ./tsconfig.json -t 10000000 test/anchor/index.test.ts",
"test:forge": "forge test",
"test:generate-features": "ts-node scripts/generate-features.ts",
"test:hardhat": "hardhat test",
"test:scripts": "jest --config jest.config.ts --runInBand --testMatch \"**/*.script.test.ts\""
},
Expand All @@ -23,6 +24,11 @@
"ethers": "^5.7.2",
"hardhat-deploy": "^0.12.1"
},
"overrides": {
"@solana/web3.js": "^1.98.0",
"ethers": "^5.7.2",
"hardhat-deploy": "^0.12.1"
},
"devDependencies": {
"@coral-xyz/anchor": "^0.31.1",
"@ethersproject/abi": "^5.7.0",
Expand Down Expand Up @@ -66,6 +72,7 @@
"@metaplex-foundation/umi-eddsa-web3js": "^0.9.2",
"@metaplex-foundation/umi-public-keys": "^0.8.9",
"@metaplex-foundation/umi-web3js-adapters": "^0.9.2",
"@noble/secp256k1": "^1.7.1",
"@nomicfoundation/hardhat-ethers": "^3.0.5",
"@nomiclabs/hardhat-ethers": "^2.2.3",
"@nomiclabs/hardhat-waffle": "^2.0.6",
Expand All @@ -83,6 +90,7 @@
"@types/jest": "^29.5.12",
"@types/mocha": "^10.0.6",
"@types/node": "~18.18.14",
"axios": "^1.6.2",
"bs58": "^6.0.0",
"chai": "^4.4.1",
"concurrently": "~9.1.0",
Expand All @@ -102,8 +110,10 @@
"prettier": "^3.2.5",
"solhint": "^4.1.1",
"solidity-bytes-utils": "^0.8.2",
"ts-mocha": "^10.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.4"
"typescript": "^5.4.4",
"zx": "^8.1.3"
},
"engines": {
"node": ">=20.19.5"
Expand All @@ -114,10 +124,5 @@
"ethers": "^5.7.2",
"hardhat-deploy": "^0.12.1"
}
},
"overrides": {
"@solana/web3.js": "^1.98.0",
"ethers": "^5.7.2",
"hardhat-deploy": "^0.12.1"
}
}
1,606 changes: 909 additions & 697 deletions examples/oft-solana/pnpm-lock.yaml

Large diffs are not rendered by default.

92 changes: 92 additions & 0 deletions examples/oft-solana/scripts/generate-features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import fs from 'fs'
import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
import path from 'path'

interface FeatureInfo {
description: string
id: string
status: string
}

interface FeaturesResponse {
features: FeatureInfo[]
}

const execFileAsync = promisify(execFile)

async function fetchFeatures(rpc: string): Promise<FeaturesResponse> {
const { stdout } = await execFileAsync(
'solana',
['feature', 'status', '-u', rpc, '--display-all', '--output', 'json-compact'],
{ maxBuffer: 10 * 1024 * 1024 }
)

return JSON.parse(stdout.trim()) as FeaturesResponse
}

async function generateFeatures(): Promise<void> {
console.log('Retrieving mainnet feature flags...')

try {
const rpcEndpoints = [
'https://api.mainnet-beta.solana.com',
'https://solana-rpc.publicnode.com',
'https://rpc.ankr.com/solana',
]

let features: FeaturesResponse | null = null
let lastError: unknown = null
let successfulRpc: string | null = null

for (const rpc of rpcEndpoints) {
try {
console.log(` Trying ${rpc}...`)
features = await fetchFeatures(rpc)
successfulRpc = rpc
break
} catch (error) {
lastError = error
console.log(` Failed with ${rpc}, trying next...`)
}
}

if (!features) {
throw lastError || new Error('All RPC endpoints failed')
}

const inactiveFeatures = features.features.filter((feature) => feature.status === 'inactive')

console.log(`Found ${inactiveFeatures.length} inactive features`)

const targetDir = path.join(__dirname, '../target/programs')
const featuresFile = path.join(targetDir, 'features.json')

if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true })
}

const featuresData = {
timestamp: new Date().toISOString(),
source: successfulRpc,
totalFeatures: features.features.length,
inactiveFeatures,
inactiveCount: inactiveFeatures.length,
}

fs.writeFileSync(featuresFile, JSON.stringify(featuresData, null, 2))

console.log(`Features data saved to ${featuresFile}`)
console.log(`Cached ${inactiveFeatures.length} inactive features for faster test startup`)
} catch (error) {
console.error('Failed to retrieve features:', error)
process.exit(1)
}
}

;(async (): Promise<void> => {
await generateFeatures()
})().catch((err: unknown) => {
console.error(err)
process.exit(1)
})
3 changes: 2 additions & 1 deletion examples/oft-solana/tasks/aptos/aptosEndpointV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Uln302SetExecutorConfig,
Uln302SetUlnConfig,
Uln302UlnConfig,
Uln302UlnUserConfig,
UlnReadSetUlnConfig,
UlnReadUlnConfig,
UlnReadUlnUserConfig,
Expand Down Expand Up @@ -262,7 +263,7 @@ export class AptosEndpointV2 implements IEndpointV2 {
_oapp: OmniAddress,
_uln: OmniAddress,
_eid: EndpointId,
_config: any,
_config: Uln302UlnUserConfig,
_type: Uln302ConfigType
) {
return false
Expand Down
6 changes: 3 additions & 3 deletions examples/oft-solana/tasks/aptos/aptosSdkFactory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { OmniAddress, OmniPoint, OmniTransaction } from '@layerzerolabs/devtools'
import { Bytes, OmniAddress, OmniPoint, OmniTransaction } from '@layerzerolabs/devtools'
import { ChainType, EndpointId, endpointIdToChainType } from '@layerzerolabs/lz-definitions'
import { IOApp, OAppEnforcedOptionParam } from '@layerzerolabs/ua-devtools'

Expand Down Expand Up @@ -49,8 +49,8 @@ export function createAptosOAppFactory() {
async isDelegate(): Promise<boolean> {
return false
},
async getEnforcedOptions(): Promise<any> {
return {}
async getEnforcedOptions(_eid: EndpointId, _msgType: number): Promise<Bytes> {
return '0x'
},
async setEnforcedOptions(enforcedOptions: OAppEnforcedOptionParam[]): Promise<OmniTransaction> {
return createStubTransaction(`setEnforcedOptions(${enforcedOptions.length} options)`)
Expand Down
2 changes: 1 addition & 1 deletion examples/oft-solana/tasks/common/config.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const getNetworkName = (eid: EndpointId) => {
if (hardhatUnsupportedEids.includes(eid)) {
return `${chainName}-${env}`
} else {
return getNetworkNameForEid(eid as any)
return getNetworkNameForEid(eid)
}
}

Expand Down
12 changes: 10 additions & 2 deletions examples/oft-solana/tasks/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,23 @@ export const keyPair: CLIArgumentType<Keypair> = {
parse(name: string, value: string) {
return Keypair.fromSecretKey(decode(value))
},
validate() {},
validate(name: string, value: unknown) {
if (!(value instanceof Keypair)) {
throw new Error(`${name} is not a valid Keypair`)
}
},
}

export const publicKey: CLIArgumentType<PublicKey> = {
name: 'keyPair',
parse(name: string, value: string) {
return new PublicKey(value)
},
validate() {},
validate(name: string, value: unknown) {
if (!(value instanceof PublicKey)) {
throw new Error(`${name} is not a valid PublicKey`)
}
},
}

export interface SendResult {
Expand Down
2 changes: 1 addition & 1 deletion examples/oft-solana/tasks/common/wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ task(TASK_LZ_OAPP_WIRE)

try {
// Use the SDK to check if configs exist
const [sendConfig, receiveConfig] = await getSolanaUlnConfigPDAs(
const [_sendConfig, _receiveConfig] = await getSolanaUlnConfigPDAs(
connection.vector.to.eid,
await connectionFactory(connection.vector.from.eid),
new PublicKey(connection.config.sendLibrary),
Expand Down
10 changes: 7 additions & 3 deletions examples/oft-solana/tasks/evm/sendEvm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,13 @@ export async function sendEvm(
const { contracts } = typeof layerzeroConfig === 'function' ? await layerzeroConfig() : layerzeroConfig
const wrapper = contracts.find((c) => c.contract.eid === srcEid)
if (!wrapper) throw new Error(`No config for EID ${srcEid}`)
wrapperAddress = wrapper.contract.contractName
? (await srcEidHre.deployments.get(wrapper.contract.contractName)).address
: wrapper.contract.address!
if (wrapper.contract.contractName) {
wrapperAddress = (await srcEidHre.deployments.get(wrapper.contract.contractName)).address
} else if (wrapper.contract.address) {
wrapperAddress = wrapper.contract.address
} else {
throw new Error(`No contract address found for EID ${srcEid}`)
}
}
// 2️⃣ load OFT ABI
const oftArtifact = await srcEidHre.artifacts.readArtifact('OFT')
Expand Down
5 changes: 3 additions & 2 deletions examples/oft-solana/tasks/solana/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,11 @@ export const getOftStoreAddress = (eid: EndpointId): string | null => {
return null
}
return oftStore
} catch (err: any) {
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err)
DebugLogger.printWarning(
KnownWarnings.ERROR_LOADING_SOLANA_DEPLOYMENT,
`Could not load Solana deployment for ${endpointIdToNetwork(eid)} (eid ${eid}): ${err.message}`
`Could not load Solana deployment for ${endpointIdToNetwork(eid)} (eid ${eid}): ${message}`
)
return null
}
Expand Down
3 changes: 1 addition & 2 deletions examples/oft-solana/tasks/solana/setOutboundRateLimit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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 { 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'
Expand Down Expand Up @@ -47,7 +47,6 @@ task(
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(
Expand Down
13 changes: 10 additions & 3 deletions examples/oft-solana/tasks/solana/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,20 @@ export function parseDecimalToUnits(amount: string, decimals: number): bigint {
*/
export function silenceSolana429(connection: Connection): void {
const origWrite = process.stderr.write.bind(process.stderr)
process.stderr.write = ((chunk: any, ...args: any[]) => {
const str = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk
process.stderr.write = ((
chunk: string | Uint8Array,
encoding?: BufferEncoding | ((err?: Error) => void),
cb?: (err?: Error) => void
) => {
const str = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')
if (typeof str === 'string' && str.includes('429 Too Many Requests')) {
// swallow it
return true
}
// otherwise pass through
return origWrite(chunk, ...args)
if (typeof encoding === 'function') {
return origWrite(chunk, encoding)
}
return origWrite(chunk, encoding, cb)
}) as typeof process.stderr.write
}
33 changes: 33 additions & 0 deletions examples/oft-solana/test/anchor/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { publicKey } from '@metaplex-foundation/umi'
import { utils } from '@noble/secp256k1'

import { UMI } from '@layerzerolabs/lz-solana-sdk-v2'

export const SRC_EID = 50168
export const DST_EID = 50125
export const INVALID_EID = 999999 // Non-existent EID for testing
export const TON_EID = 50343

export const OFT_PROGRAM_ID = publicKey('9UovNrJD8pQyBLheeHNayuG1wJSEAoxkmM14vw5gcsTT')

export const DVN_SIGNERS = new Array(4).fill(0).map(() => utils.randomPrivateKey())

export const OFT_DECIMALS = 6

export const defaultMultiplierBps = 12500 // 125%

export const simpleMessageLib: UMI.SimpleMessageLibProgram.SimpleMessageLib =
new UMI.SimpleMessageLibProgram.SimpleMessageLib(UMI.SimpleMessageLibProgram.SIMPLE_MESSAGELIB_PROGRAM_ID)

export const endpoint: UMI.EndpointProgram.Endpoint = new UMI.EndpointProgram.Endpoint(
UMI.EndpointProgram.ENDPOINT_PROGRAM_ID
)
export const uln: UMI.UlnProgram.Uln = new UMI.UlnProgram.Uln(UMI.UlnProgram.ULN_PROGRAM_ID)
export const executor: UMI.ExecutorProgram.Executor = new UMI.ExecutorProgram.Executor(
UMI.ExecutorProgram.EXECUTOR_PROGRAM_ID
)
export const priceFeed: UMI.PriceFeedProgram.PriceFeed = new UMI.PriceFeedProgram.PriceFeed(
UMI.PriceFeedProgram.PRICEFEED_PROGRAM_ID
)

export const dvns = [publicKey('HtEYV4xB4wvsj5fgTkcfuChYpvGYzgzwvNhgDZQNh7wW')]
26 changes: 26 additions & 0 deletions examples/oft-solana/test/anchor/got-shim.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const Module = require('module');

const originalLoad = Module._load;

const gotStub = {
default: Object.assign(
async () => {
throw new Error('got stub: network client not available in tests');
},
{
get: async () => {
throw new Error('got stub: network client not available in tests');
},
post: async () => {
throw new Error('got stub: network client not available in tests');
},
}
),
};

Module._load = function (request, parent, isMain) {
if (request === 'got') {
return gotStub;
}
return originalLoad.call(this, request, parent, isMain);
};
Loading