diff --git a/package.json b/package.json index 686d0b1..5051744 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,12 @@ "pino-pretty": "^11.2.2" }, "devDependencies": { - "@aws-sdk/client-s3": "^3.626.0", + "@aws-sdk/client-s3": "^3.627.0", "@commitlint/cli": "^19.4.0", "@commitlint/config-conventional": "^19.2.2", "@flashbots/ethers-provider-bundle": "^1.0.0", "@gearbox-protocol/eslint-config": "2.0.0-next.2", - "@gearbox-protocol/liquidator-v2-contracts": "^2.1.0-next.18", + "@gearbox-protocol/liquidator-v2-contracts": "^2.1.0-next.19", "@gearbox-protocol/prettier-config": "2.0.0-next.0", "@gearbox-protocol/sdk-gov": "^2.14.1", "@gearbox-protocol/types": "^1.11.0", @@ -54,7 +54,7 @@ "redstone-protocol": "^1.0.5", "tsx": "^4.17.0", "typescript": "^5.5.4", - "viem": "^2.19.2", + "viem": "^2.19.3", "vitest": "^2.0.5" }, "prettier": "@gearbox-protocol/prettier-config", diff --git a/src/services/RedstoneServiceV3.ts b/src/services/RedstoneServiceV3.ts index 769b6c6..0023ac9 100644 --- a/src/services/RedstoneServiceV3.ts +++ b/src/services/RedstoneServiceV3.ts @@ -34,6 +34,14 @@ import type { PriceOnDemandExtras, PriceUpdate } from "./liquidate/index.js"; import type { RedstoneFeed } from "./OracleServiceV3.js"; import type OracleServiceV3 from "./OracleServiceV3.js"; +interface RedstoneUpdate extends RedstoneFeed { + /** + * In case when Redstone feed is using ticker to updates, this will be the original token + * Otherwise they are the same + */ + originalToken: Address; +} + export type RedstonePriceFeed = Extract< PriceFeedData, { type: PriceFeedType.REDSTONE_ORACLE } @@ -113,12 +121,17 @@ export class RedstoneServiceV3 { const redstoneFeeds = this.oracle.getRedstoneFeeds(activeOnly); const tickers = tickerInfoTokensByNetwork[this.config.network]; - const redstoneUpdates: RedstoneFeed[] = []; + const redstoneUpdates: RedstoneUpdate[] = []; for (const t of tokens) { - const token = t.toLowerCase(); + const token = t.toLowerCase() as Address; const feeds = redstoneFeeds[token]; if (feeds?.length) { - redstoneUpdates.push(...feeds); + redstoneUpdates.push( + ...feeds.map(f => ({ + ...f, + originalToken: token, + })), + ); continue; } const symb = tokenSymbolByAddress[token]; @@ -130,8 +143,9 @@ export class RedstoneServiceV3 { `will update redstone ticker ${ticker.symbol} for ${symb}`, ); redstoneUpdates.push({ - dataFeedId: ticker.dataId, + originalToken: token, token: ticker.address, + dataFeedId: ticker.dataId, reserve: false, // tickers are always added as main feed }); } else { @@ -149,8 +163,9 @@ export class RedstoneServiceV3 { `need to update ${redstoneUpdates.length} redstone feeds: ${printFeeds(redstoneUpdates)}`, ); const result = await Promise.all( - redstoneUpdates.map(({ token, dataFeedId, reserve }) => + redstoneUpdates.map(({ originalToken, token, dataFeedId, reserve }) => this.#getRedstonePayloadForManualUsage( + originalToken, token, reserve, "redstone-primary-prod", @@ -238,8 +253,58 @@ export class RedstoneServiceV3 { })); } + /** + * Gets updates from redstone for multiple accounts at once + * Reduces duplication, so that we don't make redstone request twice if two accounts share a token + * + * @param accounts + * @param activeOnly + * @returns + */ + public async batchLiquidationPreviewUpdates( + accounts: CreditAccountData[], + activeOnly = false, + ): Promise> { + const tokensByAccount: Record> = {}; + const allTokens = new Set
(); + for (const ca of accounts) { + const accTokens = tokensByAccount[ca.addr] ?? new Set
(); + for (const { token, balance, isEnabled } of ca.allBalances) { + if (isEnabled && balance > 10n) { + accTokens.add(token); + allTokens.add(token); + } + } + tokensByAccount[ca.addr] = accTokens; + } + + const priceUpdates = await this.updatesForTokens( + Array.from(allTokens), + activeOnly, + ); + + const result: Record = {}; + for (const [accAddr, accTokens] of Object.entries(tokensByAccount)) { + const accUpdates: PriceUpdate[] = []; + // There can be 2 price feeds (main and reserve) per originalToken + for (const u of priceUpdates) { + if (accTokens.has(u.originalToken)) { + accUpdates.push({ + token: u.token, + reserve: u.reserve, + data: u.callData, + }); + } + } + result[accAddr as Address] = accUpdates; + } + + return result; + } + async #getRedstonePayloadForManualUsage( - token: Address, + originalToken: Address, + tokenOrTicker: Address, reserve: boolean, dataServiceId: string, dataFeedId: string, @@ -249,7 +314,7 @@ export class RedstoneServiceV3 { const logger = this.logger.child(logContext); const cacheAllowed = this.config.optimistic; const key = redstoneCacheKey( - token, + tokenOrTicker, reserve, dataServiceId, dataFeedId, @@ -305,8 +370,9 @@ export class RedstoneServiceV3 { ] as const; }); - const response = { - token, + const response: PriceOnDemandExtras = { + originalToken, + token: tokenOrTicker, reserve, callData: result[0][0], ts: result[0][1], diff --git a/src/services/liquidate/BatchLiquidator.ts b/src/services/liquidate/BatchLiquidator.ts index 1d76e9d..0a7ddad 100644 --- a/src/services/liquidate/BatchLiquidator.ts +++ b/src/services/liquidate/BatchLiquidator.ts @@ -105,6 +105,8 @@ export default class BatchLiquidator index: number, total: number, ): Promise { + const priceUpdatesByAccount = + await this.redstone.batchLiquidationPreviewUpdates(accounts); const inputs: EstimateBatchInput[] = []; for (const ca of accounts) { const cm = cms.find(m => ca.creditManager === m.address); @@ -113,9 +115,14 @@ export default class BatchLiquidator `cannot find credit manager data for ${ca.creditManager}`, ); } - inputs.push( - this.pathFinder.getEstimateBatchInput(ca, cm, this.config.slippage), + // pathfinder returns input without price updates + const input = this.pathFinder.getEstimateBatchInput( + ca, + cm, + this.config.slippage, ); + input.priceUpdates = priceUpdatesByAccount[ca.addr]; + inputs.push(input); } const { result } = await this.client.pub.simulateContract({ account: this.client.account, @@ -219,6 +226,7 @@ export default class BatchLiquidator pathAmount: "0", // TODO: ?? liquidatorPremium: (batch[a.addr]?.profit ?? 0n).toString(10), liquidatorProfit: "0", // cannot compute for single account + priceUpdates: priceUpdatesByAccount[a.addr], isError: !liquidated.has(a.addr), error: getError(a), batchId: `${index + 1}/${total}`, diff --git a/src/services/liquidate/types.ts b/src/services/liquidate/types.ts index 472f2b4..b3b024d 100644 --- a/src/services/liquidate/types.ts +++ b/src/services/liquidate/types.ts @@ -8,6 +8,11 @@ import type { } from "../../data/index.js"; export interface PriceOnDemandExtras extends PriceOnDemand { + /** + * In case when token in PriceOnDemand is ticker, this will be the original token + * Otherwise they are the same + */ + originalToken: Address; ts: number; reserve: boolean; } diff --git a/src/utils/ethers-6-temp/pathfinder/pathfinder.ts b/src/utils/ethers-6-temp/pathfinder/pathfinder.ts index b72be62..3acf345 100644 --- a/src/utils/ethers-6-temp/pathfinder/pathfinder.ts +++ b/src/utils/ethers-6-temp/pathfinder/pathfinder.ts @@ -117,6 +117,7 @@ export class PathFinder { pathOptions: pathOptions[0] ?? [], // TODO: what to put here? iterations: BigInt(LOOPS_PER_TX), force: false, + priceUpdates: [], }; } diff --git a/yarn.lock b/yarn.lock index 79d3ecb..cdcd8f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -88,10 +88,10 @@ "@smithy/util-utf8" "^2.0.0" tslib "^2.6.2" -"@aws-sdk/client-s3@^3.626.0": - version "3.626.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.626.0.tgz#02556328c2c05ae11a0ca9f0e5fba90ac6c80e74" - integrity sha512-+ul1NEdiAuq5L0lhxWb+FcQuw+1RKU4lNugdX/EF3Lr6Bpuo384K/4r9cRwOo/6PqRYMIengBMc9Q2HVAu8ZWg== +"@aws-sdk/client-s3@^3.627.0": + version "3.627.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.627.0.tgz#9a5fe33b15fa5613085f7f254faecc0cc150ad58" + integrity sha512-XTbtRLPVfq2lHo0SUP6HJb6HgBsKsJR54bhhVTwj5SZ4G26KOmx2iFOz9SgHie5apU7vWIhijb48LIhbLArgGg== dependencies: "@aws-crypto/sha1-browser" "5.2.0" "@aws-crypto/sha256-browser" "5.2.0" @@ -1455,10 +1455,10 @@ eslint-plugin-simple-import-sort "^10.0.0" eslint-plugin-unused-imports "^3.0.0" -"@gearbox-protocol/liquidator-v2-contracts@^2.1.0-next.18": - version "2.1.0-next.18" - resolved "https://registry.yarnpkg.com/@gearbox-protocol/liquidator-v2-contracts/-/liquidator-v2-contracts-2.1.0-next.18.tgz#701f57be55bac66ffea9306b7d572d77b2158d0c" - integrity sha512-OONhWYI5M+EEjVyvDlYqKcQqOCGF4uLj0fJYLCG3zA20mwOidnY8BBQPOof4D3UnPqiumBZ1hGXSRqyfQaqxhw== +"@gearbox-protocol/liquidator-v2-contracts@^2.1.0-next.19": + version "2.1.0-next.19" + resolved "https://registry.yarnpkg.com/@gearbox-protocol/liquidator-v2-contracts/-/liquidator-v2-contracts-2.1.0-next.19.tgz#858912a5e3ebf6f09dd563d3966f11a25daf70ba" + integrity sha512-5B1M1olNd5Dz2kobtCgw2+JSkMuknyMZ4tfm9H4VLDAoWDq+u8gJicfoAsgQ3q4FRb63vrZIEsjOY+h/VBDAlQ== "@gearbox-protocol/prettier-config@2.0.0-next.0": version "2.0.0-next.0" @@ -5816,10 +5816,10 @@ uuid@^9.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== -viem@^2.19.2: - version "2.19.2" - resolved "https://registry.yarnpkg.com/viem/-/viem-2.19.2.tgz#11f03621fd0d0d742f04e3da30fa49093a3cf612" - integrity sha512-BrR7fEEpuu9Om7obQGThb4BEu00PPHPKaUx+snB/F6yBZtr34FdXCPnphr+S73W2iIu/mt3yaRkfkLlD6a1R5g== +viem@^2.19.3: + version "2.19.3" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.19.3.tgz#537773f50d3a0e7436d2465898afc62c0c25c01c" + integrity sha512-djOw1X/jOvDOEMiol4g/T030MVncF2utC9G929ODNJ/00E7UXjSpwOeuyapmaqn831eSIHlELicZETYl2vI9oQ== dependencies: "@adraffy/ens-normalize" "1.10.0" "@noble/curves" "1.4.0"