Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

solana: Add Typescript test cases with more coverage to Solana CI/ Anchor Test #482

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion .github/workflows/solana.yml
Original file line number Diff line number Diff line change
@@ -146,5 +146,5 @@ jobs:
run: |
git diff --exit-code ts/idl
- name: Run tests
run: anchor test --skip-build
run: ./run-tests
shell: bash
2 changes: 1 addition & 1 deletion solana/Makefile
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ build:


test: idl sdk node_modules
anchor test --skip-build
./run-tests

idl: build
@echo "IDL Version: $(VERSION)"
2 changes: 1 addition & 1 deletion solana/package.json
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@
"build": "npm run build:esm && npm run build:cjs",
"rebuild": "npm run clean && npm run build",
"clean": "rm -rf ./dist",
"test:ci": "jest --config ./jest.config.ts",
"test:ci": "jest --config ./jest.config.ts --detectOpenHandles",
"build:contracts": "make build"
},
"devDependencies": {
14 changes: 14 additions & 0 deletions solana/run-tests
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash

set -euo pipefail

# Run each tests/*.test.ts file separately to avoid account state persisting between tests
for file in `ls tests/*.test.ts`
do
# convert file-name to FILE_NAME
filename=$(basename -- "$file")
filename="${filename%.test.*}"
env_flag="$(tr '[:lower:]' '[:upper:]' <<< ${filename//-/_})"

env $env_flag=1 bash -c 'anchor test --skip-build'
done
232 changes: 232 additions & 0 deletions solana/tests/transfer-fee-burning.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import * as anchor from "@coral-xyz/anchor";
import * as spl from "@solana/spl-token";
import {
SystemProgram,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
AccountAddress,
ChainAddress,
ChainContext,
Signer,
UniversalAddress,
Wormhole,
contracts,
encoding,
} from "@wormhole-foundation/sdk";
import * as testing from "@wormhole-foundation/sdk-definitions/testing";
import {
SolanaPlatform,
getSolanaSignAndSendSigner,
} from "@wormhole-foundation/sdk-solana";
import * as fs from "fs";
import { SolanaNtt } from "../ts/sdk/index.js";
import { handleTestSkip, signSendWait } from "./utils/index.js";

handleTestSkip(__filename);

const solanaRootDir = `${__dirname}/../`;

const CORE_BRIDGE_ADDRESS = contracts.coreBridge("Mainnet", "Solana");
const NTT_ADDRESS = anchor.workspace.ExampleNativeTokenTransfers.programId;

const w = new Wormhole("Devnet", [SolanaPlatform], {
chains: { Solana: { contracts: { coreBridge: CORE_BRIDGE_ADDRESS } } },
});

const remoteXcvr: ChainAddress = {
chain: "Ethereum",
address: new UniversalAddress(
encoding.bytes.encode("transceiver".padStart(32, "\0"))
),
};
const remoteMgr: ChainAddress = {
chain: "Ethereum",
address: new UniversalAddress(
encoding.bytes.encode("nttManager".padStart(32, "\0"))
),
};

const receiver = testing.utils.makeUniversalChainAddress("Ethereum");

const payerSecretKey = Uint8Array.from(
JSON.parse(
fs.readFileSync(`${solanaRootDir}/keys/test.json`, {
encoding: "utf-8",
})
)
);
const payer = anchor.web3.Keypair.fromSecretKey(payerSecretKey);

const connection = new anchor.web3.Connection(
"http://localhost:8899",
"confirmed"
);

// make sure we're using the exact same Connection obj for rpc
const ctx: ChainContext<"Devnet", "Solana"> = w
.getPlatform("Solana")
.getChain("Solana", connection);

const mintAuthority = anchor.web3.Keypair.generate();
const mintKeypair = anchor.web3.Keypair.generate();
const mint = mintKeypair.publicKey;
const transferFeeConfigAuthority = anchor.web3.Keypair.generate();
const withdrawWithheldAuthority = anchor.web3.Keypair.generate();
const decimals = 9;
const feeBasisPoints = 50;
const maxFee = BigInt(5_000);
const mintAmount = BigInt(1_000_000_000);

const transferAmount = 100_000n;

let signer: Signer;
let sender: AccountAddress<"Solana">;
let ntt: SolanaNtt<"Devnet", "Solana">;
let tokenAccount: anchor.web3.PublicKey;
let tokenAddress: string;

const TOKEN_PROGRAM = spl.TOKEN_2022_PROGRAM_ID;

describe("example-native-token-transfers", () => {
describe("Transfer Fee Burning", () => {
beforeAll(async () => {
try {
signer = await getSolanaSignAndSendSigner(connection, payer, {
//debug: true,
});
sender = Wormhole.parseAddress("Solana", signer.address());

// initialize mint
const extensions = [spl.ExtensionType.TransferFeeConfig];
const mintLen = spl.getMintLen(extensions);
const lamports = await connection.getMinimumBalanceForRentExemption(
mintLen
);
const transaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mint,
space: mintLen,
lamports,
programId: TOKEN_PROGRAM,
}),
spl.createInitializeTransferFeeConfigInstruction(
mint,
transferFeeConfigAuthority.publicKey,
withdrawWithheldAuthority.publicKey,
feeBasisPoints,
maxFee,
TOKEN_PROGRAM
),
spl.createInitializeMintInstruction(
mint,
decimals,
mintAuthority.publicKey,
null,
TOKEN_PROGRAM
)
);
await sendAndConfirmTransaction(
connection,
transaction,
[payer, mintKeypair],
undefined
);

// create and fund token account
tokenAccount = await spl.createAccount(
connection,
payer,
mint,
payer.publicKey,
undefined,
undefined,
TOKEN_PROGRAM
);
await spl.mintTo(
connection,
payer,
mint,
tokenAccount,
mintAuthority,
mintAmount,
[],
undefined,
TOKEN_PROGRAM
);

// create our contract client
tokenAddress = mint.toBase58();
ntt = new SolanaNtt("Devnet", "Solana", connection, {
...ctx.config.contracts,
ntt: {
token: tokenAddress,
manager: NTT_ADDRESS,
transceiver: { wormhole: NTT_ADDRESS },
},
});

// transfer mint authority to ntt
await spl.setAuthority(
connection,
payer,
mint,
mintAuthority,
spl.AuthorityType.MintTokens,
ntt.pdas.tokenAuthority(),
[],
undefined,
TOKEN_PROGRAM
);

// init
const initTxs = ntt.initialize(sender, {
mint,
outboundLimit: 100_000_000n,
mode: "burning",
});
await signSendWait(ctx, initTxs, signer);

// register
const registerTxs = ntt.registerTransceiver({
payer,
owner: payer,
transceiver: ntt.program.programId,
});
await signSendWait(ctx, registerTxs, signer);

// set Wormhole xcvr peer
const setXcvrPeerTxs = ntt.setWormholeTransceiverPeer(
remoteXcvr,
sender
);
await signSendWait(ctx, setXcvrPeerTxs, signer);

// set manager peer
const setPeerTxs = ntt.setPeer(remoteMgr, 18, 10_000_000n, sender);
await signSendWait(ctx, setPeerTxs, signer);
} catch (e) {
console.error("Failed to setup peer: ", e);
throw e;
}
});

it("Returns with error", async () => {
// TODO: keep or remove the `outboxItem` param?
// added as a way to keep tests the same but it technically breaks the Ntt interface
const outboxItem = anchor.web3.Keypair.generate();
const xferTxs = ntt.transfer(
sender,
transferAmount,
receiver,
{ queue: false, automatic: false, gasDropoff: 0n },
outboxItem
);
await expect(
signSendWait(ctx, xferTxs, signer, false, true)
).rejects.toThrow();
});
});
});
237 changes: 237 additions & 0 deletions solana/tests/transfer-fee-locking.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import * as anchor from "@coral-xyz/anchor";
import * as spl from "@solana/spl-token";
import {
SystemProgram,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
AccountAddress,
ChainAddress,
ChainContext,
Signer,
UniversalAddress,
Wormhole,
contracts,
encoding,
} from "@wormhole-foundation/sdk";
import * as testing from "@wormhole-foundation/sdk-definitions/testing";
import {
SolanaPlatform,
getSolanaSignAndSendSigner,
} from "@wormhole-foundation/sdk-solana";
import * as fs from "fs";
import { SolanaNtt } from "../ts/sdk/index.js";
import { handleTestSkip, signSendWait } from "./utils/index.js";

handleTestSkip(__filename);

const solanaRootDir = `${__dirname}/../`;

const CORE_BRIDGE_ADDRESS = contracts.coreBridge("Mainnet", "Solana");
const NTT_ADDRESS = anchor.workspace.ExampleNativeTokenTransfers.programId;

const w = new Wormhole("Devnet", [SolanaPlatform], {
chains: { Solana: { contracts: { coreBridge: CORE_BRIDGE_ADDRESS } } },
});

const remoteXcvr: ChainAddress = {
chain: "Ethereum",
address: new UniversalAddress(
encoding.bytes.encode("transceiver".padStart(32, "\0"))
),
};
const remoteMgr: ChainAddress = {
chain: "Ethereum",
address: new UniversalAddress(
encoding.bytes.encode("nttManager".padStart(32, "\0"))
),
};

const receiver = testing.utils.makeUniversalChainAddress("Ethereum");

const payerSecretKey = Uint8Array.from(
JSON.parse(
fs.readFileSync(`${solanaRootDir}/keys/test.json`, {
encoding: "utf-8",
})
)
);
const payer = anchor.web3.Keypair.fromSecretKey(payerSecretKey);

const connection = new anchor.web3.Connection(
"http://localhost:8899",
"confirmed"
);

// make sure we're using the exact same Connection obj for rpc
const ctx: ChainContext<"Devnet", "Solana"> = w
.getPlatform("Solana")
.getChain("Solana", connection);

const mintAuthority = anchor.web3.Keypair.generate();
const mintKeypair = anchor.web3.Keypair.generate();
const mint = mintKeypair.publicKey;
const transferFeeConfigAuthority = anchor.web3.Keypair.generate();
const withdrawWithheldAuthority = anchor.web3.Keypair.generate();
const decimals = 9;
const feeBasisPoints = 50;
const maxFee = BigInt(5_000);
const mintAmount = BigInt(1_000_000_000);

const transferAmount = 100_000n;

let signer: Signer;
let sender: AccountAddress<"Solana">;
let ntt: SolanaNtt<"Devnet", "Solana">;
let tokenAccount: anchor.web3.PublicKey;
let tokenAddress: string;

const TOKEN_PROGRAM = spl.TOKEN_2022_PROGRAM_ID;

describe("example-native-token-transfers", () => {
describe("Transfer Fee Locking", () => {
beforeAll(async () => {
try {
signer = await getSolanaSignAndSendSigner(connection, payer, {
//debug: true,
});
sender = Wormhole.parseAddress("Solana", signer.address());

// initialize mint
const extensions = [spl.ExtensionType.TransferFeeConfig];
const mintLen = spl.getMintLen(extensions);
const lamports = await connection.getMinimumBalanceForRentExemption(
mintLen
);
const transaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mint,
space: mintLen,
lamports,
programId: TOKEN_PROGRAM,
}),
spl.createInitializeTransferFeeConfigInstruction(
mint,
transferFeeConfigAuthority.publicKey,
withdrawWithheldAuthority.publicKey,
feeBasisPoints,
maxFee,
TOKEN_PROGRAM
),
spl.createInitializeMintInstruction(
mint,
decimals,
mintAuthority.publicKey,
null,
TOKEN_PROGRAM
)
);
await sendAndConfirmTransaction(
connection,
transaction,
[payer, mintKeypair],
undefined
);

// create and fund token account
tokenAccount = await spl.createAccount(
connection,
payer,
mint,
payer.publicKey,
undefined,
undefined,
TOKEN_PROGRAM
);
await spl.mintTo(
connection,
payer,
mint,
tokenAccount,
mintAuthority,
mintAmount,
[],
undefined,
TOKEN_PROGRAM
);

// create our contract client
tokenAddress = mint.toBase58();
ntt = new SolanaNtt("Devnet", "Solana", connection, {
...ctx.config.contracts,
ntt: {
token: tokenAddress,
manager: NTT_ADDRESS,
transceiver: { wormhole: NTT_ADDRESS },
},
});

// transfer mint authority to ntt
await spl.setAuthority(
connection,
payer,
mint,
mintAuthority,
spl.AuthorityType.MintTokens,
ntt.pdas.tokenAuthority(),
[],
undefined,
TOKEN_PROGRAM
);

// init
const initTxs = ntt.initialize(sender, {
mint,
outboundLimit: 100_000_000n,
mode: "locking",
});
await signSendWait(ctx, initTxs, signer);

// register
const registerTxs = ntt.registerTransceiver({
payer,
owner: payer,
transceiver: ntt.program.programId,
});
await signSendWait(ctx, registerTxs, signer);

// set Wormhole xcvr peer
const setXcvrPeerTxs = ntt.setWormholeTransceiverPeer(
remoteXcvr,
sender
);
await signSendWait(ctx, setXcvrPeerTxs, signer);

// set manager peer
const setPeerTxs = ntt.setPeer(remoteMgr, 18, 10_000_000n, sender);
await signSendWait(ctx, setPeerTxs, signer);
} catch (e) {
console.error("Failed to setup peer: ", e);
throw e;
}
});

it("Returns with BadAmountAfterTransfer error", async () => {
try {
// TODO: keep or remove the `outboxItem` param?
// added as a way to keep tests the same but it technically breaks the Ntt interface
const outboxItem = anchor.web3.Keypair.generate();
const xferTxs = ntt.transfer(
sender,
transferAmount,
receiver,
{ queue: false, automatic: false, gasDropoff: 0n },
outboxItem
);
await signSendWait(ctx, xferTxs, signer, false, true);
} catch (e) {
const error = anchor.AnchorError.parse(
(e as anchor.AnchorError).logs
)?.error;
expect(error?.errorMessage).toBe("BadAmountAfterTransfer");
}
});
});
});
404 changes: 404 additions & 0 deletions solana/tests/transfer-hook-burning.test.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import * as anchor from "@coral-xyz/anchor";
import * as spl from "@solana/spl-token";
import {
PublicKey,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
AccountAddress,
ChainAddress,
ChainContext,
Signer,
@@ -12,8 +19,6 @@ import {
encoding,
serialize,
serializePayload,
signSendWait as ssw,
AccountAddress,
} from "@wormhole-foundation/sdk";
import * as testing from "@wormhole-foundation/sdk-definitions/testing";
import {
@@ -23,10 +28,11 @@ import {
} from "@wormhole-foundation/sdk-solana";
import { SolanaWormholeCore } from "@wormhole-foundation/sdk-solana-core";
import * as fs from "fs";

import { PublicKey, SystemProgram, Transaction } from "@solana/web3.js";
import { DummyTransferHook } from "../ts/idl/1_0_0/ts/dummy_transfer_hook.js";
import { SolanaNtt } from "../ts/sdk/index.js";
import { handleTestSkip, signSendWait } from "./utils/index.js";

handleTestSkip(__filename);

const solanaRootDir = `${__dirname}/../`;

@@ -35,18 +41,6 @@ const GUARDIAN_KEY =
const CORE_BRIDGE_ADDRESS = contracts.coreBridge("Mainnet", "Solana");
const NTT_ADDRESS = anchor.workspace.ExampleNativeTokenTransfers.programId;

async function signSendWait(
chain: ChainContext<any, any, any>,
txs: AsyncGenerator<any>,
signer: Signer
) {
try {
await ssw(chain, txs, signer);
} catch (e) {
console.error(e);
}
}

const w = new Wormhole("Devnet", [SolanaPlatform], {
chains: { Solana: { contracts: { coreBridge: CORE_BRIDGE_ADDRESS } } },
});
@@ -73,19 +67,19 @@ const payerSecretKey = Uint8Array.from(
);
const payer = anchor.web3.Keypair.fromSecretKey(payerSecretKey);

const owner = anchor.web3.Keypair.generate();
const connection = new anchor.web3.Connection(
"http://localhost:8899",
"confirmed"
);

// Make sure we're using the exact same Connection obj for rpc
// make sure we're using the exact same Connection obj for rpc
const ctx: ChainContext<"Devnet", "Solana"> = w
.getPlatform("Solana")
.getChain("Solana", connection);

let tokenAccount: anchor.web3.PublicKey;

const mintAuthority = anchor.web3.Keypair.generate();
const mint = anchor.web3.Keypair.generate();

const dummyTransferHook = anchor.workspace
@@ -130,7 +124,6 @@ describe("example-native-token-transfers", () => {
const lamports = await connection.getMinimumBalanceForRentExemption(
mintLen
);

const transaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
@@ -141,26 +134,24 @@ describe("example-native-token-transfers", () => {
}),
spl.createInitializeTransferHookInstruction(
mint.publicKey,
owner.publicKey,
mintAuthority.publicKey,
dummyTransferHook.programId,
TOKEN_PROGRAM
),
spl.createInitializeMintInstruction(
mint.publicKey,
9,
owner.publicKey,
mintAuthority.publicKey,
null,
TOKEN_PROGRAM
)
);

const { blockhash } = await connection.getRecentBlockhash();

const { blockhash } = await connection.getLatestBlockhash();
transaction.feePayer = payer.publicKey;
transaction.recentBlockhash = blockhash;

const txid = await connection.sendTransaction(transaction, [payer, mint]);
await connection.confirmTransaction(txid, "confirmed");
await sendAndConfirmTransaction(connection, transaction, [payer, mint], {
commitment: "confirmed",
});

tokenAccount = await spl.createAssociatedTokenAccount(
connection,
@@ -177,15 +168,15 @@ describe("example-native-token-transfers", () => {
payer,
mint.publicKey,
tokenAccount,
owner,
mintAuthority,
10_000_000n,
undefined,
undefined,
TOKEN_PROGRAM
);

// create our contract client
tokenAddress = mint.publicKey.toBase58();
// Create our contract client
ntt = new SolanaNtt("Devnet", "Solana", connection, {
...ctx.config.contracts,
ntt: {
@@ -203,11 +194,12 @@ describe("example-native-token-transfers", () => {
describe("Locking", () => {
beforeAll(async () => {
try {
// transfer mint authority to ntt
await spl.setAuthority(
connection,
payer,
mint.publicKey,
owner,
mintAuthority,
spl.AuthorityType.MintTokens,
ntt.pdas.tokenAuthority(),
[],
@@ -219,7 +211,7 @@ describe("example-native-token-transfers", () => {
const initTxs = ntt.initialize(sender, {
mint: mint.publicKey,
outboundLimit: 1000000n,
mode: "burning",
mode: "locking",
});
await signSendWait(ctx, initTxs, signer);

@@ -231,14 +223,14 @@ describe("example-native-token-transfers", () => {
});
await signSendWait(ctx, registerTxs, signer);

// Set Wormhole xcvr peer
// set Wormhole xcvr peer
const setXcvrPeerTxs = ntt.setWormholeTransceiverPeer(
remoteXcvr,
sender
);
await signSendWait(ctx, setXcvrPeerTxs, signer);

// Set manager peer
// set manager peer
const setPeerTxs = ntt.setPeer(remoteMgr, 18, 1000000n, sender);
await signSendWait(ctx, setPeerTxs, signer);
} catch (e) {
@@ -265,13 +257,13 @@ describe("example-native-token-transfers", () => {
const transaction = new Transaction().add(
initializeExtraAccountMetaListInstruction
);
const { blockhash } = await connection.getLatestBlockhash();
transaction.feePayer = payer.publicKey;
const { blockhash } = await connection.getRecentBlockhash();
transaction.recentBlockhash = blockhash;

transaction.sign(payer);
const txid = await connection.sendTransaction(transaction, [payer]);
await connection.confirmTransaction(txid, "confirmed");
await sendAndConfirmTransaction(connection, transaction, [payer], {
commitment: "confirmed",
});
});

test("Can send tokens", async () => {
@@ -362,7 +354,6 @@ describe("example-native-token-transfers", () => {
throw e;
}

// expect(released).toEqual(true);
expect((await counterValue()).toString()).toEqual("2");
});
});
41 changes: 41 additions & 0 deletions solana/tests/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
ChainContext,
Signer,
signSendWait as ssw,
} from "@wormhole-foundation/sdk";
import path from "path";

const TESTFILE_MATCH_PATTERN = /.test.ts$/;

/**
* Skips test file execution if the corresponding environment variable is not set.
*
* eg:- To run `file-name.test.ts`, `FILE_NAME` environment variable should be set
*/
export const handleTestSkip = (filename: string) => {
const testName = path.basename(filename).replace(TESTFILE_MATCH_PATTERN, "");
const envVar = testName.replaceAll("-", "_").toUpperCase();
const shouldRun = process.env[envVar];
if (!shouldRun) {
test.only("Skipping all tests", () => {});
}
};

export const signSendWait = async (
chain: ChainContext<any, any, any>,
txs: AsyncGenerator<any>,
signer: Signer,
shouldLog = true,
shouldThrow = false
) => {
try {
await ssw(chain, txs, signer);
} catch (e) {
if (shouldLog) {
console.error(e);
}
if (shouldThrow) {
throw e;
}
}
};