diff --git a/.github/workflows/ci-autogen.yml b/.github/workflows/ci-autogen.yml index 98ee91ce9..5c783fc55 100644 --- a/.github/workflows/ci-autogen.yml +++ b/.github/workflows/ci-autogen.yml @@ -48,4 +48,4 @@ jobs: - uses: nickcharlton/diff-check@main with: - command: cd client/idl && yarn && node generateIdl.js && node generateClient.js && node generateIdl.js && cd - ; npm install -g prettier; prettier client/ts --write ; prettier client/ts/src/manifest --write; prettier client/ts/src/wrapper --write; git diff; \ No newline at end of file + command: cd client/idl && yarn && node generateIdl.js && node generateClient.js && node generateIdl.js && cd - ; npm install -g prettier; prettier client/ts --write ; prettier client/ts/src/manifest --write; prettier client/ts/src/wrapper --write; prettier client/ts/src/ui_wrapper --write; git diff; \ No newline at end of file diff --git a/client/idl/generateIdl.js b/client/idl/generateIdl.js index a933282a0..6b539a9cd 100644 --- a/client/idl/generateIdl.js +++ b/client/idl/generateIdl.js @@ -252,7 +252,7 @@ function modifyIdlCore(programName) { case 'CreateWrapper': { break; } - case 'ClaimSeat': { + case 'ClaimSeatUnused': { // Claim seat does not have params break; } diff --git a/client/idl/ui_wrapper.json b/client/idl/ui_wrapper.json index 99cb0ed6d..15deaa2fd 100644 --- a/client/idl/ui_wrapper.json +++ b/client/idl/ui_wrapper.json @@ -45,7 +45,7 @@ } }, { - "name": "ClaimSeat", + "name": "ClaimSeatUnused", "accounts": [ { "name": "manifestProgram", @@ -281,7 +281,168 @@ }, { "name": "EditOrder", - "accounts": [], + "accounts": [ + { + "name": "wrapperState", + "isMut": true, + "isSigner": false, + "docs": [ + "Wrapper state" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true, + "docs": [ + "Owner of the Manifest account" + ] + }, + { + "name": "traderTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Trader token account" + ] + }, + { + "name": "market", + "isMut": true, + "isSigner": false, + "docs": [ + "Account holding all market state" + ] + }, + { + "name": "vault", + "isMut": true, + "isSigner": false, + "docs": [ + "Vault PDA, seeds are [b'vault', market_address, mint_address]" + ] + }, + { + "name": "mint", + "isMut": true, + "isSigner": false, + "docs": [ + "Mint of trader token account" + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "System program" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "Token program owning trader token account" + ] + }, + { + "name": "manifestProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "Manifest program" + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true, + "docs": [ + "Payer of rent and gas" + ] + }, + { + "name": "baseMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Base mint" + ] + }, + { + "name": "baseGlobal", + "isMut": true, + "isSigner": false, + "docs": [ + "Base global account" + ] + }, + { + "name": "baseGlobalVault", + "isMut": true, + "isSigner": false, + "docs": [ + "Base global vault" + ] + }, + { + "name": "baseMarketVault", + "isMut": true, + "isSigner": false, + "docs": [ + "Base market vault" + ] + }, + { + "name": "baseTokenProgram", + "isMut": true, + "isSigner": false, + "docs": [ + "Base token program" + ] + }, + { + "name": "quoteMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Quote mint" + ] + }, + { + "name": "quoteGlobal", + "isMut": true, + "isSigner": false, + "docs": [ + "Quote global account" + ] + }, + { + "name": "quoteGlobalVault", + "isMut": true, + "isSigner": false, + "docs": [ + "Quote global vault" + ] + }, + { + "name": "quoteMarketVault", + "isMut": true, + "isSigner": false, + "docs": [ + "Quote market vault" + ] + }, + { + "name": "quoteTokenProgram", + "isMut": true, + "isSigner": false, + "docs": [ + "Quote token program" + ] + } + ], "args": [], "discriminant": { "type": "u8", @@ -521,6 +682,10 @@ "name": "user", "type": "publicKey" }, + { + "name": "mint", + "type": "publicKey" + }, { "name": "platformTokenAccount", "type": "publicKey" @@ -545,6 +710,10 @@ "name": "user", "type": "publicKey" }, + { + "name": "mint", + "type": "publicKey" + }, { "name": "referrerTokenAccount", "type": "publicKey" @@ -742,6 +911,13 @@ } } ], + "errors": [ + { + "code": 0, + "name": "InvalidDepositAccounts", + "msg": "Invalid deposit accounts error" + } + ], "metadata": { "origin": "shank", "address": "UMnFStVeG1ecZFc2gc5K3vFy3sMpotq8C91mXBQDGwh" diff --git a/client/ts/src/client.ts b/client/ts/src/client.ts index 273956d46..fff00d724 100644 --- a/client/ts/src/client.ts +++ b/client/ts/src/client.ts @@ -27,7 +27,7 @@ import { } from './manifest/instructions'; import { OrderType, SwapParams } from './manifest/types'; import { Market } from './market'; -import { MarketInfoParsed, Wrapper, WrapperData } from './wrapperObj'; +import { WrapperMarketInfo, Wrapper, WrapperData } from './wrapperObj'; import { PROGRAM_ID as MANIFEST_PROGRAM_ID, PROGRAM_ID } from './manifest'; import { PROGRAM_ID as WRAPPER_PROGRAM_ID, @@ -226,8 +226,8 @@ export class ManifestClient { const wrapperData: WrapperData = Wrapper.deserializeWrapperBuffer( userWrapper.account.data, ); - const existingMarketInfos: MarketInfoParsed[] = - wrapperData.marketInfos.filter((marketInfo: MarketInfoParsed) => { + const existingMarketInfos: WrapperMarketInfo[] = + wrapperData.marketInfos.filter((marketInfo: WrapperMarketInfo) => { return marketInfo.market.toBase58() == marketPk.toBase58(); }); if (existingMarketInfos.length > 0) { @@ -335,8 +335,8 @@ export class ManifestClient { userWrapper.account.data, ); - const existingMarketInfos: MarketInfoParsed[] = - wrapperData.marketInfos.filter((marketInfo: MarketInfoParsed) => { + const existingMarketInfos: WrapperMarketInfo[] = + wrapperData.marketInfos.filter((marketInfo: WrapperMarketInfo) => { return marketInfo.market.toBase58() == marketPk.toBase58(); }); if (existingMarketInfos.length > 0) { diff --git a/client/ts/src/index.ts b/client/ts/src/index.ts index e25b95a8f..446ad8383 100644 --- a/client/ts/src/index.ts +++ b/client/ts/src/index.ts @@ -1,9 +1,9 @@ export * from './client'; export * from './market'; export * from './types'; -// Do not export all of manifest because names collide with wrapper. Force users -// to use the client. -export * from './manifest/errors'; -export * from './manifest/accounts'; -export * from './wrapper'; +export * as manifest from './manifest'; +export * as utils from './utils'; +export * as wrapper from './wrapper'; export * from './wrapperObj'; +export * as uiWrapper from './ui_wrapper'; +export * from './uiWrapperObj'; diff --git a/client/ts/src/market.ts b/client/ts/src/market.ts index e6fb2b034..ece6d1163 100644 --- a/client/ts/src/market.ts +++ b/client/ts/src/market.ts @@ -20,11 +20,11 @@ import { } from './constants'; import { claimedSeatBeet, - ClaimedSeat as ClaimedSeatInternal, + ClaimedSeat as ClaimedSeatRaw, createCreateMarketInstruction, PROGRAM_ID, restingOrderBeet, - RestingOrder as RestingOrderInternal, + RestingOrder as RestingOrderRaw, } from './manifest'; import { getVaultAddress } from './utils/market'; import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; @@ -500,7 +500,7 @@ export class Market { bidsRootIndex, restingOrderBeet, ) - .map((restingOrderInternal: RestingOrderInternal) => { + .map((restingOrderInternal: RestingOrderRaw) => { return { trader: publicKeyBeet.deserialize( data.subarray( @@ -536,7 +536,7 @@ export class Market { asksRootIndex, restingOrderBeet, ) - .map((restingOrderInternal: RestingOrderInternal) => { + .map((restingOrderInternal: RestingOrderRaw) => { return { trader: publicKeyBeet.deserialize( data.subarray( @@ -571,7 +571,7 @@ export class Market { data.subarray(FIXED_MANIFEST_HEADER_SIZE), claimedSeatsRootIndex, claimedSeatBeet, - ).map((claimedSeatInternal: ClaimedSeatInternal) => { + ).map((claimedSeatInternal: ClaimedSeatRaw) => { return { publicKey: claimedSeatInternal.trader, baseBalance: claimedSeatInternal.baseWithdrawableBalance, diff --git a/client/ts/src/uiWrapperObj.ts b/client/ts/src/uiWrapperObj.ts index 5e88a498a..626beb1d9 100644 --- a/client/ts/src/uiWrapperObj.ts +++ b/client/ts/src/uiWrapperObj.ts @@ -18,7 +18,7 @@ import { PROGRAM_ID, SettleFundsInstructionArgs, wrapperOpenOrderBeet as uiWrapperOpenOrderBeet, - WrapperOpenOrder as UIWrapperOpenOrder, + WrapperOpenOrder as UIWrapperOpenOrderRaw, } from './ui_wrapper'; import { deserializeRedBlackTree } from './utils/redBlackTree'; import { @@ -39,22 +39,25 @@ import { import { convertU128 } from './utils/numbers'; import { BN } from 'bn.js'; import { getGlobalAddress, getGlobalVaultAddress } from './utils/global'; -import { MarketInfo, marketInfoBeet } from './wrapper/types'; +import { + MarketInfo as UiWrapperMarketInfoRaw, + marketInfoBeet, +} from './ui_wrapper/types'; /** * All data stored on a wrapper account. */ -export interface WrapperData { +export interface UiWrapperData { /** Public key for the owner of this wrapper. */ owner: PublicKey; /** Array of market infos that have been parsed. */ - marketInfos: MarketInfoParsed[]; + marketInfos: UiWrapperMarketInfo[]; } /** * Parsed market info on a wrapper. Accurate to the last sync. */ -export interface MarketInfoParsed { +export interface UiWrapperMarketInfo { /** Public key for market. */ market: PublicKey; /** Base balance in atoms. */ @@ -62,29 +65,15 @@ export interface MarketInfoParsed { /** Quote balance in atoms. */ quoteBalanceAtoms: bignum; /** Open orders. */ - orders: OpenOrder[]; + orders: UiWrapperOpenOrder[]; /** Last update slot number. */ lastUpdatedSlot: number; } -/** - * Raw market info on a wrapper. - */ -export interface MarketInfoRaw { - market: PublicKey; - openOrdersRootIndex: number; - traderIndex: number; - baseBalanceAtoms: bignum; - quoteBalanceAtoms: bignum; - quoteVolumeAtoms: bignum; - lastUpdatedSlot: number; - padding: number; // 3 bytes -} - /** * OpenOrder on a wrapper. Accurate as of the latest sync. */ -export interface OpenOrder { +export interface UiWrapperOpenOrder { /** Client order id used for cancelling orders. Does not need to be unique. */ clientOrderId: bignum; /** Exchange defined id for an order. */ @@ -103,7 +92,7 @@ export interface OpenOrder { orderType: OrderType; } -export interface UIOpenOrderInternal { +export interface UiWrapperOpenOrderRaw { price: Uint8Array; clientOrderId: bignum; orderSequenceNumber: bignum; @@ -122,7 +111,7 @@ export class UiWrapper { /** Public key for the market account. */ address: PublicKey; /** Deserialized data. */ - private data: WrapperData; + private data: UiWrapperData; /** * Constructs a Wrapper object. @@ -135,7 +124,7 @@ export class UiWrapper { data, }: { address: PublicKey; - data: WrapperData; + data: UiWrapperData; }) { this.address = address; this.data = data; @@ -180,9 +169,9 @@ export class UiWrapper { * * @return MarketInfoParsed */ - public marketInfoForMarket(marketPk: PublicKey): MarketInfoParsed | null { - const filtered: MarketInfoParsed[] = this.data.marketInfos.filter( - (marketInfo: MarketInfoParsed) => { + public marketInfoForMarket(marketPk: PublicKey): UiWrapperMarketInfo | null { + const filtered: UiWrapperMarketInfo[] = this.data.marketInfos.filter( + (marketInfo: UiWrapperMarketInfo) => { return marketInfo.market.equals(marketPk); }, ); @@ -199,9 +188,9 @@ export class UiWrapper { * * @return OpenOrder[] */ - public openOrdersForMarket(marketPk: PublicKey): OpenOrder[] | null { - const filtered: MarketInfoParsed[] = this.data.marketInfos.filter( - (marketInfo: MarketInfoParsed) => { + public openOrdersForMarket(marketPk: PublicKey): UiWrapperOpenOrder[] | null { + const filtered: UiWrapperMarketInfo[] = this.data.marketInfos.filter( + (marketInfo: UiWrapperMarketInfo) => { return marketInfo.market.equals(marketPk); }, ); @@ -279,14 +268,14 @@ export class UiWrapper { console.log(`Wrapper: ${this.address.toBase58()}`); console.log(`========================`); console.log(`Owner: ${this.data.owner.toBase58()}`); - this.data.marketInfos.forEach((marketInfo: MarketInfoParsed) => { + this.data.marketInfos.forEach((marketInfo: UiWrapperMarketInfo) => { console.log(`------------------------`); console.log(`Market: ${marketInfo.market}`); console.log(`Last updated slot: ${marketInfo.lastUpdatedSlot}`); console.log( `BaseAtoms: ${marketInfo.baseBalanceAtoms} QuoteAtoms: ${marketInfo.quoteBalanceAtoms}`, ); - marketInfo.orders.forEach((order: OpenOrder) => { + marketInfo.orders.forEach((order: UiWrapperOpenOrder) => { console.log( `OpenOrder: ClientOrderId: ${order.clientOrderId} ${order.numBaseAtoms}@${order.price} SeqNum: ${order.orderSequenceNumber} LastValidSlot: ${order.lastValidSlot} IsBid: ${order.isBid}`, ); @@ -305,7 +294,7 @@ export class UiWrapper { * * @returns WrapperData */ - public static deserializeWrapperBuffer(data: Buffer): WrapperData { + public static deserializeWrapperBuffer(data: Buffer): UiWrapperData { let offset = 0; // Deserialize the market header const _discriminant = data.readBigUInt64LE(0); @@ -327,7 +316,7 @@ export class UiWrapper { const _padding = data.readUInt32LE(offset); offset += 12; - const marketInfos: MarketInfo[] = + const marketInfos: UiWrapperMarketInfoRaw[] = marketInfosRootIndex != NIL ? deserializeRedBlackTree( data.subarray(FIXED_WRAPPER_HEADER_SIZE), @@ -336,10 +325,10 @@ export class UiWrapper { ) : []; - const parsedMarketInfos: MarketInfoParsed[] = marketInfos.map( - (marketInfoRaw: MarketInfo) => { + const parsedMarketInfos: UiWrapperMarketInfo[] = marketInfos.map( + (marketInfoRaw: UiWrapperMarketInfoRaw) => { const rootIndex: number = marketInfoRaw.ordersRootIndex; - const parsedOpenOrders: UIWrapperOpenOrder[] = + const rawOpenOrders: UIWrapperOpenOrderRaw[] = rootIndex != NIL ? deserializeRedBlackTree( data.subarray(FIXED_WRAPPER_HEADER_SIZE), @@ -348,15 +337,14 @@ export class UiWrapper { ) : []; - const parsedOpenOrdersWithPrice: OpenOrder[] = parsedOpenOrders.map( - (openOrder: UIWrapperOpenOrder) => { + const parsedOpenOrdersWithPrice: UiWrapperOpenOrder[] = + rawOpenOrders.map((openOrder: UIWrapperOpenOrderRaw) => { return { ...openOrder, dataIndex: openOrder.marketDataIndex, price: convertU128(new BN(openOrder.price, 10, 'le')), }; - }, - ); + }); return { market: marketInfoRaw.market, @@ -483,18 +471,28 @@ export class UiWrapper { if (market != null) { const wrapper = await UiWrapper.fetchFirstUserWrapper(connection, owner); if (wrapper) { - const placeIx = UiWrapper.loadFromBuffer({ + const wrapperParsed = UiWrapper.loadFromBuffer({ address: wrapper.pubkey, buffer: wrapper.account.data, - }).placeOrderIx(market, { payer }, args); - return { ixs: [placeIx], signers: [] }; + }); + const placeIx = wrapperParsed.placeOrderIx(market, { payer }, args); + if ( + wrapperParsed.activeMarkets().find((x) => x.equals(market.address)) + ) { + return { ixs: [placeIx], signers: [] }; + } else { + const claimSeatIx: TransactionInstruction = + createClaimSeatInstruction({ + manifestProgram: MANIFEST_PROGRAM_ID, + payer, + owner, + market: market.address, + wrapperState: wrapper.pubkey, + }); + return { ixs: [claimSeatIx, placeIx], signers: [] }; + } } else { - const setup = await this.setupIxs( - connection, - market.address, - owner, - payer, - ); + const setup = await this.setupIxs(connection, owner, payer); const wrapper = setup.signers[0].publicKey; const place = await this.placeIx_(market, wrapper, owner, payer, args); return { @@ -516,12 +514,7 @@ export class UiWrapper { baseDecimals: () => baseDecimals, quoteDecimals: () => quoteDecimals, }; - const wrapperIxs = await this.setupIxs( - connection, - market.address, - owner, - payer, - ); + const wrapperIxs = await this.setupIxs(connection, owner, payer); const wrapper = wrapperIxs.signers[0].publicKey; const placeIx = await this.placeIx_(market, wrapper, owner, payer, args); return { @@ -537,7 +530,6 @@ export class UiWrapper { public static async setupIxs( connection: Connection, - market: PublicKey, owner: PublicKey, payer: PublicKey, ): Promise<{ ixs: TransactionInstruction[]; signers: Signer[] }> { @@ -559,15 +551,8 @@ export class UiWrapper { owner, wrapperState: wrapperKeypair.publicKey, }); - const claimSeatIx: TransactionInstruction = createClaimSeatInstruction({ - manifestProgram: MANIFEST_PROGRAM_ID, - payer, - owner, - market, - wrapperState: wrapperKeypair.publicKey, - }); return { - ixs: [createAccountIx, createWrapperIx, claimSeatIx], + ixs: [createAccountIx, createWrapperIx], signers: [wrapperKeypair], }; } diff --git a/client/ts/src/ui_wrapper/accounts/PlatformFeeLog.ts b/client/ts/src/ui_wrapper/accounts/PlatformFeeLog.ts index 1c8ab5d65..2eac7a1f8 100644 --- a/client/ts/src/ui_wrapper/accounts/PlatformFeeLog.ts +++ b/client/ts/src/ui_wrapper/accounts/PlatformFeeLog.ts @@ -17,6 +17,7 @@ import * as beetSolana from '@metaplex-foundation/beet-solana'; export type PlatformFeeLogArgs = { market: web3.PublicKey; user: web3.PublicKey; + mint: web3.PublicKey; platformTokenAccount: web3.PublicKey; platformFee: beet.bignum; }; @@ -31,6 +32,7 @@ export class PlatformFeeLog implements PlatformFeeLogArgs { private constructor( readonly market: web3.PublicKey, readonly user: web3.PublicKey, + readonly mint: web3.PublicKey, readonly platformTokenAccount: web3.PublicKey, readonly platformFee: beet.bignum, ) {} @@ -42,6 +44,7 @@ export class PlatformFeeLog implements PlatformFeeLogArgs { return new PlatformFeeLog( args.market, args.user, + args.mint, args.platformTokenAccount, args.platformFee, ); @@ -149,6 +152,7 @@ export class PlatformFeeLog implements PlatformFeeLogArgs { return { market: this.market.toBase58(), user: this.user.toBase58(), + mint: this.mint.toBase58(), platformTokenAccount: this.platformTokenAccount.toBase58(), platformFee: (() => { const x = <{ toNumber: () => number }>this.platformFee; @@ -176,6 +180,7 @@ export const platformFeeLogBeet = new beet.BeetStruct< [ ['market', beetSolana.publicKey], ['user', beetSolana.publicKey], + ['mint', beetSolana.publicKey], ['platformTokenAccount', beetSolana.publicKey], ['platformFee', beet.u64], ], diff --git a/client/ts/src/ui_wrapper/accounts/ReferrerFeeLog.ts b/client/ts/src/ui_wrapper/accounts/ReferrerFeeLog.ts index 9c634b794..6d0d5af13 100644 --- a/client/ts/src/ui_wrapper/accounts/ReferrerFeeLog.ts +++ b/client/ts/src/ui_wrapper/accounts/ReferrerFeeLog.ts @@ -17,6 +17,7 @@ import * as beetSolana from '@metaplex-foundation/beet-solana'; export type ReferrerFeeLogArgs = { market: web3.PublicKey; user: web3.PublicKey; + mint: web3.PublicKey; referrerTokenAccount: web3.PublicKey; referrerFee: beet.bignum; }; @@ -31,6 +32,7 @@ export class ReferrerFeeLog implements ReferrerFeeLogArgs { private constructor( readonly market: web3.PublicKey, readonly user: web3.PublicKey, + readonly mint: web3.PublicKey, readonly referrerTokenAccount: web3.PublicKey, readonly referrerFee: beet.bignum, ) {} @@ -42,6 +44,7 @@ export class ReferrerFeeLog implements ReferrerFeeLogArgs { return new ReferrerFeeLog( args.market, args.user, + args.mint, args.referrerTokenAccount, args.referrerFee, ); @@ -149,6 +152,7 @@ export class ReferrerFeeLog implements ReferrerFeeLogArgs { return { market: this.market.toBase58(), user: this.user.toBase58(), + mint: this.mint.toBase58(), referrerTokenAccount: this.referrerTokenAccount.toBase58(), referrerFee: (() => { const x = <{ toNumber: () => number }>this.referrerFee; @@ -176,6 +180,7 @@ export const referrerFeeLogBeet = new beet.BeetStruct< [ ['market', beetSolana.publicKey], ['user', beetSolana.publicKey], + ['mint', beetSolana.publicKey], ['referrerTokenAccount', beetSolana.publicKey], ['referrerFee', beet.u64], ], diff --git a/client/ts/src/ui_wrapper/errors/index.ts b/client/ts/src/ui_wrapper/errors/index.ts new file mode 100644 index 000000000..0ef3653b7 --- /dev/null +++ b/client/ts/src/ui_wrapper/errors/index.ts @@ -0,0 +1,55 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +type ErrorWithCode = Error & { code: number }; +type MaybeErrorWithCode = ErrorWithCode | null | undefined; + +const createErrorFromCodeLookup: Map ErrorWithCode> = new Map(); +const createErrorFromNameLookup: Map ErrorWithCode> = new Map(); + +/** + * InvalidDepositAccounts: 'Invalid deposit accounts error' + * + * @category Errors + * @category generated + */ +export class InvalidDepositAccountsError extends Error { + readonly code: number = 0x0; + readonly name: string = 'InvalidDepositAccounts'; + constructor() { + super('Invalid deposit accounts error'); + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, InvalidDepositAccountsError); + } + } +} + +createErrorFromCodeLookup.set(0x0, () => new InvalidDepositAccountsError()); +createErrorFromNameLookup.set( + 'InvalidDepositAccounts', + () => new InvalidDepositAccountsError(), +); + +/** + * Attempts to resolve a custom program error from the provided error code. + * @category Errors + * @category generated + */ +export function errorFromCode(code: number): MaybeErrorWithCode { + const createError = createErrorFromCodeLookup.get(code); + return createError != null ? createError() : null; +} + +/** + * Attempts to resolve a custom program error from the provided error name, i.e. 'Unauthorized'. + * @category Errors + * @category generated + */ +export function errorFromName(name: string): MaybeErrorWithCode { + const createError = createErrorFromNameLookup.get(name); + return createError != null ? createError() : null; +} diff --git a/client/ts/src/ui_wrapper/index.ts b/client/ts/src/ui_wrapper/index.ts index ee426a3d3..2a2d2683e 100644 --- a/client/ts/src/ui_wrapper/index.ts +++ b/client/ts/src/ui_wrapper/index.ts @@ -1,5 +1,6 @@ import { PublicKey } from '@solana/web3.js'; export * from './accounts'; +export * from './errors'; export * from './instructions'; export * from './types'; diff --git a/client/ts/src/ui_wrapper/instructions/ClaimSeat.ts b/client/ts/src/ui_wrapper/instructions/ClaimSeatUnused.ts similarity index 73% rename from client/ts/src/ui_wrapper/instructions/ClaimSeat.ts rename to client/ts/src/ui_wrapper/instructions/ClaimSeatUnused.ts index 8fb273916..471b28e0d 100644 --- a/client/ts/src/ui_wrapper/instructions/ClaimSeat.ts +++ b/client/ts/src/ui_wrapper/instructions/ClaimSeatUnused.ts @@ -10,14 +10,14 @@ import * as web3 from '@solana/web3.js'; /** * @category Instructions - * @category ClaimSeat + * @category ClaimSeatUnused * @category generated */ -export const ClaimSeatStruct = new beet.BeetArgsStruct<{ +export const ClaimSeatUnusedStruct = new beet.BeetArgsStruct<{ instructionDiscriminator: number; -}>([['instructionDiscriminator', beet.u8]], 'ClaimSeatInstructionArgs'); +}>([['instructionDiscriminator', beet.u8]], 'ClaimSeatUnusedInstructionArgs'); /** - * Accounts required by the _ClaimSeat_ instruction + * Accounts required by the _ClaimSeatUnused_ instruction * * @property [] manifestProgram * @property [_writable_, **signer**] owner @@ -25,10 +25,10 @@ export const ClaimSeatStruct = new beet.BeetArgsStruct<{ * @property [_writable_, **signer**] payer * @property [_writable_] wrapperState * @category Instructions - * @category ClaimSeat + * @category ClaimSeatUnused * @category generated */ -export type ClaimSeatInstructionAccounts = { +export type ClaimSeatUnusedInstructionAccounts = { manifestProgram: web3.PublicKey; owner: web3.PublicKey; market: web3.PublicKey; @@ -37,22 +37,22 @@ export type ClaimSeatInstructionAccounts = { wrapperState: web3.PublicKey; }; -export const claimSeatInstructionDiscriminator = 1; +export const claimSeatUnusedInstructionDiscriminator = 1; /** - * Creates a _ClaimSeat_ instruction. + * Creates a _ClaimSeatUnused_ instruction. * * @param accounts that will be accessed while the instruction is processed * @category Instructions - * @category ClaimSeat + * @category ClaimSeatUnused * @category generated */ -export function createClaimSeatInstruction( - accounts: ClaimSeatInstructionAccounts, +export function createClaimSeatUnusedInstruction( + accounts: ClaimSeatUnusedInstructionAccounts, programId = new web3.PublicKey('UMnFStVeG1ecZFc2gc5K3vFy3sMpotq8C91mXBQDGwh'), ) { - const [data] = ClaimSeatStruct.serialize({ - instructionDiscriminator: claimSeatInstructionDiscriminator, + const [data] = ClaimSeatUnusedStruct.serialize({ + instructionDiscriminator: claimSeatUnusedInstructionDiscriminator, }); const keys: web3.AccountMeta[] = [ { diff --git a/client/ts/src/ui_wrapper/instructions/EditOrder.ts b/client/ts/src/ui_wrapper/instructions/EditOrder.ts index 854bcccef..3d4de6b42 100644 --- a/client/ts/src/ui_wrapper/instructions/EditOrder.ts +++ b/client/ts/src/ui_wrapper/instructions/EditOrder.ts @@ -5,6 +5,7 @@ * See: https://github.com/metaplex-foundation/solita */ +import * as splToken from '@solana/spl-token'; import * as beet from '@metaplex-foundation/beet'; import * as web3 from '@solana/web3.js'; @@ -16,23 +17,173 @@ import * as web3 from '@solana/web3.js'; export const EditOrderStruct = new beet.BeetArgsStruct<{ instructionDiscriminator: number; }>([['instructionDiscriminator', beet.u8]], 'EditOrderInstructionArgs'); +/** + * Accounts required by the _EditOrder_ instruction + * + * @property [_writable_] wrapperState + * @property [**signer**] owner + * @property [_writable_] traderTokenAccount + * @property [_writable_] market + * @property [_writable_] vault + * @property [_writable_] mint + * @property [] manifestProgram + * @property [_writable_, **signer**] payer + * @property [] baseMint + * @property [_writable_] baseGlobal + * @property [_writable_] baseGlobalVault + * @property [_writable_] baseMarketVault + * @property [_writable_] baseTokenProgram + * @property [] quoteMint + * @property [_writable_] quoteGlobal + * @property [_writable_] quoteGlobalVault + * @property [_writable_] quoteMarketVault + * @property [_writable_] quoteTokenProgram + * @category Instructions + * @category EditOrder + * @category generated + */ +export type EditOrderInstructionAccounts = { + wrapperState: web3.PublicKey; + owner: web3.PublicKey; + traderTokenAccount: web3.PublicKey; + market: web3.PublicKey; + vault: web3.PublicKey; + mint: web3.PublicKey; + systemProgram?: web3.PublicKey; + tokenProgram?: web3.PublicKey; + manifestProgram: web3.PublicKey; + payer: web3.PublicKey; + baseMint: web3.PublicKey; + baseGlobal: web3.PublicKey; + baseGlobalVault: web3.PublicKey; + baseMarketVault: web3.PublicKey; + baseTokenProgram: web3.PublicKey; + quoteMint: web3.PublicKey; + quoteGlobal: web3.PublicKey; + quoteGlobalVault: web3.PublicKey; + quoteMarketVault: web3.PublicKey; + quoteTokenProgram: web3.PublicKey; +}; export const editOrderInstructionDiscriminator = 3; /** * Creates a _EditOrder_ instruction. * + * @param accounts that will be accessed while the instruction is processed * @category Instructions * @category EditOrder * @category generated */ export function createEditOrderInstruction( + accounts: EditOrderInstructionAccounts, programId = new web3.PublicKey('UMnFStVeG1ecZFc2gc5K3vFy3sMpotq8C91mXBQDGwh'), ) { const [data] = EditOrderStruct.serialize({ instructionDiscriminator: editOrderInstructionDiscriminator, }); - const keys: web3.AccountMeta[] = []; + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.wrapperState, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.owner, + isWritable: false, + isSigner: true, + }, + { + pubkey: accounts.traderTokenAccount, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.market, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.vault, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.mint, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.systemProgram ?? web3.SystemProgram.programId, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.tokenProgram ?? splToken.TOKEN_PROGRAM_ID, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.manifestProgram, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.payer, + isWritable: true, + isSigner: true, + }, + { + pubkey: accounts.baseMint, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.baseGlobal, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.baseGlobalVault, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.baseMarketVault, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.baseTokenProgram, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.quoteMint, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.quoteGlobal, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.quoteGlobalVault, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.quoteMarketVault, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.quoteTokenProgram, + isWritable: true, + isSigner: false, + }, + ]; const ix = new web3.TransactionInstruction({ programId, diff --git a/client/ts/src/ui_wrapper/instructions/index.ts b/client/ts/src/ui_wrapper/instructions/index.ts index 15799433b..2244f94af 100644 --- a/client/ts/src/ui_wrapper/instructions/index.ts +++ b/client/ts/src/ui_wrapper/instructions/index.ts @@ -1,5 +1,5 @@ export * from './CancelOrder'; -export * from './ClaimSeat'; +export * from './ClaimSeatUnused'; export * from './CreateWrapper'; export * from './EditOrder'; export * from './PlaceOrder'; diff --git a/client/ts/src/utils/index.ts b/client/ts/src/utils/index.ts new file mode 100644 index 000000000..7f013d0a8 --- /dev/null +++ b/client/ts/src/utils/index.ts @@ -0,0 +1,7 @@ +export * from './beet'; +export * from './discriminator'; +export * from './global'; +export * from './market'; +export * from './numbers'; +export * from './redBlackTree'; +export * from './solana'; diff --git a/client/ts/src/utils/market.ts b/client/ts/src/utils/market.ts index cc19e5fa0..321a3989a 100644 --- a/client/ts/src/utils/market.ts +++ b/client/ts/src/utils/market.ts @@ -2,7 +2,7 @@ import { PROGRAM_ID } from '../manifest/index'; import { PublicKey } from '@solana/web3.js'; -export function getVaultAddress(market: PublicKey, mint: PublicKey) { +export function getVaultAddress(market: PublicKey, mint: PublicKey): PublicKey { const [vaultAddress, _unusedBump] = PublicKey.findProgramAddressSync( [Buffer.from('vault'), market.toBuffer(), mint.toBuffer()], PROGRAM_ID, diff --git a/client/ts/src/wrapperObj.ts b/client/ts/src/wrapperObj.ts index c289fd404..8889d1cb3 100644 --- a/client/ts/src/wrapperObj.ts +++ b/client/ts/src/wrapperObj.ts @@ -6,7 +6,7 @@ import { OrderType } from './manifest'; import { deserializeRedBlackTree } from './utils/redBlackTree'; import { MarketInfo, - WrapperOpenOrder, + WrapperOpenOrder as WrapperOpenOrderRaw, marketInfoBeet, wrapperOpenOrderBeet, } from './wrapper/types'; @@ -20,13 +20,13 @@ export interface WrapperData { /** Public key for the trader that owns this wrapper. */ trader: PublicKey; /** Array of market infos that have been parsed. */ - marketInfos: MarketInfoParsed[]; + marketInfos: WrapperMarketInfo[]; } /** * Parsed market info on a wrapper. Accurate to the last sync. */ -export interface MarketInfoParsed { +export interface WrapperMarketInfo { /** Public key for market. */ market: PublicKey; /** Base balance in atoms. */ @@ -36,29 +36,15 @@ export interface MarketInfoParsed { /** Quote volume in atoms. */ quoteVolumeAtoms: bignum; /** Open orders. */ - orders: OpenOrder[]; + orders: WrapperOpenOrder[]; /** Last update slot number. */ lastUpdatedSlot: number; } -/** - * Raw market info on a wrapper. - */ -export interface MarketInfoRaw { - market: PublicKey; - openOrdersRootIndex: number; - traderIndex: number; - baseBalanceAtoms: bignum; - quoteBalanceAtoms: bignum; - quoteVolumeAtoms: bignum; - lastUpdatedSlot: number; - padding: number; // 3 bytes -} - /** * OpenOrder on a wrapper. Accurate as of the latest sync. */ -export interface OpenOrder { +export interface WrapperOpenOrder { /** Client order id used for cancelling orders. Does not need to be unique. */ clientOrderId: bignum; /** Exchange defined id for an order. */ @@ -77,18 +63,6 @@ export interface OpenOrder { orderType: OrderType; } -export interface OpenOrderInternal { - price: Uint8Array; - clientOrderId: bignum; - orderSequenceNumber: bignum; - numBaseAtoms: bignum; - marketDataIndex: number; - lastValidSlot: number; - isBid: boolean; - orderType: number; - padding: bignum[]; // 30 bytes -} - /** * Wrapper object used for reading data from a wrapper for manifest markets. */ @@ -177,9 +151,9 @@ export class Wrapper { * * @return MarketInfoParsed */ - public marketInfoForMarket(marketPk: PublicKey): MarketInfoParsed | null { - const filtered: MarketInfoParsed[] = this.data.marketInfos.filter( - (marketInfo: MarketInfoParsed) => { + public marketInfoForMarket(marketPk: PublicKey): WrapperMarketInfo | null { + const filtered: WrapperMarketInfo[] = this.data.marketInfos.filter( + (marketInfo: WrapperMarketInfo) => { return marketInfo.market.toBase58() == marketPk.toBase58(); }, ); @@ -196,9 +170,9 @@ export class Wrapper { * * @return OpenOrder[] */ - public openOrdersForMarket(marketPk: PublicKey): OpenOrder[] | null { - const filtered: MarketInfoParsed[] = this.data.marketInfos.filter( - (marketInfo: MarketInfoParsed) => { + public openOrdersForMarket(marketPk: PublicKey): WrapperOpenOrder[] | null { + const filtered: WrapperMarketInfo[] = this.data.marketInfos.filter( + (marketInfo: WrapperMarketInfo) => { return marketInfo.market.toBase58() == marketPk.toBase58(); }, ); @@ -219,14 +193,14 @@ export class Wrapper { console.log(`Wrapper: ${this.address.toBase58()}`); console.log(`========================`); console.log(`Trader: ${this.data.trader.toBase58()}`); - this.data.marketInfos.forEach((marketInfo: MarketInfoParsed) => { + this.data.marketInfos.forEach((marketInfo: WrapperMarketInfo) => { console.log(`------------------------`); console.log(`Market: ${marketInfo.market}`); console.log(`Last updated slot: ${marketInfo.lastUpdatedSlot}`); console.log( `BaseAtoms: ${marketInfo.baseBalanceAtoms} QuoteAtoms: ${marketInfo.quoteBalanceAtoms}`, ); - marketInfo.orders.forEach((order: OpenOrder) => { + marketInfo.orders.forEach((order: WrapperOpenOrder) => { console.log( `OpenOrder: ClientOrderId: ${order.clientOrderId} ${order.numBaseAtoms}@${order.price} SeqNum: ${order.orderSequenceNumber} LastValidSlot: ${order.lastValidSlot} IsBid: ${order.isBid}`, ); @@ -276,10 +250,10 @@ export class Wrapper { ) : []; - const parsedMarketInfos: MarketInfoParsed[] = marketInfos.map( + const parsedMarketInfos: WrapperMarketInfo[] = marketInfos.map( (marketInfoRaw: MarketInfo) => { const rootIndex: number = marketInfoRaw.ordersRootIndex; - const parsedOpenOrders: WrapperOpenOrder[] = + const rawOpenOrders: WrapperOpenOrderRaw[] = rootIndex != NIL ? deserializeRedBlackTree( data.subarray(FIXED_WRAPPER_HEADER_SIZE), @@ -288,8 +262,8 @@ export class Wrapper { ) : []; - const parsedOpenOrdersWithPrice: OpenOrder[] = parsedOpenOrders.map( - (openOrder: WrapperOpenOrder) => { + const parsedOpenOrdersWithPrice: WrapperOpenOrder[] = rawOpenOrders.map( + (openOrder: WrapperOpenOrderRaw) => { return { ...openOrder, price: convertU128(new BN(openOrder.price, 10, 'le')), diff --git a/client/ts/tests/swap.ts b/client/ts/tests/swap.ts index 434ceddc2..2a07dab87 100644 --- a/client/ts/tests/swap.ts +++ b/client/ts/tests/swap.ts @@ -19,7 +19,7 @@ import { placeOrder } from './placeOrder'; import { airdropSol } from '../src/utils/solana'; import { depositGlobal } from './globalDeposit'; import { createGlobal } from './createGlobal'; -import { OrderType } from '../src'; +import { OrderType } from '../src/manifest/types'; import { NO_EXPIRATION_LAST_VALID_SLOT } from '../src/constants'; async function testSwap(): Promise { diff --git a/client/ts/tests/uiWrapper.ts b/client/ts/tests/uiWrapper.ts index fa75cafae..1fbbf729d 100644 --- a/client/ts/tests/uiWrapper.ts +++ b/client/ts/tests/uiWrapper.ts @@ -9,7 +9,7 @@ import { Market } from '../src/market'; import { createMarket } from './createMarket'; import { assert } from 'chai'; import { createGlobalCreateInstruction } from '../src/manifest'; -import { UiWrapper, OpenOrder } from '../src/uiWrapperObj'; +import { UiWrapper, UiWrapperOpenOrder } from '../src/uiWrapperObj'; import { TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotentInstruction, @@ -45,7 +45,6 @@ async function testWrapper(): Promise { { const setup = await UiWrapper.setupIxs( connection, - marketAddress, payerKeypair.publicKey, payerKeypair.publicKey, ); @@ -70,8 +69,8 @@ async function testWrapper(): Promise { buffer: wrapperAcc.account.data, }); assert( - wrapper.marketInfoForMarket(marketAddress)?.orders.length == 0, - 'no orders yet in market', + wrapper.marketInfoForMarket(marketAddress) == null, + 'no seat claimed yet in market', ); { @@ -141,7 +140,7 @@ async function testWrapper(): Promise { const [wrapperOrder] = wrapper.openOrdersForMarket( marketAddress, - ) as OpenOrder[]; + ) as UiWrapperOpenOrder[]; const amount = (wrapperOrder.numBaseAtoms.toString() as any) / 10 ** market.baseDecimals(); const price = diff --git a/client/ts/tests/volume.ts b/client/ts/tests/volume.ts index 99084efb1..df201aa14 100644 --- a/client/ts/tests/volume.ts +++ b/client/ts/tests/volume.ts @@ -6,7 +6,7 @@ import { deposit } from './deposit'; import { Market } from '../src/market'; import { assert } from 'chai'; import { placeOrder } from './placeOrder'; -import { MarketInfoParsed, Wrapper } from '../src'; +import { WrapperMarketInfo, Wrapper } from '../src'; async function testVolume(): Promise { const connection: Connection = new Connection('http://127.0.0.1:8899'); @@ -61,7 +61,7 @@ async function testVolume(): Promise { connection, address: client.wrapper!.address, }); - const marketInfoParsed: MarketInfoParsed = + const marketInfoParsed: WrapperMarketInfo = wrapper.marketInfoForMarket(marketAddress)!; // 2 because self trade. diff --git a/package.json b/package.json index 1e475610c..e25721b81 100644 --- a/package.json +++ b/package.json @@ -22,21 +22,6 @@ "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/types/src/index.d.ts", - "exports": { - ".": { - "require": "./dist/cjs/index.js", - "import": "./dist/esm/index.js" - }, - "./fill-feed": { - "require": "./dist/cjs/fillFeed.js", - "import": "./dist/esm/fillFeed.js" - } - }, - "typesVersions": { - "*": { - "fill-feed": ["./dist/types/src/fillFeed.d.ts"] - } - }, "license": "MIT", "scripts": { "prepublishOnly": "cp README.md README.back.md && cp ./client/ts/README.md README.md", diff --git a/programs/ui-wrapper/Cargo.toml b/programs/ui-wrapper/Cargo.toml index 2a7c7d992..abec6c15c 100644 --- a/programs/ui-wrapper/Cargo.toml +++ b/programs/ui-wrapper/Cargo.toml @@ -14,7 +14,7 @@ name = "ui_wrapper" [features] no-entrypoint = [] cpi = ["no-entrypoint"] -default = [] +default = ["trace"] trace = [ "hypertree/fuzz", "hypertree/trace", "manifest/trace" ] test = [] diff --git a/programs/ui-wrapper/src/error.rs b/programs/ui-wrapper/src/error.rs new file mode 100644 index 000000000..3cdef5d11 --- /dev/null +++ b/programs/ui-wrapper/src/error.rs @@ -0,0 +1,15 @@ +use solana_program::program_error::ProgramError; +use thiserror::Error; + +#[derive(Debug, Error)] +#[repr(u32)] +pub enum ManifestWrapperError { + #[error("Invalid deposit accounts error")] + InvalidDepositAccounts = 0, +} + +impl From for ProgramError { + fn from(e: ManifestWrapperError) -> Self { + ProgramError::Custom(e as u32) + } +} diff --git a/programs/ui-wrapper/src/instruction.rs b/programs/ui-wrapper/src/instruction.rs index e77a630db..830103d61 100644 --- a/programs/ui-wrapper/src/instruction.rs +++ b/programs/ui-wrapper/src/instruction.rs @@ -16,6 +16,7 @@ pub enum ManifestWrapperInstruction { #[account(3, writable, name = "wrapper_state", desc = "Wrapper state")] CreateWrapper = 0, + /// Unused /// Allocate a seat on a given market, this adds a market info to the given /// wrapper. #[account(0, name = "manifest_program", desc = "Manifest program")] @@ -24,7 +25,7 @@ pub enum ManifestWrapperInstruction { #[account(3, name = "system_program", desc = "System program")] #[account(4, writable, signer, name = "payer", desc = "Payer of rent and gas")] #[account(5, writable, name = "wrapper_state", desc = "Wrapper state")] - ClaimSeat = 1, + ClaimSeatUnused = 1, /// Place order, deposits additional funds needed. /// Syncs both balances and open orders on the wrapper. @@ -53,6 +54,28 @@ pub enum ManifestWrapperInstruction { PlaceOrder = 2, /// Edit order, deposits additional funds needed. TODO: Not implemented yet + /// programs/ui-wrapper/src/TODO: document return data + /// TODO: Remove the unneeded global accounts on the bookside that I am placing on. + #[account(0, writable, name = "wrapper_state", desc = "Wrapper state")] + #[account(1, signer, name = "owner", desc = "Owner of the Manifest account")] + #[account(2, writable, name = "trader_token_account", desc = "Trader token account")] + #[account(3, writable, name = "market", desc = "Account holding all market state")] + #[account(4, writable, name = "vault", desc = "Vault PDA, seeds are [b'vault', market_address, mint_address]")] + #[account(5, writable, name = "mint", desc = "Mint of trader token account")] + #[account(6, name = "system_program", desc = "System program")] + #[account(7, name = "token_program", desc = "Token program owning trader token account")] + #[account(8, name = "manifest_program", desc = "Manifest program")] + #[account(9, writable, signer, name = "payer", desc = "Payer of rent and gas")] + #[account(10, name = "base_mint", desc = "Base mint")] + #[account(11, writable, name = "base_global", desc = "Base global account")] + #[account(12, writable, name = "base_global_vault", desc = "Base global vault")] + #[account(13, writable, name = "base_market_vault", desc = "Base market vault")] + #[account(14, writable, name = "base_token_program", desc = "Base token program")] + #[account(15, name = "quote_mint", desc = "Quote mint")] + #[account(16, writable, name = "quote_global", desc = "Quote global account")] + #[account(17, writable, name = "quote_global_vault", desc = "Quote global vault")] + #[account(18, writable, name = "quote_market_vault", desc = "Quote market vault")] + #[account(19, writable, name = "quote_token_program", desc = "Quote token program")] EditOrder = 3, /// Cancel order, no funds are transferred, but token accounts are passed diff --git a/programs/ui-wrapper/src/instruction_builders/mod.rs b/programs/ui-wrapper/src/instruction_builders/mod.rs index ed9919558..520137342 100644 --- a/programs/ui-wrapper/src/instruction_builders/mod.rs +++ b/programs/ui-wrapper/src/instruction_builders/mod.rs @@ -1,5 +1,3 @@ -pub mod claim_seat_instruction; pub mod create_wrapper_instruction; -pub use claim_seat_instruction::*; pub use create_wrapper_instruction::*; diff --git a/programs/ui-wrapper/src/lib.rs b/programs/ui-wrapper/src/lib.rs index e366eb8b5..23a0623a2 100644 --- a/programs/ui-wrapper/src/lib.rs +++ b/programs/ui-wrapper/src/lib.rs @@ -1,6 +1,7 @@ //! UI-Wrapper program for Manifest //! +pub mod error; pub mod instruction; pub mod instruction_builders; pub mod logs; @@ -12,8 +13,8 @@ pub mod wrapper_user; use hypertree::trace; use instruction::ManifestWrapperInstruction; use processors::{ - cancel_order::process_cancel_order, claim_seat::process_claim_seat, - create_wrapper::process_create_wrapper, place_order::process_place_order, + cancel_order::process_cancel_order, create_wrapper::process_create_wrapper, + place_order::process_place_order, settle_funds::process_settle_funds, }; use solana_program::{ @@ -58,8 +59,8 @@ pub fn process_instruction( ManifestWrapperInstruction::CreateWrapper => { process_create_wrapper(program_id, accounts, data)?; } - ManifestWrapperInstruction::ClaimSeat => { - process_claim_seat(program_id, accounts, data)?; + ManifestWrapperInstruction::ClaimSeatUnused => { + unimplemented!("ClaimSeat has been removed and is handled on-demand in PlaceOrder") } ManifestWrapperInstruction::PlaceOrder => { process_place_order(program_id, accounts, data)?; diff --git a/programs/ui-wrapper/src/logs.rs b/programs/ui-wrapper/src/logs.rs index 98c1a8ced..f7efd6304 100644 --- a/programs/ui-wrapper/src/logs.rs +++ b/programs/ui-wrapper/src/logs.rs @@ -8,6 +8,7 @@ use solana_program::pubkey::Pubkey; pub struct PlatformFeeLog { pub market: Pubkey, pub user: Pubkey, + pub mint: Pubkey, pub platform_token_account: Pubkey, pub platform_fee: u64, } @@ -17,6 +18,7 @@ pub struct PlatformFeeLog { pub struct ReferrerFeeLog { pub market: Pubkey, pub user: Pubkey, + pub mint: Pubkey, pub referrer_token_account: Pubkey, pub referrer_fee: u64, } diff --git a/programs/ui-wrapper/src/processors/create_wrapper.rs b/programs/ui-wrapper/src/processors/create_wrapper.rs index 8389cd239..616dc3b5f 100644 --- a/programs/ui-wrapper/src/processors/create_wrapper.rs +++ b/programs/ui-wrapper/src/processors/create_wrapper.rs @@ -25,15 +25,12 @@ pub(crate) fn process_create_wrapper( let wrapper_state: WrapperStateAccountInfo = WrapperStateAccountInfo::new_init(next_account_info(account_iter)?)?; + // Initialize wrapper state. { - // Initialize wrapper state let empty_market_fixed: ManifestWrapperUserFixed = ManifestWrapperUserFixed::new_empty(owner.key); let market_bytes: &mut [u8] = &mut wrapper_state.try_borrow_mut_data()?[..]; *get_mut_helper::(market_bytes, 0_u32) = empty_market_fixed; - - // Drop the reference to wrapper_state so it can be borrowed in expand - // wrapper if needed. } // Expand wrapper so there is an initial block available. diff --git a/programs/ui-wrapper/src/processors/mod.rs b/programs/ui-wrapper/src/processors/mod.rs index 052a6a8f2..ab8c16b01 100644 --- a/programs/ui-wrapper/src/processors/mod.rs +++ b/programs/ui-wrapper/src/processors/mod.rs @@ -1,5 +1,4 @@ pub mod cancel_order; -pub mod claim_seat; pub mod create_wrapper; pub mod place_order; pub mod settle_funds; diff --git a/programs/ui-wrapper/src/processors/place_order.rs b/programs/ui-wrapper/src/processors/place_order.rs index 18c1448ce..84ceb3d52 100644 --- a/programs/ui-wrapper/src/processors/place_order.rs +++ b/programs/ui-wrapper/src/processors/place_order.rs @@ -10,29 +10,41 @@ use hypertree::{ }; use manifest::{ program::{ - batch_update::{BatchUpdateReturn, PlaceOrderParams}, - batch_update_instruction, deposit_instruction, expand_market_instruction, - get_dynamic_account, get_mut_dynamic_account, invoke, + batch_update::{BatchUpdateParams, BatchUpdateReturn, PlaceOrderParams}, + claim_seat_instruction, deposit_instruction, expand_market_instruction, + get_dynamic_account, get_mut_dynamic_account, invoke, ManifestInstruction, }, quantities::{BaseAtoms, QuoteAtoms, QuoteAtomsPerBaseAtom, WrapperU64}, - state::{DynamicAccount, MarketFixed, MarketRef, OrderType, NO_EXPIRATION_LAST_VALID_SLOT}, + require, + state::{claimed_seat::ClaimedSeat, DynamicAccount, MarketFixed, MarketRef, OrderType}, validation::{ManifestAccountInfo, Program, Signer}, }; use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, + instruction::{AccountMeta, Instruction}, program::get_return_data, + program_error::ProgramError, pubkey::Pubkey, system_program, + sysvar::{clock::Clock, Sysvar}, +}; +use spl_token_2022::{ + extension::{ + transfer_fee::TransferFeeConfig, transfer_hook::TransferHook, BaseStateWithExtensions, + StateWithExtensions, + }, + state::Mint, }; use crate::{ - market_info::MarketInfo, open_order::WrapperOpenOrder, wrapper_user::ManifestWrapperUserFixed, + error::ManifestWrapperError::InvalidDepositAccounts, market_info::MarketInfo, + open_order::WrapperOpenOrder, wrapper_user::ManifestWrapperUserFixed, }; use super::shared::{ check_signer, expand_wrapper_if_needed, get_market_info_index_for_market, sync_fast, - OpenOrdersTree, UnusedWrapperFreeListPadding, WrapperStateAccountInfo, + MarketInfosTree, OpenOrdersTree, UnusedWrapperFreeListPadding, WrapperStateAccountInfo, }; #[derive(BorshDeserialize, BorshSerialize, Clone)] @@ -67,6 +79,145 @@ impl WrapperPlaceOrderParams { } } +impl Into for WrapperPlaceOrderParams { + fn into(self) -> PlaceOrderParams { + PlaceOrderParams::new( + self.base_atoms, + self.price_mantissa, + self.price_exponent, + self.is_bid, + self.order_type, + self.last_valid_slot, + ) + } +} + +// Call expand so core has enough free space and owner doesn't get charged +// rent on a subsequent operation. This allows to keep payer and owner +// separate in the case of PDA owners. +fn expand_market_if_needed<'a, 'info>( + market: &ManifestAccountInfo<'a, 'info, MarketFixed>, + payer: &Signer<'a, 'info>, + manifest_program: &Program<'a, 'info>, + system_program: &Program<'a, 'info>, +) -> ProgramResult { + let market_data: Ref<'_, &mut [u8]> = market.try_borrow_data()?; + let dynamic_account: MarketRef = get_dynamic_account(&market_data); + // Check for two free blocks, bc. there needs to be always one free block + // after every operation. + if !dynamic_account.has_two_free_blocks() { + drop(market_data); + invoke( + &expand_market_instruction(market.key, payer.key), + &[ + manifest_program.info.clone(), + payer.info.clone(), + market.info.clone(), + system_program.info.clone(), + ], + )? + } + Ok(()) +} + +fn get_or_create_trader_index<'a, 'info>( + market: &ManifestAccountInfo<'a, 'info, MarketFixed>, + owner: &Signer<'a, 'info>, + payer: &Signer<'a, 'info>, + manifest_program: &Program<'a, 'info>, + system_program: &Program<'a, 'info>, +) -> Result { + let trader_index: DataIndex = { + let market_data: &Ref<&mut [u8]> = &market.try_borrow_data()?; + let dynamic_account: MarketRef = get_dynamic_account(market_data); + dynamic_account.get_trader_index(owner.key) + }; + + if trader_index != NIL { + // If core seat was already initialized, nothing to do here. + Ok(trader_index) + } else { + // Need to intialize a new seat on core. + expand_market_if_needed(market, payer, manifest_program, system_program)?; + invoke( + &claim_seat_instruction(market.key, owner.key), + &[ + manifest_program.info.clone(), + owner.info.clone(), + market.info.clone(), + system_program.info.clone(), + ], + )?; + + // Fetch newly assigned trader index after claiming core seat. + let market_data: &Ref<&mut [u8]> = &mut market.try_borrow_data()?; + let dynamic_account: MarketRef = get_dynamic_account(market_data); + Ok(dynamic_account.get_trader_index(owner.key)) + } +} + +fn get_or_create_market_info<'a, 'info>( + wrapper_state: &WrapperStateAccountInfo<'a, 'info>, + market: &ManifestAccountInfo<'a, 'info, MarketFixed>, + payer: &Signer<'a, 'info>, + system_program: &Program<'a, 'info>, + trader_index: u32, +) -> Result<(MarketInfo, DataIndex), ProgramError> { + let market_info_index: DataIndex = get_market_info_index_for_market(&wrapper_state, market.key); + if market_info_index != NIL { + // Do an initial sync to get all existing orders and balances fresh. This is + // needed for modifying user orders for insufficient funds. + sync_fast(&wrapper_state, &market, market_info_index)?; + + let wrapper_data: Ref<&mut [u8]> = wrapper_state.info.try_borrow_data()?; + let (_fixed_data, wrapper_dynamic_data) = + wrapper_data.split_at(size_of::()); + + let market_info: MarketInfo = + *get_helper::>(wrapper_dynamic_data, market_info_index).get_value(); + + Ok((market_info, market_info_index)) + } else { + // Market info not found, create a new one in wrapper. + expand_wrapper_if_needed(&wrapper_state, &payer, &system_program)?; + + // Load the market_infos tree and insert a new one. + let wrapper_state_info: &AccountInfo = wrapper_state.info; + let mut wrapper_data: RefMut<&mut [u8]> = wrapper_state_info.try_borrow_mut_data()?; + let (fixed_data, wrapper_dynamic_data) = + wrapper_data.split_at_mut(size_of::()); + let wrapper_fixed: &mut ManifestWrapperUserFixed = get_mut_helper(fixed_data, 0); + let mut market_info: MarketInfo = MarketInfo::new_empty(*market.key, trader_index); + market_info.quote_volume = { + // Sync volume from core seat to prevent double billing if seat + // existed before wrapper invocation + let market_data: &Ref<&mut [u8]> = &market.try_borrow_data()?; + let dynamic_account: MarketRef = get_dynamic_account(market_data); + let claimed_seat: &ClaimedSeat = + get_helper::>(dynamic_account.dynamic, trader_index) + .get_value(); + claimed_seat.quote_volume + }; + + // Put that market_info at the free list head. + let mut free_list: FreeList = + FreeList::new(wrapper_dynamic_data, wrapper_fixed.free_list_head_index); + let market_info_index: DataIndex = free_list.remove(); + wrapper_fixed.free_list_head_index = free_list.get_head(); + + // Insert into the MarketInfosTree. + let mut market_infos_tree: MarketInfosTree = MarketInfosTree::new( + wrapper_dynamic_data, + wrapper_fixed.market_infos_root_index, + NIL, + ); + market_infos_tree.insert(market_info_index, market_info); + wrapper_fixed.market_infos_root_index = market_infos_tree.get_root_index(); + + Ok((market_info, market_info_index)) + } +} + pub(crate) fn process_place_order( _program_id: &Pubkey, accounts: &[AccountInfo], @@ -88,65 +239,80 @@ pub(crate) fn process_place_order( Program::new(next_account_info(account_iter)?, &manifest::id())?; let payer: Signer = Signer::new(next_account_info(account_iter)?)?; - let base_mint: &AccountInfo = next_account_info(account_iter)?; - let base_global: &AccountInfo = next_account_info(account_iter)?; - let base_global_vault: &AccountInfo = next_account_info(account_iter)?; - let base_market_vault: &AccountInfo = next_account_info(account_iter)?; - let base_token_program: &AccountInfo = next_account_info(account_iter)?; - let quote_mint: &AccountInfo = next_account_info(account_iter)?; - let quote_global: &AccountInfo = next_account_info(account_iter)?; - let quote_global_vault: &AccountInfo = next_account_info(account_iter)?; - let quote_market_vault: &AccountInfo = next_account_info(account_iter)?; - let quote_token_program: &AccountInfo = next_account_info(account_iter)?; - - if spl_token_2022::id() == *token_program.key { - unimplemented!("token2022 not yet supported") - } - check_signer(&wrapper_state, owner.key); - let market_info_index: DataIndex = get_market_info_index_for_market(&wrapper_state, market.key); - - let (base_mint_key, quote_mint_key) = { - let market_data: &Ref<&mut [u8]> = &market.try_borrow_data()?; - let dynamic_account: MarketRef = get_dynamic_account(market_data); - let base_mint_key: Pubkey = *dynamic_account.get_base_mint(); - let quote_mint_key: Pubkey = *dynamic_account.get_quote_mint(); - (base_mint_key, quote_mint_key) - }; - - // Do an initial sync to get all existing orders and balances fresh. This is - // needed for modifying user orders for insufficient funds. - sync_fast(&wrapper_state, &market, market_info_index)?; - let order = WrapperPlaceOrderParams::try_from_slice(data)?; - - let wrapper_data: Ref<&mut [u8]> = wrapper_state.info.try_borrow_data()?; - let (_fixed_data, wrapper_dynamic_data) = - wrapper_data.split_at(size_of::()); - - let market_info: MarketInfo = - *get_helper::>(wrapper_dynamic_data, market_info_index).get_value(); + // Ensure ClaimedSeat in core and MarketInfo in wrapper are allocated. + // Syncs MarketInfo from ClaimedSeat to calculate required deposits. + let trader_index = + get_or_create_trader_index(&market, &owner, &payer, &manifest_program, &system_program)?; + let (market_info, market_info_index) = get_or_create_market_info( + &wrapper_state, + &market, + &payer, + &system_program, + trader_index, + )?; let remaining_base_atoms: BaseAtoms = market_info.base_balance; let remaining_quote_atoms: QuoteAtoms = market_info.quote_balance; - let trader_index: DataIndex = market_info.trader_index; - drop(wrapper_data); + let order = WrapperPlaceOrderParams::try_from_slice(data)?; let base_atoms = BaseAtoms::new(order.base_atoms); let price = QuoteAtomsPerBaseAtom::try_from_mantissa_and_exponent( order.price_mantissa, order.price_exponent, )?; - let deposit_amount_atoms: u64 = if order.is_bid { + let missing_amount_atoms: u64 = if order.is_bid { + // Core CPI verifies token account / vault consistency with mint. + require!( + mint.key.eq(market.get_fixed()?.get_quote_mint()), + InvalidDepositAccounts, + "expected market.quote_mint as deposit mint" + )?; let required_quote_atoms = base_atoms.checked_mul(price, true)?; required_quote_atoms .saturating_sub(remaining_quote_atoms) .as_u64() } else { + // Core CPI verifies token account / vault consistency with mint. + require!( + mint.key.eq(market.get_fixed()?.get_base_mint()), + InvalidDepositAccounts, + "expected market.base_mint as deposit mint" + )?; base_atoms.saturating_sub(remaining_base_atoms).as_u64() }; - trace!("deposit amount:{deposit_amount_atoms} mint:{:?}", mint.key); + // Adjust deposited amount for TransferFee if possible. + let deposit_amount_atoms = if *mint.owner == spl_token_2022::id() { + let mint_data: Ref<'_, &mut [u8]> = mint.data.borrow(); + let deposit_mint: StateWithExtensions<'_, Mint> = + StateWithExtensions::::unpack(&mint_data)?; + + if let Ok(extension) = deposit_mint.get_extension::() { + if !extension.program_id.0.eq(&Pubkey::default()) { + solana_program::msg!( + "Warning, you are placing an order while using TransferHook. There is no accurate way to estimate deposits required for this trade. You might need to manually deposit using the core instruction before placing orders." + ); + } + } + + if let Ok(extension) = deposit_mint.get_extension::() { + let epoch_fee = extension.get_epoch_fee(Clock::get()?.epoch); + epoch_fee + .calculate_pre_fee_amount(missing_amount_atoms) + .unwrap() + } else { + missing_amount_atoms + } + } else { + missing_amount_atoms + }; + + trace!( + "deposit amount:{deposit_amount_atoms} to cover missing: {missing_amount_atoms} mint:{:?}", + mint.key + ); if deposit_amount_atoms > 0 { invoke( &deposit_instruction( @@ -170,70 +336,49 @@ pub(crate) fn process_place_order( )?; } - // Call expand so claim seat has enough free space and owner doesn't get - // charged rent. This is done here to keep payer and owner separate in the - // case of PDA owners. There is always one free block, this checks if there - // will be an extra one after we place an order. + expand_market_if_needed(&market, &payer, &manifest_program, &system_program)?; + + // Call batch update and pass unparsed accounts without verifying them { - let market_data: Ref<'_, &mut [u8]> = market.try_borrow_data()?; - let dynamic_account: MarketRef = get_dynamic_account(&market_data); - if dynamic_account.has_two_free_blocks() { - invoke( - &expand_market_instruction(market.key, payer.key), - &[ - manifest_program.info.clone(), - payer.info.clone(), - market.info.clone(), - system_program.info.clone(), - ], - )?; - } - } + let core_place: PlaceOrderParams = order.clone().into(); + trace!("cpi place {core_place:?}"); - let core_place: PlaceOrderParams = PlaceOrderParams::new( - order.base_atoms, - order.price_mantissa, - order.price_exponent, - order.is_bid, - order.order_type, - NO_EXPIRATION_LAST_VALID_SLOT, - ); + let mut account_metas = Vec::with_capacity(13); + account_metas.extend_from_slice(&[ + AccountMeta::new(*owner.key, true), + AccountMeta::new(*market.key, false), + AccountMeta::new_readonly(system_program::id(), false), + ]); + account_metas.extend(accounts[10..].iter().map(|ai| { + if ai.is_writable { + AccountMeta::new(*ai.key, ai.is_signer) + } else { + AccountMeta::new_readonly(*ai.key, ai.is_signer) + } + })); + + let ix: Instruction = Instruction { + program_id: manifest::id(), + accounts: account_metas, + data: [ + ManifestInstruction::BatchUpdate.to_vec(), + BatchUpdateParams::new(Some(trader_index), vec![], vec![core_place]) + .try_to_vec()?, + ] + .concat(), + }; - trace!("cpi place {core_place:?}"); - - invoke( - &batch_update_instruction( - market.key, - owner.key, - Some(trader_index), - vec![], - vec![core_place], - Some(base_mint_key), - None, - Some(quote_mint_key), - None, - ), - &[ + let mut account_infos = Vec::with_capacity(18); + account_infos.extend_from_slice(&[ system_program.info.clone(), manifest_program.info.clone(), owner.info.clone(), market.info.clone(), - trader_token_account.clone(), - vault.clone(), - token_program.clone(), - mint.clone(), - base_mint.clone(), - base_global.clone(), - base_global_vault.clone(), - base_market_vault.clone(), - base_token_program.clone(), - quote_mint.clone(), - quote_global.clone(), - quote_global_vault.clone(), - quote_market_vault.clone(), - quote_token_program.clone(), - ], - )?; + ]); + account_infos.extend_from_slice(&accounts[10..]); + + invoke(&ix, &account_infos)?; + } // Process the order result @@ -295,3 +440,34 @@ pub(crate) fn process_place_order( Ok(()) } + +#[cfg(test)] +mod tests { + use super::WrapperPlaceOrderParams; + use manifest::{ + program::batch_update::PlaceOrderParams, + quantities::{BaseAtoms, QuoteAtoms, WrapperU64}, + state::OrderType, + }; + + #[test] + fn test_pass_order_params_to_core() { + let wrapper_order = + WrapperPlaceOrderParams::new(1, 2, 3, 4, true, 5, OrderType::ImmediateOrCancel); + assert_eq!(wrapper_order.client_order_id, 1); + + let core_order: PlaceOrderParams = wrapper_order.into(); + assert_eq!(core_order.base_atoms(), 2); + assert_eq!( + core_order + .try_price() + .unwrap() + .checked_quote_for_base(BaseAtoms::new(1), false) + .unwrap(), + QuoteAtoms::new(30_000) + ); + assert_eq!(core_order.is_bid(), true); + assert_eq!(core_order.order_type(), OrderType::ImmediateOrCancel); + assert_eq!(core_order.last_valid_slot(), 5); + } +} diff --git a/programs/ui-wrapper/src/processors/settle_funds.rs b/programs/ui-wrapper/src/processors/settle_funds.rs index 1832f8292..22d947ce9 100644 --- a/programs/ui-wrapper/src/processors/settle_funds.rs +++ b/programs/ui-wrapper/src/processors/settle_funds.rs @@ -1,12 +1,12 @@ -use std::cell::RefMut; +use std::cell::{Ref, RefMut}; use borsh::{BorshDeserialize, BorshSerialize}; -use hypertree::{get_mut_helper, DataIndex, RBNode}; +use hypertree::{get_mut_helper, trace, DataIndex, RBNode}; use manifest::{ logs::emit_stack, - program::{get_mut_dynamic_account, invoke, withdraw_instruction}, + program::{get_dynamic_account, get_mut_dynamic_account, invoke, withdraw_instruction}, quantities::{QuoteAtoms, WrapperU64}, - state::{DynamicAccount, MarketFixed}, + state::{DynamicAccount, MarketFixed, MarketRef}, validation::{ManifestAccountInfo, Program, Signer}, }; use solana_program::{ @@ -70,8 +70,7 @@ pub(crate) fn process_settle_funds( check_signer(&wrapper_state, owner.key); let market_info_index: DataIndex = get_market_info_index_for_market(&wrapper_state, market.key); - // Do an initial sync to get all existing orders and balances fresh. This is - // needed for modifying user orders for insufficient funds. + // Do an initial sync to update withdrawable balances and volume traded for fee calculation. sync_fast(&wrapper_state, &market, market_info_index)?; let mut wrapper_data: RefMut<&mut [u8]> = wrapper_state.info.try_borrow_mut_data()?; @@ -114,7 +113,15 @@ pub(crate) fn process_settle_funds( drop(wrapper_data); - // settle base + let quote_mint_decimals = { + let market_data: Ref<&mut [u8]> = market.try_borrow_data()?; + let dynamic_account: MarketRef = get_dynamic_account(&market_data); + dynamic_account.fixed.get_quote_mint_decimals() + }; + + trace!("base_balance:{base_balance:?} quote_balance:{quote_balance:?} fee_atoms:{fee_atoms} quote_volume_paid:{quote_volume_paid:?}"); + + // Settle withdrawable base tokens. invoke( &withdraw_instruction( market.key, @@ -136,7 +143,7 @@ pub(crate) fn process_settle_funds( ], )?; - // settle quote + // Settle withdrawable quote tokens. invoke( &withdraw_instruction( market.key, @@ -158,21 +165,39 @@ pub(crate) fn process_settle_funds( ], )?; - // pay fees - if *vault_quote.owner == spl_token_2022::id() { - unimplemented!("token2022 not yet supported") - // TODO: make sure to use least amount of transfers possible to avoid transfer fee + // limits: + // fee_atoms = [0..u64::MAX] + // platform_fee_atoms = [0..fee_atoms] + // intermediate results can extend above u64 + let platform_fee_atoms = if referrer_token_account.is_ok() { + (fee_atoms * platform_fee_percent.min(100) as u128 / 100) as u64 } else { - // limits: - // fee_atoms = [0..u64::MAX] - // platform_fee_atoms = [0..fee_atoms] - // intermediate results can extend above u64 - let platform_fee_atoms = if referrer_token_account.is_ok() { - (fee_atoms * platform_fee_percent.min(100) as u128 / 100) as u64 - } else { - fee_atoms as u64 - }; + fee_atoms as u64 + }; + + trace!("platform_fee_atoms:{platform_fee_atoms}"); + if *token_program_quote.key == spl_token_2022::id() { + invoke( + &spl_token_2022::instruction::transfer_checked( + token_program_quote.key, + trader_token_account_quote.key, + mint_quote.key, + platform_token_account.key, + owner.key, + &[], + platform_fee_atoms, + quote_mint_decimals, + )?, + &[ + token_program_quote.as_ref().clone(), + trader_token_account_quote.as_ref().clone(), + mint_quote.as_ref().clone(), + platform_token_account.as_ref().clone(), + owner.as_ref().clone(), + ], + )?; + } else { invoke( &spl_token::instruction::transfer( token_program_quote.key, @@ -189,18 +214,41 @@ pub(crate) fn process_settle_funds( owner.info.clone(), ], )?; + } - emit_stack(PlatformFeeLog { - market: *market.key, - user: *owner.key, - platform_token_account: *platform_token_account.key, - platform_fee: platform_fee_atoms, - })?; + emit_stack(PlatformFeeLog { + market: *market.key, + user: *owner.key, + mint: *mint_quote.key, + platform_token_account: *platform_token_account.key, + platform_fee: platform_fee_atoms, + })?; - if let Ok(referrer_token_account) = referrer_token_account { - // saturating_sub not needed, but doesn't hurt - let referrer_fee_atoms = (fee_atoms as u64).saturating_sub(platform_fee_atoms); + if let Ok(referrer_token_account) = referrer_token_account { + // saturating_sub not needed, but doesn't hurt + let referrer_fee_atoms = (fee_atoms as u64).saturating_sub(platform_fee_atoms); + if *token_program_quote.key == spl_token_2022::id() { + invoke( + &spl_token_2022::instruction::transfer_checked( + token_program_quote.key, + trader_token_account_quote.key, + mint_quote.key, + referrer_token_account.key, + owner.key, + &[], + referrer_fee_atoms, + quote_mint_decimals, + )?, + &[ + token_program_quote.as_ref().clone(), + trader_token_account_quote.as_ref().clone(), + mint_quote.as_ref().clone(), + referrer_token_account.as_ref().clone(), + owner.as_ref().clone(), + ], + )?; + } else { invoke( &spl_token::instruction::transfer( token_program_quote.key, @@ -217,17 +265,18 @@ pub(crate) fn process_settle_funds( owner.info.clone(), ], )?; - - emit_stack(ReferrerFeeLog { - market: *market.key, - user: *owner.key, - referrer_token_account: *referrer_token_account.key, - referrer_fee: referrer_fee_atoms, - })?; } + + emit_stack(ReferrerFeeLog { + market: *market.key, + user: *owner.key, + mint: *mint_quote.key, + referrer_token_account: *referrer_token_account.key, + referrer_fee: referrer_fee_atoms, + })?; } - // Sync to get the balance correct and remove any expired orders. + // Sync to update the remaining balances post settlement. sync_fast(&wrapper_state, &market, market_info_index)?; Ok(()) diff --git a/programs/ui-wrapper/src/processors/shared.rs b/programs/ui-wrapper/src/processors/shared.rs index 87d60efa4..ef58d7791 100644 --- a/programs/ui-wrapper/src/processors/shared.rs +++ b/programs/ui-wrapper/src/processors/shared.rs @@ -40,8 +40,8 @@ pub const EXPECTED_ORDER_BATCH_SIZE: usize = 16; #[repr(C, packed)] #[derive(Default, Copy, Clone, Pod, Zeroable)] pub struct UnusedWrapperFreeListPadding { - _padding: [u64; 9], - _padding2: [u32; 5], + _padding: [u64; 11], + _padding2: [u32; 1], } pub const FREE_LIST_HEADER_SIZE: usize = 4; // Assert that the free list blocks take up the same size as regular blocks. diff --git a/programs/ui-wrapper/tests/cases/mod.rs b/programs/ui-wrapper/tests/cases/mod.rs index 7ef9f0394..c0b97a4cc 100644 --- a/programs/ui-wrapper/tests/cases/mod.rs +++ b/programs/ui-wrapper/tests/cases/mod.rs @@ -1,2 +1 @@ -pub mod claim_seat; pub mod place_order; diff --git a/programs/ui-wrapper/tests/cases/place_order.rs b/programs/ui-wrapper/tests/cases/place_order.rs index 6fd2c7a50..c690f8b8c 100644 --- a/programs/ui-wrapper/tests/cases/place_order.rs +++ b/programs/ui-wrapper/tests/cases/place_order.rs @@ -13,13 +13,14 @@ use solana_program::{instruction::AccountMeta, system_program}; use solana_program_test::tokio; use solana_sdk::{ account::Account, instruction::Instruction, program_pack::Pack, pubkey::Pubkey, - signature::Keypair, signer::Signer, + signature::Keypair, signer::Signer, system_instruction::transfer, }; use spl_token; +use spl_token_2022::extension::StateWithExtensions; use ui_wrapper::{ self, instruction::ManifestWrapperInstruction, - instruction_builders::{claim_seat_instruction, create_wrapper_instructions}, + instruction_builders::create_wrapper_instructions, market_info::MarketInfo, open_order::WrapperOpenOrder, processors::{ @@ -37,7 +38,6 @@ use crate::{ #[tokio::test] async fn wrapper_place_order_test() -> anyhow::Result<()> { let mut test_fixture: TestFixture = TestFixture::new().await; - test_fixture.claim_seat().await?; let payer: Pubkey = test_fixture.payer(); let payer_keypair: Keypair = test_fixture.payer_keypair().insecure_clone(); @@ -303,21 +303,6 @@ async fn wrapper_place_order_with_broke_owner_test() -> anyhow::Result<()> { .await .unwrap(); - let claim_seat_ix: Instruction = claim_seat_instruction( - &test_fixture.market.key, - &payer, - &owner, - &wrapper_keypair.pubkey(), - ); - - send_tx_with_retry( - Rc::clone(&test_fixture.context), - &[claim_seat_ix], - Some(&payer), - &[&payer_keypair, &owner_keypair], - ) - .await - .unwrap(); WrapperFixture::new(Rc::clone(&test_fixture.context), wrapper_keypair.pubkey()).await }; @@ -328,6 +313,25 @@ async fn wrapper_place_order_with_broke_owner_test() -> anyhow::Result<()> { .fund_trader_wallet(&owner_keypair, Token::USDC, 1) .await; + // deplete all gas of owner + { + let owner_balance = test_fixture + .context + .borrow_mut() + .banks_client + .get_balance(owner) + .await?; + let ix = transfer(&owner, &payer, owner_balance); + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[ix], + Some(&payer), + &[&payer_keypair, &owner_keypair], + ) + .await + .unwrap(); + } + let platform_token_account = test_fixture.fund_token_account("e_mint, &payer).await; let referred_token_account = test_fixture.fund_token_account("e_mint, &payer).await; let (base_vault, _) = get_vault_address(&test_fixture.market.key, &base_mint); @@ -552,105 +556,50 @@ async fn wrapper_place_order_with_broke_owner_test() -> anyhow::Result<()> { } #[tokio::test] -async fn wrapper_fill_order_test() -> anyhow::Result<()> { +async fn wrapper_place_order_without_globals_test() -> anyhow::Result<()> { let mut test_fixture: TestFixture = TestFixture::new().await; - test_fixture.claim_seat().await?; - - let taker: Pubkey = test_fixture.payer(); - let taker_keypair: Keypair = test_fixture.payer_keypair().insecure_clone(); - let mut taker_wrapper_fixture: WrapperFixture = test_fixture.wrapper.clone(); - - let maker: Pubkey = test_fixture.second_keypair.pubkey(); - let maker_keypair: Keypair = test_fixture.second_keypair.insecure_clone(); - - // setup wrapper for maker - let mut maker_wrapper_fixture: WrapperFixture = { - let wrapper_keypair = Keypair::new(); - - let create_wrapper_ixs: Vec = - create_wrapper_instructions(&maker, &maker, &wrapper_keypair.pubkey())?; - - send_tx_with_retry( - Rc::clone(&test_fixture.context), - &create_wrapper_ixs, - Some(&maker), - &[&maker_keypair, &wrapper_keypair], - ) - .await?; - - let claim_seat_ix: Instruction = claim_seat_instruction( - &test_fixture.market.key, - &maker, - &maker, - &wrapper_keypair.pubkey(), - ); - - send_tx_with_retry( - Rc::clone(&test_fixture.context), - &[claim_seat_ix], - Some(&maker), - &[&maker_keypair], - ) - .await?; - WrapperFixture::new(Rc::clone(&test_fixture.context), wrapper_keypair.pubkey()).await - }; - // setup token accounts for taker, maker, platform & referrer - let (_, taker_token_account_base) = test_fixture - .fund_trader_wallet(&taker_keypair, Token::SOL, 1 * SOL_UNIT_SIZE) + let payer: Pubkey = test_fixture.payer(); + let payer_keypair: Keypair = test_fixture.payer_keypair().insecure_clone(); + let (base_mint, trader_token_account_base) = test_fixture + .fund_trader_wallet(&payer_keypair, Token::SOL, 1) .await; - let (_, taker_token_account_quote) = test_fixture - .fund_trader_wallet(&taker_keypair, Token::USDC, 1 * USDC_UNIT_SIZE) + let (quote_mint, trader_token_account_quote) = test_fixture + .fund_trader_wallet(&payer_keypair, Token::USDC, 1) .await; - let (base_mint, maker_token_account_base) = test_fixture - .fund_trader_wallet(&maker_keypair, Token::SOL, 1 * SOL_UNIT_SIZE) - .await; - let (quote_mint, maker_token_account_quote) = test_fixture - .fund_trader_wallet(&maker_keypair, Token::USDC, 1 * USDC_UNIT_SIZE) - .await; - let platform_token_account = test_fixture.fund_token_account("e_mint, &taker).await; - let referred_token_account = test_fixture.fund_token_account("e_mint, &taker).await; + let platform_token_account = test_fixture.fund_token_account("e_mint, &payer).await; + let referred_token_account = test_fixture.fund_token_account("e_mint, &payer).await; - let (base_vault, _) = get_vault_address(&test_fixture.market.key, &base_mint); let (quote_vault, _) = get_vault_address(&test_fixture.market.key, "e_mint); + let (base_vault, _) = get_vault_address(&test_fixture.market.key, &base_mint); let (global_base, _) = get_global_address(&base_mint); let (global_quote, _) = get_global_address("e_mint); let (global_base_vault, _) = get_global_vault_address(&base_mint); let (global_quote_vault, _) = get_global_vault_address("e_mint); - // maker buys 1 sol @ 1000 USDC - let maker_order_ix = Instruction { + // place order + let place_order_ix = Instruction { program_id: ui_wrapper::id(), accounts: vec![ - AccountMeta::new(maker_wrapper_fixture.key, false), - AccountMeta::new(maker, true), - AccountMeta::new(maker_token_account_quote, false), + AccountMeta::new(test_fixture.wrapper.key, false), + AccountMeta::new(payer, true), + AccountMeta::new(trader_token_account_quote, false), AccountMeta::new(test_fixture.market.key, false), AccountMeta::new(quote_vault, false), AccountMeta::new_readonly(quote_mint, false), AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(spl_token::id(), false), AccountMeta::new_readonly(manifest::id(), false), - AccountMeta::new(maker, true), - AccountMeta::new_readonly(base_mint, false), - AccountMeta::new(global_base, false), - AccountMeta::new(global_base_vault, false), - AccountMeta::new(base_vault, false), - AccountMeta::new_readonly(spl_token::id(), false), - AccountMeta::new_readonly(quote_mint, false), - AccountMeta::new(global_quote, false), - AccountMeta::new(global_quote_vault, false), - AccountMeta::new(quote_vault, false), - AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new(payer, true), ], data: [ ManifestWrapperInstruction::PlaceOrder.to_vec(), WrapperPlaceOrderParams::new( 1, - 1 * SOL_UNIT_SIZE, 1, - -3, + 1, + 0, true, NO_EXPIRATION_LAST_VALID_SLOT, OrderType::Limit, @@ -662,32 +611,32 @@ async fn wrapper_fill_order_test() -> anyhow::Result<()> { }; send_tx_with_retry( Rc::clone(&test_fixture.context), - &[maker_order_ix], - Some(&maker), - &[&maker_keypair], + &[place_order_ix], + Some(&payer), + &[&payer_keypair], ) .await?; // verify order is on book test_fixture.market.reload().await; - let maker_index = test_fixture.market.market.get_trader_index(&maker); + let trader_index = test_fixture.market.market.get_trader_index(&payer); let bids = test_fixture.market.market.get_bids(); let found: Option<(DataIndex, &RestingOrder)> = bids .iter::() - .find(|(_, o)| o.get_trader_index() == maker_index); + .find(|(_, o)| o.get_trader_index() == trader_index); assert!(found.is_some()); let (core_index, order) = found.unwrap(); assert_eq!(order.get_is_bid(), true); - assert_eq!(order.get_num_base_atoms(), 1 * SOL_UNIT_SIZE); + assert_eq!(order.get_num_base_atoms(), 1); // verify order is correctly tracked on wrapper - maker_wrapper_fixture.reload().await; + test_fixture.wrapper.reload().await; let open_order: WrapperOpenOrder = { let market_infos_tree: MarketInfosTreeReadOnly = MarketInfosTreeReadOnly::new( - &maker_wrapper_fixture.wrapper.dynamic, - maker_wrapper_fixture.wrapper.fixed.market_infos_root_index, + &test_fixture.wrapper.wrapper.dynamic, + test_fixture.wrapper.wrapper.fixed.market_infos_root_index, NIL, ); @@ -695,13 +644,13 @@ async fn wrapper_fill_order_test() -> anyhow::Result<()> { market_infos_tree.lookup_index(&MarketInfo::new_empty(test_fixture.market.key, NIL)); let market_info: &MarketInfo = get_helper::>( - &maker_wrapper_fixture.wrapper.dynamic, + &test_fixture.wrapper.wrapper.dynamic, market_info_index, ) .get_value(); get_helper::>( - &maker_wrapper_fixture.wrapper.dynamic, + &test_fixture.wrapper.wrapper.dynamic, market_info.orders_root_index, ) .get_value() @@ -710,98 +659,91 @@ async fn wrapper_fill_order_test() -> anyhow::Result<()> { assert_eq!(open_order.get_is_bid(), true); assert_eq!(open_order.get_client_order_id(), 1); - assert_eq!(open_order.get_num_base_atoms(), SOL_UNIT_SIZE); + assert_eq!(open_order.get_num_base_atoms(), 1); assert_eq!( open_order.get_price(), - QuoteAtomsPerBaseAtom::try_from_mantissa_and_exponent(1, -3).unwrap() + QuoteAtomsPerBaseAtom::try_from_mantissa_and_exponent(1, 0).unwrap() ); assert_eq!(open_order.get_market_data_index(), core_index); - // taker buys 1 sol @ 1000 USDC - let taker_order_ix = Instruction { + // cancel the same order + + let cancel_order_ix = Instruction { program_id: ui_wrapper::id(), accounts: vec![ - AccountMeta::new(taker_wrapper_fixture.key, false), - AccountMeta::new(taker, true), - AccountMeta::new(taker_token_account_base, false), + AccountMeta::new(test_fixture.wrapper.key, false), + AccountMeta::new(payer, true), + AccountMeta::new(trader_token_account_quote, false), AccountMeta::new(test_fixture.market.key, false), - AccountMeta::new(base_vault, false), - AccountMeta::new_readonly(base_mint, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(quote_mint, false), AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(spl_token::id(), false), AccountMeta::new_readonly(manifest::id(), false), - AccountMeta::new(taker, true), - AccountMeta::new_readonly(base_mint, false), - AccountMeta::new(global_base, false), - AccountMeta::new(global_base_vault, false), - AccountMeta::new(base_vault, false), - AccountMeta::new_readonly(spl_token::id(), false), - AccountMeta::new_readonly(quote_mint, false), - AccountMeta::new(global_quote, false), - AccountMeta::new(global_quote_vault, false), - AccountMeta::new(quote_vault, false), - AccountMeta::new_readonly(spl_token::id(), false), ], data: [ - ManifestWrapperInstruction::PlaceOrder.to_vec(), - WrapperPlaceOrderParams::new( - 1, - 1 * SOL_UNIT_SIZE, - 1, - -3, - false, - NO_EXPIRATION_LAST_VALID_SLOT, - OrderType::Limit, - ) - .try_to_vec() - .unwrap(), + ManifestWrapperInstruction::CancelOrder.to_vec(), + WrapperCancelOrderParams::new(1).try_to_vec().unwrap(), ] .concat(), }; send_tx_with_retry( Rc::clone(&test_fixture.context), - &[taker_order_ix], - Some(&taker), - &[&taker_keypair], + &[cancel_order_ix], + Some(&payer), + &[&payer_keypair], ) .await?; - // verify book is cleared + // verify order is no longer on book test_fixture.market.reload().await; - let asks = test_fixture.market.market.get_asks(); - assert_eq!(asks.iter::().next(), None); + let trader_index = test_fixture.market.market.get_trader_index(&payer); let bids = test_fixture.market.market.get_bids(); - assert_eq!(bids.iter::().next(), None); + let found: Option<(DataIndex, &RestingOrder)> = bids + .iter::() + .find(|(_, o)| o.get_trader_index() == trader_index); + assert!(found.is_none()); - // verify order is correctly not-tracked on wrapper - taker_wrapper_fixture.reload().await; - { + // verify order is no longer tracked on wrapper + test_fixture.wrapper.reload().await; + + let market_info_index: DataIndex = { let market_infos_tree: MarketInfosTreeReadOnly = MarketInfosTreeReadOnly::new( - &taker_wrapper_fixture.wrapper.dynamic, - taker_wrapper_fixture.wrapper.fixed.market_infos_root_index, + &test_fixture.wrapper.wrapper.dynamic, + test_fixture.wrapper.wrapper.fixed.market_infos_root_index, NIL, ); + market_infos_tree.lookup_index(&MarketInfo::new_empty(test_fixture.market.key, NIL)) + }; - let market_info_index: DataIndex = - market_infos_tree.lookup_index(&MarketInfo::new_empty(test_fixture.market.key, NIL)); - + let orders_root_index = { let market_info: &MarketInfo = get_helper::>( - &taker_wrapper_fixture.wrapper.dynamic, + &test_fixture.wrapper.wrapper.dynamic, market_info_index, ) .get_value(); - assert_eq!(market_info.orders_root_index, NIL); + market_info.orders_root_index }; - // settle & pay fees - let settle_taker_ix = Instruction { + let open_orders_tree: OpenOrdersTreeReadOnly = OpenOrdersTreeReadOnly::new( + &test_fixture.wrapper.wrapper.dynamic, + orders_root_index, + NIL, + ); + let found = open_orders_tree + .iter::() + .find(|(_, o)| o.get_client_order_id() == 1); + assert!(found.is_none()); + + // release funds + let settle_funds_ix = Instruction { program_id: ui_wrapper::id(), accounts: vec![ - AccountMeta::new(taker_wrapper_fixture.key, false), - AccountMeta::new(taker, true), - AccountMeta::new(taker_token_account_base, false), - AccountMeta::new(taker_token_account_quote, false), + AccountMeta::new(test_fixture.wrapper.key, false), + AccountMeta::new(payer, true), + AccountMeta::new(trader_token_account_base, false), + AccountMeta::new(trader_token_account_quote, false), AccountMeta::new(test_fixture.market.key, false), AccountMeta::new(base_vault, false), AccountMeta::new(quote_vault, false), @@ -815,7 +757,7 @@ async fn wrapper_fill_order_test() -> anyhow::Result<()> { ], data: [ ManifestWrapperInstruction::SettleFunds.to_vec(), - WrapperSettleFundsParams::new(500_000_000, 50) + WrapperSettleFundsParams::new(1_000_000_000, 50) .try_to_vec() .unwrap(), ] @@ -823,66 +765,1602 @@ async fn wrapper_fill_order_test() -> anyhow::Result<()> { }; send_tx_with_retry( Rc::clone(&test_fixture.context), - &[settle_taker_ix], - Some(&taker), - &[&taker_keypair], + &[settle_funds_ix], + Some(&payer), + &[&payer_keypair], ) .await?; - // taker sold 1/1 SOL, expect 0 - let taker_token_account_base: Account = test_fixture + // verify no fees were charged and user has deposit back in his wallet + let trader_token_account_quote: Account = test_fixture .context .borrow_mut() .banks_client - .get_account(taker_token_account_base) + .get_account(trader_token_account_quote) .await .unwrap() .unwrap(); - let taker_token_account_base = - spl_token::state::Account::unpack(&taker_token_account_base.data)?; - assert_eq!(taker_token_account_base.amount, 0); + let trader_token_account_quote = + spl_token::state::Account::unpack(&trader_token_account_quote.data)?; + assert_eq!(trader_token_account_quote.amount, 1); - // user has proceeds of trade in his wallet, but 50% fees were charged - let taker_token_account_quote: Account = test_fixture - .context - .borrow_mut() - .banks_client - .get_account(taker_token_account_quote) - .await - .unwrap() - .unwrap(); + Ok(()) +} + +#[tokio::test] +async fn wrapper_place_order_with_mixed_up_mint_ask() -> anyhow::Result<()> { + let mut test_fixture: TestFixture = TestFixture::new().await; + + let payer: Pubkey = test_fixture.payer(); + let payer_keypair: Keypair = test_fixture.payer_keypair().insecure_clone(); + let (base_mint, trader_token_account_base) = test_fixture + .fund_trader_wallet(&payer_keypair, Token::SOL, 1) + .await; + let (quote_mint, trader_token_account_quote) = test_fixture + .fund_trader_wallet(&payer_keypair, Token::USDC, 1) + .await; + + let platform_token_account = test_fixture.fund_token_account("e_mint, &payer).await; + let referred_token_account = test_fixture.fund_token_account("e_mint, &payer).await; + + let (quote_vault, _) = get_vault_address(&test_fixture.market.key, "e_mint); + let (base_vault, _) = get_vault_address(&test_fixture.market.key, &base_mint); + let (global_base, _) = get_global_address(&base_mint); + let (global_quote, _) = get_global_address("e_mint); + let (global_base_vault, _) = get_global_vault_address(&base_mint); + let (global_quote_vault, _) = get_global_vault_address("e_mint); + + // place order as ask, but passing quote currency should fail + let place_order_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(test_fixture.wrapper.key, false), + AccountMeta::new(payer, true), + AccountMeta::new(trader_token_account_quote, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(payer, true), + ], + data: [ + ManifestWrapperInstruction::PlaceOrder.to_vec(), + WrapperPlaceOrderParams::new( + 1, + 1, + 1, + 0, + false, + NO_EXPIRATION_LAST_VALID_SLOT, + OrderType::Limit, + ) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + let result = send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[place_order_ix], + Some(&payer), + &[&payer_keypair], + ) + .await; + + assert!(result.is_err()); + Ok(()) +} + +#[tokio::test] +async fn wrapper_place_order_with_mixed_up_mint_bid() -> anyhow::Result<()> { + let mut test_fixture: TestFixture = TestFixture::new().await; + + let payer: Pubkey = test_fixture.payer(); + let payer_keypair: Keypair = test_fixture.payer_keypair().insecure_clone(); + let (base_mint, trader_token_account_base) = test_fixture + .fund_trader_wallet(&payer_keypair, Token::SOL, 1) + .await; + let (quote_mint, trader_token_account_quote) = test_fixture + .fund_trader_wallet(&payer_keypair, Token::USDC, 1) + .await; + + let platform_token_account = test_fixture.fund_token_account("e_mint, &payer).await; + let referred_token_account = test_fixture.fund_token_account("e_mint, &payer).await; + + let (quote_vault, _) = get_vault_address(&test_fixture.market.key, "e_mint); + let (base_vault, _) = get_vault_address(&test_fixture.market.key, &base_mint); + let (global_base, _) = get_global_address(&base_mint); + let (global_quote, _) = get_global_address("e_mint); + let (global_base_vault, _) = get_global_vault_address(&base_mint); + let (global_quote_vault, _) = get_global_vault_address("e_mint); + + // place order as ask, but passing quote currency should fail + let place_order_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(test_fixture.wrapper.key, false), + AccountMeta::new(payer, true), + AccountMeta::new(trader_token_account_base, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(payer, true), + ], + data: [ + ManifestWrapperInstruction::PlaceOrder.to_vec(), + WrapperPlaceOrderParams::new( + 1, + 1, + 1, + 0, + true, + NO_EXPIRATION_LAST_VALID_SLOT, + OrderType::Limit, + ) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + let result = send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[place_order_ix], + Some(&payer), + &[&payer_keypair], + ) + .await; + + assert!(result.is_err()); + Ok(()) +} + +#[tokio::test] +async fn wrapper_fill_order_test() -> anyhow::Result<()> { + let mut test_fixture: TestFixture = TestFixture::new().await; + + let taker: Pubkey = test_fixture.payer(); + let taker_keypair: Keypair = test_fixture.payer_keypair().insecure_clone(); + let mut taker_wrapper_fixture: WrapperFixture = test_fixture.wrapper.clone(); + + let maker: Pubkey = test_fixture.second_keypair.pubkey(); + let maker_keypair: Keypair = test_fixture.second_keypair.insecure_clone(); + + // setup wrapper for maker + let mut maker_wrapper_fixture: WrapperFixture = { + let wrapper_keypair = Keypair::new(); + + let create_wrapper_ixs: Vec = + create_wrapper_instructions(&maker, &maker, &wrapper_keypair.pubkey())?; + + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &create_wrapper_ixs, + Some(&maker), + &[&maker_keypair, &wrapper_keypair], + ) + .await?; + + WrapperFixture::new(Rc::clone(&test_fixture.context), wrapper_keypair.pubkey()).await + }; + + // setup token accounts for taker, maker, platform & referrer + let (_, taker_token_account_base) = test_fixture + .fund_trader_wallet(&taker_keypair, Token::SOL, 1 * SOL_UNIT_SIZE) + .await; + let (_, taker_token_account_quote) = test_fixture + .fund_trader_wallet(&taker_keypair, Token::USDC, 1 * USDC_UNIT_SIZE) + .await; + + let (base_mint, maker_token_account_base) = test_fixture + .fund_trader_wallet(&maker_keypair, Token::SOL, 1 * SOL_UNIT_SIZE) + .await; + let (quote_mint, maker_token_account_quote) = test_fixture + .fund_trader_wallet(&maker_keypair, Token::USDC, 1 * USDC_UNIT_SIZE) + .await; + let platform_token_account = test_fixture.fund_token_account("e_mint, &taker).await; + let referred_token_account = test_fixture.fund_token_account("e_mint, &taker).await; + + let (base_vault, _) = get_vault_address(&test_fixture.market.key, &base_mint); + let (quote_vault, _) = get_vault_address(&test_fixture.market.key, "e_mint); + let (global_base, _) = get_global_address(&base_mint); + let (global_quote, _) = get_global_address("e_mint); + let (global_base_vault, _) = get_global_vault_address(&base_mint); + let (global_quote_vault, _) = get_global_vault_address("e_mint); + + // maker buys 1 sol @ 1000 USDC + let maker_order_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(maker_wrapper_fixture.key, false), + AccountMeta::new(maker, true), + AccountMeta::new(maker_token_account_quote, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(maker, true), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new(global_base, false), + AccountMeta::new(global_base_vault, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new(global_quote, false), + AccountMeta::new(global_quote_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: [ + ManifestWrapperInstruction::PlaceOrder.to_vec(), + WrapperPlaceOrderParams::new( + 1, + 1 * SOL_UNIT_SIZE, + 1, + -3, + true, + NO_EXPIRATION_LAST_VALID_SLOT, + OrderType::Limit, + ) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[maker_order_ix], + Some(&maker), + &[&maker_keypair], + ) + .await?; + + // verify order is on book + test_fixture.market.reload().await; + let maker_index = test_fixture.market.market.get_trader_index(&maker); + + let bids = test_fixture.market.market.get_bids(); + let found: Option<(DataIndex, &RestingOrder)> = bids + .iter::() + .find(|(_, o)| o.get_trader_index() == maker_index); + assert!(found.is_some()); + let (core_index, order) = found.unwrap(); + assert_eq!(order.get_is_bid(), true); + assert_eq!(order.get_num_base_atoms(), 1 * SOL_UNIT_SIZE); + + // verify order is correctly tracked on wrapper + maker_wrapper_fixture.reload().await; + + let open_order: WrapperOpenOrder = { + let market_infos_tree: MarketInfosTreeReadOnly = MarketInfosTreeReadOnly::new( + &maker_wrapper_fixture.wrapper.dynamic, + maker_wrapper_fixture.wrapper.fixed.market_infos_root_index, + NIL, + ); + + let market_info_index: DataIndex = + market_infos_tree.lookup_index(&MarketInfo::new_empty(test_fixture.market.key, NIL)); + + let market_info: &MarketInfo = get_helper::>( + &maker_wrapper_fixture.wrapper.dynamic, + market_info_index, + ) + .get_value(); + + get_helper::>( + &maker_wrapper_fixture.wrapper.dynamic, + market_info.orders_root_index, + ) + .get_value() + .clone() + }; + + assert_eq!(open_order.get_is_bid(), true); + assert_eq!(open_order.get_client_order_id(), 1); + assert_eq!(open_order.get_num_base_atoms(), SOL_UNIT_SIZE); + assert_eq!( + open_order.get_price(), + QuoteAtomsPerBaseAtom::try_from_mantissa_and_exponent(1, -3).unwrap() + ); + assert_eq!(open_order.get_market_data_index(), core_index); + + // taker buys 1 sol @ 1000 USDC + let taker_order_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(taker_wrapper_fixture.key, false), + AccountMeta::new(taker, true), + AccountMeta::new(taker_token_account_base, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(taker, true), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new(global_base, false), + AccountMeta::new(global_base_vault, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new(global_quote, false), + AccountMeta::new(global_quote_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: [ + ManifestWrapperInstruction::PlaceOrder.to_vec(), + WrapperPlaceOrderParams::new( + 1, + 1 * SOL_UNIT_SIZE, + 1, + -3, + false, + NO_EXPIRATION_LAST_VALID_SLOT, + OrderType::Limit, + ) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[taker_order_ix], + Some(&taker), + &[&taker_keypair], + ) + .await?; + + // verify book is cleared + test_fixture.market.reload().await; + let asks = test_fixture.market.market.get_asks(); + assert_eq!(asks.iter::().next(), None); + let bids = test_fixture.market.market.get_bids(); + assert_eq!(bids.iter::().next(), None); + + // verify order is correctly not-tracked on wrapper + taker_wrapper_fixture.reload().await; + { + let market_infos_tree: MarketInfosTreeReadOnly = MarketInfosTreeReadOnly::new( + &taker_wrapper_fixture.wrapper.dynamic, + taker_wrapper_fixture.wrapper.fixed.market_infos_root_index, + NIL, + ); + + let market_info_index: DataIndex = + market_infos_tree.lookup_index(&MarketInfo::new_empty(test_fixture.market.key, NIL)); + + let market_info: &MarketInfo = get_helper::>( + &taker_wrapper_fixture.wrapper.dynamic, + market_info_index, + ) + .get_value(); + + assert_eq!(market_info.orders_root_index, NIL); + }; + + // settle & pay fees + let settle_taker_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(taker_wrapper_fixture.key, false), + AccountMeta::new(taker, true), + AccountMeta::new(taker_token_account_base, false), + AccountMeta::new(taker_token_account_quote, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(base_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(platform_token_account, false), + ], + data: [ + ManifestWrapperInstruction::SettleFunds.to_vec(), + WrapperSettleFundsParams::new(500_000_000, 50) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[settle_taker_ix], + Some(&taker), + &[&taker_keypair], + ) + .await?; + + // taker sold 1/1 SOL, expect 0 + let taker_token_account_base: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(taker_token_account_base) + .await + .unwrap() + .unwrap(); + + let taker_token_account_base = + spl_token::state::Account::unpack(&taker_token_account_base.data)?; + assert_eq!(taker_token_account_base.amount, 0); + + // user has proceeds of trade in his wallet, but 50% fees were charged + let taker_token_account_quote: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(taker_token_account_quote) + .await + .unwrap() + .unwrap(); + + let taker_token_account_quote = + spl_token::state::Account::unpack(&taker_token_account_quote.data)?; + assert_eq!(taker_token_account_quote.amount, USDC_UNIT_SIZE * 3 / 2); + + // verify the remaining 50% was paid to platform not referrer + let platform_token_account_quote: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(platform_token_account) + .await + .unwrap() + .unwrap(); + + let platform_token_account_quote = + spl_token::state::Account::unpack(&platform_token_account_quote.data)?; + assert_eq!(platform_token_account_quote.amount, USDC_UNIT_SIZE / 2); + + let referred_token_account_quote: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(referred_token_account) + .await + .unwrap() + .unwrap(); + + let referred_token_account_quote = + spl_token::state::Account::unpack(&referred_token_account_quote.data)?; + assert_eq!(referred_token_account_quote.amount, 0); + + let settle_maker_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(maker_wrapper_fixture.key, false), + AccountMeta::new(maker, true), + AccountMeta::new(maker_token_account_base, false), + AccountMeta::new(maker_token_account_quote, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(base_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(platform_token_account, false), + ], + data: [ + ManifestWrapperInstruction::SettleFunds.to_vec(), + WrapperSettleFundsParams::new(500_000_000, 50) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[settle_maker_ix], + Some(&maker), + &[&maker_keypair], + ) + .await + .expect_err("should fail due to lack of USDC balance to pay fees"); + + // maker has 1 SOL & bought 1 SOL, but couldn't settle + let maker_token_account_base: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(maker_token_account_base) + .await + .unwrap() + .unwrap(); + + let maker_token_account_base = + spl_token::state::Account::unpack(&maker_token_account_base.data)?; + assert_eq!(maker_token_account_base.amount, SOL_UNIT_SIZE); + + Ok(()) +} + +#[tokio::test] +async fn wrapper_fill_order_without_referral_test() -> anyhow::Result<()> { + let mut test_fixture: TestFixture = TestFixture::new().await; + + let taker: Pubkey = test_fixture.payer(); + let taker_keypair: Keypair = test_fixture.payer_keypair().insecure_clone(); + let mut taker_wrapper_fixture: WrapperFixture = test_fixture.wrapper.clone(); + + let maker: Pubkey = test_fixture.second_keypair.pubkey(); + let maker_keypair: Keypair = test_fixture.second_keypair.insecure_clone(); + + // setup wrapper for maker + let mut maker_wrapper_fixture: WrapperFixture = { + let wrapper_keypair = Keypair::new(); + + let create_wrapper_ixs: Vec = + create_wrapper_instructions(&maker, &maker, &wrapper_keypair.pubkey())?; + + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &create_wrapper_ixs, + Some(&maker), + &[&maker_keypair, &wrapper_keypair], + ) + .await?; + + WrapperFixture::new(Rc::clone(&test_fixture.context), wrapper_keypair.pubkey()).await + }; + + // setup token accounts for taker, maker, platform & referrer + let (_, taker_token_account_base) = test_fixture + .fund_trader_wallet(&taker_keypair, Token::SOL, 1 * SOL_UNIT_SIZE) + .await; + let (_, taker_token_account_quote) = test_fixture + .fund_trader_wallet(&taker_keypair, Token::USDC, 1 * USDC_UNIT_SIZE) + .await; + + let (base_mint, maker_token_account_base) = test_fixture + .fund_trader_wallet(&maker_keypair, Token::SOL, 1 * SOL_UNIT_SIZE) + .await; + let (quote_mint, maker_token_account_quote) = test_fixture + .fund_trader_wallet(&maker_keypair, Token::USDC, 1 * USDC_UNIT_SIZE) + .await; + let platform_token_account = test_fixture.fund_token_account("e_mint, &taker).await; + let referred_token_account = test_fixture.fund_token_account("e_mint, &taker).await; + + let (base_vault, _) = get_vault_address(&test_fixture.market.key, &base_mint); + let (quote_vault, _) = get_vault_address(&test_fixture.market.key, "e_mint); + let (global_base, _) = get_global_address(&base_mint); + let (global_quote, _) = get_global_address("e_mint); + let (global_base_vault, _) = get_global_vault_address(&base_mint); + let (global_quote_vault, _) = get_global_vault_address("e_mint); + + // maker buys 1 sol @ 1000 USDC + let maker_order_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(maker_wrapper_fixture.key, false), + AccountMeta::new(maker, true), + AccountMeta::new(maker_token_account_quote, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(maker, true), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new(global_base, false), + AccountMeta::new(global_base_vault, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new(global_quote, false), + AccountMeta::new(global_quote_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: [ + ManifestWrapperInstruction::PlaceOrder.to_vec(), + WrapperPlaceOrderParams::new( + 1, + 1 * SOL_UNIT_SIZE, + 1, + -3, + true, + NO_EXPIRATION_LAST_VALID_SLOT, + OrderType::Limit, + ) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[maker_order_ix], + Some(&maker), + &[&maker_keypair], + ) + .await?; + + // verify order is on book + test_fixture.market.reload().await; + let maker_index = test_fixture.market.market.get_trader_index(&maker); + + let bids = test_fixture.market.market.get_bids(); + let found: Option<(DataIndex, &RestingOrder)> = bids + .iter::() + .find(|(_, o)| o.get_trader_index() == maker_index); + assert!(found.is_some()); + let (core_index, order) = found.unwrap(); + assert_eq!(order.get_is_bid(), true); + assert_eq!(order.get_num_base_atoms(), 1 * SOL_UNIT_SIZE); + + // verify order is correctly tracked on wrapper + maker_wrapper_fixture.reload().await; + + let open_order: WrapperOpenOrder = { + let market_infos_tree: MarketInfosTreeReadOnly = MarketInfosTreeReadOnly::new( + &maker_wrapper_fixture.wrapper.dynamic, + maker_wrapper_fixture.wrapper.fixed.market_infos_root_index, + NIL, + ); + + let market_info_index: DataIndex = + market_infos_tree.lookup_index(&MarketInfo::new_empty(test_fixture.market.key, NIL)); + + let market_info: &MarketInfo = get_helper::>( + &maker_wrapper_fixture.wrapper.dynamic, + market_info_index, + ) + .get_value(); + + get_helper::>( + &maker_wrapper_fixture.wrapper.dynamic, + market_info.orders_root_index, + ) + .get_value() + .clone() + }; + + assert_eq!(open_order.get_is_bid(), true); + assert_eq!(open_order.get_client_order_id(), 1); + assert_eq!(open_order.get_num_base_atoms(), SOL_UNIT_SIZE); + assert_eq!( + open_order.get_price(), + QuoteAtomsPerBaseAtom::try_from_mantissa_and_exponent(1, -3).unwrap() + ); + assert_eq!(open_order.get_market_data_index(), core_index); + + // taker buys 1 sol @ 1000 USDC + let taker_order_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(taker_wrapper_fixture.key, false), + AccountMeta::new(taker, true), + AccountMeta::new(taker_token_account_base, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(taker, true), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new(global_base, false), + AccountMeta::new(global_base_vault, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new(global_quote, false), + AccountMeta::new(global_quote_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: [ + ManifestWrapperInstruction::PlaceOrder.to_vec(), + WrapperPlaceOrderParams::new( + 1, + 1 * SOL_UNIT_SIZE, + 1, + -3, + false, + NO_EXPIRATION_LAST_VALID_SLOT, + OrderType::Limit, + ) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[taker_order_ix], + Some(&taker), + &[&taker_keypair], + ) + .await?; + + // verify book is cleared + test_fixture.market.reload().await; + let asks = test_fixture.market.market.get_asks(); + assert_eq!(asks.iter::().next(), None); + let bids = test_fixture.market.market.get_bids(); + assert_eq!(bids.iter::().next(), None); + + // verify order is correctly not-tracked on wrapper + taker_wrapper_fixture.reload().await; + { + let market_infos_tree: MarketInfosTreeReadOnly = MarketInfosTreeReadOnly::new( + &taker_wrapper_fixture.wrapper.dynamic, + taker_wrapper_fixture.wrapper.fixed.market_infos_root_index, + NIL, + ); + + let market_info_index: DataIndex = + market_infos_tree.lookup_index(&MarketInfo::new_empty(test_fixture.market.key, NIL)); + + let market_info: &MarketInfo = get_helper::>( + &taker_wrapper_fixture.wrapper.dynamic, + market_info_index, + ) + .get_value(); + + assert_eq!(market_info.orders_root_index, NIL); + }; + + // settle & pay fees + let settle_taker_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(taker_wrapper_fixture.key, false), + AccountMeta::new(taker, true), + AccountMeta::new(taker_token_account_base, false), + AccountMeta::new(taker_token_account_quote, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(base_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(platform_token_account, false), + AccountMeta::new(referred_token_account, false), + ], + data: [ + ManifestWrapperInstruction::SettleFunds.to_vec(), + WrapperSettleFundsParams::new(500_000_000, 50) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[settle_taker_ix], + Some(&taker), + &[&taker_keypair], + ) + .await?; + + // taker sold 1/1 SOL, expect 0 + let taker_token_account_base: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(taker_token_account_base) + .await + .unwrap() + .unwrap(); + + let taker_token_account_base = + spl_token::state::Account::unpack(&taker_token_account_base.data)?; + assert_eq!(taker_token_account_base.amount, 0); + + // user has proceeds of trade in his wallet, but 50% fees were charged + let taker_token_account_quote: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(taker_token_account_quote) + .await + .unwrap() + .unwrap(); let taker_token_account_quote = spl_token::state::Account::unpack(&taker_token_account_quote.data)?; assert_eq!(taker_token_account_quote.amount, USDC_UNIT_SIZE * 3 / 2); - // verify the remaining 50% was split 50/50 between platform & referrer - let platform_token_account_quote: Account = test_fixture + // verify the remaining 50% was split 50/50 between platform & referrer + let platform_token_account_quote: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(platform_token_account) + .await + .unwrap() + .unwrap(); + + let platform_token_account_quote = + spl_token::state::Account::unpack(&platform_token_account_quote.data)?; + assert_eq!(platform_token_account_quote.amount, USDC_UNIT_SIZE / 4); + + let referred_token_account_quote: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(referred_token_account) + .await + .unwrap() + .unwrap(); + + let referred_token_account_quote = + spl_token::state::Account::unpack(&referred_token_account_quote.data)?; + assert_eq!(referred_token_account_quote.amount, USDC_UNIT_SIZE / 4); + + let settle_maker_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(maker_wrapper_fixture.key, false), + AccountMeta::new(maker, true), + AccountMeta::new(maker_token_account_base, false), + AccountMeta::new(maker_token_account_quote, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(base_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(platform_token_account, false), + AccountMeta::new(referred_token_account, false), + ], + data: [ + ManifestWrapperInstruction::SettleFunds.to_vec(), + WrapperSettleFundsParams::new(500_000_000, 50) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[settle_maker_ix], + Some(&maker), + &[&maker_keypair], + ) + .await + .expect_err("should fail due to lack of USDC balance to pay fees"); + + // maker has 1 SOL & bought 1 SOL, but couldn't settle + let maker_token_account_base: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(maker_token_account_base) + .await + .unwrap() + .unwrap(); + + let maker_token_account_base = + spl_token::state::Account::unpack(&maker_token_account_base.data)?; + assert_eq!(maker_token_account_base.amount, SOL_UNIT_SIZE); + + Ok(()) +} + +#[tokio::test] +async fn wrapper_fill_order_with_transfer_fees_test() -> anyhow::Result<()> { + let mut test_fixture: TestFixture = TestFixture::new_with_extensions(true, true).await; + + let taker: Pubkey = test_fixture.payer(); + let taker_keypair: Keypair = test_fixture.payer_keypair().insecure_clone(); + let mut taker_wrapper_fixture: WrapperFixture = test_fixture.wrapper.clone(); + + let maker: Pubkey = test_fixture.second_keypair.pubkey(); + let maker_keypair: Keypair = test_fixture.second_keypair.insecure_clone(); + + // setup wrapper for maker + let mut maker_wrapper_fixture: WrapperFixture = { + let wrapper_keypair = Keypair::new(); + + let create_wrapper_ixs: Vec = + create_wrapper_instructions(&maker, &maker, &wrapper_keypair.pubkey())?; + + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &create_wrapper_ixs, + Some(&maker), + &[&maker_keypair, &wrapper_keypair], + ) + .await?; + + WrapperFixture::new(Rc::clone(&test_fixture.context), wrapper_keypair.pubkey()).await + }; + + // setup token accounts for taker, maker, platform & referrer + let (_, taker_token_account_base) = test_fixture + .fund_trader_wallet(&taker_keypair, Token::SOL, 1 * SOL_UNIT_SIZE) + .await; + let (_, taker_token_account_quote) = test_fixture + .fund_trader_wallet_2022(&taker_keypair, Token::USDC, 1 * USDC_UNIT_SIZE + 100) + .await; + + let (base_mint, maker_token_account_base) = test_fixture + .fund_trader_wallet(&maker_keypair, Token::SOL, 1 * SOL_UNIT_SIZE) + .await; + let (quote_mint, maker_token_account_quote) = test_fixture + .fund_trader_wallet_2022(&maker_keypair, Token::USDC, 1 * USDC_UNIT_SIZE + 100) + .await; + let platform_token_account = test_fixture + .fund_token_account_2022("e_mint, &taker) + .await; + let referred_token_account = test_fixture + .fund_token_account_2022("e_mint, &taker) + .await; + + let (base_vault, _) = get_vault_address(&test_fixture.market.key, &base_mint); + let (quote_vault, _) = get_vault_address(&test_fixture.market.key, "e_mint); + let (global_base, _) = get_global_address(&base_mint); + let (global_quote, _) = get_global_address("e_mint); + let (global_base_vault, _) = get_global_vault_address(&base_mint); + let (global_quote_vault, _) = get_global_vault_address("e_mint); + + // maker buys 1 sol @ 1000 USDC + let maker_order_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(maker_wrapper_fixture.key, false), + AccountMeta::new(maker, true), + AccountMeta::new(maker_token_account_quote, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(maker, true), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new(global_base, false), + AccountMeta::new(global_base_vault, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new(global_quote, false), + AccountMeta::new(global_quote_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + ], + data: [ + ManifestWrapperInstruction::PlaceOrder.to_vec(), + WrapperPlaceOrderParams::new( + 1, + 1 * SOL_UNIT_SIZE, + 1, + -3, + true, + NO_EXPIRATION_LAST_VALID_SLOT, + OrderType::Limit, + ) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[maker_order_ix], + Some(&maker), + &[&maker_keypair], + ) + .await?; + + // verify order is on book + test_fixture.market.reload().await; + let maker_index = test_fixture.market.market.get_trader_index(&maker); + + let bids = test_fixture.market.market.get_bids(); + let found: Option<(DataIndex, &RestingOrder)> = bids + .iter::() + .find(|(_, o)| o.get_trader_index() == maker_index); + assert!(found.is_some()); + let (core_index, order) = found.unwrap(); + assert_eq!(order.get_is_bid(), true); + assert_eq!(order.get_num_base_atoms(), 1 * SOL_UNIT_SIZE); + + // verify order is correctly tracked on wrapper + maker_wrapper_fixture.reload().await; + + let open_order: WrapperOpenOrder = { + let market_infos_tree: MarketInfosTreeReadOnly = MarketInfosTreeReadOnly::new( + &maker_wrapper_fixture.wrapper.dynamic, + maker_wrapper_fixture.wrapper.fixed.market_infos_root_index, + NIL, + ); + + let market_info_index: DataIndex = + market_infos_tree.lookup_index(&MarketInfo::new_empty(test_fixture.market.key, NIL)); + + let market_info: &MarketInfo = get_helper::>( + &maker_wrapper_fixture.wrapper.dynamic, + market_info_index, + ) + .get_value(); + + get_helper::>( + &maker_wrapper_fixture.wrapper.dynamic, + market_info.orders_root_index, + ) + .get_value() + .clone() + }; + + assert_eq!(open_order.get_is_bid(), true); + assert_eq!(open_order.get_client_order_id(), 1); + assert_eq!(open_order.get_num_base_atoms(), SOL_UNIT_SIZE); + assert_eq!( + open_order.get_price(), + QuoteAtomsPerBaseAtom::try_from_mantissa_and_exponent(1, -3).unwrap() + ); + assert_eq!(open_order.get_market_data_index(), core_index); + + // taker sells 1 sol @ 1000 USDC + let taker_order_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(taker_wrapper_fixture.key, false), + AccountMeta::new(taker, true), + AccountMeta::new(taker_token_account_base, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(taker, true), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new(global_base, false), + AccountMeta::new(global_base_vault, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new(global_quote, false), + AccountMeta::new(global_quote_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + ], + data: [ + ManifestWrapperInstruction::PlaceOrder.to_vec(), + WrapperPlaceOrderParams::new( + 1, + 1 * SOL_UNIT_SIZE, + 1, + -3, + false, + NO_EXPIRATION_LAST_VALID_SLOT, + OrderType::Limit, + ) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[taker_order_ix], + Some(&taker), + &[&taker_keypair], + ) + .await?; + + // verify book is cleared + test_fixture.market.reload().await; + let asks = test_fixture.market.market.get_asks(); + assert_eq!(asks.iter::().next(), None); + let bids = test_fixture.market.market.get_bids(); + assert_eq!(bids.iter::().next(), None); + + // verify order is correctly not-tracked on wrapper + taker_wrapper_fixture.reload().await; + { + let market_infos_tree: MarketInfosTreeReadOnly = MarketInfosTreeReadOnly::new( + &taker_wrapper_fixture.wrapper.dynamic, + taker_wrapper_fixture.wrapper.fixed.market_infos_root_index, + NIL, + ); + + let market_info_index: DataIndex = + market_infos_tree.lookup_index(&MarketInfo::new_empty(test_fixture.market.key, NIL)); + + let market_info: &MarketInfo = get_helper::>( + &taker_wrapper_fixture.wrapper.dynamic, + market_info_index, + ) + .get_value(); + + assert_eq!(market_info.orders_root_index, NIL); + }; + + // settle & pay fees + let settle_taker_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(taker_wrapper_fixture.key, false), + AccountMeta::new(taker, true), + AccountMeta::new(taker_token_account_base, false), + AccountMeta::new(taker_token_account_quote, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(base_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(platform_token_account, false), + AccountMeta::new(referred_token_account, false), + ], + data: [ + ManifestWrapperInstruction::SettleFunds.to_vec(), + WrapperSettleFundsParams::new(500_000_000, 50) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[settle_taker_ix], + Some(&taker), + &[&taker_keypair], + ) + .await?; + + // taker sold 1/1 SOL, expect 0 + let taker_token_account_base: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(taker_token_account_base) + .await + .unwrap() + .unwrap(); + + let taker_token_account_base = + spl_token::state::Account::unpack(&taker_token_account_base.data)?; + assert_eq!(taker_token_account_base.amount, 0); + + // user has proceeds of trade in his wallet, but 50% fees were charged + let taker_token_account_quote: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(taker_token_account_quote) + .await + .unwrap() + .unwrap(); + + let taker_token_account_quote = StateWithExtensions::::unpack( + &taker_token_account_quote.data, + )?; + assert_eq!( + taker_token_account_quote.base.amount, + USDC_UNIT_SIZE * 3 / 2 + ); + + // verify the remaining 50% was paid to platform not referrer + // transfer fees of 100 applied on the settled amount + let platform_token_account_quote: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(platform_token_account) + .await + .unwrap() + .unwrap(); + + let platform_token_account_quote = + StateWithExtensions::::unpack( + &platform_token_account_quote.data, + )?; + assert_eq!( + platform_token_account_quote.base.amount, + USDC_UNIT_SIZE / 4 - 100 + ); + + let referred_token_account_quote: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(referred_token_account) + .await + .unwrap() + .unwrap(); + + let referred_token_account_quote = + StateWithExtensions::::unpack( + &referred_token_account_quote.data, + )?; + assert_eq!( + referred_token_account_quote.base.amount, + USDC_UNIT_SIZE / 4 - 100 + ); + + let settle_maker_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(maker_wrapper_fixture.key, false), + AccountMeta::new(maker, true), + AccountMeta::new(maker_token_account_base, false), + AccountMeta::new(maker_token_account_quote, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(base_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(platform_token_account, false), + AccountMeta::new(referred_token_account, false), + ], + data: [ + ManifestWrapperInstruction::SettleFunds.to_vec(), + WrapperSettleFundsParams::new(500_000_000, 50) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[settle_maker_ix], + Some(&maker), + &[&maker_keypair], + ) + .await + .expect_err("should fail due to lack of USDC balance to pay fees"); + + // maker has 1 SOL & bought 1 SOL, but couldn't settle + let maker_token_account_base: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(maker_token_account_base) + .await + .unwrap() + .unwrap(); + + let maker_token_account_base = + spl_token::state::Account::unpack(&maker_token_account_base.data)?; + assert_eq!(maker_token_account_base.amount, SOL_UNIT_SIZE); + + Ok(()) +} + +#[tokio::test] +async fn wrapper_fill_order_with_transfer_fees_without_referral_test() -> anyhow::Result<()> { + let mut test_fixture: TestFixture = TestFixture::new_with_extensions(true, true).await; + + let taker: Pubkey = test_fixture.payer(); + let taker_keypair: Keypair = test_fixture.payer_keypair().insecure_clone(); + let mut taker_wrapper_fixture: WrapperFixture = test_fixture.wrapper.clone(); + + let maker: Pubkey = test_fixture.second_keypair.pubkey(); + let maker_keypair: Keypair = test_fixture.second_keypair.insecure_clone(); + + // setup wrapper for maker + let mut maker_wrapper_fixture: WrapperFixture = { + let wrapper_keypair = Keypair::new(); + + let create_wrapper_ixs: Vec = + create_wrapper_instructions(&maker, &maker, &wrapper_keypair.pubkey())?; + + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &create_wrapper_ixs, + Some(&maker), + &[&maker_keypair, &wrapper_keypair], + ) + .await?; + + WrapperFixture::new(Rc::clone(&test_fixture.context), wrapper_keypair.pubkey()).await + }; + + // setup token accounts for taker, maker, platform & referrer + let (_, taker_token_account_base) = test_fixture + .fund_trader_wallet(&taker_keypair, Token::SOL, 1 * SOL_UNIT_SIZE) + .await; + let (_, taker_token_account_quote) = test_fixture + .fund_trader_wallet_2022(&taker_keypair, Token::USDC, 1 * USDC_UNIT_SIZE + 100) + .await; + + let (base_mint, maker_token_account_base) = test_fixture + .fund_trader_wallet(&maker_keypair, Token::SOL, 1 * SOL_UNIT_SIZE) + .await; + let (quote_mint, maker_token_account_quote) = test_fixture + .fund_trader_wallet_2022(&maker_keypair, Token::USDC, 1 * USDC_UNIT_SIZE + 100) + .await; + let platform_token_account = test_fixture + .fund_token_account_2022("e_mint, &taker) + .await; + + let (base_vault, _) = get_vault_address(&test_fixture.market.key, &base_mint); + let (quote_vault, _) = get_vault_address(&test_fixture.market.key, "e_mint); + let (global_base, _) = get_global_address(&base_mint); + let (global_quote, _) = get_global_address("e_mint); + let (global_base_vault, _) = get_global_vault_address(&base_mint); + let (global_quote_vault, _) = get_global_vault_address("e_mint); + + // maker buys 1 sol @ 1000 USDC + let maker_order_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(maker_wrapper_fixture.key, false), + AccountMeta::new(maker, true), + AccountMeta::new(maker_token_account_quote, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(maker, true), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new(global_base, false), + AccountMeta::new(global_base_vault, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new(global_quote, false), + AccountMeta::new(global_quote_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + ], + data: [ + ManifestWrapperInstruction::PlaceOrder.to_vec(), + WrapperPlaceOrderParams::new( + 1, + 1 * SOL_UNIT_SIZE, + 1, + -3, + true, + NO_EXPIRATION_LAST_VALID_SLOT, + OrderType::Limit, + ) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[maker_order_ix], + Some(&maker), + &[&maker_keypair], + ) + .await?; + + // verify order is on book + test_fixture.market.reload().await; + let maker_index = test_fixture.market.market.get_trader_index(&maker); + + let bids = test_fixture.market.market.get_bids(); + let found: Option<(DataIndex, &RestingOrder)> = bids + .iter::() + .find(|(_, o)| o.get_trader_index() == maker_index); + assert!(found.is_some()); + let (core_index, order) = found.unwrap(); + assert_eq!(order.get_is_bid(), true); + assert_eq!(order.get_num_base_atoms(), 1 * SOL_UNIT_SIZE); + + // verify order is correctly tracked on wrapper + maker_wrapper_fixture.reload().await; + + let open_order: WrapperOpenOrder = { + let market_infos_tree: MarketInfosTreeReadOnly = MarketInfosTreeReadOnly::new( + &maker_wrapper_fixture.wrapper.dynamic, + maker_wrapper_fixture.wrapper.fixed.market_infos_root_index, + NIL, + ); + + let market_info_index: DataIndex = + market_infos_tree.lookup_index(&MarketInfo::new_empty(test_fixture.market.key, NIL)); + + let market_info: &MarketInfo = get_helper::>( + &maker_wrapper_fixture.wrapper.dynamic, + market_info_index, + ) + .get_value(); + + get_helper::>( + &maker_wrapper_fixture.wrapper.dynamic, + market_info.orders_root_index, + ) + .get_value() + .clone() + }; + + assert_eq!(open_order.get_is_bid(), true); + assert_eq!(open_order.get_client_order_id(), 1); + assert_eq!(open_order.get_num_base_atoms(), SOL_UNIT_SIZE); + assert_eq!( + open_order.get_price(), + QuoteAtomsPerBaseAtom::try_from_mantissa_and_exponent(1, -3).unwrap() + ); + assert_eq!(open_order.get_market_data_index(), core_index); + + // taker sells 1 sol @ 1000 USDC + let taker_order_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(taker_wrapper_fixture.key, false), + AccountMeta::new(taker, true), + AccountMeta::new(taker_token_account_base, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(taker, true), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new(global_base, false), + AccountMeta::new(global_base_vault, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new(global_quote, false), + AccountMeta::new(global_quote_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + ], + data: [ + ManifestWrapperInstruction::PlaceOrder.to_vec(), + WrapperPlaceOrderParams::new( + 1, + 1 * SOL_UNIT_SIZE, + 1, + -3, + false, + NO_EXPIRATION_LAST_VALID_SLOT, + OrderType::Limit, + ) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[taker_order_ix], + Some(&taker), + &[&taker_keypair], + ) + .await?; + + // verify book is cleared + test_fixture.market.reload().await; + let asks = test_fixture.market.market.get_asks(); + assert_eq!(asks.iter::().next(), None); + let bids = test_fixture.market.market.get_bids(); + assert_eq!(bids.iter::().next(), None); + + // verify order is correctly not-tracked on wrapper + taker_wrapper_fixture.reload().await; + { + let market_infos_tree: MarketInfosTreeReadOnly = MarketInfosTreeReadOnly::new( + &taker_wrapper_fixture.wrapper.dynamic, + taker_wrapper_fixture.wrapper.fixed.market_infos_root_index, + NIL, + ); + + let market_info_index: DataIndex = + market_infos_tree.lookup_index(&MarketInfo::new_empty(test_fixture.market.key, NIL)); + + let market_info: &MarketInfo = get_helper::>( + &taker_wrapper_fixture.wrapper.dynamic, + market_info_index, + ) + .get_value(); + + assert_eq!(market_info.orders_root_index, NIL); + }; + + // settle & pay fees + let settle_taker_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(taker_wrapper_fixture.key, false), + AccountMeta::new(taker, true), + AccountMeta::new(taker_token_account_base, false), + AccountMeta::new(taker_token_account_quote, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(base_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(platform_token_account, false), + ], + data: [ + ManifestWrapperInstruction::SettleFunds.to_vec(), + WrapperSettleFundsParams::new(500_000_000, 50) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[settle_taker_ix], + Some(&taker), + &[&taker_keypair], + ) + .await?; + + // taker sold 1/1 SOL, expect 0 + let taker_token_account_base: Account = test_fixture .context .borrow_mut() .banks_client - .get_account(platform_token_account) + .get_account(taker_token_account_base) .await .unwrap() .unwrap(); - let platform_token_account_quote = - spl_token::state::Account::unpack(&platform_token_account_quote.data)?; - assert_eq!(platform_token_account_quote.amount, USDC_UNIT_SIZE / 4); + let taker_token_account_base = + spl_token::state::Account::unpack(&taker_token_account_base.data)?; + assert_eq!(taker_token_account_base.amount, 0); - let referred_token_account_quote: Account = test_fixture + // user has proceeds of trade in his wallet, but 50% fees were charged + let taker_token_account_quote: Account = test_fixture .context .borrow_mut() .banks_client - .get_account(referred_token_account) + .get_account(taker_token_account_quote) .await .unwrap() .unwrap(); - let referred_token_account_quote = - spl_token::state::Account::unpack(&referred_token_account_quote.data)?; - assert_eq!(referred_token_account_quote.amount, USDC_UNIT_SIZE / 4); + let taker_token_account_quote = StateWithExtensions::::unpack( + &taker_token_account_quote.data, + )?; + assert_eq!( + taker_token_account_quote.base.amount, + USDC_UNIT_SIZE * 3 / 2 + ); + + // verify the remaining 50% was not split with referrer + // transfer fees of 100 applied on the settled amount + let platform_token_account_quote: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(platform_token_account) + .await + .unwrap() + .unwrap(); + + let platform_token_account_quote = + StateWithExtensions::::unpack( + &platform_token_account_quote.data, + )?; + assert_eq!( + platform_token_account_quote.base.amount, + USDC_UNIT_SIZE / 2 - 100 + ); let settle_maker_ix = Instruction { program_id: ui_wrapper::id(), @@ -897,10 +2375,9 @@ async fn wrapper_fill_order_test() -> anyhow::Result<()> { AccountMeta::new_readonly(base_mint, false), AccountMeta::new_readonly(quote_mint, false), AccountMeta::new_readonly(spl_token::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(spl_token_2022::id(), false), AccountMeta::new_readonly(manifest::id(), false), AccountMeta::new(platform_token_account, false), - AccountMeta::new(referred_token_account, false), ], data: [ ManifestWrapperInstruction::SettleFunds.to_vec(), @@ -935,3 +2412,213 @@ async fn wrapper_fill_order_test() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn wrapper_self_trade_test() -> anyhow::Result<()> { + let mut test_fixture: TestFixture = TestFixture::new().await; + + let payer: Pubkey = test_fixture.payer(); + let payer_keypair: Keypair = test_fixture.payer_keypair().insecure_clone(); + let (base_mint, trader_token_account_base) = test_fixture + .fund_trader_wallet(&payer_keypair, Token::SOL, 1) + .await; + let (quote_mint, trader_token_account_quote) = test_fixture + .fund_trader_wallet(&payer_keypair, Token::USDC, 1) + .await; + + let platform_token_account = test_fixture.fund_token_account("e_mint, &payer).await; + let referred_token_account = test_fixture.fund_token_account("e_mint, &payer).await; + + let (quote_vault, _) = get_vault_address(&test_fixture.market.key, "e_mint); + let (base_vault, _) = get_vault_address(&test_fixture.market.key, &base_mint); + let (global_base, _) = get_global_address(&base_mint); + let (global_quote, _) = get_global_address("e_mint); + let (global_base_vault, _) = get_global_vault_address(&base_mint); + let (global_quote_vault, _) = get_global_vault_address("e_mint); + + // place orders + let place_order_1_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(test_fixture.wrapper.key, false), + AccountMeta::new(payer, true), + AccountMeta::new(trader_token_account_quote, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new(global_base, false), + AccountMeta::new(global_base_vault, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new(global_quote, false), + AccountMeta::new(global_quote_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: [ + ManifestWrapperInstruction::PlaceOrder.to_vec(), + WrapperPlaceOrderParams::new( + 1, + 1, + 1, + 0, + true, + NO_EXPIRATION_LAST_VALID_SLOT, + OrderType::Limit, + ) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + let place_order_2_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(test_fixture.wrapper.key, false), + AccountMeta::new(payer, true), + AccountMeta::new(trader_token_account_base, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new(global_base, false), + AccountMeta::new(global_base_vault, false), + AccountMeta::new(base_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new(global_quote, false), + AccountMeta::new(global_quote_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: [ + ManifestWrapperInstruction::PlaceOrder.to_vec(), + WrapperPlaceOrderParams::new( + 2, + 1, + 1, + 0, + false, + NO_EXPIRATION_LAST_VALID_SLOT, + OrderType::Limit, + ) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[place_order_1_ix, place_order_2_ix], + Some(&payer), + &[&payer_keypair], + ) + .await?; + + // verify both orders are no longer tracked on wrapper: + test_fixture.wrapper.reload().await; + + let market_info_index: DataIndex = { + let market_infos_tree: MarketInfosTreeReadOnly = MarketInfosTreeReadOnly::new( + &test_fixture.wrapper.wrapper.dynamic, + test_fixture.wrapper.wrapper.fixed.market_infos_root_index, + NIL, + ); + market_infos_tree.lookup_index(&MarketInfo::new_empty(test_fixture.market.key, NIL)) + }; + + let orders_root_index = { + let market_info: &MarketInfo = get_helper::>( + &test_fixture.wrapper.wrapper.dynamic, + market_info_index, + ) + .get_value(); + + market_info.orders_root_index + }; + + let open_orders_tree: OpenOrdersTreeReadOnly = OpenOrdersTreeReadOnly::new( + &test_fixture.wrapper.wrapper.dynamic, + orders_root_index, + NIL, + ); + let found = open_orders_tree + .iter::() + .find(|(_, o)| o.get_client_order_id() == 1 || o.get_client_order_id() == 2); + assert!(found.is_none()); + + // release funds + let settle_funds_ix = Instruction { + program_id: ui_wrapper::id(), + accounts: vec![ + AccountMeta::new(test_fixture.wrapper.key, false), + AccountMeta::new(payer, true), + AccountMeta::new(trader_token_account_base, false), + AccountMeta::new(trader_token_account_quote, false), + AccountMeta::new(test_fixture.market.key, false), + AccountMeta::new(base_vault, false), + AccountMeta::new(quote_vault, false), + AccountMeta::new_readonly(base_mint, false), + AccountMeta::new_readonly(quote_mint, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(manifest::id(), false), + AccountMeta::new(platform_token_account, false), + AccountMeta::new(referred_token_account, false), + ], + data: [ + ManifestWrapperInstruction::SettleFunds.to_vec(), + WrapperSettleFundsParams::new(500_000_000, 50) + .try_to_vec() + .unwrap(), + ] + .concat(), + }; + send_tx_with_retry( + Rc::clone(&test_fixture.context), + &[settle_funds_ix], + Some(&payer), + &[&payer_keypair], + ) + .await?; + + // verify fees were charged and user did not receive quote tokens + let trader_token_account_quote: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(trader_token_account_quote) + .await + .unwrap() + .unwrap(); + + let trader_token_account_quote = + spl_token::state::Account::unpack(&trader_token_account_quote.data)?; + assert_eq!(trader_token_account_quote.amount, 0); + + // verify base token was received back + let trader_token_account_base: Account = test_fixture + .context + .borrow_mut() + .banks_client + .get_account(trader_token_account_base) + .await + .unwrap() + .unwrap(); + + let trader_token_account_base = + spl_token::state::Account::unpack(&trader_token_account_base.data)?; + assert_eq!(trader_token_account_base.amount, 1); + + Ok(()) +} diff --git a/programs/ui-wrapper/tests/program_test/fixtures.rs b/programs/ui-wrapper/tests/program_test/fixtures.rs index f46c4c7e1..9757738a4 100644 --- a/programs/ui-wrapper/tests/program_test/fixtures.rs +++ b/programs/ui-wrapper/tests/program_test/fixtures.rs @@ -3,6 +3,7 @@ use std::{ io::Error, }; +use hypertree::trace; use manifest::{ program::{create_global_instruction, create_market_instructions, get_dynamic_value}, quantities::WrapperU64, @@ -16,10 +17,16 @@ use solana_sdk::{ signature::Keypair, signer::Signer, system_instruction::create_account, transaction::Transaction, }; -use spl_token_2022::state::Mint; +use spl_token_2022::{ + extension::{ + transfer_fee::instruction::initialize_transfer_fee_config, transfer_hook, + BaseStateWithExtensions, ExtensionType, StateWithExtensions, + }, + state::Mint, +}; use std::rc::Rc; use ui_wrapper::{ - instruction_builders::{claim_seat_instruction, create_wrapper_instructions}, + instruction_builders::create_wrapper_instructions, wrapper_user::{ManifestWrapperUserFixed, WrapperUserValue}, }; @@ -66,6 +73,8 @@ impl TestFixture { ui_wrapper::ID, processor!(ui_wrapper::process_instruction), ); + // needed extra cu for logs and traces + program.set_compute_max_units(600_000); program.add_program( "manifest", manifest::ID, @@ -78,8 +87,6 @@ impl TestFixture { solana_sdk::account::Account::new(SOL_UNIT_SIZE, 0, &solana_sdk::system_program::id()), ); - let usdc_keypair: Keypair = Keypair::new(); - let sol_keypair: Keypair = Keypair::new(); let market_keypair: Keypair = Keypair::new(); let wrapper_keypair: Keypair = Keypair::new(); @@ -87,10 +94,8 @@ impl TestFixture { Rc::new(RefCell::new(program.start_with_context().await)); solana_logger::setup_with_default(RUST_LOG_DEFAULT); - let usdc_mint_f: MintFixture = - MintFixture::new(Rc::clone(&context), Some(usdc_keypair), Some(6)).await; - let sol_mint_f: MintFixture = - MintFixture::new(Rc::clone(&context), Some(sol_keypair), Some(9)).await; + let usdc_mint_f: MintFixture = MintFixture::new(Rc::clone(&context), Some(6)).await; + let sol_mint_f: MintFixture = MintFixture::new(Rc::clone(&context), Some(9)).await; let payer_pubkey: Pubkey = context.borrow().payer.pubkey(); let payer: Keypair = context.borrow().payer.insecure_clone(); @@ -154,6 +159,110 @@ impl TestFixture { } } + pub async fn new_with_extensions(transfer_fee: bool, transfer_hook: bool) -> TestFixture { + let mut program: ProgramTest = ProgramTest::new( + "ui_wrapper", + ui_wrapper::ID, + processor!(ui_wrapper::process_instruction), + ); + // needed extra cu for logs and traces + program.set_compute_max_units(600_000); + program.add_program( + "manifest", + manifest::ID, + processor!(manifest::process_instruction), + ); + + let second_keypair: Keypair = Keypair::new(); + program.add_account( + second_keypair.pubkey(), + solana_sdk::account::Account::new(SOL_UNIT_SIZE, 0, &solana_sdk::system_program::id()), + ); + + let market_keypair: Keypair = Keypair::new(); + let wrapper_keypair: Keypair = Keypair::new(); + + let context: Rc> = + Rc::new(RefCell::new(program.start_with_context().await)); + solana_logger::setup_with_default(RUST_LOG_DEFAULT); + + let usdc_mint_f: MintFixture = MintFixture::new_with_version( + Rc::clone(&context), + Some(6), + true, + transfer_fee, + transfer_hook, + ) + .await; + let sol_mint_f: MintFixture = MintFixture::new(Rc::clone(&context), Some(9)).await; + + let payer_pubkey: Pubkey = context.borrow().payer.pubkey(); + let payer: Keypair = context.borrow().payer.insecure_clone(); + let create_market_ixs: Vec = create_market_instructions( + &market_keypair.pubkey(), + &sol_mint_f.key, + &usdc_mint_f.key, + &payer_pubkey, + ) + .unwrap(); + + send_tx_with_retry( + Rc::clone(&context), + &create_market_ixs[..], + Some(&payer_pubkey), + &[&payer.insecure_clone(), &market_keypair], + ) + .await + .unwrap(); + + // Now that market is created, we can make a market fixture. + let market_fixture: MarketFixture = + MarketFixture::new(Rc::clone(&context), market_keypair.pubkey()).await; + + let create_wrapper_ixs: Vec = + create_wrapper_instructions(&payer_pubkey, &payer_pubkey, &wrapper_keypair.pubkey()) + .unwrap(); + send_tx_with_retry( + Rc::clone(&context), + &create_wrapper_ixs[..], + Some(&payer_pubkey), + &[&payer.insecure_clone(), &wrapper_keypair], + ) + .await + .unwrap(); + + let wrapper_fixture: WrapperFixture = + WrapperFixture::new(Rc::clone(&context), wrapper_keypair.pubkey()).await; + + let payer_sol_fixture: TokenAccountFixture = + TokenAccountFixture::new(Rc::clone(&context), &sol_mint_f.key, &payer_pubkey).await; + let payer_usdc_fixture = + TokenAccountFixture::new_2022(Rc::clone(&context), &usdc_mint_f.key, &payer_pubkey) + .await; + + let global_fixture: GlobalFixture = GlobalFixture::new_with_token_program( + Rc::clone(&context), + &usdc_mint_f.key, + &spl_token_2022::id(), + ) + .await; + let sol_global_fixture: GlobalFixture = + GlobalFixture::new(Rc::clone(&context), &sol_mint_f.key).await; + + TestFixture { + context: Rc::clone(&context), + usdc_mint: usdc_mint_f, + sol_mint: sol_mint_f, + market: market_fixture, + wrapper: wrapper_fixture, + payer_sol: payer_sol_fixture, + payer_usdc: payer_usdc_fixture, + global_fixture, + sol_global_fixture, + second_keypair, + } + } + pub async fn try_load( &self, address: &Pubkey, @@ -185,6 +294,18 @@ impl TestFixture { token_account_fixture.key } + pub async fn fund_token_account_2022(&self, mint_pk: &Pubkey, owner_pk: &Pubkey) -> Pubkey { + let token_account_keypair: Keypair = Keypair::new(); + let token_account_fixture: TokenAccountFixture = + TokenAccountFixture::new_with_keypair_2022( + Rc::clone(&self.context), + mint_pk, + owner_pk, + &token_account_keypair, + ) + .await; + token_account_fixture.key + } /// returns (mint, trader_token_account) pub async fn fund_trader_wallet( &mut self, @@ -193,6 +314,10 @@ impl TestFixture { amount_atoms: u64, ) -> (Pubkey, Pubkey) { let is_base: bool = token == Token::SOL; + trace!( + "fund_trader_wallet {} {amount_atoms}", + if is_base { "SOL" } else { "USDC" } + ); let (mint, trader_token_account) = if is_base { let trader_token_account: Pubkey = if keypair.pubkey() == self.payer() { self.payer_sol.key @@ -210,51 +335,58 @@ impl TestFixture { self.payer_usdc.key } else { // Make a temporary token account + self.fund_token_account(&self.usdc_mint.key, &keypair.pubkey()) .await }; self.usdc_mint .mint_to(&trader_token_account, amount_atoms) .await; + (self.usdc_mint.key.clone(), trader_token_account) }; (mint, trader_token_account) } - pub async fn claim_seat(&self) -> anyhow::Result<(), BanksClientError> { - self.claim_seat_for_keypair(&self.payer_keypair()).await - } - - pub async fn claim_seat_for_keypair( - &self, - keypair: &Keypair, - ) -> anyhow::Result<(), BanksClientError> { - let wrapper_key: Pubkey = self.wrapper.key; - self.claim_seat_for_keypair_with_wrapper(keypair, &wrapper_key) - .await - } - - pub async fn claim_seat_for_keypair_with_wrapper( - &self, + pub async fn fund_trader_wallet_2022( + &mut self, keypair: &Keypair, - wrapper_state: &Pubkey, - ) -> anyhow::Result<(), BanksClientError> { - let claim_seat_ix: Instruction = claim_seat_instruction( - &self.market.key, - &keypair.pubkey(), - &keypair.pubkey(), - wrapper_state, + token: Token, + amount_atoms: u64, + ) -> (Pubkey, Pubkey) { + let is_base: bool = token == Token::SOL; + trace!( + "fund_trader_wallet_2022 {} {amount_atoms}", + if is_base { "SOL" } else { "USDC" } ); - send_tx_with_retry( - Rc::clone(&self.context), - &[claim_seat_ix], - Some(&keypair.pubkey()), - &[&keypair.insecure_clone()], - ) - .await - .unwrap(); - Ok(()) + let (mint, trader_token_account) = if is_base { + let trader_token_account: Pubkey = if keypair.pubkey() == self.payer() { + self.payer_sol.key + } else { + // Make a temporary token account + self.fund_token_account_2022(&self.sol_mint.key, &keypair.pubkey()) + .await + }; + self.sol_mint + .mint_to_2022(&trader_token_account, amount_atoms) + .await; + (self.sol_mint.key.clone(), trader_token_account) + } else { + let trader_token_account: Pubkey = if keypair.pubkey() == self.payer() { + self.payer_usdc.key + } else { + // Make a temporary token account + self.fund_token_account_2022(&self.usdc_mint.key, &keypair.pubkey()) + .await + }; + self.usdc_mint + .mint_to_2022(&trader_token_account, amount_atoms) + .await; + (self.usdc_mint.key.clone(), trader_token_account) + }; + + (mint, trader_token_account) } } @@ -445,69 +577,167 @@ impl GlobalFixture { } } -// TODO: share below utilities with other test runners #[derive(Clone)] pub struct MintFixture { pub context: Rc>, pub key: Pubkey, + pub is_22: bool, + pub vanilla_mint: Option, + pub extension_mint: Option, } impl MintFixture { pub async fn new( context: Rc>, - mint_keypair: Option, - mint_decimals: Option, + mint_decimals_opt: Option, ) -> MintFixture { - let payer_pubkey: Pubkey = context.borrow().payer.pubkey(); - let payer: Keypair = context.borrow().payer.insecure_clone(); - - let mint_keypair: Keypair = mint_keypair.unwrap_or_else(Keypair::new); + // Defaults to not 22. + MintFixture::new_with_version(context, mint_decimals_opt, false, false, false).await + } + pub async fn new_with_version( + context: Rc>, + mint_decimals_opt: Option, + is_22: bool, + transfer_fee: bool, + transfer_hook: bool, + ) -> MintFixture { + let context_ref: Rc> = Rc::clone(&context); + let mint_keypair: Keypair = Keypair::new(); + let payer: Keypair = context.borrow().payer.insecure_clone(); let rent: Rent = context.borrow_mut().banks_client.get_rent().await.unwrap(); + let space = if is_22 { + let mut extensions = Vec::new(); + if transfer_fee { + extensions.push(ExtensionType::TransferFeeConfig); + } + if transfer_hook { + extensions.push(ExtensionType::TransferHook); + } + ExtensionType::try_calculate_account_len::(&extensions).unwrap() + } else { + spl_token::state::Mint::LEN + }; - let init_account_ix: Instruction = create_account( - &context.borrow().payer.pubkey(), - &mint_keypair.pubkey(), - rent.minimum_balance(spl_token::state::Mint::LEN), - spl_token::state::Mint::LEN as u64, - &spl_token::id(), - ); - let init_mint_ix: Instruction = spl_token::instruction::initialize_mint( - &spl_token::id(), + let mut instructions = Vec::new(); + + instructions.push(create_account( + &payer.pubkey(), &mint_keypair.pubkey(), - &context.borrow().payer.pubkey(), - None, - mint_decimals.unwrap_or(6), - ) - .unwrap(); + rent.minimum_balance(space), + space as u64, + &{ + if is_22 { + spl_token_2022::id() + } else { + spl_token::id() + } + }, + )); + if is_22 { + if transfer_fee { + instructions.push( + initialize_transfer_fee_config( + &spl_token_2022::id(), + &mint_keypair.pubkey(), + None, + None, + 100, + 100, + ) + .unwrap(), + ); + } + if transfer_hook { + instructions.push( + transfer_hook::instruction::initialize( + &spl_token_2022::id(), + &mint_keypair.pubkey(), + Some(payer.pubkey()), + None, + ) + .unwrap(), + ); + } + instructions.push( + spl_token_2022::instruction::initialize_mint( + &spl_token_2022::id(), + &mint_keypair.pubkey(), + &payer.pubkey(), + None, + mint_decimals_opt.unwrap_or(6), + ) + .unwrap(), + ); + } else { + instructions.push( + spl_token::instruction::initialize_mint( + &spl_token::id(), + &mint_keypair.pubkey(), + &payer.pubkey(), + None, + mint_decimals_opt.unwrap_or(6), + ) + .unwrap(), + ); + }; send_tx_with_retry( Rc::clone(&context), - &[init_account_ix, init_mint_ix], - Some(&payer_pubkey), - &[&mint_keypair.insecure_clone(), &payer], + &instructions, + Some(&payer.pubkey()), + &[&payer, &mint_keypair], ) .await .unwrap(); - let context_ref: Rc> = Rc::clone(&context); - MintFixture { + let mut result = MintFixture { context: context_ref, key: mint_keypair.pubkey(), + is_22, + vanilla_mint: None, + extension_mint: None, + }; + result.reload().await; + result + } + + pub async fn reload(&mut self) { + let mint_account = self + .context + .borrow_mut() + .banks_client + .get_account(self.key) + .await + .unwrap() + .unwrap(); + + if self.is_22 { + self.extension_mint = Some( + StateWithExtensions::::unpack(&mut mint_account.data.clone().as_slice()) + .unwrap() + .base, + ) + } else { + self.vanilla_mint = Some( + spl_token::state::Mint::unpack_unchecked(&mut mint_account.data.as_slice()) + .unwrap(), + ); } } - pub async fn mint_to(&mut self, dest: &Pubkey, native_amount: u64) { - let payer_keypair: Keypair = self.context.borrow().payer.insecure_clone(); - let mint_to_ix: Instruction = self.make_mint_to_ix(dest, native_amount); + pub async fn mint_to(&mut self, dest: &Pubkey, num_atoms: u64) { + let payer: Keypair = self.context.borrow().payer.insecure_clone(); send_tx_with_retry( Rc::clone(&self.context), - &[mint_to_ix], - Some(&payer_keypair.pubkey()), - &[&payer_keypair], + &[self.make_mint_to_ix(dest, num_atoms)], + Some(&payer.pubkey()), + &[&payer], ) .await .unwrap(); + + self.reload().await } fn make_mint_to_ix(&self, dest: &Pubkey, amount: u64) -> Instruction { @@ -523,9 +753,38 @@ impl MintFixture { .unwrap(); mint_to_instruction } + + pub async fn mint_to_2022(&mut self, dest: &Pubkey, num_atoms: u64) { + let payer: Keypair = self.context.borrow().payer.insecure_clone(); + send_tx_with_retry( + Rc::clone(&self.context), + &[self.make_mint_to_2022_ix(dest, num_atoms)], + Some(&payer.pubkey()), + &[&payer], + ) + .await + .unwrap(); + + self.reload().await + } + + fn make_mint_to_2022_ix(&self, dest: &Pubkey, amount: u64) -> Instruction { + let context: Ref = self.context.borrow(); + let mint_to_instruction: Instruction = spl_token_2022::instruction::mint_to( + &spl_token_2022::ID, + &self.key, + dest, + &context.payer.pubkey(), + &[&context.payer.pubkey()], + amount, + ) + .unwrap(); + mint_to_instruction + } } pub struct TokenAccountFixture { + context: Rc>, pub key: Pubkey, } @@ -555,34 +814,104 @@ impl TokenAccountFixture { [init_account_ix, init_token_ix] } + async fn create_ixs_2022( + rent: Rent, + mint_pk: &Pubkey, + payer_pk: &Pubkey, + owner_pk: &Pubkey, + keypair: &Keypair, + space: usize, + ) -> [Instruction; 2] { + let init_account_ix: Instruction = create_account( + payer_pk, + &keypair.pubkey(), + rent.minimum_balance(space), + space as u64, + &spl_token_2022::id(), + ); + + let init_token_ix: Instruction = spl_token_2022::instruction::initialize_account( + &spl_token_2022::id(), + &keypair.pubkey(), + mint_pk, + owner_pk, + ) + .unwrap(); + + [init_account_ix, init_token_ix] + } - pub async fn new_with_keypair( + pub async fn new_with_keypair_2022( context: Rc>, mint_pk: &Pubkey, owner_pk: &Pubkey, keypair: &Keypair, ) -> Self { let rent: Rent = context.borrow_mut().banks_client.get_rent().await.unwrap(); - let instructions: [Instruction; 2] = Self::create_ixs( - rent, - mint_pk, - &context.borrow().payer.pubkey(), - owner_pk, - keypair, + let payer: Pubkey = context.borrow().payer.pubkey(); + let payer_keypair: Keypair = context.borrow().payer.insecure_clone(); + let mint_account = context + .borrow_mut() + .banks_client + .get_account(*mint_pk) + .await + .unwrap() + .unwrap(); + + let mut mint_account_data = mint_account.data.clone(); + let mint_with_extensions = + StateWithExtensions::::unpack(mint_account_data.as_mut_slice()).unwrap(); + let mint_extensions = mint_with_extensions.get_extension_types().unwrap(); + let account_extensions = + ExtensionType::get_required_init_account_extensions(&mint_extensions); + let space = ExtensionType::try_calculate_account_len::( + &account_extensions, ) - .await; + .unwrap(); + + let instructions: [Instruction; 2] = + Self::create_ixs_2022(rent, mint_pk, &payer, owner_pk, keypair, space).await; + + send_tx_with_retry( + Rc::clone(&context), + &instructions[..], + Some(&payer), + &[&payer_keypair, keypair], + ) + .await + .unwrap(); + + let context_ref: Rc> = context.clone(); + Self { + context: context_ref.clone(), + key: keypair.pubkey(), + } + } + pub async fn new_with_keypair( + context: Rc>, + mint_pk: &Pubkey, + owner_pk: &Pubkey, + keypair: &Keypair, + ) -> Self { + let rent: Rent = context.borrow_mut().banks_client.get_rent().await.unwrap(); + let payer: Pubkey = context.borrow().payer.pubkey(); let payer_keypair: Keypair = context.borrow().payer.insecure_clone(); + let instructions: [Instruction; 2] = + Self::create_ixs(rent, mint_pk, &payer, owner_pk, keypair).await; + send_tx_with_retry( Rc::clone(&context), - &instructions, - Some(&payer_keypair.pubkey()), + &instructions[..], + Some(&payer), &[&payer_keypair, keypair], ) .await .unwrap(); + let context_ref: Rc> = context.clone(); Self { + context: context_ref.clone(), key: keypair.pubkey(), } } @@ -595,9 +924,44 @@ impl TokenAccountFixture { let keypair: Keypair = Keypair::new(); TokenAccountFixture::new_with_keypair(context, mint_pk, owner_pk, &keypair).await } + + pub async fn new_2022( + context: Rc>, + mint_pk: &Pubkey, + owner_pk: &Pubkey, + ) -> TokenAccountFixture { + let keypair: Keypair = Keypair::new(); + TokenAccountFixture::new_with_keypair_2022(context, mint_pk, owner_pk, &keypair).await + } + + pub async fn balance_atoms(&self) -> u64 { + let token_account: spl_token::state::Account = + get_and_deserialize(self.context.clone(), self.key).await; + + token_account.amount + } +} + +pub async fn get_and_deserialize( + context: Rc>, + pubkey: Pubkey, +) -> T { + let mut context: RefMut = context.borrow_mut(); + loop { + let account_or: Result, BanksClientError> = + context.banks_client.get_account(pubkey).await; + if !account_or.is_ok() { + continue; + } + let account_opt: Option = account_or.unwrap(); + if account_opt.is_none() { + continue; + } + return T::unpack_unchecked(&mut account_opt.unwrap().data.as_slice()).unwrap(); + } } -pub(crate) async fn send_tx_with_retry( +pub async fn send_tx_with_retry( context: Rc>, instructions: &[Instruction], payer: Option<&Pubkey>,