From b1ca35757081d0a6cd27d9ddfa9b486a6a35ce05 Mon Sep 17 00:00:00 2001 From: Evgeny Taktarov Date: Tue, 10 Oct 2023 20:00:23 +0700 Subject: [PATCH 01/16] feat: rewards from chain --- .../sdk/src/common/decorators/constants.ts | 1 + packages/sdk/src/common/decorators/types.ts | 3 +- packages/sdk/src/core/types.ts | 7 + packages/sdk/src/rewards/abi/rewardsEvents.ts | 60 +++++ packages/sdk/src/rewards/index.ts | 2 + packages/sdk/src/rewards/rewards.ts | 229 ++++++++++++++++++ packages/sdk/src/rewards/types.ts | 66 +++++ packages/sdk/src/sdk.ts | 6 +- playground/demo/index.tsx | 2 + playground/demo/rewards/index.tsx | 110 +++++++++ playground/hooks/useAddressState.ts | 22 +- 11 files changed, 503 insertions(+), 5 deletions(-) create mode 100644 packages/sdk/src/rewards/abi/rewardsEvents.ts create mode 100644 packages/sdk/src/rewards/index.ts create mode 100644 packages/sdk/src/rewards/rewards.ts create mode 100644 packages/sdk/src/rewards/types.ts create mode 100644 playground/demo/rewards/index.tsx diff --git a/packages/sdk/src/common/decorators/constants.ts b/packages/sdk/src/common/decorators/constants.ts index 1d3504d0..4b5e9e07 100644 --- a/packages/sdk/src/common/decorators/constants.ts +++ b/packages/sdk/src/common/decorators/constants.ts @@ -13,6 +13,7 @@ export const ConsoleCss: Record = { 'Permit:': 'color: lime', 'Events:': 'color: salmon', 'Statistic:': 'color: purple', + 'Rewards:': 'color: greenyellow', 'Init:': 'color: #33F3FF;text-shadow: 0px 0px 0 #899CD5, 1px 1px 0 #8194CD, 2px 2px 0 #788BC4, 3px 3px 0 #6F82BB, 4px 4px 0 #677AB3, 5px 5px 0 #5E71AA, 6px 6px 0 #5568A1, 7px 7px 0 #4C5F98, 8px 8px 0 #445790, 9px 9px 0 #3B4E87, 10px 10px 0 #32457E, 11px 11px 0 #2A3D76, 12px 12px 0 #21346D, 13px 13px 0 #182B64, 14px 14px 0 #0F225B, 15px 15px 0 #071A53, 16px 16px 0 #02114A, 17px 17px 0 #0B0841, 18px 18px 0 #130039, 19px 19px 0 #1C0930, 20px 20px 0 #251227, 21px 21px 20px rgba(0,0,0,1), 21px 21px 1px rgba(0,0,0,0.5), 0px 0px 20px rgba(0,0,0,.2);font-size: 50px;', }; diff --git a/packages/sdk/src/common/decorators/types.ts b/packages/sdk/src/common/decorators/types.ts index 57b24d45..3e7967c8 100644 --- a/packages/sdk/src/common/decorators/types.ts +++ b/packages/sdk/src/common/decorators/types.ts @@ -11,4 +11,5 @@ export type HeadMessage = | 'Init:' | 'Permit:' | 'Events:' - | 'Statistic:'; + | 'Statistic:' + | 'Rewards:'; diff --git a/packages/sdk/src/core/types.ts b/packages/sdk/src/core/types.ts index 4445c9aa..d07c179d 100644 --- a/packages/sdk/src/core/types.ts +++ b/packages/sdk/src/core/types.ts @@ -9,6 +9,7 @@ import { import { LIDO_TOKENS, SUPPORTED_CHAINS } from '../common/constants.js'; import { SDKError } from '../index.js'; +import type LidoSDKCore from './core.js'; export type LOG_MODE = 'info' | 'debug'; @@ -31,6 +32,12 @@ export type LidoSDKCoreProps = | LidoSDKCorePropsRpcUrls | LidoSDKCorePropsRpcProvider; +export type LidoSDKCommonProps = + | { + core: LidoSDKCore; + } + | ({ core: undefined } & LidoSDKCoreProps); + export type EtherValue = string | bigint; export enum TransactionCallbackStage { diff --git a/packages/sdk/src/rewards/abi/rewardsEvents.ts b/packages/sdk/src/rewards/abi/rewardsEvents.ts new file mode 100644 index 00000000..bb19b652 --- /dev/null +++ b/packages/sdk/src/rewards/abi/rewardsEvents.ts @@ -0,0 +1,60 @@ +export const rewardsEventsAbi = [ + { + anonymous: false, + inputs: [ + { indexed: true, name: 'from', type: 'address' }, + { indexed: true, name: 'to', type: 'address' }, + { indexed: false, name: 'sharesValue', type: 'uint256' }, + ], + name: 'TransferShares', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, name: 'reportTimestamp', type: 'uint256' }, + { indexed: false, name: 'timeElapsed', type: 'uint256' }, + { indexed: false, name: 'preTotalShares', type: 'uint256' }, + { indexed: false, name: 'preTotalEther', type: 'uint256' }, + { indexed: false, name: 'postTotalShares', type: 'uint256' }, + { indexed: false, name: 'postTotalEther', type: 'uint256' }, + { indexed: false, name: 'sharesMintedAsFees', type: 'uint256' }, + ], + name: 'TokenRebased', + type: 'event', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'getTotalShares', + outputs: [{ name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'getTotalPooledEther', + outputs: [{ name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [{ name: '_account', type: 'address' }], + name: 'sharesOf', + outputs: [{ name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, +] as const; diff --git a/packages/sdk/src/rewards/index.ts b/packages/sdk/src/rewards/index.ts new file mode 100644 index 00000000..293f26b6 --- /dev/null +++ b/packages/sdk/src/rewards/index.ts @@ -0,0 +1,2 @@ +export { LidoSDKRewards } from './rewards.js'; +export { LidoSDKRewardsProps } from './types.js'; diff --git a/packages/sdk/src/rewards/rewards.ts b/packages/sdk/src/rewards/rewards.ts new file mode 100644 index 00000000..db82cdbf --- /dev/null +++ b/packages/sdk/src/rewards/rewards.ts @@ -0,0 +1,229 @@ +import { LidoSDKCore } from '../core/index.js'; +import { Logger, ErrorHandler, Cache } from '../common/decorators/index.js'; +import { version } from '../version.js'; +import { + GetRewardsOptions, + GetRewardsResult, + LidoSDKRewardsProps, + NonPendingBlockTag, + Reward, + RewardsEvents, +} from './types.js'; +import invariant from 'tiny-invariant'; +import { rewardsEventsAbi } from './abi/rewardsEvents.js'; +import { + Address, + GetContractReturnType, + PublicClient, + getContract, + zeroAddress, +} from 'viem'; +import { LIDO_CONTRACT_NAMES } from '../common/constants.js'; + +export class LidoSDKRewards { + readonly core: LidoSDKCore; + private static readonly PRECISION = 10n ** 27n; + constructor(props: LidoSDKRewardsProps) { + if (props.core) this.core = props.core; + else this.core = new LidoSDKCore(props, version); + } + + // Contracts + + @Logger('Contracts:') + @Cache(30 * 60 * 1000, ['core.chain.id']) + private async contractAddressStETH(): Promise
{ + invariant(this.core.chain, 'Chain is not defined'); + + return await this.core.getContractAddress(LIDO_CONTRACT_NAMES.lido); + } + + @Logger('Contracts:') + @Cache(30 * 60 * 1000, ['core.chain.id']) + private async contractAddressWithdrawalQueue(): Promise
{ + invariant(this.core.chain, 'Chain is not defined'); + return await this.core.getContractAddress( + LIDO_CONTRACT_NAMES.withdrawalQueue, + ); + } + + @Logger('Contracts:') + @Cache(30 * 60 * 1000, ['core.chain.id', 'contractAddressStETH']) + private async getContractStETH(): Promise< + GetContractReturnType + > { + const address = await this.contractAddressStETH(); + + return getContract({ + address, + abi: rewardsEventsAbi, + publicClient: this.core.rpcProvider, + }); + } + + @Logger('Rewards:') + @ErrorHandler('Rewards:') + public async getRewardsFromChain( + props: GetRewardsOptions, + ): Promise { + const [ + { address, fromBlock, toBlock }, + stethContract, + withdrawalQueueAddress, + ] = await Promise.all([ + this.parseProps(props), + this.getContractStETH(), + this.contractAddressWithdrawalQueue(), + ]); + + const preBlock = fromBlock === 0n ? 0n : fromBlock - 1n; + + const [ + baseBalanceShares, + baseTotalEther, + baseTotalShares, + transferOutEvents, + transferInEvents, + rebaseEvents, + ] = await Promise.all([ + stethContract.read.sharesOf([address], { + blockNumber: preBlock, + }), + stethContract.read.getTotalPooledEther({ blockNumber: preBlock }), + stethContract.read.getTotalShares({ blockNumber: preBlock }), + stethContract.getEvents.TransferShares( + { from: address }, + { fromBlock: fromBlock, toBlock: toBlock }, + ), + stethContract.getEvents.TransferShares( + { to: address }, + { fromBlock: fromBlock, toBlock: toBlock }, + ), + stethContract.getEvents.TokenRebased(undefined, { + fromBlock: fromBlock, + toBlock: toBlock, + }), + ]); + + const events = ([] as any[]).concat( + transferInEvents, + transferOutEvents, + rebaseEvents, + ) as RewardsEvents[]; + + // JS sort might not be the most optimal way for merging presorted arrays + events.sort((event1, event2) => { + const block = event1.blockNumber - event2.blockNumber; + if (block === 0n) { + return event1.logIndex - event2.logIndex; + } + return block > 0n ? 1 : -1; + }); + + // Converts to steth based on current share rate + // it's crucial to not cut corners here else computational error will be accumulated + let currentTotalEther = baseTotalEther; + let currentTotalShares = baseTotalShares; + const sharesToSteth = (shares: bigint): bigint => + (shares * currentTotalEther * LidoSDKRewards.PRECISION) / + currentTotalShares / + LidoSDKRewards.PRECISION; + const getShareRate = (): number => + Number( + (currentTotalEther * LidoSDKRewards.PRECISION) / currentTotalShares, + ) / Number(LidoSDKRewards.PRECISION); + + let baseBalance = sharesToSteth(baseBalanceShares); + let baseShareRate = getShareRate(); + + let shareRate = baseShareRate; + let prevSharesBalance = baseBalanceShares; + const rewards: Reward[] = events.map((event) => { + if (event.eventName === 'TransferShares') { + const { from, to, sharesValue } = event.args; + let type: Reward['type'], + changeShares: Reward['changeShares'], + balanceShares: Reward['balanceShares']; + + if (to === address) { + type = from === zeroAddress ? 'submit' : 'transfer_in'; + balanceShares = prevSharesBalance + sharesValue; + changeShares = sharesValue; + } else { + type = to === withdrawalQueueAddress ? 'withdrawal' : 'transfer_out'; + balanceShares = prevSharesBalance - sharesValue; + changeShares = -sharesValue; + } + + return { + type, + balanceShares, + changeShares, + change: sharesToSteth(changeShares), + balance: sharesToSteth(balanceShares), + shareRate, + originalEvent: event, + }; + } + if (event.eventName === 'TokenRebased') { + const { postTotalEther, postTotalShares } = event.args; + const oldBalance = sharesToSteth(prevSharesBalance); + currentTotalEther = postTotalEther; + currentTotalShares = postTotalShares; + const newBalance = sharesToSteth(prevSharesBalance); + shareRate = getShareRate(); + + return { + type: 'rebase', + change: newBalance - oldBalance, + changeShares: 0n, + balance: newBalance, + balanceShares: prevSharesBalance, + shareRate, + originalEvent: event, + }; + } + throw new Error('Impossible event type'); + }); + return { + rewards, + baseBalanceShares, + baseShareRate, + baseBalance, + fromBlock: fromBlock, + toBlock: toBlock, + }; + } + + private async parseProps( + props: TRewardsProps, + ): Promise< + Omit & { + toBlock: bigint; + fromBlock: bigint; + } + > { + const toBlock = await this.toBlockNumber(props.toBlock ?? 'latest'); + if (props.fromBlock == undefined) { + invariant(toBlock - props.blocksBack >= 0n, 'blockBack too far'); + } + const fromBlock = await this.toBlockNumber( + props.fromBlock ?? toBlock - props.blocksBack, + ); + + invariant(toBlock > fromBlock, 'toBlock is higher than fromBlock'); + + return { ...props, fromBlock, toBlock }; + } + + private async toBlockNumber( + block: bigint | NonPendingBlockTag, + ): Promise { + if (typeof block === 'bigint') return block; + const { number } = await this.core.rpcProvider.getBlock({ + blockTag: block, + }); + invariant(number, 'block must not be pending'); + return number; + } +} diff --git a/packages/sdk/src/rewards/types.ts b/packages/sdk/src/rewards/types.ts new file mode 100644 index 00000000..ed8c672f --- /dev/null +++ b/packages/sdk/src/rewards/types.ts @@ -0,0 +1,66 @@ +import { type Address, type BlockTag, type Log } from 'viem'; +import { type rewardsEventsAbi } from './abi/rewardsEvents.js'; +import { type LidoSDKCommonProps } from '../core/types.js'; + +export type NonPendingBlockTag = Exclude; + +export type LidoSDKRewardsProps = LidoSDKCommonProps; + +export type GetRewardsOptions = { + address: Address; + toBlock?: bigint | NonPendingBlockTag; +} & ( + | { + fromBlock: bigint | NonPendingBlockTag; + } + | { + fromBlock?: undefined; + blocksBack: bigint; + } +); + +export type RewardsEvents = + | Log< + bigint, + number, + false, + undefined, + true, + typeof rewardsEventsAbi, + 'TransferShares' + > + | Log< + bigint, + number, + false, + undefined, + true, + typeof rewardsEventsAbi, + 'TokenRebased' + >; + +export type RewardType = + | 'submit' + | 'withdrawal' + | 'rebase' + | 'transfer_in' + | 'transfer_out'; + +export type Reward = { + type: RewardType; + change: bigint; + changeShares: bigint; + balance: bigint; + balanceShares: bigint; + shareRate: number; + originalEvent: RewardsEvents; +}; + +export type GetRewardsResult = { + rewards: Reward[]; + baseBalance: bigint; + baseBalanceShares: bigint; + baseShareRate: number; + fromBlock: bigint; + toBlock: bigint; +}; diff --git a/packages/sdk/src/sdk.ts b/packages/sdk/src/sdk.ts index 0286e304..a182a75d 100644 --- a/packages/sdk/src/sdk.ts +++ b/packages/sdk/src/sdk.ts @@ -4,9 +4,10 @@ import { LidoSDKStake } from './stake/index.js'; import { LidoSDKWrap } from './wrap/index.js'; import { LidoSDKWithdraw } from './withdraw/index.js'; import { LidoSDKstETH, LidoSDKwstETH } from './erc20/index.js'; -import { LidoSDKUnstETH } from './unsteth/unsteth.js'; +import { LidoSDKUnstETH } from './unsteth/index.js'; import { LidoSDKEvents } from './events/index.js'; import { LidoSDKStatistics } from './statistics/index.js'; +import { LidoSDKRewards } from './rewards/index.js'; import { version } from './version.js'; @@ -20,6 +21,7 @@ export class LidoSDK { readonly unsteth: LidoSDKUnstETH; readonly events: LidoSDKEvents; readonly statistics: LidoSDKStatistics; + readonly rewards: LidoSDKRewards; constructor(props: LidoSDKCoreProps) { // Core functionality @@ -39,5 +41,7 @@ export class LidoSDK { this.events = new LidoSDKEvents({ ...props, core }); // Statistic functionality this.statistics = new LidoSDKStatistics({ ...props, core }); + // Rewards functionality + this.rewards = new LidoSDKRewards({ ...props, core }); } } diff --git a/playground/demo/index.tsx b/playground/demo/index.tsx index 3cb49186..f7041f6d 100644 --- a/playground/demo/index.tsx +++ b/playground/demo/index.tsx @@ -11,6 +11,7 @@ import { StethDemo, WstethDemo } from './tokens'; import { UnstethDemo } from './unsteth'; import { EventsDemo } from './events'; import { StatisticsDemo } from './statistics'; +import { RewardsDemo } from './rewards'; export const Demo = () => { return ( @@ -27,6 +28,7 @@ export const Demo = () => { + ); }; diff --git a/playground/demo/rewards/index.tsx b/playground/demo/rewards/index.tsx new file mode 100644 index 00000000..79c72537 --- /dev/null +++ b/playground/demo/rewards/index.tsx @@ -0,0 +1,110 @@ +import { GetRewardsResult } from '@lidofinance/lido-ethereum-sdk/dist/types/rewards/types'; +import { + Input, + Accordion, + Table, + Thead, + Tbody, + Tr, + Td, + Th, + Container, + DataTableRow, +} from '@lidofinance/lido-ui'; +import { useWeb3 } from '@reef-knot/web3-react'; +import { Action, renderTokenResult } from 'components/action'; +import { DEFAULT_VALUE, ValueType } from 'components/tokenInput'; +import TokenInput from 'components/tokenInput/tokenInput'; +import { useAddressState } from 'hooks/useAddressState'; +import { useLidoSDK } from 'providers/sdk'; +import { useState } from 'react'; +import { transactionToast } from 'utils/transaction-toast'; +import { Address } from 'viem'; + +const renderRewards = (result: GetRewardsResult) => { + const steth = renderTokenResult('stETH'); + const shares = renderTokenResult('shares'); + return ( + + + {result.fromBlock.toString()} + + + {result.toBlock.toString()} + + + {steth(result.baseBalance)} + + + {shares(result.baseBalanceShares)} + + + {result.baseShareRate} + + + + + + + + + + + + {result.rewards.map((r, index) => ( + + + + + + + ))} + +
BlockTypeBalanceReward
{r.originalEvent.blockNumber.toString()}{r.type} + {steth(r.balance)} +
({shares(r.balanceShares)}) +
+ {steth(r.change)} +
({shares(r.changeShares)}) +
+
+ ); +}; + +export const RewardsDemo = () => { + const [rewardsAddress, setRewardsAddress] = useAddressState(undefined, { + useAccount: true, + }); + const [blocksBack, setBlocksBack] = useState(10000); + const { rewards } = useLidoSDK(); + + return ( + + { + return rewards.getRewardsFromChain({ + address: rewardsAddress, + blocksBack: BigInt(blocksBack), + }); + }} + > + setRewardsAddress(e.currentTarget.value as Address)} + /> + setBlocksBack(e.currentTarget.valueAsNumber)} + /> + + + ); +}; diff --git a/playground/hooks/useAddressState.ts b/playground/hooks/useAddressState.ts index 21f13955..41d6f244 100644 --- a/playground/hooks/useAddressState.ts +++ b/playground/hooks/useAddressState.ts @@ -1,5 +1,21 @@ -import { useState } from 'react'; +import { useWeb3 } from '@reef-knot/web3-react'; +import { useEffect, useState } from 'react'; import { Address } from 'viem'; -export const useAddressState = (defaultValue: Address = '0x0') => - useState
(defaultValue); +type UseAddressStateOptions = { + useAccount?: boolean; +}; + +export const useAddressState = ( + defaultValue: Address = '0x0', + { useAccount }: UseAddressStateOptions = { useAccount: false }, +) => { + const { account: web3account = '0x0' } = useWeb3(); + const state = useState
(defaultValue); + useEffect(() => { + if (useAccount && web3account && (!state[0] || state[0] === defaultValue)) { + state[1](web3account as Address); + } + }, [useAccount, web3account]); + return state; +}; From 4a540c1627ca95c80280acc015829ffd6d6c5b7b Mon Sep 17 00:00:00 2001 From: Eugene Mamin Date: Fri, 13 Oct 2023 01:08:26 +0300 Subject: [PATCH 02/16] feat: rough example of APR calculation WIP --- packages/sdk/src/events/stethEvents.ts | 36 +++++++++++++++++++++++--- packages/sdk/src/statistics/apr.ts | 36 +++++++++++++++++++------- playground/demo/statistics/index.tsx | 2 +- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/packages/sdk/src/events/stethEvents.ts b/packages/sdk/src/events/stethEvents.ts index 43c197b1..db3c1aaa 100644 --- a/packages/sdk/src/events/stethEvents.ts +++ b/packages/sdk/src/events/stethEvents.ts @@ -17,6 +17,7 @@ import { type LidoSDKEventsProps, RebaseEvent } from './types.js'; const BLOCKS_BY_DAY = 7600n; const REBASE_EVENT_ABI_INDEX = 8; +const DAYS_LIMIT = 7; export class LidoSDKStethEvents { readonly core: LidoSDKCore; @@ -58,15 +59,15 @@ export class LidoSDKStethEvents { @ErrorHandler() public async getLastRebaseEvent(): Promise { const contract = await this.getContractStETH(); - const DAYS_LIMIT = 3; + const lastBlock = await this.getLastBlock() for (let days = 1; days <= DAYS_LIMIT; days++) { - const dayAgoBlock = await this.getBlockByDays({ days }); + const fromBlock = lastBlock.number - BLOCKS_BY_DAY * BigInt(days) const logs = await this.core.rpcProvider.getLogs({ address: contract.address, event: StethEventsAbi[REBASE_EVENT_ABI_INDEX], - fromBlock: dayAgoBlock.number, - toBlock: 'latest', + fromBlock: fromBlock, + toBlock: fromBlock + BLOCKS_BY_DAY, }); if (logs.length > 0) return logs[logs.length - 1]; @@ -75,6 +76,33 @@ export class LidoSDKStethEvents { return undefined; } + @Logger('Events:') + @ErrorHandler() + public async getFirstRebaseEvent(props: { + fromBlock: bigint; + daysAgo: number + }): Promise { + const { fromBlock, daysAgo } = props; + + const contract = await this.getContractStETH(); + + for (let days = 1; days <= DAYS_LIMIT; days++) { + const from = fromBlock + BigInt(days - 1 - daysAgo) * BLOCKS_BY_DAY + const to = from + BLOCKS_BY_DAY; + + const logs = await this.core.rpcProvider.getLogs({ + address: contract.address, + event: StethEventsAbi[REBASE_EVENT_ABI_INDEX], + fromBlock: from, + toBlock: to, + }); + + if (logs.length > 0) return logs[0]; + } + + return undefined; + } + @Logger('Events:') @ErrorHandler() public async getRebaseEventsByDays(props: { diff --git a/packages/sdk/src/statistics/apr.ts b/packages/sdk/src/statistics/apr.ts index 606215a2..9c47cb1f 100644 --- a/packages/sdk/src/statistics/apr.ts +++ b/packages/sdk/src/statistics/apr.ts @@ -33,16 +33,32 @@ export class LidoSDKApr { @Logger('Statistic:') @ErrorHandler() - public async getSmaApr(): Promise<{ aprs: number[]; smaApr: number }> { - const events = await this.events.stethEvents.getRebaseEventsByDays({ - days: 7, - }); - invariant(events.length, 'Events is not defined'); - - const aprs = events.map((event) => this.calculateApr(event.args)); - const sum = aprs.reduce((acc, apr) => apr + acc, 0); - - return { aprs, smaApr: Number((sum / aprs.length).toFixed(1)) }; + public async getSmaApr(props: { + days: number; + }): Promise { + const { days } = props; + + const lastEvent = await this.events.stethEvents.getLastRebaseEvent() + invariant(lastEvent, 'Last event is not defined') + + const firstEvent = await this.events.stethEvents.getFirstRebaseEvent({ + fromBlock: lastEvent.blockNumber, daysAgo: days + }) + invariant(firstEvent, 'First event is not defined') + + invariant(firstEvent.args.timeElapsed, 'time elapsed is not defined') + invariant(lastEvent.args.reportTimestamp, 'Last event reportTimestamp is not defined') + invariant(firstEvent.args.reportTimestamp, 'First event reportTimestamp is not defined') + + const smaApr = this.calculateApr({ + preTotalEther: firstEvent.args.preTotalEther, + preTotalShares: firstEvent.args.preTotalShares, + postTotalEther: lastEvent.args.postTotalEther, + postTotalShares: lastEvent.args.postTotalShares, + timeElapsed: firstEvent.args.timeElapsed + (lastEvent.args.reportTimestamp - firstEvent.args.reportTimestamp), + }) + + return smaApr; } private calculateApr(props: { diff --git a/playground/demo/statistics/index.tsx b/playground/demo/statistics/index.tsx index dbc5e0ba..ecbd881a 100644 --- a/playground/demo/statistics/index.tsx +++ b/playground/demo/statistics/index.tsx @@ -9,7 +9,7 @@ export const StatisticsDemo = () => { return ( statistics.apr.getLastApr()} /> - statistics.apr.getSmaApr()} /> + statistics.apr.getSmaApr({ days: 7 })} /> ); }; From e88d17b7f8af5b844239b7913daf5926cfb775d0 Mon Sep 17 00:00:00 2001 From: Eugene M Date: Fri, 13 Oct 2023 11:33:53 +0300 Subject: [PATCH 03/16] fix: interface refinements --- packages/sdk/src/events/stethEvents.ts | 12 ++++++++---- packages/sdk/src/statistics/apr.ts | 8 ++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/sdk/src/events/stethEvents.ts b/packages/sdk/src/events/stethEvents.ts index db3c1aaa..00dad881 100644 --- a/packages/sdk/src/events/stethEvents.ts +++ b/packages/sdk/src/events/stethEvents.ts @@ -79,15 +79,19 @@ export class LidoSDKStethEvents { @Logger('Events:') @ErrorHandler() public async getFirstRebaseEvent(props: { - fromBlock: bigint; - daysAgo: number + daysAgo: number; + goBackFromBlock?: bigint; }): Promise { - const { fromBlock, daysAgo } = props; + const { daysAgo } = props; + + invariant(daysAgo > 0, 'Days ago must be positive') + + const goBackFromBlock = props.goBackFromBlock ?? (await this.getLastBlock()).number const contract = await this.getContractStETH(); for (let days = 1; days <= DAYS_LIMIT; days++) { - const from = fromBlock + BigInt(days - 1 - daysAgo) * BLOCKS_BY_DAY + const from = goBackFromBlock - BigInt(daysAgo + 1 - days) * BLOCKS_BY_DAY const to = from + BLOCKS_BY_DAY; const logs = await this.core.rpcProvider.getLogs({ diff --git a/packages/sdk/src/statistics/apr.ts b/packages/sdk/src/statistics/apr.ts index 9c47cb1f..d9f2cb2a 100644 --- a/packages/sdk/src/statistics/apr.ts +++ b/packages/sdk/src/statistics/apr.ts @@ -42,7 +42,7 @@ export class LidoSDKApr { invariant(lastEvent, 'Last event is not defined') const firstEvent = await this.events.stethEvents.getFirstRebaseEvent({ - fromBlock: lastEvent.blockNumber, daysAgo: days + daysAgo: days, goBackFromBlock: lastEvent.blockNumber, }) invariant(firstEvent, 'First event is not defined') @@ -50,12 +50,16 @@ export class LidoSDKApr { invariant(lastEvent.args.reportTimestamp, 'Last event reportTimestamp is not defined') invariant(firstEvent.args.reportTimestamp, 'First event reportTimestamp is not defined') + const timeElapsed = firstEvent.args.timeElapsed + ( + lastEvent.args.reportTimestamp - firstEvent.args.reportTimestamp + ); + const smaApr = this.calculateApr({ preTotalEther: firstEvent.args.preTotalEther, preTotalShares: firstEvent.args.preTotalShares, postTotalEther: lastEvent.args.postTotalEther, postTotalShares: lastEvent.args.postTotalShares, - timeElapsed: firstEvent.args.timeElapsed + (lastEvent.args.reportTimestamp - firstEvent.args.reportTimestamp), + timeElapsed, }) return smaApr; From 7bab111c414e4bba5ba8b0deb603983778087892 Mon Sep 17 00:00:00 2001 From: Eugene M Date: Fri, 13 Oct 2023 11:34:31 +0300 Subject: [PATCH 04/16] demo: APR and arbitrary back event --- playground/demo/events/index.tsx | 21 ++++++++++++++++++++- playground/demo/statistics/index.tsx | 22 ++++++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/playground/demo/events/index.tsx b/playground/demo/events/index.tsx index bd892ba2..4ce6b74c 100644 --- a/playground/demo/events/index.tsx +++ b/playground/demo/events/index.tsx @@ -1,9 +1,11 @@ -import { Accordion } from '@lidofinance/lido-ui'; +import { Accordion, Input } from '@lidofinance/lido-ui'; import { useWeb3 } from '@reef-knot/web3-react'; import { Action } from 'components/action'; import { useLidoSDK } from 'providers/sdk'; +import { useState } from 'react'; export const EventsDemo = () => { + const [daysAgoValue, setDaysAgoValue] = useState(0); const { events } = useLidoSDK(); return ( @@ -12,6 +14,23 @@ export const EventsDemo = () => { title="Last Rebase event" action={() => events.stethEvents.getLastRebaseEvent()} /> + + events.stethEvents.getFirstRebaseEvent({ + daysAgo: daysAgoValue + }) + } + > + setDaysAgoValue(e.target.valueAsNumber)} + /> + events.stethEvents.getRebaseEvents({ count: 10 })} diff --git a/playground/demo/statistics/index.tsx b/playground/demo/statistics/index.tsx index ecbd881a..c5c290b5 100644 --- a/playground/demo/statistics/index.tsx +++ b/playground/demo/statistics/index.tsx @@ -1,15 +1,33 @@ -import { Accordion } from '@lidofinance/lido-ui'; +import { Input, Accordion } from '@lidofinance/lido-ui'; import { useWeb3 } from '@reef-knot/web3-react'; import { Action } from 'components/action'; import { useLidoSDK } from 'providers/sdk'; +import { useState } from 'react'; export const StatisticsDemo = () => { + const [daysValue, setDaysValue] = useState(0); const { statistics } = useLidoSDK(); return ( statistics.apr.getLastApr()} /> - statistics.apr.getSmaApr({ days: 7 })} /> + + statistics.apr.getSmaApr({ + days: daysValue + }) + } + > + setDaysValue(e.target.valueAsNumber)} + /> + ); }; From d50f81c3b1cf6aea0add2cc93f305cb33acf3796 Mon Sep 17 00:00:00 2001 From: Eugene M Date: Fri, 13 Oct 2023 11:43:45 +0300 Subject: [PATCH 05/16] chore: fix boundaries --- packages/sdk/src/events/stethEvents.ts | 3 --- playground/demo/events/index.tsx | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/events/stethEvents.ts b/packages/sdk/src/events/stethEvents.ts index 00dad881..0fec5ff4 100644 --- a/packages/sdk/src/events/stethEvents.ts +++ b/packages/sdk/src/events/stethEvents.ts @@ -83,9 +83,6 @@ export class LidoSDKStethEvents { goBackFromBlock?: bigint; }): Promise { const { daysAgo } = props; - - invariant(daysAgo > 0, 'Days ago must be positive') - const goBackFromBlock = props.goBackFromBlock ?? (await this.getLastBlock()).number const contract = await this.getContractStETH(); diff --git a/playground/demo/events/index.tsx b/playground/demo/events/index.tsx index 4ce6b74c..05000f30 100644 --- a/playground/demo/events/index.tsx +++ b/playground/demo/events/index.tsx @@ -5,7 +5,7 @@ import { useLidoSDK } from 'providers/sdk'; import { useState } from 'react'; export const EventsDemo = () => { - const [daysAgoValue, setDaysAgoValue] = useState(0); + const [daysAgoValue, setDaysAgoValue] = useState(1); const { events } = useLidoSDK(); return ( @@ -26,7 +26,7 @@ export const EventsDemo = () => { label="Days ago" placeholder='7' type="number" - min={0} + min={1} value={daysAgoValue} onChange={(e) => setDaysAgoValue(e.target.valueAsNumber)} /> From 4e5af31ecc527ab612f3c42084bbdc48e2701dfe Mon Sep 17 00:00:00 2001 From: Evgeny Taktarov Date: Fri, 13 Oct 2023 16:00:09 +0700 Subject: [PATCH 06/16] feat: subgraph fetch --- packages/sdk/package.json | 2 + packages/sdk/src/common/constants.ts | 8 + .../sdk/src/common/utils/address-equal.ts | 4 + packages/sdk/src/common/utils/index.ts | 1 + packages/sdk/src/core/core.ts | 9 + packages/sdk/src/rewards/index.ts | 12 +- packages/sdk/src/rewards/rewards.ts | 286 ++++++++++++++++-- packages/sdk/src/rewards/subgraph/index.ts | 15 + packages/sdk/src/rewards/subgraph/queries.ts | 138 +++++++++ packages/sdk/src/rewards/subgraph/subrgaph.ts | 131 ++++++++ packages/sdk/src/rewards/subgraph/types.ts | 112 +++++++ packages/sdk/src/rewards/types.ts | 28 +- playground/demo/rewards/index.tsx | 48 ++- yarn.lock | 30 ++ 14 files changed, 779 insertions(+), 45 deletions(-) create mode 100644 packages/sdk/src/common/utils/address-equal.ts create mode 100644 packages/sdk/src/rewards/subgraph/index.ts create mode 100644 packages/sdk/src/rewards/subgraph/queries.ts create mode 100644 packages/sdk/src/rewards/subgraph/subrgaph.ts create mode 100644 packages/sdk/src/rewards/subgraph/types.ts diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 6642ac05..fa1063e4 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -111,6 +111,8 @@ }, "dependencies": { "@ethersproject/bytes": "^5.7.0", + "graphql": "^16.8.1", + "graphql-request": "^6.1.0", "tiny-invariant": "^1.3.1", "viem": "^1.15.3" }, diff --git a/packages/sdk/src/common/constants.ts b/packages/sdk/src/common/constants.ts index 573d919b..923d4301 100644 --- a/packages/sdk/src/common/constants.ts +++ b/packages/sdk/src/common/constants.ts @@ -24,6 +24,14 @@ export const LIDO_LOCATOR_BY_CHAIN: { [CHAINS.Holesky]: '0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8', }; +export const SUBRGRAPH_ID_BY_CHAIN: { + [key in CHAINS]: string; +} = { + [CHAINS.Mainnet]: 'HXfMc1jPHfFQoccWd7VMv66km75FoxVHDMvsJj5vG5vf', + [CHAINS.Goerli]: 'QmeDfGTuNbSoZ71zi3Ch4WNRbzALfiFPnJMYUFPinLiFNa', + [CHAINS.Holesky]: '', +}; + export const LIDO_TOKENS = { steth: 'stETH', wsteth: 'wstETH', diff --git a/packages/sdk/src/common/utils/address-equal.ts b/packages/sdk/src/common/utils/address-equal.ts new file mode 100644 index 00000000..7e5caa0c --- /dev/null +++ b/packages/sdk/src/common/utils/address-equal.ts @@ -0,0 +1,4 @@ +import { Address } from 'viem'; + +export const addressEqual = (a: Address | string, b: Address | string) => + a.toLowerCase() === b.toLowerCase(); diff --git a/packages/sdk/src/common/utils/index.ts b/packages/sdk/src/common/utils/index.ts index 952d8e2a..535b6aa4 100644 --- a/packages/sdk/src/common/utils/index.ts +++ b/packages/sdk/src/common/utils/index.ts @@ -3,3 +3,4 @@ export { getFeeData, type FeeData } from './getFeeData.js'; export { SDKError, type SDKErrorProps } from './SDKError.js'; export { getErrorMessage, type ErrorMessage } from './getErrorMessage.js'; export { isBigint } from './isBigint.js'; +export { addressEqual } from './address-equal.js'; diff --git a/packages/sdk/src/core/core.ts b/packages/sdk/src/core/core.ts index a46b8214..e76d1c3d 100644 --- a/packages/sdk/src/core/core.ts +++ b/packages/sdk/src/core/core.ts @@ -38,6 +38,7 @@ import { LIDO_TOKENS, PERMIT_MESSAGE_TYPES, VIEM_CHAINS, + SUBRGRAPH_ID_BY_CHAIN, } from '../common/constants.js'; import { LidoLocatorAbi } from './abi/lidoLocator.js'; @@ -351,6 +352,14 @@ export default class LidoSDKCore { } } + @Logger('Utils:') + @Cache(30 * 60 * 1000, ['chain.id']) + public getSubgraphId(): string { + const id = SUBRGRAPH_ID_BY_CHAIN[this.chainId]; + invariant(id, `Subgraph is not supported for chain ${this.chainId}`); + return id; + } + public async performTransaction( props: PerformTransactionProps, getGasLimit: (overrides: TransactionOptions) => Promise, diff --git a/packages/sdk/src/rewards/index.ts b/packages/sdk/src/rewards/index.ts index 293f26b6..66101acc 100644 --- a/packages/sdk/src/rewards/index.ts +++ b/packages/sdk/src/rewards/index.ts @@ -1,2 +1,12 @@ export { LidoSDKRewards } from './rewards.js'; -export { LidoSDKRewardsProps } from './types.js'; +export { + type LidoSDKRewardsProps, + type GetRewardsFromChainResults, + type GetRewardsFromChainOptions, + type GetRewardsFromSubgraphOptions, + type GetRewardsFromSubgraphResults, + type RewardsSubgraphEvents, + type RewardsChainEvents, + type RewardType, + type Reward, +} from './types.js'; diff --git a/packages/sdk/src/rewards/rewards.ts b/packages/sdk/src/rewards/rewards.ts index db82cdbf..e8302cc6 100644 --- a/packages/sdk/src/rewards/rewards.ts +++ b/packages/sdk/src/rewards/rewards.ts @@ -1,28 +1,58 @@ +import invariant from 'tiny-invariant'; +import { + Address, + GetContractReturnType, + PublicClient, + getContract, + zeroAddress, +} from 'viem'; import { LidoSDKCore } from '../core/index.js'; import { Logger, ErrorHandler, Cache } from '../common/decorators/index.js'; import { version } from '../version.js'; +import { rewardsEventsAbi } from './abi/rewardsEvents.js'; import { + GetRewardsFromChainResults as GetRewardsFromChainResult, + GetRewardsFromSubgraphOptions, + GetRewardsFromSubgraphResults as GetRewardsFromSubgraphResult, GetRewardsOptions, - GetRewardsResult, LidoSDKRewardsProps, NonPendingBlockTag, Reward, - RewardsEvents, + RewardsChainEvents, + RewardsSubgraphEvents, } from './types.js'; -import invariant from 'tiny-invariant'; -import { rewardsEventsAbi } from './abi/rewardsEvents.js'; -import { - Address, - GetContractReturnType, - PublicClient, - getContract, - zeroAddress, -} from 'viem'; + import { LIDO_CONTRACT_NAMES } from '../common/constants.js'; +import { + TotalRewardEntity, + TransferEventEntity, + getLastIndexedBlock, + getTotalRewards, + getTransfers, +} from './subgraph/index.js'; +import { addressEqual } from '../common/utils/address-equal.js'; +import { getInitialData } from './subgraph/subrgaph.js'; export class LidoSDKRewards { readonly core: LidoSDKCore; private static readonly PRECISION = 10n ** 27n; + + private static calcShareRate = ( + totalEther: bigint, + totalShares: bigint, + ): number => + Number((totalEther * LidoSDKRewards.PRECISION) / totalShares) / + Number(LidoSDKRewards.PRECISION); + + private static sharesToSteth = ( + shares: bigint, + totalEther: bigint, + totalShares: bigint, + ): bigint => + (shares * totalEther * LidoSDKRewards.PRECISION) / + totalShares / + LidoSDKRewards.PRECISION; + constructor(props: LidoSDKRewardsProps) { if (props.core) this.core = props.core; else this.core = new LidoSDKCore(props, version); @@ -65,7 +95,7 @@ export class LidoSDKRewards { @ErrorHandler('Rewards:') public async getRewardsFromChain( props: GetRewardsOptions, - ): Promise { + ): Promise { const [ { address, fromBlock, toBlock }, stethContract, @@ -93,23 +123,26 @@ export class LidoSDKRewards { stethContract.read.getTotalShares({ blockNumber: preBlock }), stethContract.getEvents.TransferShares( { from: address }, - { fromBlock: fromBlock, toBlock: toBlock }, + { fromBlock, toBlock }, ), stethContract.getEvents.TransferShares( { to: address }, - { fromBlock: fromBlock, toBlock: toBlock }, + { fromBlock, toBlock }, + ), + stethContract.getEvents.TokenRebased( + {}, + { + fromBlock, + toBlock, + }, ), - stethContract.getEvents.TokenRebased(undefined, { - fromBlock: fromBlock, - toBlock: toBlock, - }), ]); - const events = ([] as any[]).concat( + const events = ([] as any).concat( transferInEvents, transferOutEvents, rebaseEvents, - ) as RewardsEvents[]; + ) as RewardsChainEvents[]; // JS sort might not be the most optimal way for merging presorted arrays events.sort((event1, event2) => { @@ -125,25 +158,26 @@ export class LidoSDKRewards { let currentTotalEther = baseTotalEther; let currentTotalShares = baseTotalShares; const sharesToSteth = (shares: bigint): bigint => - (shares * currentTotalEther * LidoSDKRewards.PRECISION) / - currentTotalShares / - LidoSDKRewards.PRECISION; - const getShareRate = (): number => - Number( - (currentTotalEther * LidoSDKRewards.PRECISION) / currentTotalShares, - ) / Number(LidoSDKRewards.PRECISION); + LidoSDKRewards.sharesToSteth( + shares, + currentTotalEther, + currentTotalShares, + ); + const getShareRate = () => + LidoSDKRewards.calcShareRate(currentTotalEther, currentTotalShares); let baseBalance = sharesToSteth(baseBalanceShares); let baseShareRate = getShareRate(); let shareRate = baseShareRate; let prevSharesBalance = baseBalanceShares; - const rewards: Reward[] = events.map((event) => { + type RewardEntry = Reward; + const rewards: Reward[] = events.map((event) => { if (event.eventName === 'TransferShares') { const { from, to, sharesValue } = event.args; - let type: Reward['type'], - changeShares: Reward['changeShares'], - balanceShares: Reward['balanceShares']; + let type: RewardEntry['type'], + changeShares: RewardEntry['changeShares'], + balanceShares: RewardEntry['balanceShares']; if (to === address) { type = from === zeroAddress ? 'submit' : 'transfer_in'; @@ -195,6 +229,195 @@ export class LidoSDKRewards { }; } + @Logger('Rewards:') + @ErrorHandler('Rewards:') + public async getRewardsFromSubgraph( + props: GetRewardsFromSubgraphOptions, + ): Promise { + const [ + { getSubgraphUrl, address, fromBlock, toBlock, step = 1000 }, + withdrawalQueueAddress, + ] = await Promise.all([ + this.parseProps(props), + this.contractAddressWithdrawalQueue(), + ]); + const url = getSubgraphUrl(this.core.getSubgraphId(), this.core.chainId); + + // Cap toBlock to last indexed + const lastIndexedBlock = BigInt( + (await getLastIndexedBlock({ url })).number, + ); + const cappedToBlock = + lastIndexedBlock < toBlock ? lastIndexedBlock : toBlock; + const preBlock = fromBlock === 0n ? 0n : fromBlock - 1n; + + // fetch data from subgraph + const [ + transfers, + totalRewards, + { transfer: initialTransfer, rebase: initialRebase }, + ] = await Promise.all([ + getTransfers({ + url, + address, + fromBlock, + toBlock: cappedToBlock, + step, + }), + getTotalRewards({ url, fromBlock, toBlock: cappedToBlock, step }), + getInitialData({ url, address, block: preBlock }), + ]); + + const events = ([] as (TransferEventEntity | TotalRewardEntity)[]).concat( + totalRewards, + transfers, + ); + + events.sort((event1, event2) => { + const block = BigInt(event1.block) - BigInt(event2.block); + if (block === 0n) { + return Number(event1.logIndex) - Number(event2.logIndex); + } + return block > 0n ? 1 : -1; + }); + + /// these allow us to count changes in rebase events + // even if no transfers were detected in our range + let prevBalanceShares = 0n; + let prevBalance = 0n; + let baseShareRate = 0; + + // last transfer before main query + if (initialTransfer) { + const { + to, + from, + balanceAfterDecrease, + balanceAfterIncrease, + sharesAfterDecrease, + sharesAfterIncrease, + } = initialTransfer; + if (addressEqual(to, address)) { + prevBalanceShares = BigInt(sharesAfterIncrease); + prevBalance = BigInt(balanceAfterIncrease); + } else if (addressEqual(from, address)) { + prevBalanceShares = BigInt(sharesAfterDecrease); + prevBalance = BigInt(balanceAfterDecrease); + } + } + + // last rebase before main query + if (initialRebase) { + const { totalPooledEtherAfter, totalSharesAfter } = initialRebase; + const totalEther = BigInt(totalPooledEtherAfter); + const totalShares = BigInt(totalSharesAfter); + baseShareRate = LidoSDKRewards.calcShareRate(totalEther, totalShares); + // we recount initial balance in case this rebase was after transfer + // in opposite case recount will be the same value anyway + prevBalance = LidoSDKRewards.sharesToSteth( + prevBalanceShares, + totalEther, + totalShares, + ); + } + + // fix values for return meta + const baseBalance = prevBalance; + const baseBalanceShares = prevBalanceShares; + + type RewardEntry = Reward; + + const rewards: Reward[] = events.map((event) => { + // it's a transfer + if ('value' in event) { + const { + from, + to, + shares, + sharesAfterIncrease, + value, + balanceAfterDecrease, + balanceAfterIncrease, + sharesAfterDecrease, + totalPooledEther, + totalShares, + } = event; + let type: RewardEntry['type'], + changeShares: RewardEntry['changeShares'], + balanceShares: RewardEntry['balanceShares'], + change: RewardEntry['change'], + balance: RewardEntry['balance']; + + if (addressEqual(to, address)) { + type = from === zeroAddress ? 'submit' : 'transfer_in'; + changeShares = BigInt(shares); + balanceShares = BigInt(sharesAfterIncrease); + change = BigInt(value); + balance = BigInt(balanceAfterIncrease); + } else { + type = addressEqual(to, withdrawalQueueAddress) + ? 'withdrawal' + : 'transfer_out'; + balance = BigInt(balanceAfterDecrease); + change = -BigInt(value); + changeShares = -BigInt(shares); + balanceShares = BigInt(sharesAfterDecrease); + } + + const shareRate = LidoSDKRewards.calcShareRate( + BigInt(totalPooledEther), + BigInt(totalShares), + ); + prevBalance = balance; + prevBalanceShares = balanceShares; + + return { + type, + balanceShares, + changeShares, + change, + balance, + shareRate, + originalEvent: event, + }; + } + // it's a rebase + if ('apr' in event) { + const { totalPooledEtherAfter, totalSharesAfter } = event; + + const totalEther = BigInt(totalPooledEtherAfter); + const totalShares = BigInt(totalSharesAfter); + const newBalance = LidoSDKRewards.sharesToSteth( + prevBalanceShares, + totalEther, + totalShares, + ); + const change = newBalance - prevBalance; + prevBalance = newBalance; + return { + type: 'rebase', + change, + changeShares: 0n, + balance: newBalance, + balanceShares: prevBalanceShares, + shareRate: LidoSDKRewards.calcShareRate(totalEther, totalShares), + originalEvent: event, + }; + } + throw new Error('Impossible event'); + }); + + return { + rewards, + baseBalance, + lastIndexedBlock, + baseBalanceShares, + baseShareRate, + fromBlock, + toBlock: cappedToBlock, + }; + } + private async parseProps( props: TRewardsProps, ): Promise< @@ -216,6 +439,7 @@ export class LidoSDKRewards { return { ...props, fromBlock, toBlock }; } + @Logger('Utils:') private async toBlockNumber( block: bigint | NonPendingBlockTag, ): Promise { diff --git a/packages/sdk/src/rewards/subgraph/index.ts b/packages/sdk/src/rewards/subgraph/index.ts new file mode 100644 index 00000000..06054122 --- /dev/null +++ b/packages/sdk/src/rewards/subgraph/index.ts @@ -0,0 +1,15 @@ +export { + getLastIndexedBlock, + getTotalRewards, + getTransfers, +} from './subrgaph.js'; +export { + type GetLastIndexedBlockOptions, + type GetLastIndexedBlockResult, + type GetTotalRewardsOptions, + type GetTotalRewardsResult, + type GetTransfersOptions, + type GetTransfersResult, + type TotalRewardEntity, + type TransferEventEntity, +} from './types.js'; diff --git a/packages/sdk/src/rewards/subgraph/queries.ts b/packages/sdk/src/rewards/subgraph/queries.ts new file mode 100644 index 00000000..d8d6ac77 --- /dev/null +++ b/packages/sdk/src/rewards/subgraph/queries.ts @@ -0,0 +1,138 @@ +import { gql } from 'graphql-request'; + +export const LidoTransfersQuery = gql` + query LidoTransfers( + $skip: Int + $first: Int! + $fromBlock: Int! + $toBlock: Int! + $address: Bytes! + ) { + lidoTransfers( + first: $first + skip: $skip + where: { + and: [ + { or: [{ to: $address }, { from: $address }] } + { _change_block: { number_gte: $fromBlock } } + ] + } + block: { number: $toBlock } + ) { + from + to + value + + shares + sharesBeforeDecrease + sharesAfterDecrease + sharesBeforeIncrease + sharesAfterIncrease + + totalPooledEther + totalShares + + balanceAfterDecrease + balanceAfterIncrease + + block + blockTime + transactionHash + transactionIndex + logIndex + } + } +`; + +export const TotalRewardsQuery = gql` + query TotalRewards( + $skip: Int + $first: Int! + $fromBlock: Int! + $toBlock: Int! + ) { + totalRewards( + first: $first + skip: $skip + where: { _change_block: { number_gte: $fromBlock } } + block: { number: $toBlock } + ) { + id + + totalPooledEtherBefore + totalPooledEtherAfter + totalSharesBefore + totalSharesAfter + + apr + + block + blockTime + logIndex + } + } +`; + +export const StatusQuery = gql` + query { + _meta { + block { + number + hash + } + } + } +`; + +export const InitialStateQuery = gql` + query BalanceBefore($address: Bytes!, $block: Int!) { + lidoTransfers( + first: 1 + skip: 0 + orderBy: block + orderDirection: desc + where: { or: [{ to: $address }, { from: $address }] } + block: { number: $block } + ) { + from + to + value + + shares + sharesBeforeDecrease + sharesAfterDecrease + sharesBeforeIncrease + sharesAfterIncrease + + totalPooledEther + totalShares + + balanceAfterDecrease + balanceAfterIncrease + + block + blockTime + transactionHash + transactionIndex + logIndex + } + totalRewards( + first: 1 + skip: 0 + orderBy: block + orderDirection: desc + block: { number: $block } + ) { + totalPooledEtherBefore + totalPooledEtherAfter + totalSharesBefore + totalSharesAfter + + apr + + block + blockTime + logIndex + } + } +`; diff --git a/packages/sdk/src/rewards/subgraph/subrgaph.ts b/packages/sdk/src/rewards/subgraph/subrgaph.ts new file mode 100644 index 00000000..33bc5ffe --- /dev/null +++ b/packages/sdk/src/rewards/subgraph/subrgaph.ts @@ -0,0 +1,131 @@ +import { request } from 'graphql-request'; +import { + StatusQuery, + LidoTransfersQuery, + TotalRewardsQuery, + InitialStateQuery, +} from './queries.js'; +import { + type SubgraphRequestOptions, + type GetLastIndexedBlockOptions, + type StatusQueryResult, + type GetTransfersOptions, + type TransferEventEntity, + type LidoTransfersQueryVariablesNoPagination, + type GetTotalRewardsOptions, + type TotalRewardEntity, + type GetTotalRewardsResult, + type GetTransfersResult, + LidoTransfersQueryResult, + TotalRewardsQueryResult, + GetInitialDataOptions, + GetInitialDataResult, + InitialDataQueryVariables, + InitialDataQueryResult, +} from './types.js'; + +const requestAllWithStep = async ({ + url, + step, + document, + variables, + fromBlock, + toBlock, + extractArray, +}: { + variables: TVariables; + document: any; + extractArray: (result: TResult | null) => TResultEntry[]; +} & SubgraphRequestOptions): Promise> => { + let skip = 0; + const results: TResultEntry[] = []; + do { + const partialResult = await request({ + url, + document, + variables: { + ...variables, + first: step, + skip, + fromBlock: Number(fromBlock), + toBlock: Number(toBlock), + }, + }); + const array = extractArray(partialResult); + results.push(...array); + // break if we don't fetch more than step + if (array.length < step) break; + skip += step; + // maybe some sort of max iterations mechanism + } while (true); + return results; +}; + +export const getLastIndexedBlock = async ({ + url, +}: GetLastIndexedBlockOptions): Promise< + StatusQueryResult['_meta']['block'] +> => { + return (await request({ url, document: StatusQuery })) + ._meta.block; +}; + +export const getInitialData = async ({ + url, + address, + block, +}: GetInitialDataOptions): Promise => { + const { lidoTransfers, totalRewards } = await request< + InitialDataQueryResult, + InitialDataQueryVariables + >({ + document: InitialStateQuery, + url, + variables: { address, block: Number(block) }, + }); + return { + transfer: lidoTransfers.length > 0 ? lidoTransfers[0]! : null, + rebase: totalRewards.length > 0 ? totalRewards[0]! : null, + }; +}; + +export const getTransfers = async ({ + url, + address, + fromBlock, + toBlock, + step, +}: GetTransfersOptions): Promise => { + return requestAllWithStep< + LidoTransfersQueryResult, + TransferEventEntity, + LidoTransfersQueryVariablesNoPagination + >({ + url, + document: LidoTransfersQuery, + step, + fromBlock, + extractArray: (result) => result?.lidoTransfers ?? [], + toBlock, + variables: { + address, + }, + }); +}; + +export const getTotalRewards = async ({ + url, + fromBlock, + step, + toBlock, +}: GetTotalRewardsOptions): Promise => { + return requestAllWithStep({ + url, + document: TotalRewardsQuery, + step, + extractArray: (result) => result?.totalRewards ?? [], + fromBlock, + toBlock, + variables: {}, + }); +}; diff --git a/packages/sdk/src/rewards/subgraph/types.ts b/packages/sdk/src/rewards/subgraph/types.ts new file mode 100644 index 00000000..97366526 --- /dev/null +++ b/packages/sdk/src/rewards/subgraph/types.ts @@ -0,0 +1,112 @@ +// Base entities + +export type TransferEventEntity = { + from: string; + to: string; + value: string; + + shares: string; + sharesBeforeDecrease: string; + sharesAfterDecrease: string; + sharesBeforeIncrease: string; + sharesAfterIncrease: string; + + totalPooledEther: string; + totalShares: string; + + balanceAfterDecrease: string; + balanceAfterIncrease: string; + + block: string; + blockTime: string; + transactionHash: string; + transactionIndex: string; + logIndex: string; +}; + +export type TotalRewardEntity = { + id: string; + + totalPooledEtherBefore: string; + totalPooledEtherAfter: string; + totalSharesBefore: string; + totalSharesAfter: string; + + apr: string; + + block: string; + blockTime: string; + logIndex: string; +}; + +// Queries + +export type LidoTransfersQueryResult = { + lidoTransfers: TransferEventEntity[]; +}; +export type TotalRewardsQueryResult = { totalRewards: TotalRewardEntity[] }; + +export type LidoTransfersQueryVariablesNoPagination = { + address: string; +}; + +export type StatusQueryResult = { + _meta: { + block: { + number: number; + hash: string; + }; + }; +}; + +export type InitialDataQueryVariables = { + address: string; + block: number; +}; + +export type InitialDataQueryResult = { + lidoTransfers: TransferEventEntity[]; + totalRewards: TotalRewardEntity[]; +}; + +// Requests + +export type SubgraphRequestOptions = { + url: string; + step: number; + fromBlock: bigint; + toBlock: bigint; +}; + +// last indexed + +export type GetLastIndexedBlockOptions = Pick; + +export type GetLastIndexedBlockResult = StatusQueryResult['_meta']['block']; + +// last transfer + +export type GetInitialDataOptions = { + address: string; + block: bigint; + url: string; +}; + +export type GetInitialDataResult = { + transfer: TransferEventEntity | null; + rebase: TotalRewardEntity | null; +}; + +// get transfers + +export type GetTransfersOptions = SubgraphRequestOptions & { + address: string; +}; + +export type GetTransfersResult = TransferEventEntity[]; + +// get rewards + +export type GetTotalRewardsOptions = SubgraphRequestOptions; + +export type GetTotalRewardsResult = TotalRewardEntity[]; diff --git a/packages/sdk/src/rewards/types.ts b/packages/sdk/src/rewards/types.ts index ed8c672f..e6018724 100644 --- a/packages/sdk/src/rewards/types.ts +++ b/packages/sdk/src/rewards/types.ts @@ -1,6 +1,7 @@ import { type Address, type BlockTag, type Log } from 'viem'; import { type rewardsEventsAbi } from './abi/rewardsEvents.js'; import { type LidoSDKCommonProps } from '../core/types.js'; +import { TotalRewardEntity, TransferEventEntity } from './subgraph/types.js'; export type NonPendingBlockTag = Exclude; @@ -19,7 +20,7 @@ export type GetRewardsOptions = { } ); -export type RewardsEvents = +export type RewardsChainEvents = | Log< bigint, number, @@ -39,6 +40,8 @@ export type RewardsEvents = 'TokenRebased' >; +export type RewardsSubgraphEvents = TransferEventEntity | TotalRewardEntity; + export type RewardType = | 'submit' | 'withdrawal' @@ -46,21 +49,36 @@ export type RewardType = | 'transfer_in' | 'transfer_out'; -export type Reward = { +export type Reward = { type: RewardType; change: bigint; changeShares: bigint; balance: bigint; balanceShares: bigint; shareRate: number; - originalEvent: RewardsEvents; + originalEvent: TEvent; }; -export type GetRewardsResult = { - rewards: Reward[]; +type GetRewardsCommonResult = { baseBalance: bigint; baseBalanceShares: bigint; baseShareRate: number; fromBlock: bigint; toBlock: bigint; }; + +export type GetRewardsFromSubgraphOptions = GetRewardsOptions & { + getSubgraphUrl: (id: string, chainId: number) => string; + step?: number; +}; + +export type GetRewardsFromSubgraphResults = { + rewards: Reward[]; + lastIndexedBlock: bigint; +} & GetRewardsCommonResult; + +export type GetRewardsFromChainOptions = GetRewardsOptions; + +export type GetRewardsFromChainResults = { + rewards: Reward[]; +} & GetRewardsCommonResult; diff --git a/playground/demo/rewards/index.tsx b/playground/demo/rewards/index.tsx index 79c72537..36cfe01b 100644 --- a/playground/demo/rewards/index.tsx +++ b/playground/demo/rewards/index.tsx @@ -1,4 +1,7 @@ -import { GetRewardsResult } from '@lidofinance/lido-ethereum-sdk/dist/types/rewards/types'; +import { + GetRewardsFromChainResults, + GetRewardsFromSubgraphResults, +} from '@lidofinance/lido-ethereum-sdk/dist/types/rewards/types'; import { Input, Accordion, @@ -11,19 +14,18 @@ import { Container, DataTableRow, } from '@lidofinance/lido-ui'; -import { useWeb3 } from '@reef-knot/web3-react'; import { Action, renderTokenResult } from 'components/action'; -import { DEFAULT_VALUE, ValueType } from 'components/tokenInput'; -import TokenInput from 'components/tokenInput/tokenInput'; import { useAddressState } from 'hooks/useAddressState'; import { useLidoSDK } from 'providers/sdk'; import { useState } from 'react'; -import { transactionToast } from 'utils/transaction-toast'; import { Address } from 'viem'; -const renderRewards = (result: GetRewardsResult) => { +const renderRewards = ( + result: GetRewardsFromChainResults | GetRewardsFromSubgraphResults, +) => { const steth = renderTokenResult('stETH'); const shares = renderTokenResult('shares'); + console.log(result); return ( @@ -32,6 +34,11 @@ const renderRewards = (result: GetRewardsResult) => { {result.toBlock.toString()} + {'lastIndexedBlock' in result ? ( + + {result.lastIndexedBlock.toString()} + + ) : null} {steth(result.baseBalance)} @@ -53,7 +60,11 @@ const renderRewards = (result: GetRewardsResult) => { {result.rewards.map((r, index) => ( - {r.originalEvent.blockNumber.toString()} + + {'block' in r.originalEvent + ? r.originalEvent.block + : r.originalEvent.blockNumber.toString()} + {r.type} {steth(r.balance)} @@ -75,7 +86,7 @@ export const RewardsDemo = () => { const [rewardsAddress, setRewardsAddress] = useAddressState(undefined, { useAccount: true, }); - const [blocksBack, setBlocksBack] = useState(10000); + const [blocksBack, setBlocksBack] = useState(100000); const { rewards } = useLidoSDK(); return ( @@ -105,6 +116,27 @@ export const RewardsDemo = () => { onChange={(e) => setBlocksBack(e.currentTarget.valueAsNumber)} /> + { + return rewards.getRewardsFromSubgraph({ + address: rewardsAddress, + blocksBack: BigInt(blocksBack), + // Warning! these endpoints will be deprecated + getSubgraphUrl(_, chainId) { + switch (chainId) { + case 1: + return 'https://api.thegraph.com/subgraphs/name/lidofinance/lido'; + case 5: + return 'https://api.thegraph.com/subgraphs/name/lidofinance/lido-testnet'; + default: + throw new Error('unsupported chain'); + } + }, + }); + }} + renderResult={renderRewards} + title="Get Rewards From Subgraph" + > ); }; diff --git a/yarn.lock b/yarn.lock index 79a28de9..d7871ebb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2233,6 +2233,15 @@ __metadata: languageName: node linkType: hard +"@graphql-typed-document-node/core@npm:^3.2.0": + version: 3.2.0 + resolution: "@graphql-typed-document-node/core@npm:3.2.0" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: fa44443accd28c8cf4cb96aaaf39d144a22e8b091b13366843f4e97d19c7bfeaf609ce3c7603a4aeffe385081eaf8ea245d078633a7324c11c5ec4b2011bb76d + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.10": version: 0.11.10 resolution: "@humanwhocodes/config-array@npm:0.11.10" @@ -2624,6 +2633,8 @@ __metadata: "@ethersproject/bytes": ^5.7.0 "@types/fs-extra": ^11.0.1 fs-extra: ^11.1.1 + graphql: ^16.8.1 + graphql-request: ^6.1.0 rimraf: ^5.0.1 tiny-invariant: ^1.3.1 typescript: 5.1.6 @@ -8368,6 +8379,25 @@ __metadata: languageName: node linkType: hard +"graphql-request@npm:^6.1.0": + version: 6.1.0 + resolution: "graphql-request@npm:6.1.0" + dependencies: + "@graphql-typed-document-node/core": ^3.2.0 + cross-fetch: ^3.1.5 + peerDependencies: + graphql: 14 - 16 + checksum: 6d62630a0169574442320651c1f7626c0c602025c3c46b19e09417c9579bb209306ee63de9793a03be2e1701bb7f13971f8545d99bc6573e340f823af0ad35b2 + languageName: node + linkType: hard + +"graphql@npm:^16.8.1": + version: 16.8.1 + resolution: "graphql@npm:16.8.1" + checksum: 8d304b7b6f708c8c5cc164b06e92467dfe36aff6d4f2cf31dd19c4c2905a0e7b89edac4b7e225871131fd24e21460836b369de0c06532644d15b461d55b1ccc0 + languageName: node + linkType: hard + "gzip-size@npm:^6.0.0": version: 6.0.0 resolution: "gzip-size@npm:6.0.0" From 2f8604e2b33d3bd34d57b6e9f4f4c688fdad41f3 Mon Sep 17 00:00:00 2001 From: Evgeny Taktarov Date: Fri, 13 Oct 2023 17:35:37 +0700 Subject: [PATCH 07/16] feat: add includeZeroRebase --- packages/sdk/src/rewards/rewards.ts | 28 ++++++++++++++++--- packages/sdk/src/rewards/types.ts | 1 + playground/components/toggle-button/index.tsx | 1 + .../toggle-button/toggle-button.tsx | 22 +++++++++++++++ playground/demo/rewards/index.tsx | 11 +++++++- 5 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 playground/components/toggle-button/index.tsx create mode 100644 playground/components/toggle-button/toggle-button.tsx diff --git a/packages/sdk/src/rewards/rewards.ts b/packages/sdk/src/rewards/rewards.ts index e8302cc6..1e80291d 100644 --- a/packages/sdk/src/rewards/rewards.ts +++ b/packages/sdk/src/rewards/rewards.ts @@ -97,7 +97,7 @@ export class LidoSDKRewards { props: GetRewardsOptions, ): Promise { const [ - { address, fromBlock, toBlock }, + { address, fromBlock, toBlock, includeZeroRebases = false }, stethContract, withdrawalQueueAddress, ] = await Promise.all([ @@ -172,7 +172,7 @@ export class LidoSDKRewards { let shareRate = baseShareRate; let prevSharesBalance = baseBalanceShares; type RewardEntry = Reward; - const rewards: Reward[] = events.map((event) => { + let rewards: Reward[] = events.map((event) => { if (event.eventName === 'TransferShares') { const { from, to, sharesValue } = event.args; let type: RewardEntry['type'], @@ -219,6 +219,13 @@ export class LidoSDKRewards { } throw new Error('Impossible event type'); }); + + if (!includeZeroRebases) { + rewards = rewards.filter( + (r) => !(r.type === 'rebase' && r.change === 0n), + ); + } + return { rewards, baseBalanceShares, @@ -235,7 +242,14 @@ export class LidoSDKRewards { props: GetRewardsFromSubgraphOptions, ): Promise { const [ - { getSubgraphUrl, address, fromBlock, toBlock, step = 1000 }, + { + getSubgraphUrl, + address, + fromBlock, + toBlock, + step = 1000, + includeZeroRebases = false, + }, withdrawalQueueAddress, ] = await Promise.all([ this.parseProps(props), @@ -327,7 +341,7 @@ export class LidoSDKRewards { type RewardEntry = Reward; - const rewards: Reward[] = events.map((event) => { + let rewards: Reward[] = events.map((event) => { // it's a transfer if ('value' in event) { const { @@ -407,6 +421,12 @@ export class LidoSDKRewards { throw new Error('Impossible event'); }); + if (!includeZeroRebases) { + rewards = rewards.filter( + (r) => !(r.type === 'rebase' && r.change === 0n), + ); + } + return { rewards, baseBalance, diff --git a/packages/sdk/src/rewards/types.ts b/packages/sdk/src/rewards/types.ts index e6018724..d13b00df 100644 --- a/packages/sdk/src/rewards/types.ts +++ b/packages/sdk/src/rewards/types.ts @@ -9,6 +9,7 @@ export type LidoSDKRewardsProps = LidoSDKCommonProps; export type GetRewardsOptions = { address: Address; + includeZeroRebases?: boolean; toBlock?: bigint | NonPendingBlockTag; } & ( | { diff --git a/playground/components/toggle-button/index.tsx b/playground/components/toggle-button/index.tsx new file mode 100644 index 00000000..1dfa46be --- /dev/null +++ b/playground/components/toggle-button/index.tsx @@ -0,0 +1 @@ +export { ToggleButton } from './toggle-button'; diff --git a/playground/components/toggle-button/toggle-button.tsx b/playground/components/toggle-button/toggle-button.tsx new file mode 100644 index 00000000..8a86412b --- /dev/null +++ b/playground/components/toggle-button/toggle-button.tsx @@ -0,0 +1,22 @@ +import { Button } from '@lidofinance/lido-ui'; +import { ComponentProps } from 'react'; + +type ToggleProps = { + onChange: (state: boolean) => void; + value: boolean; + title: string; +} & Omit, 'value' | 'onChange'>; + +// TODO: change color on state +export const ToggleButton = ({ + onChange, + value, + title, + ...props +}: ToggleProps) => { + return ( + + ); +}; diff --git a/playground/demo/rewards/index.tsx b/playground/demo/rewards/index.tsx index 36cfe01b..32a273b9 100644 --- a/playground/demo/rewards/index.tsx +++ b/playground/demo/rewards/index.tsx @@ -12,9 +12,11 @@ import { Td, Th, Container, + Button, DataTableRow, } from '@lidofinance/lido-ui'; import { Action, renderTokenResult } from 'components/action'; +import { ToggleButton } from 'components/toggle-button/toggle-button'; import { useAddressState } from 'hooks/useAddressState'; import { useLidoSDK } from 'providers/sdk'; import { useState } from 'react'; @@ -25,7 +27,6 @@ const renderRewards = ( ) => { const steth = renderTokenResult('stETH'); const shares = renderTokenResult('shares'); - console.log(result); return ( @@ -87,6 +88,7 @@ export const RewardsDemo = () => { useAccount: true, }); const [blocksBack, setBlocksBack] = useState(100000); + const [includeZeroRebases, setIncludeZeroRebases] = useState(false); const { rewards } = useLidoSDK(); return ( @@ -98,6 +100,7 @@ export const RewardsDemo = () => { return rewards.getRewardsFromChain({ address: rewardsAddress, blocksBack: BigInt(blocksBack), + includeZeroRebases, }); }} > @@ -115,12 +118,18 @@ export const RewardsDemo = () => { value={blocksBack} onChange={(e) => setBlocksBack(e.currentTarget.valueAsNumber)} /> + { return rewards.getRewardsFromSubgraph({ address: rewardsAddress, blocksBack: BigInt(blocksBack), + includeZeroRebases, // Warning! these endpoints will be deprecated getSubgraphUrl(_, chainId) { switch (chainId) { From 6522df779d748b154218ad2f571ecef410a8d6c5 Mon Sep 17 00:00:00 2001 From: Evgeny Taktarov Date: Fri, 13 Oct 2023 18:27:28 +0700 Subject: [PATCH 08/16] feat: imports and readme --- packages/sdk/README.md | 81 +++++++++++++++++++++++++++++++ packages/sdk/package.json | 5 ++ playground/demo/rewards/index.tsx | 63 ++++++++++++------------ 3 files changed, 119 insertions(+), 30 deletions(-) diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 02a7a04b..964a5c24 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -56,6 +56,7 @@ The project is currently under development and may change in the future. - [APR](#apr) - [Lido events](#lido-events) - [Rebase](#rebase) +- [Rewards](#Rewards) ## Installation @@ -1122,3 +1123,83 @@ console.log(lastRebaseEvent, 'last rebase event'); console.log(lastRebaseEventsByCount, 'last rebase events by count'); console.log(lastRebaseEventsByDays, 'last rebase events by days'); ``` + +## Rewards + +This module allows you to query historical rewards data for given address via chain events or subgraph. + +### Common Options + +- **address** - address of an account you want to query rewards for +- **toBlock** - **default**:`latest` block number or tag for upper bound for rewards. `pending` not allowed +- **fromBlock** - block number or tag for lower bound for rewards. `pending` not allowed +- **blocksBack** - alternative to **fromBlock**. Amount of blocks to look back to from **toBlock**. +- **includeZeroRebases** - **default**:`false` include rebase events when users had no rewards(because of empty balance) + +### Common Return + +```Typescript +type RewardsResult = { + // pre query states + baseBalance: bigint; + baseBalanceShares: bigint; + baseShareRate: number; + // computed block numbers + fromBlock: bigint; + toBlock: bigint; + // query result in block/logIndex ascending order + rewards: { + type: 'submit' | 'withdrawal' | 'rebase' | 'transfer_in' | 'transfer_out'; + change: bigint; // negative or positive change in stETH + changeShares: bigint; // same in shares + balance: bigint; // post event balance in stETH + balanceShares: bigint; // same in shares + shareRate: number; // apx share rate at a time of event + originalEvent: RewardsChainEvents | RewardsSubgraphEvents ; // original event from chain/subgraph, contains extra info + }[] +}; +``` + +### Get Rewards from chain + +This method heavily utilizes RPC fetching chain event logs. It's better suited for smaller,recent queries. Beware that this might cause rate limit issues on free RPC endpoints. + +```ts +const lidoSDK = new LidoSDK({ + chainId: 5, + rpcUrls: ['https://eth-goerli.alchemyapi.io/v2/{ALCHEMY_API_KEY}'], +}); + +const rewardsQuery = await lidoSDK.rewards.getRewardsFromChain({ + address: rewardsAddress, + blocksBack: 1000, +}); + +console.log(rewardsQuery.rewards); +``` + +### Get Rewards from subgraph + +This method requires you to provide API URL to send subgraph requests to. It's better suited for larger, more historical queries. + +#### Important notes + +**toBlock** is capped by last index block in subgraph. Block number is available in result object by `lastIndexedBlock`. + +```ts +const lidoSDK = new LidoSDK({ + chainId: 5, + rpcUrls: ['https://eth-goerli.alchemyapi.io/v2/{ALCHEMY_API_KEY}'], +}); + +const rewardsQuery = await lidoSDK.rewards.getRewardsFromSubgraph({ + address: rewardsAddress, + blocksBack: 10000, + step: 500, // defaults to 1000, max entities per one request to endpoint + getSubgraphUrl(graphId, chainId) { + return `https://gateway.thegraph.com/api/${apiKey}/subgraphs/id/${id}`; + }, +}); + +console.log(rewardsQuery.rewards); +``` diff --git a/packages/sdk/package.json b/packages/sdk/package.json index fa1063e4..025bbf5d 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -61,6 +61,11 @@ "default": "./dist/cjs/events/index.js", "types": "./dist/types/events/index.d.ts" }, + "./rewards": { + "import": "./dist/esm/rewards/index.js", + "default": "./dist/cjs/rewards/index.js", + "types": "./dist/types/rewards/index.d.ts" + }, "./package.json": "./package.json" }, "typesVersions": { diff --git a/playground/demo/rewards/index.tsx b/playground/demo/rewards/index.tsx index 32a273b9..05eb63ae 100644 --- a/playground/demo/rewards/index.tsx +++ b/playground/demo/rewards/index.tsx @@ -12,7 +12,6 @@ import { Td, Th, Container, - Button, DataTableRow, } from '@lidofinance/lido-ui'; import { Action, renderTokenResult } from 'components/action'; @@ -49,36 +48,40 @@ const renderRewards = ( {result.baseShareRate} - - - - - - - - - - - {result.rewards.map((r, index) => ( - - - - - + {result.rewards.length > 0 ? ( +
BlockTypeBalanceReward
- {'block' in r.originalEvent - ? r.originalEvent.block - : r.originalEvent.blockNumber.toString()} - {r.type} - {steth(r.balance)} -
({shares(r.balanceShares)}) -
- {steth(r.change)} -
({shares(r.changeShares)}) -
+ + + + + + - ))} - -
BlockTypeBalanceReward
+ + + {result.rewards.map((r, index) => ( + + + {'block' in r.originalEvent + ? r.originalEvent.block + : r.originalEvent.blockNumber.toString()} + + {r.type} + + {steth(r.balance)} +
({shares(r.balanceShares)}) + + + {steth(r.change)} +
({shares(r.changeShares)}) + + + ))} + + + ) : ( + + )}
); }; From 54b6b0f3c79b97dd313456636d6cf6a8ef17d110 Mon Sep 17 00:00:00 2001 From: Andrei Date: Mon, 16 Oct 2023 16:22:09 +0700 Subject: [PATCH 09/16] docs: update readme --- packages/sdk/README.md | 73 ++++++++++++++++++++++++++++++-- packages/sdk/src/events/index.ts | 1 + packages/sdk/src/index.ts | 2 + 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 02a7a04b..548c0372 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -1085,6 +1085,27 @@ const wsteth = await lidoSDK.core.getContractAddress( ### APR +#### Methods + +##### `getLastApr` + +###### Output Parameters: + +- Type: number + +##### `getSmaApr` + +###### Input Parameters: + +- `props: { days }` + - `days` (Type: number): The number of days back to return sma apr. + +###### Output Parameters: + +- Type: number + +#### Examples + ```ts import { LidoSDK } from '@lidofinance/lido-ethereum-sdk'; @@ -1094,16 +1115,60 @@ const lidoSDK = new LidoSDK({ }); const lastApr = await lidoSDK.statistics.apr.getLastApr(); -const smaApr = await lidoSDK.statistics.apr.getSmaApr(); +const smaApr = await lidoSDK.statistics.apr.getSmaApr({ days: 7 }); console.log(lastApr, 'last apr'); -console.log(smaApr, 'sma apr'); +console.log(smaApr, 'sma apr by 7 days'); ``` ## Lido events ### Rebase +#### Methods + +##### `getLastRebaseEvent` + +###### Output Parameters: + +- Type: RebaseEvent + +##### `getFirstRebaseEvent` + +###### Input Parameters: + +- `props: { daysAgo, goBackFromBlock }` + - `daysAgo` (Type: number): The number of days ago from which to start searching for the first rebase event. + - `goBackFromBlock` (Type: number | undefined): Block number from which to start the search. + +###### Output Parameters: + +- Type: RebaseEvent + +##### `getRebaseEvents` + +###### Input Parameters: + +- `props: { count }` + - `count` (Type: number): The number of events to return. + +###### Output Parameters: + +- Type: Array of RebaseEvent objects + +##### `getRebaseEventsByDays` + +###### Input Parameters: + +- `props: { days }` + - `days` (Type: number): The number of days back to return rebase events. + +###### Output Parameters: + +- Type: Array of RebaseEvent objects + +#### Examples + ```ts import { LidoSDK } from '@lidofinance/lido-ethereum-sdk'; @@ -1113,12 +1178,14 @@ const lidoSDK = new LidoSDK({ }); const lastRebaseEvent = await lidoSDK.events.stethEvents.getLastRebaseEvent(); +const firstRebaseEvent = await lidoSDK.events.stethEvents.getFirstRebaseEvent(); const lastRebaseEventsByCount = await lidoSDK.events.stethEvents.getRebaseEvents({ count: 10 }); const lastRebaseEventsByDays = - await lidoSDK.events.stethEvents.getRebaseEventByDays({ days: 10 }); + await lidoSDK.events.stethEvents.getRebaseEventsByDays({ days: 10 }); console.log(lastRebaseEvent, 'last rebase event'); +console.log(firstRebaseEvent, 'first rebase event'); console.log(lastRebaseEventsByCount, 'last rebase events by count'); console.log(lastRebaseEventsByDays, 'last rebase events by days'); ``` diff --git a/packages/sdk/src/events/index.ts b/packages/sdk/src/events/index.ts index fb848876..df5449fa 100644 --- a/packages/sdk/src/events/index.ts +++ b/packages/sdk/src/events/index.ts @@ -1 +1,2 @@ export { LidoSDKEvents } from './events.js'; +export * from './types.js'; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index f836230d..d5eb59a2 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -16,3 +16,5 @@ export { } from './withdraw/index.js'; export { LIDO_CONTRACT_NAMES } from './common/constants.js'; export { type WrapProps } from './wrap/index.js'; +export { LidoSDKEvents, RebaseEvent } from './events/index.js'; +export { LidoSDKStatistics } from './statistics/index.js'; From 9b8b0e5e6a2ce87794a3a84d62f8941609a8f168 Mon Sep 17 00:00:00 2001 From: Andrei Date: Mon, 16 Oct 2023 16:26:06 +0700 Subject: [PATCH 10/16] refactor: add strict mode --- packages/sdk/src/events/stethEvents.ts | 13 +++++--- packages/sdk/src/events/types.ts | 14 ++++----- packages/sdk/src/statistics/apr.ts | 43 ++++++++++---------------- 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/packages/sdk/src/events/stethEvents.ts b/packages/sdk/src/events/stethEvents.ts index 0fec5ff4..b0b417bf 100644 --- a/packages/sdk/src/events/stethEvents.ts +++ b/packages/sdk/src/events/stethEvents.ts @@ -59,15 +59,16 @@ export class LidoSDKStethEvents { @ErrorHandler() public async getLastRebaseEvent(): Promise { const contract = await this.getContractStETH(); - const lastBlock = await this.getLastBlock() + const lastBlock = await this.getLastBlock(); for (let days = 1; days <= DAYS_LIMIT; days++) { - const fromBlock = lastBlock.number - BLOCKS_BY_DAY * BigInt(days) + const fromBlock = lastBlock.number - BLOCKS_BY_DAY * BigInt(days); const logs = await this.core.rpcProvider.getLogs({ address: contract.address, event: StethEventsAbi[REBASE_EVENT_ABI_INDEX], fromBlock: fromBlock, toBlock: fromBlock + BLOCKS_BY_DAY, + strict: true, }); if (logs.length > 0) return logs[logs.length - 1]; @@ -83,12 +84,13 @@ export class LidoSDKStethEvents { goBackFromBlock?: bigint; }): Promise { const { daysAgo } = props; - const goBackFromBlock = props.goBackFromBlock ?? (await this.getLastBlock()).number + const goBackFromBlock = + props.goBackFromBlock ?? (await this.getLastBlock()).number; const contract = await this.getContractStETH(); for (let days = 1; days <= DAYS_LIMIT; days++) { - const from = goBackFromBlock - BigInt(daysAgo + 1 - days) * BLOCKS_BY_DAY + const from = goBackFromBlock - BigInt(daysAgo + 1 - days) * BLOCKS_BY_DAY; const to = from + BLOCKS_BY_DAY; const logs = await this.core.rpcProvider.getLogs({ @@ -96,6 +98,7 @@ export class LidoSDKStethEvents { event: StethEventsAbi[REBASE_EVENT_ABI_INDEX], fromBlock: from, toBlock: to, + strict: true, }); if (logs.length > 0) return logs[0]; @@ -124,6 +127,7 @@ export class LidoSDKStethEvents { event: StethEventsAbi[REBASE_EVENT_ABI_INDEX], fromBlock: block.number, toBlock: 'latest', + strict: true, }); const logsByDays = logs.filter((log) => { @@ -150,6 +154,7 @@ export class LidoSDKStethEvents { event: StethEventsAbi[8], fromBlock: block.number, toBlock: 'latest', + strict: true, }); return logs.slice(logs.length - count); diff --git a/packages/sdk/src/events/types.ts b/packages/sdk/src/events/types.ts index c7b4552b..b674f6e5 100644 --- a/packages/sdk/src/events/types.ts +++ b/packages/sdk/src/events/types.ts @@ -15,13 +15,13 @@ export type RebaseEvent = { transactionHash: string; transactionIndex: number; args: { - reportTimestamp?: bigint; - timeElapsed?: bigint; - preTotalShares?: bigint; - preTotalEther?: bigint; - postTotalShares?: bigint; - postTotalEther?: bigint; - sharesMintedAsFees?: bigint; + reportTimestamp: bigint; + timeElapsed: bigint; + preTotalShares: bigint; + preTotalEther: bigint; + postTotalShares: bigint; + postTotalEther: bigint; + sharesMintedAsFees: bigint; }; eventName: 'TokenRebased'; }; diff --git a/packages/sdk/src/statistics/apr.ts b/packages/sdk/src/statistics/apr.ts index d9f2cb2a..c9c4a06a 100644 --- a/packages/sdk/src/statistics/apr.ts +++ b/packages/sdk/src/statistics/apr.ts @@ -33,26 +33,21 @@ export class LidoSDKApr { @Logger('Statistic:') @ErrorHandler() - public async getSmaApr(props: { - days: number; - }): Promise { + public async getSmaApr(props: { days: number }): Promise { const { days } = props; - const lastEvent = await this.events.stethEvents.getLastRebaseEvent() - invariant(lastEvent, 'Last event is not defined') + const lastEvent = await this.events.stethEvents.getLastRebaseEvent(); + invariant(lastEvent, 'Last event is not defined'); const firstEvent = await this.events.stethEvents.getFirstRebaseEvent({ - daysAgo: days, goBackFromBlock: lastEvent.blockNumber, - }) - invariant(firstEvent, 'First event is not defined') + daysAgo: days, + goBackFromBlock: lastEvent.blockNumber, + }); + invariant(firstEvent, 'First event is not defined'); - invariant(firstEvent.args.timeElapsed, 'time elapsed is not defined') - invariant(lastEvent.args.reportTimestamp, 'Last event reportTimestamp is not defined') - invariant(firstEvent.args.reportTimestamp, 'First event reportTimestamp is not defined') - - const timeElapsed = firstEvent.args.timeElapsed + ( - lastEvent.args.reportTimestamp - firstEvent.args.reportTimestamp - ); + const timeElapsed = + firstEvent.args.timeElapsed + + (lastEvent.args.reportTimestamp - firstEvent.args.reportTimestamp); const smaApr = this.calculateApr({ preTotalEther: firstEvent.args.preTotalEther, @@ -60,17 +55,17 @@ export class LidoSDKApr { postTotalEther: lastEvent.args.postTotalEther, postTotalShares: lastEvent.args.postTotalShares, timeElapsed, - }) + }); return smaApr; } private calculateApr(props: { - preTotalEther?: bigint; - preTotalShares?: bigint; - postTotalEther?: bigint; - postTotalShares?: bigint; - timeElapsed?: bigint; + preTotalEther: bigint; + preTotalShares: bigint; + postTotalEther: bigint; + postTotalShares: bigint; + timeElapsed: bigint; }): number { const { preTotalEther, @@ -80,12 +75,6 @@ export class LidoSDKApr { timeElapsed, } = props; - invariant(preTotalEther, 'preTotalEther is not defined'); - invariant(preTotalShares, 'preTotalShares is not defined'); - invariant(postTotalEther, 'postTotalEther is not defined'); - invariant(postTotalShares, 'postTotalShares is not defined'); - invariant(timeElapsed, 'timeElapsed is not defined'); - const preShareRate = (preTotalEther * BigInt(10 ** 27)) / preTotalShares; const postShareRate = (postTotalEther * BigInt(10 ** 27)) / postTotalShares; const mulForPrecision = 1000; From 696b961cd54e1e1c3cc3bb9f5f11d7f6998b1340 Mon Sep 17 00:00:00 2001 From: Andrei Date: Mon, 16 Oct 2023 16:37:54 +0700 Subject: [PATCH 11/16] refactor: rename args --- packages/sdk/README.md | 14 ++++++++------ packages/sdk/src/events/stethEvents.ts | 18 +++++++++--------- packages/sdk/src/statistics/apr.ts | 4 ++-- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 548c0372..ae79a32c 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -1137,9 +1137,9 @@ console.log(smaApr, 'sma apr by 7 days'); ###### Input Parameters: -- `props: { daysAgo, goBackFromBlock }` - - `daysAgo` (Type: number): The number of days ago from which to start searching for the first rebase event. - - `goBackFromBlock` (Type: number | undefined): Block number from which to start the search. +- `props: { days, fromBlockNumber }` + - `days` (Type: number): The number of days ago from which to start searching for the first rebase event. + - `fromBlockNumber` (Type: number | undefined): Block number from which to start the search. ###### Output Parameters: @@ -1178,11 +1178,13 @@ const lidoSDK = new LidoSDK({ }); const lastRebaseEvent = await lidoSDK.events.stethEvents.getLastRebaseEvent(); -const firstRebaseEvent = await lidoSDK.events.stethEvents.getFirstRebaseEvent(); +const firstRebaseEvent = await lidoSDK.events.stethEvents.getFirstRebaseEvent({ + days: 3, +}); const lastRebaseEventsByCount = - await lidoSDK.events.stethEvents.getRebaseEvents({ count: 10 }); + await lidoSDK.events.stethEvents.getRebaseEvents({ count: 7 }); const lastRebaseEventsByDays = - await lidoSDK.events.stethEvents.getRebaseEventsByDays({ days: 10 }); + await lidoSDK.events.stethEvents.getRebaseEventsByDays({ days: 7 }); console.log(lastRebaseEvent, 'last rebase event'); console.log(firstRebaseEvent, 'first rebase event'); diff --git a/packages/sdk/src/events/stethEvents.ts b/packages/sdk/src/events/stethEvents.ts index b0b417bf..a2cc6f5b 100644 --- a/packages/sdk/src/events/stethEvents.ts +++ b/packages/sdk/src/events/stethEvents.ts @@ -61,8 +61,8 @@ export class LidoSDKStethEvents { const contract = await this.getContractStETH(); const lastBlock = await this.getLastBlock(); - for (let days = 1; days <= DAYS_LIMIT; days++) { - const fromBlock = lastBlock.number - BLOCKS_BY_DAY * BigInt(days); + for (let day = 1; day <= DAYS_LIMIT; day++) { + const fromBlock = lastBlock.number - BLOCKS_BY_DAY * BigInt(day); const logs = await this.core.rpcProvider.getLogs({ address: contract.address, event: StethEventsAbi[REBASE_EVENT_ABI_INDEX], @@ -80,17 +80,17 @@ export class LidoSDKStethEvents { @Logger('Events:') @ErrorHandler() public async getFirstRebaseEvent(props: { - daysAgo: number; - goBackFromBlock?: bigint; + days: number; + fromBlockNumber?: bigint; }): Promise { - const { daysAgo } = props; - const goBackFromBlock = - props.goBackFromBlock ?? (await this.getLastBlock()).number; + const { days } = props; + const fromBlockNumber = + props.fromBlockNumber ?? (await this.getLastBlock()).number; const contract = await this.getContractStETH(); - for (let days = 1; days <= DAYS_LIMIT; days++) { - const from = goBackFromBlock - BigInt(daysAgo + 1 - days) * BLOCKS_BY_DAY; + for (let day = 1; day <= DAYS_LIMIT; day++) { + const from = fromBlockNumber - BigInt(days + 1 - day) * BLOCKS_BY_DAY; const to = from + BLOCKS_BY_DAY; const logs = await this.core.rpcProvider.getLogs({ diff --git a/packages/sdk/src/statistics/apr.ts b/packages/sdk/src/statistics/apr.ts index c9c4a06a..b1382598 100644 --- a/packages/sdk/src/statistics/apr.ts +++ b/packages/sdk/src/statistics/apr.ts @@ -40,8 +40,8 @@ export class LidoSDKApr { invariant(lastEvent, 'Last event is not defined'); const firstEvent = await this.events.stethEvents.getFirstRebaseEvent({ - daysAgo: days, - goBackFromBlock: lastEvent.blockNumber, + days, + fromBlockNumber: lastEvent.blockNumber, }); invariant(firstEvent, 'First event is not defined'); From 3b2b271ff99a9ec0d69fc8b285b09f2c7d5c0399 Mon Sep 17 00:00:00 2001 From: Andrei Date: Mon, 16 Oct 2023 16:38:49 +0700 Subject: [PATCH 12/16] refactor: fix call methods --- playground/demo/events/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/playground/demo/events/index.tsx b/playground/demo/events/index.tsx index 05000f30..4633e9d4 100644 --- a/playground/demo/events/index.tsx +++ b/playground/demo/events/index.tsx @@ -1,5 +1,4 @@ import { Accordion, Input } from '@lidofinance/lido-ui'; -import { useWeb3 } from '@reef-knot/web3-react'; import { Action } from 'components/action'; import { useLidoSDK } from 'providers/sdk'; import { useState } from 'react'; @@ -18,13 +17,13 @@ export const EventsDemo = () => { title="First Rebase event" action={() => events.stethEvents.getFirstRebaseEvent({ - daysAgo: daysAgoValue + days: daysAgoValue, }) } > Date: Mon, 16 Oct 2023 20:59:47 +0700 Subject: [PATCH 13/16] fix: refactor & bugfixes --- packages/sdk/README.md | 1 + packages/sdk/src/common/constants.ts | 8 + packages/sdk/src/rewards/index.ts | 4 +- packages/sdk/src/rewards/rewards.ts | 197 ++++++++++-------- packages/sdk/src/rewards/subgraph/index.ts | 3 +- .../subgraph/{subrgaph.ts => subgraph.ts} | 17 +- packages/sdk/src/rewards/types.ts | 46 ++-- packages/sdk/src/rewards/utils.ts | 29 +++ playground/demo/rewards/index.tsx | 2 + 9 files changed, 191 insertions(+), 116 deletions(-) rename packages/sdk/src/rewards/subgraph/{subrgaph.ts => subgraph.ts} (92%) create mode 100644 packages/sdk/src/rewards/utils.ts diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 964a5c24..36aa927d 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -1134,6 +1134,7 @@ This module allows you to query historical rewards data for given address via ch - **toBlock** - **default**:`latest` block number or tag for upper bound for rewards. `pending` not allowed - **fromBlock** - block number or tag for lower bound for rewards. `pending` not allowed - **blocksBack** - alternative to **fromBlock**. Amount of blocks to look back to from **toBlock**. +- **step** - **default**:`1000` step per one request for large queries. For chain method max amount of blocks per one request. For subgraph method max amount of entities returned per one requests. - **includeZeroRebases** - **default**:`false` include rebase events when users had no rewards(because of empty balance) ### Common Return diff --git a/packages/sdk/src/common/constants.ts b/packages/sdk/src/common/constants.ts index 923d4301..066d4c7d 100644 --- a/packages/sdk/src/common/constants.ts +++ b/packages/sdk/src/common/constants.ts @@ -32,6 +32,14 @@ export const SUBRGRAPH_ID_BY_CHAIN: { [CHAINS.Holesky]: '', }; +export const EARLIEST_TOKEN_REBASED_EVENT: { + [key in CHAINS]: bigint; +} = { + [CHAINS.Mainnet]: 17272708n, + [CHAINS.Goerli]: 8712039n, + [CHAINS.Holesky]: 52174n, +} as const; + export const LIDO_TOKENS = { steth: 'stETH', wsteth: 'wstETH', diff --git a/packages/sdk/src/rewards/index.ts b/packages/sdk/src/rewards/index.ts index 66101acc..4025ab0d 100644 --- a/packages/sdk/src/rewards/index.ts +++ b/packages/sdk/src/rewards/index.ts @@ -1,10 +1,10 @@ export { LidoSDKRewards } from './rewards.js'; export { type LidoSDKRewardsProps, - type GetRewardsFromChainResults, + type GetRewardsFromChainResult, type GetRewardsFromChainOptions, type GetRewardsFromSubgraphOptions, - type GetRewardsFromSubgraphResults, + type GetRewardsFromSubgraphResult, type RewardsSubgraphEvents, type RewardsChainEvents, type RewardType, diff --git a/packages/sdk/src/rewards/rewards.ts b/packages/sdk/src/rewards/rewards.ts index 1e80291d..42a9888e 100644 --- a/packages/sdk/src/rewards/rewards.ts +++ b/packages/sdk/src/rewards/rewards.ts @@ -1,8 +1,8 @@ import invariant from 'tiny-invariant'; import { - Address, - GetContractReturnType, - PublicClient, + type Address, + type GetContractReturnType, + type PublicClient, getContract, zeroAddress, } from 'viem'; @@ -11,18 +11,21 @@ import { Logger, ErrorHandler, Cache } from '../common/decorators/index.js'; import { version } from '../version.js'; import { rewardsEventsAbi } from './abi/rewardsEvents.js'; import { - GetRewardsFromChainResults as GetRewardsFromChainResult, - GetRewardsFromSubgraphOptions, - GetRewardsFromSubgraphResults as GetRewardsFromSubgraphResult, - GetRewardsOptions, - LidoSDKRewardsProps, - NonPendingBlockTag, - Reward, - RewardsChainEvents, - RewardsSubgraphEvents, + type GetRewardsFromChainResult, + type GetRewardsFromSubgraphOptions, + type GetRewardsFromSubgraphResult, + type GetRewardsOptions, + type LidoSDKRewardsProps, + type NonPendingBlockTag, + type Reward, + type RewardsChainEvents, + type RewardsSubgraphEvents, } from './types.js'; -import { LIDO_CONTRACT_NAMES } from '../common/constants.js'; +import { + EARLIEST_TOKEN_REBASED_EVENT, + LIDO_CONTRACT_NAMES, +} from '../common/constants.js'; import { TotalRewardEntity, TransferEventEntity, @@ -31,27 +34,14 @@ import { getTransfers, } from './subgraph/index.js'; import { addressEqual } from '../common/utils/address-equal.js'; -import { getInitialData } from './subgraph/subrgaph.js'; +import { getInitialData } from './subgraph/index.js'; +import { calcShareRate, requestWithBlockStep, sharesToSteth } from './utils.js'; export class LidoSDKRewards { - readonly core: LidoSDKCore; private static readonly PRECISION = 10n ** 27n; + private static readonly DEFAULT_STEP = 1000; - private static calcShareRate = ( - totalEther: bigint, - totalShares: bigint, - ): number => - Number((totalEther * LidoSDKRewards.PRECISION) / totalShares) / - Number(LidoSDKRewards.PRECISION); - - private static sharesToSteth = ( - shares: bigint, - totalEther: bigint, - totalShares: bigint, - ): bigint => - (shares * totalEther * LidoSDKRewards.PRECISION) / - totalShares / - LidoSDKRewards.PRECISION; + readonly core: LidoSDKCore; constructor(props: LidoSDKRewardsProps) { if (props.core) this.core = props.core; @@ -77,6 +67,13 @@ export class LidoSDKRewards { ); } + @Logger('Contracts:') + @Cache(30 * 60 * 1000, ['core.chain.id']) + private earliestRebaseEventBlock(): bigint { + invariant(this.core.chain, 'Chain is not defined'); + return EARLIEST_TOKEN_REBASED_EVENT[this.core.chainId]; + } + @Logger('Contracts:') @Cache(30 * 60 * 1000, ['core.chain.id', 'contractAddressStETH']) private async getContractStETH(): Promise< @@ -97,7 +94,7 @@ export class LidoSDKRewards { props: GetRewardsOptions, ): Promise { const [ - { address, fromBlock, toBlock, includeZeroRebases = false }, + { address, fromBlock, toBlock, includeZeroRebases, step }, stethContract, withdrawalQueueAddress, ] = await Promise.all([ @@ -106,6 +103,12 @@ export class LidoSDKRewards { this.contractAddressWithdrawalQueue(), ]); + const lowerBound = this.earliestRebaseEventBlock(); + if (fromBlock < lowerBound) + throw new Error( + `Cannot index events earlier than first TokenRebased event at block ${lowerBound.toString()}`, + ); + const preBlock = fromBlock === 0n ? 0n : fromBlock - 1n; const [ @@ -121,24 +124,31 @@ export class LidoSDKRewards { }), stethContract.read.getTotalPooledEther({ blockNumber: preBlock }), stethContract.read.getTotalShares({ blockNumber: preBlock }), - stethContract.getEvents.TransferShares( - { from: address }, - { fromBlock, toBlock }, + requestWithBlockStep(step, fromBlock, toBlock, (fromBlock, toBlock) => + stethContract.getEvents.TransferShares( + { from: address }, + { fromBlock, toBlock }, + ), ), - stethContract.getEvents.TransferShares( - { to: address }, - { fromBlock, toBlock }, + requestWithBlockStep(step, fromBlock, toBlock, (fromBlock, toBlock) => + stethContract.getEvents.TransferShares( + { to: address }, + { fromBlock, toBlock }, + ), ), - stethContract.getEvents.TokenRebased( - {}, - { - fromBlock, - toBlock, - }, + requestWithBlockStep(step, fromBlock, toBlock, (fromBlock, toBlock) => + stethContract.getEvents.TokenRebased( + {}, + { + fromBlock, + toBlock, + }, + ), ), ]); - const events = ([] as any).concat( + // concat types are broken + const events = ([] as any[]).concat( transferInEvents, transferOutEvents, rebaseEvents, @@ -154,30 +164,33 @@ export class LidoSDKRewards { }); // Converts to steth based on current share rate - // it's crucial to not cut corners here else computational error will be accumulated let currentTotalEther = baseTotalEther; let currentTotalShares = baseTotalShares; - const sharesToSteth = (shares: bigint): bigint => - LidoSDKRewards.sharesToSteth( + const getCurrentStethFromShares = (shares: bigint): bigint => + sharesToSteth( shares, currentTotalEther, currentTotalShares, + LidoSDKRewards.PRECISION, + ); + const getCurrentShareRate = () => + calcShareRate( + currentTotalEther, + currentTotalShares, + LidoSDKRewards.PRECISION, ); - const getShareRate = () => - LidoSDKRewards.calcShareRate(currentTotalEther, currentTotalShares); - let baseBalance = sharesToSteth(baseBalanceShares); - let baseShareRate = getShareRate(); + const baseBalance = getCurrentStethFromShares(baseBalanceShares); + const baseShareRate = getCurrentShareRate(); let shareRate = baseShareRate; let prevSharesBalance = baseBalanceShares; - type RewardEntry = Reward; let rewards: Reward[] = events.map((event) => { if (event.eventName === 'TransferShares') { const { from, to, sharesValue } = event.args; - let type: RewardEntry['type'], - changeShares: RewardEntry['changeShares'], - balanceShares: RewardEntry['balanceShares']; + let type: Reward['type'], + changeShares: Reward['changeShares'], + balanceShares: Reward['balanceShares']; if (to === address) { type = from === zeroAddress ? 'submit' : 'transfer_in'; @@ -189,23 +202,24 @@ export class LidoSDKRewards { changeShares = -sharesValue; } + prevSharesBalance = balanceShares; return { type, balanceShares, changeShares, - change: sharesToSteth(changeShares), - balance: sharesToSteth(balanceShares), + change: getCurrentStethFromShares(changeShares), + balance: getCurrentStethFromShares(balanceShares), shareRate, originalEvent: event, }; } if (event.eventName === 'TokenRebased') { const { postTotalEther, postTotalShares } = event.args; - const oldBalance = sharesToSteth(prevSharesBalance); + const oldBalance = getCurrentStethFromShares(prevSharesBalance); currentTotalEther = postTotalEther; currentTotalShares = postTotalShares; - const newBalance = sharesToSteth(prevSharesBalance); - shareRate = getShareRate(); + const newBalance = getCurrentStethFromShares(prevSharesBalance); + shareRate = getCurrentShareRate(); return { type: 'rebase', @@ -242,14 +256,7 @@ export class LidoSDKRewards { props: GetRewardsFromSubgraphOptions, ): Promise { const [ - { - getSubgraphUrl, - address, - fromBlock, - toBlock, - step = 1000, - includeZeroRebases = false, - }, + { getSubgraphUrl, address, fromBlock, toBlock, step, includeZeroRebases }, withdrawalQueueAddress, ] = await Promise.all([ this.parseProps(props), @@ -282,6 +289,7 @@ export class LidoSDKRewards { getInitialData({ url, address, block: preBlock }), ]); + // concat types are broken const events = ([] as (TransferEventEntity | TotalRewardEntity)[]).concat( totalRewards, transfers, @@ -325,13 +333,18 @@ export class LidoSDKRewards { const { totalPooledEtherAfter, totalSharesAfter } = initialRebase; const totalEther = BigInt(totalPooledEtherAfter); const totalShares = BigInt(totalSharesAfter); - baseShareRate = LidoSDKRewards.calcShareRate(totalEther, totalShares); + baseShareRate = calcShareRate( + totalEther, + totalShares, + LidoSDKRewards.PRECISION, + ); // we recount initial balance in case this rebase was after transfer // in opposite case recount will be the same value anyway - prevBalance = LidoSDKRewards.sharesToSteth( + prevBalance = sharesToSteth( prevBalanceShares, totalEther, totalShares, + LidoSDKRewards.PRECISION, ); } @@ -339,8 +352,6 @@ export class LidoSDKRewards { const baseBalance = prevBalance; const baseBalanceShares = prevBalanceShares; - type RewardEntry = Reward; - let rewards: Reward[] = events.map((event) => { // it's a transfer if ('value' in event) { @@ -356,11 +367,11 @@ export class LidoSDKRewards { totalPooledEther, totalShares, } = event; - let type: RewardEntry['type'], - changeShares: RewardEntry['changeShares'], - balanceShares: RewardEntry['balanceShares'], - change: RewardEntry['change'], - balance: RewardEntry['balance']; + let type: Reward['type'], + changeShares: Reward['changeShares'], + balanceShares: Reward['balanceShares'], + change: Reward['change'], + balance: Reward['balance']; if (addressEqual(to, address)) { type = from === zeroAddress ? 'submit' : 'transfer_in'; @@ -378,9 +389,10 @@ export class LidoSDKRewards { balanceShares = BigInt(sharesAfterDecrease); } - const shareRate = LidoSDKRewards.calcShareRate( + const shareRate = calcShareRate( BigInt(totalPooledEther), BigInt(totalShares), + LidoSDKRewards.PRECISION, ); prevBalance = balance; prevBalanceShares = balanceShares; @@ -401,10 +413,11 @@ export class LidoSDKRewards { const totalEther = BigInt(totalPooledEtherAfter); const totalShares = BigInt(totalSharesAfter); - const newBalance = LidoSDKRewards.sharesToSteth( + const newBalance = sharesToSteth( prevBalanceShares, totalEther, totalShares, + LidoSDKRewards.PRECISION, ); const change = newBalance - prevBalance; prevBalance = newBalance; @@ -414,7 +427,11 @@ export class LidoSDKRewards { changeShares: 0n, balance: newBalance, balanceShares: prevBalanceShares, - shareRate: LidoSDKRewards.calcShareRate(totalEther, totalShares), + shareRate: calcShareRate( + totalEther, + totalShares, + LidoSDKRewards.PRECISION, + ), originalEvent: event, }; } @@ -441,9 +458,14 @@ export class LidoSDKRewards { private async parseProps( props: TRewardsProps, ): Promise< - Omit & { + Omit< + TRewardsProps, + 'toBlock' | 'fromBlock' | 'includeZeroRebases' | 'step' + > & { toBlock: bigint; fromBlock: bigint; + step: number; + includeZeroRebases: boolean; } > { const toBlock = await this.toBlockNumber(props.toBlock ?? 'latest'); @@ -453,10 +475,19 @@ export class LidoSDKRewards { const fromBlock = await this.toBlockNumber( props.fromBlock ?? toBlock - props.blocksBack, ); - invariant(toBlock > fromBlock, 'toBlock is higher than fromBlock'); - return { ...props, fromBlock, toBlock }; + const { step = LidoSDKRewards.DEFAULT_STEP, includeZeroRebases = false } = + props; + invariant(step > 0, 'steps must be a positive integer'); + + return { + ...props, + fromBlock, + step, + includeZeroRebases, + toBlock, + }; } @Logger('Utils:') diff --git a/packages/sdk/src/rewards/subgraph/index.ts b/packages/sdk/src/rewards/subgraph/index.ts index 06054122..fb206189 100644 --- a/packages/sdk/src/rewards/subgraph/index.ts +++ b/packages/sdk/src/rewards/subgraph/index.ts @@ -2,7 +2,8 @@ export { getLastIndexedBlock, getTotalRewards, getTransfers, -} from './subrgaph.js'; + getInitialData, +} from './subgraph.js'; export { type GetLastIndexedBlockOptions, type GetLastIndexedBlockResult, diff --git a/packages/sdk/src/rewards/subgraph/subrgaph.ts b/packages/sdk/src/rewards/subgraph/subgraph.ts similarity index 92% rename from packages/sdk/src/rewards/subgraph/subrgaph.ts rename to packages/sdk/src/rewards/subgraph/subgraph.ts index 33bc5ffe..f43d8f3c 100644 --- a/packages/sdk/src/rewards/subgraph/subrgaph.ts +++ b/packages/sdk/src/rewards/subgraph/subgraph.ts @@ -16,12 +16,12 @@ import { type TotalRewardEntity, type GetTotalRewardsResult, type GetTransfersResult, - LidoTransfersQueryResult, - TotalRewardsQueryResult, - GetInitialDataOptions, - GetInitialDataResult, - InitialDataQueryVariables, - InitialDataQueryResult, + type LidoTransfersQueryResult, + type TotalRewardsQueryResult, + type GetInitialDataOptions, + type GetInitialDataResult, + type InitialDataQueryVariables, + type InitialDataQueryResult, } from './types.js'; const requestAllWithStep = async ({ @@ -39,7 +39,7 @@ const requestAllWithStep = async ({ } & SubgraphRequestOptions): Promise> => { let skip = 0; const results: TResultEntry[] = []; - do { + while (true) { const partialResult = await request({ url, document, @@ -56,8 +56,7 @@ const requestAllWithStep = async ({ // break if we don't fetch more than step if (array.length < step) break; skip += step; - // maybe some sort of max iterations mechanism - } while (true); + } return results; }; diff --git a/packages/sdk/src/rewards/types.ts b/packages/sdk/src/rewards/types.ts index d13b00df..3625e7ec 100644 --- a/packages/sdk/src/rewards/types.ts +++ b/packages/sdk/src/rewards/types.ts @@ -11,6 +11,7 @@ export type GetRewardsOptions = { address: Address; includeZeroRebases?: boolean; toBlock?: bigint | NonPendingBlockTag; + step?: number; } & ( | { fromBlock: bigint | NonPendingBlockTag; @@ -21,25 +22,29 @@ export type GetRewardsOptions = { } ); +export type RewardChainEventTransfer = Log< + bigint, + number, + false, + undefined, + true, + typeof rewardsEventsAbi, + 'TransferShares' +>; + +export type RewardChainEventRebase = Log< + bigint, + number, + false, + undefined, + true, + typeof rewardsEventsAbi, + 'TokenRebased' +>; + export type RewardsChainEvents = - | Log< - bigint, - number, - false, - undefined, - true, - typeof rewardsEventsAbi, - 'TransferShares' - > - | Log< - bigint, - number, - false, - undefined, - true, - typeof rewardsEventsAbi, - 'TokenRebased' - >; + | RewardChainEventTransfer + | RewardChainEventRebase; export type RewardsSubgraphEvents = TransferEventEntity | TotalRewardEntity; @@ -70,16 +75,15 @@ type GetRewardsCommonResult = { export type GetRewardsFromSubgraphOptions = GetRewardsOptions & { getSubgraphUrl: (id: string, chainId: number) => string; - step?: number; }; -export type GetRewardsFromSubgraphResults = { +export type GetRewardsFromSubgraphResult = { rewards: Reward[]; lastIndexedBlock: bigint; } & GetRewardsCommonResult; export type GetRewardsFromChainOptions = GetRewardsOptions; -export type GetRewardsFromChainResults = { +export type GetRewardsFromChainResult = { rewards: Reward[]; } & GetRewardsCommonResult; diff --git a/packages/sdk/src/rewards/utils.ts b/packages/sdk/src/rewards/utils.ts new file mode 100644 index 00000000..7731fba1 --- /dev/null +++ b/packages/sdk/src/rewards/utils.ts @@ -0,0 +1,29 @@ +export const calcShareRate = ( + totalEther: bigint, + totalShares: bigint, + precision: bigint, +): number => Number((totalEther * precision) / totalShares) / Number(precision); + +export const sharesToSteth = ( + shares: bigint, + totalEther: bigint, + totalShares: bigint, + precision: bigint, +): bigint => (shares * totalEther * precision) / (totalShares * precision); + +export const requestWithBlockStep = async ( + step: number, + fromBlock: bigint, + toBlock: bigint, + request: (fromBlock: bigint, toBlock: bigint) => Promise, +): Promise => { + let from = fromBlock; + const result: TResultEntry[] = []; + while (from < toBlock) { + const to = from + BigInt(step); + const nextResult = await request(from, to > toBlock ? toBlock : to); + result.push(...nextResult); + from = to + 1n; + } + return result; +}; diff --git a/playground/demo/rewards/index.tsx b/playground/demo/rewards/index.tsx index 05eb63ae..3a692ccd 100644 --- a/playground/demo/rewards/index.tsx +++ b/playground/demo/rewards/index.tsx @@ -19,6 +19,7 @@ import { ToggleButton } from 'components/toggle-button/toggle-button'; import { useAddressState } from 'hooks/useAddressState'; import { useLidoSDK } from 'providers/sdk'; import { useState } from 'react'; + import { Address } from 'viem'; const renderRewards = ( @@ -103,6 +104,7 @@ export const RewardsDemo = () => { return rewards.getRewardsFromChain({ address: rewardsAddress, blocksBack: BigInt(blocksBack), + step: 25000, includeZeroRebases, }); }} From a30a736de22020e9c99ce363579aac576816595b Mon Sep 17 00:00:00 2001 From: Evgeny Taktarov Date: Mon, 16 Oct 2023 21:03:51 +0700 Subject: [PATCH 14/16] fix: demo build --- playground/demo/rewards/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playground/demo/rewards/index.tsx b/playground/demo/rewards/index.tsx index 3a692ccd..a2149799 100644 --- a/playground/demo/rewards/index.tsx +++ b/playground/demo/rewards/index.tsx @@ -1,6 +1,6 @@ import { - GetRewardsFromChainResults, - GetRewardsFromSubgraphResults, + GetRewardsFromChainResult, + GetRewardsFromSubgraphResult, } from '@lidofinance/lido-ethereum-sdk/dist/types/rewards/types'; import { Input, @@ -23,7 +23,7 @@ import { useState } from 'react'; import { Address } from 'viem'; const renderRewards = ( - result: GetRewardsFromChainResults | GetRewardsFromSubgraphResults, + result: GetRewardsFromChainResult | GetRewardsFromSubgraphResult, ) => { const steth = renderTokenResult('stETH'); const shares = renderTokenResult('shares'); From 9c9fb4e9d1364599a5c504e3d2bb35013c9d3e76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 05:44:46 +0000 Subject: [PATCH 15/16] build(deps): bump @babel/traverse from 7.22.11 to 7.23.2 Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.11 to 7.23.2. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 106 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 79a28de9..15279cb7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -39,6 +39,16 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.22.13": + version: 7.22.13 + resolution: "@babel/code-frame@npm:7.22.13" + dependencies: + "@babel/highlight": ^7.22.13 + chalk: ^2.4.2 + checksum: 22e342c8077c8b77eeb11f554ecca2ba14153f707b85294fcf6070b6f6150aae88a7b7436dd88d8c9289970585f3fe5b9b941c5aa3aa26a6d5a8ef3f292da058 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9": version: 7.22.9 resolution: "@babel/compat-data@npm:7.22.9" @@ -81,6 +91,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/generator@npm:7.23.0" + dependencies: + "@babel/types": ^7.23.0 + "@jridgewell/gen-mapping": ^0.3.2 + "@jridgewell/trace-mapping": ^0.3.17 + jsesc: ^2.5.1 + checksum: 8efe24adad34300f1f8ea2add420b28171a646edc70f2a1b3e1683842f23b8b7ffa7e35ef0119294e1901f45bfea5b3dc70abe1f10a1917ccdfb41bed69be5f1 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-annotate-as-pure@npm:7.22.5" @@ -159,6 +181,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-environment-visitor@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-environment-visitor@npm:7.22.20" + checksum: d80ee98ff66f41e233f36ca1921774c37e88a803b2f7dca3db7c057a5fea0473804db9fb6729e5dbfd07f4bed722d60f7852035c2c739382e84c335661590b69 + languageName: node + linkType: hard + "@babel/helper-environment-visitor@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-environment-visitor@npm:7.22.5" @@ -176,6 +205,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-function-name@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/helper-function-name@npm:7.23.0" + dependencies: + "@babel/template": ^7.22.15 + "@babel/types": ^7.23.0 + checksum: e44542257b2d4634a1f979244eb2a4ad8e6d75eb6761b4cfceb56b562f7db150d134bc538c8e6adca3783e3bc31be949071527aa8e3aab7867d1ad2d84a26e10 + languageName: node + linkType: hard + "@babel/helper-hoist-variables@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-hoist-variables@npm:7.22.5" @@ -301,6 +340,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-validator-identifier@npm:7.22.20" + checksum: 136412784d9428266bcdd4d91c32bcf9ff0e8d25534a9d94b044f77fe76bc50f941a90319b05aafd1ec04f7d127cd57a179a3716009ff7f3412ef835ada95bdc + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-validator-option@npm:7.22.5" @@ -341,6 +387,17 @@ __metadata: languageName: node linkType: hard +"@babel/highlight@npm:^7.22.13": + version: 7.22.20 + resolution: "@babel/highlight@npm:7.22.20" + dependencies: + "@babel/helper-validator-identifier": ^7.22.20 + chalk: ^2.4.2 + js-tokens: ^4.0.0 + checksum: 84bd034dca309a5e680083cd827a766780ca63cef37308404f17653d32366ea76262bd2364b2d38776232f2d01b649f26721417d507e8b4b6da3e4e739f6d134 + languageName: node + linkType: hard + "@babel/parser@npm:^7.22.11, @babel/parser@npm:^7.22.5": version: 7.22.11 resolution: "@babel/parser@npm:7.22.11" @@ -350,6 +407,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/parser@npm:7.23.0" + bin: + parser: ./bin/babel-parser.js + checksum: 453fdf8b9e2c2b7d7b02139e0ce003d1af21947bbc03eb350fb248ee335c9b85e4ab41697ddbdd97079698de825a265e45a0846bb2ed47a2c7c1df833f42a354 + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.22.5" @@ -1408,6 +1474,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/template@npm:7.22.15" + dependencies: + "@babel/code-frame": ^7.22.13 + "@babel/parser": ^7.22.15 + "@babel/types": ^7.22.15 + checksum: 1f3e7dcd6c44f5904c184b3f7fe280394b191f2fed819919ffa1e529c259d5b197da8981b6ca491c235aee8dbad4a50b7e31304aa531271cb823a4a24a0dd8fd + languageName: node + linkType: hard + "@babel/template@npm:^7.22.5": version: 7.22.5 resolution: "@babel/template@npm:7.22.5" @@ -1420,20 +1497,20 @@ __metadata: linkType: hard "@babel/traverse@npm:^7.22.11, @babel/traverse@npm:^7.4.5": - version: 7.22.11 - resolution: "@babel/traverse@npm:7.22.11" + version: 7.23.2 + resolution: "@babel/traverse@npm:7.23.2" dependencies: - "@babel/code-frame": ^7.22.10 - "@babel/generator": ^7.22.10 - "@babel/helper-environment-visitor": ^7.22.5 - "@babel/helper-function-name": ^7.22.5 + "@babel/code-frame": ^7.22.13 + "@babel/generator": ^7.23.0 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-function-name": ^7.23.0 "@babel/helper-hoist-variables": ^7.22.5 "@babel/helper-split-export-declaration": ^7.22.6 - "@babel/parser": ^7.22.11 - "@babel/types": ^7.22.11 + "@babel/parser": ^7.23.0 + "@babel/types": ^7.23.0 debug: ^4.1.0 globals: ^11.1.0 - checksum: 4ad62d548ca8b95dbf45bae16cc167428f174f3c837d55a5878b1f17bdbc8b384d6df741dc7c461b62c04d881cf25644d3ab885909ba46e3ac43224e2b15b504 + checksum: 26a1eea0dde41ab99dde8b9773a013a0dc50324e5110a049f5d634e721ff08afffd54940b3974a20308d7952085ac769689369e9127dea655f868c0f6e1ab35d languageName: node linkType: hard @@ -1448,6 +1525,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.22.15, @babel/types@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/types@npm:7.23.0" + dependencies: + "@babel/helper-string-parser": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.20 + to-fast-properties: ^2.0.0 + checksum: 215fe04bd7feef79eeb4d33374b39909ce9cad1611c4135a4f7fdf41fe3280594105af6d7094354751514625ea92d0875aba355f53e86a92600f290e77b0e604 + languageName: node + linkType: hard + "@coinbase/wallet-sdk@npm:^3.0.4, @coinbase/wallet-sdk@npm:^3.6.6": version: 3.7.1 resolution: "@coinbase/wallet-sdk@npm:3.7.1" From a06c8d5f6c483af1276b6651f10e44b3e020e758 Mon Sep 17 00:00:00 2001 From: Evgeny Taktarov Date: Thu, 19 Oct 2023 17:49:14 +0700 Subject: [PATCH 16/16] fix: styling and readme --- packages/sdk/README.md | 10 +++++----- playground/demo/rewards/index.tsx | 6 ++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 36aa927d..b5130aa0 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -1131,11 +1131,11 @@ This module allows you to query historical rewards data for given address via ch ### Common Options - **address** - address of an account you want to query rewards for -- **toBlock** - **default**:`latest` block number or tag for upper bound for rewards. `pending` not allowed +- **toBlock** [default: `latest` ] - block number or tag for upper bound for rewards. `pending` not allowed - **fromBlock** - block number or tag for lower bound for rewards. `pending` not allowed -- **blocksBack** - alternative to **fromBlock**. Amount of blocks to look back to from **toBlock**. -- **step** - **default**:`1000` step per one request for large queries. For chain method max amount of blocks per one request. For subgraph method max amount of entities returned per one requests. -- **includeZeroRebases** - **default**:`false` include rebase events when users had no rewards(because of empty balance) +- **blocksBack** - alternative to **fromBlock**. Amount of blocks to look back from **toBlock**. +- **step** [default: `1000` ] - step per one request for large queries. For chain method max amount of blocks per one request. For subgraph method max amount of entities returned per one requests. +- **includeZeroRebases** [default: `false` ] - include rebase events when users had no rewards(because of empty balance) ### Common Return @@ -1185,7 +1185,7 @@ This method requires you to provide API URL to send subgraph requests to. It's b #### Important notes -**toBlock** is capped by last index block in subgraph. Block number is available in result object by `lastIndexedBlock`. +**toBlock** is capped by last indexed block in subgraph. Block number is available in result object by `lastIndexedBlock`. ```ts const lidoSDK = new LidoSDK({ diff --git a/playground/demo/rewards/index.tsx b/playground/demo/rewards/index.tsx index a2149799..2d898e00 100644 --- a/playground/demo/rewards/index.tsx +++ b/playground/demo/rewards/index.tsx @@ -113,7 +113,9 @@ export const RewardsDemo = () => { label="Rewards address" placeholder="0x0000000" value={rewardsAddress} - onChange={(e) => setRewardsAddress(e.currentTarget.value as Address)} + onChange={(event) => + setRewardsAddress(event.currentTarget.value as Address) + } /> { min="1" type="number" value={blocksBack} - onChange={(e) => setBlocksBack(e.currentTarget.valueAsNumber)} + onChange={(event) => setBlocksBack(event.currentTarget.valueAsNumber)} />