Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
11 changes: 8 additions & 3 deletions examples/oft-solana/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
"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 --require tests/got-shim.cjs tests/index.test.ts",
"test:forge": "forge test",
"test:hardhat": "hardhat test",
"test:scripts": "jest --config jest.config.ts --runInBand --testMatch \"**/*.script.test.ts\""
"test:scripts": "jest --config jest.config.ts --runInBand --testMatch \"**/*.script.test.ts\"",
"test:generate-features": "ts-node scripts/generate-features.ts"
},
"resolutions": {
"@solana/web3.js": "^1.98.0",
Expand Down Expand Up @@ -66,6 +67,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 +85,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 +105,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 Down
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)
})
32 changes: 32 additions & 0 deletions examples/oft-solana/tests/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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/tests/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)
}
94 changes: 94 additions & 0 deletions examples/oft-solana/tests/helpers/error-assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Program, ProgramError } from '@metaplex-foundation/umi'
import assert from 'assert'
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`)
} 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' &&
'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/tests/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'
107 changes: 107 additions & 0 deletions examples/oft-solana/tests/helpers/oft-layerzero-simulation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { KeypairSigner, PublicKey, Umi, publicKeyBytes } from '@metaplex-foundation/umi'
import { base58 } from '@metaplex-foundation/umi/serializers'
import { fromWeb3JsPublicKey, toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters'
import { Options } from '@layerzerolabs/lz-v2-utilities'
import { UMI } from '@layerzerolabs/lz-solana-sdk-v2'
import { TOKEN_PROGRAM_ID } from '@solana/spl-token'

import { endpoint, OFT_PROGRAM_ID } from '../constants'
import { TestContext, OftKeys, PacketSentEvent } from '../types'
import { sendAndConfirm } from '../utils'
import { oft, OFT_DECIMALS } from '@layerzerolabs/oft-v2-solana-sdk'

const SIMULATION_EXECUTION_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(200000, 200000 * 5)
const SIMULATION_COMPUTE_UNITS = 400000

export async function initOft(
umi: Umi,
keys: OftKeys,
oftType: oft.types.OFTType,
sharedDecimals = OFT_DECIMALS
): Promise<void> {
const ix = oft.initOft(
{
payer: keys.oappAdmin,
admin: keys.oappAdmin.publicKey,
mint: keys.mint.publicKey,
escrow: keys.escrow,
},
oftType,
sharedDecimals,
{
oft: OFT_PROGRAM_ID,
endpoint: endpoint.programId,
}
)

await sendAndConfirm(umi, ix, keys.oappAdmin)
}

export async function send(
context: TestContext,
oftKeys: OftKeys,
source: KeypairSigner,
sourceTokenAccount: PublicKey,
dest: PublicKey,
dstEid: number,
sendAmount: bigint,
fee: UMI.EndpointProgram.types.MessagingFee,
composeMsg?: Uint8Array,
minAmountLd: bigint = 0n
): Promise<PacketSentEvent> {
const { umi } = context

const ix = await oft.send(
umi.rpc,
{
payer: source,
tokenMint: oftKeys.mint.publicKey,
tokenEscrow: oftKeys.escrow.publicKey,
tokenSource: sourceTokenAccount,
},
{
dstEid,
to: publicKeyBytes(dest),
amountLd: sendAmount,
minAmountLd,
options: SIMULATION_EXECUTION_OPTIONS.toBytes(),
composeMsg,
nativeFee: fee.nativeFee,
lzTokenFee: fee.lzTokenFee,
},
{
oft: context.program.publicKey,
endpoint: endpoint.programId,
token: fromWeb3JsPublicKey(TOKEN_PROGRAM_ID),
}
)

const { signature } = await sendAndConfirm(
umi,
ix,
source,
SIMULATION_COMPUTE_UNITS,
context.lookupTable === undefined ? undefined : [context.lookupTable]
)

return extractPacketSentEvent(context, signature)
}

async function extractPacketSentEvent(context: TestContext, signature: Uint8Array): Promise<PacketSentEvent> {
const signatureBase58 = base58.deserialize(signature)[0]
const maxAttempts = 10
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const events = await UMI.extractEventFromTransactionSignature(
context.connection,
toWeb3JsPublicKey(endpoint.programId),
signatureBase58,
UMI.EndpointProgram.events.getPacketSentEventSerializer(),
{ commitment: 'confirmed', maxSupportedTransactionVersion: 0 }
)
if (events && events.length > 0) {
return events[0]
}
await new Promise((resolve) => setTimeout(resolve, 500))
}
throw new Error(`PacketSent event not found for signature ${signatureBase58}`)
}
Loading
Loading