diff --git a/.github/workflows/publish-alpha.yml b/.github/workflows/publish-alpha.yml index d0b1925a..b90d76b3 100644 --- a/.github/workflows/publish-alpha.yml +++ b/.github/workflows/publish-alpha.yml @@ -35,6 +35,14 @@ jobs: - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies run: yarn npm audit + - name: Authenticate in npm + run: | + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc + echo "workspaces-update=false" >> .npmrc + echo "provenance=true" >> .npmrc + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Publish Alpha run: yarn multi-semantic-release --deps.bump=override --deps.release=patch --sequential-init env: 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 7dce448e..9ac09254 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,3 +1,18 @@ +# 3.2.0 + +## SDK + +### 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 ## SDK 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/package.json b/packages/sdk/package.json index 3b294563..35c5ff9c 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -120,7 +120,7 @@ "homepage": "https://github.com/lidofinance/lido-ethereum-sdk", "repository": { "type": "git", - "url": "https://github.com/lidofinance/lido-ethereum-sdk.git", + "url": "git+https://github.com/lidofinance/lido-ethereum-sdk.git", "directory": "packages/sdk" }, "bugs": { diff --git a/packages/sdk/src/common/constants.ts b/packages/sdk/src/common/constants.ts index e3f76e42..b81e29a9 100644 --- a/packages/sdk/src/common/constants.ts +++ b/packages/sdk/src/common/constants.ts @@ -1,10 +1,11 @@ import { type Address, type Chain } from 'viem'; -import { goerli, mainnet, holesky } from 'viem/chains'; +import { goerli, mainnet, holesky, sepolia } from 'viem/chains'; export enum CHAINS { Goerli = 5, Mainnet = 1, Holesky = 17000, + Sepolia = 11155111, } export const APPROX_BLOCKS_BY_DAY = 7600n; @@ -13,6 +14,7 @@ export const SUPPORTED_CHAINS: CHAINS[] = [ CHAINS.Goerli, CHAINS.Mainnet, CHAINS.Holesky, + CHAINS.Sepolia, ]; export const SUBMIT_EXTRA_GAS_TRANSACTION_RATIO = 1.05; @@ -25,6 +27,7 @@ export const LIDO_LOCATOR_BY_CHAIN: { [CHAINS.Mainnet]: '0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb', [CHAINS.Goerli]: '0x1eDf09b5023DC86737b59dE68a8130De878984f5', [CHAINS.Holesky]: '0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8', + [CHAINS.Sepolia]: '0x8f6254332f69557A72b0DA2D5F0Bc07d4CA991E7', }; export const SUBRGRAPH_ID_BY_CHAIN: { @@ -33,6 +36,7 @@ export const SUBRGRAPH_ID_BY_CHAIN: { [CHAINS.Mainnet]: 'Sxx812XgeKyzQPaBpR5YZWmGV5fZuBaPdh7DFhzSwiQ', [CHAINS.Goerli]: 'QmeDfGTuNbSoZ71zi3Ch4WNRbzALfiFPnJMYUFPinLiFNa', [CHAINS.Holesky]: null, + [CHAINS.Sepolia]: null, }; export const EARLIEST_TOKEN_REBASED_EVENT: { @@ -41,6 +45,7 @@ export const EARLIEST_TOKEN_REBASED_EVENT: { [CHAINS.Mainnet]: 17272708n, [CHAINS.Goerli]: 8712039n, [CHAINS.Holesky]: 52174n, + [CHAINS.Sepolia]: 5434668n, } as const; export const LIDO_TOKENS = { @@ -104,4 +109,12 @@ export const VIEM_CHAINS: { [key in CHAINS]: Chain } = { [CHAINS.Mainnet]: mainnet, [CHAINS.Goerli]: goerli, [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 = { '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 { + 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 { + 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/components/layout/header/headerWallet.tsx b/playground/components/layout/header/headerWallet.tsx index d40bc607..f4527ea1 100644 --- a/playground/components/layout/header/headerWallet.tsx +++ b/playground/components/layout/header/headerWallet.tsx @@ -11,12 +11,17 @@ import { HeaderWalletChainStyle } from './headerWalletStyles'; const HeaderWallet: FC = () => { const { active, chainId } = useWeb3(); - const chainName = chainId && CHAINS[chainId]; + // TODO: update @lido-sdk/constants + const chainName = chainId && { ...CHAINS, 11155111: 'Sepolia' }[chainId]; + const isSepolia = chainId === 11155111; return ( <> {chainId && ( - + getChainColor + $color={isSepolia ? '#FFD700' : getChainColor(chainId)} + > {chainName} )} 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 = () => { }) } /> + + + withdraw.waitingTime.getWithdrawalWaitingTimeByAmount({ + amount, + }) + } + /> + + { + const ids = await withdraw.views.getWithdrawalRequestsIds({ + account, + }); + return withdraw.waitingTime.getWithdrawalWaitingTimeByRequestIds({ + ids, + }); + }} + /> ); }; diff --git a/playground/env-dynamics.mjs b/playground/env-dynamics.mjs index 4ce7269d..7b7ad3bf 100644 --- a/playground/env-dynamics.mjs +++ b/playground/env-dynamics.mjs @@ -3,11 +3,12 @@ export const rpcProviderUrls = { 1: process.env[`RPC_PROVIDER_URL_1`], 5: process.env[`RPC_PROVIDER_URL_5`], 17000: process.env[`RPC_PROVIDER_URL_17000`], + 11155111: process.env[`RPC_PROVIDER_URL_11155111`], }; /** @type number */ export const defaultChain = parseInt(process.env.DEFAULT_CHAIN, 10) || 17000; /** @type number[] */ export const supportedChains = process.env?.SUPPORTED_CHAINS?.split(',').map( (chainId) => parseInt(chainId, 10), -) ?? [1, 5, 17000]; +) ?? [1, 5, 17000, 11155111]; export const walletconnectProjectId = process.env.WALLETCONNECT_PROJECT_ID; diff --git a/playground/providers/web3.tsx b/playground/providers/web3.tsx index 8d826d4d..46e3d844 100644 --- a/playground/providers/web3.tsx +++ b/playground/providers/web3.tsx @@ -57,6 +57,8 @@ const Web3Provider: FC = ({ children }) => { [CHAINS.Mainnet]: getRpc(CHAINS.Mainnet), [CHAINS.Goerli]: getRpc(CHAINS.Goerli), [CHAINS.Holesky]: getRpc(CHAINS.Holesky), + // TODO: update @lido-sdk/constants + 11155111: getRpc(11155111), }; }, [customRpc]); diff --git a/yarn.lock b/yarn.lock index 9002d550..c6edf2f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9242,12 +9242,12 @@ __metadata: linkType: hard "follow-redirects@npm:^1.14.8, follow-redirects@npm:^1.15.4": - version: 1.15.5 - resolution: "follow-redirects@npm:1.15.5" + version: 1.15.6 + resolution: "follow-redirects@npm:1.15.6" peerDependenciesMeta: debug: optional: true - checksum: 5ca49b5ce6f44338cbfc3546823357e7a70813cecc9b7b768158a1d32c1e62e7407c944402a918ea8c38ae2e78266312d617dc68783fac502cbb55e1047b34ec + checksum: a62c378dfc8c00f60b9c80cab158ba54e99ba0239a5dd7c81245e5a5b39d10f0c35e249c3379eae719ff0285fff88c365dd446fab19dee771f1d76252df1bbf5 languageName: node linkType: hard