diff --git a/src/actions/index.ts b/src/actions/index.ts index 5b03d55..0df466a 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -1,5 +1,6 @@ export * from './transactions'; export * from './initStore'; +export * from './initStoreV2'; export * from './mintNFT'; export * from './mintEditionFromMaster'; export * from './closeVault'; diff --git a/src/actions/initStoreV2.ts b/src/actions/initStoreV2.ts new file mode 100644 index 0000000..61cef13 --- /dev/null +++ b/src/actions/initStoreV2.ts @@ -0,0 +1,42 @@ +import { PublicKey } from '@solana/web3.js'; +import { Wallet } from '../wallet'; +import { Connection } from '../Connection'; +import { sendTransaction } from './transactions'; +import { SetStoreV2, Store, StoreConfig } from '../programs/metaplex'; + +interface IInitStoreV2Params { + connection: Connection; + wallet: Wallet; + isPublic?: boolean; + settingsUri?: string | null; +} + +interface IInitStoreV2Response { + storeId: PublicKey; + configId: PublicKey; + txId: string; +} + +export const initStoreV2 = async ({ + connection, + wallet, + settingsUri = null, + isPublic = true, +}: IInitStoreV2Params): Promise => { + const storeId = await Store.getPDA(wallet.publicKey); + const configId = await StoreConfig.getPDA(storeId); + const tx = new SetStoreV2( + { feePayer: wallet.publicKey }, + { + admin: new PublicKey(wallet.publicKey), + store: storeId, + config: configId, + isPublic, + settingsUri, + }, + ); + + const txId = await sendTransaction({ connection, wallet, txs: [tx] }); + + return { storeId, configId, txId }; +}; diff --git a/src/programs/metaplex/MetaplexProgram.ts b/src/programs/metaplex/MetaplexProgram.ts index f483c74..7f0dca0 100644 --- a/src/programs/metaplex/MetaplexProgram.ts +++ b/src/programs/metaplex/MetaplexProgram.ts @@ -16,10 +16,14 @@ export enum MetaplexKey { AuctionManagerV2 = 10, BidRedemptionTicketV2 = 11, AuctionWinnerTokenTypeTrackerV1 = 12, + StoreIndexerV1 = 13, + AuctionCacheV1 = 14, + StoreConfigV1 = 15, } export class MetaplexProgram extends Program { static readonly PREFIX = 'metaplex'; + static readonly CONFIG = 'config'; static readonly TOTALS = 'totals'; static readonly PUBKEY = new PublicKey(config.programs.metaplex); } diff --git a/src/programs/metaplex/accounts/StoreConfig.ts b/src/programs/metaplex/accounts/StoreConfig.ts new file mode 100644 index 0000000..76fff8a --- /dev/null +++ b/src/programs/metaplex/accounts/StoreConfig.ts @@ -0,0 +1,54 @@ +import { ERROR_INVALID_ACCOUNT_DATA, ERROR_INVALID_OWNER } from '@metaplex/errors'; +import { AnyPublicKey } from '@metaplex/types'; +import { Borsh } from '@metaplex/utils'; +import { AccountInfo, PublicKey } from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { Account } from '../../../Account'; +import { MetaplexKey, MetaplexProgram } from '../MetaplexProgram'; + +type Args = { + settingsUri: string; +}; +export class StoreConfigData extends Borsh.Data { + static readonly SCHEMA = this.struct([ + ['key', 'u8'], + ['settingsUri', { kind: 'option', type: 'string' }], + ]); + + key: MetaplexKey = MetaplexKey.StoreConfigV1; + settingsUri: string; + + constructor(args: Args) { + super(args); + this.key = MetaplexKey.StoreConfigV1; + } +} + +export class StoreConfig extends Account { + constructor(pubkey: AnyPublicKey, info: AccountInfo) { + super(pubkey, info); + + if (!this.assertOwner(MetaplexProgram.PUBKEY)) { + throw ERROR_INVALID_OWNER(); + } + + if (!StoreConfig.isCompatible(this.info.data)) { + throw ERROR_INVALID_ACCOUNT_DATA(); + } + + this.data = StoreConfigData.deserialize(this.info.data); + } + + static isCompatible(data: Buffer) { + return data[0] === MetaplexKey.StoreConfigV1; + } + + static async getPDA(store: AnyPublicKey) { + return MetaplexProgram.findProgramAddress([ + Buffer.from(MetaplexProgram.PREFIX), + MetaplexProgram.PUBKEY.toBuffer(), + Buffer.from(MetaplexProgram.CONFIG), + new PublicKey(store).toBuffer(), + ]); + } +} diff --git a/src/programs/metaplex/index.ts b/src/programs/metaplex/index.ts index 9500c3c..13d9ab5 100644 --- a/src/programs/metaplex/index.ts +++ b/src/programs/metaplex/index.ts @@ -4,6 +4,7 @@ export * from './accounts/PayoutTicket'; export * from './accounts/PrizeTrackingTicket'; export * from './accounts/SafetyDepositConfig'; export * from './accounts/Store'; +export * from './accounts/StoreConfig'; export * from './accounts/WhitelistedCreator'; export * from './accounts/AuctionWinnerTokenTypeTracker'; export * from './MetaplexProgram'; diff --git a/src/programs/metaplex/transactions/SetStoreV2.ts b/src/programs/metaplex/transactions/SetStoreV2.ts new file mode 100644 index 0000000..d8bb78b --- /dev/null +++ b/src/programs/metaplex/transactions/SetStoreV2.ts @@ -0,0 +1,99 @@ +import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { + PublicKey, + SystemProgram, + SYSVAR_RENT_PUBKEY, + TransactionCtorFields, + TransactionInstruction, +} from '@solana/web3.js'; +import { Borsh } from '@metaplex/utils'; +import { Transaction } from '../../../Transaction'; +import { VaultProgram } from '../../vault'; +import { MetadataProgram } from '../../metadata'; +import { AuctionProgram } from '../../auction'; +import { MetaplexProgram } from '../MetaplexProgram'; +import { ParamsWithStore } from '@metaplex/types'; + +export class SetStoreV2Args extends Borsh.Data<{ public: boolean; settingsUri: string | null }> { + static readonly SCHEMA = this.struct([ + ['instruction', 'u8'], + ['public', 'u8'], + ['settingsUri', { kind: 'option', type: 'string' }], + ]); + + instruction = 23; + public: boolean; + settingsUri: string | null; +} + +type SetStoreV2Params = { + admin: PublicKey; + config: PublicKey; + isPublic: boolean; + settingsUri: string | null; +}; + +export class SetStoreV2 extends Transaction { + constructor(options: TransactionCtorFields, params: ParamsWithStore) { + super(options); + const { feePayer } = options; + const { admin, store, config, isPublic, settingsUri } = params; + + const data = SetStoreV2Args.serialize({ public: isPublic, settingsUri }); + + this.add( + new TransactionInstruction({ + keys: [ + { + pubkey: store, + isSigner: false, + isWritable: true, + }, + { + pubkey: config, + isSigner: false, + isWritable: true, + }, + { + pubkey: admin, + isSigner: true, + isWritable: false, + }, + { + pubkey: feePayer, + isSigner: true, + isWritable: false, + }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { + pubkey: VaultProgram.PUBKEY, + isSigner: false, + isWritable: false, + }, + { + pubkey: MetadataProgram.PUBKEY, + isSigner: false, + isWritable: false, + }, + { + pubkey: AuctionProgram.PUBKEY, + isSigner: false, + isWritable: false, + }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + { + pubkey: SYSVAR_RENT_PUBKEY, + isSigner: false, + isWritable: false, + }, + ], + programId: MetaplexProgram.PUBKEY, + data, + }), + ); + } +} diff --git a/src/programs/metaplex/transactions/index.ts b/src/programs/metaplex/transactions/index.ts index c153017..4cb2801 100644 --- a/src/programs/metaplex/transactions/index.ts +++ b/src/programs/metaplex/transactions/index.ts @@ -1,4 +1,5 @@ export * from './SetStore'; +export * from './SetStoreV2'; export * from './SetWhitelistedCreator'; export * from './StartAuction'; export * from './InitAuctionManagerV2'; diff --git a/test/actions/initStore.test.ts b/test/actions/initStore.test.ts new file mode 100644 index 0000000..1e434b3 --- /dev/null +++ b/test/actions/initStore.test.ts @@ -0,0 +1,60 @@ +import { Keypair } from '@solana/web3.js'; +import { Connection, NodeWallet } from '../../src'; +import { initStore, initStoreV2 } from '../../src/actions'; +import { FEE_PAYER, NETWORK } from '../utils'; +import { Store } from '../../src/programs/metaplex'; +import { uri } from './shared'; + +describe('init Store', () => { + const connection = new Connection(NETWORK); + const wallet = new NodeWallet(FEE_PAYER); + let mint: Keypair; + + beforeEach(() => { + mint = Keypair.generate(); + jest.spyOn(Keypair, 'generate').mockReturnValue(mint); + }); + + test('creates store with initStore', async () => { + const storeResponse = await initStore({ + connection, + wallet, + isPublic: false, + }); + + const storeId = await Store.getPDA(wallet.publicKey); + + expect(storeResponse).toMatchObject({ + storeId, + }); + }); + + test('creates store with initStoreV2', async () => { + const storeResponse = await initStoreV2({ + connection, + wallet, + isPublic: false, + settingsUri: uri, + }); + + const storeId = await Store.getPDA(wallet.publicKey); + + expect(storeResponse).toMatchObject({ + storeId, + }); + }); + + test('creates store with initStoreV2 without storeV2', async () => { + const storeResponse = await initStoreV2({ + connection, + wallet, + isPublic: false, + }); + + const storeId = await Store.getPDA(wallet.publicKey); + + expect(storeResponse).toMatchObject({ + storeId, + }); + }); +});