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: 19 additions & 0 deletions examples/oft-solana/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
- Rust `1.84.1`
- Anchor `0.31.1`
- Solana CLI `2.2.20`
- Surfpool CLI (required for `pnpm test:anchor`)
- Docker `28.3.0`
- Node.js `>=20.19.5`
- `pnpm` (recommended) - or another package manager of your choice (npm, yarn)
Expand Down Expand Up @@ -360,6 +361,24 @@ Before deploying, ensure the following:
pnpm test
```

To run the Surfpool-backed Solana tests:

```bash
pnpm test:anchor
```

`pnpm test:anchor` starts a Surfnet forked from mainnet-beta by default. If mainnet-beta state blocks initialization (pre-existing PDAs), set a devnet upstream instead:

```bash
SURFPOOL_RPC_URL=https://api.devnet.solana.com pnpm test:anchor
```

To avoid upstream state entirely, deploy local LayerZero program binaries into Surfnet:

```bash
SURFPOOL_USE_LOCAL_PROGRAMS=1 pnpm test:anchor
```

### Adding other chains

To add additional chains to your OFT deployment:
Expand Down
5 changes: 4 additions & 1 deletion examples/oft-solana/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"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": "OFT_ID=$(node scripts/resolve-oft-id.js) 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:hardhat": "hardhat test",
"test:scripts": "jest --config jest.config.ts --runInBand --testMatch \"**/*.script.test.ts\""
Expand Down Expand Up @@ -69,6 +69,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 @@ -86,6 +87,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 @@ -105,6 +107,7 @@
"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"
},
Expand Down
2,074 changes: 1,144 additions & 930 deletions examples/oft-solana/pnpm-lock.yaml

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions examples/oft-solana/scripts/resolve-oft-id.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env node
'use strict';

// This script is used specifically in the `test:anchor` npm script to resolve
// the OFT program ID and output it to stdout. The test/anchor/constants.ts file
// has its own implementation since it needs the value at TypeScript import time.

const fs = require('fs');

const { Keypair } = require('@solana/web3.js');

const keypairPath = 'target/deploy/oft-keypair.json';
const fallbackId = '9UovNrJD8pQyBLheeHNayuG1wJSEAoxkmM14vw5gcsTT';

function resolveOftId() {
try {
if (!fs.existsSync(keypairPath)) {
return fallbackId;
}

const secret = JSON.parse(fs.readFileSync(keypairPath, 'utf8'));
return Keypair.fromSecretKey(Uint8Array.from(secret)).publicKey.toBase58();
} catch (error) {
return fallbackId;
}
}

process.stdout.write(resolveOftId());
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 @@ -163,7 +163,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 @@ -78,14 +78,21 @@ 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
}

Expand Down
55 changes: 55 additions & 0 deletions examples/oft-solana/test/anchor/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import fs from 'fs'
import path from 'path'

import { publicKey } from '@metaplex-foundation/umi'
import { utils } from '@noble/secp256k1'
import { Keypair } from '@solana/web3.js'

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

const DEFAULT_OFT_KEYPAIR = path.resolve(process.cwd(), 'target/deploy/oft-keypair.json')
const OFT_PROGRAM_ID_VALUE =
process.env.OFT_ID ?? readKeypairPublicKey(DEFAULT_OFT_KEYPAIR) ?? '9UovNrJD8pQyBLheeHNayuG1wJSEAoxkmM14vw5gcsTT'
const ENDPOINT_PROGRAM_ID_VALUE = process.env.LZ_ENDPOINT_PROGRAM_ID ?? '76y77prsiCMvXMjuoZ5VRrhG5qYBrUMYTE5WgHqgjEn6'
const ULN_PROGRAM_ID_VALUE = process.env.LZ_ULN_PROGRAM_ID ?? '7a4WjyR8VZ7yZz5XJAKm39BUGn5iT9CKcv2pmG9tdXVH'
const EXECUTOR_PROGRAM_ID_VALUE = process.env.LZ_EXECUTOR_PROGRAM_ID ?? '6doghB248px58JSSwG4qejQ46kFMW4AMj7vzJnWZHNZn'
const PRICEFEED_PROGRAM_ID_VALUE = process.env.LZ_PRICEFEED_PROGRAM_ID ?? '8ahPGPjEbpgGaZx2NV1iG5Shj7TDwvsjkEDcGWjt94TP'
const DVN_PROGRAM_IDS_VALUE = (process.env.LZ_DVN_PROGRAM_IDS ?? 'HtEYV4xB4wvsj5fgTkcfuChYpvGYzgzwvNhgDZQNh7wW')
.split(',')
.map((value) => value.trim())
.filter(Boolean)

export const OFT_PROGRAM_ID = publicKey(OFT_PROGRAM_ID_VALUE)

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

export const OFT_DECIMALS = 6

export const defaultMultiplierBps = 12500 // 125%

export const endpoint: UMI.EndpointProgram.Endpoint = new UMI.EndpointProgram.Endpoint(ENDPOINT_PROGRAM_ID_VALUE)
export const uln: UMI.UlnProgram.Uln = new UMI.UlnProgram.Uln(ULN_PROGRAM_ID_VALUE)
export const executor: UMI.ExecutorProgram.Executor = new UMI.ExecutorProgram.Executor(EXECUTOR_PROGRAM_ID_VALUE)
export const priceFeed: UMI.PriceFeedProgram.PriceFeed = new UMI.PriceFeedProgram.PriceFeed(PRICEFEED_PROGRAM_ID_VALUE)

export const dvns = DVN_PROGRAM_IDS_VALUE.map((value) => publicKey(value))

function readKeypairPublicKey(keypairPath: string): string | undefined {
if (!fs.existsSync(keypairPath)) {
return undefined
}

try {
const secret = JSON.parse(fs.readFileSync(keypairPath, 'utf-8')) as number[]
const keypair = Keypair.fromSecretKey(Uint8Array.from(secret))
return keypair.publicKey.toBase58()
} catch (error) {
console.warn(`Failed to read keypair at ${keypairPath}: ${String(error)}`)
return undefined
}
}
Loading