Files
drift-keeper/auto-swap/src/_main.js
Patrick Balsiger f05fac48ca Multi-Cloud Deployment (#1)
* Introduce instances list to scale bots and add config templating

* Use env var for wallet address if not provided as path param

* Expose SOL price as metric

* Update docs

* Add auto-swap

* Add Panopticon

* implement backoff stategy in autoswap

* Add retry logic for withdraw and swap

* bump drift-sdk, update ctl.sh and docs

* Update filler bot, add tx metrics

* Add user-metrics

* Update build and dashboard
2024-04-28 07:25:08 +02:00

311 lines
12 KiB
JavaScript

require('dotenv').config()
const web3 = require("@solana/web3.js");
const drift = require("@drift-labs/sdk");
const LAMPORTS_PER_SOL = 1000000000;
const AUTOSWAP_INTERVAL = process.env.AUTOSWAP_INTERVAL || 1000 * 60;
const USDC_MARKET = 0;
const SOL_MARKET = 1;
const SWAP_RATIO = process.env.SWAP_RATIO || 0.5;
const SWAP_THRESHOLD = process.env.SWAP_THRESHOLD || 10;
const USDC_INT = 1000000;
const SOL_MINT_ADDRESS = 'So11111111111111111111111111111111111111112';
const USDC_MINT_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
const USDC_MINT_PUBLIC_KEY = new web3.PublicKey(USDC_MINT_ADDRESS);
const keyPairFile = process.env.PRIVATE_KEY || process.env.PRIVATE_KEY_FILE;
const wallet = new drift.Wallet(drift.loadKeypair(keyPairFile));
const connection = new web3.Connection(process.env.RPC_ENDPOINT);
const thresholdReached = (amount) => amount / USDC_INT > SWAP_THRESHOLD;
const driftClient = new drift.DriftClient({
connection,
wallet,
env: 'mainnet-beta',
activeSubAccountId: 0,
subAccountIds: [0],
});
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
}
const log = (msg) => {
console.log(`[${new Date().toISOString()}] ${msg}`)
};
const quoteNumber = (val) => {
return drift.convertToNumber(val, drift.QUOTE_PRECISION);
}
const baseNumber = (val) => {
return drift.convertToNumber(val, drift.BASE_PRECISION);
}
const quoteUsdcSol = async (amount) => {
const quoteUrl = `https://quote-api.jup.ag/v6/quote?inputMint=${USDC_MINT_ADDRESS}&outputMint=${SOL_MINT_ADDRESS}&amount=${amount}&slippageBps=50`;
const quoteResponse = await (
await fetch(quoteUrl)
).json();
return quoteResponse;
}
const getWalletBalance = async (connection, publicKey) => {
const lamportsBalance = await connection.getBalance(publicKey);
return lamportsBalance / LAMPORTS_PER_SOL;
};
const getUsdcBalance = async (connection, publicKey) => {
const balance = await connection.getParsedTokenAccountsByOwner(
publicKey, { mint: USDC_MINT_PUBLIC_KEY }
);
return balance.value[0]?.account.data.parsed.info.tokenAmount.uiAmount;
};
const printInfo = async (user) => {
let usdcInAccount = quoteNumber(user.getTokenAmount(USDC_MARKET));
let solInWallet = await getWalletBalance(connection, wallet.publicKey);
let usdcWallet = await getUsdcBalance(connection, wallet.publicKey)
log(`USDC in account: ${usdcInAccount}`);
log(`USDC in wallet: ${usdcWallet}`);
log(`SOL in wallet: ${solInWallet}`);
};
const calcSwapAmount = (usdcInAccount) => {
return usdcInAccount * SWAP_RATIO;
};
const getSerializedTransaction = async (quote) => {
return await (
await fetch('https://quote-api.jup.ag/v6/swap', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
quoteResponse: quote,
userPublicKey: wallet.publicKey.toString(),
wrapAndUnwrapSol: true,
dynamicComputeUnitLimit: true, // allow dynamic compute limit instead of max 1,400,000
prioritizationFeeLamports: 'auto' // capped at 5,000,000 lamports / 0.005 SOL.
})
})
).json();
};
const signTransaction = (swapTransaction) => {
const swapTransactionBuf = Buffer.from(swapTransaction, 'base64');
var transaction = web3.VersionedTransaction.deserialize(swapTransactionBuf);
transaction.sign([wallet.payer]);
return transaction;
};
const sendTransaction = async (transaction) => {
const rawTransaction = transaction.serialize()
return connection.sendRawTransaction(rawTransaction, {
skipPreflight: false,
preflightCommitment: 'confirmed',
// Maximum number of times for the RPC node to retry sending the transaction to the leader.
// If this parameter is not provided, the RPC node will retry the transaction until it is finalized or until the blockhash expires.
//maxRetries: 0
})
};
const withdraw = async (marketIndex, withdrawAmount) => {
const amount = driftClient.convertToSpotPrecision(marketIndex, withdrawAmount);
const associatedTokenAccount = await driftClient.getAssociatedTokenAccount(marketIndex);
const reduceOnly = true;
return driftClient.withdraw(
amount,
marketIndex,
associatedTokenAccount,
reduceOnly
);
};
const signAndSendTx = async (quote) => {
let tx = await getSerializedTransaction(quote);
let signedTx = signTransaction(tx.swapTransaction);
return sendTransaction(signedTx);
};
const confirmTx = async (txSig) => {
const latestBlockHash = await connection.getLatestBlockhash();
return connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: txSig,
}, 'confirmed');
};
const run2 = async () => {
await driftClient.subscribe();
const user = driftClient.getUser();
// force markets to be included
driftClient.perpMarketLastSlotCache.set(SOL_MARKET, Number.MAX_SAFE_INTEGER);
driftClient.perpMarketLastSlotCache.set(USDC_MARKET, Number.MAX_SAFE_INTEGER);
log('---------------------------------')
log("DriftClient initialized")
log(`Swap Ratio: ${SWAP_RATIO}`);
log(`Swap Threshold: ${SWAP_THRESHOLD} USDC`);
printInfo(user);
let swapInProgress = false;
let lastUsdcInAccount = await connection.getBalance(publicKey);
let lastSolInWallet = await getWalletBalance(connection, wallet.publicKey);
setInterval(async () => {
if (!swapInProgress) {
let currentUsdcInAccount = await connection.getBalance(publicKey);
let currentSolInWallet = await getWalletBalance(connection, wallet.publicKey);
let differenceSol = lastSolInWallet - currentSolInWallet;
let differenceUsdc = currentUsdcInAccount - lastUsdcInAccount;
await quoteUsdcSol(differenceUsdc)
.then(quote => {
log(`Swap: ${differenceUsdc / USDC_INT}$ to ${quote.outAmount / LAMPORTS_PER_SOL} SOL`);
//return signAndSendTx(quote);
})
lastUsdcInAccount = currentUsdcInAccount;
}
}, AUTOSWAP_INTERVAL);
};
const run = async () => {
await driftClient.subscribe();
const user = driftClient.getUser();
// force markets to be included
driftClient.perpMarketLastSlotCache.set(SOL_MARKET, Number.MAX_SAFE_INTEGER);
driftClient.perpMarketLastSlotCache.set(USDC_MARKET, Number.MAX_SAFE_INTEGER);
log('---------------------------------')
log("DriftClient initialized")
log(`Swap Ratio: ${SWAP_RATIO}`);
log(`Swap Threshold: ${SWAP_THRESHOLD} USDC`);
printInfo(user);
let swapInProgress = false;
setInterval(async () => {
if (!swapInProgress) {
try {
let usdcInAccount = user.getTokenAmount(USDC_MARKET);
let swapAmount = Math.floor(calcSwapAmount(usdcInAccount));
if (thresholdReached(usdcInAccount)) {
log('---------------------------------')
printInfo(user);
swapInProgress = true;
let withdrawSuccessful = false;
let withdrawRetries = 0;
let maxWithdrawRetries = 3;
while (!withdrawSuccessful && withdrawRetries < maxWithdrawRetries) {
log(`Withdraw from exchange`);
await withdraw(USDC_MARKET, quoteNumber(user.getTokenAmount(USDC_MARKET)))
.then(async (txSig) => {
log(`Confirm withdraw: https://solscan.io/tx/${txSig}`);
return confirmTx(txSig);
}, (error) => {
throw `Withdraw TX failed: ${error}`;
})
.then((confirmResult) => {
if (confirmResult.value.err) {
throw `Withdraw not confirmed`;
} else {
log(`Withdraw confirmed`);
withdrawSuccessful = true;
}
})
.catch(error => {
withdrawRetries++;
log(`Withdraw failed (${withdrawRetries}/${maxWithdrawRetries}): ${error}`);
// TODO check if withdraw was still somehow successfull
// it sometimes happens and we're then stuck in the loop
});
}
//backoff & try again in next iteration
if (withdrawRetries === maxWithdrawRetries) {
log('Withdraw failed - check if account balance changed and the withdraw actualy did not fail');
let currentUsdcInAccount = user.getTokenAmount(USDC_MARKET);
if(currentUsdcInAccount < usdcInAccount) {
log('Withdraw seemed to went through - continue with swap');
} else {
log('Withdraw really failed - backoff & try again in next iteration');
swapInProgress = false;
return;
}
}
let swapSuccessful = false
let swapRetries = 0;
let maxSwapRetries = 3;
while (!swapSuccessful && swapRetries < maxSwapRetries) {
// TODO calculate fresh swapAmount every iteration as collateral may has increased in the mean time
await quoteUsdcSol(swapAmount)
.then(quote => {
log(`Swap: ${swapAmount / USDC_INT}$ to ${quote.outAmount / LAMPORTS_PER_SOL} SOL`);
return signAndSendTx(quote);
})
.then(async txSig => {
log(`Confirm swap: https://solscan.io/tx/${txSig}`);
return confirmTx(txSig);
})
.then(confirmResult => {
if (confirmResult.value.err) {
throw `Confirm swap failed ${confirmResult.value.err}`;
} else {
log('Swap successful')
log('---------------------------------')
swapInProgress = false;
swapSuccessful = true;
}
})
.catch(error => {
swapRetries++;
log(`FAAAAAIL (${swapRetries}/${maxSwapRetries}): ${error}`);
})
sleep(1000);
}
//backoff & try again in next iteration
if (swapRetries === maxSwapRetries) {
log('Swap failed - backoff & try again in next iteration');
swapInProgress = false;
return;
}
} else {
log(`Waiting to reach threshold - current balance on DEX: ${usdcInAccount / USDC_INT} USDC`);
swapInProgress = false;
}
} catch (error) {
log(error);
swapInProgress = false;
}
} else {
log('Swap in progress')
}
}, AUTOSWAP_INTERVAL);
};
run2();