312 lines
11 KiB
JavaScript
312 lines
11 KiB
JavaScript
const assert = require("assert");
|
|
const anchor = require("@project-serum/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.
|
|
anchor.setProvider(anchor.Provider.env());
|
|
|
|
// 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.ok(tokenAChange === expectedResultantAmount);
|
|
assert.ok(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.ok(tokenAChange === -swapAmount);
|
|
assert.ok(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.ok(tokenAChange === -swapAmount);
|
|
// TODO: calculate this dynamically from the swap amount.
|
|
assert.ok(tokenBChange === 9.8);
|
|
assert.ok(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.ok(tokenAChange === 22.6);
|
|
assert.ok(tokenBChange === -swapAmount);
|
|
assert.ok(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;
|
|
}
|