Skip to content

Commit

Permalink
Add optional fields for globals in idl and sdk for swap (#176)
Browse files Browse the repository at this point in the history
* Add global as optional to swap

* add test for swap global

* fmt

* update idl

* optional init global in swap

* fix test

* fmt

* fix expectations

* flip test

* update wallet expectations

* fmt

* balances

* fmt

* fix amount

* Update program

* lint
  • Loading branch information
brittcyr authored Oct 12, 2024
1 parent f522bde commit 694fc0a
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 100 deletions.
25 changes: 23 additions & 2 deletions client/idl/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -323,14 +323,16 @@
"name": "baseMint",
"isMut": false,
"isSigner": false,
"isOptional": true,
"docs": [
"Base mint, only inlcuded if base is Token22, otherwise not required"
"Base mint, only included if base is Token22, otherwise not required"
]
},
{
"name": "tokenProgramQuote",
"isMut": false,
"isSigner": false,
"isOptional": true,
"docs": [
"Token program(22) quote. Optional. Only include if different from base"
]
Expand All @@ -339,8 +341,27 @@
"name": "quoteMint",
"isMut": false,
"isSigner": false,
"isOptional": true,
"docs": [
"Quote mint, only inlcuded if base is Token22, otherwise not required"
"Quote mint, only included if base is Token22, otherwise not required"
]
},
{
"name": "global",
"isMut": true,
"isSigner": false,
"isOptional": true,
"docs": [
"Global account"
]
},
{
"name": "globalVault",
"isMut": true,
"isSigner": false,
"isOptional": true,
"docs": [
"Global vault"
]
}
],
Expand Down
17 changes: 16 additions & 1 deletion client/ts/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,8 +712,21 @@ export class ManifestClient {
this.market.address,
this.quoteMint.address,
);

const global: PublicKey = getGlobalAddress(
params.isBaseIn ? this.quoteMint.address : this.baseMint.address,
);
const globalVault: PublicKey = getGlobalVaultAddress(
params.isBaseIn ? this.quoteMint.address : this.baseMint.address,
);

// Assumes just normal token program for now.
// No Token22 support here in sdk yet.
// No Token22 support here in sdk yet, but includes programs and mints as
// though it was.

// No support for the case where global are not needed. That is an
// optimization that needs to be made when looking at the orderbook and
// deciding if it is worthwhile to lock the accounts.
return createSwapInstruction(
{
payer,
Expand All @@ -730,6 +743,8 @@ export class ManifestClient {
? TOKEN_2022_PROGRAM_ID
: TOKEN_PROGRAM_ID,
quoteMint: this.quoteMint.address,
global,
globalVault,
},
{
params,
Expand Down
85 changes: 72 additions & 13 deletions client/ts/src/manifest/instructions/Swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ export const SwapStruct = new beet.BeetArgsStruct<
* @property [_writable_] baseVault
* @property [_writable_] quoteVault
* @property [] tokenProgramBase
* @property [] baseMint
* @property [] tokenProgramQuote
* @property [] quoteMint
* @property [] baseMint (optional)
* @property [] tokenProgramQuote (optional)
* @property [] quoteMint (optional)
* @property [_writable_] global (optional)
* @property [_writable_] globalVault (optional)
* @category Instructions
* @category Swap
* @category generated
Expand All @@ -58,16 +60,23 @@ export type SwapInstructionAccounts = {
baseVault: web3.PublicKey;
quoteVault: web3.PublicKey;
tokenProgramBase: web3.PublicKey;
baseMint: web3.PublicKey;
tokenProgramQuote: web3.PublicKey;
quoteMint: web3.PublicKey;
baseMint?: web3.PublicKey;
tokenProgramQuote?: web3.PublicKey;
quoteMint?: web3.PublicKey;
global?: web3.PublicKey;
globalVault?: web3.PublicKey;
};

export const swapInstructionDiscriminator = 4;

/**
* Creates a _Swap_ instruction.
*
* Optional accounts that are not provided will be omitted from the accounts
* array passed with the instruction.
* An optional account that is set cannot follow an optional account that is unset.
* Otherwise an Error is raised.
*
* @param accounts that will be accessed while the instruction is processed
* @param args to provide as instruction data to the program
*
Expand Down Expand Up @@ -120,22 +129,72 @@ export function createSwapInstruction(
isWritable: false,
isSigner: false,
},
{
];

if (accounts.baseMint != null) {
keys.push({
pubkey: accounts.baseMint,
isWritable: false,
isSigner: false,
},
{
});
}
if (accounts.tokenProgramQuote != null) {
if (accounts.baseMint == null) {
throw new Error(
"When providing 'tokenProgramQuote' then 'accounts.baseMint' need(s) to be provided as well.",
);
}
keys.push({
pubkey: accounts.tokenProgramQuote,
isWritable: false,
isSigner: false,
},
{
});
}
if (accounts.quoteMint != null) {
if (accounts.baseMint == null || accounts.tokenProgramQuote == null) {
throw new Error(
"When providing 'quoteMint' then 'accounts.baseMint', 'accounts.tokenProgramQuote' need(s) to be provided as well.",
);
}
keys.push({
pubkey: accounts.quoteMint,
isWritable: false,
isSigner: false,
},
];
});
}
if (accounts.global != null) {
if (
accounts.baseMint == null ||
accounts.tokenProgramQuote == null ||
accounts.quoteMint == null
) {
throw new Error(
"When providing 'global' then 'accounts.baseMint', 'accounts.tokenProgramQuote', 'accounts.quoteMint' need(s) to be provided as well.",
);
}
keys.push({
pubkey: accounts.global,
isWritable: true,
isSigner: false,
});
}
if (accounts.globalVault != null) {
if (
accounts.baseMint == null ||
accounts.tokenProgramQuote == null ||
accounts.quoteMint == null ||
accounts.global == null
) {
throw new Error(
"When providing 'globalVault' then 'accounts.baseMint', 'accounts.tokenProgramQuote', 'accounts.quoteMint', 'accounts.global' need(s) to be provided as well.",
);
}
keys.push({
pubkey: accounts.globalVault,
isWritable: true,
isSigner: false,
});
}

const ix = new web3.TransactionInstruction({
programId,
Expand Down
1 change: 1 addition & 0 deletions client/ts/tests/createGlobal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export async function createGlobal(
tokenMint: PublicKey,
): Promise<void> {
console.log(`Cluster is ${await getClusterFromConnection(connection)}`);
await airdropSol(connection, payerKeypair.publicKey);

const createGlobalIx = await ManifestClient['createGlobalCreateIx'](
connection,
Expand Down
7 changes: 4 additions & 3 deletions client/ts/tests/placeOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createMarket } from './createMarket';
import { deposit } from './deposit';
import { Market } from '../src/market';
import { assert } from 'chai';
import { NO_EXPIRATION_LAST_VALID_SLOT } from '../src/constants';

async function testPlaceOrder(): Promise<void> {
const connection: Connection = new Connection(
Expand Down Expand Up @@ -72,7 +73,7 @@ export async function placeOrder(
isBid: boolean,
orderType: OrderType,
clientOrderId: number,
lastValidSlot: number = 0,
lastValidSlot: number = NO_EXPIRATION_LAST_VALID_SLOT,
): Promise<void> {
const client: ManifestClient = await ManifestClient.getClientForMarket(
connection,
Expand All @@ -84,8 +85,8 @@ export async function placeOrder(
numBaseTokens,
tokenPrice,
isBid,
lastValidSlot: lastValidSlot,
orderType: orderType,
lastValidSlot,
orderType,
clientOrderId,
});

Expand Down
126 changes: 117 additions & 9 deletions client/ts/tests/swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@ import { createMarket } from './createMarket';
import { Market } from '../src/market';
import {
createAssociatedTokenAccountIdempotent,
getAssociatedTokenAddress,
mintTo,
} from '@solana/spl-token';
import { assert } from 'chai';
import { placeOrder } from './placeOrder';
import { airdropSol } from '../src/utils/solana';
import { depositGlobal } from './globalDeposit';
import { createGlobal } from './createGlobal';
import { OrderType } from '../src';
import { NO_EXPIRATION_LAST_VALID_SLOT } from '../src/constants';

async function testSwap(): Promise<void> {
const connection: Connection = new Connection(
Expand Down Expand Up @@ -76,15 +83,12 @@ export async function swap(
payerKeypair,
);

const swapIx: TransactionInstruction = await client.swapIx(
payerKeypair.publicKey,
{
inAtoms: amountAtoms,
outAtoms: minOutAtoms,
isBaseIn: isBid,
isExactIn: true,
},
);
const swapIx: TransactionInstruction = client.swapIx(payerKeypair.publicKey, {
inAtoms: amountAtoms,
outAtoms: minOutAtoms,
isBaseIn: isBid,
isExactIn: true,
});

const signature = await sendAndConfirmTransaction(
connection,
Expand All @@ -94,8 +98,112 @@ export async function swap(
console.log(`Placed order in ${signature}`);
}

async function _testSwapGlobal(): Promise<void> {
const connection: Connection = new Connection(
'http://127.0.0.1:8899',
'confirmed',
);
const payerKeypair: Keypair = Keypair.generate();

const marketAddress: PublicKey = await createMarket(connection, payerKeypair);
const market: Market = await Market.loadFromAddress({
connection,
address: marketAddress,
});

const traderBaseTokenAccount: PublicKey =
await createAssociatedTokenAccountIdempotent(
connection,
payerKeypair,
market.baseMint(),
payerKeypair.publicKey,
);
// Initialize trader quote so they can receive.
await createAssociatedTokenAccountIdempotent(
connection,
payerKeypair,
market.quoteMint(),
payerKeypair.publicKey,
);

const amountBaseAtoms: number = 1_000_000_000;
const mintSig = await mintTo(
connection,
payerKeypair,
market.baseMint(),
traderBaseTokenAccount,
payerKeypair.publicKey,
amountBaseAtoms,
);
console.log(
`Minted ${amountBaseAtoms} to ${traderBaseTokenAccount} in ${mintSig}`,
);

// Note that this is a self-trade for simplicity.
await airdropSol(connection, payerKeypair.publicKey);
await createGlobal(connection, payerKeypair, market.quoteMint());
await depositGlobal(connection, payerKeypair, market.quoteMint(), 10_000);
await placeOrder(
connection,
payerKeypair,
marketAddress,
5,
5,
true,
OrderType.Global,
1,
NO_EXPIRATION_LAST_VALID_SLOT,
);

await swap(connection, payerKeypair, marketAddress, amountBaseAtoms, false);
await market.reload(connection);
market.prettyPrint();

// Verify that the resting order got matched and resulted in deposited base on
// the market. Quote came from global and got withdrawn in the swap. Because
// it is a self-trade, it resets to zero, so we need to check the wallet.
assert(
market.getWithdrawableBalanceTokens(payerKeypair.publicKey, false) == 0,
`Expected quote ${0} actual quote ${market.getWithdrawableBalanceTokens(payerKeypair.publicKey, false)}`,
);
assert(
market.getWithdrawableBalanceTokens(payerKeypair.publicKey, true) == 0,
`Expected base ${0} actual base ${market.getWithdrawableBalanceTokens(payerKeypair.publicKey, true)}`,
);
const baseBalance: number = (
await connection.getTokenAccountBalance(
await getAssociatedTokenAddress(
market.baseMint(),
payerKeypair.publicKey,
),
)
).value.uiAmount!;
const quoteBalance: number = (
await connection.getTokenAccountBalance(
await getAssociatedTokenAddress(
market.quoteMint(),
payerKeypair.publicKey,
),
)
).value.uiAmount!;
// Because of the self trade, it resets the wallet to pre-trade amount.
assert(
baseBalance == 1,
`Expected wallet base ${1} actual base ${baseBalance}`,
);
// 5 * 5, received from matching the global order.
assert(
quoteBalance == 25,
`Expected quote ${25} actual quote ${quoteBalance}`,
);
}

describe('Swap test', () => {
it('Swap', async () => {
await testSwap();
});
it('Swap against global', async () => {
// TODO: Enable once able to place global order through batch update
// await testSwapGlobal();
});
});
Loading

0 comments on commit 694fc0a

Please sign in to comment.