Skip to content

Commit

Permalink
feat: fetch all redstone payloads in one request
Browse files Browse the repository at this point in the history
  • Loading branch information
doomsower committed Aug 27, 2024
1 parent 05c7ee8 commit 30c75d4
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 91 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@gearbox-protocol/eslint-config": "2.0.0-next.2",
"@gearbox-protocol/liquidator-v2-contracts": "^2.1.0",
"@gearbox-protocol/prettier-config": "2.0.0-next.0",
"@gearbox-protocol/sdk-gov": "^2.18.2",
"@gearbox-protocol/sdk-gov": "^2.18.5",
"@gearbox-protocol/types": "^1.12.1",
"@redstone-finance/evm-connector": "^0.6.1",
"@types/node": "^22.5.0",
Expand All @@ -52,9 +52,9 @@
"pino": "^9.3.2",
"prettier": "^3.3.3",
"redstone-protocol": "^1.0.5",
"tsx": "^4.18.0",
"tsx": "^4.19.0",
"typescript": "^5.5.4",
"viem": "^2.20.0",
"viem": "^2.20.1",
"vitest": "^2.0.5"
},
"prettier": "@gearbox-protocol/prettier-config",
Expand Down
3 changes: 3 additions & 0 deletions src/services/OracleServiceV3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ interface PriceFeedEntry {
}

export interface RedstoneFeed {
/**
* Can be real token or ticker address
*/
token: Address;
dataFeedId: string;
reserve: boolean;
Expand Down
213 changes: 137 additions & 76 deletions src/services/RedstoneServiceV3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@gearbox-protocol/sdk-gov";
import { iCreditFacadeV3MulticallAbi } from "@gearbox-protocol/types/abi";
import { DataServiceWrapper } from "@redstone-finance/evm-connector";
import type { SignedDataPackage } from "redstone-protocol";
import { RedstonePayload } from "redstone-protocol";
import type { Address } from "viem";
import {
Expand All @@ -34,6 +35,18 @@ import type { PriceOnDemandExtras, PriceUpdate } from "./liquidate/index.js";
import type { RedstoneFeed } from "./OracleServiceV3.js";
import type OracleServiceV3 from "./OracleServiceV3.js";

interface RedstoneRequest {
originalToken: Address;
tokenOrTicker: Address;
reserve: boolean;
dataFeedId: string;
}

interface TimestampedCalldata {
callData: `0x${string}`;
ts: number;
}

interface RedstoneUpdate extends RedstoneFeed {
/**
* In case when Redstone feed is using ticker to updates, this will be the original token
Expand Down Expand Up @@ -113,7 +126,7 @@ export class RedstoneServiceV3 {
}

public async updatesForTokens(
tokens: string[],
tokens: Address[],
activeOnly: boolean,
logContext: Record<string, any> = {},
): Promise<PriceOnDemandExtras[]> {
Expand Down Expand Up @@ -168,18 +181,11 @@ export class RedstoneServiceV3 {
logger.debug(
`need to update ${redstoneUpdates.length} redstone feeds: ${printFeeds(redstoneUpdates)}`,
);
const result = await Promise.all(
redstoneUpdates.map(({ originalToken, token, dataFeedId, reserve }) =>
this.#getRedstonePayloadForManualUsage(
originalToken,
token,
reserve,
"redstone-primary-prod",
dataFeedId,
REDSTONE_SIGNERS.signersThreshold,
logContext,
),
),

const result = await this.#getRedstonePayloadForManualUsage(
redstoneUpdates,
"redstone-primary-prod",
REDSTONE_SIGNERS.signersThreshold,
);

if (this.config.optimistic && result.length > 0) {
Expand Down Expand Up @@ -245,7 +251,7 @@ export class RedstoneServiceV3 {
ca: CreditAccountData,
activeOnly = false,
): Promise<PriceUpdate[]> {
const accTokens: string[] = [];
const accTokens: Address[] = [];
for (const { token, balance, isEnabled } of ca.allBalances) {
if (isEnabled && balance > 10n) {
accTokens.push(token);
Expand Down Expand Up @@ -313,96 +319,96 @@ export class RedstoneServiceV3 {
}

async #getRedstonePayloadForManualUsage(
originalToken: Address,
tokenOrTicker: Address,
reserve: boolean,
updates: RedstoneUpdate[],
dataServiceId: string,
dataFeedId: string,
uniqueSignersCount: number,
logContext: Record<string, any> = {},
): Promise<PriceOnDemandExtras> {
): Promise<PriceOnDemandExtras[]> {
const logger = this.logger.child(logContext);
const cacheAllowed = this.config.optimistic;
const key = redstoneCacheKey(
tokenOrTicker,
reserve,

const networkUpdates: RedstoneUpdate[] = [];
const cachedResponses: PriceOnDemandExtras[] = [];

for (const upd of updates) {
const key = redstoneCacheKey(upd, dataServiceId, uniqueSignersCount);
if (cacheAllowed && this.#optimisticCache.has(key)) {
logger.debug(`using cached response for ${key}`);
cachedResponses.push(this.#optimisticCache.get(key)!);
} else {
networkUpdates.push(upd);
}
}

const networkResponses = await this.#fetchRedstonePayloadForManualUsage(
networkUpdates,
dataServiceId,
dataFeedId,
uniqueSignersCount,
);

if (cacheAllowed) {
if (this.#optimisticCache.has(key)) {
logger.debug(`using cached response for ${key}`);
return this.#optimisticCache.get(key)!;
for (const resp of networkResponses) {
const key = redstoneCacheKey(resp, dataServiceId, uniqueSignersCount);
this.#optimisticCache.set(key, resp);
}
}

this.logger.debug(
`got ${networkResponses.length} updates from redstone and ${cachedResponses.length} from cache`,
);

return [...networkResponses, ...cachedResponses];
}

async #fetchRedstonePayloadForManualUsage(
updates: RedstoneUpdate[],
dataServiceId: string,
uniqueSignersCount: number,
): Promise<PriceOnDemandExtras[]> {
const dataPayload = await new DataServiceWrapper({
dataServiceId,
dataPackagesIds: [dataFeedId],
dataPackagesIds: Array.from(new Set(updates.map(t => t.dataFeedId))),
uniqueSignersCount,
historicalTimestamp: HISTORICAL_BLOCKLIST.has(dataFeedId)
? undefined
: this.#optimisticTimestamp,
historicalTimestamp: this.#optimisticTimestamp,
}).prepareRedstonePayload(true);

const { signedDataPackages, unsignedMetadata } = RedstonePayload.parse(
toBytes(`0x${dataPayload}`),
);

const dataPackagesList = splitResponse(
signedDataPackages,
uniqueSignersCount,
);

const result = dataPackagesList.map(list => {
const payload = new RedstonePayload(
list,
bytesToString(unsignedMetadata),
// unsigned metadata looks like
// "1724772413180#0.6.1#redstone-primary-prod___"
// where 0.6.1 is @redstone-finance/evm-connector version
// and 1724772413180 is current timestamp
const parsed = RedstonePayload.parse(toBytes(`0x${dataPayload}`));
const packagesByDataFeedId = groupDataPackages(parsed.signedDataPackages);

const result: PriceOnDemandExtras[] = [];
for (const t of updates) {
const { dataFeedId, originalToken, reserve, token } = t;
const signedDataPackages = packagesByDataFeedId[dataFeedId];
if (!signedDataPackages) {
throw new Error(`cannot find data package for ${dataFeedId}`);
}
const calldataWithTs = getCalldataWithTimestamp(
signedDataPackages,
parsed.unsignedMetadata,
);

let ts = 0;
list.forEach(p => {
const newTimestamp = p.dataPackage.timestampMilliseconds / 1000;
if (ts === 0) {
ts = newTimestamp;
} else if (ts !== newTimestamp) {
throw new Error("Timestamps are not equal");
}
result.push({
dataFeedId,
originalToken,
token,
reserve,
...calldataWithTs,
});

return [
encodeAbiParameters(parseAbiParameters("uint256, bytes"), [
BigInt(ts),
`0x${payload.toBytesHexWithout0xPrefix()}`,
]),
ts,
] as const;
});

const response: PriceOnDemandExtras = {
originalToken,
token: tokenOrTicker,
reserve,
callData: result[0][0],
ts: result[0][1],
};

if (cacheAllowed) {
this.#optimisticCache.set(key, response);
}

return response;
return result;
}
}

function redstoneCacheKey(
token: Address,
reserve: boolean,
update: RedstoneUpdate,
dataServiceId: string,
dataFeedId: string,
uniqueSignersCount: number,
): string {
const { token, dataFeedId, reserve } = update;
return [
getTokenSymbolOrTicker(token),
reserve ? "reserve" : "main",
Expand All @@ -412,6 +418,61 @@ function redstoneCacheKey(
].join("|");
}

function groupDataPackages(
signedDataPackages: SignedDataPackage[],
): Record<string, SignedDataPackage[]> {
const packagesByDataFeedId: Record<string, SignedDataPackage[]> = {};
for (const p of signedDataPackages) {
const { dataPoints } = p.dataPackage;

// Check if all data points have the same dataFeedId
const dataFeedId0 = dataPoints[0].dataFeedId;
for (const dp of dataPoints) {
if (dp.dataFeedId !== dataFeedId0) {
throw new Error(
`data package contains data points with different dataFeedIds: ${dp.dataFeedId} and ${dataFeedId0}`,
);
}
}

// Group data packages by dataFeedId
if (!packagesByDataFeedId[dataFeedId0]) {
packagesByDataFeedId[dataFeedId0] = [];
}
packagesByDataFeedId[dataFeedId0].push(p);
}

return packagesByDataFeedId;
}

function getCalldataWithTimestamp(
packages: SignedDataPackage[],
unsignedMetadata: Uint8Array,
): TimestampedCalldata {
const payload = new RedstonePayload(
packages,
bytesToString(unsignedMetadata),
);

let ts = 0;
packages.forEach(p => {
const newTimestamp = p.dataPackage.timestampMilliseconds / 1000;
if (ts === 0) {
ts = newTimestamp;
} else if (ts !== newTimestamp) {
throw new Error("Timestamps are not equal");
}
});

return {
callData: encodeAbiParameters(parseAbiParameters("uint256, bytes"), [
BigInt(ts),
`0x${payload.toBytesHexWithout0xPrefix()}`,
]),
ts,
};
}

function splitResponse<T>(arr: T[], size: number): T[][] {
const chunks = [];

Expand Down
1 change: 1 addition & 0 deletions src/services/liquidate/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from "../../data/index.js";

export interface PriceOnDemandExtras extends PriceOnDemand {
dataFeedId: string;
/**
* In case when token in PriceOnDemand is ticker, this will be the original token
* Otherwise they are the same
Expand Down
24 changes: 12 additions & 12 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1586,10 +1586,10 @@
resolved "https://registry.yarnpkg.com/@gearbox-protocol/prettier-config/-/prettier-config-2.0.0-next.0.tgz#8183cfa8c6ee538543089961ecb4d03fe77045de"
integrity sha512-hDokre6TjEkpNdf+tTk/Gh2dTJpkJFgMPTpE7KS4KFddUqGLqDKMaE4/ZzBA8kvYNm5gSXytCwWrxPXO8kFKYA==

"@gearbox-protocol/sdk-gov@^2.18.2":
version "2.18.2"
resolved "https://registry.yarnpkg.com/@gearbox-protocol/sdk-gov/-/sdk-gov-2.18.2.tgz#0be92c2c36dc3824ebe748fb4c2ce74114099ac4"
integrity sha512-THIMyeHl7V9K8GPCGFHoC12NMjq+/A1IWKj0uNn/KT6KIQeaA8AvfSANLj21CaKeLWVVmSrecT5yXTixydoZNg==
"@gearbox-protocol/sdk-gov@^2.18.5":
version "2.18.5"
resolved "https://registry.yarnpkg.com/@gearbox-protocol/sdk-gov/-/sdk-gov-2.18.5.tgz#325d1fcc3d941a70bd740efdcf9acfd6e2b4df93"
integrity sha512-zsXlvPSh/rAyGw022o+iz73uBHzWENI/dMOc7N2theEDmMiU1vx7gXZofpZa9Frofk24U1EyFtRZmpd22g+O+Q==
dependencies:
ethers "6.12.1"
humanize-duration-ts "^2.1.1"
Expand Down Expand Up @@ -5856,10 +5856,10 @@ tslib@^2.6.2:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==

tsx@^4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.18.0.tgz#c5c6e8af9e7d162446ed22dc7b53dc4792bf920b"
integrity sha512-a1jaKBSVQkd6yEc1/NI7G6yHFfefIcuf3QJST7ZEyn4oQnxLYrZR5uZAM8UrwUa3Ge8suiZHcNS1gNrEvmobqg==
tsx@^4.19.0:
version "4.19.0"
resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.19.0.tgz#6166cb399b17d14d125e6158d23384045cfdf4f6"
integrity sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==
dependencies:
esbuild "~0.23.0"
get-tsconfig "^4.7.5"
Expand Down Expand Up @@ -5969,10 +5969,10 @@ uuid@^9.0.1:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==

viem@^2.20.0:
version "2.20.0"
resolved "https://registry.yarnpkg.com/viem/-/viem-2.20.0.tgz#abff4c2cf733bcc20978e662ea17db117a2881ef"
integrity sha512-cM4vs81HnSNbfceI1MLkx4pCVzbVjl9xiNSv5SCutYjUyFFOVSPDlEyhpg2iHinxx1NM4Qne3END5eLT8rvUdg==
viem@^2.20.1:
version "2.20.1"
resolved "https://registry.yarnpkg.com/viem/-/viem-2.20.1.tgz#91742e19c24e6294cf5c4015f1cba46afd0b95d7"
integrity sha512-a/BSe25TSfkc423GTSKYl1O0ON2J5huoQeOLkylHT1WS8wh3JFqb8nfAq7vg+aZ+W06BCTn36bbi47yp4D92Cg==
dependencies:
"@adraffy/ens-normalize" "1.10.0"
"@noble/curves" "1.4.0"
Expand Down

0 comments on commit 30c75d4

Please sign in to comment.