diff --git a/.github/workflows/solana.yml b/.github/workflows/solana.yml index ad760544f..7d0f0b74a 100644 --- a/.github/workflows/solana.yml +++ b/.github/workflows/solana.yml @@ -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 diff --git a/solana/Makefile b/solana/Makefile index 2b2682543..902731a53 100644 --- a/solana/Makefile +++ b/solana/Makefile @@ -17,7 +17,7 @@ build: test: idl sdk node_modules - anchor test --skip-build + ./run-tests idl: build @echo "IDL Version: $(VERSION)" diff --git a/solana/package.json b/solana/package.json index 62a3d8b8a..01c9d6b4f 100644 --- a/solana/package.json +++ b/solana/package.json @@ -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": { diff --git a/solana/run-tests b/solana/run-tests new file mode 100755 index 000000000..ce45fa028 --- /dev/null +++ b/solana/run-tests @@ -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 \ No newline at end of file diff --git a/solana/tests/transfer-fee-burning.test.ts b/solana/tests/transfer-fee-burning.test.ts new file mode 100644 index 000000000..954209fb9 --- /dev/null +++ b/solana/tests/transfer-fee-burning.test.ts @@ -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(); + }); + }); +}); diff --git a/solana/tests/transfer-fee-locking.test.ts b/solana/tests/transfer-fee-locking.test.ts new file mode 100644 index 000000000..6764747de --- /dev/null +++ b/solana/tests/transfer-fee-locking.test.ts @@ -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"); + } + }); + }); +}); diff --git a/solana/tests/transfer-hook-burning.test.ts b/solana/tests/transfer-hook-burning.test.ts new file mode 100644 index 000000000..ef23a0bd2 --- /dev/null +++ b/solana/tests/transfer-hook-burning.test.ts @@ -0,0 +1,404 @@ +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, + UniversalAddress, + Wormhole, + contracts, + deserialize, + deserializePayload, + encoding, + serialize, + serializePayload, +} from "@wormhole-foundation/sdk"; +import * as testing from "@wormhole-foundation/sdk-definitions/testing"; +import { + SolanaAddress, + SolanaPlatform, + getSolanaSignAndSendSigner, +} from "@wormhole-foundation/sdk-solana"; +import { SolanaWormholeCore } from "@wormhole-foundation/sdk-solana-core"; +import * as fs from "fs"; +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}/../`; + +const GUARDIAN_KEY = + "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"; +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 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); + +let tokenAccount: anchor.web3.PublicKey; + +const mintAuthority = anchor.web3.Keypair.generate(); +const mint = anchor.web3.Keypair.generate(); + +const dummyTransferHook = anchor.workspace + .DummyTransferHook as anchor.Program; + +const [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()], + dummyTransferHook.programId +); + +const [counterPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("counter")], + dummyTransferHook.programId +); + +async function counterValue(): Promise { + const counter = await dummyTransferHook.account.counter.fetch(counterPDA); + return counter.count; +} + +const coreBridge = new SolanaWormholeCore("Devnet", "Solana", connection, { + coreBridge: CORE_BRIDGE_ADDRESS, +}); + +const TOKEN_PROGRAM = spl.TOKEN_2022_PROGRAM_ID; + +describe("example-native-token-transfers", () => { + let ntt: SolanaNtt<"Devnet", "Solana">; + let signer: Signer; + let sender: AccountAddress<"Solana">; + let tokenAddress: string; + + beforeAll(async () => { + try { + signer = await getSolanaSignAndSendSigner(connection, payer, { + //debug: true, + }); + sender = Wormhole.parseAddress("Solana", signer.address()); + + const extensions = [spl.ExtensionType.TransferHook]; + const mintLen = spl.getMintLen(extensions); + const lamports = await connection.getMinimumBalanceForRentExemption( + mintLen + ); + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: mint.publicKey, + space: mintLen, + lamports, + programId: TOKEN_PROGRAM, + }), + spl.createInitializeTransferHookInstruction( + mint.publicKey, + mintAuthority.publicKey, + dummyTransferHook.programId, + TOKEN_PROGRAM + ), + spl.createInitializeMintInstruction( + mint.publicKey, + 9, + mintAuthority.publicKey, + null, + TOKEN_PROGRAM + ) + ); + const { blockhash } = await connection.getLatestBlockhash(); + transaction.feePayer = payer.publicKey; + transaction.recentBlockhash = blockhash; + await sendAndConfirmTransaction(connection, transaction, [payer, mint], { + commitment: "confirmed", + }); + + tokenAccount = await spl.createAssociatedTokenAccount( + connection, + payer, + mint.publicKey, + payer.publicKey, + undefined, + TOKEN_PROGRAM, + spl.ASSOCIATED_TOKEN_PROGRAM_ID + ); + + await spl.mintTo( + connection, + payer, + mint.publicKey, + tokenAccount, + mintAuthority, + 10_000_000n, + undefined, + undefined, + TOKEN_PROGRAM + ); + + // create our contract client + tokenAddress = mint.publicKey.toBase58(); + ntt = new SolanaNtt("Devnet", "Solana", connection, { + ...ctx.config.contracts, + ntt: { + token: tokenAddress, + manager: NTT_ADDRESS, + transceiver: { wormhole: NTT_ADDRESS }, + }, + }); + } catch (e) { + console.error("Failed to setup solana token: ", e); + throw e; + } + }); + + describe("Burning", () => { + beforeAll(async () => { + try { + // transfer mint authority to ntt + await spl.setAuthority( + connection, + payer, + mint.publicKey, + mintAuthority, + spl.AuthorityType.MintTokens, + ntt.pdas.tokenAuthority(), + [], + undefined, + TOKEN_PROGRAM + ); + + // init + const initTxs = ntt.initialize(sender, { + mint: mint.publicKey, + outboundLimit: 1000000n, + 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, 1000000n, sender); + await signSendWait(ctx, setPeerTxs, signer); + } catch (e) { + console.error("Failed to setup peer: ", e); + throw e; + } + }); + + it("Create ExtraAccountMetaList Account", async () => { + const initializeExtraAccountMetaListInstruction = + await dummyTransferHook.methods + .initializeExtraAccountMetaList() + .accountsStrict({ + payer: payer.publicKey, + mint: mint.publicKey, + counter: counterPDA, + extraAccountMetaList: extraAccountMetaListPDA, + tokenProgram: TOKEN_PROGRAM, + associatedTokenProgram: spl.ASSOCIATED_TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .instruction(); + + const transaction = new Transaction().add( + initializeExtraAccountMetaListInstruction + ); + const { blockhash } = await connection.getLatestBlockhash(); + transaction.feePayer = payer.publicKey; + transaction.recentBlockhash = blockhash; + transaction.sign(payer); + await sendAndConfirmTransaction(connection, transaction, [payer], { + commitment: "confirmed", + }); + }); + + test("Can send tokens", async () => { + const amount = 100000n; + const sender = Wormhole.parseAddress("Solana", signer.address()); + + const receiver = testing.utils.makeUniversalChainAddress("Ethereum"); + + // 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, + amount, + receiver, + { queue: false, automatic: false, gasDropoff: 0n }, + outboxItem + ); + await signSendWait(ctx, xferTxs, signer); + + const wormholeMessage = ntt.pdas.wormholeMessageAccount( + outboxItem.publicKey + ); + + const unsignedVaa = await coreBridge.parsePostMessageAccount( + wormholeMessage + ); + + const transceiverMessage = deserializePayload( + "Ntt:WormholeTransfer", + unsignedVaa.payload + ); + + // assert that amount is what we expect + expect( + transceiverMessage.nttManagerPayload.payload.trimmedAmount + ).toMatchObject({ amount: 10000n, decimals: 8 }); + + // get from balance + const balance = await connection.getTokenAccountBalance(tokenAccount); + expect(balance.value.amount).toBe("9900000"); + }); + + it("Can receive tokens", async () => { + const emitter = new testing.mocks.MockEmitter( + remoteXcvr.address as UniversalAddress, + "Ethereum", + 0n + ); + + const guardians = new testing.mocks.MockGuardians(0, [GUARDIAN_KEY]); + const sender = Wormhole.parseAddress("Solana", signer.address()); + + const sendingTransceiverMessage = { + sourceNttManager: remoteMgr.address as UniversalAddress, + recipientNttManager: new UniversalAddress( + ntt.program.programId.toBytes() + ), + nttManagerPayload: { + id: encoding.bytes.encode("sequence1".padEnd(32, "0")), + sender: new UniversalAddress("FACE".padStart(64, "0")), + payload: { + trimmedAmount: { + amount: 10000n, + decimals: 8, + }, + sourceToken: new UniversalAddress("FAFA".padStart(64, "0")), + recipientAddress: new UniversalAddress(payer.publicKey.toBytes()), + recipientChain: "Solana", + }, + }, + transceiverPayload: new Uint8Array(), + } as const; + + const serialized = serializePayload( + "Ntt:WormholeTransfer", + sendingTransceiverMessage + ); + const published = emitter.publishMessage(0, serialized, 200); + const rawVaa = guardians.addSignatures(published, [0]); + const vaa = deserialize("Ntt:WormholeTransfer", serialize(rawVaa)); + + const redeemTxs = ntt.redeem([vaa], sender); + try { + await signSendWait(ctx, redeemTxs, signer); + } catch (e) { + console.error(e); + throw e; + } + + expect((await counterValue()).toString()).toEqual("2"); + }); + }); + + describe("Static Checks", () => { + const wh = new Wormhole("Devnet", [SolanaPlatform]); + const ctx = wh.getChain("Solana"); + const overrides = { + Solana: { + token: tokenAddress, + manager: NTT_ADDRESS, + transceiver: { wormhole: NTT_ADDRESS }, + }, + }; + + describe("ABI Versions Test", function () { + test("It initializes from Rpc", async function () { + const ntt = await SolanaNtt.fromRpc(connection, { + Solana: { + ...ctx.config, + contracts: { + ...ctx.config.contracts, + ntt: overrides["Solana"], + }, + }, + }); + expect(ntt).toBeTruthy(); + }); + + test("It initializes from constructor", async function () { + const ntt = new SolanaNtt("Devnet", "Solana", connection, { + ...ctx.config.contracts, + ...{ ntt: overrides["Solana"] }, + }); + expect(ntt).toBeTruthy(); + }); + + test("It gets the correct version", async function () { + const version = await SolanaNtt.getVersion( + connection, + { ntt: overrides["Solana"] }, + new SolanaAddress(payer.publicKey.toBase58()) + ); + expect(version).toBe("2.0.0"); + }); + }); + }); +}); diff --git a/solana/tests/anchor.test.ts b/solana/tests/transfer-hook-locking.test.ts similarity index 91% rename from solana/tests/anchor.test.ts rename to solana/tests/transfer-hook-locking.test.ts index 8f29f61f2..d0fc1e667 100644 --- a/solana/tests/anchor.test.ts +++ b/solana/tests/transfer-hook-locking.test.ts @@ -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, - txs: AsyncGenerator, - 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"); }); }); diff --git a/solana/tests/utils/index.ts b/solana/tests/utils/index.ts new file mode 100644 index 000000000..0afa033f2 --- /dev/null +++ b/solana/tests/utils/index.ts @@ -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, + txs: AsyncGenerator, + signer: Signer, + shouldLog = true, + shouldThrow = false +) => { + try { + await ssw(chain, txs, signer); + } catch (e) { + if (shouldLog) { + console.error(e); + } + if (shouldThrow) { + throw e; + } + } +};