diff --git a/cross-chain/solana/package-lock.json b/cross-chain/solana/package-lock.json index ddcd6cb2d..caf320a81 100644 --- a/cross-chain/solana/package-lock.json +++ b/cross-chain/solana/package-lock.json @@ -9,6 +9,7 @@ }, "devDependencies": { "@certusone/wormhole-sdk": "^0.9.22", + "@metaplex-foundation/mpl-token-metadata": "^2.13.0", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.77.3", "@types/bn.js": "^5.1.0", @@ -1602,6 +1603,99 @@ "rlp": "^2.2.3" } }, + "node_modules/@metaplex-foundation/beet": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/beet/-/beet-0.7.1.tgz", + "integrity": "sha512-hNCEnS2WyCiYyko82rwuISsBY3KYpe828ubsd2ckeqZr7tl0WVLivGkoyA/qdiaaHEBGdGl71OpfWa2rqL3DiA==", + "dev": true, + "dependencies": { + "ansicolors": "^0.3.2", + "bn.js": "^5.2.0", + "debug": "^4.3.3" + } + }, + "node_modules/@metaplex-foundation/beet-solana": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/beet-solana/-/beet-solana-0.4.0.tgz", + "integrity": "sha512-B1L94N3ZGMo53b0uOSoznbuM5GBNJ8LwSeznxBxJ+OThvfHQ4B5oMUqb+0zdLRfkKGS7Q6tpHK9P+QK0j3w2cQ==", + "dev": true, + "dependencies": { + "@metaplex-foundation/beet": ">=0.1.0", + "@solana/web3.js": "^1.56.2", + "bs58": "^5.0.0", + "debug": "^4.3.4" + } + }, + "node_modules/@metaplex-foundation/beet-solana/node_modules/base-x": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==", + "dev": true + }, + "node_modules/@metaplex-foundation/beet-solana/node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "dev": true, + "dependencies": { + "base-x": "^4.0.0" + } + }, + "node_modules/@metaplex-foundation/beet-solana/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@metaplex-foundation/cusper": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/cusper/-/cusper-0.0.2.tgz", + "integrity": "sha512-S9RulC2fFCFOQraz61bij+5YCHhSO9llJegK8c8Y6731fSi6snUSQJdCUqYS8AIgR0TKbQvdvgSyIIdbDFZbBA==", + "dev": true + }, + "node_modules/@metaplex-foundation/mpl-token-metadata": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-2.13.0.tgz", + "integrity": "sha512-Fl/8I0L9rv4bKTV/RAl5YIbJe9SnQPInKvLz+xR1fEc4/VQkuCn3RPgypfUMEKWmCznzaw4sApDxy6CFS4qmJw==", + "dev": true, + "dependencies": { + "@metaplex-foundation/beet": "^0.7.1", + "@metaplex-foundation/beet-solana": "^0.4.0", + "@metaplex-foundation/cusper": "^0.0.2", + "@solana/spl-token": "^0.3.6", + "@solana/web3.js": "^1.66.2", + "bn.js": "^5.2.0", + "debug": "^4.3.4" + } + }, + "node_modules/@metaplex-foundation/mpl-token-metadata/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/@mysten/bcs": { "version": "0.7.1", "dev": true, @@ -2222,6 +2316,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", + "dev": true + }, "node_modules/anymatch": { "version": "3.1.3", "dev": true, diff --git a/cross-chain/solana/package.json b/cross-chain/solana/package.json index a18adfd59..ea15b6a98 100644 --- a/cross-chain/solana/package.json +++ b/cross-chain/solana/package.json @@ -8,6 +8,7 @@ }, "devDependencies": { "@certusone/wormhole-sdk": "^0.9.22", + "@metaplex-foundation/mpl-token-metadata": "^2.13.0", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.77.3", "@types/bn.js": "^5.1.0", diff --git a/cross-chain/solana/tests/01__tbtc.ts b/cross-chain/solana/tests/01__tbtc.ts index 0a70e3d93..7d3591ec0 100644 --- a/cross-chain/solana/tests/01__tbtc.ts +++ b/cross-chain/solana/tests/01__tbtc.ts @@ -1,363 +1,17 @@ import * as anchor from "@coral-xyz/anchor"; -import { Program, AnchorError } from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; import * as spl from "@solana/spl-token"; -import * as web3 from '@solana/web3.js'; +import { assert, expect } from "chai"; import { Tbtc } from "../target/types/tbtc"; -import { expect } from 'chai'; -import { ASSOCIATED_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/utils/token"; -import { transferLamports } from "./helpers/utils"; - -function maybeAuthorityAnd( - signer, - signers -) { - return signers.concat(signer instanceof (anchor.Wallet as any) ? [] : [signer]); -} - -async function setup( - program: Program, - authority -) { - const [config,] = getConfigPDA(program); - const [tbtcMintPDA, _] = getTokenPDA(program); - - await program.methods - .initialize() - .accounts({ - mint: tbtcMintPDA, - config, - authority: authority.publicKey - }) - .rpc(); -} - -async function checkState( - program: Program, - expectedAuthority, - expectedMinters, - expectedGuardians, - expectedTokensSupply -) { - const [config,] = getConfigPDA(program); - let configState = await program.account.config.fetch(config); - - expect(configState.authority).to.eql(expectedAuthority.publicKey); - expect(configState.numMinters).to.equal(expectedMinters); - expect(configState.numGuardians).to.equal(expectedGuardians); - - let tbtcMint = configState.mint; - - let mintState = await spl.getMint(program.provider.connection, tbtcMint); - - expect(mintState.supply).to.equal(BigInt(expectedTokensSupply)); -} - -async function changeAuthority( - program: Program, - authority, - newAuthority, -) { - const [config,] = getConfigPDA(program); - await program.methods - .changeAuthority() - .accounts({ - config, - authority: authority.publicKey, - newAuthority: newAuthority.publicKey, - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); -} - -async function takeAuthority( - program: Program, - newAuthority, -) { - const [config,] = getConfigPDA(program); - await program.methods - .takeAuthority() - .accounts({ - config, - pendingAuthority: newAuthority.publicKey, - }) - .signers(maybeAuthorityAnd(newAuthority, [])) - .rpc(); -} - -async function cancelAuthorityChange( - program: Program, - authority, -) { - const [config,] = getConfigPDA(program); - await program.methods - .cancelAuthorityChange() - .accounts({ - config, - authority: authority.publicKey, - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); -} - -async function checkPendingAuthority( - program: Program, - pendingAuthority, -) { - const [config,] = getConfigPDA(program); - let configState = await program.account.config.fetch(config); - expect(configState.pendingAuthority).to.eql(pendingAuthority.publicKey); -} - -async function checkNoPendingAuthority( - program: Program, -) { - const [config,] = getConfigPDA(program); - let configState = await program.account.config.fetch(config); - expect(configState.pendingAuthority).to.equal(null); -} - -async function checkPaused( - program: Program, - paused: boolean -) { - const [config,] = getConfigPDA(program); - let configState = await program.account.config.fetch(config); - expect(configState.paused).to.equal(paused); -} - - -function getConfigPDA( - program: Program, -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('config'), - ], - program.programId - ); -} - -function getTokenPDA( - program: Program, -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('tbtc-mint'), - ], - program.programId - ); -} - -function getMinterPDA( - program: Program, - minter -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('minter-info'), - minter.publicKey.toBuffer(), - ], - program.programId - ); -} - -async function addMinter( - program: Program, - authority, - minter, - payer -): Promise { - const [config,] = getConfigPDA(program); - const [minterInfoPDA, _] = getMinterPDA(program, minter); - await program.methods - .addMinter() - .accounts({ - config, - authority: authority.publicKey, - minter: minter.publicKey, - minterInfo: minterInfoPDA, - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); - return minterInfoPDA; -} - -async function checkMinter( - program: Program, - minter -) { - const [minterInfoPDA, bump] = getMinterPDA(program, minter); - let minterInfo = await program.account.minterInfo.fetch(minterInfoPDA); - - expect(minterInfo.minter).to.eql(minter.publicKey); - expect(minterInfo.bump).to.equal(bump); -} - -async function removeMinter( - program: Program, - authority, - minter, - minterInfo -) { - const [config,] = getConfigPDA(program); - await program.methods - .removeMinter() - .accounts({ - config, - authority: authority.publicKey, - minterInfo: minterInfo, - minter: minter.publicKey - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); -} - -function getGuardianPDA( - program: Program, - guardian -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('guardian-info'), - guardian.publicKey.toBuffer(), - ], - program.programId - ); -} - -async function addGuardian( - program: Program, - authority, - guardian, - payer -): Promise { - const [config,] = getConfigPDA(program); - const [guardianInfoPDA, _] = getGuardianPDA(program, guardian); - await program.methods - .addGuardian() - .accounts({ - config, - authority: authority.publicKey, - guardianInfo: guardianInfoPDA, - guardian: guardian.publicKey, - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); - return guardianInfoPDA; -} - -async function checkGuardian( - program: Program, - guardian -) { - const [guardianInfoPDA, bump] = getGuardianPDA(program, guardian); - let guardianInfo = await program.account.guardianInfo.fetch(guardianInfoPDA); - - expect(guardianInfo.guardian).to.eql(guardian.publicKey); - expect(guardianInfo.bump).to.equal(bump); -} - -async function removeGuardian( - program: Program, - authority, - guardian, - guardianInfo -) { - const [config,] = getConfigPDA(program); - await program.methods - .removeGuardian() - .accounts({ - config, - authority: authority.publicKey, - guardianInfo: guardianInfo, - guardian: guardian.publicKey - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); -} - -async function pause( - program: Program, - guardian -) { - const [config,] = getConfigPDA(program); - const [guardianInfoPDA, _] = getGuardianPDA(program, guardian); - await program.methods - .pause() - .accounts({ - config, - guardianInfo: guardianInfoPDA, - guardian: guardian.publicKey - }) - .signers([guardian]) - .rpc(); -} - -async function unpause( - program: Program, - authority -) { - const [config,] = getConfigPDA(program); - await program.methods - .unpause() - .accounts({ - config, - authority: authority.publicKey - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); -} - -async function mint( - program: Program, - minter, - minterInfoPDA, - recipient, - amount, - payer, -) { - const connection = program.provider.connection; - - const [config,] = getConfigPDA(program); - const [tbtcMintPDA, _] = getTokenPDA(program); - const recipientToken = spl.getAssociatedTokenAddressSync(tbtcMintPDA, recipient.publicKey); - - const tokenData = await spl.getAccount(connection, recipientToken).catch((err) => { - if (err instanceof spl.TokenAccountNotFoundError) { - return null; - } else { - throw err; - }; - }); - - if (tokenData === null) { - const tx = await web3.sendAndConfirmTransaction( - connection, - new web3.Transaction().add( - spl.createAssociatedTokenAccountIdempotentInstruction( - payer.publicKey, - recipientToken, - recipient.publicKey, - tbtcMintPDA, - ) - ), - [payer.payer] - ); - } - - - await program.methods - .mint(new anchor.BN(amount)) - .accounts({ - mint: tbtcMintPDA, - config, - minterInfo: minterInfoPDA, - minter: minter.publicKey, - recipientToken, - }) - .signers(maybeAuthorityAnd(payer, [minter])) - .rpc(); -} +import * as tbtc from "./helpers/tbtc"; +import { + expectIxFail, + expectIxSuccess, + getOrCreateAta, + getTokenBalance, + sleep, + transferLamports, +} from "./helpers/utils"; describe("tbtc", () => { // Configure the client to use the local cluster. @@ -365,348 +19,805 @@ describe("tbtc", () => { const program = anchor.workspace.Tbtc as Program; - const authority = (program.provider as anchor.AnchorProvider).wallet as anchor.Wallet; + const authority = ( + (program.provider as anchor.AnchorProvider).wallet as anchor.Wallet + ).payer; const newAuthority = anchor.web3.Keypair.generate(); - const minterKeys = anchor.web3.Keypair.generate(); - const minter2Keys = anchor.web3.Keypair.generate(); - const impostorKeys = anchor.web3.Keypair.generate(); - const guardianKeys = anchor.web3.Keypair.generate(); - const guardian2Keys = anchor.web3.Keypair.generate(); - - const recipientKeys = anchor.web3.Keypair.generate(); - - it('setup', async () => { - await setup(program, authority); - await checkState(program, authority, 0, 0, 0); - }); - - it('change authority', async () => { - await checkState(program, authority, 0, 0, 0); - await checkNoPendingAuthority(program); - try { - await cancelAuthorityChange(program, authority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('NoPendingAuthorityChange'); - expect(err.program.equals(program.programId)).is.true; - } - try { - await takeAuthority(program, newAuthority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('NoPendingAuthorityChange'); - expect(err.program.equals(program.programId)).is.true; - } - - await changeAuthority(program, authority, newAuthority); - await checkPendingAuthority(program, newAuthority); - await takeAuthority(program, newAuthority); - await checkNoPendingAuthority(program); - await checkState(program, newAuthority, 0, 0, 0); - await changeAuthority(program, newAuthority, authority.payer); - try { - await takeAuthority(program, impostorKeys); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotPendingAuthority'); - expect(err.program.equals(program.programId)).is.true; - } - try { - await takeAuthority(program, newAuthority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotPendingAuthority'); - expect(err.program.equals(program.programId)).is.true; - } - try { - await cancelAuthorityChange(program, authority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotAuthority'); - expect(err.program.equals(program.programId)).is.true; - } - await takeAuthority(program, authority); - - await checkState(program, authority, 0, 0, 0); - }) - - it('add minter', async () => { - await checkState(program, authority, 0, 0, 0); - await addMinter(program, authority, minterKeys, authority); - await checkMinter(program, minterKeys); - await checkState(program, authority, 1, 0, 0); - - // Transfer lamports to imposter. - await transferLamports(program.provider.connection, authority.payer, impostorKeys.publicKey, 1000000000); - // await web3.sendAndConfirmTransaction( - // program.provider.connection, - // new web3.Transaction().add( - // web3.SystemProgram.transfer({ - // fromPubkey: authority.publicKey, - // toPubkey: impostorKeys.publicKey, - // lamports: 1000000000, - // }) - // ), - // [authority.payer] - // ); - - try { - await addMinter(program, impostorKeys, minter2Keys, authority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotAuthority'); - expect(err.program.equals(program.programId)).is.true; - } - }); - - it('mint', async () => { - await checkState(program, authority, 1, 0, 0); - const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); - await checkMinter(program, minterKeys); - - // await setupMint(program, authority, recipientKeys); - await mint(program, minterKeys, minterInfoPDA, recipientKeys, 1000, authority); - - await checkState(program, authority, 1, 0, 1000); - - // // Burn for next test. - // const ix = spl.createBurnCheckedInstruction( - // account, // PublicKey of Owner's Associated Token Account - // new PublicKey(MINT_ADDRESS), // Public Key of the Token Mint Address - // WALLET.publicKey, // Public Key of Owner's Wallet - // BURN_QUANTITY * (10**MINT_DECIMALS), // Number of tokens to burn - // MINT_DECIMALS // Number of Decimals of the Token Mint - // ) - - }); - - it('won\'t mint', async () => { - await checkState(program, authority, 1, 0, 1000); - const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); - await checkMinter(program, minterKeys); - - // await setupMint(program, authority, recipientKeys); - - try { - await mint(program, impostorKeys, minterInfoPDA, recipientKeys, 1000, authority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('ConstraintSeeds'); - expect(err.program.equals(program.programId)).is.true; - } - }); - - it('use two minters', async () => { - await checkState(program, authority, 1, 0, 1000); - const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); - await checkMinter(program, minterKeys); - const minter2InfoPDA = await addMinter(program, authority, minter2Keys, authority); - await checkMinter(program, minter2Keys); - await checkState(program, authority, 2, 0, 1000); - // await setupMint(program, authority, recipientKeys); - - // cannot mint with wrong keys - try { - await mint(program, minter2Keys, minterInfoPDA, recipientKeys, 1000, authority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('ConstraintSeeds'); - expect(err.program.equals(program.programId)).is.true; - } - - // cannot remove minter with wrong keys - try { - await removeMinter(program, authority, minter2Keys, minterInfoPDA); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('ConstraintSeeds'); - expect(err.program.equals(program.programId)).is.true; - } - - await mint(program, minterKeys, minterInfoPDA, recipientKeys, 500, authority); - await checkState(program, authority, 2, 0, 1500); - }); - - it('remove minter', async () => { - await checkState(program, authority, 2, 0, 1500); - const [minter2InfoPDA, _] = getMinterPDA(program, minter2Keys); - await checkMinter(program, minter2Keys); - await removeMinter(program, authority, minter2Keys, minter2InfoPDA); - await checkState(program, authority, 1, 0, 1500); + const minter = anchor.web3.Keypair.generate(); + const anotherMinter = anchor.web3.Keypair.generate(); + const imposter = anchor.web3.Keypair.generate(); + const guardian = anchor.web3.Keypair.generate(); + const anotherGuardian = anchor.web3.Keypair.generate(); + + const recipient = anchor.web3.Keypair.generate(); + const txPayer = anchor.web3.Keypair.generate(); + + it("set up payers", async () => { + await transferLamports(authority, newAuthority.publicKey, 10000000000); + await transferLamports(authority, imposter.publicKey, 10000000000); + await transferLamports(authority, recipient.publicKey, 10000000000); + await transferLamports(authority, txPayer.publicKey, 10000000000); }); - it('won\'t remove minter', async () => { - await checkState(program, authority, 1, 0, 1500); - const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); - await checkMinter(program, minterKeys); - - try { - await removeMinter(program, impostorKeys, minterKeys, minterInfoPDA); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotAuthority'); - expect(err.program.equals(program.programId)).is.true; - } - - await removeMinter(program, authority, minterKeys, minterInfoPDA); - await checkState(program, authority, 0, 0, 1500); - - try { - await removeMinter(program, authority, minterKeys, minterInfoPDA); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('AccountNotInitialized'); - expect(err.program.equals(program.programId)).is.true; - } - }); - - it('add guardian', async () => { - await checkState(program, authority, 0, 0, 1500); - await addGuardian(program, authority, guardianKeys, authority); - await checkGuardian(program, guardianKeys); - await checkState(program, authority, 0, 1, 1500); - - try { - await addGuardian(program, impostorKeys, guardian2Keys, authority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotAuthority'); - expect(err.program.equals(program.programId)).is.true; - } - }); - - it('remove guardian', async () => { - await checkState(program, authority, 0, 1, 1500); - const [guardianInfoPDA, _] = getGuardianPDA(program, guardianKeys); - await checkGuardian(program, guardianKeys); - - try { - await removeGuardian(program, impostorKeys, guardianKeys, guardianInfoPDA); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotAuthority'); - expect(err.program.equals(program.programId)).is.true; - } - - await removeGuardian(program, authority, guardianKeys, guardianInfoPDA); - await checkState(program, authority, 0, 0, 1500); - - try { - await removeGuardian(program, authority, guardianKeys, guardianInfoPDA); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('AccountNotInitialized'); - expect(err.program.equals(program.programId)).is.true; - } + it("initialize", async () => { + const ix = await tbtc.initializeIx({ authority: authority.publicKey }); + await expectIxSuccess([ix], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(0), + paused: false, + pendingAuthority: null, + }); }); - it('pause', async () => { - await checkState(program, authority, 0, 0, 1500); - await addGuardian(program, authority, guardianKeys, authority); - await checkPaused(program, false); - await pause(program, guardianKeys); - await checkPaused(program, true); + describe("authority changes", () => { + it("cannot cancel authority if no pending", async () => { + const failedCancelIx = await tbtc.cancelAuthorityChangeIx({ + authority: authority.publicKey, + }); + await expectIxFail( + [failedCancelIx], + [authority], + "NoPendingAuthorityChange" + ); + }); + + it("cannot take authority if no pending", async () => { + const failedTakeIx = await tbtc.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxFail( + [failedTakeIx], + [newAuthority], + "NoPendingAuthorityChange" + ); + }); + + it("change authority to new authority", async () => { + const changeIx = await tbtc.changeAuthorityIx({ + authority: authority.publicKey, + newAuthority: newAuthority.publicKey, + }); + await expectIxSuccess([changeIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(0), + paused: false, + pendingAuthority: newAuthority.publicKey, + }); + }); + + it("take as new authority", async () => { + // Bug in validator? Need to wait a bit for new blockhash. + await sleep(10000); + + const takeIx = await tbtc.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxSuccess([takeIx], [newAuthority]); + await tbtc.checkConfig({ + authority: newAuthority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(0), + paused: false, + pendingAuthority: null, + }); + }); + + it("change pending authority back to original authority", async () => { + const changeBackIx = await tbtc.changeAuthorityIx({ + authority: newAuthority.publicKey, + newAuthority: authority.publicKey, + }); + await expectIxSuccess([changeBackIx], [newAuthority]); + await tbtc.checkConfig({ + authority: newAuthority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(0), + paused: false, + pendingAuthority: authority.publicKey, + }); + }); + + it("cannot take as signers that are not pending authority", async () => { + const failedImposterTakeIx = await tbtc.takeAuthorityIx({ + pendingAuthority: imposter.publicKey, + }); + await expectIxFail( + [failedImposterTakeIx], + [imposter], + "IsNotPendingAuthority" + ); + + const failedNewAuthorityTakeIx = await tbtc.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxFail( + [failedNewAuthorityTakeIx], + [newAuthority], + "IsNotPendingAuthority" + ); + }); + + it("cannot cancel as someone else", async () => { + const anotherFailedCancelIx = await tbtc.cancelAuthorityChangeIx({ + authority: authority.publicKey, + }); + await expectIxFail( + [anotherFailedCancelIx], + [authority], + "IsNotAuthority" + ); + }); + + it("finally take as authority", async () => { + const anotherTakeIx = await tbtc.takeAuthorityIx({ + pendingAuthority: authority.publicKey, + }); + await expectIxSuccess([anotherTakeIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(0), + paused: false, + pendingAuthority: null, + }); + }); }); - it('unpause', async () => { - await checkState(program, authority, 0, 1, 1500); - await checkPaused(program, true); - await unpause(program, authority); - await checkPaused(program, false); - - try { - await unpause(program, authority); - - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotPaused'); - expect(err.program.equals(program.programId)).is.true; - } + describe("minting", () => { + it("cannot add minter without authority", async () => { + const cannotAddMinterIx = await tbtc.addMinterIx({ + authority: imposter.publicKey, + minter: minter.publicKey, + }); + await expectIxFail([cannotAddMinterIx], [imposter], "IsNotAuthority"); + }); + + it("add minter", async () => { + const mustBeNull = await tbtc + .checkMinterInfo(minter.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "minter info found"); + + const addMinterIx = await tbtc.addMinterIx({ + authority: authority.publicKey, + minter: minter.publicKey, + }); + await expectIxSuccess([addMinterIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(0), + paused: false, + pendingAuthority: null, + }); + await tbtc.checkMinterInfo(minter.publicKey); + }); + + it("mint", async () => { + const amount = BigInt(1000); + + const recipientToken = await getOrCreateAta( + authority, + tbtc.getMintPDA(), + recipient.publicKey + ); + const recipientBefore = await getTokenBalance(recipientToken); + expect(recipientBefore).to.equal(BigInt(0)); + + const mintIx = await tbtc.mintIx( + { + minter: minter.publicKey, + recipientToken, + }, + new anchor.BN(amount.toString()) + ); + await expectIxSuccess([mintIx], [txPayer, minter]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(1000), + paused: false, + pendingAuthority: null, + }); + + const recipientAfter = await getTokenBalance(recipientToken); + expect(recipientAfter).to.equal(amount); + }); + + it("cannot mint without minter", async () => { + const recipientToken = spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + + const cannotMintIx = await tbtc.mintIx( + { + minter: imposter.publicKey, + recipientToken, + }, + new anchor.BN(420) + ); + await expectIxFail( + [cannotMintIx], + [txPayer, imposter], + "AccountNotInitialized" + ); + + // Now try with actual minter's info account. + const minterInfo = tbtc.getMinterInfoPDA(minter.publicKey); + + const cannotMintAgainIx = await tbtc.mintIx( + { + minterInfo, + minter: imposter.publicKey, + recipientToken, + }, + new anchor.BN(420) + ); + await expectIxFail( + [cannotMintAgainIx], + [txPayer, imposter], + "ConstraintSeeds" + ); + }); + + it("add another minter", async () => { + const mustBeNull = await tbtc + .checkMinterInfo(anotherMinter.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "minter info found"); + + const addMinterIx = await tbtc.addMinterIx({ + authority: authority.publicKey, + minter: anotherMinter.publicKey, + }); + await expectIxSuccess([addMinterIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 2, + numGuardians: 0, + supply: BigInt(1000), + paused: false, + pendingAuthority: null, + }); + await tbtc.checkMinterInfo(anotherMinter.publicKey); + }); + + it("cannot remove minter with wrong key", async () => { + const minterInfo = tbtc.getMinterInfoPDA(minter.publicKey); + const cannotRemoveIx = await tbtc.removeMinterIx({ + authority: authority.publicKey, + minterInfo, + minter: anotherMinter.publicKey, + }); + await expectIxFail([cannotRemoveIx], [authority], "ConstraintSeeds"); + }); + + it("mint with another minter", async () => { + const amount = BigInt(500); + + const recipientToken = await spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + const recipientBefore = await getTokenBalance(recipientToken); + expect(recipientBefore).to.equal(BigInt(1000)); + + const mintIx = await tbtc.mintIx( + { + minter: anotherMinter.publicKey, + recipientToken, + }, + new anchor.BN(amount.toString()) + ); + await expectIxSuccess([mintIx], [txPayer, anotherMinter]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 2, + numGuardians: 0, + supply: BigInt(1500), + paused: false, + pendingAuthority: null, + }); + + const recipientAfter = await getTokenBalance(recipientToken); + expect(recipientAfter).to.equal(recipientBefore + amount); + }); + + it("cannot remove minter without authority", async () => { + const cannotRemoveIx = await tbtc.removeMinterIx({ + authority: imposter.publicKey, + minter: anotherMinter.publicKey, + }); + await expectIxFail([cannotRemoveIx], [imposter], "IsNotAuthority"); + }); + + it("remove minter", async () => { + const removeIx = await tbtc.removeMinterIx({ + authority: authority.publicKey, + minter: anotherMinter.publicKey, + }); + await expectIxSuccess([removeIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(1500), + paused: false, + pendingAuthority: null, + }); + const mustBeNull = await tbtc + .checkMinterInfo(anotherMinter.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "minter info found"); + }); + + it("cannot remove same minter again", async () => { + const cannotRemoveIx = await tbtc.removeMinterIx({ + authority: authority.publicKey, + minter: anotherMinter.publicKey, + }); + await expectIxFail( + [cannotRemoveIx], + [authority], + "AccountNotInitialized" + ); + }); + + it("remove last minter", async () => { + const removeIx = await tbtc.removeMinterIx({ + authority: authority.publicKey, + minter: minter.publicKey, + }); + await expectIxSuccess([removeIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(1500), + paused: false, + pendingAuthority: null, + }); + const mustBeNull = await tbtc + .checkMinterInfo(minter.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "minter info found"); + }); }); - it('won\'t mint when paused', async () => { - await checkState(program, authority, 0, 1, 1500); - const minterInfoPDA = await addMinter(program, authority, minterKeys, authority); - await pause(program, guardianKeys); - // await setupMint(program, authority, recipientKeys); - - try { - await mint(program, minterKeys, minterInfoPDA, recipientKeys, 1000, authority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsPaused'); - expect(err.program.equals(program.programId)).is.true; - } - - await unpause(program, authority); - await checkPaused(program, false); - }) - - it('use two guardians', async () => { - await checkState(program, authority, 1, 1, 1500); - const [guardianInfoPDA, _] = getGuardianPDA(program, guardianKeys); - await checkGuardian(program, guardianKeys); - await addGuardian(program, authority, guardian2Keys, authority); - await checkGuardian(program, guardian2Keys); - - await pause(program, guardianKeys); - - try { - await pause(program, guardian2Keys); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsPaused'); - expect(err.program.equals(program.programId)).is.true; - } - - await unpause(program, authority); - await pause(program, guardian2Keys); - await checkPaused(program, true); - await unpause(program, authority); - - // cannot remove guardian with wrong keys - try { - await removeGuardian(program, authority, guardian2Keys, guardianInfoPDA); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('ConstraintSeeds'); - expect(err.program.equals(program.programId)).is.true; - } + describe("guardians", () => { + it("cannot add guardian without authority", async () => { + const cannotAddIx = await tbtc.addGuardianIx({ + authority: imposter.publicKey, + guardian: guardian.publicKey, + }); + await expectIxFail([cannotAddIx], [imposter], "IsNotAuthority"); + }); + + it("add guardian", async () => { + const mustBeNull = await tbtc + .checkGuardianInfo(guardian.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "guardian info found"); + + const addIx = await tbtc.addGuardianIx({ + authority: authority.publicKey, + guardian: guardian.publicKey, + }); + await expectIxSuccess([addIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 1, + supply: BigInt(1500), + paused: false, + pendingAuthority: null, + }); + await tbtc.checkGuardianInfo(guardian.publicKey); + }); + + it("cannot pause without guardian", async () => { + const cannotPauseIx = await tbtc.pauseIx({ + guardian: imposter.publicKey, + }); + await expectIxFail( + [cannotPauseIx], + [txPayer, imposter], + "AccountNotInitialized" + ); + + // Now try with actual guardian's info account. + const guardianInfo = tbtc.getGuardianInfoPDA(guardian.publicKey); + + const cannotPauseAgainIx = await tbtc.pauseIx({ + guardianInfo, + guardian: imposter.publicKey, + }); + await expectIxFail( + [cannotPauseAgainIx], + [txPayer, imposter], + "ConstraintSeeds" + ); + }); + + it("add minter and mint", async () => { + const addMinterIx = await tbtc.addMinterIx({ + authority: authority.publicKey, + minter: minter.publicKey, + }); + await expectIxSuccess([addMinterIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 1, + supply: BigInt(1500), + paused: false, + pendingAuthority: null, + }); + + const amount = BigInt(100); + + const recipientToken = spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + const recipientBefore = await getTokenBalance(recipientToken); + expect(recipientBefore).to.equal(BigInt(1500)); + + const mintIx = await tbtc.mintIx( + { + minter: minter.publicKey, + recipientToken, + }, + new anchor.BN(amount.toString()) + ); + await expectIxSuccess([mintIx], [txPayer, minter]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 1, + supply: BigInt(1600), + paused: false, + pendingAuthority: null, + }); + + const recipientAfter = await getTokenBalance(recipientToken); + expect(recipientAfter).to.equal(recipientBefore + amount); + }); + + it("pause", async () => { + const pauseIx = await tbtc.pauseIx({ + guardian: guardian.publicKey, + }); + await expectIxSuccess([pauseIx], [txPayer, guardian]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 1, + supply: BigInt(1600), + paused: true, + pendingAuthority: null, + }); + }); + + it("cannot mint while paused", async () => { + const recipientToken = spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + + const mintIx = await tbtc.mintIx( + { + minter: minter.publicKey, + recipientToken, + }, + new anchor.BN(100) + ); + await expectIxFail([mintIx], [txPayer, minter], "IsPaused"); + }); + + it("add another guardian", async () => { + const mustBeNull = await tbtc + .checkGuardianInfo(anotherGuardian.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "guardian info found"); + + const addIx = await tbtc.addGuardianIx({ + authority: authority.publicKey, + guardian: anotherGuardian.publicKey, + }); + await expectIxSuccess([addIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 2, + supply: BigInt(1600), + paused: true, + pendingAuthority: null, + }); + await tbtc.checkGuardianInfo(anotherGuardian.publicKey); + }); + + it("cannot pause again", async () => { + const cannotPauseIx = await tbtc.pauseIx({ + guardian: anotherGuardian.publicKey, + }); + await expectIxFail( + [cannotPauseIx], + [txPayer, anotherGuardian], + "IsPaused" + ); + }); + + it("unpause", async () => { + const unpauseIx = await tbtc.unpauseIx({ + authority: authority.publicKey, + }); + await expectIxSuccess([unpauseIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 2, + supply: BigInt(1600), + paused: false, + pendingAuthority: null, + }); + }); + + it("cannot unpause again", async () => { + const cannotUnpauseIx = await tbtc.unpauseIx({ + authority: authority.publicKey, + }); + await expectIxFail( + [cannotUnpauseIx], + [txPayer, authority], + "IsNotPaused" + ); + }); + + it("mint while unpaused", async () => { + const amount = BigInt(200); + + const recipientToken = spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + const recipientBefore = await getTokenBalance(recipientToken); + expect(recipientBefore).to.equal(BigInt(1600)); + + const mintIx = await tbtc.mintIx( + { + minter: minter.publicKey, + recipientToken, + }, + new anchor.BN(amount.toString()) + ); + await expectIxSuccess([mintIx], [txPayer, minter]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 2, + supply: BigInt(1800), + paused: false, + pendingAuthority: null, + }); + + const recipientAfter = await getTokenBalance(recipientToken); + expect(recipientAfter).to.equal(recipientBefore + amount); + }); + + it("pause as another guardian", async () => { + const pauseIx = await tbtc.pauseIx({ + guardian: anotherGuardian.publicKey, + }); + await expectIxSuccess([pauseIx], [txPayer, anotherGuardian]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 2, + supply: BigInt(1800), + paused: true, + pendingAuthority: null, + }); + }); + + it("cannot mint again while paused", async () => { + const recipientToken = spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + + const mintIx = await tbtc.mintIx( + { + minter: minter.publicKey, + recipientToken, + }, + new anchor.BN(100) + ); + await expectIxFail([mintIx], [txPayer, minter], "IsPaused"); + }); + + it("cannot remove guardian without authority", async () => { + const cannotRemoveIx = await tbtc.removeGuardianIx({ + authority: imposter.publicKey, + guardian: anotherGuardian.publicKey, + }); + await expectIxFail([cannotRemoveIx], [imposter], "IsNotAuthority"); + }); + + it("cannot remove guardian with mismatched info", async () => { + const guardianInfo = tbtc.getGuardianInfoPDA(anotherGuardian.publicKey); + const cannotRemoveIx = await tbtc.removeGuardianIx({ + authority: authority.publicKey, + guardianInfo, + guardian: guardian.publicKey, + }); + await expectIxFail([cannotRemoveIx], [authority], "ConstraintSeeds"); + }); + + it("remove guardian", async () => { + const removeIx = await tbtc.removeGuardianIx({ + authority: authority.publicKey, + guardian: anotherGuardian.publicKey, + }); + await expectIxSuccess([removeIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 1, + supply: BigInt(1800), + paused: true, + pendingAuthority: null, + }); + const mustBeNull = await tbtc + .checkGuardianInfo(anotherGuardian.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "guardian info found"); + }); + + it("unpause", async () => { + const unpauseIx = await tbtc.unpauseIx({ + authority: authority.publicKey, + }); + await expectIxSuccess([unpauseIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 1, + supply: BigInt(1800), + paused: false, + pendingAuthority: null, + }); + }); + + it("cannot pause with removed guardian", async () => { + const pauseIx = await tbtc.pauseIx({ + guardian: anotherGuardian.publicKey, + }); + await expectIxFail( + [pauseIx], + [txPayer, anotherGuardian], + "AccountNotInitialized" + ); + }); + + it("pause and remove last guardian", async () => { + const pauseIx = await tbtc.pauseIx({ + guardian: guardian.publicKey, + }); + await expectIxSuccess([pauseIx], [txPayer, guardian]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 1, + supply: BigInt(1800), + paused: true, + pendingAuthority: null, + }); + + const removeIx = await tbtc.removeGuardianIx({ + authority: authority.publicKey, + guardian: guardian.publicKey, + }); + await expectIxSuccess([removeIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(1800), + paused: true, + pendingAuthority: null, + }); + const mustBeNull = await tbtc + .checkGuardianInfo(guardian.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "guardian info found"); + }); + + it("cannot mint yet again", async () => { + const recipientToken = spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + + const mintIx = await tbtc.mintIx( + { + minter: minter.publicKey, + recipientToken, + }, + new anchor.BN(100) + ); + await expectIxFail([mintIx], [txPayer, minter], "IsPaused"); + }); + + it("unpause without any guardians then mint", async () => { + const unpauseIx = await tbtc.unpauseIx({ + authority: authority.publicKey, + }); + await expectIxSuccess([unpauseIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(1800), + paused: false, + pendingAuthority: null, + }); + + const recipientToken = spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + + const amount = BigInt(200); + + const recipientBefore = await getTokenBalance(recipientToken); + const mintIx = await tbtc.mintIx( + { + minter: minter.publicKey, + recipientToken, + }, + new anchor.BN(amount.toString()) + ); + await expectIxSuccess([mintIx], [txPayer, minter]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(2000), + paused: false, + pendingAuthority: null, + }); + + const recipientAfter = await getTokenBalance(recipientToken); + expect(recipientAfter).to.equal(recipientBefore + amount); + }); + + it("remove minter", async () => { + const removeIx = await tbtc.removeMinterIx({ + authority: authority.publicKey, + minter: minter.publicKey, + }); + await expectIxSuccess([removeIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(2000), + paused: false, + pendingAuthority: null, + }); + }); }); }); diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index 69eb14fe3..e5641ddea 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -1,116 +1,1232 @@ -import * as mock from "@certusone/wormhole-sdk/lib/cjs/mock"; -import * as tokenBridge from "@certusone/wormhole-sdk/lib/cjs/solana/tokenBridge"; -import * as coreBridge from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; -import { NodeWallet } from "@certusone/wormhole-sdk/lib/cjs/solana"; -import { parseTokenTransferVaa, postVaaSolana, redeemOnSolana, tryNativeToHexString } from "@certusone/wormhole-sdk"; +import { MockEthereumTokenBridge } from "@certusone/wormhole-sdk/lib/cjs/mock"; import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; -import * as spl from "@solana/spl-token"; -import { expect } from 'chai'; +import { getAccount, getAssociatedTokenAddressSync } from "@solana/spl-token"; +import { PublicKey } from "@solana/web3.js"; +import { expect } from "chai"; import { WormholeGateway } from "../target/types/wormhole_gateway"; -import { generatePayer, getOrCreateTokenAccount } from "./helpers/utils"; -import { web3 } from "@coral-xyz/anchor"; +import { + ETHEREUM_TOKEN_BRIDGE_ADDRESS, + WORMHOLE_GATEWAY_PROGRAM_ID, + WRAPPED_TBTC_MINT, + ethereumGatewaySendTbtc, + expectIxFail, + expectIxSuccess, + generatePayer, + getOrCreateAta, + preloadWrappedTbtc, + transferLamports, +} from "./helpers"; +import * as tbtc from "./helpers/tbtc"; +import * as wormholeGateway from "./helpers/wormholeGateway"; -const SOLANA_CORE_BRIDGE_ADDRESS = "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth"; -const SOLANA_TOKEN_BRIDGE_ADDRESS = "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb"; -const ETHEREUM_TOKEN_BRIDGE_ADDRESS = "0x3ee18B2214AFF97000D974cf647E7C347E8fa585"; -const ETHEREUM_TBTC_ADDRESS = "0x18084fbA666a33d37592fA2633fD49a74DD93a88"; - -const GUARDIAN_SET_INDEX = 3; - -function getCustodianPDA( +async function setup( program: Program, -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('custodian'), - ], - program.programId - ); -} + authority, + mintingLimit: bigint +) { + const custodian = wormholeGateway.getCustodianPDA(); + const tbtcMint = tbtc.getMintPDA(); + const gatewayWrappedTbtcToken = wormholeGateway.getWrappedTbtcTokenPDA(); + const tokenBridgeSender = wormholeGateway.getTokenBridgeSenderPDA(); + await program.methods + .initialize(new anchor.BN(mintingLimit.toString())) + .accounts({ + authority: authority.publicKey, + custodian, + tbtcMint, + wrappedTbtcMint: WRAPPED_TBTC_MINT, + wrappedTbtcToken: gatewayWrappedTbtcToken, + tokenBridgeSender, + }) + .rpc(); +} describe("wormhole-gateway", () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env()); + // Initialize anchor program. const program = anchor.workspace.WormholeGateway as Program; const connection = program.provider.connection; - const authority = (program.provider as anchor.AnchorProvider).wallet as anchor.Wallet; + const custodian = wormholeGateway.getCustodianPDA(); + const tbtcMint = tbtc.getMintPDA(); + const tbtcConfig = tbtc.getConfigPDA(); + const gatewayWrappedTbtcToken = wormholeGateway.getWrappedTbtcTokenPDA(); + const tokenBridgeSender = wormholeGateway.getTokenBridgeSenderPDA(); + const tokenBridgeRedeemer = wormholeGateway.getTokenBridgeRedeemerPDA(); + + const authority = ( + (program.provider as anchor.AnchorProvider).wallet as anchor.Wallet + ).payer; const newAuthority = anchor.web3.Keypair.generate(); const minterKeys = anchor.web3.Keypair.generate(); const minter2Keys = anchor.web3.Keypair.generate(); - const impostorKeys = anchor.web3.Keypair.generate(); + const imposter = anchor.web3.Keypair.generate(); const guardianKeys = anchor.web3.Keypair.generate(); const guardian2Keys = anchor.web3.Keypair.generate(); - const recipientKeys = anchor.web3.Keypair.generate(); - - const ethereumTokenBridge = new mock.MockEthereumTokenBridge(ETHEREUM_TOKEN_BRIDGE_ADDRESS); - - it('check core bridge and token bridge', async () => { - // Check core bridge guardian set. - const guardianSetData = await coreBridge.getGuardianSet(connection, SOLANA_CORE_BRIDGE_ADDRESS, GUARDIAN_SET_INDEX); - expect(guardianSetData.keys).has.length(1); - - // Set up new wallet - const payer = await generatePayer(connection, authority.payer); - - // Check wrapped tBTC mint. - const wrappedTbtcMint = tokenBridge.deriveWrappedMintKey(SOLANA_TOKEN_BRIDGE_ADDRESS, 2, ETHEREUM_TBTC_ADDRESS); - const mintData = await spl.getMint(connection, wrappedTbtcMint); - expect(mintData.decimals).to.equal(8); - expect(mintData.supply).to.equal(BigInt(90)); - - const wrappedTbtcToken = await getOrCreateTokenAccount(connection, payer, wrappedTbtcMint, payer.publicKey); - - // Bridge tbtc to token account. - const published = ethereumTokenBridge.publishTransferTokens( - tryNativeToHexString(ETHEREUM_TBTC_ADDRESS, "ethereum"), - 2, - BigInt("100000000000"), - 1, - wrappedTbtcToken.address.toBuffer().toString("hex"), - BigInt(0), - 0, - 0 - ); - - const signedVaa = await mockSignAndPostVaa(connection, payer, published); - - const tx = await redeemOnSolana( - connection, - SOLANA_CORE_BRIDGE_ADDRESS, - SOLANA_TOKEN_BRIDGE_ADDRESS, - payer.publicKey, - signedVaa, - ); - await web3.sendAndConfirmTransaction(connection, tx, [payer]); + const recipient = anchor.web3.Keypair.generate(); + const txPayer = anchor.web3.Keypair.generate(); + + const commonTokenOwner = anchor.web3.Keypair.generate(); + + // Mock foreign emitter. + const ethereumTokenBridge = new MockEthereumTokenBridge( + ETHEREUM_TOKEN_BRIDGE_ADDRESS + ); + + it("set up payers", async () => { + await transferLamports(authority, newAuthority.publicKey, 10000000000); + await transferLamports(authority, imposter.publicKey, 10000000000); + await transferLamports(authority, recipient.publicKey, 10000000000); + await transferLamports(authority, txPayer.publicKey, 10000000000); + await transferLamports(authority, commonTokenOwner.publicKey, 10000000000); }); - it('setup', async () => { - // await setup(program, authority); - // await checkState(program, authority, 0, 0, 0); + describe("setup", () => { + it("initialize", async () => { + // Max amount of TBTC that can be minted. + const mintingLimit = BigInt(10000); + + // Initialize the program. + await setup(program, authority, mintingLimit); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit, + pendingAuthority: null, + }); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(2000), + paused: false, + pendingAuthority: null, + }); + + // Also set up common token account. + await getOrCreateAta( + authority, + tbtc.getMintPDA(), + commonTokenOwner.publicKey + ); + + // Give the impostor some lamports. + await transferLamports(authority, imposter.publicKey, 100000000000); + }); }); -}); -async function mockSignAndPostVaa(connection: web3.Connection, payer: web3.Keypair, published: Buffer) { - const guardians = new mock.MockGuardians( - GUARDIAN_SET_INDEX, - ["cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"] - ); + describe("authority changes", () => { + it("cannot cancel authority if no pending", async () => { + const failedCancelIx = await wormholeGateway.cancelAuthorityChangeIx({ + authority: authority.publicKey, + }); + await expectIxFail( + [failedCancelIx], + [authority], + "NoPendingAuthorityChange" + ); + }); - // Add guardian signature. - const signedVaa = guardians.addSignatures(published, [0]); + it("cannot take authority if no pending", async () => { + const failedTakeIx = await wormholeGateway.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxFail( + [failedTakeIx], + [newAuthority], + "NoPendingAuthorityChange" + ); + }); - // Verify and post VAA. - await postVaaSolana(connection, - new NodeWallet(payer).signTransaction, - SOLANA_CORE_BRIDGE_ADDRESS, - payer.publicKey, - signedVaa - ); + it("change authority to new authority", async () => { + const changeIx = await wormholeGateway.changeAuthorityIx({ + authority: authority.publicKey, + newAuthority: newAuthority.publicKey, + }); + await expectIxSuccess([changeIx], [authority]); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit: BigInt(10000), + pendingAuthority: newAuthority.publicKey, + }); + }); - return signedVaa; -} + it("take as new authority", async () => { + // Bug in validator? Need to wait a bit for new blockhash. + //await sleep(10000); + + const takeIx = await wormholeGateway.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxSuccess([takeIx], [newAuthority]); + await wormholeGateway.checkCustodian({ + authority: newAuthority.publicKey, + mintingLimit: BigInt(10000), + pendingAuthority: null, + }); + }); + + it("change pending authority back to original authority", async () => { + const changeBackIx = await wormholeGateway.changeAuthorityIx({ + authority: newAuthority.publicKey, + newAuthority: authority.publicKey, + }); + await expectIxSuccess([changeBackIx], [newAuthority]); + await wormholeGateway.checkCustodian({ + authority: newAuthority.publicKey, + mintingLimit: BigInt(10000), + pendingAuthority: authority.publicKey, + }); + }); + + it("cannot take as signers that are not pending authority", async () => { + const failedImposterTakeIx = await wormholeGateway.takeAuthorityIx({ + pendingAuthority: imposter.publicKey, + }); + await expectIxFail( + [failedImposterTakeIx], + [imposter], + "IsNotPendingAuthority" + ); + + const failedNewAuthorityTakeIx = await wormholeGateway.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxFail( + [failedNewAuthorityTakeIx], + [newAuthority], + "IsNotPendingAuthority" + ); + }); + + it("cannot cancel as someone else", async () => { + const anotherFailedCancelIx = + await wormholeGateway.cancelAuthorityChangeIx({ + authority: authority.publicKey, + }); + await expectIxFail( + [anotherFailedCancelIx], + [authority], + "IsNotAuthority" + ); + }); + + it("finally take as authority", async () => { + const anotherTakeIx = await wormholeGateway.takeAuthorityIx({ + pendingAuthority: authority.publicKey, + }); + await expectIxSuccess([anotherTakeIx], [authority]); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit: BigInt(10000), + pendingAuthority: null, + }); + }); + }); + + describe("minting limit", () => { + it("update minting limit", async () => { + // Update minting limit as authority. + const newLimit = BigInt(20000); + const ix = await wormholeGateway.updateMintingLimitIx( + { + authority: authority.publicKey, + }, + newLimit + ); + await expectIxSuccess([ix], [authority]); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit: newLimit, + pendingAuthority: null, + }); + }); + + it("cannot update minting limit (not authority)", async () => { + // Only the authority can update the minting limit. + const newLimit = BigInt(69000); + const failingIx = await wormholeGateway.updateMintingLimitIx( + { + authority: imposter.publicKey, + }, + newLimit + ); + await expectIxFail([failingIx], [imposter], "IsNotAuthority"); + }); + }); + + describe("gateway address", () => { + const chain = 2; + + it("gateway does not exist", async () => { + // demonstrate gateway address does not exist + const gatewayInfo = await connection.getAccountInfo( + wormholeGateway.getGatewayInfoPDA(chain) + ); + expect(gatewayInfo).is.null; + }); + + it("set initial gateway address", async () => { + // Make new gateway. + const firstAddress = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const firstIx = await wormholeGateway.updateGatewayAddress( + { + authority: authority.publicKey, + }, + { chain, address: firstAddress } + ); + await expectIxSuccess([firstIx], [authority]); + await wormholeGateway.checkGateway(chain, firstAddress); + }); + + it("update gateway address", async () => { + // Update gateway. + const goodAddress = Array.from(ethereumTokenBridge.address); + const secondIx = await wormholeGateway.updateGatewayAddress( + { + authority: authority.publicKey, + }, + { chain, address: goodAddress } + ); + await expectIxSuccess([secondIx], [authority]); + await wormholeGateway.checkGateway(chain, goodAddress); + }); + + it("cannot update gateway address (not authority)", async () => { + // Only the authority can update the gateway address. + const goodAddress = Array.from(ethereumTokenBridge.address); + const failingIx = await wormholeGateway.updateGatewayAddress( + { + authority: imposter.publicKey, + }, + { chain, address: goodAddress } + ); + await expectIxFail([failingIx], [imposter], "IsNotAuthority"); + }); + }); + + describe("deposit wrapped tbtc", () => { + it("cannot deposit wrapped tbtc (custodian not a minter)", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Check wrapped tBTC mint. + const recipientWrappedToken = await preloadWrappedTbtc( + payer, + ethereumTokenBridge, + BigInt("100000000000"), + payer.publicKey + ); + + const recipientToken = await getOrCreateAta( + payer, + tbtcMint, + payer.publicKey + ); + + const depositAmount = BigInt(500); + + // Attempt to deposit before the custodian is a minter. + const ix = await wormholeGateway.depositWormholeTbtcIx( + { + recipientWrappedToken, + recipientToken, + recipient: payer.publicKey, + }, + depositAmount + ); + await expectIxFail([ix], [payer], "AccountNotInitialized"); + }); + + it("deposit wrapped tokens", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Check wrapped tBTC mint. + const recipientWrappedToken = await preloadWrappedTbtc( + payer, + ethereumTokenBridge, + BigInt("100000000000"), + payer.publicKey + ); + + const recipientToken = await getOrCreateAta( + payer, + tbtcMint, + payer.publicKey + ); + + const mintedAmountBefore = await wormholeGateway.getMintedAmount(); + + const depositAmount = BigInt(500); + + const ix = await wormholeGateway.depositWormholeTbtcIx( + { + recipientWrappedToken, + recipientToken, + recipient: payer.publicKey, + }, + depositAmount + ); + + // Add custodian as minter. + const addMinterIx = await tbtc.addMinterIx({ + authority: authority.publicKey, + minter: custodian, + }); + await expectIxSuccess([addMinterIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(2000), + paused: false, + pendingAuthority: null, + }); + + // Check token account balances before deposit. + const [wrappedBefore, tbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, recipientWrappedToken), + getAccount(connection, recipientToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + await expectIxSuccess([ix], [payer]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(2500), + paused: false, + pendingAuthority: null, + }); + + const [wrappedAfter, tbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, recipientWrappedToken), + getAccount(connection, recipientToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check minted amount after. + const mintedAmountAfter = await wormholeGateway.getMintedAmount(); + expect(mintedAmountAfter).to.equal(mintedAmountBefore + depositAmount); + + // Check balance change. + expect(wrappedAfter.amount).to.equal( + wrappedBefore.amount - depositAmount + ); + expect(tbtcAfter.amount).to.equal(tbtcBefore.amount + depositAmount); + expect(gatewayAfter.amount).to.equal( + gatewayBefore.amount + depositAmount + ); + }); + + it("cannot deposit wrapped tbtc (minting limit exceeded)", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Check wrapped tBTC mint. + const recipientWrappedToken = await preloadWrappedTbtc( + payer, + ethereumTokenBridge, + BigInt("100000000000"), + payer.publicKey + ); + + const recipientToken = await getOrCreateAta( + payer, + tbtcMint, + payer.publicKey + ); + + // Cannot deposit past minting limit. + const failingIx = await wormholeGateway.depositWormholeTbtcIx( + { + recipientWrappedToken, + recipientToken, + recipient: payer.publicKey, + }, + BigInt(50000) + ); + await expectIxFail([failingIx], [payer], "MintingLimitExceeded"); + }); + + it("deposit wrapped tbtc after increasing mint limit", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Check wrapped tBTC mint. + const recipientWrappedToken = await preloadWrappedTbtc( + payer, + ethereumTokenBridge, + BigInt("100000000000"), + payer.publicKey + ); + + const recipientToken = await getOrCreateAta( + payer, + tbtcMint, + payer.publicKey + ); + + // Check minted amount before deposit. + const mintedAmountBefore = await wormholeGateway.getMintedAmount(); + + const depositAmount = BigInt(50000); + + // Cannot deposit past minting limit. + const failingIx = await wormholeGateway.depositWormholeTbtcIx( + { + recipientWrappedToken, + recipientToken, + recipient: payer.publicKey, + }, + depositAmount + ); + + // Will succeed if minting limit is increased. + const newLimit = BigInt(70000); + const updateLimitIx = await wormholeGateway.updateMintingLimitIx( + { + authority: authority.publicKey, + }, + newLimit + ); + await expectIxSuccess([updateLimitIx], [authority]); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit: newLimit, + pendingAuthority: null, + }); + await expectIxSuccess([failingIx], [payer]); + + // Check minted amount after. + const mintedAmountAfter = await wormholeGateway.getMintedAmount(); + expect(mintedAmountAfter).to.equal(mintedAmountBefore + depositAmount); + }); + }); + + describe("receive tbtc", () => { + let replayVaa; + + it("receive tbtc", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Use common token account. + const recipient = commonTokenOwner.publicKey; + const recipientToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient + ); + + // Get foreign gateway. + const fromGateway = await wormholeGateway + .getGatewayInfo(2) + .then((info) => info.address); + + // Get minted amount before. + const mintedAmountBefore = await wormholeGateway.getMintedAmount(); + + const sentAmount = BigInt(5000); + const signedVaa = await ethereumGatewaySendTbtc( + payer, + ethereumTokenBridge, + sentAmount, + fromGateway, + WORMHOLE_GATEWAY_PROGRAM_ID, + recipient + ); + + const [tbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, recipientToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + const ix = await wormholeGateway.receiveTbtcIx( + { + payer: payer.publicKey, + recipientToken, + recipient, + }, + signedVaa + ); + await expectIxSuccess([ix], [payer]); + + const [tbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, recipientToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check balance change. + expect(tbtcAfter.amount).to.equal(tbtcBefore.amount + sentAmount); + expect(gatewayAfter.amount).to.equal(gatewayBefore.amount + sentAmount); + + // Check minted amount. + const mintedAmountAfter = await wormholeGateway.getMintedAmount(); + expect(mintedAmountAfter).to.equal(mintedAmountBefore + sentAmount); + + // Save vaa. + replayVaa = signedVaa; + }); + + it("cannot receive tbtc (vaa already redeemed)", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Use common token account. + const recipient = commonTokenOwner.publicKey; + const recipientToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient + ); + + const ix = await wormholeGateway.receiveTbtcIx( + { + payer: payer.publicKey, + recipientToken, + recipient, + }, + replayVaa + ); + + // Cannot receive tbtc again. + await expectIxFail([ix], [payer], "TransferAlreadyRedeemed"); + }); + + it("receive wrapped tbtc (ata doesn't exist)", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Use common token account. + const recipient = commonTokenOwner.publicKey; + const recipientToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient + ); + const recipientWrappedToken = getAssociatedTokenAddressSync( + WRAPPED_TBTC_MINT, + recipient + ); + + // Get minted amount before. + const mintedAmountBefore = await wormholeGateway.getMintedAmount(); + + // Verify that the wrapped token account doesn't exist yet. + try { + await getAccount(connection, recipientWrappedToken); + } catch (e: any) { + expect(e.toString()).to.equal("TokenAccountNotFoundError"); + } + + // Get foreign gateway. + const fromGateway = await wormholeGateway + .getGatewayInfo(2) + .then((info) => info.address); + + // Create transfer VAA. + const sentAmount = BigInt(5000); + const signedVaa = await ethereumGatewaySendTbtc( + payer, + ethereumTokenBridge, + sentAmount, + fromGateway, + WORMHOLE_GATEWAY_PROGRAM_ID, + recipient + ); + + // Set the mint limit to a value smaller than sentAmount. + const newLimit = sentAmount - BigInt(69); + const updateLimitIx = await wormholeGateway.updateMintingLimitIx( + { + authority: authority.publicKey, + }, + newLimit + ); + await expectIxSuccess([updateLimitIx], [authority]); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit: newLimit, + pendingAuthority: null, + }); + + // Balance check before receiving wrapped tbtc. We can't + // check the balance of the recipient's wrapped tbtc yet, + // since the contract will create the ATA. + const [tbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, recipientToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + const ix = await wormholeGateway.receiveTbtcIx( + { + payer: payer.publicKey, + recipientToken, + recipient, + }, + signedVaa + ); + await expectIxSuccess([ix], [payer]); + + // Check token accounts after receiving wrapped tbtc. We should + // be able to fetch the recipient's wrapped tbtc now. + const [tbtcAfter, wrappedTbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, recipientToken), + getAccount(connection, recipientWrappedToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check minted amount. + const mintedAmountAfter = await wormholeGateway.getMintedAmount(); + expect(mintedAmountAfter).to.equal(mintedAmountBefore); + + // Check balance change. + expect(tbtcAfter.amount).to.equal(tbtcBefore.amount); + expect(gatewayAfter.amount).to.equal(gatewayBefore.amount); + expect(wrappedTbtcAfter.amount).to.equal(sentAmount); + }); + + it("receive wrapped tbtc (ata exists)", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Use common token account. + const recipient = commonTokenOwner.publicKey; + const recipientToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient + ); + const recipientWrappedToken = await getOrCreateAta( + payer, + WRAPPED_TBTC_MINT, + recipient + ); + + // Get foreign gateway. + const fromGateway = await wormholeGateway + .getGatewayInfo(2) + .then((info) => info.address); + + // Get minted amount before. + const mintedAmountBefore = await wormholeGateway.getMintedAmount(); + + // Create transfer VAA. + const sentAmount = BigInt(5000); + const signedVaa = await ethereumGatewaySendTbtc( + payer, + ethereumTokenBridge, + sentAmount, + fromGateway, + WORMHOLE_GATEWAY_PROGRAM_ID, + recipient + ); + + // Set the mint limit to a value smaller than sentAmount. + const newLimit = sentAmount - BigInt(69); + const updateLimitIx = await wormholeGateway.updateMintingLimitIx( + { + authority: authority.publicKey, + }, + newLimit + ); + await expectIxSuccess([updateLimitIx], [authority]); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit: newLimit, + pendingAuthority: null, + }); + + // Balance check before receiving wrapped tbtc. If this + // line successfully executes, then the recipient's + // wrapped tbtc account already exists. + const [tbtcBefore, wrappedTbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, recipientToken), + getAccount(connection, recipientWrappedToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + const ix = await wormholeGateway.receiveTbtcIx( + { + payer: payer.publicKey, + recipientToken, + recipient, + }, + signedVaa + ); + await expectIxSuccess([ix], [payer]); + + // Check token accounts after receiving wrapped tbtc. + const [tbtcAfter, wrappedTbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, recipientToken), + getAccount(connection, recipientWrappedToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check minted amount. + const mintedAmountAfter = await wormholeGateway.getMintedAmount(); + expect(mintedAmountAfter).to.equal(mintedAmountBefore); + + // Check balance change. + expect(tbtcAfter.amount).to.equal(tbtcBefore.amount); + expect(gatewayAfter.amount).to.equal(gatewayBefore.amount); + expect(wrappedTbtcAfter.amount).to.equal( + wrappedTbtcBefore.amount + sentAmount + ); + }); + + it("cannot receive non-tbtc transfers", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Use common token account. + const recipient = commonTokenOwner.publicKey; + const recipientToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient + ); + + // Get foreign gateway. + const fromGateway = await wormholeGateway + .getGatewayInfo(2) + .then((info) => info.address); + + const sentAmount = BigInt(5000); + const signedVaa = await ethereumGatewaySendTbtc( + payer, + ethereumTokenBridge, + sentAmount, + fromGateway, + WORMHOLE_GATEWAY_PROGRAM_ID, + recipient, + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH address + 69 // hehe + ); + + const failingIx = await wormholeGateway.receiveTbtcIx( + { + payer: payer.publicKey, + recipientToken, + recipient, + }, + signedVaa + ); + await expectIxFail([failingIx], [payer], "InvalidEthereumTbtc"); + }); + + it("cannot receive zero-amount tbtc transfers", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Use common token account. + const recipient = commonTokenOwner.publicKey; + const recipientToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient + ); + + // Get foreign gateway. + const fromGateway = await wormholeGateway + .getGatewayInfo(2) + .then((info) => info.address); + + const sentAmount = BigInt(0); + const signedVaa = await ethereumGatewaySendTbtc( + payer, + ethereumTokenBridge, + sentAmount, + fromGateway, + WORMHOLE_GATEWAY_PROGRAM_ID, + recipient + ); + + const failingIx = await wormholeGateway.receiveTbtcIx( + { + payer: payer.publicKey, + recipientToken, + recipient, + }, + signedVaa + ); + await expectIxFail([failingIx], [payer], "NoTbtcTransferred"); + }); + + it("cannot receive tbtc transfer with zero address as recipient", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Use common token account. Set the recipient to the zero address. + const recipient = PublicKey.default; + const defaultTokenAccount = await getOrCreateAta( + payer, + tbtc.getMintPDA(), + recipient + ); + + // Get foreign gateway. + const fromGateway = await wormholeGateway + .getGatewayInfo(2) + .then((info) => info.address); + + const sentAmount = BigInt(100); + const signedVaa = await ethereumGatewaySendTbtc( + payer, + ethereumTokenBridge, + sentAmount, + fromGateway, + WORMHOLE_GATEWAY_PROGRAM_ID, + recipient + ); + + const failingIx = await wormholeGateway.receiveTbtcIx( + { + payer: payer.publicKey, + recipientToken: defaultTokenAccount, + recipient, + }, + signedVaa + ); + await expectIxFail([failingIx], [payer], "RecipientZeroAddress"); + }); + }); + + describe("send tbtc", () => { + it("send tbtc to gateway", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Check token accounts. + const [senderTbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, senderToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check minted amount before. + const mintedAmountBefore = await wormholeGateway.getMintedAmount(); + + // Get destination gateway. + const recipientChain = 2; + const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const nonce = 420; + + // This should work. + const sendAmount = BigInt(2000); + const ix = await wormholeGateway.sendTbtcGatewayIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + nonce, + } + ); + await expectIxSuccess([ix], [commonTokenOwner]); + + // Check token accounts after sending tbtc. + const [senderTbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, senderToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check minted amount. + const mintedAmountAfter = await wormholeGateway.getMintedAmount(); + expect(mintedAmountAfter).to.equal(mintedAmountBefore - sendAmount); + + // Check balance change. + expect(senderTbtcAfter.amount).to.equal( + senderTbtcBefore.amount - sendAmount + ); + expect(gatewayAfter.amount).to.equal(gatewayBefore.amount - sendAmount); + }); + + it("cannot send tbtc to gateway (insufficient wrapped balance)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Get destination gateway. + const recipientChain = 2; + const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const nonce = 420; + + // Check token accounts. + const gatewayWrappedBalance = await getAccount( + connection, + gatewayWrappedTbtcToken + ); + + // Try an amount that won't work. + const sendAmount = gatewayWrappedBalance.amount + BigInt(69); + const ix = await wormholeGateway.sendTbtcGatewayIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + nonce, + } + ); + await expectIxFail([ix], [commonTokenOwner], "NotEnoughWrappedTbtc"); + }); + + it("cannot send tbtc to gateway (zero amount)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Get destination gateway. + const recipientChain = 2; + const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const nonce = 420; + + // Try an amount that won't work. + const sendAmount = BigInt(0); + const ix = await wormholeGateway.sendTbtcGatewayIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + nonce, + } + ); + await expectIxFail([ix], [commonTokenOwner], "ZeroAmount"); + }); + + it("cannot send tbtc to gateway (recipient is zero address)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Get destination gateway. + const recipientChain = 2; + const recipient = Array.from(Buffer.alloc(32)); // empty buffer + const nonce = 420; + + const sendAmount = BigInt(69); + const ix = await wormholeGateway.sendTbtcGatewayIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + nonce, + } + ); + await expectIxFail([ix], [commonTokenOwner], "ZeroRecipient"); + }); + + it("cannot send tbtc to gateway (invalid target gateway)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Get destination gateway. + const recipientChain = 69; // bad gateway + const recipient = Array.from(Buffer.alloc(32)); // empty buffer + const nonce = 420; + + const sendAmount = BigInt(69); + const ix = await wormholeGateway.sendTbtcGatewayIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + nonce, + } + ); + await expectIxFail([ix], [commonTokenOwner], "AccountNotInitialized"); + }); + }); + + describe("send wrapped tbtc", () => { + it("send wrapped tbtc", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Check token accounts. + const [senderTbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, senderToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check minted amount before. + const mintedAmountBefore = await wormholeGateway.getMintedAmount(); + + // Get destination gateway. + const recipientChain = 69; + const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const nonce = 420; + + // This should work. + const sendAmount = BigInt(2000); + const ix = await wormholeGateway.sendTbtcWrappedIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + arbiterFee: new anchor.BN(0), + nonce, + } + ); + await expectIxSuccess([ix], [commonTokenOwner]); + + // Check token accounts after sending tbtc. + const [senderTbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, senderToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check minted amount. + const mintedAmountAfter = await wormholeGateway.getMintedAmount(); + expect(mintedAmountAfter).to.equal(mintedAmountBefore - sendAmount); + + // Check balance change. + expect(senderTbtcAfter.amount).to.equal( + senderTbtcBefore.amount - sendAmount + ); + expect(gatewayAfter.amount).to.equal(gatewayBefore.amount - sendAmount); + }); + + it("cannot send wrapped tbtc (insufficient wrapped balance)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Get destination gateway. + const recipientChain = 2; + const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const nonce = 420; + + // Check token accounts. + const gatewayWrappedBalance = await getAccount( + connection, + gatewayWrappedTbtcToken + ); + + // Try an amount that won't work. + const sendAmount = gatewayWrappedBalance.amount + BigInt(69); + const ix = await wormholeGateway.sendTbtcWrappedIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + arbiterFee: new anchor.BN(0), + nonce, + } + ); + await expectIxFail([ix], [commonTokenOwner], "NotEnoughWrappedTbtc"); + }); + + it("cannot send wrapped tbtc(zero amount)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Get destination gateway. + const recipientChain = 2; + const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const nonce = 420; + + // Try an amount that won't work. + const sendAmount = BigInt(0); + const ix = await wormholeGateway.sendTbtcWrappedIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + arbiterFee: new anchor.BN(0), + nonce, + } + ); + await expectIxFail([ix], [commonTokenOwner], "ZeroAmount"); + }); + + it("cannot send wrapped tbtc (recipient is zero address)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Get destination gateway. + const recipientChain = 2; + const recipient = Array.from(Buffer.alloc(32)); // empty buffer + const nonce = 420; + + const sendAmount = BigInt(69); + const ix = await wormholeGateway.sendTbtcWrappedIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + arbiterFee: new anchor.BN(0), + nonce, + } + ); + await expectIxFail([ix], [commonTokenOwner], "ZeroRecipient"); + }); + }); +}); diff --git a/cross-chain/solana/tests/accounts/core_bridge.json b/cross-chain/solana/tests/accounts/core_bridge_data.json similarity index 100% rename from cross-chain/solana/tests/accounts/core_bridge.json rename to cross-chain/solana/tests/accounts/core_bridge_data.json diff --git a/cross-chain/solana/tests/accounts/core_fee_collector.json b/cross-chain/solana/tests/accounts/core_fee_collector.json index 6f355d442..beae11950 100644 --- a/cross-chain/solana/tests/accounts/core_fee_collector.json +++ b/cross-chain/solana/tests/accounts/core_fee_collector.json @@ -1,7 +1,7 @@ { "pubkey": "9bFNrXNb2WTx8fMHXCheaZqkLZ3YCCaiqTftHxeintHy", "account": { - "lamports": 86533780, + "lamports": 86533880, "data": [ "", "base64" diff --git a/cross-chain/solana/tests/helpers/consts.ts b/cross-chain/solana/tests/helpers/consts.ts new file mode 100644 index 000000000..748be2689 --- /dev/null +++ b/cross-chain/solana/tests/helpers/consts.ts @@ -0,0 +1,50 @@ +import { PublicKey } from "@solana/web3.js"; + +export const TBTC_PROGRAM_ID = new PublicKey( + "HksEtDgsXJV1BqcuhzbLRTmXp5gHgHJktieJCtQd3pG" +); +export const WORMHOLE_GATEWAY_PROGRAM_ID = new PublicKey( + "8H9F5JGbEMyERycwaGuzLS5MQnV7dn2wm2h6egJ3Leiu" +); + +export const CORE_BRIDGE_PROGRAM_ID = new PublicKey( + "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth" +); +export const TOKEN_BRIDGE_PROGRAM_ID = new PublicKey( + "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb" +); + +export const ETHEREUM_TOKEN_BRIDGE_ADDRESS = + "0x3ee18B2214AFF97000D974cf647E7C347E8fa585"; +export const ETHEREUM_TBTC_ADDRESS = + "0x18084fbA666a33d37592fA2633fD49a74DD93a88"; + +export const GUARDIAN_SET_INDEX = 3; +export const GUARDIAN_DEVNET_PRIVATE_KEYS = [ + "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0", +]; + +// relevant core bridge PDAs +export const CORE_BRIDGE_DATA = new PublicKey( + "2yVjuQwpsvdsrywzsJJVs9Ueh4zayyo5DYJbBNc3DDpn" +); +export const CORE_EMITTER_SEQUENCE = new PublicKey( + "GF2ghkjwsR9CHkGk1RvuZrApPZGBZynxMm817VNi51Nf" +); +export const CORE_FEE_COLLECTOR = new PublicKey( + "9bFNrXNb2WTx8fMHXCheaZqkLZ3YCCaiqTftHxeintHy" +); + +// relevant token bridge PDAs +export const WRAPPED_TBTC_MINT = new PublicKey( + "25rXTx9zDZcHyTav5sRqM6YBvTGu9pPH9yv83uAEqbgG" +); +export const WRAPPED_TBTC_ASSET = new PublicKey( + "5LEUZpBxUQmoxoNGqmYmFEGAPDuhWbAY5CGt519UixLo" +); +export const ETHEREUM_ENDPOINT = new PublicKey( + "DujfLgMKW71CT2W8pxknf42FT86VbcK5PjQ6LsutjWKC" +); +export const TOKEN_BRIDGE_CONFIG = new PublicKey( + "DapiQYH3BGonhN8cngWcXQ6SrqSm3cwysoznoHr6Sbsx" +); diff --git a/cross-chain/solana/tests/helpers/index.ts b/cross-chain/solana/tests/helpers/index.ts new file mode 100644 index 000000000..cb1b44601 --- /dev/null +++ b/cross-chain/solana/tests/helpers/index.ts @@ -0,0 +1,2 @@ +export * from "./consts"; +export * from "./utils"; \ No newline at end of file diff --git a/cross-chain/solana/tests/helpers/tbtc.ts b/cross-chain/solana/tests/helpers/tbtc.ts new file mode 100644 index 000000000..7302c3ed3 --- /dev/null +++ b/cross-chain/solana/tests/helpers/tbtc.ts @@ -0,0 +1,510 @@ +import { BN, Program, Wallet, workspace } from "@coral-xyz/anchor"; +import { getMint } from "@solana/spl-token"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { config, expect } from "chai"; +import { Tbtc } from "../../target/types/tbtc"; +import { TBTC_PROGRAM_ID } from "./consts"; +import { PROGRAM_ID as METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata"; + +export function getConfigPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("config")], + TBTC_PROGRAM_ID + )[0]; +} + +export function getMintPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("tbtc-mint")], + TBTC_PROGRAM_ID + )[0]; +} + +export function getTbtcMetadataPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from("metadata"), + METADATA_PROGRAM_ID.toBuffer(), + getMintPDA().toBuffer(), + ], + METADATA_PROGRAM_ID + )[0]; +} + +export function getMinterInfoPDA(minter: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("minter-info"), minter.toBuffer()], + TBTC_PROGRAM_ID + )[0]; +} + +export function getGuardianInfoPDA(guardian: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("guardian-info"), guardian.toBuffer()], + TBTC_PROGRAM_ID + )[0]; +} + +export function getGuardiansPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("guardians")], + TBTC_PROGRAM_ID + )[0]; +} + +export function getMintersPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("minters")], + TBTC_PROGRAM_ID + )[0]; +} + +export async function getConfigData() { + const program = workspace.Tbtc as Program; + const config = getConfigPDA(); + return program.account.config.fetch(config); +} + +export async function checkConfig(expected: { + authority: PublicKey; + numMinters: number; + numGuardians: number; + supply: bigint; + paused: boolean; + pendingAuthority: PublicKey | null; +}) { + let { + authority, + numMinters, + numGuardians, + supply, + paused, + pendingAuthority, + } = expected; + const program = workspace.Tbtc as Program; + const configState = await getConfigData(); + + expect(configState.authority).to.eql(authority); + expect(configState.numMinters).to.equal(numMinters); + expect(configState.numGuardians).to.equal(numGuardians); + expect(configState.paused).to.equal(paused); + expect(configState.pendingAuthority).to.eql(pendingAuthority); + + const mintState = await getMint( + program.provider.connection, + configState.mint + ); + expect(mintState.supply).to.equal(supply); + + const guardians = getGuardiansPDA(); + const guardiansState = await program.account.guardians.fetch(guardians); + expect(guardiansState.keys).has.length(numGuardians); + + const minters = getMintersPDA(); + const mintersState = await program.account.minters.fetch(minters); + expect(mintersState.keys).has.length(numMinters); +} + +export async function getMinterInfo(minter: PublicKey) { + const program = workspace.Tbtc as Program; + const minterInfoPDA = getMinterInfoPDA(minter); + return program.account.minterInfo.fetch(minterInfoPDA); +} + +export async function checkMinterInfo(minter: PublicKey) { + const minterInfo = await getMinterInfo(minter); + expect(minterInfo.minter).to.eql(minter); +} + +export async function getGuardianInfo(guardian: PublicKey) { + const program = workspace.Tbtc as Program; + const guardianInfoPDA = getGuardianInfoPDA(guardian); + return program.account.guardianInfo.fetch(guardianInfoPDA); +} + +export async function checkGuardianInfo(guardian: PublicKey) { + let guardianInfo = await getGuardianInfo(guardian); + expect(guardianInfo.guardian).to.eql(guardian); +} + +type AddGuardianContext = { + config?: PublicKey; + authority: PublicKey; + guardians?: PublicKey; + guardianInfo?: PublicKey; + guardian: PublicKey; +}; + +export async function addGuardianIx( + accounts: AddGuardianContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, authority, guardians, guardianInfo, guardian } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + if (guardians === undefined) { + guardians = getGuardiansPDA(); + } + + if (guardianInfo === undefined) { + guardianInfo = getGuardianInfoPDA(guardian); + } + + return program.methods + .addGuardian() + .accounts({ + config, + authority, + guardians, + guardianInfo, + guardian, + }) + .instruction(); +} + +type AddMinterContext = { + config?: PublicKey; + authority: PublicKey; + minters?: PublicKey; + minterInfo?: PublicKey; + minter: PublicKey; +}; + +export async function addMinterIx( + accounts: AddMinterContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, authority, minters, minterInfo, minter } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + if (minters === undefined) { + minters = getMintersPDA(); + } + + if (minterInfo === undefined) { + minterInfo = getMinterInfoPDA(minter); + } + + return program.methods + .addMinter() + .accounts({ + config, + authority, + minters, + minterInfo, + minter, + }) + .instruction(); +} + +type CancelAuthorityChange = { + config?: PublicKey; + authority: PublicKey; +}; + +export async function cancelAuthorityChangeIx( + accounts: CancelAuthorityChange +): Promise { + const program = workspace.Tbtc as Program; + + let { config, authority } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + return program.methods + .cancelAuthorityChange() + .accounts({ + config, + authority, + }) + .instruction(); +} + +type ChangeAuthorityContext = { + config?: PublicKey; + authority: PublicKey; + newAuthority: PublicKey; +}; + +export async function changeAuthorityIx( + accounts: ChangeAuthorityContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, authority, newAuthority } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + return program.methods + .changeAuthority() + .accounts({ + config, + authority, + newAuthority, + }) + .instruction(); +} + +type InitializeContext = { + mint?: PublicKey; + config?: PublicKey; + guardians?: PublicKey; + minters?: PublicKey; + authority: PublicKey; + tbtcMetadata?: PublicKey; + mplTokenMetadataProgram?: PublicKey; +}; + +export async function initializeIx( + accounts: InitializeContext +): Promise { + const program = workspace.Tbtc as Program; + + let { + mint, + config, + guardians, + minters, + authority, + tbtcMetadata, + mplTokenMetadataProgram, + } = accounts; + + if (mint === undefined) { + mint = getMintPDA(); + } + + if (config === undefined) { + config = getConfigPDA(); + } + + if (guardians === undefined) { + guardians = getGuardiansPDA(); + } + + if (minters === undefined) { + minters = getMintersPDA(); + } + + if (tbtcMetadata === undefined) { + tbtcMetadata = getTbtcMetadataPDA(); + } + + if (mplTokenMetadataProgram === undefined) { + mplTokenMetadataProgram = METADATA_PROGRAM_ID; + } + + return program.methods + .initialize() + .accounts({ + mint, + config, + guardians, + minters, + authority, + tbtcMetadata, + mplTokenMetadataProgram, + }) + .instruction(); +} + +type PauseContext = { + config?: PublicKey; + guardianInfo?: PublicKey; + guardian: PublicKey; +}; + +export async function pauseIx( + accounts: PauseContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, guardianInfo, guardian } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + if (guardianInfo === undefined) { + guardianInfo = getGuardianInfoPDA(guardian); + } + + return program.methods + .pause() + .accounts({ + config, + guardianInfo, + guardian, + }) + .instruction(); +} + +type RemoveGuardianContext = { + config?: PublicKey; + authority: PublicKey; + guardians?: PublicKey; + guardianInfo?: PublicKey; + guardian: PublicKey; +}; + +export async function removeGuardianIx( + accounts: RemoveGuardianContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, authority, guardians, guardianInfo, guardian } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + if (guardians === undefined) { + guardians = getGuardiansPDA(); + } + + if (guardianInfo === undefined) { + guardianInfo = getGuardianInfoPDA(guardian); + } + + return program.methods + .removeGuardian() + .accounts({ + config, + authority, + guardians, + guardianInfo, + guardian, + }) + .instruction(); +} + +type RemoveMinterContext = { + config?: PublicKey; + authority: PublicKey; + minters?: PublicKey; + minterInfo?: PublicKey; + minter: PublicKey; +}; + +export async function removeMinterIx( + accounts: RemoveMinterContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, authority, minters, minterInfo, minter } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + if (minters === undefined) { + minters = getMintersPDA(); + } + + if (minterInfo === undefined) { + minterInfo = getMinterInfoPDA(minter); + } + + return program.methods + .removeMinter() + .accounts({ + config, + authority, + minters, + minterInfo, + minter, + }) + .instruction(); +} + +type TakeAuthorityContext = { + config?: PublicKey; + pendingAuthority: PublicKey; +}; + +export async function takeAuthorityIx( + accounts: TakeAuthorityContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, pendingAuthority } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + return program.methods + .takeAuthority() + .accounts({ + config, + pendingAuthority, + }) + .instruction(); +} + +type UnpauseContext = { + config?: PublicKey; + authority: PublicKey; +}; + +export async function unpauseIx( + accounts: UnpauseContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, authority } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + return program.methods + .unpause() + .accounts({ + config, + authority, + }) + .instruction(); +} + +type MintContext = { + mint?: PublicKey; + config?: PublicKey; + minterInfo?: PublicKey; + minter: PublicKey; + recipientToken: PublicKey; +}; + +export async function mintIx( + accounts: MintContext, + amount: BN +): Promise { + const program = workspace.Tbtc as Program; + + let { mint, config, minterInfo, minter, recipientToken } = accounts; + if (mint === undefined) { + mint = getMintPDA(); + } + + if (config === undefined) { + config = getConfigPDA(); + } + + if (minterInfo === undefined) { + minterInfo = getMinterInfoPDA(minter); + } + + return program.methods + .mint(amount) + .accounts({ + mint, + config, + minterInfo, + minter, + recipientToken, + }) + .instruction(); +} diff --git a/cross-chain/solana/tests/helpers/utils.ts b/cross-chain/solana/tests/helpers/utils.ts index b87a0a948..40980e668 100644 --- a/cross-chain/solana/tests/helpers/utils.ts +++ b/cross-chain/solana/tests/helpers/utils.ts @@ -1,53 +1,282 @@ -import { web3 } from "@coral-xyz/anchor"; -import { Account, TokenAccountNotFoundError, createAssociatedTokenAccountIdempotentInstruction, getAccount, getAssociatedTokenAddressSync } from "@solana/spl-token"; -import { Connection, Keypair, PublicKey, SystemProgram, Transaction, sendAndConfirmTransaction } from "@solana/web3.js"; - -export async function transferLamports(connection: web3.Connection, fromSigner: web3.Keypair, toPubkey: web3.PublicKey, lamports: number) { - return sendAndConfirmTransaction( - connection, - new Transaction().add( - SystemProgram.transfer({ - fromPubkey: fromSigner.publicKey, - toPubkey, - lamports, - }) - ), - [fromSigner] +import { + postVaaSolana, + redeemOnSolana, + tryNativeToHexString, +} from "@certusone/wormhole-sdk"; +import { + MockEthereumTokenBridge, + MockGuardians, +} from "@certusone/wormhole-sdk/lib/cjs/mock"; +import { NodeWallet } from "@certusone/wormhole-sdk/lib/cjs/solana"; +import * as coreBridge from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; +import { Program, web3, workspace } from "@coral-xyz/anchor"; +import { + Account, + TokenAccountNotFoundError, + createAssociatedTokenAccountIdempotentInstruction, + getAccount, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; +import { + Keypair, + PublicKey, + SystemProgram, + Transaction, + TransactionInstruction, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { assert, expect } from "chai"; +import { WormholeGateway } from "../../target/types/wormhole_gateway"; // This is only here to hack a connection. +import { + CORE_BRIDGE_PROGRAM_ID, + ETHEREUM_TBTC_ADDRESS, + GUARDIAN_DEVNET_PRIVATE_KEYS, + GUARDIAN_SET_INDEX, + TOKEN_BRIDGE_PROGRAM_ID, + WRAPPED_TBTC_MINT, +} from "./consts"; + +export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export async function transferLamports( + fromSigner: web3.Keypair, + toPubkey: web3.PublicKey, + lamports: number +) { + const program = workspace.WormholeGateway as Program; + return sendAndConfirmTransaction( + program.provider.connection, + new Transaction().add( + SystemProgram.transfer({ + fromPubkey: fromSigner.publicKey, + toPubkey, + lamports, + }) + ), + [fromSigner] + ); +} + +export async function generatePayer(funder: Keypair, lamports?: number) { + const newPayer = Keypair.generate(); + await transferLamports( + funder, + newPayer.publicKey, + lamports === undefined ? 1000000000 : lamports + ); + return newPayer; +} + +export async function getOrCreateAta( + payer: Keypair, + mint: PublicKey, + owner: PublicKey +) { + const program = workspace.WormholeGateway as Program; + const connection = program.provider.connection; + + const token = getAssociatedTokenAddressSync(mint, owner); + const tokenData: Account = await getAccount(connection, token).catch( + (err) => { + if (err instanceof TokenAccountNotFoundError) { + return null; + } else { + throw err; + } + } + ); + + if (tokenData === null) { + await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add( + createAssociatedTokenAccountIdempotentInstruction( + payer.publicKey, + token, + owner, + mint + ) + ), + [payer] ); + } + + return token; +} + +export async function getTokenBalance(token: PublicKey): Promise { + const program = workspace.WormholeGateway as Program; + return getAccount(program.provider.connection, token).then( + (account) => account.amount + ); +} + +export async function preloadWrappedTbtc( + payer: Keypair, + ethereumTokenBridge: MockEthereumTokenBridge, + amount: bigint, + tokenOwner: PublicKey +) { + const program = workspace.WormholeGateway as Program; + const connection = program.provider.connection; + + const wrappedTbtcToken = await getOrCreateAta( + payer, + WRAPPED_TBTC_MINT, + tokenOwner + ); + + // Bridge tbtc to token account. + const published = ethereumTokenBridge.publishTransferTokens( + tryNativeToHexString(ETHEREUM_TBTC_ADDRESS, "ethereum"), + 2, + amount, + 1, + wrappedTbtcToken.toBuffer().toString("hex"), + BigInt(0), + 0, + 0 + ); + + const signedVaa = await mockSignAndPostVaa(payer, published); + + const tx = await redeemOnSolana( + connection, + CORE_BRIDGE_PROGRAM_ID, + TOKEN_BRIDGE_PROGRAM_ID, + payer.publicKey, + signedVaa + ); + await web3.sendAndConfirmTransaction(connection, tx, [payer]); + + return wrappedTbtcToken; +} + +export async function mockSignAndPostVaa( + payer: web3.Keypair, + published: Buffer +) { + const program = workspace.WormholeGateway as Program; + + const guardians = new MockGuardians(GUARDIAN_SET_INDEX, [ + "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0", + ]); + + // Add guardian signature. + const signedVaa = guardians.addSignatures(published, [0]); + + // Verify and post VAA. + await postVaaSolana( + program.provider.connection, + new NodeWallet(payer).signTransaction, + CORE_BRIDGE_PROGRAM_ID, + payer.publicKey, + signedVaa + ); + + return signedVaa; +} + +export async function ethereumGatewaySendTbtc( + payer: web3.Keypair, + ethereumTokenBridge: MockEthereumTokenBridge, + amount: bigint, + fromGateway: number[], + toGateway: PublicKey, + recipient: PublicKey, + tokenAddress?: string, + tokenChain?: number +) { + const program = workspace.WormholeGateway as Program; + + const published = ethereumTokenBridge.publishTransferTokensWithPayload( + tryNativeToHexString(tokenAddress ?? ETHEREUM_TBTC_ADDRESS, "ethereum"), + tokenChain ?? 2, + amount, + 1, + toGateway.toBuffer().toString("hex"), + Buffer.from(fromGateway), + recipient.toBuffer(), + 0, + 0 + ); + + const guardians = new MockGuardians( + GUARDIAN_SET_INDEX, + GUARDIAN_DEVNET_PRIVATE_KEYS + ); + + // Add guardian signature. + const signedVaa = guardians.addSignatures(published, [0]); + + // Verify and post VAA. + await postVaaSolana( + program.provider.connection, + new NodeWallet(payer).signTransaction, + CORE_BRIDGE_PROGRAM_ID, + payer.publicKey, + signedVaa + ); + + return signedVaa; } -export async function generatePayer(connection: web3.Connection, payer: Keypair, lamports?: number) { - const newPayer = Keypair.generate(); - await transferLamports(connection, payer, newPayer.publicKey, lamports === undefined ? 1000000000 : lamports); - return newPayer; -} - -export async function getOrCreateTokenAccount(connection: Connection, payer: Keypair, mint: PublicKey, owner: PublicKey) { - const token = getAssociatedTokenAddressSync(mint, owner); - const tokenData: Account = await getAccount(connection, token).catch((err) => { - if (err instanceof TokenAccountNotFoundError) { - return null; - } else { - throw err; - }; - }); - - if (tokenData === null) { - await web3.sendAndConfirmTransaction( - connection, - new web3.Transaction().add( - createAssociatedTokenAccountIdempotentInstruction( - payer.publicKey, - token, - owner, - mint, - ) - ), - [payer] - ); - - return getAccount(connection, token); - } else { - return tokenData; +export async function expectIxSuccess( + ixes: TransactionInstruction[], + signers: Keypair[] +) { + const program = workspace.WormholeGateway as Program; + await sendAndConfirmTransaction( + program.provider.connection, + new Transaction().add(...ixes), + signers + ).catch((err) => { + if (err.logs !== undefined) { + console.log(err.logs); } -} \ No newline at end of file + throw err; + }); +} + +export async function expectIxFail( + ixes: TransactionInstruction[], + signers: Keypair[], + errorMessage: string +) { + const program = workspace.WormholeGateway as Program; + try { + const txSig = await sendAndConfirmTransaction( + program.provider.connection, + new Transaction().add(...ixes), + signers + ); + assert(false, `transaction should have failed: ${txSig}`); + } catch (err) { + if (err.logs === undefined) { + throw err; + } + const logs: string[] = err.logs; + expect(logs.join("\n")).includes(errorMessage); + } +} + +export function getTokenBridgeCoreEmitter() { + const [tokenBridgeCoreEmitter] = PublicKey.findProgramAddressSync( + [Buffer.from("emitter")], + TOKEN_BRIDGE_PROGRAM_ID + ); + + return tokenBridgeCoreEmitter; +} + +export async function getTokenBridgeSequence() { + const program = workspace.WormholeGateway as Program; + const emitter = getTokenBridgeCoreEmitter(); + return coreBridge + .getSequenceTracker( + program.provider.connection, + emitter, + CORE_BRIDGE_PROGRAM_ID + ) + .then((tracker) => tracker.sequence); +} diff --git a/cross-chain/solana/tests/helpers/wormholeGateway.ts b/cross-chain/solana/tests/helpers/wormholeGateway.ts new file mode 100644 index 000000000..efb3f06d6 --- /dev/null +++ b/cross-chain/solana/tests/helpers/wormholeGateway.ts @@ -0,0 +1,809 @@ +import { parseVaa } from "@certusone/wormhole-sdk"; +import * as tokenBridge from "@certusone/wormhole-sdk/lib/cjs/solana/tokenBridge"; +import * as coreBridge from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; +import { BN, Program, workspace } from "@coral-xyz/anchor"; +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; +import { + PublicKey, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { expect } from "chai"; +import { WormholeGateway } from "../../target/types/wormhole_gateway"; +import { + CORE_BRIDGE_DATA, + CORE_BRIDGE_PROGRAM_ID, + ETHEREUM_ENDPOINT, + TBTC_PROGRAM_ID, + TOKEN_BRIDGE_PROGRAM_ID, + WORMHOLE_GATEWAY_PROGRAM_ID, + WRAPPED_TBTC_ASSET, + WRAPPED_TBTC_MINT, +} from "./consts"; +import * as tbtc from "./tbtc"; +import { getTokenBridgeCoreEmitter, getTokenBridgeSequence } from "./utils"; + +export function getCustodianPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("redeemer")], + WORMHOLE_GATEWAY_PROGRAM_ID + )[0]; +} + +export function getCoreMessagePDA(sequence: bigint): PublicKey { + const encodedSequence = Buffer.alloc(8); + encodedSequence.writeBigUInt64LE(sequence); + return PublicKey.findProgramAddressSync( + [Buffer.from("msg"), encodedSequence], + WORMHOLE_GATEWAY_PROGRAM_ID + )[0]; +} + +export function getGatewayInfoPDA(targetChain: number): PublicKey { + const encodedChain = Buffer.alloc(2); + encodedChain.writeUInt16LE(targetChain); + return PublicKey.findProgramAddressSync( + [Buffer.from("gateway-info"), encodedChain], + WORMHOLE_GATEWAY_PROGRAM_ID + )[0]; +} + +export function getWrappedTbtcTokenPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("wrapped-token")], + WORMHOLE_GATEWAY_PROGRAM_ID + )[0]; +} + +export function getTokenBridgeSenderPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("sender")], + WORMHOLE_GATEWAY_PROGRAM_ID + )[0]; +} + +export function getTokenBridgeRedeemerPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("redeemer")], + WORMHOLE_GATEWAY_PROGRAM_ID + )[0]; +} + +export async function getCustodianData() { + const program = workspace.WormholeGateway as Program; + const custodian = getCustodianPDA(); + return program.account.custodian.fetch(custodian); +} + +export async function checkCustodian(expected: { + authority: PublicKey; + mintingLimit: bigint; + pendingAuthority: PublicKey | null; +}) { + let { authority, mintingLimit, pendingAuthority } = expected; + const custodianState = await getCustodianData(); + + expect(custodianState.mintingLimit.eq(new BN(mintingLimit.toString()))).to.be + .true; + expect(custodianState.authority).to.eql(authority); + expect(custodianState.pendingAuthority).to.eql(pendingAuthority); +} + +export async function getMintedAmount(): Promise { + const custodianState = await getCustodianData(); + return BigInt(custodianState.mintedAmount.toString()); +} + +export async function getGatewayInfo(chain: number) { + const program = workspace.WormholeGateway as Program; + const gatewayInfo = getGatewayInfoPDA(chain); + return program.account.gatewayInfo.fetch(gatewayInfo); +} + +export async function checkGateway(chain: number, expectedAddress: number[]) { + const gatewayInfoState = await getGatewayInfo(chain); + expect(gatewayInfoState.address).to.eql(expectedAddress); +} + +type CancelAuthorityChange = { + custodian?: PublicKey; + authority: PublicKey; +}; + +export async function cancelAuthorityChangeIx( + accounts: CancelAuthorityChange +): Promise { + const program = workspace.WormholeGateway as Program; + + let { custodian, authority } = accounts; + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + return program.methods + .cancelAuthorityChange() + .accounts({ + custodian, + authority, + }) + .instruction(); +} + +type ChangeAuthorityContext = { + custodian?: PublicKey; + authority: PublicKey; + newAuthority: PublicKey; +}; + +export async function changeAuthorityIx( + accounts: ChangeAuthorityContext +): Promise { + const program = workspace.WormholeGateway as Program; + + let { custodian, authority, newAuthority } = accounts; + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + return program.methods + .changeAuthority() + .accounts({ + custodian, + authority, + newAuthority, + }) + .instruction(); +} + +type TakeAuthorityContext = { + custodian?: PublicKey; + pendingAuthority: PublicKey; +}; + +export async function takeAuthorityIx( + accounts: TakeAuthorityContext +): Promise { + const program = workspace.WormholeGateway as Program; + + let { custodian, pendingAuthority } = accounts; + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + return program.methods + .takeAuthority() + .accounts({ + custodian, + pendingAuthority, + }) + .instruction(); +} + +type UpdateMintingLimitContext = { + custodian?: PublicKey; + authority: PublicKey; +}; + +export async function updateMintingLimitIx( + accounts: UpdateMintingLimitContext, + amount: bigint +): Promise { + const program = workspace.WormholeGateway as Program; + + let { custodian, authority } = accounts; + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + return program.methods + .updateMintingLimit(new BN(amount.toString())) + .accounts({ + custodian, + authority, + }) + .instruction(); +} + +type UpdateGatewayAddressContext = { + custodian?: PublicKey; + gatewayInfo?: PublicKey; + authority: PublicKey; +}; + +type UpdateGatewayAddressArgs = { + chain: number; + address: number[]; +}; + +export async function updateGatewayAddress( + accounts: UpdateGatewayAddressContext, + args: UpdateGatewayAddressArgs +) { + const program = workspace.WormholeGateway as Program; + let { custodian, gatewayInfo, authority } = accounts; + + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + if (gatewayInfo === undefined) { + gatewayInfo = getGatewayInfoPDA(args.chain); + } + + return program.methods + .updateGatewayAddress(args) + .accounts({ + custodian, + gatewayInfo, + authority, + }) + .instruction(); +} + +type DepositWormholeTbtcContext = { + custodian?: PublicKey; + wrappedTbtcToken?: PublicKey; + wrappedTbtcMint?: PublicKey; + tbtcMint?: PublicKey; + recipientWrappedToken: PublicKey; + recipientToken: PublicKey; + recipient: PublicKey; + tbtcConfig?: PublicKey; + tbtcMinterInfo?: PublicKey; + tbtcProgram?: PublicKey; +}; + +export async function depositWormholeTbtcIx( + accounts: DepositWormholeTbtcContext, + amount: bigint +): Promise { + const program = workspace.WormholeGateway as Program; + let { + custodian, + wrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + recipientWrappedToken, + recipientToken, + recipient, + tbtcConfig, + tbtcMinterInfo, + tbtcProgram, + } = accounts; + + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + if (wrappedTbtcToken === undefined) { + wrappedTbtcToken = getWrappedTbtcTokenPDA(); + } + + if (wrappedTbtcMint === undefined) { + wrappedTbtcMint = WRAPPED_TBTC_MINT; + } + + if (tbtcMint === undefined) { + tbtcMint = tbtc.getMintPDA(); + } + + if (tbtcConfig === undefined) { + tbtcConfig = tbtc.getConfigPDA(); + } + + if (tbtcMinterInfo === undefined) { + tbtcMinterInfo = tbtc.getMinterInfoPDA(custodian); + } + + if (tbtcProgram === undefined) { + tbtcProgram = TBTC_PROGRAM_ID; + } + + return program.methods + .depositWormholeTbtc(new BN(amount.toString())) + .accounts({ + custodian, + wrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + recipientWrappedToken, + recipientToken, + recipient, + tbtcConfig, + tbtcMinterInfo, + tbtcProgram, + }) + .instruction(); +} + +type ReceiveTbtcContext = { + payer: PublicKey; + custodian?: PublicKey; + postedVaa?: PublicKey; + tokenBridgeClaim?: PublicKey; + wrappedTbtcToken?: PublicKey; + wrappedTbtcMint?: PublicKey; + tbtcMint?: PublicKey; + recipientToken: PublicKey; + recipient: PublicKey; + recipientWrappedToken?: PublicKey; + tbtcConfig?: PublicKey; + tbtcMinterInfo?: PublicKey; + tokenBridgeConfig?: PublicKey; + tokenBridgeRegisteredEmitter?: PublicKey; + //tokenBridgeRedeemer?: PublicKey; + tokenBridgeWrappedAsset?: PublicKey; + tokenBridgeMintAuthority?: PublicKey; + rent?: PublicKey; + tbtcProgram?: PublicKey; + tokenBridgeProgram?: PublicKey; + coreBridgeProgram?: PublicKey; +}; + +export async function receiveTbtcIx( + accounts: ReceiveTbtcContext, + signedVaa: Buffer +): Promise { + const parsed = parseVaa(signedVaa); + + const program = workspace.WormholeGateway as Program; + let { + payer, + custodian, + postedVaa, + tokenBridgeClaim, + wrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + recipientToken, + recipient, + recipientWrappedToken, + tbtcConfig, + tbtcMinterInfo, + tokenBridgeConfig, + tokenBridgeRegisteredEmitter, + //tokenBridgeRedeemer, + tokenBridgeWrappedAsset, + tokenBridgeMintAuthority, + rent, + tbtcProgram, + tokenBridgeProgram, + coreBridgeProgram, + } = accounts; + + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + if (postedVaa === undefined) { + postedVaa = coreBridge.derivePostedVaaKey( + CORE_BRIDGE_PROGRAM_ID, + parsed.hash + ); + } + + if (tokenBridgeClaim === undefined) { + tokenBridgeClaim = coreBridge.deriveClaimKey( + TOKEN_BRIDGE_PROGRAM_ID, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ); + } + + if (wrappedTbtcToken === undefined) { + wrappedTbtcToken = getWrappedTbtcTokenPDA(); + } + + if (wrappedTbtcMint === undefined) { + wrappedTbtcMint = WRAPPED_TBTC_MINT; + } + + if (tbtcMint === undefined) { + tbtcMint = tbtc.getMintPDA(); + } + + if (recipientWrappedToken == undefined) { + recipientWrappedToken = getAssociatedTokenAddressSync( + wrappedTbtcMint, + recipient + ); + } + + if (tbtcConfig === undefined) { + tbtcConfig = tbtc.getConfigPDA(); + } + + if (tbtcMinterInfo === undefined) { + tbtcMinterInfo = tbtc.getMinterInfoPDA(custodian); + } + + if (tokenBridgeConfig === undefined) { + tokenBridgeConfig = tokenBridge.deriveTokenBridgeConfigKey( + TOKEN_BRIDGE_PROGRAM_ID + ); + } + + if (tokenBridgeRegisteredEmitter === undefined) { + tokenBridgeRegisteredEmitter = ETHEREUM_ENDPOINT; + } + + // if (tokenBridgeRedeemer === undefined) { + // tokenBridgeRedeemer = tokenBridge.deriveRedeemerAccountKey( + // WORMHOLE_GATEWAY_PROGRAM_ID + // ); + // } + + if (tokenBridgeWrappedAsset === undefined) { + tokenBridgeWrappedAsset = WRAPPED_TBTC_ASSET; + } + + if (tokenBridgeMintAuthority === undefined) { + tokenBridgeMintAuthority = tokenBridge.deriveMintAuthorityKey( + TOKEN_BRIDGE_PROGRAM_ID + ); + } + + if (rent === undefined) { + rent = SYSVAR_RENT_PUBKEY; + } + + if (tbtcProgram === undefined) { + tbtcProgram = TBTC_PROGRAM_ID; + } + + if (tokenBridgeProgram === undefined) { + tokenBridgeProgram = TOKEN_BRIDGE_PROGRAM_ID; + } + + if (coreBridgeProgram === undefined) { + coreBridgeProgram = CORE_BRIDGE_PROGRAM_ID; + } + + return program.methods + .receiveTbtc(Array.from(parsed.hash)) + .accounts({ + payer, + custodian, + postedVaa, + tokenBridgeClaim, + wrappedTbtcToken, + tbtcMint, + recipientToken, + recipient, + recipientWrappedToken, + tbtcConfig, + tbtcMinterInfo, + wrappedTbtcMint, + tokenBridgeConfig, + tokenBridgeRegisteredEmitter, + //tokenBridgeRedeemer, + tokenBridgeWrappedAsset, + tokenBridgeMintAuthority, + rent, + tbtcProgram, + tokenBridgeProgram, + coreBridgeProgram, + }) + .instruction(); +} + +type SendTbtcGatewayContext = { + custodian?: PublicKey; + gatewayInfo?: PublicKey; + wrappedTbtcToken?: PublicKey; + wrappedTbtcMint?: PublicKey; + tbtcMint?: PublicKey; + senderToken: PublicKey; + sender: PublicKey; + tokenBridgeConfig?: PublicKey; + tokenBridgeWrappedAsset?: PublicKey; + tokenBridgeTransferAuthority?: PublicKey; + coreBridgeData?: PublicKey; + coreMessage?: PublicKey; + tokenBridgeCoreEmitter?: PublicKey; + coreEmitterSequence?: PublicKey; + coreFeeCollector?: PublicKey; + clock?: PublicKey; + tokenBridgeSender?: PublicKey; + rent?: PublicKey; + tokenBridgeProgram?: PublicKey; + coreBridgeProgram?: PublicKey; +}; + +type SendTbtcGatewayArgs = { + amount: BN; + recipientChain: number; + recipient: number[]; + nonce: number; +}; + +export async function sendTbtcGatewayIx( + accounts: SendTbtcGatewayContext, + args: SendTbtcGatewayArgs +): Promise { + const program = workspace.WormholeGateway as Program; + let { + custodian, + gatewayInfo, + wrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + senderToken, + sender, + tokenBridgeConfig, + tokenBridgeWrappedAsset, + tokenBridgeTransferAuthority, + coreBridgeData, + coreMessage, + tokenBridgeCoreEmitter, + coreEmitterSequence, + coreFeeCollector, + clock, + tokenBridgeSender, + rent, + tokenBridgeProgram, + coreBridgeProgram, + } = accounts; + + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + if (gatewayInfo === undefined) { + gatewayInfo = getGatewayInfoPDA(args.recipientChain); + } + + if (wrappedTbtcToken === undefined) { + wrappedTbtcToken = getWrappedTbtcTokenPDA(); + } + + if (wrappedTbtcMint === undefined) { + wrappedTbtcMint = WRAPPED_TBTC_MINT; + } + + if (tbtcMint === undefined) { + tbtcMint = tbtc.getMintPDA(); + } + + if (tokenBridgeConfig === undefined) { + tokenBridgeConfig = tokenBridge.deriveTokenBridgeConfigKey( + TOKEN_BRIDGE_PROGRAM_ID + ); + } + + if (tokenBridgeWrappedAsset === undefined) { + tokenBridgeWrappedAsset = WRAPPED_TBTC_ASSET; + } + + if (tokenBridgeTransferAuthority === undefined) { + tokenBridgeTransferAuthority = tokenBridge.deriveAuthoritySignerKey( + TOKEN_BRIDGE_PROGRAM_ID + ); + } + + if (coreBridgeData === undefined) { + coreBridgeData = CORE_BRIDGE_DATA; + } + + if (coreMessage === undefined) { + const sequence = await getTokenBridgeSequence(); + coreMessage = getCoreMessagePDA(sequence); + } + + if (tokenBridgeCoreEmitter === undefined) { + tokenBridgeCoreEmitter = getTokenBridgeCoreEmitter(); + } + + if (coreEmitterSequence === undefined) { + coreEmitterSequence = coreBridge.deriveEmitterSequenceKey( + tokenBridgeCoreEmitter, + CORE_BRIDGE_PROGRAM_ID + ); + } + + if (coreFeeCollector === undefined) { + coreFeeCollector = coreBridge.deriveFeeCollectorKey(CORE_BRIDGE_PROGRAM_ID); + } + + if (clock === undefined) { + clock = SYSVAR_CLOCK_PUBKEY; + } + + if (tokenBridgeSender === undefined) { + tokenBridgeSender = tokenBridge.deriveSenderAccountKey( + WORMHOLE_GATEWAY_PROGRAM_ID + ); + } + + if (rent === undefined) { + rent = SYSVAR_RENT_PUBKEY; + } + + if (tokenBridgeProgram === undefined) { + tokenBridgeProgram = TOKEN_BRIDGE_PROGRAM_ID; + } + + if (coreBridgeProgram === undefined) { + coreBridgeProgram = CORE_BRIDGE_PROGRAM_ID; + } + + return program.methods + .sendTbtcGateway(args) + .accounts({ + custodian, + gatewayInfo, + wrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + senderToken, + sender, + tokenBridgeConfig, + tokenBridgeWrappedAsset, + tokenBridgeTransferAuthority, + coreBridgeData, + coreMessage, + tokenBridgeCoreEmitter, + coreEmitterSequence, + coreFeeCollector, + clock, + tokenBridgeSender, + rent, + tokenBridgeProgram, + coreBridgeProgram, + }) + .instruction(); +} + +type SendTbtcWrappedContext = { + custodian?: PublicKey; + wrappedTbtcToken?: PublicKey; + wrappedTbtcMint?: PublicKey; + tbtcMint?: PublicKey; + senderToken: PublicKey; + sender: PublicKey; + tokenBridgeConfig?: PublicKey; + tokenBridgeWrappedAsset?: PublicKey; + tokenBridgeTransferAuthority?: PublicKey; + coreBridgeData?: PublicKey; + coreMessage?: PublicKey; + tokenBridgeCoreEmitter?: PublicKey; + coreEmitterSequence?: PublicKey; + coreFeeCollector?: PublicKey; + clock?: PublicKey; + rent?: PublicKey; + tokenBridgeProgram?: PublicKey; + coreBridgeProgram?: PublicKey; +}; + +type SendTbtcWrappedArgs = { + amount: BN; + recipientChain: number; + recipient: number[]; + arbiterFee: BN; + nonce: number; +}; + +export async function sendTbtcWrappedIx( + accounts: SendTbtcWrappedContext, + args: SendTbtcWrappedArgs +): Promise { + const program = workspace.WormholeGateway as Program; + let { + custodian, + wrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + senderToken, + sender, + tokenBridgeConfig, + tokenBridgeWrappedAsset, + tokenBridgeTransferAuthority, + coreBridgeData, + coreMessage, + tokenBridgeCoreEmitter, + coreEmitterSequence, + coreFeeCollector, + clock, + rent, + tokenBridgeProgram, + coreBridgeProgram, + } = accounts; + + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + if (wrappedTbtcToken === undefined) { + wrappedTbtcToken = getWrappedTbtcTokenPDA(); + } + + if (wrappedTbtcMint === undefined) { + wrappedTbtcMint = WRAPPED_TBTC_MINT; + } + + if (tbtcMint === undefined) { + tbtcMint = tbtc.getMintPDA(); + } + + if (tokenBridgeConfig === undefined) { + tokenBridgeConfig = tokenBridge.deriveTokenBridgeConfigKey( + TOKEN_BRIDGE_PROGRAM_ID + ); + } + + if (tokenBridgeWrappedAsset === undefined) { + tokenBridgeWrappedAsset = WRAPPED_TBTC_ASSET; + } + + if (tokenBridgeTransferAuthority === undefined) { + tokenBridgeTransferAuthority = tokenBridge.deriveAuthoritySignerKey( + TOKEN_BRIDGE_PROGRAM_ID + ); + } + + if (coreBridgeData === undefined) { + coreBridgeData = CORE_BRIDGE_DATA; + } + + if (coreMessage === undefined) { + const sequence = await getTokenBridgeSequence(); + coreMessage = getCoreMessagePDA(sequence); + } + + if (tokenBridgeCoreEmitter === undefined) { + tokenBridgeCoreEmitter = getTokenBridgeCoreEmitter(); + } + + if (coreEmitterSequence === undefined) { + coreEmitterSequence = coreBridge.deriveEmitterSequenceKey( + tokenBridgeCoreEmitter, + CORE_BRIDGE_PROGRAM_ID + ); + } + + if (coreFeeCollector === undefined) { + coreFeeCollector = coreBridge.deriveFeeCollectorKey(CORE_BRIDGE_PROGRAM_ID); + } + + if (clock === undefined) { + clock = SYSVAR_CLOCK_PUBKEY; + } + + if (rent === undefined) { + rent = SYSVAR_RENT_PUBKEY; + } + + if (tokenBridgeProgram === undefined) { + tokenBridgeProgram = TOKEN_BRIDGE_PROGRAM_ID; + } + + if (coreBridgeProgram === undefined) { + coreBridgeProgram = CORE_BRIDGE_PROGRAM_ID; + } + + return program.methods + .sendTbtcWrapped(args) + .accounts({ + custodian, + wrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + senderToken, + sender, + tokenBridgeConfig, + tokenBridgeWrappedAsset, + tokenBridgeTransferAuthority, + coreBridgeData, + coreMessage, + tokenBridgeCoreEmitter, + coreEmitterSequence, + coreFeeCollector, + clock, + rent, + tokenBridgeProgram, + coreBridgeProgram, + }) + .instruction(); +}