diff --git a/README.md b/README.md index fdcfcf0d..b0121501 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ console.log(balanceETH.toString(), 'ETH balance'); ## Migration -For breaking changes between versions see [MIGRATION.md](MIGRATION.md) +For breaking changes between versions see [MIGRATION.md](packages/sdk/MIGRATION.md) ## Documentation diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 393e46d3..9ac09254 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -5,10 +5,13 @@ ### Added - `Sepolia` testnet +- New method `getWithdrawalWaitingTimeByAmount` for fetching withdrawal waiting time for amount of eth +- New method `getWithdrawalWaitingTimeByRequestIds` for fetching withdrawal waiting time for earlier created requests by their ids ## Playground - Support for `Sepolia` testnet +- Added blocks with new methods `getWithdrawalWaitingTimeByAmount` and `getWithdrawalWaitingTimeByRequestIds` # 3.1.0 diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 6de68160..b55a565d 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -66,6 +66,9 @@ For breaking changes between versions see [MIGRATION.md](MIGRATION.md) - [Views](#views) - [Constants](#constants) - [Requests info](#requests-info) + - [Waiting time](#waiting-time) + - [Get time by amount](#get-time-by-amount) + - [Get time by request ids](#get-time-by-request-ids) - [(w)stETH](#wsteth) - [unstETH NFT](#unsteth-nft) - [Shares](#shares) @@ -654,7 +657,7 @@ try { console.log( 'transaction hash, transaction receipt, confirmations', requestResult, - 'array of requests(nfts) created with ids, amounts,creator, owner' + 'array of requests(nfts) created with ids, amounts,creator, owner', request.results.requests, ); } catch (error) { @@ -1010,6 +1013,53 @@ try { - `pendingRequests` (Type: Array[RequestStatusWithId]): A list of requests pending finalization. - `pendingAmountStETH` (Type: bigint): The amount of ETH pending claiming. +### Waiting time + +#### Methods + +##### Get time by amount + +###### `getWithdrawalWaitingTimeByAmount` + +###### Input Parameters: + +- `props: { amount?: bigint }` + - `amount?` (Type: bigint **optional**): The amount of withdrawable eth. In case when it is not passed, it is calculated as default information about queue. + +##### Output Parameters: + +- Type: Object +- Structure: + - `requestInfo` (Type: Object): Information about withdrawal request + - `finalizationIn` (Type: number): The time needed for withdrawal in milliseconds. + - `finalizationAt` (Type: string): The time when request finalized for withdrawal. + - `type` (Type: WaitingTimeCalculationType): Type of final source of eth for withdrawal. + - `status` (Type: WaitingTimeStatus): Status of withdrawal request. + - `nextCalculationAt` (Type: string): Time when next calculation can be changed. + +##### Get time by request ids + +###### `getWithdrawalWaitingTimeByRequestIds` + +###### Input Parameters: + +- `props: { ids: bigint[] }` + - `ids` (ids: Array[bigint]): The ids of withdrawal requests. + +##### Output Parameters: + +- Type: Array of WithdrawalWaitingTimeRequestInfo objects +- Structure of each object: + - `requestInfo` (Type: RequestByIdInfoDto): Information about withdrawal request. + - `finalizationIn` (Type: number): The time needed for withdrawal in milliseconds. + - `finalizationAt` (Type: string): The time when request finalized for withdrawal. + - `requestId` (Type: string): The request id. + - `requestedAt` (Type: string): The time when withdrawal requested. + - `type` (Type: WaitingTimeCalculationType): Type of final source of eth for withdrawal. + - `status` (Type: WaitingTimeStatus): Status of withdrawal request. + - `nextCalculationAt` (Type: string): Time when next calculation can be changed. + + ## (w)stETH stETH and wstETH tokens functionality is presented trough modules with same ERC20 interface that exposes balances, allowances, transfers and ERC2612 permits signing. diff --git a/packages/sdk/src/common/constants.ts b/packages/sdk/src/common/constants.ts index 3709bb53..b81e29a9 100644 --- a/packages/sdk/src/common/constants.ts +++ b/packages/sdk/src/common/constants.ts @@ -111,3 +111,10 @@ export const VIEM_CHAINS: { [key in CHAINS]: Chain } = { [CHAINS.Holesky]: holesky, [CHAINS.Sepolia]: sepolia, }; + +export const WQ_API_URLS: { [key in CHAINS]: string | null } = { + [CHAINS.Mainnet]: 'https://wq-api.lido.fi', + [CHAINS.Goerli]: 'https://wq-api.testnet.fi', + [CHAINS.Holesky]: 'https://wq-api-holesky.testnet.fi', + [CHAINS.Sepolia]: null, +}; diff --git a/packages/sdk/src/common/decorators/constants.ts b/packages/sdk/src/common/decorators/constants.ts index b872cc72..59a7047c 100644 --- a/packages/sdk/src/common/decorators/constants.ts +++ b/packages/sdk/src/common/decorators/constants.ts @@ -15,6 +15,7 @@ export const ConsoleCss: Record<HeadMessage, string> = { 'Events:': 'color: salmon', 'Statistic:': 'color: purple', 'Rewards:': 'color: greenyellow', + 'API:': 'color: mediumpurple', '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 fa7ab1ee..d8a31330 100644 --- a/packages/sdk/src/common/decorators/types.ts +++ b/packages/sdk/src/common/decorators/types.ts @@ -13,4 +13,5 @@ export type HeadMessage = | 'Events:' | 'Statistic:' | 'Rewards:' - | 'Deprecation:'; + | 'Deprecation:' + | 'API:'; diff --git a/packages/sdk/src/withdraw/__test__/withdraw-waiting-time.test.ts b/packages/sdk/src/withdraw/__test__/withdraw-waiting-time.test.ts new file mode 100644 index 00000000..edbf847c --- /dev/null +++ b/packages/sdk/src/withdraw/__test__/withdraw-waiting-time.test.ts @@ -0,0 +1,29 @@ +import { expect, describe, test } from '@jest/globals'; +import { useWithdraw } from '../../../tests/utils/fixtures/use-withdraw.js'; +import { WithdrawalWaitingTimeByRequestIdsParams } from '../types.js'; + +describe('withdraw waiting time', () => { + const withdraw = useWithdraw(); + const { waitingTime } = withdraw; + + test('can get withdrawal waiting time by amount', async () => { + const amount = 110n; + const requestInfos = await waitingTime.getWithdrawalWaitingTimeByAmount({ + amount, + }); + expect(typeof requestInfos.status).toEqual('string'); + }); + + test('can get withdrawal waiting time by request ids', async () => { + const requestsIds = [1234n, 1235n]; + + const requestInfos = await waitingTime.getWithdrawalWaitingTimeByRequestIds( + { + ids: requestsIds, + } as WithdrawalWaitingTimeByRequestIdsParams, + ); + + expect(Array.isArray(requestInfos)).toEqual(true); + expect(requestInfos.length).toEqual(requestsIds.length); + }); +}); diff --git a/packages/sdk/src/withdraw/bus.ts b/packages/sdk/src/withdraw/bus.ts index 960a44ca..03c91665 100644 --- a/packages/sdk/src/withdraw/bus.ts +++ b/packages/sdk/src/withdraw/bus.ts @@ -7,6 +7,7 @@ import { LidoSDKWithdrawApprove, } from './request/index.js'; import { LidoSDKModule } from '../common/class-primitives/sdk-module.js'; +import { LidoSDKWithdrawWaitingTime } from './withdraw-waiting-time.js'; export class Bus extends LidoSDKModule { private version: string | undefined; @@ -17,6 +18,7 @@ export class Bus extends LidoSDKModule { private approvalInstance: LidoSDKWithdrawApprove | undefined; private claimInstance: LidoSDKWithdrawClaim | undefined; private requestInstance: LidoSDKWithdrawRequest | undefined; + private waitingTimeInstance: LidoSDKWithdrawWaitingTime | undefined; // Contract @@ -89,4 +91,16 @@ export class Bus extends LidoSDKModule { } return this.requestInstance; } + + // Waiting Time + + get waitingTime(): LidoSDKWithdrawWaitingTime { + if (!this.waitingTimeInstance) { + this.waitingTimeInstance = new LidoSDKWithdrawWaitingTime({ + bus: this, + version: this.version, + }); + } + return this.waitingTimeInstance; + } } diff --git a/packages/sdk/src/withdraw/types.ts b/packages/sdk/src/withdraw/types.ts index 3da0479d..9b6dea29 100644 --- a/packages/sdk/src/withdraw/types.ts +++ b/packages/sdk/src/withdraw/types.ts @@ -1,6 +1,7 @@ import type { Address } from 'viem'; import type { Bus } from './bus.js'; import type { AccountValue } from '../index.js'; +import { CHAINS } from '../index.js'; export type LidoSDKWithdrawModuleProps = { bus: Bus; version?: string }; @@ -51,3 +52,62 @@ export type GetWithdrawalRequestsInfoReturnType = { pendingInfo: GetPendingRequestsInfoReturnType; claimableETH: GetClaimableRequestsETHByAccountReturnType; }; + +export type WqApiCustomUrlGetter = ( + defaultUrl: string | null, + chainId: CHAINS, +) => string; + +export type WithdrawalWaitingTimeByAmountParams = { + amount?: bigint; + getCustomApiUrl?: WqApiCustomUrlGetter; +}; + +export type RequestInfo = { + finalizationIn: number; + finalizationAt: string; + type: WaitingTimeCalculationType; +}; + +export type WithdrawalWaitingTimeByAmountResponse = { + requestInfo: RequestInfo; + status: WaitingTimeStatus; + nextCalculationAt: string; +}; + +export type WithdrawalWaitingTimeByRequestIdsParams = { + ids: readonly bigint[]; + requestDelay?: number; + getCustomApiUrl?: WqApiCustomUrlGetter; +}; + +export type RequestByIdInfo = { + finalizationIn: number; + finalizationAt: string; + requestId?: string; + requestedAt?: string; + type: WaitingTimeCalculationType; +}; + +export type WithdrawalWaitingTimeRequestInfo = { + requestInfo: RequestByIdInfo; + status: WaitingTimeStatus; + nextCalculationAt: string; +}; + +export enum WaitingTimeStatus { + initializing = 'initializing', + calculating = 'calculating', + finalized = 'finalized', + calculated = 'calculated', +} + +export enum WaitingTimeCalculationType { + buffer = 'buffer', + bunker = 'bunker', + vaultsBalance = 'vaultsBalance', + rewardsOnly = 'rewardsOnly', + validatorBalances = 'validatorBalances', + requestTimestampMargin = 'requestTimestampMargin', + exitValidators = 'exitValidators', +} diff --git a/packages/sdk/src/withdraw/withdraw-waiting-time.ts b/packages/sdk/src/withdraw/withdraw-waiting-time.ts new file mode 100644 index 00000000..4727674f --- /dev/null +++ b/packages/sdk/src/withdraw/withdraw-waiting-time.ts @@ -0,0 +1,112 @@ +import { Logger, ErrorHandler } from '../common/decorators/index.js'; + +import { BusModule } from './bus-module.js'; +import type { + WithdrawalWaitingTimeByAmountResponse, + WithdrawalWaitingTimeRequestInfo, + WithdrawalWaitingTimeByAmountParams, + WithdrawalWaitingTimeByRequestIdsParams, + WqApiCustomUrlGetter, +} from './types.js'; +import { ERROR_CODE, WQ_API_URLS } from '../common/index.js'; +import { formatEther } from 'viem'; + +const endpoints = { + calculateByAmount: '/v2/request-time/calculate', + calculateByRequestId: '/v2/request-time', +}; + +export class LidoSDKWithdrawWaitingTime extends BusModule { + // API call integrations + @Logger('API:') + @ErrorHandler() + public async getWithdrawalWaitingTimeByAmount( + props: WithdrawalWaitingTimeByAmountParams, + ): Promise<WithdrawalWaitingTimeByAmountResponse> { + const getCustomApiUrl = props?.getCustomApiUrl; + + const query = new URLSearchParams(); + if (props.amount) { + query.set('amount', formatEther(props.amount)); + } + + const baseUrl = this.getBaseUrl(getCustomApiUrl); + const url = `${baseUrl}${endpoints.calculateByAmount}?${query.toString()}`; + + const response = await fetch(url, { + headers: { + 'WQ-Request-Source': 'sdk', + }, + }); + + return response.json(); + } + + @Logger('API:') + @ErrorHandler() + public async getWithdrawalWaitingTimeByRequestIds( + props: WithdrawalWaitingTimeByRequestIdsParams, + ): Promise<readonly WithdrawalWaitingTimeRequestInfo[]> { + const requestDelay = props?.requestDelay ?? 1000; + const getCustomApiUrl = props?.getCustomApiUrl; + + if (!Array.isArray(props.ids) || props.ids.length === 0) { + throw this.bus.core.error({ + code: ERROR_CODE.INVALID_ARGUMENT, + message: 'expected not empty array ids', + }); + } + + const idsPages = []; + const pageSize = 20; + const baseUrl = this.getBaseUrl(getCustomApiUrl); + const path = `${baseUrl}${endpoints.calculateByRequestId}`; + + for (let i = 0; i < props.ids.length; i += pageSize) { + idsPages.push(props.ids.slice(i, i + pageSize)); + } + + const result = []; + + for (const page of idsPages) { + const query = new URLSearchParams(); + query.set('ids', page.toString()); + + const url = `${path}?${query.toString()}`; + + const response = await fetch(url, { + headers: { + 'WQ-Request-Source': 'sdk', + }, + }); + + const requests = await response.json(); + result.push(...requests); + + if (idsPages.length > 1) { + // avoid backend spam + await new Promise((resolve) => setTimeout(resolve, requestDelay)); + } + } + + return result; + } + + getBaseUrl(getCustomApiUrl?: WqApiCustomUrlGetter) { + const defaultUrl = WQ_API_URLS[this.bus.core.chainId]; + + const baseUrl = + getCustomApiUrl && typeof getCustomApiUrl === 'function' + ? getCustomApiUrl(defaultUrl, this.bus.core.chainId) + : defaultUrl; + + if (!baseUrl) { + throw this.bus.core.error({ + code: ERROR_CODE.INVALID_ARGUMENT, + message: `wq-api URL is not found for chain ${this.bus.core.chainId}, use getCustomApiUrl prop to setup custom URL`, + }); + } + + return baseUrl; + } +} diff --git a/playground/demo/withdrawals/request.tsx b/playground/demo/withdrawals/request.tsx index 8e42617f..9445f97c 100644 --- a/playground/demo/withdrawals/request.tsx +++ b/playground/demo/withdrawals/request.tsx @@ -180,6 +180,29 @@ export const WithdrawalsRequestDemo = () => { }) } /> + + <Action + title={`Withdrawal Waiting Time By Amount`} + walletAction + action={() => + withdraw.waitingTime.getWithdrawalWaitingTimeByAmount({ + amount, + }) + } + /> + + <Action + title={`Withdrawal Waiting Time For Account Requests`} + walletAction + action={async () => { + const ids = await withdraw.views.getWithdrawalRequestsIds({ + account, + }); + return withdraw.waitingTime.getWithdrawalWaitingTimeByRequestIds({ + ids, + }); + }} + /> </Accordion> ); };