anchor/tests/swap/tests/swap.js

316 lines
11 KiB
JavaScript

const { assert } = require("chai");
const anchor = require("@coral-xyz/anchor");
const BN = anchor.BN;
const OpenOrders = require("@project-serum/serum").OpenOrders;
const TOKEN_PROGRAM_ID = require("@solana/spl-token").TOKEN_PROGRAM_ID;
const serumCmn = require("@project-serum/common");
const utils = require("./utils");
// Taker fee rate (bps).
const TAKER_FEE = 0.0022;
describe("swap", () => {
// Configure the client to use the local cluster.
const provider = anchor.AnchorProvider.env();
// hack so we don't have to update serum-common library
// to the new AnchorProvider class and Provider interface
provider.send = provider.sendAndConfirm;
anchor.setProvider(provider);
// Swap program client.
const program = anchor.workspace.Swap;
// Accounts used to setup the orderbook.
let ORDERBOOK_ENV,
// Accounts used for A -> USDC swap transactions.
SWAP_A_USDC_ACCOUNTS,
// Accounts used for USDC -> A swap transactions.
SWAP_USDC_A_ACCOUNTS,
// Serum DEX vault PDA for market A/USDC.
marketAVaultSigner,
// Serum DEX vault PDA for market B/USDC.
marketBVaultSigner;
// Open orders accounts on the two markets for the provider.
const openOrdersA = anchor.web3.Keypair.generate();
const openOrdersB = anchor.web3.Keypair.generate();
it("BOILERPLATE: Sets up two markets with resting orders", async () => {
ORDERBOOK_ENV = await utils.setupTwoMarkets({
provider: program.provider,
});
});
it("BOILERPLATE: Sets up reusable accounts", async () => {
const marketA = ORDERBOOK_ENV.marketA;
const marketB = ORDERBOOK_ENV.marketB;
const [vaultSignerA] = await utils.getVaultOwnerAndNonce(
marketA._decoded.ownAddress
);
const [vaultSignerB] = await utils.getVaultOwnerAndNonce(
marketB._decoded.ownAddress
);
marketAVaultSigner = vaultSignerA;
marketBVaultSigner = vaultSignerB;
SWAP_USDC_A_ACCOUNTS = {
market: {
market: marketA._decoded.ownAddress,
requestQueue: marketA._decoded.requestQueue,
eventQueue: marketA._decoded.eventQueue,
bids: marketA._decoded.bids,
asks: marketA._decoded.asks,
coinVault: marketA._decoded.baseVault,
pcVault: marketA._decoded.quoteVault,
vaultSigner: marketAVaultSigner,
// User params.
openOrders: openOrdersA.publicKey,
orderPayerTokenAccount: ORDERBOOK_ENV.godUsdc,
coinWallet: ORDERBOOK_ENV.godA,
},
pcWallet: ORDERBOOK_ENV.godUsdc,
authority: program.provider.wallet.publicKey,
dexProgram: utils.DEX_PID,
tokenProgram: TOKEN_PROGRAM_ID,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
};
SWAP_A_USDC_ACCOUNTS = {
...SWAP_USDC_A_ACCOUNTS,
market: {
...SWAP_USDC_A_ACCOUNTS.market,
orderPayerTokenAccount: ORDERBOOK_ENV.godA,
},
};
});
it("Swaps from USDC to Token A", async () => {
const marketA = ORDERBOOK_ENV.marketA;
// Swap exactly enough USDC to get 1.2 A tokens (best offer price is 6.041 USDC).
const expectedResultantAmount = 7.2;
const bestOfferPrice = 6.041;
const amountToSpend = expectedResultantAmount * bestOfferPrice;
const swapAmount = new BN((amountToSpend / (1 - TAKER_FEE)) * 10 ** 6);
const [tokenAChange, usdcChange] = await withBalanceChange(
program.provider,
[ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godUsdc],
async () => {
await program.rpc.swap(Side.Bid, swapAmount, new BN(1.0), {
accounts: SWAP_USDC_A_ACCOUNTS,
instructions: [
// First order to this market so one must create the open orders account.
await OpenOrders.makeCreateAccountTransaction(
program.provider.connection,
marketA._decoded.ownAddress,
program.provider.wallet.publicKey,
openOrdersA.publicKey,
utils.DEX_PID
),
// Might as well create the second open orders account while we're here.
// In prod, this should actually be done within the same tx as an
// order to market B.
await OpenOrders.makeCreateAccountTransaction(
program.provider.connection,
ORDERBOOK_ENV.marketB._decoded.ownAddress,
program.provider.wallet.publicKey,
openOrdersB.publicKey,
utils.DEX_PID
),
],
signers: [openOrdersA, openOrdersB],
});
}
);
assert.strictEqual(tokenAChange, expectedResultantAmount);
assert.strictEqual(usdcChange, -swapAmount.toNumber() / 10 ** 6);
});
it("Swaps from Token A to USDC", async () => {
const marketA = ORDERBOOK_ENV.marketA;
// Swap out A tokens for USDC.
const swapAmount = 8.1;
const bestBidPrice = 6.004;
const amountToFill = swapAmount * bestBidPrice;
const takerFee = 0.0022;
const resultantAmount = new BN(amountToFill * (1 - TAKER_FEE) * 10 ** 6);
const [tokenAChange, usdcChange] = await withBalanceChange(
program.provider,
[ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godUsdc],
async () => {
await program.rpc.swap(
Side.Ask,
new BN(swapAmount * 10 ** 6),
new BN(swapAmount),
{
accounts: SWAP_A_USDC_ACCOUNTS,
}
);
}
);
assert.strictEqual(tokenAChange, -swapAmount);
assert.strictEqual(usdcChange, resultantAmount.toNumber() / 10 ** 6);
});
it("Swaps from Token A to Token B", async () => {
const marketA = ORDERBOOK_ENV.marketA;
const marketB = ORDERBOOK_ENV.marketB;
const swapAmount = 10;
const [tokenAChange, tokenBChange, usdcChange] = await withBalanceChange(
program.provider,
[ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godB, ORDERBOOK_ENV.godUsdc],
async () => {
// Perform the actual swap.
await program.rpc.swapTransitive(
new BN(swapAmount * 10 ** 6),
new BN(swapAmount - 1),
{
accounts: {
from: {
market: marketA._decoded.ownAddress,
requestQueue: marketA._decoded.requestQueue,
eventQueue: marketA._decoded.eventQueue,
bids: marketA._decoded.bids,
asks: marketA._decoded.asks,
coinVault: marketA._decoded.baseVault,
pcVault: marketA._decoded.quoteVault,
vaultSigner: marketAVaultSigner,
// User params.
openOrders: openOrdersA.publicKey,
// Swapping from A -> USDC.
orderPayerTokenAccount: ORDERBOOK_ENV.godA,
coinWallet: ORDERBOOK_ENV.godA,
},
to: {
market: marketB._decoded.ownAddress,
requestQueue: marketB._decoded.requestQueue,
eventQueue: marketB._decoded.eventQueue,
bids: marketB._decoded.bids,
asks: marketB._decoded.asks,
coinVault: marketB._decoded.baseVault,
pcVault: marketB._decoded.quoteVault,
vaultSigner: marketBVaultSigner,
// User params.
openOrders: openOrdersB.publicKey,
// Swapping from USDC -> B.
orderPayerTokenAccount: ORDERBOOK_ENV.godUsdc,
coinWallet: ORDERBOOK_ENV.godB,
},
pcWallet: ORDERBOOK_ENV.godUsdc,
authority: program.provider.wallet.publicKey,
dexProgram: utils.DEX_PID,
tokenProgram: TOKEN_PROGRAM_ID,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
},
}
);
}
);
assert.strictEqual(tokenAChange, -swapAmount);
// TODO: calculate this dynamically from the swap amount.
assert.strictEqual(tokenBChange, 9.8);
assert.strictEqual(usdcChange, 0);
});
it("Swaps from Token B to Token A", async () => {
const marketA = ORDERBOOK_ENV.marketA;
const marketB = ORDERBOOK_ENV.marketB;
const swapAmount = 23;
const [tokenAChange, tokenBChange, usdcChange] = await withBalanceChange(
program.provider,
[ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godB, ORDERBOOK_ENV.godUsdc],
async () => {
// Perform the actual swap.
await program.rpc.swapTransitive(
new BN(swapAmount * 10 ** 6),
new BN(swapAmount - 1),
{
accounts: {
from: {
market: marketB._decoded.ownAddress,
requestQueue: marketB._decoded.requestQueue,
eventQueue: marketB._decoded.eventQueue,
bids: marketB._decoded.bids,
asks: marketB._decoded.asks,
coinVault: marketB._decoded.baseVault,
pcVault: marketB._decoded.quoteVault,
vaultSigner: marketBVaultSigner,
// User params.
openOrders: openOrdersB.publicKey,
// Swapping from B -> USDC.
orderPayerTokenAccount: ORDERBOOK_ENV.godB,
coinWallet: ORDERBOOK_ENV.godB,
},
to: {
market: marketA._decoded.ownAddress,
requestQueue: marketA._decoded.requestQueue,
eventQueue: marketA._decoded.eventQueue,
bids: marketA._decoded.bids,
asks: marketA._decoded.asks,
coinVault: marketA._decoded.baseVault,
pcVault: marketA._decoded.quoteVault,
vaultSigner: marketAVaultSigner,
// User params.
openOrders: openOrdersA.publicKey,
// Swapping from USDC -> A.
orderPayerTokenAccount: ORDERBOOK_ENV.godUsdc,
coinWallet: ORDERBOOK_ENV.godA,
},
pcWallet: ORDERBOOK_ENV.godUsdc,
authority: program.provider.wallet.publicKey,
dexProgram: utils.DEX_PID,
tokenProgram: TOKEN_PROGRAM_ID,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
},
}
);
}
);
// TODO: calculate this dynamically from the swap amount.
assert.strictEqual(tokenAChange, 22.6);
assert.strictEqual(tokenBChange, -swapAmount);
assert.strictEqual(usdcChange, 0);
});
});
// Side rust enum used for the program's RPC API.
const Side = {
Bid: { bid: {} },
Ask: { ask: {} },
};
// Executes a closure. Returning the change in balances from before and after
// its execution.
async function withBalanceChange(provider, addrs, fn) {
const beforeBalances = [];
for (let k = 0; k < addrs.length; k += 1) {
beforeBalances.push(
(await serumCmn.getTokenAccount(provider, addrs[k])).amount
);
}
await fn();
const afterBalances = [];
for (let k = 0; k < addrs.length; k += 1) {
afterBalances.push(
(await serumCmn.getTokenAccount(provider, addrs[k])).amount
);
}
const deltas = [];
for (let k = 0; k < addrs.length; k += 1) {
deltas.push(
(afterBalances[k].toNumber() - beforeBalances[k].toNumber()) / 10 ** 6
);
}
return deltas;
}