@thespidercode/openbook-swap
Version:
Ready-to-use swap tool using Openbook DEX
221 lines (220 loc) • 11.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Side = exports.newSwap = exports.getSwapTransaction = exports.getCloseOpenOrdersInstruction = void 0;
const web3_js_1 = require("@solana/web3.js");
const dex_constant_1 = require("./constants/dex.constant");
const market_1 = require("./market");
const account_1 = require("./account");
const spl_token_1 = require("@solana/spl-token");
const bn_js_1 = __importDefault(require("bn.js"));
const instructions_1 = require("./serum/instructions");
const market_2 = require("./serum/market");
const getCloseOpenOrdersInstruction = (openOrders, market, owner) => {
// TODO: SHOULD WE LET THE USER CHOOSE THE PROGRAM ADDRESS?
const programAddress = new web3_js_1.PublicKey(dex_constant_1.DEX_ADDRESS);
const keys = [
{ pubkey: openOrders, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: true, isWritable: false },
{ pubkey: owner, isSigner: false, isWritable: true },
{ pubkey: market, isSigner: false, isWritable: false },
];
return new web3_js_1.TransactionInstruction({
keys,
programId: programAddress,
data: (0, instructions_1.encodeInstruction)({
closeOpenOrders: {},
}),
});
};
exports.getCloseOpenOrdersInstruction = getCloseOpenOrdersInstruction;
const getSwapTransaction = async (owner, side, limit, size, marketDetails, connection, onchain) => {
try {
const transaction = new web3_js_1.Transaction();
const programAddress = new web3_js_1.PublicKey(dex_constant_1.DEX_ADDRESS);
let marketInfo = null;
const market = await market_2.Market.load(connection, marketDetails.address, {}, programAddress);
// USING ONCHAIN DATA
if (onchain) {
marketInfo = (await (0, market_1.getMarketOrdersOnChain)(market.address, connection, market))?.market ?? null;
if (!marketInfo?.lowestAsk || !marketInfo.highestBid) {
throw ('Cannot get market information - please check your RPC and market address');
}
}
// USING API
// TODO: FINISH THIS THING
else {
let getMarketOrdersResponse = await (0, market_1.getMarketOrders)(marketDetails.address);
if (!getMarketOrdersResponse?.market)
throw ('Cannot get market information from API');
marketInfo = getMarketOrdersResponse.market;
}
if (marketInfo === null) {
throw ('Cannot get market information');
}
// ALSO CHECK IF WALLET HAS ENOUGH FUNDS
// SEE IF THIS 1.01 SLIPPAGE SHOULD BE VARIABLE
let accountDetails = await (0, account_1.getAccountDetail)(marketDetails, market, transaction, owner, connection, side == Side.Buy ? limit * size * 1.01 * web3_js_1.LAMPORTS_PER_SOL : 0);
if (!accountDetails || !accountDetails.baseTokenAccount || !accountDetails.quoteTokenAccount) {
return 'Cannot get account information';
}
// CHECK ALSO IF QUANTITY IS ENOUGH (FOR SOL SPECIFICALLY) BUT SHOULD BE DONE BEFORE IN THE UI
// ADDING AN EXTRA 0.5% SO THE PHANTOM ORDER AS A LITTLE ROOM TO MOVE
const margin = marketDetails.swapMargin + 0.005;
if ((side == Side.Buy && (limit * 1.05) < marketInfo.lowestAsk) || (side == Side.Sell && (limit * 0.95) > marketInfo.highestBid)) {
throw (`Oops quote changed, please refresh (old quote ${limit.toFixed(10)} and new quote ${side == Side.Buy ? marketInfo.lowestAsk.toFixed(10) : marketInfo.highestBid.toFixed(10)})`);
}
else {
// MAKE THE CALCULATION MORE PRECISE
const minimumReceive = +(side == Side.Buy ? +marketInfo.lowestAsk.toFixed(10) * (1 + margin) : +marketInfo.highestBid.toFixed(10) * (1 - margin)) * size;
console.log('Going to process the order at rate', side == Side.Buy ? marketInfo.lowestAsk.toFixed(10) : marketInfo.highestBid.toFixed(10), '. Should pay/receive max/min', minimumReceive.toFixed(10), 'qty', size);
}
const orderTransaction = await getOrderTransaction(market, side, side == Side.Buy ? marketInfo.lowestAsk * (1 + margin) : marketInfo.highestBid * (1 - margin), size, accountDetails, owner, margin, connection);
if (!orderTransaction) {
throw ('Cannot create order instruction');
}
transaction.add(orderTransaction.transaction);
const settleIx = getSettleInstruction(market, marketDetails, accountDetails, owner);
if (!settleIx) {
throw ('Cannot create settle instruction');
}
transaction.add(settleIx);
if (marketDetails.base.mint.toString() == spl_token_1.NATIVE_MINT.toString()) {
const closeAccountInstruction = (0, spl_token_1.createCloseAccountInstruction)(accountDetails.baseTokenAccount, owner, owner);
transaction.add(closeAccountInstruction);
}
else if (marketDetails.quote.mint.toString() == spl_token_1.NATIVE_MINT.toString()) {
const closeAccountInstruction = (0, spl_token_1.createCloseAccountInstruction)(accountDetails.quoteTokenAccount, owner, owner);
transaction.add(closeAccountInstruction);
}
return {
signers: (accountDetails.signers ? accountDetails.signers : []).concat(accountDetails.openOrders && accountDetails.openOrders.hasOwnProperty('_keypair') ? [accountDetails.openOrders] : []),
transaction: transaction,
isNewOpenOrders: (accountDetails.openOrders && accountDetails.openOrders.hasOwnProperty('_keypair')) ?? false
};
}
catch (error) {
console.log(error);
return error.toString();
}
};
exports.getSwapTransaction = getSwapTransaction;
const getOrderTransaction = async (market, side, price, size, accountDetails, owner, swapMargin, connection) => {
try {
if (!accountDetails.quoteTokenAccount || !accountDetails.baseTokenAccount) {
return null;
}
const programAddress = new web3_js_1.PublicKey(dex_constant_1.DEX_ADDRESS);
// TODO: WHY NOT USING THIS ONE: makeMatchOrdersTransaction ? Maybe the response to the partial fill
// CHECK DIFFERENCE WITH THIS ONE: makeNewOrderV3Instruction
if (accountDetails.openOrders?.hasOwnProperty('_keypair')) {
return await market.makePlaceOrderTransaction(connection, {
owner: owner,
payer: side == Side.Buy ? accountDetails.quoteTokenAccount : accountDetails.baseTokenAccount,
price: price * (side == Side.Buy ? (1 + swapMargin) : (1 - swapMargin)),
side,
size,
orderType: 'ioc',
selfTradeBehavior: "decrementTake",
openOrdersAccount: new web3_js_1.Account(accountDetails.openOrders.secretKey),
openOrdersAddressKey: accountDetails.openOrders.publicKey,
programId: programAddress
});
}
else {
return await market.makePlaceOrderTransaction(connection, {
owner: owner,
payer: side == Side.Buy ? accountDetails.quoteTokenAccount : accountDetails.baseTokenAccount,
price: price * (side == Side.Buy ? (1 + swapMargin) : (1 - swapMargin)),
side,
size,
orderType: 'ioc',
selfTradeBehavior: "decrementTake",
programId: programAddress
});
}
}
catch (error) {
console.log(error);
return null;
}
};
const getSettleInstruction = (market, marketDetails, accountDetails, owner) => {
try {
if (!accountDetails.openOrders || !accountDetails.baseTokenAccount || !accountDetails.quoteTokenAccount) {
return null;
}
const programAddress = new web3_js_1.PublicKey(dex_constant_1.DEX_ADDRESS);
let vaultSigner;
// SHOULD WE MANUALLY PUT THE VAULT SIGNER IN THE CONST?
// OR FIND A WAY TO AUTO FIND THEM
// MARKET INFO?
if (marketDetails.quote.mint.toString() === spl_token_1.NATIVE_MINT.toString()) {
vaultSigner = new web3_js_1.PublicKey('51Cdt3oASXuVD88tAqJEeR6XH3PjQQ3xb7Cd22KaW2GK');
}
else {
vaultSigner = web3_js_1.PublicKey.createProgramAddressSync([
market.address.toBuffer(),
new bn_js_1.default(1).toArrayLike(Buffer, 'le', 8), // ?? Might be when no PDAs
], programAddress);
}
const keys = [
{ pubkey: market.address, isSigner: false, isWritable: true },
{ pubkey: accountDetails.openOrders.hasOwnProperty('_keypair') ? accountDetails.openOrders.publicKey : accountDetails.openOrders, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: true, isWritable: false },
{ pubkey: marketDetails.base.vault, isSigner: false, isWritable: true },
{ pubkey: marketDetails.quote.vault, isSigner: false, isWritable: true },
{ pubkey: accountDetails.baseTokenAccount, isSigner: false, isWritable: true },
{ pubkey: accountDetails.quoteTokenAccount, isSigner: false, isWritable: true },
{ pubkey: vaultSigner, isSigner: false, isWritable: false },
{ pubkey: spl_token_1.TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
];
return new web3_js_1.TransactionInstruction({
keys,
programId: programAddress,
data: (0, instructions_1.encodeInstruction)({
settleFunds: {},
}),
});
}
catch (error) {
return null;
}
};
const newSwap = async (owner, swap, lowestAsk, highestBid, connection) => {
try {
const baseAmount = parseFloat(swap.inputAmounts.base) ?? 0;
const quoteAmount = parseFloat(swap.inputAmounts.quote) ?? 0;
if (swap.sell ? baseAmount == 0 : quoteAmount == 0) {
return { error: `Amount incorrect` };
}
if (swap.sell ? baseAmount < swap.market.minBase : swap.amounts.base < swap.market.minBase) {
return { error: `Must swap at least ${swap.market.minBase} ${swap.market.base.name}` };
}
if (!lowestAsk || !highestBid) {
return { error: `Error getting market data` };
}
const limit = swap.sell ? highestBid * (1 - swap.market.swapMargin) : lowestAsk * (1 + swap.market.swapMargin);
const size = swap.sell ? baseAmount : swap.amounts.base;
const side = swap.sell ? Side.Sell : Side.Buy;
const onchain = false;
const swapTransaction = await (0, exports.getSwapTransaction)(owner, side, limit, size, swap.market, connection, onchain);
if (typeof swapTransaction == 'string') {
return { error: `Swap error, ${swapTransaction}` };
}
else {
return { transaction: swapTransaction };
}
}
catch (error) {
return { error: `Swap error ${error}` };
}
};
exports.newSwap = newSwap;
var Side;
(function (Side) {
Side["Buy"] = "buy";
Side["Sell"] = "sell";
})(Side = exports.Side || (exports.Side = {}));