Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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"
}
}
1,606 changes: 909 additions & 697 deletions examples/oft-solana/pnpm-lock.yaml

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions examples/oft-solana/scripts/generate-features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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

for (const rpc of rpcEndpoints) {
try {
console.log(` Trying ${rpc}...`)
features = await fetchFeatures(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: '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)
})
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