Skip to content

Commit

Permalink
Port price updaters from arbitrum-lp app
Browse files Browse the repository at this point in the history
  • Loading branch information
noisekit committed Oct 2, 2024
1 parent a44a298 commit 5d7c619
Show file tree
Hide file tree
Showing 14 changed files with 448 additions and 0 deletions.
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]
);
}
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

0 comments on commit 5d7c619

Please sign in to comment.