diff --git a/package.json b/package.json index 9c4cfb0..88d540c 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@solana/spl-token": "^0.1.8", - "@solana/web3.js": "^1.24.1", + "@solana/web3.js": "^1.30.2", "@types/bs58": "^4.0.1", "axios": "^0.21.4", "bn.js": "^5.2.0", diff --git a/src/actions/cancelBid.ts b/src/actions/cancelBid.ts new file mode 100644 index 0000000..7bb8d01 --- /dev/null +++ b/src/actions/cancelBid.ts @@ -0,0 +1,128 @@ +import { Keypair, PublicKey, Transaction } from '@solana/web3.js'; +import { Wallet } from '../wallet'; +import { Connection } from '../Connection'; +import { sendTransaction } from './transactions'; +import { AccountLayout, NATIVE_MINT, Token, TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { AuctionExtended, BidderMetadata, BidderPot, CancelBid } from '../programs/auction'; +import { TransactionsBatch } from '../utils/transactions-batch'; +import { AuctionManager } from '../programs/metaplex'; +import { CreateTokenAccount } from '../programs'; + +interface ICancelBidParams { + connection: Connection; + wallet: Wallet; + auctionManager: PublicKey; + bidderPotToken: PublicKey; + destAccount?: PublicKey; +} + +interface ICancelBidResponse { + txId: string; +} + +export const cancelBid = async ({ + connection, + wallet, + auctionManager, + bidderPotToken, + destAccount, +}: ICancelBidParams): Promise => { + const bidder = wallet.publicKey; + + const manager = await AuctionManager.load(connection, auctionManager); + const auction = await manager.getAuction(connection); + + const auctionStrKey = manager.data.auction; + const auctionTokenMintStrKey = auction.data.tokenMint; + const vaultStrKey = manager.data.vault; + const auctionExtendedKey = await AuctionExtended.getPDA(vaultStrKey); + const bidderPotKey = await BidderPot.getPDA(auctionStrKey, bidder); + const bidderMetaKey = await BidderMetadata.getPDA(auctionStrKey, bidder); + + const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span); + const txBatch = await getCancelTransactions({ + destAccount, + bidder, + accountRentExempt, + bidderPotKey, + bidderPotToken, + bidderMetaKey, + auctionStrKey, + auctionExtendedKey, + auctionTokenMintStrKey, + vaultStrKey, + }); + + const txId = await sendTransaction({ + connection, + wallet, + txs: txBatch.toTransactions(), + signers: txBatch.signers, + }); + + return { txId }; +}; + +interface ICancelBidTransactionsParams { + destAccount: PublicKey; + bidder: PublicKey; + accountRentExempt: number; + bidderPotKey: PublicKey; + bidderPotToken: PublicKey; + bidderMetaKey: PublicKey; + auctionStrKey: string; + auctionExtendedKey: PublicKey; + auctionTokenMintStrKey: string; + vaultStrKey: string; +} + +export const getCancelTransactions = async ({ + destAccount, + bidder, + accountRentExempt, + bidderPotKey, + bidderPotToken, + bidderMetaKey, + auctionStrKey, + auctionExtendedKey, + auctionTokenMintStrKey, + vaultStrKey, +}: ICancelBidTransactionsParams): Promise => { + const txBatch = new TransactionsBatch({ transactions: [] }); + if (!destAccount) { + const account = Keypair.generate(); + const createTokenAccountTransaction = new CreateTokenAccount( + { feePayer: bidder }, + { + newAccountPubkey: account.publicKey, + lamports: accountRentExempt, + mint: NATIVE_MINT, + }, + ); + const closeTokenAccountInstruction = new Transaction().add( + Token.createCloseAccountInstruction(TOKEN_PROGRAM_ID, account.publicKey, bidder, bidder, []), + ); + txBatch.addTransaction(createTokenAccountTransaction); + txBatch.addAfterTransaction(closeTokenAccountInstruction); + txBatch.addSigner(account); + destAccount = account.publicKey; + } + + const cancelBidTransaction = new CancelBid( + { feePayer: bidder }, + { + bidder, + bidderToken: destAccount, + bidderPot: bidderPotKey, + bidderPotToken, + bidderMeta: bidderMetaKey, + auction: new PublicKey(auctionStrKey), + auctionExtended: auctionExtendedKey, + tokenMint: new PublicKey(auctionTokenMintStrKey), + resource: new PublicKey(vaultStrKey), + }, + ); + txBatch.addTransaction(cancelBidTransaction); + + return txBatch; +}; diff --git a/src/actions/index.ts b/src/actions/index.ts index 1eb2a02..93bf35e 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -1,3 +1,4 @@ export * from './transactions'; export * from './initStore'; export * from './mintNFT'; +export * from './cancelBid'; diff --git a/src/config.ts b/src/config.ts index ab16638..d3cb09c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,6 +10,7 @@ export const config = { packs: 'BNRmGgciUJuyznkYHnmitA9an1BcDDiU9JmjEQwvBYVR', // External memo: 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr', + token: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', }, maxCreatorLimit: 5, }; diff --git a/src/programs/auction/accounts/BidderMetadata.ts b/src/programs/auction/accounts/BidderMetadata.ts index 50f6ab1..8924737 100644 --- a/src/programs/auction/accounts/BidderMetadata.ts +++ b/src/programs/auction/accounts/BidderMetadata.ts @@ -1,4 +1,4 @@ -import { AccountInfo } from '@solana/web3.js'; +import { AccountInfo, PublicKey } from '@solana/web3.js'; import BN from 'bn.js'; import { Account } from '../../../Account'; import { AnyPublicKey, StringPublicKey } from '@metaplex/types'; @@ -56,4 +56,14 @@ export class BidderMetadata extends Account { static isCompatible(data: Buffer) { return data.length === BidderMetadata.DATA_SIZE; } + + static getPDA(auction: AnyPublicKey, bidder: AnyPublicKey) { + return AuctionProgram.findProgramAddress([ + Buffer.from(AuctionProgram.PREFIX), + AuctionProgram.PUBKEY.toBuffer(), + new PublicKey(auction).toBuffer(), + new PublicKey(bidder).toBuffer(), + Buffer.from('metadata'), + ]); + } } diff --git a/src/programs/auction/accounts/BidderPot.ts b/src/programs/auction/accounts/BidderPot.ts index 9fae58c..a145a85 100644 --- a/src/programs/auction/accounts/BidderPot.ts +++ b/src/programs/auction/accounts/BidderPot.ts @@ -1,7 +1,7 @@ import { Borsh } from '@metaplex/utils'; import { AnyPublicKey, StringPublicKey } from '@metaplex/types'; import { AuctionProgram } from '../AuctionProgram'; -import { AccountInfo } from '@solana/web3.js'; +import { AccountInfo, PublicKey } from '@solana/web3.js'; import { Account } from '../../../Account'; import { ERROR_INVALID_ACCOUNT_DATA, ERROR_INVALID_OWNER } from '@metaplex/errors'; import { Buffer } from 'buffer'; @@ -47,4 +47,13 @@ export class BidderPot extends Account { static isCompatible(data: Buffer) { return data.length === BidderPot.DATA_SIZE; } + + static getPDA(auction: AnyPublicKey, bidder: AnyPublicKey) { + return AuctionProgram.findProgramAddress([ + Buffer.from(AuctionProgram.PREFIX), + AuctionProgram.PUBKEY.toBuffer(), + new PublicKey(auction).toBuffer(), + new PublicKey(bidder).toBuffer(), + ]); + } } diff --git a/src/utils/transactions-batch.ts b/src/utils/transactions-batch.ts new file mode 100644 index 0000000..12b33a6 --- /dev/null +++ b/src/utils/transactions-batch.ts @@ -0,0 +1,50 @@ +import { Keypair } from '@solana/web3.js'; +import { Transaction } from '../Transaction'; + +interface TransactionsBatchParams { + beforeTransactions?: Transaction[]; + transactions: Transaction[]; + afterTransactions?: Transaction[]; +} + +export class TransactionsBatch { + beforeTransactions: Transaction[]; + transactions: Transaction[]; + afterTransactions: Transaction[]; + + signers: Keypair[] = []; + + constructor({ + beforeTransactions = [], + transactions, + afterTransactions = [], + }: TransactionsBatchParams) { + this.beforeTransactions = beforeTransactions; + this.transactions = transactions; + this.afterTransactions = afterTransactions; + } + + addSigner(signer: Keypair) { + this.signers.push(signer); + } + + addBeforeTransaction(transaction: Transaction) { + this.beforeTransactions.push(transaction); + } + + addTransaction(transaction: Transaction) { + this.transactions.push(transaction); + } + + addAfterTransaction(transaction: Transaction) { + this.afterTransactions.push(transaction); + } + + toTransactions() { + return [...this.beforeTransactions, ...this.transactions, ...this.afterTransactions]; + } + + toInstructions() { + return this.toTransactions().flatMap((t) => t.instructions); + } +} diff --git a/yarn.lock b/yarn.lock index f65fe9e..5e251b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -493,6 +493,27 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@ethersproject/bytes@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.5.0.tgz#cb11c526de657e7b45d2e0f0246fb3b9d29a601c" + integrity sha512-ABvc7BHWhZU9PNM/tANm/Qx4ostPGadAuQzWTr3doklZOhDlmcBqclrQe/ZXUIj3K8wC28oYeuRa+A37tX9kog== + dependencies: + "@ethersproject/logger" "^5.5.0" + +"@ethersproject/logger@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.5.0.tgz#0c2caebeff98e10aefa5aef27d7441c7fd18cf5d" + integrity sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg== + +"@ethersproject/sha2@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.5.0.tgz#a40a054c61f98fd9eee99af2c3cc6ff57ec24db7" + integrity sha512-B5UBoglbCiHamRVPLA110J+2uqsifpZaTmid2/7W5rbtYVz6gus6/hSDieIU/6gaKIDcOj12WnOdiymEUHIAOA== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + hash.js "1.1.7" + "@gar/promisify@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" @@ -1170,7 +1191,7 @@ buffer-layout "^1.2.0" dotenv "10.0.0" -"@solana/web3.js@^1.21.0", "@solana/web3.js@^1.24.1": +"@solana/web3.js@^1.21.0": version "1.29.2" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.29.2.tgz#05c162f477c226ee3211f8ee8c1c6d4203e08f54" integrity sha512-gtoHzimv7upsKF2DIO4/vNfIMKN+cxSImBHvsdiMyp9IPqb8sctsHVU/+80xXl0JKXVKeairDv5RvVnesJYrtw== @@ -1190,6 +1211,26 @@ superstruct "^0.14.2" tweetnacl "^1.0.0" +"@solana/web3.js@^1.30.2": + version "1.30.2" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.30.2.tgz#e85da75e0825dc64f53eb64a1ff0115b27bec135" + integrity sha512-hznCj+rkfvM5taRP3Z+l5lumB7IQnDrB4l55Wpsg4kDU9Zds8pE5YOH5Z9bbF/pUzZJKQjyBjnY/6kScBm3Ugg== + dependencies: + "@babel/runtime" "^7.12.5" + "@ethersproject/sha2" "^5.5.0" + "@solana/buffer-layout" "^3.0.0" + bn.js "^5.0.0" + borsh "^0.4.0" + bs58 "^4.0.1" + buffer "6.0.1" + cross-fetch "^3.1.4" + jayson "^3.4.4" + js-sha3 "^0.8.0" + rpc-websockets "^7.4.2" + secp256k1 "^4.0.2" + superstruct "^0.14.2" + tweetnacl "^1.0.0" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -3338,7 +3379,7 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -hash.js@^1.0.0, hash.js@^1.0.3: +hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==