Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce price updates #6

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "index.js",
"types": "index.d.ts",
"dependencies": {
"@synthetixio/v3-contracts": "^6.10.0",
"@synthetixio/v3-contracts": "^6.13.0",
"viem": "^2.13.5"
}
}
3 changes: 0 additions & 3 deletions liquidity/lib/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ export const MAXUINT = new Wei(constants.MaxUint256);
export const GWEI_DECIMALS = 9;
export const GAS_LIMIT_MULTIPLIER = 1.5;

export const DEFAULT_QUERY_REFRESH_INTERVAL = 600_000; // 10min
export const DEFAULT_QUERY_STALE_TIME = 300_000; // 5min

export const INFURA_KEY = process.env.INFURA_KEY || '8678fe160b1f4d45ad3f3f71502fc57b';
export const ONBOARD_KEY = 'sec_jykTuCK0ZuqXWf3wNYqizxs2';

Expand Down
1 change: 1 addition & 0 deletions liquidity/lib/useAllPriceFeeds/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useAllPriceFeeds';
12 changes: 12 additions & 0 deletions liquidity/lib/useAllPriceFeeds/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@snx-v3/useAllPriceFeeds",
"private": true,
"main": "index.ts",
"version": "0.0.2",
"dependencies": {
"@snx-v3/contracts": "workspace:*",
"@snx-v3/useBlockchain": "workspace:*",
"@tanstack/react-query": "^5.8.3",
"react": "^18.2.0"
}
}
26 changes: 26 additions & 0 deletions liquidity/lib/useAllPriceFeeds/useAllPriceFeeds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { importExtras } from '@snx-v3/contracts';
import { Network, useNetwork } from '@snx-v3/useBlockchain';
import { useQuery } from '@tanstack/react-query';

export function useAllPriceFeeds(customNetwork?: Network) {
const { network } = useNetwork();
const targetNetwork = customNetwork || network;
return useQuery({
enabled: Boolean(targetNetwork?.id && targetNetwork?.preset),
queryKey: [targetNetwork?.id, 'AllPriceFeeds'],
queryFn: async () => {
if (!(targetNetwork?.id && targetNetwork?.preset)) {
throw 'OMFG';
}
const extras = await importExtras(targetNetwork.id, targetNetwork.preset);
return Object.entries(extras)
.filter(
([key, value]) =>
String(value).length === 66 &&
(key.startsWith('pyth_feed_id_') || (key.startsWith('pyth') && key.endsWith('FeedId')))
)
.map(([, value]) => value);
},
staleTime: 60 * 60 * 1000,
});
}
45 changes: 45 additions & 0 deletions liquidity/lib/useErrorParser/decodeBuiltinErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ethers } from 'ethers';

export function decodeBuiltinErrors(data: string) {
let sighash = ethers.utils.id('Panic(uint256)').slice(0, 10);
if (data.startsWith(sighash)) {
// this is the `Panic` builtin opcode
const reason = ethers.utils.defaultAbiCoder.decode(['uint256'], '0x' + data.slice(10))[0];
switch (reason.toNumber()) {
case 0x00:
return { name: 'Panic("generic/unknown error")', sighash, args: [{ reason }] };
case 0x01:
return { name: 'Panic("assertion failed")', sighash, args: [{ reason }] };
case 0x11:
return { name: 'Panic("unchecked underflow/overflow")', sighash, args: [{ reason }] };
case 0x12:
return { name: 'Panic("division by zero")', sighash, args: [{ reason }] };
case 0x21:
return { name: 'Panic("invalid number to enum conversion")', sighash, args: [{ reason }] };
case 0x22:
return {
name: 'Panic("access to incorrect storage byte array")',
sighash,
args: [{ reason }],
};
case 0x31:
return { name: 'Panic("pop() empty array")', sighash, args: [{ reason }] };
case 0x32:
return { name: 'Panic("out of bounds array access")', sighash, args: [{ reason }] };
case 0x41:
return { name: 'Panic("out of memory")', sighash, args: [{ reason }] };
case 0x51:
return { name: 'Panic("invalid internal function")', sighash, args: [{ reason }] };
default:
return { name: 'Panic("unknown")', sighash, args: [{ reason }] };
}
}
sighash = ethers.utils.id('Error(string)').slice(0, 10);
if (data.startsWith(sighash)) {
// this is the `Error` builtin opcode
const reason = ethers.utils.defaultAbiCoder.decode(['string'], '0x' + data.slice(10));
return { name: `Error("${reason}")`, sighash, args: [{ reason }] };
}

return;
}
59 changes: 59 additions & 0 deletions liquidity/lib/useErrorParser/decodePythErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ethers } from 'ethers';

export const PythErrorsABI = [
// Function arguments are invalid (e.g., the arguments lengths mismatch)
// Signature: 0xa9cb9e0d
'error InvalidArgument()',
// Update data is coming from an invalid data source.
// Signature: 0xe60dce71
'error InvalidUpdateDataSource()',
// Update data is invalid (e.g., deserialization error)
// Signature: 0xe69ffece
'error InvalidUpdateData()',
// Insufficient fee is paid to the method.
// Signature: 0x025dbdd4
'error InsufficientFee()',
// There is no fresh update, whereas expected fresh updates.
// Signature: 0xde2c57fa
'error NoFreshUpdate()',
// There is no price feed found within the given range or it does not exists.
// Signature: 0x45805f5d
'error PriceFeedNotFoundWithinRange()',
// Price feed not found or it is not pushed on-chain yet.
// Signature: 0x14aebe68
'error PriceFeedNotFound()',
// Requested price is stale.
// Signature: 0x19abf40e
'error StalePrice()',
// Given message is not a valid Wormhole VAA.
// Signature: 0x2acbe915
'error InvalidWormholeVaa()',
// Governance message is invalid (e.g., deserialization error).
// Signature: 0x97363b35
'error InvalidGovernanceMessage()',
// Governance message is not for this contract.
// Signature: 0x63daeb77
'error InvalidGovernanceTarget()',
// Governance message is coming from an invalid data source.
// Signature: 0x360f2d87
'error InvalidGovernanceDataSource()',
// Governance message is old.
// Signature: 0x88d1b847
'error OldGovernanceMessage()',
// The wormhole address to set in SetWormholeAddress governance is invalid.
// Signature: 0x13d3ed82
'error InvalidWormholeAddressToSet()',
];

export function decodePythErrors(data: string) {
const PythInterface = new ethers.utils.Interface(PythErrorsABI);
try {
const decodedError = PythInterface.parseError(data);
Object.assign(decodedError, {
name: `PythError.${decodedError.name}`,
});
return decodedError;
} catch (e) {
// whatever
}
}
4 changes: 4 additions & 0 deletions liquidity/lib/useErrorParser/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './useErrorParser';
export * from './parseError';
export * from './decodeBuiltinErrors';
export * from './decodePythErrors';
12 changes: 12 additions & 0 deletions liquidity/lib/useErrorParser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@snx-v3/useErrorParser",
"private": true,
"main": "index.ts",
"version": "0.0.2",
"dependencies": {
"@snx-v3/contracts": "workspace:*",
"@snx-v3/useBlockchain": "workspace:*",
"ethers": "^5.7.2",
"react": "^18.2.0"
}
}
102 changes: 102 additions & 0 deletions liquidity/lib/useErrorParser/parseError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* eslint-disable no-console */
import { importAllErrors } from '@snx-v3/contracts';
import { ethers } from 'ethers';
import { decodeBuiltinErrors } from './decodeBuiltinErrors';
import { decodePythErrors } from './decodePythErrors';

export async function parseError({
error,
chainId,
preset,
}: {
error: Error | any;
chainId: number;
preset: string;
}) {
const errorData =
error?.error?.error?.error?.data ||
error?.error?.error?.data ||
error?.error?.data?.data ||
error?.error?.data ||
error?.data?.data ||
error?.data;

if (typeof errorData !== 'string') {
console.log('Error data missing', { error });
throw error;
}
const AllErrorsContract = await importAllErrors(chainId, preset);
const errorParsed = (() => {
try {
const panic = decodeBuiltinErrors(errorData);
if (panic) {
return panic;
}
const pythError = decodePythErrors(errorData);
if (pythError) {
return pythError;
}
const AllErrorsInterface = new ethers.utils.Interface(AllErrorsContract.abi);
const data = AllErrorsInterface.parseError(errorData);
console.log({ decodedError: data });

if (
data?.name === 'OracleDataRequired' &&
data?.args?.oracleContract &&
data?.args?.oracleQuery
) {
const oracleAddress = data?.args?.oracleContract;
const oracleQueryRaw = data?.args?.oracleQuery;

let updateType, publishTime, stalenessTolerance, feedIds, priceId;
// eslint-disable-next-line prefer-const
[updateType, publishTime, priceId] = ethers.utils.defaultAbiCoder.decode(
['uint8', 'uint64', 'bytes32'],
oracleQueryRaw
);

if (updateType === 1) {
[updateType, stalenessTolerance, feedIds] = ethers.utils.defaultAbiCoder.decode(
['uint8', 'uint64', 'bytes32[]'],
oracleQueryRaw
);
publishTime = undefined;
} else {
feedIds = [priceId];
}
Object.assign(error, {
name: data.name,
error,
args: {
oracleAddress,
oracleQuery: {
updateType,
publishTime: Number(publishTime),
stalenessTolerance: Number(stalenessTolerance),
feedIds,
},
oracleQueryRaw,
},
signature: data.signature,
sighash: data.sighash,
errorFragment: data.errorFragment,
});
return error;
}
return data;
} catch (e) {
console.log(e);
}
return error;
})();
if (!errorParsed.name) {
throw error;
}
const args = errorParsed?.args
? Object.fromEntries(
Object.entries(errorParsed.args).filter(([key]) => `${parseInt(key)}` !== key)
)
: {};
error.message = `${errorParsed?.name}, ${errorParsed?.sighash} (${JSON.stringify(args)})`;
throw error;
}
17 changes: 17 additions & 0 deletions liquidity/lib/useErrorParser/useErrorParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Network, useNetwork } from '@snx-v3/useBlockchain';
import React from 'react';
import { parseError } from './parseError';

export function useErrorParser(customNetwork?: Network) {
const { network: walletNetwork } = useNetwork();
const network = customNetwork ? customNetwork : walletNetwork;
return React.useCallback(
async (error: Error) => {
if (network?.id && network?.preset) {
return await parseError({ error, chainId: network?.id, preset: network?.preset });
}
throw error;
},
[network?.id, network?.preset]
);
}
4 changes: 3 additions & 1 deletion liquidity/lib/useLiquidityPosition/useLiquidityPosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ export const useLiquidityPosition = ({
{ priceUpdateTxHash },
],
staleTime: 60000 * 5,
enabled: !!tokenAddress && !!accountId,
enabled: Boolean(
CoreProxy && accountId && poolId && tokenAddress && systemToken && network && provider
),
queryFn: async () => {
if (
!(CoreProxy && accountId && poolId && tokenAddress && systemToken && network && provider)
Expand Down
63 changes: 63 additions & 0 deletions liquidity/lib/usePriceUpdateTxn/fetchPriceUpdateTxn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* eslint-disable no-console */
import { EvmPriceServiceConnection } from '@pythnetwork/pyth-evm-js';
import { ethers } from 'ethers';

export async function fetchPriceUpdateTxn({
provider,
MulticallContract,
PythERC7412WrapperContract,
priceIds,
}: {
provider: ethers.providers.BaseProvider;
MulticallContract: { address: string; abi: string[] };
PythERC7412WrapperContract: { address: string; abi: string[] };
priceIds: string[];
}) {
console.time('fetchPriceUpdateTxn');
const stalenessTolerance = 1800; // half of 3600 required tolerance

const MulticallInterface = new ethers.utils.Interface(MulticallContract.abi);
const PythERC7412WrapperInterface = new ethers.utils.Interface(PythERC7412WrapperContract.abi);
const txs = priceIds.map((priceId) => ({
target: PythERC7412WrapperContract.address,
callData: PythERC7412WrapperInterface.encodeFunctionData('getLatestPrice', [
priceId,
stalenessTolerance,
]),
value: 0,
requireSuccess: false,
}));

const result = await provider.call({
to: MulticallContract.address,
data: MulticallInterface.encodeFunctionData('aggregate3Value', [txs]),
});
const [latestPrices] = MulticallInterface.decodeFunctionResult('aggregate3Value', result);
const stalePriceIds = priceIds.filter((_priceId, i) => !latestPrices[i].success);
if (stalePriceIds.length < 1) {
return {
target: PythERC7412WrapperContract.address,
callData: ethers.constants.HashZero,
value: 0,
requireSuccess: false,
};
}
console.log({ stalePriceIds });

const priceService = new EvmPriceServiceConnection('https://hermes.pyth.network');
const signedOffchainData = await priceService.getPriceFeedsUpdateData(stalePriceIds);
const updateType = 1;
const data = ethers.utils.defaultAbiCoder.encode(
['uint8', 'uint64', 'bytes32[]', 'bytes[]'],
[updateType, stalenessTolerance, stalePriceIds, signedOffchainData]
);
console.timeEnd('fetchPriceUpdateTxn');
const priceUpdateTxn = {
target: PythERC7412WrapperContract.address,
callData: PythERC7412WrapperInterface.encodeFunctionData('fulfillOracleQuery', [data]),
value: stalePriceIds.length,
requireSuccess: true,
};
console.log({ priceUpdateTxn });
return priceUpdateTxn;
}
1 change: 1 addition & 0 deletions liquidity/lib/usePriceUpdateTxn/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './usePriceUpdateTxn';
15 changes: 15 additions & 0 deletions liquidity/lib/usePriceUpdateTxn/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@snx-v3/usePriceUpdateTxn",
"private": true,
"main": "index.ts",
"version": "0.0.2",
"dependencies": {
"@pythnetwork/pyth-evm-js": "^1.42.0",
"@snx-v3/contracts": "workspace:*",
"@snx-v3/useBlockchain": "workspace:*",
"@snx-v3/useErrorParser": "workspace:*",
"@tanstack/react-query": "^5.8.3",
"ethers": "^5.7.2",
"react": "^18.2.0"
}
}
Loading