diff --git a/.base-ammanrc.js b/.base-ammanrc.js index 25fef75411..87e2b1f8fe 100644 --- a/.base-ammanrc.js +++ b/.base-ammanrc.js @@ -40,9 +40,15 @@ const programs = { programId: 'cndy3Z4yapfJBmL3ShUp5exZKqR3z33thTzeNMm2gRZ', deployPath: localDeployPath('mpl_candy_machine'), }, + auction_house: { + label: "Auction House", + programId: 'hausS13jsjafwWwGqZTUQRmWyvyxn9EQpqMwV1PBBmk', + deployPath: localDeployPath('mpl_auction_house'), + }, }; const validator = { + accountsCluster: 'https://api.metaplex.solana.com', killRunningValidators: true, programs, commitment: 'singleGossip', diff --git a/.github/actions/yarn-install-and-build/action.yml b/.github/actions/yarn-install-and-build/action.yml index 7291cf11cd..c0e0be7979 100644 --- a/.github/actions/yarn-install-and-build/action.yml +++ b/.github/actions/yarn-install-and-build/action.yml @@ -24,6 +24,14 @@ inputs: description: If true it will build the token_vault package required: false default: false + build_auction_house: + description: If true it will build the auction_house package + required: false + default: false + build_auction_house_cli: + description: If true it will build the auction_house CLI package + required: false + default: false runs: using: composite @@ -83,6 +91,26 @@ runs: working-directory: ./token-vault/js shell: bash + - name: Install and Build auction-house + if: inputs.build_auction_house == 'true' + run: | + echo 'Install and Build auction_house: yarn install' + yarn install + echo 'Install and Build auction_house: yarn build' + yarn build + working-directory: ./auction-house/js + shell: bash + + - name: Install and Build auction-house CLI + if: inputs.build_auction_house_cli == 'true' + run: | + echo 'Install and Build auction_house CLI: yarn install' + yarn install + echo 'Install and Build auction_house CLI: yarn build' + yarn build + working-directory: ./auction-house/cli + shell: bash + ############## # Build Contract ############## diff --git a/.github/workflows/integration-auction-house.yml b/.github/workflows/integration-auction-house.yml new file mode 100644 index 0000000000..577aa676ef --- /dev/null +++ b/.github/workflows/integration-auction-house.yml @@ -0,0 +1,104 @@ +name: Integration Auction House + +on: + push: + branches: [master] + pull_request: + branches: [master] + +env: + CARGO_TERM_COLOR: always + SOLANA_VERSION: 1.9.14 + RUST_TOOLCHAIN: stable + +jobs: + changes: + runs-on: ubuntu-latest + # Set job outputs to values from filter step + outputs: + core: ${{ steps.filter.outputs.core }} + package: ${{ steps.filter.outputs.package }} + steps: + - uses: actions/checkout@v2 + # For pull requests it's not necessary to checkout the code + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + core: + - 'core/**' + package: + - 'auction-house/**' + build-and-integration-test-auction-house: + runs-on: ubuntu-latest + env: + cache_id: program-auction-house + needs: changes + if: ${{ needs.changes.outputs.core == 'true' || needs.changes.outputs.package == 'true' }} + steps: + # Setup Deps + - uses: actions/checkout@v2 + - uses: ./.github/actions/install-linux-build-deps + - uses: ./.github/actions/install-solana + with: + solana_version: ${{ env.SOLANA_VERSION }} + - uses: ./.github/actions/install-rust + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + + # Restore Cache from previous build/test + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ./rust/target + key: ${{ env.cache_id }}-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUSTC_HASH }} + + # Build Rust Programs + - uses: ./.github/actions/build-auction-house + + # Install JS SDK deps + - uses: ./.github/actions/yarn-install-and-build + with: + cache_id: sdk-auction-house + working_dir: ./auction-house/js + build_auction_house: true + + # Install CLI deps + - uses: ./.github/actions/yarn-install-and-build + with: + cache_id: sdk-auction-house + working_dir: ./auction-house/cli + build_auction_house_cli: true + + # Run integration test + - name: start-local-test-validator + working-directory: ./auction-house/js + run: DEBUG=amman* yarn amman:start + + - name: integration-test-auction-house-program + id: run_integration_test + working-directory: ./auction-house/js + run: DEBUG=mpl* yarn test + + - name: stop-local-test-validator + working-directory: ./auction-house/js + run: DEBUG=amman* yarn amman:stop + + # Run integration test - onchain program + - name: start-local-test-validator-onchain + working-directory: ./auction-house/js + run: DEBUG=amman* yarn amman:start-custom-ammanrc onchain.ammanrc.js + + - name: integration-test-auction-house-program + id: run_integration_test_onchain + working-directory: ./auction-house/js + run: DEBUG=mpl* yarn test + + - name: stop-local-test-validator + working-directory: ./auction-house/js + run: DEBUG=amman* yarn amman:stop + diff --git a/Cargo.toml b/Cargo.toml index 1f7441db93..85263fa26c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members=[ "token-vault/program", "token-vault/test", "nft-packs/program", - "fixed-price-sale/program" + "fixed-price-sale/program", ] exclude = [ "fixed-price-sale/cli", diff --git a/auction-house/cli/package.json b/auction-house/cli/package.json index 411e9b8739..036a257cf5 100644 --- a/auction-house/cli/package.json +++ b/auction-house/cli/package.json @@ -9,7 +9,8 @@ "format:check": "prettier --loglevel warn --check \"**/*.{ts,js,json,yaml}\"", "lint:eslint": "eslint '{src,test}/**/*.{ts,tsx}'", "lint:fix": "prettier --write '{src,test}/**/*.{ts,tsx}' && eslint --fix '{src,test}/**/*.{ts,tsx}'", - "test": "jest" + "test": "jest", + "build": "yarn tsc -p ./src" }, "pkg": { "scripts": "./build/**/*.{js|json}" diff --git a/auction-house/cli/src/helpers/accounts.ts b/auction-house/cli/src/helpers/accounts.ts index 29c793902d..36aa89178c 100644 --- a/auction-house/cli/src/helpers/accounts.ts +++ b/auction-house/cli/src/helpers/accounts.ts @@ -1,3 +1,6 @@ +// @ts-nocheck +// Errors from this file broke the build for +// auction-house/js which references it in tests import { Keypair, PublicKey, diff --git a/auction-house/cli/src/helpers/instructions.ts b/auction-house/cli/src/helpers/instructions.ts index 3a8de16057..489a2c3907 100644 --- a/auction-house/cli/src/helpers/instructions.ts +++ b/auction-house/cli/src/helpers/instructions.ts @@ -1,3 +1,6 @@ +// @ts-nocheck +// Errors from this file broke the build for +// auction-house/js which references it in tests import { PublicKey, SystemProgram, diff --git a/auction-house/cli/src/helpers/various.ts b/auction-house/cli/src/helpers/various.ts index 734e038e40..8c96deda6c 100644 --- a/auction-house/cli/src/helpers/various.ts +++ b/auction-house/cli/src/helpers/various.ts @@ -1,3 +1,6 @@ +// @ts-nocheck +// Errors from this file broke the build for +// auction-house/js which references it in tests import { LAMPORTS_PER_SOL, AccountInfo, diff --git a/auction-house/cli/src/tsconfig.json b/auction-house/cli/src/tsconfig.json index e5dc17b288..5cd1deb88b 100644 --- a/auction-house/cli/src/tsconfig.json +++ b/auction-house/cli/src/tsconfig.json @@ -16,7 +16,8 @@ "suppressImplicitAnyIndexErrors": true, "resolveJsonModule": true, "lib": ["dom", "es2019"], - "types": ["jest", "node", "webgl2"] + "types": ["jest", "node", "webgl2"], + "skipLibCheck": true }, "exclude": ["node_modules", "typings/browser", "typings/browser.d.ts"], "atom": { diff --git a/auction-house/cli/tsconfig.json b/auction-house/cli/tsconfig.json index e9dd44415d..3c67c7fdc2 100644 --- a/auction-house/cli/tsconfig.json +++ b/auction-house/cli/tsconfig.json @@ -10,7 +10,7 @@ "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - "noImplicitAny": true, + "noImplicitAny": false, "typeRoots": ["types", "node_modules/@types"] }, "include": ["src/**/*"] diff --git a/auction-house/js/.ammanrc.js b/auction-house/js/.ammanrc.js new file mode 100644 index 0000000000..e2b8c56755 --- /dev/null +++ b/auction-house/js/.ammanrc.js @@ -0,0 +1,11 @@ +'use strict'; +// @ts-check +const base = require('../../.base-ammanrc.js'); + +const validator = { + ...base.validator, + programs: [ + base.programs.auction_house, + ], +}; +module.exports = { validator }; diff --git a/auction-house/js/.gitignore b/auction-house/js/.gitignore new file mode 100644 index 0000000000..6e4834ddf9 --- /dev/null +++ b/auction-house/js/.gitignore @@ -0,0 +1 @@ +.amman \ No newline at end of file diff --git a/auction-house/js/onchain.ammanrc.js b/auction-house/js/onchain.ammanrc.js new file mode 100644 index 0000000000..8675a4361a --- /dev/null +++ b/auction-house/js/onchain.ammanrc.js @@ -0,0 +1,19 @@ +'use strict'; +// @ts-check +const base = require('../../.base-ammanrc.js'); + +const accounts = [ + { + label: "Auction House", + accountId: 'hausS13jsjafwWwGqZTUQRmWyvyxn9EQpqMwV1PBBmk', + executable: true, + }, +] + +const validator = { + ...base.validator, + programs: [], + accounts: accounts +}; + +module.exports = { validator }; diff --git a/auction-house/js/package.json b/auction-house/js/package.json index a618793450..432abe0a78 100644 --- a/auction-house/js/package.json +++ b/auction-house/js/package.json @@ -13,7 +13,12 @@ "build:docs": "typedoc", "build": "rimraf dist && tsc -p tsconfig.json", "build:watch": "rimraf dist && tsc -p tsconfig.json --watch", - "test": "esr ./test/*.test.ts", + "amman:start": "DEBUG=\"amman*\" amman start", + "amman:start-custom-ammanrc": "DEBUG=\"amman*\" amman start --config ${0}", + "amman:stop": "DEBUG=\"amman*\" amman stop", + "test:all": "yarn build && yarn amman:start && yarn test", + "test:all-custom-ammanrc": "yarn build && yarn amman:start-custom-ammanrc ${0} && yarn test", + "test": "esr ./test/**/*.ts", "api:gen": "DEBUG='(solita|rustbin):(info|error)' solita", "lint": "eslint \"{src,test}/**/*.ts\" --format stylish", "fix:lint": "yarn lint --fix", @@ -46,8 +51,10 @@ "bn.js": "^5.2.0" }, "devDependencies": { + "@metaplex-foundation/amman": "^0.10.0", "@metaplex-foundation/solita": "^0.5.0", "@types/tape": "^4.13.2", + "esbuild-runner": "^2.2.1", "eslint": "^8.3.0", "prettier": "^2.5.1", "rimraf": "^3.0.2", diff --git a/auction-house/js/test/account-instructions.auction-house.test.ts b/auction-house/js/test/account-instructions.auction-house.test.ts new file mode 100644 index 0000000000..0734bd6e9d --- /dev/null +++ b/auction-house/js/test/account-instructions.auction-house.test.ts @@ -0,0 +1,237 @@ +import { + AccountInfo, + Connection, + Keypair, + PublicKey, + Transaction, + LAMPORTS_PER_SOL, +} from '@solana/web3.js'; +import { AuctionHouse, AuctionHouseArgs } from 'src/generated'; +import test from 'tape'; +import spok from 'spok'; +import { + getAuctionHouse, + getAuctionHouseFeeAcct, + getAuctionHouseTreasuryAcct, + getAuctionHouseBuyerEscrow, +} from '../../cli/src/helpers/accounts'; + +import { + CreateAuctionHouseInstructionAccounts, + CreateAuctionHouseInstructionArgs, + createCreateAuctionHouseInstruction, + DepositInstructionAccounts, + DepositInstructionArgs, + createDepositInstruction, + WithdrawInstructionAccounts, + WithdrawInstructionArgs, + createWithdrawInstruction, +} from 'src/generated'; +import { Amman } from '@metaplex-foundation/amman-client'; +import { LOCALHOST } from '@metaplex-foundation/amman'; + +const connectionURL = LOCALHOST; + +const WRAPPED_SOL_MINT = new PublicKey('So11111111111111111111111111111111111111112'); +const REQUIRED_RENT_EXEMPTION = 890_880; + +const AUCTION_HOUSE_PROGRAM_ID = new PublicKey('hausS13jsjafwWwGqZTUQRmWyvyxn9EQpqMwV1PBBmk'); +const amman = Amman.instance({ + knownLabels: { [AUCTION_HOUSE_PROGRAM_ID.toString()]: 'Auction House' }, + log: console.log, +}); + +function quickKeypair(): [PublicKey, Uint8Array] { + const kp = Keypair.generate(); + return [kp.publicKey, kp.secretKey]; +} + +test('account auction-house: round trip serialization', async (t) => { + const [creator] = quickKeypair(); + const [auctionHouseTreasury] = quickKeypair(); + const [treasuryWithdrawalDestination] = quickKeypair(); + const [feeWithdrawalDestination] = quickKeypair(); + const [treasuryMint] = quickKeypair(); + + const args: AuctionHouseArgs = { + auctionHouseFeeAccount: creator, + auctionHouseTreasury, + treasuryWithdrawalDestination, + feeWithdrawalDestination, + treasuryMint, + authority: creator, + creator, + bump: 0, + treasuryBump: 1, + feePayerBump: 2, + sellerFeeBasisPoints: 3, + requiresSignOff: false, + canChangeSalePrice: true, + escrowPaymentBump: 255, + hasAuctioneer: false, + auctioneerAddress: PublicKey.default, + scopes: Array(7).fill(false), // constant size field in the contract + }; + + const expected = AuctionHouse.fromArgs(args); + const [data] = expected.serialize(); + + const info: AccountInfo = { + executable: false, + data, + owner: creator, + lamports: 1000, + }; + + const actual = AuctionHouse.fromAccountInfo(info)[0]; + spok(t, actual, expected); +}); + +test('test auction-house instructions', async (t) => { + const authority = Keypair.generate(); + const connection = new Connection(connectionURL, 'confirmed'); + + test('instruction auction-house: create auction-house', async (t) => { + const treasuryWithdrawal = Keypair.generate(); + const transactionHandler = amman.payerTransactionHandler(connection, authority); + const authority_aidrop_sol = 2; + await amman.airdrop(connection, authority.publicKey, authority_aidrop_sol); + const authority_balance = await connection.getBalance(authority.publicKey); + t.equal(authority_balance / LAMPORTS_PER_SOL, authority_aidrop_sol); + + const [auctionHouse, ahBump] = await getAuctionHouse(authority.publicKey, WRAPPED_SOL_MINT); + const [feeAccount, feeBump] = await getAuctionHouseFeeAcct(auctionHouse); + const [treasuryAccount, treasuryBump] = await getAuctionHouseTreasuryAcct(auctionHouse); + + const accounts: CreateAuctionHouseInstructionAccounts = { + treasuryMint: WRAPPED_SOL_MINT, + payer: authority.publicKey, + authority: authority.publicKey, + feeWithdrawalDestination: authority.publicKey, + treasuryWithdrawalDestination: treasuryWithdrawal.publicKey, + treasuryWithdrawalDestinationOwner: treasuryWithdrawal.publicKey, + auctionHouse: auctionHouse, + auctionHouseFeeAccount: feeAccount, + auctionHouseTreasury: treasuryAccount, + }; + + const args: CreateAuctionHouseInstructionArgs = { + bump: ahBump, + feePayerBump: feeBump, + treasuryBump: treasuryBump, + sellerFeeBasisPoints: 250, + requiresSignOff: true, + canChangeSalePrice: false, + }; + const create_ah_instruction = createCreateAuctionHouseInstruction(accounts, args); + const tx = new Transaction().add(create_ah_instruction); + tx.recentBlockhash = (await connection.getRecentBlockhash()).blockhash; + const txId = await transactionHandler.sendAndConfirmTransaction(tx, [authority], { + skipPreflight: false, + }); + t.ok(txId); + t.end(); + }); + test('instruction auction-house: deposit and withdraw', async (t) => { + const wallet = Keypair.generate(); + const transactionHandler = amman.payerTransactionHandler(connection, wallet); + const [auctionHouse, {}] = await getAuctionHouse(authority.publicKey, WRAPPED_SOL_MINT); + const [feeAccount, {}] = await getAuctionHouseFeeAcct(auctionHouse); + const [escrowPaymentAccount, escrowPaymentBump] = await getAuctionHouseBuyerEscrow( + auctionHouse, + wallet.publicKey, + ); + + const auction_house_fee_account_pre_balance = await connection.getBalance(feeAccount); + await amman.airdrop(connection, wallet.publicKey, 2); + const wallet_sol_pre_balance = await connection.getBalance(wallet.publicKey); + const deposit_amount = 1000; + const expected_balance_post_withdraw = REQUIRED_RENT_EXEMPTION; + + const depositAccounts: DepositInstructionAccounts = { + wallet: wallet.publicKey, + paymentAccount: wallet.publicKey, + transferAuthority: authority.publicKey, + escrowPaymentAccount: escrowPaymentAccount, + treasuryMint: WRAPPED_SOL_MINT, + authority: authority.publicKey, + auctionHouse: auctionHouse, + auctionHouseFeeAccount: feeAccount, + }; + + const deposit_args: DepositInstructionArgs = { + escrowPaymentBump: escrowPaymentBump, + amount: deposit_amount, + }; + + const deposit_instruction = createDepositInstruction(depositAccounts, deposit_args); + const deposit_tx = new Transaction().add(deposit_instruction); + deposit_tx.recentBlockhash = (await connection.getRecentBlockhash()).blockhash; + await transactionHandler.sendAndConfirmTransaction(deposit_tx, [wallet, authority], { + skipPreflight: false, + }); + + const deposit_fee_paid = (await connection.getFeeForMessage(deposit_tx.compileMessage())).value; + const wallet_sol_post_deposit_balance = await connection.getBalance(wallet.publicKey); + const escrow_post_deposit_balance = await connection.getBalance(escrowPaymentAccount); + + t.equal( + wallet_sol_post_deposit_balance, + wallet_sol_pre_balance - deposit_amount - deposit_fee_paid - REQUIRED_RENT_EXEMPTION, + 'wallet_sol_post_deposit_balance', + ); + t.equal( + escrow_post_deposit_balance, + deposit_amount + REQUIRED_RENT_EXEMPTION, + 'escrow_sol_post_deposit_balance', + ); + + // withdraw + const withdrawAccounts: WithdrawInstructionAccounts = { + wallet: wallet.publicKey, + receiptAccount: wallet.publicKey, + escrowPaymentAccount: escrowPaymentAccount, + treasuryMint: WRAPPED_SOL_MINT, + authority: authority.publicKey, + auctionHouse: auctionHouse, + auctionHouseFeeAccount: feeAccount, + }; + + const withdraw_args: WithdrawInstructionArgs = { + escrowPaymentBump: escrowPaymentBump, + amount: deposit_amount, + }; + + const withdraw_instruction = createWithdrawInstruction(withdrawAccounts, withdraw_args); + const withdraw_tx = new Transaction().add(withdraw_instruction); + withdraw_tx.recentBlockhash = (await connection.getRecentBlockhash()).blockhash; + await transactionHandler.sendAndConfirmTransaction(withdraw_tx, [wallet, authority], { + skipPreflight: false, + }); + + const withdraw_fee_paid = (await connection.getFeeForMessage(withdraw_tx.compileMessage())) + .value; + const escrow_post_withdraw_balance = await connection.getBalance(escrowPaymentAccount); + const wallet_sol_post_withdraw_balance = await connection.getBalance(wallet.publicKey); + + t.equal( + escrow_post_withdraw_balance, + expected_balance_post_withdraw, + 'escrow balance post withdraw == expected', + ); + t.equal( + wallet_sol_post_withdraw_balance, + wallet_sol_post_deposit_balance - withdraw_fee_paid + deposit_amount, + 'wallet balance post withdraw == expected', + ); + + const auction_house_fee_account_post_balance = await connection.getBalance(feeAccount); + t.equal( + auction_house_fee_account_pre_balance, + auction_house_fee_account_post_balance, + 'auction_house_fee_account_pre_balance == auction_house_fee_account_post_balance', + ); + t.end(); + }); + t.ok(true); +}); diff --git a/auction-house/js/test/account.auction-house.test.ts b/auction-house/js/test/account.auction-house.test.ts deleted file mode 100644 index 61cbdd55d1..0000000000 --- a/auction-house/js/test/account.auction-house.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { AccountInfo, Keypair, PublicKey } from '@solana/web3.js'; -import { AuctionHouse, AuctionHouseArgs } from '../src/generated'; -import test from 'tape'; -import spok from 'spok'; - -function quickKeypair(): [PublicKey, Uint8Array] { - const kp = Keypair.generate(); - return [kp.publicKey, kp.secretKey]; -} - -test('account auction-house: round trip serilization', async (t) => { - const [creator] = quickKeypair(); - const [auctionHouseTreasury] = quickKeypair(); - const [treasuryWithdrawalDestination] = quickKeypair(); - const [feeWithdrawalDestination] = quickKeypair(); - const [treasuryMint] = quickKeypair(); - - const args: AuctionHouseArgs = { - auctionHouseFeeAccount: creator, - auctionHouseTreasury, - treasuryWithdrawalDestination, - feeWithdrawalDestination, - treasuryMint, - authority: creator, - creator, - bump: 0, - treasuryBump: 1, - feePayerBump: 2, - sellerFeeBasisPoints: 3, - requiresSignOff: false, - canChangeSalePrice: true, - escrowPaymentBump: 255, - hasAuctioneer: false, - auctioneerAddress: PublicKey.default, - scopes: [], - }; - - const expected = AuctionHouse.fromArgs(args); - const [data] = expected.serialize(); - - const info: AccountInfo = { - executable: false, - data, - owner: creator, - lamports: 1000, - }; - - const actual = AuctionHouse.fromAccountInfo(info)[0]; - spok(t, actual, expected); -}); diff --git a/auction-house/js/tsconfig.json b/auction-house/js/tsconfig.json index b070394b14..15fd1ab6a8 100644 --- a/auction-house/js/tsconfig.json +++ b/auction-house/js/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.build.json", "compilerOptions": { - "rootDir": "." + "rootDirs": [".", ".."] }, - "include": ["./src", "test"] + "include": ["./src", "test", "../cli/js"] } diff --git a/yarn.lock b/yarn.lock index 5a84851b30..03b112e0a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -932,7 +932,7 @@ __metadata: languageName: node linkType: hard -"@metaplex-foundation/amman@npm:0.10.0": +"@metaplex-foundation/amman@npm:0.10.0, @metaplex-foundation/amman@npm:^0.10.0": version: 0.10.0 resolution: "@metaplex-foundation/amman@npm:0.10.0" dependencies: @@ -1082,6 +1082,7 @@ __metadata: version: 0.0.0-use.local resolution: "@metaplex-foundation/mpl-auction-house@workspace:auction-house/js" dependencies: + "@metaplex-foundation/amman": ^0.10.0 "@metaplex-foundation/beet": ^0.1.0 "@metaplex-foundation/beet-solana": ^0.1.1 "@metaplex-foundation/cusper": ^0.0.2 @@ -1089,6 +1090,7 @@ __metadata: "@solana/web3.js": ^1.35.1 "@types/tape": ^4.13.2 bn.js: ^5.2.0 + esbuild-runner: ^2.2.1 eslint: ^8.3.0 prettier: ^2.5.1 rimraf: ^3.0.2