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: Ensure Solana CI/ Anchor test fails on faulty push #485

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/solana.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -17,7 +17,7 @@ build:


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

idl: build
@echo "IDL Version: $(VERSION)"
Expand Down
2 changes: 1 addition & 1 deletion solana/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
22 changes: 22 additions & 0 deletions solana/run-tests
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash

set -euo pipefail

RPC_PORT_NUMBER=8899
FAUCET_PORT_NUMBER=9900

# 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'

# kill solana validator if still running to avoid port already in use error
if pgrep solana-test-validator; then pkill solana-test-validator; fi
lsof -i tcp:${RPC_PORT_NUMBER} | awk 'NR!=1 {print $2}' | xargs kill
lsof -i tcp:${FAUCET_PORT_NUMBER} | awk 'NR!=1 {print $2}' | xargs kill
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();
});
});
});
Loading
Loading