import { AnchorProvider, BN, Program, Wallet } from '@coral-xyz/anchor'; import { ComputeBudgetProgram, Connection, Keypair, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY, Transaction, TransactionInstruction } from '@solana/web3.js'; import { getAssociatedTokenAddress, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotentInstruction } from '@solana/spl-token'; import { PROGRAM_ID as METADATA_PROGRAM_ID } from '@metaplex-foundation/mpl-token-metadata'; import * as base58 from 'bs58'; import * as nacl from 'tweetnacl'; import { SogaNodeSale, IDL } from './types/soga_node_sale'; import { InitializeConfig, InitializeSalePhaseConfig, UpdateSalePhaseConfig, InitializeSalePhasePaymentTokenConfig, UpdateSalePhasePaymentTokenConfig, InitializeSalePhaseTierConfig, UpdateSalePhaseTierConfig, AirdropConfig, BuyConfig, BuyWithTokenConfig, FillOrderConfig, CreateOrderReceiptConfig, } from './configs'; import { AirdropEventName, AirdropEvent, BuyEventName, BuyEvent, BuyWithTokenEventName, BuyWithTokenEvent, FillOrderEventName, FillOrderEvent, InitializeSalePhaseEventName, InitializeSalePhaseEvent, InitializeSalePhasePaymentTokenEventName, InitializeSalePhasePaymentTokenEvent, InitializeSalePhaseTierEventName, InitializeSalePhaseTierEvent, UpdateSalePhaseEventName, UpdateSalePhaseEvent, UpdateSalePhasePaymentTokenEventName, UpdateSalePhasePaymentTokenEvent, UpdateSalePhaseTierEventName, UpdateSalePhaseTierEvent, CreateOrderReceiptEvent, CreateOrderReceiptEventName, } from './events-types'; const SOGA_NODE_SALE_CONFIG_ACCOUNT_PREFIX: string = 'CONFIG'; const SOGA_NODE_SALE_PHASE_DETAIL_ACCOUNT_PREFIX: string = 'PHASE'; const SOGA_NODE_SALE_PHASE_TIER_DETAIL_ACCOUNT_PREFIX: string = 'PHASE_TIER'; const USER_DETAIL_ACCOUNT_PREFIX: string = 'USER'; const USER_TIER_DETAIL_ACCOUNT_PREFIX: string = 'USER_TIER'; const SOGA_NODE_SALE_PHASE_PAYMENT_TOKEN_ACCOUNT_PREFIX: string = 'PHASE_PAYMENT_TOKEN'; const ORDER_DETAIL_ACCOUNT_PREFIX: string = 'ORDER'; const COLLECTION_ACCOUNT_PREFIX: string = 'COLLECTION'; const NODE_ACCOUNT_PREFIX: string = 'NODE'; export class SogaNodeSaleLib { program: Program; connection: Connection; constructor(programId: PublicKey, connection: Connection, wallet: Wallet) { this.connection = connection; const provider = new AnchorProvider(connection, wallet, AnchorProvider.defaultOptions()); this.program = new Program(IDL, programId, provider); } signTransaction(tx: Transaction, secretKey: string): Transaction { const keypair: Keypair = Keypair.fromSecretKey(base58.decode(secretKey)); const signature = nacl.sign.detached(tx.serializeMessage(), keypair.secretKey); tx.addSignature(keypair.publicKey, Buffer.from(signature)); return tx; } addSignatureInTransaction(tx: Transaction, signerAddress: PublicKey, signature: Buffer): Transaction { tx.addSignature(signerAddress, signature); return tx; } async addFeePayerAndRecentBlockHashInTransaction(tx: Transaction, feePayer: PublicKey, units: number = 100000): Promise { tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: units })); tx.feePayer = feePayer; tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash; return tx; } async isPdaAddressInitialize(pdaAddress: PublicKey): Promise { const pdaAccountInfo = await this.connection.getAccountInfo(pdaAddress); return pdaAccountInfo != null; } async getMetadataAccountPda(mintAccountPda: PublicKey, mplProgramId: PublicKey = METADATA_PROGRAM_ID): Promise<[PublicKey, number]> { return PublicKey.findProgramAddressSync([Buffer.from('metadata'), mplProgramId.toBuffer(), mintAccountPda.toBuffer()], mplProgramId); } async getMasterEditionAccountPda(mintAccountPda: PublicKey, mplProgramId: PublicKey = METADATA_PROGRAM_ID): Promise<[PublicKey, number]> { return PublicKey.findProgramAddressSync( [Buffer.from('metadata'), mplProgramId.toBuffer(), mintAccountPda.toBuffer(), Buffer.from('edition')], mplProgramId ); } async getSogaNodeSaleConfigAccountPdaAndBump(): Promise<[PublicKey, number]> { return PublicKey.findProgramAddressSync([Buffer.from(SOGA_NODE_SALE_CONFIG_ACCOUNT_PREFIX)], this.program.programId); } async getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName: string): Promise<[PublicKey, number]> { return PublicKey.findProgramAddressSync([Buffer.from(SOGA_NODE_SALE_PHASE_DETAIL_ACCOUNT_PREFIX), Buffer.from(salePhaseName)], this.program.programId); } async getSogaNodeSalePhaseTierDetailAccountPdaAndBump(sogaNodeSalePhaseDetailPda: PublicKey, tierId: string): Promise<[PublicKey, number]> { return PublicKey.findProgramAddressSync( [Buffer.from(SOGA_NODE_SALE_PHASE_TIER_DETAIL_ACCOUNT_PREFIX), sogaNodeSalePhaseDetailPda.toBuffer(), Buffer.from(tierId)], this.program.programId ); } async getSogaNodeSalePhasePaymentTokenDetailAccountPdaAndBump( sogaNodeSalePhaseDetailPda: PublicKey, tokenMintAccount: PublicKey ): Promise<[PublicKey, number]> { return PublicKey.findProgramAddressSync( [Buffer.from(SOGA_NODE_SALE_PHASE_PAYMENT_TOKEN_ACCOUNT_PREFIX), sogaNodeSalePhaseDetailPda.toBuffer(), tokenMintAccount.toBuffer()], this.program.programId ); } async getSogaNodeSalePhaseTierCollectionMintAccountPdaAndBump(sogaNodeSalePhaseTierDetailPda: PublicKey): Promise<[PublicKey, number]> { return PublicKey.findProgramAddressSync([Buffer.from(COLLECTION_ACCOUNT_PREFIX), sogaNodeSalePhaseTierDetailPda.toBuffer()], this.program.programId); } async getUserDetailAccountPdaAndBump(sogaNodeSalePhaseDetailPda: PublicKey, user: PublicKey): Promise<[PublicKey, number]> { return PublicKey.findProgramAddressSync( [Buffer.from(USER_DETAIL_ACCOUNT_PREFIX), sogaNodeSalePhaseDetailPda.toBuffer(), user.toBuffer()], this.program.programId ); } async getUserTierDetailAccountPdaAndBump(userDetailPda: PublicKey, sogaNodeSalePhaseTierDetailPda: PublicKey): Promise<[PublicKey, number]> { return PublicKey.findProgramAddressSync( [Buffer.from(USER_TIER_DETAIL_ACCOUNT_PREFIX), userDetailPda.toBuffer(), sogaNodeSalePhaseTierDetailPda.toBuffer()], this.program.programId ); } async getNodeMintAccountPdaAndBump(sogaNodeSalePhaseTierCollectionMintAccountPda: PublicKey, tokenId: string): Promise<[PublicKey, number]> { return PublicKey.findProgramAddressSync( [Buffer.from(NODE_ACCOUNT_PREFIX), sogaNodeSalePhaseTierCollectionMintAccountPda.toBuffer(), Buffer.from(tokenId)], this.program.programId ); } async getOrderDetailAccountPdaAndBump(sogaNodeSalePhaseDetailPda: PublicKey, userDetailPda: PublicKey, orderId: string): Promise<[PublicKey, number]> { return PublicKey.findProgramAddressSync( [Buffer.from(ORDER_DETAIL_ACCOUNT_PREFIX), sogaNodeSalePhaseDetailPda.toBuffer(), userDetailPda.toBuffer(), Buffer.from(orderId)], this.program.programId ); } async createInitializeTransaction( payer: PublicKey, mainSigningAuthority: PublicKey, config: InitializeConfig = { systemProgram: SystemProgram.programId, rent: SYSVAR_RENT_PUBKEY, } ): Promise { const [saleConfigPda] = await this.getSogaNodeSaleConfigAccountPdaAndBump(); return this.program.methods .initialize() .accounts({ payer: payer, mainSigningAuthority: mainSigningAuthority, saleConfig: saleConfigPda, systemProgram: config.systemProgram, rent: config.rent, }) .transaction(); } async createInitializeSalePhaseTransaction( payer: PublicKey, mainSigningAuthority: PublicKey, signingAuthority: PublicKey, backAuthority: PublicKey, salePhaseName: string, totalTier: number, nodeName: string, nodeSymbol: string, nodeUrl: string, priceFeed: PublicKey, priceFeedId: string, paymentReceiver: PublicKey, config: InitializeSalePhaseConfig = { systemProgram: SystemProgram.programId, rent: SYSVAR_RENT_PUBKEY, } ): Promise { const [saleConfigPda, saleConfigBump] = await this.getSogaNodeSaleConfigAccountPdaAndBump(); const [salePhaseDetailPda] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); return this.program.methods .initializeSalePhase(saleConfigBump, salePhaseName, totalTier, nodeName, nodeSymbol, nodeUrl, priceFeedId) .accounts({ payer: payer, mainSigningAuthority: mainSigningAuthority, signingAuthority: signingAuthority, backAuthority: backAuthority, saleConfig: saleConfigPda, salePhaseDetail: salePhaseDetailPda, priceFeed: priceFeed, paymentReceiver: paymentReceiver, systemProgram: config.systemProgram, rent: config.rent, }) .transaction(); } async createUpdateKeyTransaction( payer: PublicKey, mainSigningAuthority: PublicKey, signingAuthority: PublicKey, backAuthority: PublicKey, salePhaseName: string ): Promise { const [saleConfigPda, saleConfigBump] = await this.getSogaNodeSaleConfigAccountPdaAndBump(); const [salePhaseDetailPda, salePhaseDetailBump] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); return this.program.methods .updateKey(saleConfigBump, salePhaseName, salePhaseDetailBump) .accounts({ payer: payer, mainSigningAuthority: mainSigningAuthority, signingAuthority: signingAuthority, backAuthority: backAuthority, saleConfig: saleConfigPda, salePhaseDetail: salePhaseDetailPda, }) .transaction(); } async createUpdateSalePhaseTransaction( payer: PublicKey, signingAuthority: PublicKey, salePhaseName: string, nodeName: string, nodeSymbol: string, nodeUrl: string, priceFeed: PublicKey, priceFeedId: string, paymentReceiver: PublicKey, buyEnable: boolean, buyWithTokenEnable: boolean, airdropEnable: boolean, config: UpdateSalePhaseConfig = { systemProgram: SystemProgram.programId, rent: SYSVAR_RENT_PUBKEY, } ): Promise { const [salePhaseDetailPda, salePhaseDetailBump] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); return this.program.methods .updateSalePhase(salePhaseDetailBump, salePhaseName, nodeName, nodeSymbol, nodeUrl, buyEnable, buyWithTokenEnable, airdropEnable, priceFeedId) .accounts({ payer: payer, signingAuthority: signingAuthority, salePhaseDetail: salePhaseDetailPda, priceFeed: priceFeed, paymentReceiver: paymentReceiver, systemProgram: config.systemProgram, rent: config.rent, }) .transaction(); } async createInitializeSalePhaseTokenPaymentTransaction( payer: PublicKey, signingAuthority: PublicKey, salePhaseName: string, priceFeed: PublicKey, priceFeedId: string, paymentTokenMintAccount: PublicKey, paymentTokenProgram: PublicKey, config: InitializeSalePhasePaymentTokenConfig = { systemProgram: SystemProgram.programId, rent: SYSVAR_RENT_PUBKEY, } ): Promise { const [salePhaseDetailPda, salePhaseDetailBump] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); const [salePhasePaymentTokenDetailPda] = await this.getSogaNodeSalePhasePaymentTokenDetailAccountPdaAndBump(salePhaseDetailPda, paymentTokenMintAccount); return this.program.methods .initializeSalePhaseTokenPayment(salePhaseDetailBump, salePhaseName, priceFeedId) .accounts({ payer: payer, signingAuthority: signingAuthority, salePhaseDetail: salePhaseDetailPda, priceFeed: priceFeed, salePhasePaymentTokenDetail: salePhasePaymentTokenDetailPda, paymentTokenMintAccount: paymentTokenMintAccount, paymentTokenProgram: paymentTokenProgram, systemProgram: config.systemProgram, rent: config.rent, }) .transaction(); } async createUpdateSalePhaseTokenPaymentTransaction( payer: PublicKey, signingAuthority: PublicKey, salePhaseName: string, priceFeed: PublicKey, priceFeedId: string, paymentTokenMintAccount: PublicKey, paymentTokenProgram: PublicKey, enable: boolean, config: UpdateSalePhasePaymentTokenConfig = { systemProgram: SystemProgram.programId, rent: SYSVAR_RENT_PUBKEY, } ): Promise { const [salePhaseDetailPda, salePhaseDetailBump] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); const [salePhasePaymentTokenDetailPda, salePhasePaymentTokenDetailBump] = await this.getSogaNodeSalePhasePaymentTokenDetailAccountPdaAndBump( salePhaseDetailPda, paymentTokenMintAccount ); return this.program.methods .updateSalePhaseTokenPayment(salePhaseDetailBump, salePhasePaymentTokenDetailBump, salePhaseName, enable, priceFeedId) .accounts({ payer: payer, signingAuthority: signingAuthority, salePhaseDetail: salePhaseDetailPda, priceFeed: priceFeed, salePhasePaymentTokenDetail: salePhasePaymentTokenDetailPda, paymentTokenMintAccount: paymentTokenMintAccount, paymentTokenProgram: paymentTokenProgram, systemProgram: config.systemProgram, rent: config.rent, }) .transaction(); } async createInitializeSalePhaseTierTransaction( payer: PublicKey, signingAuthority: PublicKey, salePhaseName: string, price: BN, quantity: BN, mintLimit: BN, collectionName: string, collectionSymbol: string, collectionUrl: string, whitelistQuantity: BN, tierId: number, config: InitializeSalePhaseTierConfig = { systemProgram: SystemProgram.programId, rent: SYSVAR_RENT_PUBKEY, tokenProgram: TOKEN_PROGRAM_ID, associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, tokenMetadataProgram: METADATA_PROGRAM_ID, } ): Promise { const [salePhaseDetailPda, salePhaseDetailBump] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); const salePhaseDetailData = await this.program.account.sogaNodeSalePhaseDetailAccount.fetch(salePhaseDetailPda.toBase58()); // const tierId = salePhaseDetailData.totalInitializeTiers + 1; if (tierId > salePhaseDetailData.totalTiers) { throw new Error('Cannot create tier'); } const [salePhaseTierDetailPda] = await this.getSogaNodeSalePhaseTierDetailAccountPdaAndBump(salePhaseDetailPda, tierId.toString()); const [collectionMintAccountPda] = await this.getSogaNodeSalePhaseTierCollectionMintAccountPdaAndBump(salePhaseTierDetailPda); const [collectionMasterEditionPda] = await this.getMasterEditionAccountPda(collectionMintAccountPda); const [collectionMetadataPda] = await this.getMetadataAccountPda(collectionMintAccountPda); const collectionTokenAccount = await getAssociatedTokenAddress( collectionMintAccountPda, salePhaseTierDetailPda, true, config.tokenProgram, config.associatedTokenProgram ); return this.program.methods .initializeSalePhaseTier( salePhaseDetailBump, salePhaseName, tierId.toString(), price, quantity, mintLimit, collectionName, collectionSymbol, collectionUrl, whitelistQuantity ) .accounts({ payer: payer, signingAuthority: signingAuthority, salePhaseDetail: salePhaseDetailPda, salePhaseTierDetail: salePhaseTierDetailPda, collectionMintAccount: collectionMintAccountPda, collectionMasterEdition: collectionMasterEditionPda, collectionMetadata: collectionMetadataPda, collectionTokenAccount: collectionTokenAccount, tokenMetadataProgram: config.tokenMetadataProgram, tokenProgram: config.tokenProgram, associatedTokenProgram: config.associatedTokenProgram, systemProgram: config.systemProgram, rent: config.rent, }) .transaction(); } async createUpdateSalePhaseTierTransaction( payer: PublicKey, signingAuthority: PublicKey, salePhaseName: string, tierId: string, price: BN, mintLimit: BN, buyEnable: boolean, buyWithTokenEnable: boolean, airdropEnable: boolean, whitelistQuantity: BN, config: UpdateSalePhaseTierConfig = { systemProgram: SystemProgram.programId, rent: SYSVAR_RENT_PUBKEY, } ): Promise { const [salePhaseDetailPda, salePhaseDetailBump] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); const [salePhaseTierDetailPda, salePhaseTierDetailBump] = await this.getSogaNodeSalePhaseTierDetailAccountPdaAndBump(salePhaseDetailPda, tierId.toString()); return this.program.methods .updateSalePhaseTier( salePhaseDetailBump, salePhaseTierDetailBump, salePhaseName, tierId, price, mintLimit, buyEnable, buyWithTokenEnable, airdropEnable, whitelistQuantity ) .accounts({ payer: payer, signingAuthority: signingAuthority, salePhaseDetail: salePhaseDetailPda, salePhaseTierDetail: salePhaseTierDetailPda, systemProgram: config.systemProgram, rent: config.rent, }) .transaction(); } async createAirdropTransaction( payer: PublicKey, backAuthority: PublicKey, user: PublicKey, salePhaseName: string, config: AirdropConfig = { systemProgram: SystemProgram.programId, rent: SYSVAR_RENT_PUBKEY, tokenProgram: TOKEN_PROGRAM_ID, associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, tokenMetadataProgram: METADATA_PROGRAM_ID, } ): Promise { const [salePhaseDetailPda, salePhaseDetailBump] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); const [tierId, salePhaseTierDetailPda, salePhaseTierDetailBump] = await this.getCurrentTierId(salePhaseName); const salePhaseTierDetailData = await this.program.account.sogaNodeSalePhaseTierDetailAccount.fetch(salePhaseTierDetailPda.toBase58()); const tokenId: BN = new BN(salePhaseTierDetailData.totalMint.toNumber() + 1); const [collectionMintAccountPda, collectionMintAccountBump] = await this.getSogaNodeSalePhaseTierCollectionMintAccountPdaAndBump(salePhaseTierDetailPda); const [collectionMasterEditionPda] = await this.getMasterEditionAccountPda(collectionMintAccountPda); const [collectionMetadataPda] = await this.getMetadataAccountPda(collectionMintAccountPda); const [userDetailPda] = await this.getUserDetailAccountPdaAndBump(salePhaseDetailPda, user); const [userTierDetailPda] = await this.getUserTierDetailAccountPdaAndBump(userDetailPda, salePhaseTierDetailPda); const [nodeMintAccountPda] = await this.getNodeMintAccountPdaAndBump(collectionMintAccountPda, tokenId.toString()); const [nodeMasterEditionPda] = await this.getMasterEditionAccountPda(nodeMintAccountPda); const [nodeMetadataPda] = await this.getMetadataAccountPda(nodeMintAccountPda); const nodeUserTokenAccount = await getAssociatedTokenAddress(nodeMintAccountPda, user, true, config.tokenProgram, config.associatedTokenProgram); return this.program.methods .airdrop(salePhaseDetailBump, salePhaseTierDetailBump, collectionMintAccountBump, salePhaseName, tierId.toString(), tokenId.toString()) .accounts({ payer: payer, backAuthority: backAuthority, user: user, salePhaseDetail: salePhaseDetailPda, salePhaseTierDetail: salePhaseTierDetailPda, userDetail: userDetailPda, userTierDetail: userTierDetailPda, collectionMintAccount: collectionMintAccountPda, nodeMintAccount: nodeMintAccountPda, userTokenAccount: nodeUserTokenAccount, tokenMetadataProgram: config.tokenMetadataProgram, tokenProgram: config.tokenProgram, associatedTokenProgram: config.associatedTokenProgram, systemProgram: config.systemProgram, rent: config.rent, }) .remainingAccounts([ { pubkey: collectionMetadataPda, isWritable: true, isSigner: false, }, { pubkey: collectionMasterEditionPda, isWritable: true, isSigner: false, }, { pubkey: nodeMetadataPda, isWritable: true, isSigner: false, }, { pubkey: nodeMasterEditionPda, isWritable: true, isSigner: false, }, ]) .transaction(); } async getCurrentTierId(salePhaseName: string, addTier: number = 1): Promise<[number, PublicKey, number]> { let tierId: number; let salePhaseTierDetailPda: PublicKey; let salePhaseTierDetailBump: number; const [salePhaseDetailPda] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); const salePhaseDetailData = await this.program.account.sogaNodeSalePhaseDetailAccount.fetch(salePhaseDetailPda.toBase58()); if (salePhaseDetailData.totalCompletedTiers === 0) { tierId = 1; [salePhaseTierDetailPda, salePhaseTierDetailBump] = await this.getSogaNodeSalePhaseTierDetailAccountPdaAndBump(salePhaseDetailPda, tierId.toString()); } else { if (salePhaseDetailData.totalTiers === salePhaseDetailData.totalCompletedTiers) { throw new Error('No tier found'); } tierId = salePhaseDetailData.totalCompletedTiers + addTier; [salePhaseTierDetailPda, salePhaseTierDetailBump] = await this.getSogaNodeSalePhaseTierDetailAccountPdaAndBump(salePhaseDetailPda, tierId.toString()); } return [tierId, salePhaseTierDetailPda, salePhaseTierDetailBump]; } async createBuyTransaction( payer: PublicKey, backAuthority: PublicKey, userPayer: PublicKey, user: PublicKey, salePhaseName: string, quantity: BN, allowFullDiscount: boolean, fullDiscount: number, fullDiscountReceiver: PublicKey, allowHalfDiscount: boolean, halfDiscount: number, halfDiscountReceiver: PublicKey, allowUserDiscount: boolean, userDiscount: number, isWhitelist: boolean = false, currentTierId: number = 0, perNodeRent: BN, nodeRentReceiver: PublicKey, config: BuyConfig = { systemProgram: SystemProgram.programId, rent: SYSVAR_RENT_PUBKEY, } ): Promise { const [salePhaseDetailPda, salePhaseDetailBump] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); const salePhaseDetailData = await this.program.account.sogaNodeSalePhaseDetailAccount.fetch(salePhaseDetailPda.toBase58()); let tierId: number; let salePhaseTierDetailPda: PublicKey; let salePhaseTierDetailBump: number; if (currentTierId <= 0) { throw new Error('Invalid whitelist tier id'); } tierId = currentTierId; [salePhaseTierDetailPda, salePhaseTierDetailBump] = await this.getSogaNodeSalePhaseTierDetailAccountPdaAndBump(salePhaseDetailPda, tierId.toString()); // if (isWhitelist) { // if (whitelistTierId <= 0) { // throw new Error('Invalid whitelist tier id'); // } // // tierId = whitelistTierId; // // [salePhaseTierDetailPda, salePhaseTierDetailBump] = await this.getSogaNodeSalePhaseTierDetailAccountPdaAndBump(salePhaseDetailPda, tierId.toString()); // } else { // [tierId, salePhaseTierDetailPda, salePhaseTierDetailBump] = await this.getCurrentTierId(salePhaseName); // } const salePhaseTierDetailData = await this.program.account.sogaNodeSalePhaseTierDetailAccount.fetch(salePhaseTierDetailPda.toBase58()); if (salePhaseTierDetailData.totalMint.toNumber() + quantity.toNumber() > salePhaseTierDetailData.quantity.toNumber()) { throw new Error('Invalid quantity'); } let orderId = new BN(1); const [userDetailPda] = await this.getUserDetailAccountPdaAndBump(salePhaseDetailPda, user); if (await this.isPdaAddressInitialize(userDetailPda)) { const userDetailData = await this.program.account.userDetailAccount.fetch(userDetailPda.toBase58()); orderId = new BN(userDetailData.totalOrders.toNumber() + 1); } const [userTierDetailPda] = await this.getUserTierDetailAccountPdaAndBump(userDetailPda, salePhaseTierDetailPda); const [orderDetailPda] = await this.getOrderDetailAccountPdaAndBump(salePhaseDetailPda, userDetailPda, orderId.toString()); // const nodeRentTx = SystemProgram.transfer({ // fromPubkey: userPayer, // toPubkey: nodeRentReceiver, // lamports: perNodeRent.mul(quantity), // programId: config.systemProgram, // }); return this.program.methods .buy( salePhaseDetailBump, salePhaseTierDetailBump, salePhaseName, tierId.toString(), orderId.toString(), quantity, allowFullDiscount, fullDiscount, allowHalfDiscount, halfDiscount, isWhitelist, allowUserDiscount, userDiscount ) .accounts({ payer: payer, backAuthority: backAuthority, userPayer: userPayer, user: user, salePhaseDetail: salePhaseDetailPda, salePhaseTierDetail: salePhaseTierDetailPda, userDetail: userDetailPda, userTierDetail: userTierDetailPda, orderDetail: orderDetailPda, priceUpdate: salePhaseDetailData.priceFeedAddress, systemProgram: config.systemProgram, rent: config.rent, }) .remainingAccounts([ { pubkey: salePhaseDetailData.paymentReceiver, isWritable: true, isSigner: false, }, { pubkey: fullDiscountReceiver, isWritable: true, isSigner: false, }, { pubkey: halfDiscountReceiver, isWritable: true, isSigner: false, }, ]) .transaction(); } async createBuyWithTokenTransaction( payer: PublicKey, backAuthority: PublicKey, userPayer: PublicKey, user: PublicKey, salePhaseName: string, quantity: BN, allowFullDiscount: boolean, fullDiscount: number, fullDiscountReceiver: PublicKey, allowHalfDiscount: boolean, halfDiscount: number, halfDiscountReceiver: PublicKey, allowUserDiscount: boolean, userDiscount: number, paymentTokenMintAccount: PublicKey, paymentTokenProgram: PublicKey, isWhitelist: boolean = false, currentTierId: number = 0, perNodeRent: BN, nodeRentReceiver: PublicKey, config: BuyWithTokenConfig = { systemProgram: SystemProgram.programId, rent: SYSVAR_RENT_PUBKEY, associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, } ): Promise { const [salePhaseDetailPda, salePhaseDetailBump] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); const salePhaseDetailData = await this.program.account.sogaNodeSalePhaseDetailAccount.fetch(salePhaseDetailPda.toBase58()); let tierId: number; let salePhaseTierDetailPda: PublicKey; let salePhaseTierDetailBump: number; if (currentTierId <= 0) { throw new Error('Invalid whitelist tier id'); } tierId = currentTierId; [salePhaseTierDetailPda, salePhaseTierDetailBump] = await this.getSogaNodeSalePhaseTierDetailAccountPdaAndBump(salePhaseDetailPda, tierId.toString()); // if (isWhitelist) { // if (whitelistTierId <= 0) { // throw new Error('Invalid whitelist tier id'); // } // // tierId = whitelistTierId; // // [salePhaseTierDetailPda, salePhaseTierDetailBump] = await this.getSogaNodeSalePhaseTierDetailAccountPdaAndBump(salePhaseDetailPda, tierId.toString()); // } else { // [tierId, salePhaseTierDetailPda, salePhaseTierDetailBump] = await this.getCurrentTierId(salePhaseName); // } const salePhaseTierDetailData = await this.program.account.sogaNodeSalePhaseTierDetailAccount.fetch(salePhaseTierDetailPda.toBase58()); if (salePhaseTierDetailData.totalMint.toNumber() + quantity.toNumber() > salePhaseTierDetailData.quantity.toNumber()) { throw new Error('Invalid quantity'); } let orderId = new BN(1); const [userDetailPda] = await this.getUserDetailAccountPdaAndBump(salePhaseDetailPda, user); if (await this.isPdaAddressInitialize(userDetailPda)) { const userDetailData = await this.program.account.userDetailAccount.fetch(userDetailPda.toBase58()); orderId = new BN(userDetailData.totalOrders.toNumber() + 1); } const [userTierDetailPda] = await this.getUserTierDetailAccountPdaAndBump(userDetailPda, salePhaseTierDetailPda); const [orderDetailPda] = await this.getOrderDetailAccountPdaAndBump(salePhaseDetailPda, userDetailPda, orderId.toString()); const [salePhasePaymentTokenDetailPda] = await this.getSogaNodeSalePhasePaymentTokenDetailAccountPdaAndBump(salePhaseDetailPda, paymentTokenMintAccount); if (!(await this.isPdaAddressInitialize(salePhasePaymentTokenDetailPda))) { throw new Error('Invalid payment token mint account'); } const salePhasePaymentTokenDetailData = await this.program.account.sogaNodeSalePhasePaymentTokenDetailAccount.fetch( salePhasePaymentTokenDetailPda.toBase58() ); const userPaymentTokenAccount = await getAssociatedTokenAddress( paymentTokenMintAccount, userPayer, true, paymentTokenProgram, config.associatedTokenProgram ); const paymentReceiverPaymentTokenAccount = await getAssociatedTokenAddress( paymentTokenMintAccount, salePhaseDetailData.paymentReceiver, true, paymentTokenProgram, config.associatedTokenProgram ); const fullDiscountReceiverPaymentTokenAccount = await getAssociatedTokenAddress( paymentTokenMintAccount, fullDiscountReceiver, true, paymentTokenProgram, config.associatedTokenProgram ); const halfDiscountReceiverPaymentTokenAccount = await getAssociatedTokenAddress( paymentTokenMintAccount, halfDiscountReceiver, true, paymentTokenProgram, config.associatedTokenProgram ); const createTokenAccounts: TransactionInstruction[] = []; // if (!(await this.isPdaAddressInitialize(paymentReceiverPaymentTokenAccount))) { // createTokenAccounts.push( // createAssociatedTokenAccountIdempotentInstruction( // payer, // paymentReceiverPaymentTokenAccount, // salePhaseDetailData.paymentReceiver, // paymentTokenMintAccount, // paymentTokenProgram, // config.associatedTokenProgram // ) // ); // } if (allowFullDiscount && !(await this.isPdaAddressInitialize(fullDiscountReceiverPaymentTokenAccount))) { createTokenAccounts.push( createAssociatedTokenAccountIdempotentInstruction( payer, fullDiscountReceiverPaymentTokenAccount, fullDiscountReceiver, paymentTokenMintAccount, paymentTokenProgram, config.associatedTokenProgram ) ); } if (allowHalfDiscount && !(await this.isPdaAddressInitialize(halfDiscountReceiverPaymentTokenAccount))) { createTokenAccounts.push( createAssociatedTokenAccountIdempotentInstruction( payer, halfDiscountReceiverPaymentTokenAccount, halfDiscountReceiver, paymentTokenMintAccount, paymentTokenProgram, config.associatedTokenProgram ) ); } // const nodeRentTx = SystemProgram.transfer({ // fromPubkey: userPayer, // toPubkey: nodeRentReceiver, // lamports: perNodeRent.mul(quantity), // programId: config.systemProgram, // }); // // createTokenAccounts.push(nodeRentTx); return this.program.methods .buyWithToken( salePhaseDetailBump, salePhaseTierDetailBump, salePhaseName, tierId.toString(), orderId.toString(), quantity, allowFullDiscount, fullDiscount, allowHalfDiscount, halfDiscount, isWhitelist, allowUserDiscount, userDiscount ) .accounts({ payer: payer, backAuthority: backAuthority, userPayer: userPayer, user: user, salePhaseDetail: salePhaseDetailPda, salePhaseTierDetail: salePhaseTierDetailPda, userDetail: userDetailPda, userTierDetail: userTierDetailPda, orderDetail: orderDetailPda, priceUpdate: salePhasePaymentTokenDetailData.priceFeedAddress, systemProgram: config.systemProgram, rent: config.rent, }) .remainingAccounts([ { pubkey: salePhaseDetailData.paymentReceiver, // 1 isWritable: true, isSigner: false, }, { pubkey: fullDiscountReceiver, // 2 isWritable: true, isSigner: false, }, { pubkey: halfDiscountReceiver, // 3 isWritable: true, isSigner: false, }, { pubkey: salePhasePaymentTokenDetailPda, // 4 isWritable: true, isSigner: false, }, { pubkey: paymentTokenMintAccount, // 5 isWritable: true, isSigner: false, }, { pubkey: paymentTokenProgram, // 6 isWritable: true, isSigner: false, }, { pubkey: userPaymentTokenAccount, // 7 isWritable: true, isSigner: false, }, { pubkey: paymentReceiverPaymentTokenAccount, // 8 isWritable: true, isSigner: false, }, { pubkey: fullDiscountReceiverPaymentTokenAccount, // 9 isWritable: true, isSigner: false, }, { pubkey: halfDiscountReceiverPaymentTokenAccount, // 10 isWritable: true, isSigner: false, }, ]) .preInstructions(createTokenAccounts) .transaction(); } async createOrderReceiptTransaction( payer: PublicKey, backAuthority: PublicKey, user: PublicKey, salePhaseName: string, quantity: BN, followTiers: boolean = true, followTierId: number = 0, config: CreateOrderReceiptConfig = { systemProgram: SystemProgram.programId, rent: SYSVAR_RENT_PUBKEY, } ): Promise { const [salePhaseDetailPda, salePhaseDetailBump] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); let tierId: number; let salePhaseTierDetailPda: PublicKey; let salePhaseTierDetailBump: number; if (!followTiers) { if (followTierId <= 0) { throw new Error('Invalid whitelist tier id'); } tierId = followTierId; [salePhaseTierDetailPda, salePhaseTierDetailBump] = await this.getSogaNodeSalePhaseTierDetailAccountPdaAndBump(salePhaseDetailPda, tierId.toString()); } else { [tierId, salePhaseTierDetailPda, salePhaseTierDetailBump] = await this.getCurrentTierId(salePhaseName); } const salePhaseTierDetailData = await this.program.account.sogaNodeSalePhaseTierDetailAccount.fetch(salePhaseTierDetailPda.toBase58()); if (salePhaseTierDetailData.totalMint.toNumber() + quantity.toNumber() > salePhaseTierDetailData.quantity.toNumber()) { throw new Error('Invalid quantity'); } let orderId = new BN(1); const [userDetailPda] = await this.getUserDetailAccountPdaAndBump(salePhaseDetailPda, user); if (await this.isPdaAddressInitialize(userDetailPda)) { const userDetailData = await this.program.account.userDetailAccount.fetch(userDetailPda.toBase58()); orderId = new BN(userDetailData.totalOrders.toNumber() + 1); } const [userTierDetailPda] = await this.getUserTierDetailAccountPdaAndBump(userDetailPda, salePhaseTierDetailPda); const [orderDetailPda] = await this.getOrderDetailAccountPdaAndBump(salePhaseDetailPda, userDetailPda, orderId.toString()); return this.program.methods .createOrderReceipt(salePhaseDetailBump, salePhaseTierDetailBump, salePhaseName, tierId.toString(), orderId.toString(), quantity, followTiers) .accounts({ payer: payer, backAuthority: backAuthority, user: user, salePhaseDetail: salePhaseDetailPda, salePhaseTierDetail: salePhaseTierDetailPda, userDetail: userDetailPda, userTierDetail: userTierDetailPda, orderDetail: orderDetailPda, systemProgram: config.systemProgram, rent: config.rent, }) .transaction(); } async anyPendingOrder(salePhaseName: string, user: PublicKey): Promise { let value: boolean = false; const [salePhaseDetailPda, salePhaseDetailBump] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); const [userDetailPda] = await this.getUserDetailAccountPdaAndBump(salePhaseDetailPda, user); const userDetailData = await this.program.account.userDetailAccount.fetch(userDetailPda.toBase58()); for (let i = 1; i <= userDetailData.totalOrders.toNumber(); i++) { const [orderDetailPda] = await this.getOrderDetailAccountPdaAndBump(salePhaseDetailPda, userDetailPda, i.toString()); const orderDetailData = await this.program.account.orderDetailAccount.fetch(orderDetailPda.toBase58()); if (!orderDetailData.isCompleted) { value = true; break; } } return value; } async createFillOrderTransaction( payer: PublicKey, backAuthority: PublicKey, user: PublicKey, salePhaseName: string, config: FillOrderConfig = { systemProgram: SystemProgram.programId, rent: SYSVAR_RENT_PUBKEY, tokenProgram: TOKEN_PROGRAM_ID, associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, tokenMetadataProgram: METADATA_PROGRAM_ID, } ): Promise { if (!(await this.anyPendingOrder(salePhaseName, user))) { throw new Error('No pending order'); } const [salePhaseDetailPda, salePhaseDetailBump] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); const [userDetailPda, userDetailBump] = await this.getUserDetailAccountPdaAndBump(salePhaseDetailPda, user); let orderId: BN; let tokenId: BN; let tierId: number; let orderDetailPda: PublicKey = salePhaseDetailPda; let orderDetailBump: number = 0; let orderDetailData: any; const userDetailData = await this.program.account.userDetailAccount.fetch(userDetailPda.toBase58()); for (let i = 1; i <= userDetailData.totalOrders.toNumber(); i++) { [orderDetailPda, orderDetailBump] = await this.getOrderDetailAccountPdaAndBump(salePhaseDetailPda, userDetailPda, i.toString()); orderDetailData = await this.program.account.orderDetailAccount.fetch(orderDetailPda.toBase58()); if (!orderDetailData.isCompleted) { orderId = new BN(i); break; } } tierId = orderDetailData.tierId; for (let i = 0; i < orderDetailData.isTokenIdsMinted.length; i++) { if (!orderDetailData.isTokenIdsMinted[i]) { tokenId = orderDetailData.tokenIds[i]; break; } } const [salePhaseTierDetailPda, salePhaseTierDetailBump] = await this.getSogaNodeSalePhaseTierDetailAccountPdaAndBump(salePhaseDetailPda, tierId.toString()); const [collectionMintAccountPda, collectionMintAccountBump] = await this.getSogaNodeSalePhaseTierCollectionMintAccountPdaAndBump(salePhaseTierDetailPda); const [collectionMasterEditionPda] = await this.getMasterEditionAccountPda(collectionMintAccountPda); const [collectionMetadataPda] = await this.getMetadataAccountPda(collectionMintAccountPda); const [nodeMintAccountPda] = await this.getNodeMintAccountPdaAndBump(collectionMintAccountPda, tokenId.toString()); const [nodeMasterEditionPda] = await this.getMasterEditionAccountPda(nodeMintAccountPda); const [nodeMetadataPda] = await this.getMetadataAccountPda(nodeMintAccountPda); const nodeUserTokenAccount = await getAssociatedTokenAddress(nodeMintAccountPda, user, true, config.tokenProgram, config.associatedTokenProgram); return this.program.methods .fileOrder( salePhaseDetailBump, salePhaseTierDetailBump, collectionMintAccountBump, userDetailBump, orderDetailBump, salePhaseName, tierId.toString(), tokenId.toString(), orderId.toString() ) .accounts({ payer: payer, backAuthority: backAuthority, user: user, salePhaseDetail: salePhaseDetailPda, salePhaseTierDetail: salePhaseTierDetailPda, userDetail: userDetailPda, orderDetail: orderDetailPda, collectionMintAccount: collectionMintAccountPda, nodeMintAccount: nodeMintAccountPda, userTokenAccount: nodeUserTokenAccount, tokenMetadataProgram: config.tokenMetadataProgram, tokenProgram: config.tokenProgram, associatedTokenProgram: config.associatedTokenProgram, systemProgram: config.systemProgram, rent: config.rent, }) .remainingAccounts([ { pubkey: collectionMetadataPda, isWritable: true, isSigner: false, }, { pubkey: collectionMasterEditionPda, isWritable: true, isSigner: false, }, { pubkey: nodeMetadataPda, isWritable: true, isSigner: false, }, { pubkey: nodeMasterEditionPda, isWritable: true, isSigner: false, }, ]) .transaction(); } async getSalePhaseDetail(salePhaseName: string) { const [salePhaseDetailPda] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); return this.program.account.sogaNodeSalePhaseDetailAccount.fetch(salePhaseDetailPda.toBase58()); } async getSalePhaseTierDetail(salePhaseName: string, tierId: number) { const [salePhaseDetailPda] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); const [salePhaseTierDetailPda] = await this.getSogaNodeSalePhaseTierDetailAccountPdaAndBump(salePhaseDetailPda, tierId.toString()); return this.program.account.sogaNodeSalePhaseTierDetailAccount.fetch(salePhaseTierDetailPda.toBase58()); } async getUserDetail(salePhaseName: string, user: PublicKey) { const [salePhaseDetailPda] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); const [userDetailPda] = await this.getUserDetailAccountPdaAndBump(salePhaseDetailPda, user); return this.program.account.userDetailAccount.fetch(userDetailPda.toBase58()); } async getUserTierDetail(salePhaseName: string, user: PublicKey, tierId: number) { const [salePhaseDetailPda] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); const [salePhaseTierDetailPda] = await this.getSogaNodeSalePhaseTierDetailAccountPdaAndBump(salePhaseDetailPda, tierId.toString()); const [userDetailPda] = await this.getUserDetailAccountPdaAndBump(salePhaseDetailPda, user); const [userTierDetailPda] = await this.getUserTierDetailAccountPdaAndBump(userDetailPda, salePhaseTierDetailPda); return this.program.account.userTierDetailAccount.fetch(userTierDetailPda.toBase58()); } async getOrderDetail(salePhaseName: string, user: PublicKey, orderId: number) { const [salePhaseDetailPda] = await this.getSogaNodeSalePhaseDetailAccountPdaAndBump(salePhaseName); const [userDetailPda] = await this.getUserDetailAccountPdaAndBump(salePhaseDetailPda, user); const [orderDetailPda] = await this.getOrderDetailAccountPdaAndBump(salePhaseDetailPda, userDetailPda, orderId.toString()); return this.program.account.orderDetailAccount.fetch(orderDetailPda.toBase58()); } async removeEventListener(eventId: number) { await this.program.removeEventListener(eventId); } addAirdropEventListener(callback: (event: AirdropEvent) => void): number { return this.program.addEventListener(AirdropEventName, callback); } addBuyEventListener(callback: (event: BuyEvent) => void): number { return this.program.addEventListener(BuyEventName, callback); } addBuyWithTokenEventListener(callback: (event: BuyWithTokenEvent) => void): number { return this.program.addEventListener(BuyWithTokenEventName, callback); } addFillOrderEventListener(callback: (event: FillOrderEvent) => void): number { return this.program.addEventListener(FillOrderEventName, callback); } addInitializeSalePhaseEventListener(callback: (event: InitializeSalePhaseEvent) => void): number { return this.program.addEventListener(InitializeSalePhaseEventName, callback); } addInitializeSalePhasePaymentTokenEventListener(callback: (event: InitializeSalePhasePaymentTokenEvent) => void): number { return this.program.addEventListener(InitializeSalePhasePaymentTokenEventName, callback); } addInitializeSalePhaseTierEventListener(callback: (event: InitializeSalePhaseTierEvent) => void): number { return this.program.addEventListener(InitializeSalePhaseTierEventName, callback); } addUpdateSalePhaseEventListener(callback: (event: UpdateSalePhaseEvent) => void): number { return this.program.addEventListener(UpdateSalePhaseEventName, callback); } addUpdateSalePhasePaymentTokenEventListener(callback: (event: UpdateSalePhasePaymentTokenEvent) => void): number { return this.program.addEventListener(UpdateSalePhasePaymentTokenEventName, callback); } addUpdateSalePhaseTierEventListener(callback: (event: UpdateSalePhaseTierEvent) => void): number { return this.program.addEventListener(UpdateSalePhaseTierEventName, callback); } addCreateOrderReceiptEventListener(callback: (event: CreateOrderReceiptEvent) => void): number { return this.program.addEventListener(CreateOrderReceiptEventName, callback); } }