Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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"
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
},
"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"
}
}
15,360 changes: 8,473 additions & 6,887 deletions examples/oft-solana/pnpm-lock.yaml

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions examples/oft-solana/scripts/generate-features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import fs from 'fs'
import path from 'path'
import { $ } from 'zx'

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

interface FeaturesResponse {
features: FeatureInfo[]
}

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

for (const rpc of rpcEndpoints) {
try {
console.log(` Trying ${rpc}...`)
features = (await $`solana feature status -u ${rpc} --display-all --output json-compact`).json()
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: 'https://solana-rpc.publicnode.com',
Comment thread
nazreen marked this conversation as resolved.
Outdated
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)
})
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')]
23 changes: 23 additions & 0 deletions examples/oft-solana/test/anchor/got-shim.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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)
}
96 changes: 96 additions & 0 deletions examples/oft-solana/test/anchor/helpers/error-assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import assert from 'assert'

import { Program, ProgramError } from '@metaplex-foundation/umi'

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

export async function expectOftError<T extends ProgramError>(
operation: () => Promise<unknown>,
expectedErrorClass: new (...args: unknown[]) => T,
program: Program,
customMessage?: string
): Promise<void> {
try {
await operation()
assert.fail(`Expected ${expectedErrorClass.name} to be thrown, but operation succeeded`)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when the operation succeeds, ie doesnt throw, this throws an err that will be caught by L16. L16 won't match the expected err class, and then produce a confusing double-wrapped err message instead of something clear like Operation succeeded but should have failed. I think this even affects every negative test in the suite.

Is this intentional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! addressed

} catch (error: unknown) {
const errorMessage = customMessage || `Expected ${expectedErrorClass.name}`
assertOftError(error, expectedErrorClass, program, errorMessage)
}
}

export function assertOftError<T extends ProgramError>(
error: unknown,
expectedErrorClass: new (...args: unknown[]) => T,
program: Program,
message: string
): void {
const isMatch = isOftError(error, expectedErrorClass, program)

if (!isMatch) {
const actualInfo = getErrorInfo(error)
assert.fail(`${message}, but got: ${actualInfo}\nExpected: ${expectedErrorClass.name}`)
}
}

function isOftError<T extends ProgramError>(
error: unknown,
expectedErrorClass: new (...args: unknown[]) => T,
program: Program
): boolean {
const instance = new expectedErrorClass(program)
if (!('code' in instance) || typeof instance.code !== 'number') {
return false
}
const expectedMessage = `Error Code: ${instance.name}. Error Number: ${instance.code.toString(10)}`

if (
typeof error === 'object' &&
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
typeof error === 'object' &&
typeof error === 'object' &&
error !== null &&

typeof null === 'object' is true in JS, so the guard typeof error === 'object' passes for null. The func sig accepts unknown so it should be defensive.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! addressed

'message' in error &&
typeof error.message === 'string' &&
error.message.startsWith('Simulate Fail:')
) {
const msg = error.message.split('Simulate Fail:')[1]
return JSON.stringify(msg).includes(expectedMessage)
}

if (
typeof error !== 'object' ||
!('transactionError' in error) ||
typeof error.transactionError !== 'object' ||
!('logs' in error.transactionError)
) {
return false
}

const logs = error.transactionError.logs
return JSON.stringify(logs).includes(expectedMessage)
}

function getErrorInfo(error: unknown): string {
const err = error as { message?: string; code?: string; name?: string; logs?: string[] }
const parts = []

if (err.message) {
parts.push(`Message: "${err.message}"`)
}

if (err.code) {
parts.push(`Code: ${err.code}`)
}

if (err.name) {
parts.push(`Name: ${err.name}`)
}

if (err.logs) {
const errorLogs = err.logs.filter((log: string) => log.includes('Error Code:') || log.includes('Error Number:'))
if (errorLogs.length > 0) {
parts.push(`Logs: [${errorLogs.slice(0, 2).join(', ')}]`)
}
}

return parts.length > 0 ? parts.join(', ') : 'Unknown error format'
}

export const OftErrors = oft.errors
4 changes: 4 additions & 0 deletions examples/oft-solana/test/anchor/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { initOft, send } from './oft-layerzero-simulation'
export { quoteSend, quoteOft } from './oft-quotes'
export { createOftKeys, createOftKeySets, createKeypairFromSeed, createSignerFromSeed } from './test-keys'
export { expectOftError, assertOftError, OftErrors } from './error-assertions'
Loading
Loading