import { Connection, PublicKey, TransactionInstruction } from "@solana/web3.js"; import { Wallet } from "./txUtils"; import { Basket, FormattedRebalanceIntent, getJupTokenLedgerAndSwapInstructions, Intent, SymmetryCore, UIRebalanceIntent } from "."; import { getSwapPairs } from "./states/intents/rebalanceIntent"; import { PRIORITY_FEE } from "./constants"; export class KeeperMonitor { private params: { wallet: Wallet, connection: Connection, symmetryCore: SymmetryCore, network: "devnet" | "mainnet", jupiterApiKey: string; maxAllowedAccounts: number; simulateTransactions: boolean; } private intents: Map; private rebalanceIntents: Map; private baskets: Map; constructor(params: { wallet: Wallet, connection: Connection, network: "devnet" | "mainnet", jupiterApiKey: string, maxAllowedAccounts: number, priorityFee?: number, simulateTransactions?: boolean, }) { this.params = { wallet: params.wallet, connection: params.connection, symmetryCore: new SymmetryCore({ connection: params.connection, network: params.network, priorityFee: params.priorityFee ?? PRIORITY_FEE, }), network: params.network, jupiterApiKey: params.jupiterApiKey, maxAllowedAccounts: params.maxAllowedAccounts, simulateTransactions: params.simulateTransactions ?? false, }; //@ts-ignore this.globalConfig = {}; this.intents = new Map(); this.rebalanceIntents = new Map(); this.baskets = new Map(); } delay = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); async update() { let baskets try { baskets = await this.params.symmetryCore.fetchAllBaskets(); } catch (e) { console.log("Error fetching baskets:", e); } if (baskets) { for (let basket of baskets) this.baskets.set(basket.ownAddress.toBase58(), basket); } let intents; try { intents = await this.params.symmetryCore.fetchAllIntents(); } catch (e) { console.log("Error fetching intents:", e); } if (intents) { for (let intent of intents) { let oldInstance = this.intents.get(intent.ownAddress!.toBase58()); this.intents.set(intent.ownAddress!.toBase58(), intent); let isMonitored = oldInstance ? true : false; if (!isMonitored) { console.log("New Intent:", intent.ownAddress!.toBase58()); this.monitorIntent(intent.ownAddress!.toBase58()); } } for (let intent of this.intents.values()) { if (!intents.find(i => i.ownAddress!.equals(intent.ownAddress!))) { this.intents.delete(intent.ownAddress!.toBase58()); console.log("Intent deleted:", intent.ownAddress!.toBase58()); } } } let rebalanceIntents; try { rebalanceIntents = await this.params.symmetryCore.fetchAllRebalanceIntents(); } catch (e) { console.log("Error fetching rebalance intents:", e); } if (rebalanceIntents) { for (let rebalanceIntent of rebalanceIntents) { let oldInstance = this.rebalanceIntents.get(rebalanceIntent.formatted_data.pubkey); this.rebalanceIntents.set(rebalanceIntent.formatted_data.pubkey, rebalanceIntent); let isMonitored = oldInstance && ( oldInstance.formatted_data.current_action != "deposit_tokens" && oldInstance.formatted_data.current_action != "not_active" ); let shouldMonitor = rebalanceIntent && ( rebalanceIntent.formatted_data.current_action != "deposit_tokens" && rebalanceIntent.formatted_data.current_action != "not_active" ); if (!isMonitored && shouldMonitor) { console.log("New Rebalance Intent:", rebalanceIntent.formatted_data.pubkey); this.monitorRebalanceIntent(rebalanceIntent.formatted_data.pubkey); } } for (let rebalanceIntent of this.rebalanceIntents.values()) { if (!rebalanceIntents.find(i => i.formatted_data.pubkey == rebalanceIntent.formatted_data.pubkey)) { this.rebalanceIntents.delete(rebalanceIntent.formatted_data.pubkey); console.log("Rebalance Intent deleted:", rebalanceIntent.formatted_data.pubkey); } } } console.log("Baskets: ", this.baskets?.size, " Intents: ", this.intents.size, " Rebalance intents: ", this.rebalanceIntents.size); } async run(runTime?: number) { console.log("Starting keeper monitor", this.params.wallet.publicKey.toBase58()); if (!runTime) runTime = 10 * 60; if (runTime < 60) runTime = 60; let startTime = Date.now() / 1000; while (true) { let now = Date.now() / 1000; if (now + 45 > startTime + runTime) break; await this.update(); await this.delay(Math.min(30 * 1000, Math.max(0, startTime + runTime - now - 45 + 0.2) * 1000)); } this.baskets = new Map(); this.intents = new Map(); this.rebalanceIntents = new Map(); await this.delay(45 * 1000); } async monitorIntent(pubkey: string) { let nextCheckTime = 0; let numTries = 0; while (true) { let intent = this.intents.get(pubkey); if (!intent) break; let now = Date.now() / 1000; if (now < nextCheckTime) { await this.delay(Math.min(30 * 1000, Math.max(0, nextCheckTime - now + 0.2) * 1000)); continue; } nextCheckTime = now + 35; if (intent.formatted!.activation_timestamp > now) { nextCheckTime = intent.formatted!.activation_timestamp; continue; } if (now < intent.formatted!.expiration_timestamp && numTries >= 2) { nextCheckTime = intent.formatted!.expiration_timestamp; continue; } if (now < intent.formatted!.expiration_timestamp) { numTries += 1; try { let tx = await this.params.symmetryCore.executeBasketIntentTx({ keeper: this.params.wallet.publicKey.toBase58(), intent: intent.ownAddress!.toBase58(), }); let res = await this.params.symmetryCore.signAndSendTxPayloadBatchSequence({txPayloadBatchSequence: tx, wallet: this.params.wallet, simulateTransactions: this.params.simulateTransactions}); console.log("Execute Basket Intent -", pubkey, " : ", res); nextCheckTime = now + 60; } catch {} continue; } numTries = Math.max(2, numTries); if (numTries >= 4) break; numTries += 1; try { let tx = await this.params.symmetryCore.cancelBasketIntentTx({ keeper: this.params.wallet.publicKey.toBase58(), intent: intent.ownAddress!.toBase58(), }); let res = await this.params.symmetryCore.signAndSendTxPayloadBatchSequence({txPayloadBatchSequence: tx, wallet: this.params.wallet, simulateTransactions: this.params.simulateTransactions}); console.log("Cancel Basket Intent -", pubkey, " : ", res); nextCheckTime = now + 60; } catch (e) { if (numTries == 4) { console.log("Stop monitoring -", pubkey, " : ", e); } } continue; } } async monitorRebalanceIntent(pubkey: string) { let nextCheckTime = 0; let numTriesUpdatePrices = 0; let numTriesMint = 0; let numTriesRedeemTokens = 0; let numTriesClaimBounty = 0; let lastJupQuotesUpdate = 0; let jupQuotes: ({ inMint: string, outMint: string, tokenLedgerInstruction: TransactionInstruction, swapInstruction: TransactionInstruction, addressLookupTableAddresses: PublicKey[], quoteResponse: any, } | undefined)[] = []; while (true) { let uiIntent = this.rebalanceIntents.get(pubkey); if (!uiIntent) break; let intent = uiIntent.formatted_data; let chainData = uiIntent.chain_data; let now = Date.now() / 1000; if (now < nextCheckTime) { await this.delay(Math.min(30 * 1000, Math.max(0, nextCheckTime - now + 0.2) * 1000)); continue; } nextCheckTime = now + 35; if (intent.current_action == "not_active") continue; if (intent.current_action == "deposit_tokens") continue; if (intent.current_action == "update_prices" && intent.last_action_timestamp > now) { nextCheckTime = intent.last_action_timestamp; continue; } if (intent.current_action == "update_prices") { if (numTriesUpdatePrices >= 5) break; numTriesUpdatePrices += 1; try { let tx = await this.params.symmetryCore.updateTokenPricesTx({ keeper: this.params.wallet.publicKey.toBase58(), basket: intent.basket, rebalance_intent: intent.pubkey, }); let res = await this.params.symmetryCore.signAndSendTxPayloadBatchSequence({txPayloadBatchSequence: tx, wallet: this.params.wallet, simulateTransactions: this.params.simulateTransactions}); console.log("Update Prices - ", pubkey, " : ", res); } catch (e) { if (numTriesUpdatePrices == 4) { console.log("Stop monitoring - ", pubkey, " : ", e); } } nextCheckTime += 60; continue; } if (intent.auctions[2].end_time > now) { let basket = this.baskets.get(intent.basket); if (!basket) continue; let pairs = getSwapPairs(chainData, basket!); if (Date.now() / 1000 > lastJupQuotesUpdate + 60) { lastJupQuotesUpdate = Date.now() / 1000; jupQuotes = []; for (let pair of pairs) { if (pair.value < 0.005) continue; if (this.params.network == "mainnet") try { let res = await getJupTokenLedgerAndSwapInstructions({ keeper: this.params.wallet.publicKey, basketMintIn: new PublicKey(pair.inMint), basketMintOut: new PublicKey(pair.outMint), basketAmountIn: pair.inAmount, basketAmountOut: pair.outAmount, swapMode: "ioc", apiKey: this.params.jupiterApiKey, maxJupAccounts: this.params.maxAllowedAccounts, }); jupQuotes.push({ ...res, inMint: pair.inMint, outMint: pair.outMint, }); console.log("Fetch new Jup Quote:", pair.inMint, pair.outMint); console.log(pair, "Jup Quote:", parseFloat(res.quoteResponse?.outAmount ?? 0), "Requested In:", pair.inAmount); } catch {} } } for (let index = 0; index < pairs.length; index++) try { let pair = pairs[index]; let jupIndex = jupQuotes.findIndex(q => q && q.inMint == pair.inMint && q.outMint == pair.outMint); let quote = jupIndex >= 0 ? jupQuotes[jupIndex] : undefined; if (!quote && this.params.network == "mainnet") continue; if (pair.value < 0.005) continue; let tokenLedgerInstruction = quote?.tokenLedgerInstruction; let swapInstruction = quote?.swapInstruction; let addressLookupTableAddresses = quote?.addressLookupTableAddresses ?? []; let quoteResponse = quote?.quoteResponse; if (this.params.network == "mainnet" && !quoteResponse) continue; console.log(pair, "Jup Quote:", parseFloat(quoteResponse?.outAmount ?? 0), "Requested In:", pair.inAmount); let quoteResponseAmount = parseFloat(quoteResponse?.outAmount ?? 0); if (this.params.network == "mainnet" && quoteResponseAmount * 1.005 <= pair.inAmount) continue; try { let tx = await this.params.symmetryCore.flashSwapTx({ keeper: this.params.wallet.publicKey.toBase58(), basket: basket!.ownAddress.toBase58(), rebalance_intent: intent.pubkey, mint_in: pair.inMint, mint_out: pair.outMint, amount_in: pair.inAmount, amount_out: pair.outAmount, mode: 2, jup_token_ledger_ix: tokenLedgerInstruction, jup_swap_ix: swapInstruction, jup_address_lookup_table_addresses: addressLookupTableAddresses, }); let res = await this.params.symmetryCore.signAndSendTxPayloadBatchSequence({txPayloadBatchSequence: tx, wallet: this.params.wallet, simulateTransactions: this.params.simulateTransactions}); console.log("Flash Swap - ", pubkey, " : ", res); jupQuotes = jupQuotes.filter(q => q && (q.inMint != pair.inMint || q.outMint != pair.outMint)); } catch {} } catch { } nextCheckTime = (Date.now() / 1000) + 8; continue; } if (intent.rebalance_type == "deposit") { if (numTriesMint >= 3) break; lastJupQuotesUpdate = 0; numTriesMint += 1; try { let tx = await this.params.symmetryCore.mintTx({ keeper: this.params.wallet.publicKey.toBase58(), rebalance_intent: intent.pubkey, }); let res = await this.params.symmetryCore.signAndSendTxPayloadBatchSequence({txPayloadBatchSequence: tx, wallet: this.params.wallet, simulateTransactions: this.params.simulateTransactions}); console.log("Mint - ", pubkey, " : ", res); } catch (e) { if (numTriesMint == 3) { console.log("Stop monitoring - ", pubkey, " : ", e); } } continue; } let hasTokens = intent.tokens.find(token => token.amount > 0); if (hasTokens && intent.rebalance_type == "withdraw") { if (numTriesRedeemTokens >= 3) break; numTriesRedeemTokens += 1; try { let tx = await this.params.symmetryCore.redeemTokensTx({ keeper: this.params.wallet.publicKey.toBase58(), rebalance_intent: intent.pubkey, }); let res = await this.params.symmetryCore.signAndSendTxPayloadBatchSequence({txPayloadBatchSequence: tx, wallet: this.params.wallet, simulateTransactions: this.params.simulateTransactions}); console.log("Redeem Tokens:", res); } catch (e) { if (numTriesRedeemTokens == 3) { console.log("Stop monitoring - ", pubkey, " : ", e); } } continue; } if (numTriesClaimBounty >= 3) break; numTriesClaimBounty += 1; try { let tx = await this.params.symmetryCore.claimBountyTx({ keeper: this.params.wallet.publicKey.toBase58(), rebalance_intent: intent.pubkey, }); let res = await this.params.symmetryCore.signAndSendTxPayloadBatchSequence({txPayloadBatchSequence: tx, wallet: this.params.wallet, simulateTransactions: this.params.simulateTransactions}); console.log("Claim Bounty - ", pubkey, " : ", res); nextCheckTime = now + 60; } catch (e) { if (numTriesClaimBounty == 3) { console.log("Stop monitoring - ", pubkey, " : ", e); } } continue; } } } export class RebalanceHandler { private params: { wallet: Wallet, connection: Connection, symmetryCore: SymmetryCore, network: "devnet" | "mainnet", jupiterApiKey: string; maxAllowedAccounts: number; simulateTransactions: boolean; } private intent: UIRebalanceIntent; private basket: Basket; constructor(params: { intent: UIRebalanceIntent, basket: Basket, wallet: Wallet, connection: Connection, network: "devnet" | "mainnet", jupiterApiKey: string, maxAllowedAccounts: number, priorityFee?: number, simulateTransactions?: boolean, }) { this.params = { wallet: params.wallet, connection: params.connection, symmetryCore: new SymmetryCore({ connection: params.connection, network: params.network, priorityFee: params.priorityFee ?? PRIORITY_FEE, }), network: params.network, jupiterApiKey: params.jupiterApiKey, maxAllowedAccounts: params.maxAllowedAccounts, simulateTransactions: params.simulateTransactions ?? false, }; this.intent = params.intent; this.basket = params.basket; } delay = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); async refresh() { this.intent = await this.params.symmetryCore.fetchRebalanceIntent(this.intent.formatted_data.pubkey); this.basket = await this.params.symmetryCore.fetchBasket(this.intent.formatted_data.basket); } static async run(params: { intentPubkey: PublicKey, wallet: Wallet, connection: Connection, network: "devnet" | "mainnet", jupiterApiKey: string, maxAllowedAccounts: number, priorityFee?: number, simulateTransactions?: boolean, }) { let symmetryCore = new SymmetryCore({ connection: params.connection, network: params.network, priorityFee: params.priorityFee ?? PRIORITY_FEE, }); let intent = await symmetryCore.fetchRebalanceIntent(params.intentPubkey.toBase58()); let basket = await symmetryCore.fetchBasket(intent.formatted_data.basket); let handler = new RebalanceHandler({ intent, basket, wallet: params.wallet, connection: params.connection, network: params.network, jupiterApiKey: params.jupiterApiKey, maxAllowedAccounts: params.maxAllowedAccounts, simulateTransactions: params.simulateTransactions, }); handler.execute(); for (let i = 0; i < 20; i++) { await handler.delay(15 * 1000); try { await handler.refresh(); } catch (e) { handler.intent = undefined as any; break;} } } private async execute() { console.log("Starting rebalance handler for intent:", this.intent.formatted_data.pubkey); let nextCheckTime = 0; let numTriesUpdatePrices = 0; let numTriesMint = 0; let numTriesRedeemTokens = 0; let numTriesClaimBounty = 0; let rebalancePairs: { inMint: string, outMint: string, inAmount: number, outAmount: number, value: number, }[] = []; let lastJupQuotesUpdate = 0; let jupQuotes: ({ inMint: string, outMint: string, tokenLedgerInstruction: TransactionInstruction, swapInstruction: TransactionInstruction, addressLookupTableAddresses: PublicKey[], quoteResponse: any, }|undefined)[] = []; while (true) { if (!this.intent) break; let intent = this.intent.formatted_data; let chainData = this.intent.chain_data; let now = Date.now() / 1000; if (now < nextCheckTime) { await this.delay(Math.min(30 * 1000, Math.max(0, nextCheckTime - now + 0.2) * 1000)); continue; } nextCheckTime = now + 5; if (intent.current_action == "not_active") { console.log("Intent not active, stopping"); break; } if (intent.current_action == "deposit_tokens") { console.log("Waiting for deposit..."); continue; } if (intent.current_action == "update_prices" && intent.last_action_timestamp > now) { nextCheckTime = intent.last_action_timestamp; continue; } if (intent.current_action == "update_prices") { if (numTriesUpdatePrices >= 3) { console.log("Max retries for update_prices"); break; } numTriesUpdatePrices += 1; try { let tx = await this.params.symmetryCore.updateTokenPricesTx({ keeper: this.params.wallet.publicKey.toBase58(), basket: intent.basket, rebalance_intent: intent.pubkey, }); let res = await this.params.symmetryCore.signAndSendTxPayloadBatchSequence({txPayloadBatchSequence: tx, wallet: this.params.wallet, simulateTransactions: this.params.simulateTransactions}); console.log("Update Prices:", res); } catch (e) { if (numTriesUpdatePrices == 3) { console.log("Stop - update prices failed:", e); } } continue; } if (intent.auctions[2].end_time > now) { rebalancePairs = getSwapPairs(chainData, this.basket); if (Date.now() / 1000 > lastJupQuotesUpdate + 60) { let usedValue: Map = new Map(); jupQuotes = []; for (let pair of rebalancePairs) { let inValue = usedValue.get(pair.inMint) ?? 0; let outValue = usedValue.get(pair.outMint) ?? 0; let swapValue = Math.min(pair.value - inValue, pair.value - outValue); if (swapValue < 0.005) { jupQuotes.push(undefined); continue; } usedValue.set(pair.inMint, inValue + swapValue); usedValue.set(pair.outMint, outValue + swapValue); let res = undefined; if (this.params.network == "mainnet") try { res = { ...(await getJupTokenLedgerAndSwapInstructions({ keeper: this.params.wallet.publicKey, basketMintIn: new PublicKey(pair.inMint), basketMintOut: new PublicKey(pair.outMint), basketAmountIn: pair.inAmount, basketAmountOut: pair.outAmount, swapMode: "ioc", apiKey: this.params.jupiterApiKey, maxJupAccounts: this.params.maxAllowedAccounts, })), inMint: pair.inMint, outMint: pair.outMint, }; } catch {}; jupQuotes.push(res); } } for (let index = 0; index < rebalancePairs.length; index++) try { let pair = rebalancePairs[index]; let quote = jupQuotes.find(q => q && q.inMint == pair.inMint && q.outMint == pair.outMint); if (!quote) continue; if (pair.value < 0.005) continue; let { tokenLedgerInstruction, swapInstruction, addressLookupTableAddresses, quoteResponse } = quote; if (!quoteResponse) continue; console.log(pair, "Jup Quote:", parseFloat(quoteResponse.outAmount), "Requested In:", pair.inAmount); if (parseFloat(quoteResponse.outAmount) <= pair.inAmount && this.params.network == "mainnet") continue; try { let tx = await this.params.symmetryCore.flashSwapTx({ keeper: this.params.wallet.publicKey.toBase58(), basket: this.basket.ownAddress.toBase58(), rebalance_intent: intent.pubkey, mint_in: pair.inMint, mint_out: pair.outMint, amount_in: pair.inAmount, amount_out: pair.outAmount, mode: 2, jup_token_ledger_ix: tokenLedgerInstruction, jup_swap_ix: swapInstruction, jup_address_lookup_table_addresses: addressLookupTableAddresses, }); let res = await this.params.symmetryCore.signAndSendTxPayloadBatchSequence({txPayloadBatchSequence: tx, wallet: this.params.wallet, simulateTransactions: this.params.simulateTransactions}); console.log("Flash Swap:", res); rebalancePairs = rebalancePairs.splice(index, 1); index -= 1; } catch {} } catch { } nextCheckTime = (Date.now() / 1000) + 10; continue; } if (intent.rebalance_type == "deposit") { if (numTriesMint >= 3) break; lastJupQuotesUpdate = 0; numTriesMint += 1; try { let tx = await this.params.symmetryCore.mintTx({ keeper: this.params.wallet.publicKey.toBase58(), rebalance_intent: intent.pubkey, }); let res = await this.params.symmetryCore.signAndSendTxPayloadBatchSequence({txPayloadBatchSequence: tx, wallet: this.params.wallet, simulateTransactions: this.params.simulateTransactions}); console.log("Mint -", res); } catch (e) { if (numTriesMint == 3) { console.log("Stop monitoring -", e); } } continue; } let hasTokens = intent.tokens.find(token => token.amount > 0); if (hasTokens && intent.rebalance_type == "withdraw") { if (numTriesRedeemTokens >= 3) break; numTriesRedeemTokens += 1; try { let tx = await this.params.symmetryCore.redeemTokensTx({ keeper: this.params.wallet.publicKey.toBase58(), rebalance_intent: intent.pubkey, }); let res = await this.params.symmetryCore.signAndSendTxPayloadBatchSequence({txPayloadBatchSequence: tx, wallet: this.params.wallet, simulateTransactions: this.params.simulateTransactions}); console.log("Redeem Tokens -", res); } catch (e) { if (numTriesRedeemTokens == 3) { console.log("Stop monitoring -", e); } } continue; } if (numTriesClaimBounty >= 3) break; numTriesClaimBounty += 1; try { let tx = await this.params.symmetryCore.claimBountyTx({ keeper: this.params.wallet.publicKey.toBase58(), rebalance_intent: intent.pubkey, }); let res = await this.params.symmetryCore.signAndSendTxPayloadBatchSequence({txPayloadBatchSequence: tx, wallet: this.params.wallet, simulateTransactions: this.params.simulateTransactions}); console.log("Claim Bounty -", res); nextCheckTime = now + 60; } catch (e) { if (numTriesClaimBounty == 3) { console.log("Stop monitoring -", e); } } continue; } } }