diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index f1867c50..87ae53bb 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -37,7 +37,7 @@ jobs: id: config_pages uses: actions/configure-pages@v4 - name: Restore cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | playground/.next/cache @@ -64,7 +64,7 @@ jobs: env: NODE_NO_BUILD_DYNAMICS: true - name: Upload artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 with: path: ./playground/out @@ -78,4 +78,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v3 + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index e808c62e..fdcfcf0d 100644 --- a/README.md +++ b/README.md @@ -92,24 +92,24 @@ All examples and usage instructions can be found in the [Docs SDK package](packa const lidoSDK = new LidoSDK({ chainId: 5, rpcUrls: ['https://eth-goerli.alchemyapi.io/v2/{ALCHEMY_API_KEY}'], + web3Provider: provider, }); -// Define default web3 provider in sdk (window.ethereum) if web3Provider is not defined in constructor -lidoSDK.core.defineWeb3Provider(); - // Views const balanceETH = await lidoSDK.core.balanceETH(address); // Calls -const stakeResult = await lidoSDK.stake.stakeEth({ +const stakeTx = await lidoSDK.stake.stakeEth({ value, callback, referralAddress, account, }); +// relevant results are returned with transaction +const { stethReceived, sharesReceived } = stakeTx.result; + console.log(balanceETH.toString(), 'ETH balance'); -console.log(stakeResult, 'stake result'); ``` ## Migration diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index a27fa13f..4c977540 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,3 +1,24 @@ +# 3.1.0 + +## SDK + +### Added + +- `viem` version up to `2.0.6` +- Account hoisting support: methods no longer require address/account if it's hoisted to `walletClient` or available via `eth_requestAccounts` +- Stake, Wrap, Withdraw Request & Claim transaction methods now return parsed transaction result +- `waitForTransactionReceiptParameters` [optional config](https://viem.sh/docs/actions/public/waitForTransactionReceipt.html) added to all transaction methods props + +### Fixed + +- better multisig behavior for transactions +- Simulate methods now have correct return types +- `stakeEthPopulateTx` not does not calculate `gasLimit` which prevented usage when stake limit is reached + +## Playground + +- Upped `next` and `viem` versions + # 3.0.1 ## SDK diff --git a/packages/sdk/README.md b/packages/sdk/README.md index d1f1f3c7..6de68160 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -249,7 +249,7 @@ const addressStETH = await lidoSDK.stake.contractAddressStETH(); const contractStETH = await lidoSDK.stake.getContractStETH(); // Calls -const stakeResult = await lidoSDK.stake.stakeEth({ +const stakeTx = await lidoSDK.stake.stakeEth({ value, callback, referralAddress, @@ -257,7 +257,7 @@ const stakeResult = await lidoSDK.stake.stakeEth({ console.log(addressStETH, 'stETH contract address'); console.log(contractStETH, 'stETH contract'); -console.log(stakeResult, 'stake result'); +console.log(stakeTx, 'stake tx result'); ``` ### Withdraw example @@ -276,15 +276,15 @@ const contractWithdrawalQueue = await lidoSDK.withdraw.contract.getContractWithdrawalQueue(); // Calls -const requestResult = await lidoSDK.withdraw.request.requestByToken({ - account, +const requestTx = await lidoSDK.withdraw.request.requestWithdrawalWithPermit({ amount: 10000000n, // `10000000` string is accepted as well token: 'stETH', + // account will be requested from provider }); console.log(addressWithdrawalQueue, 'Withdrawal Queue contract address'); console.log(contractWithdrawalQueue, 'Withdrawal Queue contract'); -console.log(requestResult, 'request result'); +console.log(requestTx.result.requests, 'array of created requests'); ``` ### Wrap example @@ -301,14 +301,16 @@ const addressWstETH = await lidoSDK.wrap.contractAddressWstETH(); const contractWstETH = await lidoSDK.withdraw.getContractWstETH(); // Calls -const wrapResult = await lidoSDK.wrap.wrapEth({ +const wrapTx = await lidoSDK.wrap.wrapEth({ value, account, }); +const { stethWrapped, wstethReceived } = wrapTx.result; + console.log(addressWstETH, 'wstETH contract address'); console.log(contractWstETH, 'wstETH contract'); -console.log(wrapResult, 'wrap result'); +console.log({ stethWrapped, wstethReceived }, 'wrap result'); ``` ## Error Codes @@ -397,7 +399,7 @@ const callback: StakeStageCallback = ({ stage, payload }) => { }; try { - const stakeResult = await lidoSDK.stake.stakeEth({ + const stakeTx = await lidoSDK.stake.stakeEth({ value, callback, referralAddress, @@ -405,8 +407,10 @@ try { }); console.log( - stakeResult, - 'transaction hash, transaction receipt, confirmations', + stakeTx, + 'transaction hash, transaction receipt, confirmations, stake result', + stakeTx.result.stethReceived, + stakeTx.result.sharesReceived, ); } catch (error) { console.log((error as SDKError).errorMessage, (error as SDKError).code); @@ -502,15 +506,17 @@ const callback: TransactionCallback = ({ stage, payload }) => { }; try { - const wrapResult = await lidoSDK.staking.wrapETH({ + const wrapTx = await lidoSDK.staking.wrapETH({ value, callback, account, }); console.log( - stakeResult, - 'transaction hash, transaction receipt, confirmations', + wrapTx, + 'transaction hash, transaction receipt, confirmations, wrap result', + wrapTx.result.stethWrapped, + wrapTx.result.wstethReceived, ); } catch (error) { console.log((error as SDKError).errorMessage, (error as SDKError).code); @@ -552,10 +558,12 @@ const wrapResult = await lidoSDK.wrap.wrapSteth({ value, callback }); ```ts // unwrap wstETH to receive stETH -const unwrapResult = await lidoSDK.wrap.unwrap({ +const unwrapTx = await lidoSDK.wrap.unwrap({ value: unwrapAmount, callback, }); + +console.log(unwrapTx.result.stethReceived, unwrapTx.result.wstethUnwrapped); ``` ### Wrap utilities @@ -636,7 +644,7 @@ const callback: TransactionCallback = ({ stage, payload }) => { }; try { - const requestResult = await lidoSDK.withdrawals.request.requestWithPermit({ + const requestTx = await lidoSDK.withdrawals.request.requestWithPermit({ requests, token, // 'stETH' | 'wstETH' callback, @@ -644,8 +652,10 @@ try { }); console.log( + 'transaction hash, transaction receipt, confirmations', requestResult, - 'transaction hash, transaction receipt, confirmations', + 'array of requests(nfts) created with ids, amounts,creator, owner' + request.results.requests, ); } catch (error) { console.log((error as SDKError).errorMessage, (error as SDKError).code); @@ -709,7 +719,7 @@ const callback: TransactionCallback = ({ stage, payload }) => { }; try { - const requestResult = await lidoSDK.withdrawals.request.requestWithoutPermit({ + const requestResult = await lidoSDK.withdrawals.request.requestWithdrawal({ amount, token, // 'stETH' | 'wstETH' callback, @@ -781,14 +791,16 @@ const callback: TransactionCallback = ({ stage, payload }) => { }; try { - const claimResult = await lidoSDK.withdrawals.claim.claimRequests({ + const claimTx = await lidoSDK.withdrawals.claim.claimRequests({ requestsIds, callback, }); console.log( - claimResult, + claimTx, 'transaction hash, transaction receipt, confirmations', + claim.result.requests, + 'array of claimed requests, with amounts of ETH claimed', ); } catch (error) { console.log((error as SDKError).errorMessage, (error as SDKError).code); diff --git a/packages/sdk/jest.config.ts b/packages/sdk/jest.config.ts index 69bc7821..7907b43e 100644 --- a/packages/sdk/jest.config.ts +++ b/packages/sdk/jest.config.ts @@ -3,10 +3,11 @@ import type { JestConfigWithTsJest } from 'ts-jest'; const jestConfig: JestConfigWithTsJest = { displayName: 'LidoSDK tests', testEnvironment: 'node', + // fix for leftover handles when running locally on macos + detectOpenHandles: true, + forceExit: true, preset: 'ts-jest', verbose: true, - - detectOpenHandles: true, extensionsToTreatAsEsm: ['.ts'], moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1', @@ -22,6 +23,7 @@ const jestConfig: JestConfigWithTsJest = { }, maxWorkers: 1, globalSetup: '/tests/global-setup.cjs', + globalTeardown: '/tests/global-teardown.cjs', testTimeout: 300_000, }; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 3c4dd54e..bfbb4217 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -145,7 +145,7 @@ "@ethersproject/bytes": "^5.7.0", "graphql": "^16.8.1", "graphql-request": "^6.1.0", - "viem": "^1.18.8" + "viem": "^2.0.6" }, "devDependencies": { "@jest/globals": "^29.7.0", diff --git a/packages/sdk/src/common/decorators/constants.ts b/packages/sdk/src/common/decorators/constants.ts index 4b5e9e07..b872cc72 100644 --- a/packages/sdk/src/common/decorators/constants.ts +++ b/packages/sdk/src/common/decorators/constants.ts @@ -8,6 +8,7 @@ export const ConsoleCss: Record = { 'Views:': 'color: aquamarine', 'Call:': 'color: orange', 'Error:': 'color: red', + 'Deprecation:': 'color: red', 'LOG:': 'color: lightblue', 'Cache:': 'color: mediumvioletred', 'Permit:': 'color: lime', diff --git a/packages/sdk/src/common/decorators/logger.ts b/packages/sdk/src/common/decorators/logger.ts index 8ca37320..c0826c27 100644 --- a/packages/sdk/src/common/decorators/logger.ts +++ b/packages/sdk/src/common/decorators/logger.ts @@ -12,6 +12,13 @@ export const Logger = function (headMessage: HeadMessage = 'LOG:') { const methodName = String(context.name); const replacementMethod = function (this: This, ...args: Args): Return { + if (headMessage === 'Deprecation:') + callConsoleMessage.call( + this, + headMessage, + `Method '${methodName}' is being deprecated in the next major version`, + ); + callConsoleMessage.call( this, headMessage, diff --git a/packages/sdk/src/common/decorators/types.ts b/packages/sdk/src/common/decorators/types.ts index 3e7967c8..fa7ab1ee 100644 --- a/packages/sdk/src/common/decorators/types.ts +++ b/packages/sdk/src/common/decorators/types.ts @@ -12,4 +12,5 @@ export type HeadMessage = | 'Permit:' | 'Events:' | 'Statistic:' - | 'Rewards:'; + | 'Rewards:' + | 'Deprecation:'; diff --git a/packages/sdk/src/common/utils/sdk-error.ts b/packages/sdk/src/common/utils/sdk-error.ts index 9ec7cdb6..b92ae685 100644 --- a/packages/sdk/src/common/utils/sdk-error.ts +++ b/packages/sdk/src/common/utils/sdk-error.ts @@ -38,7 +38,10 @@ export class SDKError extends Error { constructor({ code, error = {}, message }: SDKErrorProps) { super(message); - Object.assign(this, error); + if (error instanceof Error) { + this.cause = error.cause; + this.stack = error.stack; + } this.code = code ?? ERROR_CODE.UNKNOWN_ERROR; this.errorMessage = message; } diff --git a/packages/sdk/src/core/__tests__/core-wallet.test.ts b/packages/sdk/src/core/__tests__/core-wallet.test.ts index c4d3fce9..ba02897a 100644 --- a/packages/sdk/src/core/__tests__/core-wallet.test.ts +++ b/packages/sdk/src/core/__tests__/core-wallet.test.ts @@ -1,11 +1,41 @@ -import { test, expect, describe } from '@jest/globals'; +import { test, expect, describe, jest } from '@jest/globals'; -import { useWeb3Core } from '../../../tests/utils/fixtures/use-core.js'; -import { getContract, maxUint256, parseEther } from 'viem'; +import { + useRpcCore, + useWeb3Core, +} from '../../../tests/utils/fixtures/use-core.js'; +import { + WalletClient, + createWalletClient, + getContract, + http, + maxUint256, + parseEther, + zeroAddress, +} from 'viem'; import { expectAddress } from '../../../tests/utils/expect/expect-address.js'; import { useTestsEnvs } from '../../../tests/utils/fixtures/use-test-envs.js'; import { expectNonNegativeBn } from '../../../tests/utils/expect/expect-bn.js'; -import { LIDO_CONTRACT_NAMES } from '../../index.js'; +import { + CHAINS, + LIDO_CONTRACT_NAMES, + LidoSDKCore, + LidoSDKStake, + PerformTransactionGasLimit, + PerformTransactionSendTransaction, + VIEM_CHAINS, +} from '../../index.js'; +import { + useAccount, + useAltAccount, +} from '../../../tests/utils/fixtures/use-wallet-client.js'; +import { + MockTransportCallback, + useMockTransport, +} from '../../../tests/utils/fixtures/use-mock-transport.js'; +import { testSpending } from '../../../tests/utils/test-spending.js'; +import { expectTxCallback } from '../../../tests/utils/expect/expect-tx-callback.js'; +import { usePublicRpcProvider } from '../../../tests/utils/fixtures/use-test-rpc-provider.js'; const permitAbi = [ { @@ -25,6 +55,17 @@ const permitAbi = [ }, ] as const; +const createCore = (walletClient?: WalletClient) => { + const { chainId } = useTestsEnvs(); + const rpcProvider = usePublicRpcProvider(); + return new LidoSDKCore({ + chainId, + logMode: 'none', + rpcProvider, + web3Provider: walletClient, + }); +}; + describe('Core Wallet Tests', () => { const { chainId } = useTestsEnvs(); const web3Core = useWeb3Core(); @@ -68,7 +109,7 @@ describe('Core Wallet Tests', () => { const contract = getContract({ abi: permitAbi, address: contractAddress, - publicClient: web3Core.rpcProvider, + client: web3Core.rpcProvider, }); await contract.simulate.permit([ @@ -87,7 +128,7 @@ describe('Core Wallet Tests', () => { expect(provider).toBeDefined(); }); - test('account is available', async () => { + test('getWeb3Address works', async () => { const address = await web3Core.getWeb3Address(); expect(address).toBeDefined(); expectAddress(address); @@ -107,3 +148,190 @@ describe('Core Wallet Tests', () => { await expect(testPermit(false)).resolves.not.toThrow(); }); }); + +describe('Account hoisting', () => { + const altAccount = useAltAccount(); + const { rpcUrl } = useTestsEnvs(); + + test('useAccount returns from prop address', async () => { + const core = createCore(); + const account = await core.useAccount(altAccount.address); + expectAddress(account.address, altAccount.address); + expect(account.type).toBe('json-rpc'); + }); + + test('useAccount returns from prop account', async () => { + const core = createCore(); + const account = await core.useAccount(altAccount); + expect(account).toBe(altAccount); + }); + + test('useAccount returns hoisted account', async () => { + const walletClient = createWalletClient({ + account: altAccount, + transport: http(rpcUrl), + }); + const core = createCore(walletClient); + const account = await core.useAccount(); + expect(account).toBe(altAccount); + }); + + test('useAccount requests account from web3Provider', async () => { + const mockFn = jest.fn(); + const mockTransport = useMockTransport(async (args, originalRequest) => { + mockFn(args.method); + if (args.method === 'eth_requestAccounts') { + return [altAccount.address]; + } + return originalRequest(); + }); + + // setup, no account + const walletClient = createWalletClient({ + transport: mockTransport, + }); + const core = createCore(walletClient); + expect(core.useWeb3Provider().account).toBeUndefined(); + + // first call, account hoisted + const account = await core.useAccount(); + expect(account.address).toBe(altAccount.address); + expect(account.type).toBe('json-rpc'); + expect(core.web3Provider?.account).toBe(account); + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn.mock.calls[0]?.[0]).toBe('eth_requestAccounts'); + + // second call, account reused + const accountReused = await core.useAccount(); + expect(accountReused).toBe(account); + expect(mockFn).toHaveBeenCalledTimes(1); + }); +}); + +describe('Perform Transaction', () => { + const { chainId } = useTestsEnvs(); + const mockMultisigAddress = useRpcCore().getContractLidoLocator().address; + const account = useAccount(); + const value = 100n; + const testHash = '0xaaaaaabbbbbbb'; + + testSpending('perform transaction works with EOA', async () => { + const mockTransportCallback = jest.fn((_, next) => + next(), + ); + const core = createCore( + createWalletClient({ + account, + chain: VIEM_CHAINS[chainId as CHAINS], + transport: useMockTransport(mockTransportCallback), + }), + ); + const stake = new LidoSDKStake({ core }); + const rawContract = await stake.getContractStETH(); + + const mockGetGasLimit = jest.fn((options) => + rawContract.estimateGas.submit([zeroAddress], { + ...options, + value, + }), + ); + const mockSendTransaction = jest.fn( + (options) => + rawContract.write.submit([zeroAddress], { ...options, value }), + ); + const mockTxCallback = jest.fn(); + + const txResult = await core.performTransaction({ + getGasLimit: mockGetGasLimit, + sendTransaction: mockSendTransaction, + callback: mockTxCallback, + }); + const gasLimit = await mockGetGasLimit.mock.results[0]?.value; + expectTxCallback(mockTxCallback, txResult); + expect(mockGetGasLimit).toHaveBeenCalledWith({ + account, + chain: core.chain, + gas: undefined, + maxFeePerGas: expect.any(BigInt), + maxPriorityFeePerGas: expect.any(BigInt), + }); + expect(mockSendTransaction).toHaveBeenCalledWith({ + account, + chain: core.chain, + gas: gasLimit, + maxFeePerGas: mockGetGasLimit.mock.calls[0]?.[0]?.maxFeePerGas, + maxPriorityFeePerGas: + mockGetGasLimit.mock.calls[0]?.[0].maxPriorityFeePerGas, + }); + + expect(mockTransportCallback).toHaveBeenLastCalledWith( + { + method: 'eth_sendRawTransaction', + params: expect.any(Array), + }, + expect.anything(), + ); + }); + + testSpending('perform transaction works with Multisig', async () => { + const mockTransportCallback = jest.fn( + async (args, next) => { + if (args.method === 'eth_sendTransaction') { + return testHash; + } + return next(); + }, + ); + const core = createCore( + createWalletClient({ + account: mockMultisigAddress, + chain: VIEM_CHAINS[chainId as CHAINS], + transport: useMockTransport(mockTransportCallback), + }), + ); + + const stake = new LidoSDKStake({ core }); + const rawContract = await stake.getContractStETH(); + + const mockGetGasLimit = jest.fn((options) => + rawContract.estimateGas.submit([zeroAddress], { + ...options, + value, + }), + ); + const mockSendTransaction = jest.fn( + (options) => + rawContract.write.submit([zeroAddress], { ...options, value }), + ); + const mockTxCallback = jest.fn(); + + const txResult = await core.performTransaction({ + getGasLimit: mockGetGasLimit, + sendTransaction: mockSendTransaction, + callback: mockTxCallback, + }); + + expect(txResult.hash).toBe(testHash); + + expectTxCallback(mockTxCallback, { ...txResult, isMultisig: true }); + expect(mockGetGasLimit).not.toHaveBeenCalled(); + expect(mockSendTransaction).toHaveBeenCalledWith({ + chain: core.chain, + account: { address: mockMultisigAddress, type: 'json-rpc' }, + gas: 21000n, + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + nonce: 1, + }); + + // first call is chainId cross-check + expect(mockTransportCallback).toHaveBeenCalledTimes(2); + expect(mockTransportCallback).toHaveBeenLastCalledWith( + { + method: 'eth_sendTransaction', + params: expect.any(Array), + }, + expect.anything(), + ); + }); +}); diff --git a/packages/sdk/src/core/__tests__/core.test.ts b/packages/sdk/src/core/__tests__/core.test.ts index a7908356..7bd60ba6 100644 --- a/packages/sdk/src/core/__tests__/core.test.ts +++ b/packages/sdk/src/core/__tests__/core.test.ts @@ -28,8 +28,8 @@ describe('Core Tests', () => { expect(core.web3Provider).toBeUndefined(); }); - test('Core accepts only valid arguments', () => { - void expectSDKError( + test('Core accepts only valid arguments', async () => { + await expectSDKError( () => new LidoSDKCore({ chainId: chainId, @@ -38,7 +38,7 @@ describe('Core Tests', () => { ERROR_CODE.INVALID_ARGUMENT, ); - void expectSDKError( + await expectSDKError( () => new LidoSDKCore({ chainId: 100 as any, diff --git a/packages/sdk/src/core/__tests__/get-block-by-timestamp.test.ts b/packages/sdk/src/core/__tests__/get-block-by-timestamp.test.ts index 27755f0d..f09062dc 100644 --- a/packages/sdk/src/core/__tests__/get-block-by-timestamp.test.ts +++ b/packages/sdk/src/core/__tests__/get-block-by-timestamp.test.ts @@ -39,7 +39,7 @@ describe('getLatestBlockToTimestamp', () => { // eslint-disable-next-line jest/no-disabled-tests test.skip('throws error at invalid timestamp', async () => { - // TODO: research for some reason expectSDKError does not work here + // TODO: research for some reason jest prints this error and abruptly exits await expect(core.getLatestBlockToTimestamp(0n)).rejects.toThrow( 'No blocks at this timestamp', ); diff --git a/packages/sdk/src/core/core.ts b/packages/sdk/src/core/core.ts index 796cdd7e..c471b65c 100644 --- a/packages/sdk/src/core/core.ts +++ b/packages/sdk/src/core/core.ts @@ -5,6 +5,7 @@ import { type Chain, type GetContractReturnType, type CustomTransportConfig, + type GetBlockReturnType, createPublicClient, createWalletClient, fallback, @@ -12,7 +13,7 @@ import { custom, getContract, maxUint256, - GetBlockReturnType, + JsonRpcAccount, } from 'viem'; import { ERROR_CODE, @@ -159,8 +160,9 @@ export default class LidoSDKCore extends LidoSDKCacheable { // Balances @Logger('Balances:') - public async balanceETH(address: Address): Promise { - return this.rpcProvider.getBalance({ address }); + public async balanceETH(address?: AccountValue): Promise { + const parsedAccount = await this.useAccount(address); + return this.rpcProvider.getBalance({ address: parsedAccount.address }); } // Contracts @@ -175,14 +177,12 @@ export default class LidoSDKCore extends LidoSDKCacheable { @Cache(30 * 60 * 1000, ['chain.id', 'contractAddressLidoLocator']) public getContractLidoLocator(): GetContractReturnType< typeof LidoLocatorAbi, - PublicClient, - WalletClient + PublicClient > { return getContract({ address: this.contractAddressLidoLocator(), abi: LidoLocatorAbi, - publicClient: this.rpcProvider, - walletClient: this.web3Provider, + client: this.rpcProvider, }); } @@ -194,7 +194,7 @@ export default class LidoSDKCore extends LidoSDKCacheable { return getContract({ address, abi: wqAbi, - publicClient: this.rpcProvider, + client: this.rpcProvider, }); } @@ -204,22 +204,22 @@ export default class LidoSDKCore extends LidoSDKCacheable { const { token, amount, - account, + account: accountProp, spender, deadline = LidoSDKCore.INFINITY_DEADLINE_VALUE, } = props; const web3Provider = this.useWeb3Provider(); - const accountAddress = await this.getWeb3Address(account); + const account = await this.useAccount(accountProp); const { contract, domain } = await this.getPermitContractData(token); - const nonce = await contract.read.nonces([accountAddress]); + const nonce = await contract.read.nonces([account.address]); const signature = await web3Provider.signTypedData({ - account: account ?? web3Provider.account ?? accountAddress, + account, domain, types: PERMIT_MESSAGE_TYPES, primaryType: 'Permit', message: { - owner: accountAddress, + owner: account.address, spender, value: amount, nonce, @@ -236,7 +236,7 @@ export default class LidoSDKCore extends LidoSDKCacheable { deadline, nonce, chainId: domain.chainId, - owner: accountAddress, + owner: account.address, spender, }; } @@ -252,8 +252,7 @@ export default class LidoSDKCore extends LidoSDKCacheable { const contract = getContract({ address: tokenAddress, abi: permitAbi, - publicClient: this.rpcProvider, - walletClient: this.web3Provider, + client: this.rpcProvider, }); let domain = { @@ -311,7 +310,7 @@ export default class LidoSDKCore extends LidoSDKCacheable { }; } - @Logger('Utils:') + @Logger('Deprecation:') public async getWeb3Address(accountValue?: AccountValue): Promise
{ if (typeof accountValue === 'string') return accountValue; if (accountValue) return accountValue.address; @@ -328,6 +327,33 @@ export default class LidoSDKCore extends LidoSDKCacheable { return account; } + @Logger('Utils:') + public async useAccount( + accountValue?: AccountValue, + ): Promise { + if (accountValue) { + if (typeof accountValue === 'string') + return { address: accountValue, type: 'json-rpc' }; + else return accountValue as JsonRpcAccount; + } + if (this.web3Provider) { + if (!this.web3Provider.account) { + const [account] = await withSDKError( + this.web3Provider.requestAddresses(), + ERROR_CODE.READ_ERROR, + ); + invariant( + account, + 'web3provider must have at least 1 account', + ERROR_CODE.PROVIDER_ERROR, + ); + this.web3Provider.account = { address: account, type: 'json-rpc' }; + } + return this.web3Provider.account as unknown as JsonRpcAccount; + } + invariantArgument(false, 'No account or web3Provider is available'); + } + @Logger('Utils:') @Cache(60 * 60 * 1000, ['chain.id']) public async isContract(address: Address): Promise { @@ -440,28 +466,39 @@ export default class LidoSDKCore extends LidoSDKCacheable { } } - // TODO separate test suit with multisig - public async performTransaction( - props: PerformTransactionOptions, - ): Promise { - const provider = this.useWeb3Provider(); - const { account, callback = NOOP, getGasLimit, sendTransaction } = props; - const accountAddress = await this.getWeb3Address(account); - const isContract = await this.isContract(accountAddress); - - // we need account to be defined for transactions so we fallback in this order - // 1. whatever user passed - // 2. hoisted account - // 3. just address (this will break on local accounts as per viem behavior) - const overrides: TransactionOptions = { - account: account ?? provider.account ?? accountAddress, + public async performTransaction( + props: PerformTransactionOptions, + ): Promise> { + // this guards against not having web3Provider + this.useWeb3Provider(); + const { + callback = NOOP, + getGasLimit, + sendTransaction, + decodeResult, + waitForTransactionReceiptParameters = {}, + } = props; + const account = await this.useAccount(props.account); + const isContract = await this.isContract(account.address); + + let overrides: TransactionOptions = { + account, chain: this.chain, gas: undefined, maxFeePerGas: undefined, maxPriorityFeePerGas: undefined, }; - if (!isContract) { + if (isContract) { + // passing these stub params prevent unnecessary possibly errorish RPC calls + overrides = { + ...overrides, + gas: 21000n, + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + nonce: 1, + }; + } else { callback({ stage: TransactionCallbackStage.GAS_LIMIT }); const feeData = await this.getFeeData(); overrides.maxFeePerGas = feeData.maxFeePerGas; @@ -487,46 +524,52 @@ export default class LidoSDKCore extends LidoSDKCacheable { callback({ stage: TransactionCallbackStage.SIGN, payload: overrides.gas }); - const transactionHash = await withSDKError( - sendTransaction({ ...overrides }), + const hash = await withSDKError( + sendTransaction({ + ...overrides, + }), ERROR_CODE.TRANSACTION_ERROR, ); if (isContract) { callback({ stage: TransactionCallbackStage.MULTISIG_DONE }); - return { hash: transactionHash }; + return { hash }; } callback({ stage: TransactionCallbackStage.RECEIPT, - payload: transactionHash, + payload: hash, }); - const transactionReceipt = await withSDKError( + const receipt = await withSDKError( this.rpcProvider.waitForTransactionReceipt({ - hash: transactionHash, + hash, timeout: 120_000, + ...waitForTransactionReceiptParameters, }), ERROR_CODE.TRANSACTION_ERROR, ); callback({ stage: TransactionCallbackStage.CONFIRMATION, - payload: transactionReceipt, + payload: receipt, }); const confirmations = await this.rpcProvider.getTransactionConfirmations({ - hash: transactionReceipt.transactionHash, + hash: receipt.transactionHash, }); + const result = await decodeResult?.(receipt); + callback({ stage: TransactionCallbackStage.DONE, payload: confirmations, }); return { - hash: transactionHash, - receipt: transactionReceipt, + hash, + receipt, + result, confirmations, }; } diff --git a/packages/sdk/src/core/types.ts b/packages/sdk/src/core/types.ts index b295a870..a08efc3d 100644 --- a/packages/sdk/src/core/types.ts +++ b/packages/sdk/src/core/types.ts @@ -8,6 +8,7 @@ import type { FormattedTransactionRequest, BlockTag, Account, + WaitForTransactionReceiptParameters, } from 'viem'; import { LIDO_TOKENS, SUPPORTED_CHAINS } from '../common/constants.js'; @@ -60,6 +61,7 @@ export enum TransactionCallbackStage { export type CommonTransactionProps = { callback?: TransactionCallback; account?: AccountValue; + waitForTransactionReceiptParameters?: WaitForTransactionReceiptParameters; }; export type PerformTransactionGasLimit = ( @@ -70,10 +72,21 @@ export type PerformTransactionSendTransaction = ( override: TransactionOptions, ) => Promise; -export type PerformTransactionOptions = CommonTransactionProps & { - getGasLimit: PerformTransactionGasLimit; - sendTransaction: PerformTransactionSendTransaction; -}; +export type PerformTransactionDecodeResult = ( + receipt: TransactionReceipt, +) => Promise; + +type PerformTransactionOptionsDecodePartial = + TDecodedResult extends undefined + ? { decodeResult?: undefined } + : { decodeResult: PerformTransactionDecodeResult }; + +export type PerformTransactionOptions = + CommonTransactionProps & { + getGasLimit: PerformTransactionGasLimit; + sendTransaction: PerformTransactionSendTransaction; + waitForTransactionReceiptParameters?: WaitForTransactionReceiptParameters; + } & PerformTransactionOptionsDecodePartial; export type TransactionOptions = { account: AccountValue; @@ -81,12 +94,14 @@ export type TransactionOptions = { gas?: bigint; maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint; + nonce?: number; }; -export type TransactionResult = { +export type TransactionResult = { hash: Hash; receipt?: TransactionReceipt; confirmations?: bigint; + result?: TDecodedResult; }; export type PopulatedTransaction = Omit; diff --git a/packages/sdk/src/erc20/erc20.ts b/packages/sdk/src/erc20/erc20.ts index de4e9a93..135a2328 100644 --- a/packages/sdk/src/erc20/erc20.ts +++ b/packages/sdk/src/erc20/erc20.ts @@ -12,7 +12,6 @@ import { type Address, type GetContractReturnType, type Hash, - type PublicClient, type WalletClient, encodeFunctionData, getContract, @@ -37,14 +36,16 @@ export abstract class AbstractLidoSDKErc20 extends LidoSDKModule { @Logger('Contracts:') @Cache(30 * 60 * 1000, ['core.chain.id']) public async getContract(): Promise< - GetContractReturnType + GetContractReturnType > { const address = await this.contractAddress(); return getContract({ address, abi: erc20abi, - publicClient: this.core.rpcProvider, - walletClient: this.core.web3Provider, + client: { + public: this.core.rpcProvider, + wallet: this.core.web3Provider as WalletClient, + }, }); } @@ -52,9 +53,10 @@ export abstract class AbstractLidoSDKErc20 extends LidoSDKModule { @Logger('Balances:') @ErrorHandler() - public async balance(address: Address): Promise { + public async balance(address?: Address): Promise { + const { address: parsedAddress } = await this.core.useAccount(address); const contract = await this.getContract(); - return contract.read.balanceOf([address]); + return contract.read.balanceOf([parsedAddress]); } // Transfer @@ -63,10 +65,9 @@ export abstract class AbstractLidoSDKErc20 extends LidoSDKModule { @ErrorHandler() public async transfer(props: TransferProps): Promise { this.core.useWeb3Provider(); - const parsedProps = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(props.account); - const { amount, to, from = accountAddress } = parsedProps; - const isTransferFrom = from !== accountAddress; + const parsedProps = await this.parseProps(props); + const { account, amount, to, from = account.address } = parsedProps; + const isTransferFrom = from !== account.address; const contract = await this.getContract(); const getGasLimit = async (overrides: TransactionOptions) => @@ -89,19 +90,13 @@ export abstract class AbstractLidoSDKErc20 extends LidoSDKModule { @Logger('Utils:') @ErrorHandler() public async populateTransfer(props: NoCallback) { - const accountAddress = await this.core.getWeb3Address(props.account); - const { - account, - amount, - to, - from = accountAddress, - } = this.parseProps(props); - - const isTransferFrom = from !== accountAddress; - const address = await this.contractAddress(); + const parsedProps = await this.parseProps(props); + const { account, amount, to, from = account.address } = parsedProps; + const isTransferFrom = from !== account.address; + const contractAddress = await this.contractAddress(); return { - to: address, + to: contractAddress, from: account, data: isTransferFrom ? encodeFunctionData({ @@ -120,14 +115,10 @@ export abstract class AbstractLidoSDKErc20 extends LidoSDKModule { @Logger('Utils:') @ErrorHandler() public async simulateTransfer(props: NoCallback) { - const accountAddress = await this.core.getWeb3Address(props.account); - const { - account, - amount, - to, - from = accountAddress, - } = this.parseProps(props); - const isTransferFrom = from !== accountAddress; + const parsedProps = await this.parseProps(props); + const { account, amount, to, from = account.address } = parsedProps; + const isTransferFrom = from !== account.address; + const contract = await this.getContract(); return isTransferFrom ? contract.simulate.transferFrom([from, to, amount], { account }) @@ -159,18 +150,18 @@ export abstract class AbstractLidoSDKErc20 extends LidoSDKModule { public async populatePermit(props: SignTokenPermitProps) { const { amount, - account, + account: accountProp, spender, deadline = LidoSDKCore.INFINITY_DEADLINE_VALUE, } = props; - const web3Provider = this.core.useWeb3Provider(); + const contract = await this.getContract(); const domain = await this.erc721Domain(); - const accountAddress = await this.core.getWeb3Address(account); - const nonce = await contract.read.nonces([accountAddress]); + const account = await this.core.useAccount(accountProp); + const nonce = await contract.read.nonces([account.address]); const message = { - owner: accountAddress, + owner: account.address, spender, value: amount, nonce, @@ -178,7 +169,7 @@ export abstract class AbstractLidoSDKErc20 extends LidoSDKModule { }; return { - account: account ?? web3Provider.account ?? accountAddress, + account, domain, types: PERMIT_MESSAGE_TYPES, primaryType: 'Permit', @@ -192,7 +183,7 @@ export abstract class AbstractLidoSDKErc20 extends LidoSDKModule { @ErrorHandler() public async approve(props: ApproveProps): Promise { this.core.useWeb3Provider(); - const parsedProps = this.parseProps(props); + const parsedProps = await this.parseProps(props); const contract = await this.getContract(); const txArguments = [parsedProps.to, parsedProps.amount] as const; return this.core.performTransaction({ @@ -207,13 +198,12 @@ export abstract class AbstractLidoSDKErc20 extends LidoSDKModule { @Logger('Utils:') @ErrorHandler() public async populateApprove(props: NoCallback) { - const { account, amount, to } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); + const { account, amount, to } = await this.parseProps(props); const address = await this.contractAddress(); return { to: address, - from: accountAddress, + from: account.address, data: encodeFunctionData({ abi: erc20abi, functionName: 'approve', @@ -225,15 +215,18 @@ export abstract class AbstractLidoSDKErc20 extends LidoSDKModule { @Logger('Utils:') @ErrorHandler() public async simulateApprove(props: NoCallback) { - const { account, amount, to } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); + const { account, amount, to } = await this.parseProps(props); const contract = await this.getContract(); - return contract.simulate.approve([to, amount], { account: accountAddress }); + return contract.simulate.approve([to, amount], { account }); } @Logger('Views:') - public async allowance({ account, to }: AllowanceProps): Promise { - return (await this.getContract()).read.allowance([account, to]); + public async allowance({ + account: accountProp, + to, + }: AllowanceProps): Promise { + const account = await this.core.useAccount(accountProp); + return (await this.getContract()).read.allowance([account.address, to]); } // Views @@ -303,11 +296,12 @@ export abstract class AbstractLidoSDKErc20 extends LidoSDKModule { return { name, version, chainId, verifyingContract }; } - private parseProps< + private async parseProps< TProps extends CommonTransactionProps & { amount: EtherValue }, - >(props: TProps): ParsedTransactionProps { + >(props: TProps): Promise> { return { ...props, + account: await this.core.useAccount(props.account), amount: parseValue(props.amount), callback: props.callback ?? NOOP, }; diff --git a/packages/sdk/src/erc20/types.ts b/packages/sdk/src/erc20/types.ts index 7c40903b..d0fa0c04 100644 --- a/packages/sdk/src/erc20/types.ts +++ b/packages/sdk/src/erc20/types.ts @@ -1,17 +1,17 @@ -import { Address } from 'viem'; -import { type TransactionCallback, type EtherValue } from '../core/index.js'; -import { CommonTransactionProps, SignPermitProps } from '../core/types.js'; - -export type TransactionProps = { - account: Address; - callback?: TransactionCallback; -}; +import type { Address, JsonRpcAccount } from 'viem'; +import { type EtherValue } from '../core/index.js'; +import { + AccountValue, + CommonTransactionProps, + SignPermitProps, +} from '../core/types.js'; export type InnerTransactionProps = Required; export type ParsedTransactionProps = Omit & { callback: NonNullable; + account: JsonRpcAccount; } & (TProps extends { amount: EtherValue } ? { amount: bigint } : object); export type TransferProps = { @@ -26,7 +26,7 @@ export type ApproveProps = { } & CommonTransactionProps; export type AllowanceProps = { - account: Address; + account?: AccountValue; to: Address; }; diff --git a/packages/sdk/src/events/steth-events.ts b/packages/sdk/src/events/steth-events.ts index 77e92a78..b6968c7d 100644 --- a/packages/sdk/src/events/steth-events.ts +++ b/packages/sdk/src/events/steth-events.ts @@ -1,10 +1,5 @@ import { getContract } from 'viem'; -import type { - Address, - GetContractReturnType, - PublicClient, - WalletClient, -} from 'viem'; +import type { Address, GetContractReturnType, PublicClient } from 'viem'; import { Logger, Cache, ErrorHandler } from '../common/decorators/index.js'; import { LIDO_CONTRACT_NAMES } from '../common/constants.js'; @@ -41,14 +36,14 @@ export class LidoSDKStethEvents extends LidoSDKModule { @Logger('Contracts:') @Cache(30 * 60 * 1000, ['core.chain.id', 'contractAddressStETH']) private async getContractStETH(): Promise< - GetContractReturnType + GetContractReturnType > { const address = await this.contractAddressStETH(); return getContract({ address, abi: StethEventsAbi, - publicClient: this.core.rpcProvider, + client: this.core.rpcProvider, }); } diff --git a/packages/sdk/src/rewards/rewards.ts b/packages/sdk/src/rewards/rewards.ts index be13f53d..e2257a6d 100644 --- a/packages/sdk/src/rewards/rewards.ts +++ b/packages/sdk/src/rewards/rewards.ts @@ -79,7 +79,7 @@ export class LidoSDKRewards extends LidoSDKModule { return getContract({ address, abi: rewardsEventsAbi, - publicClient: this.core.rpcProvider, + client: this.core.rpcProvider, }); } diff --git a/packages/sdk/src/shares/shares.ts b/packages/sdk/src/shares/shares.ts index e310535e..d3f46a00 100644 --- a/packages/sdk/src/shares/shares.ts +++ b/packages/sdk/src/shares/shares.ts @@ -1,7 +1,6 @@ import { type GetContractReturnType, type Address, - type PublicClient, type WalletClient, getContract, encodeFunctionData, @@ -14,7 +13,12 @@ import { TransactionResult } from '../core/index.js'; import { SharesTotalSupplyResult, SharesTransferProps } from './types.js'; import { stethSharesAbi } from './abi/steth-shares-abi.js'; import { parseValue } from '../common/utils/parse-value.js'; -import { EtherValue, NoCallback, TransactionOptions } from '../core/types.js'; +import type { + AccountValue, + EtherValue, + NoCallback, + TransactionOptions, +} from '../core/types.js'; import { calcShareRate } from '../rewards/utils.js'; import { LidoSDKModule } from '../common/class-primitives/sdk-module.js'; @@ -32,23 +36,26 @@ export class LidoSDKShares extends LidoSDKModule { @Logger('Contracts:') @Cache(30 * 60 * 1000, ['core.chain.id', 'contractAddressStETH']) public async getContractStETHshares(): Promise< - GetContractReturnType + GetContractReturnType > { const address = await this.contractAddressStETH(); return getContract({ address, abi: stethSharesAbi, - publicClient: this.core.rpcProvider, - walletClient: this.core.web3Provider, + client: { + public: this.core.rpcProvider, + wallet: this.core.web3Provider as WalletClient, + }, }); } @Logger('Balances:') @ErrorHandler() - public async balance(address: Address): Promise { + public async balance(address?: AccountValue): Promise { const contract = await this.getContractStETHshares(); - return contract.read.sharesOf([address]); + const account = await this.core.useAccount(address); + return contract.read.sharesOf([account.address]); } // Transfer @@ -56,18 +63,19 @@ export class LidoSDKShares extends LidoSDKModule { @Logger('Call:') @ErrorHandler() public async transfer({ - account, + account: accountProp, to, amount: _amount, callback = NOOP, from: _from, + ...rest }: SharesTransferProps): Promise { this.core.useWeb3Provider(); - const accountAddress = await this.core.getWeb3Address(account); - const from = _from ?? accountAddress; + const account = await this.core.useAccount(accountProp); + const from = _from ?? account.address; const amount = parseValue(_amount); - const isTransferFrom = from !== accountAddress; + const isTransferFrom = from !== account.address; const contract = await this.getContractStETHshares(); const getGasLimit = async (overrides: TransactionOptions) => @@ -81,6 +89,7 @@ export class LidoSDKShares extends LidoSDKModule { : contract.write.transferShares([to, amount], overrides); return this.core.performTransaction({ + ...rest, account, callback, getGasLimit, @@ -91,20 +100,20 @@ export class LidoSDKShares extends LidoSDKModule { @Logger('Utils:') @ErrorHandler() public async populateTransfer({ - account, + account: accountProp, to, amount: _amount, from: _from, }: NoCallback) { - const accountAddress = await this.core.getWeb3Address(account); + const account = await this.core.useAccount(accountProp); const amount = parseValue(_amount); - const from = _from ?? accountAddress; + const from = _from ?? account.address; const address = await this.contractAddressStETH(); - const isTransferFrom = from !== accountAddress; + const isTransferFrom = from !== account.address; return { to: address, - from: accountAddress, + from: account.address, data: isTransferFrom ? encodeFunctionData({ abi: stethSharesAbi, @@ -122,22 +131,22 @@ export class LidoSDKShares extends LidoSDKModule { @Logger('Utils:') @ErrorHandler() public async simulateTransfer({ - account, + account: _account, to, amount: _amount, from: _from, }: NoCallback) { const amount = parseValue(_amount); - const accountAddress = await this.core.getWeb3Address(account); - const from = _from ?? accountAddress; + const account = await this.core.useAccount(_account); + const from = _from ?? account.address; const contract = await this.getContractStETHshares(); - const isTransferFrom = from !== accountAddress; + const isTransferFrom = from !== account.address; return isTransferFrom ? contract.simulate.transferSharesFrom([from, to, amount], { - account: accountAddress, + account, }) : contract.simulate.transferShares([to, amount], { - account: accountAddress, + account, }); } diff --git a/packages/sdk/src/stake/__tests__/stake-wallet.test.ts b/packages/sdk/src/stake/__tests__/stake-wallet.test.ts index 8e908a13..fbe1b66d 100644 --- a/packages/sdk/src/stake/__tests__/stake-wallet.test.ts +++ b/packages/sdk/src/stake/__tests__/stake-wallet.test.ts @@ -1,4 +1,4 @@ -import { describe, jest } from '@jest/globals'; +import { describe, jest, expect } from '@jest/globals'; import { useStake } from '../../../tests/utils/fixtures/use-stake.js'; import { SPENDING_TIMEOUT, @@ -7,25 +7,35 @@ import { import { expectTxCallback } from '../../../tests/utils/expect/expect-tx-callback.js'; import { expectAlmostEqualBn } from '../../../tests/utils/expect/expect-bn.js'; import { useSteth } from '../../../tests/utils/fixtures/use-steth.js'; +import { useShares } from '../../../tests/utils/fixtures/use-shares.js'; describe('LidoSDKStake wallet methods', () => { const stake = useStake(); const steth = useSteth(); + const shares = useShares(); testSpending( 'can stake', async () => { - const address = await stake.core.getWeb3Address(); const stakeValue = 100n; - const balanceBefore = await steth.balance(address); + const balanceBefore = await steth.balance(); + const balanceSharesBefore = await shares.balance(); const mockTxCallback = jest.fn(); const tx = await stake.stakeEth({ value: stakeValue, callback: mockTxCallback, }); expectTxCallback(mockTxCallback, tx); - const balanceAfter = await steth.balance(address); + const balanceAfter = await steth.balance(); + const balanceSharesAfter = await shares.balance(); + const balanceDiff = balanceAfter - balanceBefore; + const balanceSharesDiff = balanceSharesAfter - balanceSharesBefore; // due to protocol rounding error this can happen - expectAlmostEqualBn(balanceAfter - balanceBefore, stakeValue); + expectAlmostEqualBn(balanceDiff, stakeValue); + expect(tx.result).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const result = tx.result!; + expectAlmostEqualBn(result.stethReceived, balanceDiff); + expect(result.sharesReceived).toEqual(balanceSharesDiff); }, SPENDING_TIMEOUT, ); diff --git a/packages/sdk/src/stake/abi/steth.ts b/packages/sdk/src/stake/abi/steth.ts index de954a92..272bdc2d 100644 --- a/packages/sdk/src/stake/abi/steth.ts +++ b/packages/sdk/src/stake/abi/steth.ts @@ -695,3 +695,27 @@ export const StethAbi = [ type: 'event', }, ] as const; + +// smaller ABI for less overhead when parsing submit events +export const StethEventsPartialAbi = [ + { + 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: 'from', type: 'address' }, + { indexed: true, name: 'to', type: 'address' }, + { indexed: false, name: 'value', type: 'uint256' }, + ], + name: 'Transfer', + type: 'event', + }, +] as const; diff --git a/packages/sdk/src/stake/stake.ts b/packages/sdk/src/stake/stake.ts index 92d7b5b7..925c68ba 100644 --- a/packages/sdk/src/stake/stake.ts +++ b/packages/sdk/src/stake/stake.ts @@ -1,11 +1,19 @@ -import { zeroAddress, getContract, encodeFunctionData } from 'viem'; +import { + zeroAddress, + getContract, + encodeFunctionData, + decodeEventLog, + getAbiItem, + getEventSelector, +} from 'viem'; + import type { Address, GetContractReturnType, - PublicClient, WalletClient, Hash, WriteContractParameters, + TransactionReceipt, } from 'viem'; import { @@ -13,7 +21,7 @@ import { type PopulatedTransaction, TransactionOptions, } from '../core/index.js'; -import { ERROR_CODE } from '../common/utils/sdk-error.js'; +import { ERROR_CODE, invariant } from '../common/utils/sdk-error.js'; import { Logger, Cache, ErrorHandler } from '../common/decorators/index.js'; import { SUBMIT_EXTRA_GAS_TRANSACTION_RATIO, @@ -23,16 +31,25 @@ import { } from '../common/constants.js'; import { parseValue } from '../common/utils/parse-value.js'; -import { StethAbi } from './abi/steth.js'; +import { StethAbi, StethEventsPartialAbi } from './abi/steth.js'; import type { StakeProps, StakeEncodeDataProps, StakeInnerProps, StakeLimitResult, + StakeResult, } from './types.js'; import { LidoSDKModule } from '../common/class-primitives/sdk-module.js'; +import { addressEqual } from '../common/utils/address-equal.js'; export class LidoSDKStake extends LidoSDKModule { + // Precomputed event signatures + private static TRANSFER_SIGNATURE = getEventSelector( + getAbiItem({ abi: StethEventsPartialAbi, name: 'Transfer' }), + ); + private static TRANSFER_SHARES_SIGNATURE = getEventSelector( + getAbiItem({ abi: StethEventsPartialAbi, name: 'TransferShares' }), + ); // Contracts @Logger('Contracts:') @@ -44,15 +61,17 @@ export class LidoSDKStake extends LidoSDKModule { @Logger('Contracts:') @Cache(30 * 60 * 1000, ['core.chain.id', 'contractAddressStETH']) public async getContractStETH(): Promise< - GetContractReturnType + GetContractReturnType > { const address = await this.contractAddressStETH(); return getContract({ address, abi: StethAbi, - publicClient: this.core.rpcProvider, - walletClient: this.core.web3Provider, + client: { + public: this.core.rpcProvider, + wallet: this.core.web3Provider as WalletClient, + }, }); } @@ -60,21 +79,25 @@ export class LidoSDKStake extends LidoSDKModule { @Logger('Call:') @ErrorHandler() - public async stakeEth(props: StakeProps): Promise { + public async stakeEth( + props: StakeProps, + ): Promise> { this.core.useWeb3Provider(); - const { callback, account, referralAddress, value } = - this.parseProps(props); + const { callback, account, referralAddress, value, ...rest } = + await this.parseProps(props); await this.validateStakeLimit(value); - + const { address } = await this.core.useAccount(account); const contract = await this.getContractStETH(); - return this.core.performTransaction({ + return this.core.performTransaction({ + ...rest, callback, account, getGasLimit: async (options) => this.submitGasLimit(value, referralAddress, options), sendTransaction: (options) => contract.write.submit([referralAddress], { ...options, value }), + decodeResult: async (receipt) => this.submitParseEvents(receipt, address), }); } @@ -83,15 +106,10 @@ export class LidoSDKStake extends LidoSDKModule { public async stakeEthSimulateTx( props: StakeProps, ): Promise { - const { referralAddress, value, account } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); - const address = await this.contractAddressStETH(); - const { request } = await this.core.rpcProvider.simulateContract({ - address, - abi: StethAbi, - functionName: 'submit', - account: accountAddress, - args: [referralAddress], + const { referralAddress, value, account } = await this.parseProps(props); + const contract = await this.getContractStETH(); + const { request } = await contract.simulate.submit([referralAddress], { + account, value: value, }); @@ -149,6 +167,50 @@ export class LidoSDKStake extends LidoSDKModule { return gasLimit; } + @Logger('Utils:') + private submitParseEvents( + receipt: TransactionReceipt, + address: Address, + ): StakeResult { + let stethReceived: bigint | undefined; + let sharesReceived: bigint | undefined; + for (const log of receipt.logs) { + // skips non-relevant events + if ( + log.topics[0] !== LidoSDKStake.TRANSFER_SIGNATURE && + log.topics[0] !== LidoSDKStake.TRANSFER_SHARES_SIGNATURE + ) + continue; + const parsedLog = decodeEventLog({ + abi: StethEventsPartialAbi, + strict: true, + ...log, + }); + if ( + parsedLog.eventName === 'Transfer' && + addressEqual(parsedLog.args.to, address) + ) { + stethReceived = parsedLog.args.value; + } else if ( + parsedLog.eventName === 'TransferShares' && + addressEqual(parsedLog.args.to, address) + ) { + sharesReceived = parsedLog.args.sharesValue; + } + } + invariant( + stethReceived, + 'could not find Transfer event in stake transaction', + ERROR_CODE.TRANSACTION_ERROR, + ); + invariant( + sharesReceived, + 'could not find TransferShares event in stake transaction', + ERROR_CODE.TRANSACTION_ERROR, + ); + return { sharesReceived, stethReceived }; + } + @Logger('Utils:') private async validateStakeLimit(value: bigint): Promise { const { currentStakeLimit } = await this.getStakeLimitInfo(); @@ -164,7 +226,6 @@ export class LidoSDKStake extends LidoSDKModule { @Logger('Utils:') private stakeEthEncodeData(props: StakeEncodeDataProps): Hash { const { referralAddress = zeroAddress } = props; - return encodeFunctionData({ abi: StethAbi, functionName: 'submit', @@ -176,22 +237,21 @@ export class LidoSDKStake extends LidoSDKModule { public async stakeEthPopulateTx( props: StakeProps, ): Promise { - const { referralAddress, value, account } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); + const { referralAddress, value, account } = await this.parseProps(props); const data = this.stakeEthEncodeData({ referralAddress }); const address = await this.contractAddressStETH(); - return { to: address, - from: accountAddress, + from: account.address, value, data, }; } - private parseProps(props: StakeProps): StakeInnerProps { + private async parseProps(props: StakeProps): Promise { return { ...props, + account: await this.core.useAccount(props.account), referralAddress: props.referralAddress ?? zeroAddress, value: parseValue(props.value), callback: props.callback ?? NOOP, diff --git a/packages/sdk/src/stake/types.ts b/packages/sdk/src/stake/types.ts index 2c80d5e0..11d34545 100644 --- a/packages/sdk/src/stake/types.ts +++ b/packages/sdk/src/stake/types.ts @@ -1,4 +1,4 @@ -import { type Address, type Hash, type TransactionReceipt } from 'viem'; +import type { Address, JsonRpcAccount } from 'viem'; import { CommonTransactionProps } from '../core/types.js'; import { EtherValue } from '../core/types.js'; @@ -10,12 +10,12 @@ export type StakeProps = CommonTransactionProps & { export type StakeInnerProps = CommonTransactionProps & { value: bigint; referralAddress: Address; + account: JsonRpcAccount; }; export type StakeResult = { - hash: Hash; - receipt?: TransactionReceipt; - confirmations?: bigint; + stethReceived: bigint; + sharesReceived: bigint; }; export type StakeEncodeDataProps = { diff --git a/packages/sdk/src/unsteth/index.ts b/packages/sdk/src/unsteth/index.ts index a3ae26af..4d0f8a4b 100644 --- a/packages/sdk/src/unsteth/index.ts +++ b/packages/sdk/src/unsteth/index.ts @@ -1,10 +1,10 @@ export { LidoSDKUnstETH } from './unsteth.js'; -export { - type UnstethNFT, - type UnstethTransferProps, - type UnstethApproveAllProps, - type UnstethApproveProps, - type UnstethApprovedForProps, - type UnstethIsApprovedForAllProps, - type UnstethNFTstatus, +export type { + UnstethNFT, + UnstethTransferProps, + UnstethApproveAllProps, + UnstethApproveProps, + UnstethApprovedForProps, + UnstethIsApprovedForAllProps, + UnstethNFTstatus, } from './types.js'; diff --git a/packages/sdk/src/unsteth/types.ts b/packages/sdk/src/unsteth/types.ts index 73c28160..5b23f81d 100644 --- a/packages/sdk/src/unsteth/types.ts +++ b/packages/sdk/src/unsteth/types.ts @@ -1,9 +1,19 @@ -import { type Hash, type Address, type ContractFunctionResult } from 'viem'; -import { TransactionCallback, CommonTransactionProps } from '../core/types.js'; -import { type unstethAbi } from './abi/unsteth-abi.js'; - -export type UnstethNFTstatus = ContractFunctionResult< +import type { + Hash, + Address, + ContractFunctionReturnType, + JsonRpcAccount, +} from 'viem'; +import type { + TransactionCallback, + CommonTransactionProps, + AccountValue, +} from '../core/types.js'; +import type { unstethAbi } from './abi/unsteth-abi.js'; + +export type UnstethNFTstatus = ContractFunctionReturnType< typeof unstethAbi, + 'view', 'getWithdrawalStatus' >[number]; @@ -11,8 +21,8 @@ export type UnstethNFT = { id: bigint } & UnstethNFTstatus; export type ParsedProps = Omit< TProps, - 'callback' -> & { callback: TransactionCallback }; + 'callback' | 'account' +> & { callback: TransactionCallback; account: JsonRpcAccount }; export type UnstethTransferProps = { to: Address; @@ -32,12 +42,12 @@ export type UnstethApproveAllProps = { } & CommonTransactionProps; export type UnstethApprovedForProps = { - account: Address; + account?: AccountValue; id: bigint; }; export type UnstethIsApprovedForAllProps = { - account: Address; + account?: AccountValue; to: Address; }; diff --git a/packages/sdk/src/unsteth/unsteth.ts b/packages/sdk/src/unsteth/unsteth.ts index 8218a0f8..4a0ddfaa 100644 --- a/packages/sdk/src/unsteth/unsteth.ts +++ b/packages/sdk/src/unsteth/unsteth.ts @@ -11,11 +11,14 @@ import type { SafeTransferFromArguments, UnstethTransferProps, } from './types.js'; -import type { NoCallback, PopulatedTransaction } from '../core/types.js'; +import type { + AccountValue, + NoCallback, + PopulatedTransaction, +} from '../core/types.js'; import { type Address, type GetContractReturnType, - type PublicClient, type WalletClient, getContract, zeroAddress, @@ -37,24 +40,29 @@ export class LidoSDKUnstETH extends LidoSDKModule { @Logger('Contracts:') @Cache(30 * 60 * 1000, ['core.chain.id', 'contractAddressWstETH']) public async getContract(): Promise< - GetContractReturnType + GetContractReturnType > { const address = await this.contractAddress(); return getContract({ address, abi: unstethAbi, - publicClient: this.core.rpcProvider, - walletClient: this.core.web3Provider, + client: { + public: this.core.rpcProvider, + wallet: this.core.web3Provider as WalletClient, + }, }); } // Balance @Logger('Balances:') @ErrorHandler() - public async getNFTsByAccount(account: Address): Promise { + public async getNFTsByAccount(account?: AccountValue): Promise { + const parsedAccount = await this.core.useAccount(account); const contract = await this.getContract(); - const ids = await contract.read.getWithdrawalRequests([account]); + const ids = await contract.read.getWithdrawalRequests([ + parsedAccount.address, + ]); const statuses = await contract.read.getWithdrawalStatus([ids]); return ids.map((id, index) => ({ ...statuses[index], id }) as UnstethNFT); @@ -80,14 +88,16 @@ export class LidoSDKUnstETH extends LidoSDKModule { to, from: _from, data, - } = this.parseProps(props); - const from = _from ?? (await this.core.getWeb3Address(account)); + ...rest + } = await this.parseProps(props); + const from = _from ?? account.address; const args = ( data ? [from, to, id, data] : [from, to, id] ) as SafeTransferFromArguments; const contract = await this.getContract(); return this.core.performTransaction({ + ...rest, callback, account, getGasLimit: (options) => @@ -100,15 +110,14 @@ export class LidoSDKUnstETH extends LidoSDKModule { @Logger('Utils:') @ErrorHandler() public async transferSimulateTx(props: NoCallback) { - const { account, id, to, from: _from, data } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); - const from = _from ?? accountAddress; + const { account, id, to, from: _from, data } = await this.parseProps(props); + const from = _from ?? account.address; const args = ( data ? [from, to, id, data] : [from, to, id] ) as SafeTransferFromArguments; const contract = await this.getContract(); return contract.simulate.safeTransferFrom(args, { - account: accountAddress, + account, }); } @@ -117,15 +126,14 @@ export class LidoSDKUnstETH extends LidoSDKModule { public async transferPopulateTx( props: NoCallback, ): Promise { - const { account, id, to, from: _from, data } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); - const from = _from ?? accountAddress; + const { account, id, to, from: _from, data } = await this.parseProps(props); + const from = _from ?? account.address; const args = ( data ? [from, to, id, data] : [from, to, id] ) as SafeTransferFromArguments; const contract = await this.getContract(); return { - from: accountAddress, + from: account.address, to: contract.address, data: encodeFunctionData({ abi: contract.abi, @@ -141,9 +149,10 @@ export class LidoSDKUnstETH extends LidoSDKModule { @ErrorHandler() public async getSingleTokenApproval({ id, - account, + account: accountProp, }: UnstethApprovedForProps) { const contract = await this.getContract(); + const account = await this.core.useAccount(accountProp); return contract.read.getApproved([id], { account }); } @@ -152,10 +161,17 @@ export class LidoSDKUnstETH extends LidoSDKModule { public async setSingleTokenApproval( props: UnstethApproveProps, ): Promise { - const { account, callback, to = zeroAddress, id } = this.parseProps(props); + const { + account, + callback, + to = zeroAddress, + id, + ...rest + } = await this.parseProps(props); const args = [to, id] as const; const contract = await this.getContract(); return this.core.performTransaction({ + ...rest, callback, account, getGasLimit: (options) => contract.estimateGas.approve(args, options), @@ -168,12 +184,11 @@ export class LidoSDKUnstETH extends LidoSDKModule { public async setSingleTokenApprovalPopulateTx( props: NoCallback, ): Promise { - const { account, to = zeroAddress, id } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); + const { account, to = zeroAddress, id } = await this.parseProps(props); const args = [to, id] as const; const contract = await this.getContract(); return { - from: accountAddress, + from: account.address, to: contract.address, data: encodeFunctionData({ abi: contract.abi, @@ -188,12 +203,11 @@ export class LidoSDKUnstETH extends LidoSDKModule { public async setSingleTokenApprovalSimulateTx( props: NoCallback, ) { - const { account, to = zeroAddress, id } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); + const { account, to = zeroAddress, id } = await this.parseProps(props); const args = [to, id] as const; const contract = await this.getContract(); return contract.simulate.approve(args, { - account: accountAddress, + account, }); } @@ -204,8 +218,9 @@ export class LidoSDKUnstETH extends LidoSDKModule { account, to, }: UnstethIsApprovedForAllProps) { + const parsedAccount = await this.core.useAccount(account); const contract = await this.getContract(); - return contract.read.isApprovedForAll([account, to]); + return contract.read.isApprovedForAll([parsedAccount.address, to]); } @Logger('Call:') @@ -213,10 +228,12 @@ export class LidoSDKUnstETH extends LidoSDKModule { public async setAllTokensApproval( props: UnstethApproveAllProps, ): Promise { - const { account, callback, to, allow } = this.parseProps(props); + const { account, callback, to, allow, ...rest } = + await this.parseProps(props); const args = [to, allow] as const; const contract = await this.getContract(); return this.core.performTransaction({ + ...rest, callback, account, getGasLimit: (options) => @@ -231,12 +248,11 @@ export class LidoSDKUnstETH extends LidoSDKModule { public async setAllTokensApprovalPopulateTx( props: NoCallback, ): Promise { - const { account, to, allow } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); + const { account, to, allow } = await this.parseProps(props); const args = [to, allow] as const; const contract = await this.getContract(); return { - from: accountAddress, + from: account.address, to: contract.address, data: encodeFunctionData({ abi: contract.abi, @@ -251,12 +267,11 @@ export class LidoSDKUnstETH extends LidoSDKModule { public async setAllTokensApprovalSimulateTx( props: NoCallback, ) { - const { account, to, allow } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); + const { account, to, allow } = await this.parseProps(props); const args = [to, allow] as const; const contract = await this.getContract(); return contract.simulate.setApprovalForAll(args, { - account: accountAddress, + account, }); } @@ -305,12 +320,13 @@ export class LidoSDKUnstETH extends LidoSDKModule { return contract.read.tokenURI([id]); } - private parseProps( + private async parseProps( props: TProps, - ): ParsedProps { + ): Promise> { return { ...props, callback: props.callback ?? NOOP, + account: await this.core.useAccount(props.account), }; } } diff --git a/packages/sdk/src/withdraw/__test__/withdraw-claim.test.ts b/packages/sdk/src/withdraw/__test__/withdraw-claim.test.ts index 6822cef5..5c6da7a8 100644 --- a/packages/sdk/src/withdraw/__test__/withdraw-claim.test.ts +++ b/packages/sdk/src/withdraw/__test__/withdraw-claim.test.ts @@ -78,5 +78,15 @@ describe('withdraw request claim', () => { const balanceAfter = await core.balanceETH(address); expect(balanceAfter - balanceBefore).toEqual(claimableETH - ethForGas); await expect(unsteth.getAccountByNFT(request.id)).rejects.toBeDefined(); + + expect(tx.result).toBeDefined(); + expect(tx.result?.requests).toHaveLength(1); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const claimedRequest = tx.result!.requests[0]!; + + expectAddress(claimedRequest.owner, address); + expectAddress(claimedRequest.receiver, address); + expect(claimedRequest.requestId).toEqual(request.id); + expect(claimedRequest.amountOfETH).toEqual(claimableETH); }); }); diff --git a/packages/sdk/src/withdraw/__test__/withdraw-request.test.ts b/packages/sdk/src/withdraw/__test__/withdraw-request.test.ts index 8a96534e..3eda2ee0 100644 --- a/packages/sdk/src/withdraw/__test__/withdraw-request.test.ts +++ b/packages/sdk/src/withdraw/__test__/withdraw-request.test.ts @@ -1,6 +1,9 @@ import { expect, describe, jest, beforeAll, test } from '@jest/globals'; import { useWithdraw } from '../../../tests/utils/fixtures/use-withdraw.js'; -import { expectAlmostEqualBn } from '../../../tests/utils/expect/expect-bn.js'; +import { + expectAlmostEqualBn, + expectPositiveBn, +} from '../../../tests/utils/expect/expect-bn.js'; import { Address } from 'viem'; import { useWrap } from '../../../tests/utils/fixtures/use-wrap.js'; import { useAccount } from '../../../tests/utils/fixtures/use-wallet-client.js'; @@ -277,6 +280,17 @@ const testWithdrawals = (token: WithdrawableTokens, ethAmount: bigint) => { const nftsAfter = await unsteth.getNFTsByAccount(address); expect(nftsAfter.length - nftsBefore.length).toBe(requestsAmounts.length); + + expect(tx.result).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const result = tx.result!; + expect(result.requests).toHaveLength(nftsAfter.length - nftsBefore.length); + for (const request of result.requests) { + expectAddress(request.owner, address); + expectAddress(request.requestor, address); + expectPositiveBn(request.amountOfStETH); + expectPositiveBn(request.amountOfShares); + } }); }; diff --git a/packages/sdk/src/withdraw/abi/withdrawalQueue.ts b/packages/sdk/src/withdraw/abi/withdrawalQueue.ts index b737170c..83b9978a 100644 --- a/packages/sdk/src/withdraw/abi/withdrawalQueue.ts +++ b/packages/sdk/src/withdraw/abi/withdrawalQueue.ts @@ -146,7 +146,7 @@ export const WithdrawalQueueAbi = [ type: 'address', }, { - indexed: true, + indexed: false, internalType: 'uint256', name: 'tokenId', type: 'uint256', @@ -353,7 +353,7 @@ export const WithdrawalQueueAbi = [ { indexed: true, internalType: 'address', name: 'from', type: 'address' }, { indexed: true, internalType: 'address', name: 'to', type: 'address' }, { - indexed: true, + indexed: false, internalType: 'uint256', name: 'tokenId', type: 'uint256', @@ -1120,3 +1120,74 @@ export const WithdrawalQueueAbi = [ type: 'function', }, ] as const; + +export const PartialWithdrawalQueueEventsAbi = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint256', + name: 'requestId', + type: 'uint256', + }, + { + indexed: true, + internalType: 'address', + name: 'requestor', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amountOfStETH', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amountOfShares', + type: 'uint256', + }, + ], + name: 'WithdrawalRequested', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint256', + name: 'requestId', + type: 'uint256', + }, + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'receiver', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amountOfETH', + type: 'uint256', + }, + ], + name: 'WithdrawalClaimed', + type: 'event', + }, +] as const; diff --git a/packages/sdk/src/withdraw/claim/claim.ts b/packages/sdk/src/withdraw/claim/claim.ts index 8f5be380..1ff0a5ad 100644 --- a/packages/sdk/src/withdraw/claim/claim.ts +++ b/packages/sdk/src/withdraw/claim/claim.ts @@ -1,4 +1,10 @@ -import { encodeFunctionData, type SimulateContractReturnType } from 'viem'; +import { + encodeFunctionData, + TransactionReceipt, + decodeEventLog, + getAbiItem, + getEventSelector, +} from 'viem'; import { Logger, ErrorHandler } from '../../common/decorators/index.js'; import type { @@ -9,18 +15,31 @@ import type { import { NOOP } from '../../common/constants.js'; import { BusModule } from '../bus-module.js'; -import type { ClaimRequestsProps } from './types.js'; -import { invariantArgument } from '../../index.js'; +import type { + ClaimRequestsProps, + ClaimResult, + ClaimResultEvent, +} from './types.js'; import { bigintComparator } from '../../common/utils/bigint-comparator.js'; +import { invariantArgument } from '../../common/index.js'; +import { PartialWithdrawalQueueEventsAbi } from '../abi/withdrawalQueue.js'; export class LidoSDKWithdrawClaim extends BusModule { + // Precomputed event signatures + private static CLAIM_SIGNATURE = getEventSelector( + getAbiItem({ + abi: PartialWithdrawalQueueEventsAbi, + name: 'WithdrawalClaimed', + }), + ); + // Calls @Logger('Call:') @ErrorHandler() public async claimRequests( props: ClaimRequestsProps, - ): Promise { - const { account, callback = NOOP } = props; + ): Promise> { + const { account, callback = NOOP, ...rest } = props; const { requestsIds, hints } = await this.sortRequestsWithHints( props.requestsIds, props.hints, @@ -28,21 +47,21 @@ export class LidoSDKWithdrawClaim extends BusModule { const params = [requestsIds, hints] as const; const contract = await this.bus.contract.getContractWithdrawalQueue(); return this.bus.core.performTransaction({ + ...rest, account, callback, getGasLimit: (options) => contract.estimateGas.claimWithdrawals(params, options), sendTransaction: (options) => contract.write.claimWithdrawals(params, options), + decodeResult: (receipt) => this.decodeClaimEvents(receipt), }); } @Logger('Views:') @ErrorHandler() - public async claimRequestsSimulateTx( - props: NoCallback, - ): Promise { - const accountAddress = await this.bus.core.getWeb3Address(props.account); + public async claimRequestsSimulateTx(props: NoCallback) { + const account = await this.bus.core.useAccount(props.account); const { requestsIds, hints } = await this.sortRequestsWithHints( props.requestsIds, props.hints, @@ -51,7 +70,7 @@ export class LidoSDKWithdrawClaim extends BusModule { const contract = await this.bus.contract.getContractWithdrawalQueue(); return contract.simulate.claimWithdrawals([requestsIds, hints], { - account: accountAddress, + account, }); } @@ -60,7 +79,7 @@ export class LidoSDKWithdrawClaim extends BusModule { public async claimRequestsPopulateTx( props: NoCallback, ): Promise { - const accountAddress = await this.bus.core.getWeb3Address(props.account); + const account = await this.bus.core.useAccount(props.account); const { requestsIds, hints } = await this.sortRequestsWithHints( props.requestsIds, props.hints, @@ -68,7 +87,7 @@ export class LidoSDKWithdrawClaim extends BusModule { const contract = await this.bus.contract.getContractWithdrawalQueue(); return { - from: accountAddress, + from: account.address, to: contract.address, data: encodeFunctionData({ abi: contract.abi, @@ -110,4 +129,24 @@ export class LidoSDKWithdrawClaim extends BusModule { hints: fetchedHints, }; } + + @Logger('Utils:') + private async decodeClaimEvents( + receipt: TransactionReceipt, + ): Promise { + const requests: ClaimResultEvent[] = []; + for (const log of receipt.logs) { + if (log.topics[0] !== LidoSDKWithdrawClaim.CLAIM_SIGNATURE) continue; + const parsedLog = decodeEventLog({ + // fits both wsteth and steth events + abi: PartialWithdrawalQueueEventsAbi, + strict: true, + ...log, + }); + if (parsedLog.eventName === 'WithdrawalClaimed') { + requests.push({ ...parsedLog.args }); + } + } + return { requests }; + } } diff --git a/packages/sdk/src/withdraw/claim/types.ts b/packages/sdk/src/withdraw/claim/types.ts index ddc76b6d..7624ad8c 100644 --- a/packages/sdk/src/withdraw/claim/types.ts +++ b/packages/sdk/src/withdraw/claim/types.ts @@ -1,6 +1,18 @@ +import type { Address } from 'viem'; import type { CommonTransactionProps } from '../../core/index.js'; export type ClaimRequestsProps = CommonTransactionProps & { requestsIds: readonly bigint[]; hints?: readonly bigint[]; }; + +export type ClaimResultEvent = { + requestId: bigint; + owner: Address; + receiver: Address; + amountOfETH: bigint; +}; + +export type ClaimResult = { + requests: ClaimResultEvent[]; +}; diff --git a/packages/sdk/src/withdraw/index.ts b/packages/sdk/src/withdraw/index.ts index 867372a6..5b2ce111 100644 --- a/packages/sdk/src/withdraw/index.ts +++ b/packages/sdk/src/withdraw/index.ts @@ -1,5 +1,9 @@ export { LidoSDKWithdraw } from './withdraw.js'; -export type { ClaimRequestsProps } from './claim/types.js'; +export type { + ClaimRequestsProps, + ClaimResult, + ClaimResultEvent, +} from './claim/types.js'; export type { RequestProps, RequestWithPermitProps, @@ -9,6 +13,8 @@ export type { GetAllowanceProps, SplitAmountToRequestsProps, PermitWstETHStETHProps, + WithdrawalResult, + WithdrawalEventRequest, } from './request/types.js'; export type { RequestStatusWithId, diff --git a/packages/sdk/src/withdraw/request/approve.ts b/packages/sdk/src/withdraw/request/approve.ts index 5383ab98..92383cb6 100644 --- a/packages/sdk/src/withdraw/request/approve.ts +++ b/packages/sdk/src/withdraw/request/approve.ts @@ -1,4 +1,4 @@ -import { type SimulateContractReturnType, encodeFunctionData } from 'viem'; +import { encodeFunctionData } from 'viem'; import type { NoCallback, @@ -24,7 +24,7 @@ export class LidoSDKWithdrawApprove extends BusModule { props: WithdrawApproveProps, ): Promise { this.bus.core.useWeb3Provider(); - const { account, token, callback = NOOP, amount: _amount } = props; + const { account, token, callback = NOOP, amount: _amount, ...rest } = props; const amount = parseValue(_amount); const addressWithdrawalsQueue = await this.bus.contract.contractAddressWithdrawalQueue(); @@ -39,6 +39,7 @@ export class LidoSDKWithdrawApprove extends BusModule { ) as Awaited>; return this.bus.core.performTransaction({ + ...rest, account, callback, getGasLimit: (options) => @@ -53,10 +54,8 @@ export class LidoSDKWithdrawApprove extends BusModule { @Logger('Views:') @ErrorHandler() - public async approveSimulateTx( - props: NoCallback, - ): Promise { - const accountAddress = await this.bus.core.getWeb3Address(props.account); + public async approveSimulateTx(props: NoCallback) { + const account = await this.bus.core.useAccount(props.account); const { token, amount: _amount } = props; const amount = parseValue(_amount); const isSteth = token === 'stETH'; @@ -71,7 +70,7 @@ export class LidoSDKWithdrawApprove extends BusModule { const result = contract.simulate.approve( [addressWithdrawalsQueue, amount], - { account: accountAddress }, + { account }, ); return result; @@ -82,8 +81,8 @@ export class LidoSDKWithdrawApprove extends BusModule { public async approvePopulateTx( props: NoCallback, ): Promise { - const { token, account, amount: _amount } = props; - const accountAddress = await this.bus.core.getWeb3Address(account); + const { token, amount: _amount } = props; + const account = await this.bus.core.useAccount(props.account); const amount = parseValue(_amount); const isSteth = token === 'stETH'; @@ -96,7 +95,7 @@ export class LidoSDKWithdrawApprove extends BusModule { ) as Awaited>; return { - from: accountAddress, + from: account.address, to: contract.address, data: encodeFunctionData({ abi: contract.abi, @@ -111,10 +110,11 @@ export class LidoSDKWithdrawApprove extends BusModule { @Logger('Utils:') @Cache(30 * 1000, ['bus.core.chain.id']) public async approveGasLimit({ - account, + account: accountProp, token, amount, }: Required>): Promise { + const account = await this.bus.core.useAccount(accountProp); const value = parseValue(amount); const isSteth = token === 'stETH'; let estimateGasMethod; @@ -141,9 +141,10 @@ export class LidoSDKWithdrawApprove extends BusModule { @Logger('Utils:') @ErrorHandler() public async getAllowance({ - account, + account: accountProp, token, }: GetAllowanceProps): Promise { + const account = await this.bus.core.useAccount(accountProp); const isSteth = token === 'stETH'; let allowanceMethod; @@ -159,7 +160,7 @@ export class LidoSDKWithdrawApprove extends BusModule { const allowance = await allowanceMethod.call( this, - [account, addressWithdrawalsQueue], + [account.address, addressWithdrawalsQueue], { account }, ); @@ -170,9 +171,10 @@ export class LidoSDKWithdrawApprove extends BusModule { @ErrorHandler() public async checkAllowance({ amount: _amount, - account, + account: accountProp, token, }: CheckAllowanceProps): Promise { + const account = await this.bus.core.useAccount(accountProp); const amount = parseValue(_amount); const isSteth = token === 'stETH'; let allowanceMethod; @@ -189,7 +191,7 @@ export class LidoSDKWithdrawApprove extends BusModule { const allowance = await allowanceMethod.call( this, - [account, addressWithdrawalsQueue], + [account.address, addressWithdrawalsQueue], { account }, ); const needsApprove = allowance < amount; diff --git a/packages/sdk/src/withdraw/request/request.ts b/packages/sdk/src/withdraw/request/request.ts index ab6675a2..591e5a81 100644 --- a/packages/sdk/src/withdraw/request/request.ts +++ b/packages/sdk/src/withdraw/request/request.ts @@ -17,6 +17,8 @@ import type { SignedPermit, SplitAmountToRequestsProps, RequirePermit, + WithdrawalResult, + WithdrawalEventRequest, } from './types.js'; import { invariant, @@ -24,13 +26,25 @@ import { ERROR_CODE, } from '../../common/utils/sdk-error.js'; import { - SimulateContractReturnType, + TransactionReceipt, + decodeEventLog, encodeFunctionData, formatEther, + getAbiItem, + getEventSelector, } from 'viem'; import { parseValue } from '../../common/utils/parse-value.js'; +import { PartialWithdrawalQueueEventsAbi } from '../abi/withdrawalQueue.js'; export class LidoSDKWithdrawRequest extends BusModule { + // Precomputed event signatures + private static WITHDRAW_SIGNATURE = getEventSelector( + getAbiItem({ + abi: PartialWithdrawalQueueEventsAbi, + name: 'WithdrawalRequested', + }), + ); + @Logger('Views:') public async splitAmountToRequests({ amount: _amount, @@ -76,13 +90,13 @@ export class LidoSDKWithdrawRequest extends BusModule { @ErrorHandler('Error:') public async requestWithdrawal( props: RequestProps, - ): Promise { - const accountAddress = await this.bus.core.getWeb3Address(props.account); + ): Promise> { + const account = await this.bus.core.useAccount(props.account); const { - account, token, - receiver = accountAddress, + receiver = account.address, callback = NOOP, + ...rest } = props; const requests = @@ -106,22 +120,22 @@ export class LidoSDKWithdrawRequest extends BusModule { ); return this.bus.core.performTransaction({ + ...rest, account, callback, getGasLimit, sendTransaction, + decodeResult: (receipt) => this.decodeWithdrawEvents(receipt), }); } @Logger('Views:') @ErrorHandler() - public async requestWithdrawalSimulateTx( - props: NoCallback, - ): Promise { - const accountAddress = await this.bus.core.getWeb3Address(props.account); + public async requestWithdrawalSimulateTx(props: NoCallback) { + const account = await this.bus.core.useAccount(props.account); const { token, - receiver = accountAddress, + receiver = account.address, amount = 0n, requests: _requests, } = props; @@ -132,9 +146,9 @@ export class LidoSDKWithdrawRequest extends BusModule { const args = [requests, receiver] as const; const result = await (isSteth - ? contract.simulate.requestWithdrawals(args, { account: accountAddress }) + ? contract.simulate.requestWithdrawals(args, { account }) : contract.simulate.requestWithdrawalsWstETH(args, { - account: accountAddress, + account, })); return result; @@ -145,10 +159,10 @@ export class LidoSDKWithdrawRequest extends BusModule { public async requestWithdrawalPopulateTx( props: NoCallback, ): Promise { - const accountAddress = await this.bus.core.getWeb3Address(props.account); + const account = await this.bus.core.useAccount(props.account); const { token, - receiver = accountAddress, + receiver = account.address, amount = 0n, requests: _requests, } = props; @@ -163,7 +177,7 @@ export class LidoSDKWithdrawRequest extends BusModule { : ('requestWithdrawalsWstETH' as const); return { - from: accountAddress, + from: account.address, to: contract.address, data: encodeFunctionData({ abi: contract.abi, @@ -177,14 +191,14 @@ export class LidoSDKWithdrawRequest extends BusModule { @ErrorHandler('Error:') public async requestWithdrawalWithPermit( props: RequestWithPermitProps, - ): Promise { - const accountAddress = await this.bus.core.getWeb3Address(props.account); + ): Promise> { + const account = await this.bus.core.useAccount(props.account); const { - account, token, - receiver = accountAddress, + receiver = account.address, callback = NOOP, permit: permitProp, + ...rest } = props; const requests = props.requests ?? (await this.splitAmountToRequests(props)); @@ -196,7 +210,7 @@ export class LidoSDKWithdrawRequest extends BusModule { permit = permitProp; } else { callback({ stage: TransactionCallbackStage.PERMIT }); - const isContract = await this.bus.core.isContract(accountAddress); + const isContract = await this.bus.core.isContract(account.address); invariant( !isContract, 'Cannot sign permit for contract', @@ -240,10 +254,12 @@ export class LidoSDKWithdrawRequest extends BusModule { ); return this.bus.core.performTransaction({ + ...rest, account, callback, getGasLimit, sendTransaction, + decodeResult: (receipt) => this.decodeWithdrawEvents(receipt), }); } @@ -251,11 +267,11 @@ export class LidoSDKWithdrawRequest extends BusModule { @ErrorHandler() public async requestWithdrawalWithPermitSimulateTx( props: NoCallback>, - ): Promise { - const accountAddress = await this.bus.core.getWeb3Address(props.account); + ) { + const account = await this.bus.core.useAccount(props.account); const { token, - receiver = accountAddress, + receiver = account.address, permit, amount = 0n, requests: _requests, @@ -268,10 +284,10 @@ export class LidoSDKWithdrawRequest extends BusModule { const args = [requests, receiver, permit] as const; const result = await (isSteth ? contract.simulate.requestWithdrawalsWithPermit(args, { - account: accountAddress, + account, }) : contract.simulate.requestWithdrawalsWstETHWithPermit(args, { - account: accountAddress, + account, })); return result; @@ -282,10 +298,10 @@ export class LidoSDKWithdrawRequest extends BusModule { public async requestWithdrawalWithPermitPopulateTx( props: NoCallback>, ): Promise { - const accountAddress = await this.bus.core.getWeb3Address(props.account); + const account = await this.bus.core.useAccount(props.account); const { token, - receiver = accountAddress, + receiver = account.address, permit, amount = 0n, requests: _requests, @@ -301,7 +317,7 @@ export class LidoSDKWithdrawRequest extends BusModule { : ('requestWithdrawalsWstETHWithPermit' as const); return { - from: accountAddress, + from: account.address, to: contract.address, data: encodeFunctionData({ abi: contract.abi, @@ -310,4 +326,24 @@ export class LidoSDKWithdrawRequest extends BusModule { }), }; } + + @Logger('Utils:') + private async decodeWithdrawEvents( + receipt: TransactionReceipt, + ): Promise { + const requests: WithdrawalEventRequest[] = []; + for (const log of receipt.logs) { + if (log.topics[0] !== LidoSDKWithdrawRequest.WITHDRAW_SIGNATURE) continue; + const parsedLog = decodeEventLog({ + // lightweight abi + abi: PartialWithdrawalQueueEventsAbi, + strict: true, + ...log, + }); + if (parsedLog.eventName === 'WithdrawalRequested') { + requests.push({ ...parsedLog.args }); + } + } + return { requests }; + } } diff --git a/packages/sdk/src/withdraw/request/types.ts b/packages/sdk/src/withdraw/request/types.ts index 7ec68aaf..d11a9cfe 100644 --- a/packages/sdk/src/withdraw/request/types.ts +++ b/packages/sdk/src/withdraw/request/types.ts @@ -1,6 +1,10 @@ import type { Hash, Address } from 'viem'; -import type { CommonTransactionProps, EtherValue } from '../../core/index.js'; +import type { + AccountValue, + CommonTransactionProps, + EtherValue, +} from '../../core/index.js'; import { LIDO_TOKENS } from '../../common/constants.js'; export type WithdrawableTokens = @@ -59,7 +63,7 @@ export type WithdrawApproveProps = CommonTransactionProps & { }; export type GetAllowanceProps = { - account: Address; + account?: AccountValue; token: WithdrawableTokens; }; @@ -71,3 +75,15 @@ export type CheckAllowanceResult = { allowance: bigint; needsApprove: boolean; }; + +export type WithdrawalEventRequest = { + requestId: bigint; + requestor: Address; + owner: Address; + amountOfStETH: bigint; + amountOfShares: bigint; +}; + +export type WithdrawalResult = { + requests: WithdrawalEventRequest[]; +}; diff --git a/packages/sdk/src/withdraw/types.ts b/packages/sdk/src/withdraw/types.ts index 8ccd87b1..3da0479d 100644 --- a/packages/sdk/src/withdraw/types.ts +++ b/packages/sdk/src/withdraw/types.ts @@ -25,7 +25,7 @@ export type RequestStatusWithId = { }; export type PropsWithAccount = { - account: AccountValue; + account?: AccountValue; }; export type GetPendingRequestsInfoReturnType = { diff --git a/packages/sdk/src/withdraw/withdraw-contract.ts b/packages/sdk/src/withdraw/withdraw-contract.ts index 0103fb75..61ea3c29 100644 --- a/packages/sdk/src/withdraw/withdraw-contract.ts +++ b/packages/sdk/src/withdraw/withdraw-contract.ts @@ -1,10 +1,5 @@ import { getContract } from 'viem'; -import { - type Address, - type GetContractReturnType, - type PublicClient, - type WalletClient, -} from 'viem'; +import type { Address, GetContractReturnType, WalletClient } from 'viem'; import { Logger, Cache } from '../common/decorators/index.js'; import { LIDO_CONTRACT_NAMES } from '../common/constants.js'; @@ -30,15 +25,17 @@ export class LidoSDKWithdrawContract extends BusModule { 'contractAddressWithdrawalQueue', ]) public async getContractWithdrawalQueue(): Promise< - GetContractReturnType + GetContractReturnType > { const address = await this.contractAddressWithdrawalQueue(); return getContract({ address, abi: WithdrawalQueueAbi, - publicClient: this.bus.core.rpcProvider, - walletClient: this.bus.core.web3Provider, + client: { + public: this.bus.core.rpcProvider, + wallet: this.bus.core.web3Provider as WalletClient, + }, }); } @@ -51,15 +48,17 @@ export class LidoSDKWithdrawContract extends BusModule { @Logger('Contracts:') @Cache(30 * 60 * 1000, ['bus.core.chain.id', 'contractAddressStETH']) public async getContractStETH(): Promise< - GetContractReturnType + GetContractReturnType > { const address = await this.contractAddressStETH(); return getContract({ address, abi: PartStethAbi, - publicClient: this.bus.core.rpcProvider, - walletClient: this.bus.core.web3Provider, + client: { + public: this.bus.core.rpcProvider, + wallet: this.bus.core.web3Provider as WalletClient, + }, }); } @@ -72,15 +71,17 @@ export class LidoSDKWithdrawContract extends BusModule { @Logger('Contracts:') @Cache(30 * 60 * 1000, ['bus.core.chain.id', 'contractAddressWstETH']) public async getContractWstETH(): Promise< - GetContractReturnType + GetContractReturnType > { const address = await this.contractAddressWstETH(); return getContract({ address, abi: PartWstethAbi, - publicClient: this.bus.core.rpcProvider, - walletClient: this.bus.core.web3Provider, + client: { + public: this.bus.core.rpcProvider, + wallet: this.bus.core.web3Provider as WalletClient, + }, }); } } diff --git a/packages/sdk/src/withdraw/withdraw-requests-info.ts b/packages/sdk/src/withdraw/withdraw-requests-info.ts index 94df9b41..f6147c27 100644 --- a/packages/sdk/src/withdraw/withdraw-requests-info.ts +++ b/packages/sdk/src/withdraw/withdraw-requests-info.ts @@ -35,8 +35,9 @@ export class LidoSDKWithdrawRequestsInfo extends BusModule { public async getWithdrawalRequestsStatus( props: PropsWithAccount, ): Promise { + const account = await this.bus.core.useAccount(props.account); const requestsIds = await this.bus.views.getWithdrawalRequestsIds({ - account: await this.bus.core.getWeb3Address(props.account), + account, }); return this.bus.views.getWithdrawalStatus({ requestsIds }); diff --git a/packages/sdk/src/withdraw/withdraw-views.ts b/packages/sdk/src/withdraw/withdraw-views.ts index ca6907e0..789e2d81 100644 --- a/packages/sdk/src/withdraw/withdraw-views.ts +++ b/packages/sdk/src/withdraw/withdraw-views.ts @@ -1,26 +1,24 @@ -import { type Address } from 'viem'; - import { Logger, Cache } from '../common/decorators/index.js'; import { BusModule } from './bus-module.js'; import { type RequestStatusWithId } from './types.js'; -import { invariantArgument } from '../index.js'; +import { type AccountValue } from '../core/index.js'; +import { invariantArgument } from '../common/index.js'; export class LidoSDKWithdrawViews extends BusModule { // Views @Logger('Views:') public async getWithdrawalRequestsIds(props: { - account: Address; + account?: AccountValue; }): Promise { const contract = await this.bus.contract.getContractWithdrawalQueue(); - - return contract.read.getWithdrawalRequests([props.account]); + const parsedAccount = await this.bus.core.useAccount(props.account); + return contract.read.getWithdrawalRequests([parsedAccount.address]); } @Logger('Views:') public async getLastCheckpointIndex(): Promise { const contract = await this.bus.contract.getContractWithdrawalQueue(); - return contract.read.getLastCheckpointIndex(); } diff --git a/packages/sdk/src/wrap/__test__/wrap-wallet.test.ts b/packages/sdk/src/wrap/__test__/wrap-wallet.test.ts index 7489a249..2b0916ec 100644 --- a/packages/sdk/src/wrap/__test__/wrap-wallet.test.ts +++ b/packages/sdk/src/wrap/__test__/wrap-wallet.test.ts @@ -84,8 +84,17 @@ describe('LidoSDKWrap wallet methods', () => { const stethBalanceAfter = await steth.balance(address); const wstethBalanceAfter = await wsteth.balance(address); - expectAlmostEqualBn(stethBalanceAfter - stethBalanceBefore, -value); - expectAlmostEqualBn(wstethBalanceAfter - wstethBalanceBefore, wstethValue); + const stethDiff = stethBalanceAfter - stethBalanceBefore; + const wstethDiff = wstethBalanceAfter - wstethBalanceBefore; + + expectAlmostEqualBn(stethDiff, -value); + expectAlmostEqualBn(wstethDiff, wstethValue); + + expect(tx.result).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const result = tx.result!; + expectAlmostEqualBn(result.stethWrapped, -stethDiff); + expect(result.wstethReceived).toEqual(wstethDiff); await expect(wrap.getStethForWrapAllowance(address)).resolves.toEqual(0n); }); @@ -118,7 +127,16 @@ describe('LidoSDKWrap wallet methods', () => { const stethBalanceAfter = await steth.balance(address); const wstethBalanceAfter = await wsteth.balance(address); - expectAlmostEqualBn(stethBalanceAfter - stethBalanceBefore, value); - expectAlmostEqualBn(wstethBalanceAfter - wstethBalanceBefore, -wstethValue); + const stethDiff = stethBalanceAfter - stethBalanceBefore; + const wstethDiff = wstethBalanceAfter - wstethBalanceBefore; + + expectAlmostEqualBn(stethDiff, value); + expectAlmostEqualBn(wstethDiff, -wstethValue); + + expect(tx.result).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const result = tx.result!; + expectAlmostEqualBn(result.stethReceived, stethDiff); + expect(result.wstethUnwrapped).toEqual(-wstethDiff); }); }); diff --git a/packages/sdk/src/wrap/abi/steth-partial.ts b/packages/sdk/src/wrap/abi/steth-partial.ts index 1c861137..73579433 100644 --- a/packages/sdk/src/wrap/abi/steth-partial.ts +++ b/packages/sdk/src/wrap/abi/steth-partial.ts @@ -40,4 +40,57 @@ export const stethPartialAbi = [ stateMutability: 'view', type: 'function', }, + { + anonymous: false, + inputs: [ + { indexed: true, name: 'from', type: 'address' }, + { indexed: true, name: 'to', type: 'address' }, + { indexed: false, name: 'value', type: 'uint256' }, + ], + name: 'Transfer', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, name: 'owner', type: 'address' }, + { indexed: true, name: 'spender', type: 'address' }, + { indexed: false, name: 'value', type: 'uint256' }, + ], + name: 'Approval', + type: 'event', + }, + { + 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: 'sender', type: 'address' }, + { indexed: false, name: 'amount', type: 'uint256' }, + { indexed: false, name: 'referral', type: 'address' }, + ], + name: 'Submitted', + type: 'event', + }, +] as const; + +export const PartialTransferEventAbi = [ + { + anonymous: false, + inputs: [ + { indexed: true, name: 'from', type: 'address' }, + { indexed: true, name: 'to', type: 'address' }, + { indexed: false, name: 'value', type: 'uint256' }, + ], + name: 'Transfer', + type: 'event', + }, ] as const; diff --git a/packages/sdk/src/wrap/types.ts b/packages/sdk/src/wrap/types.ts index 4f472004..181dd492 100644 --- a/packages/sdk/src/wrap/types.ts +++ b/packages/sdk/src/wrap/types.ts @@ -1,14 +1,25 @@ -import type { FormattedTransactionRequest } from 'viem'; +import type { FormattedTransactionRequest, JsonRpcAccount } from 'viem'; import type { EtherValue, CommonTransactionProps } from '../core/types.js'; export type WrapProps = CommonTransactionProps & { value: EtherValue; }; +export type WrapResults = { + stethWrapped: bigint; + wstethReceived: bigint; +}; + +export type UnwrapResults = { + wstethUnwrapped: bigint; + stethReceived: bigint; +}; + export type WrapPropsWithoutCallback = Omit; -export type WrapInnerProps = CommonTransactionProps & { +export type WrapInnerProps = Omit & { value: bigint; + account: JsonRpcAccount; }; export type PopulatedTx = Omit; diff --git a/packages/sdk/src/wrap/wrap.ts b/packages/sdk/src/wrap/wrap.ts index 3d59c2e1..e72f1016 100644 --- a/packages/sdk/src/wrap/wrap.ts +++ b/packages/sdk/src/wrap/wrap.ts @@ -3,10 +3,13 @@ import { encodeFunctionData, type Address, type GetContractReturnType, - type PublicClient, type WalletClient, type FormattedTransactionRequest, type WriteContractParameters, + TransactionReceipt, + decodeEventLog, + getAbiItem, + getEventSelector, } from 'viem'; import { LIDO_CONTRACT_NAMES, NOOP } from '../common/constants.js'; @@ -23,14 +26,24 @@ import type { WrapProps, WrapInnerProps, WrapPropsWithoutCallback, + WrapResults, + UnwrapResults, } from './types.js'; import { abi } from './abi/wsteth.js'; -import { stethPartialAbi } from './abi/steth-partial.js'; -import { ERROR_CODE } from '../common/utils/sdk-error.js'; +import { + stethPartialAbi, + PartialTransferEventAbi, +} from './abi/steth-partial.js'; +import { ERROR_CODE, invariant } from '../common/utils/sdk-error.js'; import { LidoSDKModule } from '../common/class-primitives/sdk-module.js'; +import { addressEqual } from '../common/utils/address-equal.js'; export class LidoSDKWrap extends LidoSDKModule { + private static TRANSFER_SIGNATURE = getEventSelector( + getAbiItem({ abi: PartialTransferEventAbi, name: 'Transfer' }), + ); + // Contracts @Logger('Contracts:') @@ -42,22 +55,24 @@ export class LidoSDKWrap extends LidoSDKModule { @Logger('Contracts:') @Cache(30 * 60 * 1000, ['core.chain.id', 'contractAddressWstETH']) public async getContractWstETH(): Promise< - GetContractReturnType + GetContractReturnType > { const address = await this.contractAddressWstETH(); return getContract({ address, abi: abi, - publicClient: this.core.rpcProvider, - walletClient: this.core.web3Provider, + client: { + public: this.core.rpcProvider, + wallet: this.core.web3Provider as WalletClient, + }, }); } @Logger('Contracts:') @Cache(30 * 60 * 1000, ['core.chain.id']) private async getPartialContractSteth(): Promise< - GetContractReturnType + GetContractReturnType > { const address = await this.core.getContractAddress( LIDO_CONTRACT_NAMES.lido, @@ -66,8 +81,10 @@ export class LidoSDKWrap extends LidoSDKModule { return getContract({ address, abi: stethPartialAbi, - publicClient: this.core.rpcProvider, - walletClient: this.core.web3Provider, + client: { + public: this.core.rpcProvider, + wallet: this.core.web3Provider as WalletClient, + }, }); } @@ -75,14 +92,17 @@ export class LidoSDKWrap extends LidoSDKModule { @Logger('Call:') @ErrorHandler() - public async wrapEth(props: WrapProps): Promise { - const { account, callback, value } = this.parseProps(props); + public async wrapEth( + props: WrapProps, + ): Promise> { + const { account, callback, value, ...rest } = await this.parseProps(props); const web3Provider = this.core.useWeb3Provider(); const contract = await this.getContractWstETH(); await this.validateStakeLimit(value); return this.core.performTransaction({ + ...rest, account, callback, getGasLimit: (options) => @@ -97,6 +117,7 @@ export class LidoSDKWrap extends LidoSDKModule { to: contract.address, ...options, }), + decodeResult: (receipt) => this.wrapParseEvents(receipt, account.address), }); } @@ -104,13 +125,12 @@ export class LidoSDKWrap extends LidoSDKModule { public async wrapEthPopulateTx( props: WrapPropsWithoutCallback, ): Promise { - const { value, account } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); + const { value, account } = await this.parseProps(props); const address = await this.contractAddressWstETH(); return { to: address, - from: accountAddress, + from: account.address, value, }; } @@ -120,12 +140,11 @@ export class LidoSDKWrap extends LidoSDKModule { public async wrapEthEstimateGas( props: WrapPropsWithoutCallback, ): Promise { - const { value, account } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); + const { value, account } = await this.parseProps(props); const address = await this.contractAddressWstETH(); return this.core.rpcProvider.estimateGas({ - account: accountAddress, + account, to: address, value, }); @@ -135,16 +154,20 @@ export class LidoSDKWrap extends LidoSDKModule { @Logger('Call:') @ErrorHandler() - public async wrapSteth(props: WrapProps): Promise { + public async wrapSteth( + props: WrapProps, + ): Promise> { this.core.useWeb3Provider(); - const { account, callback, value } = this.parseProps(props); + const { account, callback, value, ...rest } = await this.parseProps(props); const contract = await this.getContractWstETH(); - return this.core.performTransaction({ + return this.core.performTransaction({ + ...rest, account, callback, getGasLimit: (options) => contract.estimateGas.wrap([value], options), sendTransaction: (options) => contract.write.wrap([value], options), + decodeResult: (receipt) => this.wrapParseEvents(receipt, account.address), }); } @@ -152,13 +175,12 @@ export class LidoSDKWrap extends LidoSDKModule { public async wrapStethPopulateTx( props: WrapPropsWithoutCallback, ): Promise { - const { value, account } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); + const { value, account } = await this.parseProps(props); const address = await this.contractAddressWstETH(); return { to: address, - from: accountAddress, + from: account.address, data: encodeFunctionData({ abi, functionName: 'wrap', @@ -172,15 +194,11 @@ export class LidoSDKWrap extends LidoSDKModule { public async wrapStethSimulateTx( props: WrapPropsWithoutCallback, ): Promise { - const { value, account } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); - const address = await this.contractAddressWstETH(); - const { request } = await this.core.rpcProvider.simulateContract({ - address, - abi, - account: accountAddress, - functionName: 'wrap', - args: [value], + const { value, account } = await this.parseProps(props); + const contract = await this.getContractWstETH(); + + const { request } = await contract.simulate.wrap([value], { + account, }); return request; @@ -194,11 +212,12 @@ export class LidoSDKWrap extends LidoSDKModule { props: WrapProps, ): Promise { this.core.useWeb3Provider(); - const { account, callback, value } = this.parseProps(props); + const { account, callback, value, ...rest } = await this.parseProps(props); const stethContract = await this.getPartialContractSteth(); const wstethContractAddress = await this.contractAddressWstETH(); return this.core.performTransaction({ + ...rest, account, callback, getGasLimit: (options) => @@ -216,25 +235,24 @@ export class LidoSDKWrap extends LidoSDKModule { public async getStethForWrapAllowance( account?: AccountValue, ): Promise { - const accountAddress = await this.core.getWeb3Address(account); + const parsedAccount = await this.core.useAccount(account); const stethContract = await this.getPartialContractSteth(); const wstethAddress = await this.contractAddressWstETH(); - return stethContract.read.allowance([accountAddress, wstethAddress]); + return stethContract.read.allowance([parsedAccount.address, wstethAddress]); } @Logger('Utils:') public async approveStethForWrapPopulateTx( props: WrapPropsWithoutCallback, ): Promise> { - const { value, account } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); + const { value, account } = await this.parseProps(props); const stethContract = await this.getPartialContractSteth(); const wstethContractAddress = await this.contractAddressWstETH(); return { to: stethContract.address, - from: accountAddress, + from: account.address, data: encodeFunctionData({ abi: stethPartialAbi, functionName: 'approve', @@ -248,19 +266,16 @@ export class LidoSDKWrap extends LidoSDKModule { public async approveStethForWrapSimulateTx( props: WrapPropsWithoutCallback, ): Promise { - const { value, account } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); + const { value, account } = await this.parseProps(props); const stethContract = await this.getPartialContractSteth(); const wstethContractAddress = await this.contractAddressWstETH(); - const { request } = await this.core.rpcProvider.simulateContract({ - address: stethContract.address, - abi: stethPartialAbi, - account: accountAddress, - functionName: 'approve', - args: [wstethContractAddress, value], - }); - + const { request } = await stethContract.simulate.approve( + [wstethContractAddress, value], + { + account, + }, + ); return request; } @@ -268,16 +283,21 @@ export class LidoSDKWrap extends LidoSDKModule { @Logger('Call:') @ErrorHandler() - public async unwrap(props: WrapProps): Promise { + public async unwrap( + props: WrapProps, + ): Promise> { this.core.useWeb3Provider(); - const { account, callback, value } = this.parseProps(props); + const { account, callback, value, ...rest } = await this.parseProps(props); const contract = await this.getContractWstETH(); return this.core.performTransaction({ + ...rest, account, callback, getGasLimit: (options) => contract.estimateGas.unwrap([value], options), sendTransaction: (options) => contract.write.unwrap([value], options), + decodeResult: (receipt) => + this.unwrapParseEvents(receipt, account.address), }); } @@ -285,13 +305,12 @@ export class LidoSDKWrap extends LidoSDKModule { public async unwrapPopulateTx( props: Omit, ): Promise { - const { value, account } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); + const { value, account } = await this.parseProps(props); const to = await this.contractAddressWstETH(); return { to, - from: accountAddress, + from: account.address, data: encodeFunctionData({ abi: abi, functionName: 'unwrap', @@ -305,16 +324,11 @@ export class LidoSDKWrap extends LidoSDKModule { public async unwrapSimulateTx( props: Omit, ): Promise { - const { value, account } = this.parseProps(props); - const accountAddress = await this.core.getWeb3Address(account); - const wstethContractAddress = await this.contractAddressWstETH(); + const { value, account } = await this.parseProps(props); + const contract = await this.getContractWstETH(); - const { request } = await this.core.rpcProvider.simulateContract({ - address: wstethContractAddress, - abi, - account: accountAddress, - functionName: 'unwrap', - args: [value], + const { request } = await contract.simulate.unwrap([value], { + account, }); return request; @@ -356,11 +370,89 @@ export class LidoSDKWrap extends LidoSDKModule { } @Logger('Utils:') - private parseProps(props: WrapProps): WrapInnerProps { + private async parseProps(props: WrapProps): Promise { return { ...props, + account: await this.core.useAccount(props.account), value: parseValue(props.value), callback: props.callback ?? NOOP, }; } + + @Logger('Utils:') + private async wrapParseEvents( + receipt: TransactionReceipt, + address: Address, + ): Promise { + const wstethAddress = await this.contractAddressWstETH(); + + let stethWrapped: bigint | undefined; + let wstethReceived: bigint | undefined; + for (const log of receipt.logs) { + // skips non-relevant events + if (log.topics[0] !== LidoSDKWrap.TRANSFER_SIGNATURE) continue; + const parsedLog = decodeEventLog({ + // fits both wsteth and steth events + abi: PartialTransferEventAbi, + strict: true, + ...log, + }); + if (addressEqual(parsedLog.args.to, address)) { + wstethReceived = parsedLog.args.value; + } else if (addressEqual(parsedLog.args.to, wstethAddress)) { + stethWrapped = parsedLog.args.value; + } + } + invariant( + stethWrapped, + 'could not find Transfer event in wrap transaction', + ERROR_CODE.TRANSACTION_ERROR, + ); + invariant( + wstethReceived, + 'could not find Transfer event in wrap transaction', + ERROR_CODE.TRANSACTION_ERROR, + ); + return { + stethWrapped, + wstethReceived, + }; + } + + @Logger('Utils:') + private async unwrapParseEvents( + receipt: TransactionReceipt, + address: Address, + ): Promise { + let stethReceived: bigint | undefined; + let wstethUnwrapped: bigint | undefined; + for (const log of receipt.logs) { + // skips non-relevant events + if (log.topics[0] !== LidoSDKWrap.TRANSFER_SIGNATURE) continue; + const parsedLog = decodeEventLog({ + abi: PartialTransferEventAbi, + strict: true, + ...log, + }); + if (addressEqual(parsedLog.args.from, address)) { + wstethUnwrapped = parsedLog.args.value; + } else if (addressEqual(parsedLog.args.to, address)) { + stethReceived = parsedLog.args.value; + } + } + invariant( + stethReceived, + 'could not find Transfer event in unwrap transaction', + ERROR_CODE.TRANSACTION_ERROR, + ); + invariant( + wstethUnwrapped, + 'could not find Transfer event in unwrap transaction', + ERROR_CODE.TRANSACTION_ERROR, + ); + return { + stethReceived, + wstethUnwrapped, + }; + } } diff --git a/packages/sdk/tests/global-setup.cjs b/packages/sdk/tests/global-setup.cjs index d1bf8982..f374e85f 100644 --- a/packages/sdk/tests/global-setup.cjs +++ b/packages/sdk/tests/global-setup.cjs @@ -25,7 +25,7 @@ module.exports = async function () { chain: { chainId, asyncRequestProcessing: false }, }); - console.debug('Initializing ganache provider...'); + console.debug('\nInitializing ganache provider...'); await ganacheProvider.initialize(); console.debug('Initialized ganache provider'); diff --git a/packages/sdk/tests/global-teardown.cjs b/packages/sdk/tests/global-teardown.cjs new file mode 100644 index 00000000..2413a5b6 --- /dev/null +++ b/packages/sdk/tests/global-teardown.cjs @@ -0,0 +1,7 @@ +module.exports = async function () { + if (globalThis.__ganache_provider__) { + console.debug('Disconnecting ganache provider...'); + await globalThis.__ganache_provider__.disconnect(); + console.debug('Disconnected ganache provider'); + } +}; diff --git a/packages/sdk/tests/utils/expect/expect-erc20-wallet.ts b/packages/sdk/tests/utils/expect/expect-erc20-wallet.ts index d237f4cd..bb6d703a 100644 --- a/packages/sdk/tests/utils/expect/expect-erc20-wallet.ts +++ b/packages/sdk/tests/utils/expect/expect-erc20-wallet.ts @@ -72,8 +72,10 @@ export const expectERC20Wallet = ({ return getContract({ address, abi: erc20abi, - publicClient: rpcCore.rpcProvider, - walletClient: web3Core.web3Provider, + client: { + public: rpcCore.rpcProvider, + wallet: web3Core.web3Provider, + }, }); }; @@ -321,7 +323,7 @@ export const expectERC20Wallet = ({ expect(tx).toHaveProperty('primaryType'); expect(tx).toHaveProperty('message'); - expect(tx.account).toBe(params.account); + expect(tx.account.address).toBe(params.account); expect(tx.domain).toMatchObject(domain); expect(tx.types).toBe(PERMIT_MESSAGE_TYPES); expect(tx.primaryType).toBe('Permit'); diff --git a/packages/sdk/tests/utils/expect/expect-erc20.ts b/packages/sdk/tests/utils/expect/expect-erc20.ts index 9fcce529..ea80600a 100644 --- a/packages/sdk/tests/utils/expect/expect-erc20.ts +++ b/packages/sdk/tests/utils/expect/expect-erc20.ts @@ -37,12 +37,14 @@ export const expectERC20 = ({ return getContract({ address, abi: erc20abi, - publicClient: rpcCore.rpcProvider, - walletClient: web3Core.web3Provider, + client: { + public: rpcCore.rpcProvider, + wallet: web3Core.web3Provider, + }, }); }; - describe('construModulePrototype', () => { + describe('constructModulePrototype', () => { test('is correct module', () => { expectSDKModule(ModulePrototype); }); @@ -61,7 +63,7 @@ export const expectERC20 = ({ }); test('balance', async () => { - const address = await token.core.getWeb3Address(); + const { address } = await token.core.useAccount(); const contract = await token.getContract(); const balanceViaRawContract = await contract.read.balanceOf([address]); const balanceViaClass = await token.balance(address); @@ -96,7 +98,7 @@ export const expectERC20 = ({ }); test('nonces', async () => { - const address = await token.core.getWeb3Address(); + const { address } = await token.core.useAccount(); const nonces = await token.nonces(address); expectBn(nonces); }); diff --git a/packages/sdk/tests/utils/expect/expect-tx-callback.ts b/packages/sdk/tests/utils/expect/expect-tx-callback.ts index ee6e2ff2..44800eda 100644 --- a/packages/sdk/tests/utils/expect/expect-tx-callback.ts +++ b/packages/sdk/tests/utils/expect/expect-tx-callback.ts @@ -12,6 +12,7 @@ export const useMockCallback = () => jest.fn(); type ExpectTxCallbackOptions = { hasPermit?: boolean; + isMultisig?: boolean; confirmations?: bigint; hash?: string; receipt?: any; @@ -21,66 +22,86 @@ export const expectTxCallback = ( callback: typeof mockTxCallback, { hasPermit = false, + isMultisig = false, confirmations, hash, receipt, }: ExpectTxCallbackOptions = {}, ) => { - expect(callback).toBeCalledTimes(hasPermit ? 6 : 5); let callIndex = 0; - // PERMIT - if (hasPermit) { + if (isMultisig) { + expect(callback).toBeCalledTimes(2); + // SIGN expect(callback.mock.calls[callIndex]?.[0]).toHaveProperty( 'stage', - TransactionCallbackStage.PERMIT, + TransactionCallbackStage.SIGN, ); - expect(callback.mock.calls[callIndex]?.[0].payload).toBeUndefined(); + expectPositiveBn(callback.mock.calls[callIndex]?.[0].payload); callIndex++; - } - // GAS LIMIT - expect(callback.mock.calls[callIndex]?.[0]).toHaveProperty( - 'stage', - TransactionCallbackStage.GAS_LIMIT, - ); - expect(callback.mock.calls[callIndex]?.[0].payload).toBeUndefined(); - callIndex++; + // DONE + expect(callback.mock.calls[callIndex]?.[0]).toHaveProperty( + 'stage', + TransactionCallbackStage.MULTISIG_DONE, + ); + } else { + expect(callback).toBeCalledTimes(hasPermit ? 6 : 5); - // SIGN - expect(callback.mock.calls[callIndex]?.[0]).toHaveProperty( - 'stage', - TransactionCallbackStage.SIGN, - ); - expectPositiveBn(callback.mock.calls[callIndex]?.[0].payload); - callIndex++; + // PERMIT + if (hasPermit) { + expect(callback.mock.calls[callIndex]?.[0]).toHaveProperty( + 'stage', + TransactionCallbackStage.PERMIT, + ); + expect(callback.mock.calls[callIndex]?.[0].payload).toBeUndefined(); + callIndex++; + } - // RECEIPT - expect(callback.mock.calls[callIndex]?.[0]).toHaveProperty( - 'stage', - TransactionCallbackStage.RECEIPT, - ); - expectHash(callback.mock.calls[callIndex]?.[0].payload); - hash && expect(callback.mock.calls[callIndex]?.[0].payload).toBe(hash); - callIndex++; + // GAS LIMIT + expect(callback.mock.calls[callIndex]?.[0]).toHaveProperty( + 'stage', + TransactionCallbackStage.GAS_LIMIT, + ); + expect(callback.mock.calls[callIndex]?.[0].payload).toBeUndefined(); + callIndex++; - // CONFIRMATION - expect(callback.mock.calls[callIndex]?.[0]).toHaveProperty( - 'stage', - TransactionCallbackStage.CONFIRMATION, - ); - expect(callback.mock.calls[callIndex]?.[0].payload).toHaveProperty( - 'blockNumber', - ); - receipt && expect(callback.mock.calls[callIndex]?.[0].payload).toBe(receipt); - callIndex++; + // SIGN + expect(callback.mock.calls[callIndex]?.[0]).toHaveProperty( + 'stage', + TransactionCallbackStage.SIGN, + ); + expectPositiveBn(callback.mock.calls[callIndex]?.[0].payload); + callIndex++; - // DONE - expect(callback.mock.calls[callIndex]?.[0]).toHaveProperty( - 'stage', - TransactionCallbackStage.DONE, - ); - expectPositiveBn(callback.mock.calls[callIndex]?.[0].payload); - confirmations && - expect(callback.mock.calls[callIndex]?.[0].payload).toBe(confirmations); + // RECEIPT + expect(callback.mock.calls[callIndex]?.[0]).toHaveProperty( + 'stage', + TransactionCallbackStage.RECEIPT, + ); + expectHash(callback.mock.calls[callIndex]?.[0].payload); + hash && expect(callback.mock.calls[callIndex]?.[0].payload).toBe(hash); + callIndex++; + + // CONFIRMATION + expect(callback.mock.calls[callIndex]?.[0]).toHaveProperty( + 'stage', + TransactionCallbackStage.CONFIRMATION, + ); + expect(callback.mock.calls[callIndex]?.[0].payload).toHaveProperty( + 'blockNumber', + ); + receipt && + expect(callback.mock.calls[callIndex]?.[0].payload).toBe(receipt); + callIndex++; + + // DONE + expect(callback.mock.calls[callIndex]?.[0]).toHaveProperty( + 'stage', + TransactionCallbackStage.DONE, + ); + expectPositiveBn(callback.mock.calls[callIndex]?.[0].payload); + confirmations && + expect(callback.mock.calls[callIndex]?.[0].payload).toBe(confirmations); + } }; diff --git a/packages/sdk/tests/utils/fixtures/use-mock-transport.ts b/packages/sdk/tests/utils/fixtures/use-mock-transport.ts new file mode 100644 index 00000000..0a238c4a --- /dev/null +++ b/packages/sdk/tests/utils/fixtures/use-mock-transport.ts @@ -0,0 +1,35 @@ +import { custom, http } from 'viem'; +import { useTestRpcProvider } from './use-test-rpc-provider.js'; +import { useTestsEnvs } from './use-test-envs.js'; + +export type MockTransportCallback = ( + args: any, + originalRequest: (args?: any) => Promise, +) => Promise; + +type MockTransportOptions = { + useDirectRpc?: boolean; +}; +// provides mockTransport to pass to Public/Wallet client +// async callback is called instead of provider.request +// first argument for callback is args as passed to provider.request +// second argument for callback is default request handler +// call it to invoke default handler with custom args if needed +// callback must return suitable rpc response +export const useMockTransport = ( + callback: MockTransportCallback, + options: MockTransportOptions = {}, +) => { + const { useDirectRpc = false } = options; + const { rpcUrl } = useTestsEnvs(); + const originalRequest: (args: any) => Promise = useDirectRpc + ? (args: any) => http(rpcUrl)({}).request(args) + : (args: any) => useTestRpcProvider().ganacheProvider.request(args); + return custom({ + async request(args) { + return callback(args, async (customArgs: any = args) => + originalRequest(customArgs), + ); + }, + }); +}; diff --git a/playground/package.json b/playground/package.json index a4f12d60..933a2eca 100644 --- a/playground/package.json +++ b/playground/package.json @@ -33,7 +33,7 @@ "@reef-knot/web3-react": "1.5.0", "copy-to-clipboard": "^3.3.1", "fs-extra": "^10.1.0", - "next": "^13.0.5", + "next": "^13.5.1", "next-logger": "^3.0.0", "prom-client": "^14.2.0", "react": "^18.2.0", @@ -43,7 +43,7 @@ "styled-components": "^5.3.5", "swr": "^1.3.0", "tiny-invariant": "^1.3.1", - "viem": "^1.10.3", + "viem": "^2.0.6", "wagmi": "^0.12.19" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index a2ba68c8..2c884d61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,10 +12,10 @@ __metadata: languageName: node linkType: hard -"@adraffy/ens-normalize@npm:1.9.4": - version: 1.9.4 - resolution: "@adraffy/ens-normalize@npm:1.9.4" - checksum: 7d7fff58ebe2c4961f7e5e61dad123aa6a63fec0df5c84af1fa41079dc05d398599690be4427b3a94d2baa94084544bcfdf2d51cbed7504b9b0583b0960ad550 +"@adraffy/ens-normalize@npm:1.10.0": + version: 1.10.0 + resolution: "@adraffy/ens-normalize@npm:1.10.0" + checksum: af0540f963a2632da2bbc37e36ea6593dcfc607b937857133791781e246d47f870d5e3d21fa70d5cfe94e772c284588c81ea3f5b7f4ea8fbb824369444e4dbcb languageName: node linkType: hard @@ -3097,7 +3097,7 @@ __metadata: rimraf: ^5.0.1 ts-jest: ^29.1.1 typescript: 5.1.6 - viem: ^1.18.8 + viem: ^2.0.6 languageName: unknown linkType: soft @@ -3262,10 +3262,10 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:13.4.19": - version: 13.4.19 - resolution: "@next/env@npm:13.4.19" - checksum: ace4f82890954ade0164fbe2b7ff988268d2b99b2e80caa6707c51fa4cbfaaa31e48fbbcecd4fd142af3503c544e1b4c91e8185d4af253c8fb46550e9e70ad7e +"@next/env@npm:13.5.6": + version: 13.5.6 + resolution: "@next/env@npm:13.5.6" + checksum: 5e8f3f6f987a15dad3cd7b2bcac64a6382c2ec372d95d0ce6ab295eb59c9731222017eebf71ff3005932de2571f7543bce7e5c6a8c90030207fb819404138dc2 languageName: node linkType: hard @@ -3278,65 +3278,65 @@ __metadata: languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:13.4.19": - version: 13.4.19 - resolution: "@next/swc-darwin-arm64@npm:13.4.19" +"@next/swc-darwin-arm64@npm:13.5.6": + version: 13.5.6 + resolution: "@next/swc-darwin-arm64@npm:13.5.6" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@next/swc-darwin-x64@npm:13.4.19": - version: 13.4.19 - resolution: "@next/swc-darwin-x64@npm:13.4.19" +"@next/swc-darwin-x64@npm:13.5.6": + version: 13.5.6 + resolution: "@next/swc-darwin-x64@npm:13.5.6" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:13.4.19": - version: 13.4.19 - resolution: "@next/swc-linux-arm64-gnu@npm:13.4.19" +"@next/swc-linux-arm64-gnu@npm:13.5.6": + version: 13.5.6 + resolution: "@next/swc-linux-arm64-gnu@npm:13.5.6" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:13.4.19": - version: 13.4.19 - resolution: "@next/swc-linux-arm64-musl@npm:13.4.19" +"@next/swc-linux-arm64-musl@npm:13.5.6": + version: 13.5.6 + resolution: "@next/swc-linux-arm64-musl@npm:13.5.6" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:13.4.19": - version: 13.4.19 - resolution: "@next/swc-linux-x64-gnu@npm:13.4.19" +"@next/swc-linux-x64-gnu@npm:13.5.6": + version: 13.5.6 + resolution: "@next/swc-linux-x64-gnu@npm:13.5.6" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:13.4.19": - version: 13.4.19 - resolution: "@next/swc-linux-x64-musl@npm:13.4.19" +"@next/swc-linux-x64-musl@npm:13.5.6": + version: 13.5.6 + resolution: "@next/swc-linux-x64-musl@npm:13.5.6" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:13.4.19": - version: 13.4.19 - resolution: "@next/swc-win32-arm64-msvc@npm:13.4.19" +"@next/swc-win32-arm64-msvc@npm:13.5.6": + version: 13.5.6 + resolution: "@next/swc-win32-arm64-msvc@npm:13.5.6" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-ia32-msvc@npm:13.4.19": - version: 13.4.19 - resolution: "@next/swc-win32-ia32-msvc@npm:13.4.19" +"@next/swc-win32-ia32-msvc@npm:13.5.6": + version: 13.5.6 + resolution: "@next/swc-win32-ia32-msvc@npm:13.5.6" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:13.4.19": - version: 13.4.19 - resolution: "@next/swc-win32-x64-msvc@npm:13.4.19" +"@next/swc-win32-x64-msvc@npm:13.5.6": + version: 13.5.6 + resolution: "@next/swc-win32-x64-msvc@npm:13.5.6" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -4740,12 +4740,12 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:0.5.1": - version: 0.5.1 - resolution: "@swc/helpers@npm:0.5.1" +"@swc/helpers@npm:0.5.2": + version: 0.5.2 + resolution: "@swc/helpers@npm:0.5.2" dependencies: tslib: ^2.4.0 - checksum: 71e0e27234590435e4c62b97ef5e796f88e786841a38c7116a5e27a3eafa7b9ead7cdec5249b32165902076de78446945311c973e59bddf77c1e24f33a8f272a + checksum: 51d7e3d8bd56818c49d6bfbd715f0dbeedc13cf723af41166e45c03e37f109336bbcb57a1f2020f4015957721aeb21e1a7fff281233d797ff7d3dd1f447fa258 languageName: node linkType: hard @@ -5211,15 +5211,6 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^8.5.5": - version: 8.5.5 - resolution: "@types/ws@npm:8.5.5" - dependencies: - "@types/node": "*" - checksum: d00bf8070e6938e3ccf933010921c6ce78ac3606696ce37a393b27a9a603f7bd93ea64f3c5fa295a2f743575ba9c9a9fdb904af0f5fe2229bf2adf0630386e4a - languageName: node - linkType: hard - "@types/yargs-parser@npm:*": version: 21.0.2 resolution: "@types/yargs-parser@npm:21.0.2" @@ -5916,18 +5907,18 @@ __metadata: languageName: node linkType: hard -"abitype@npm:0.9.8": - version: 0.9.8 - resolution: "abitype@npm:0.9.8" +"abitype@npm:0.10.0": + version: 0.10.0 + resolution: "abitype@npm:0.10.0" peerDependencies: typescript: ">=5.0.4" - zod: ^3 >=3.19.1 + zod: ^3 >=3.22.0 peerDependenciesMeta: typescript: optional: true zod: optional: true - checksum: d7d887f29d6821e3f7a400de9620511b80ead3f85c5c87308aaec97965d3493e6687ed816e88722b4f512249bd66dee9e69231b49af0e1db8f69400a62c87cf6 + checksum: 01a75393740036121414024aa7ed61e6a2104bfd90c91b6aa1a7778cf1edfa15b828779acbbb13ac641939d1ba9c836d143d9f7310699cd7496273bb24c599b3 languageName: node linkType: hard @@ -9050,12 +9041,12 @@ __metadata: linkType: hard "follow-redirects@npm:^1.14.8, follow-redirects@npm:^1.15.0": - version: 1.15.2 - resolution: "follow-redirects@npm:1.15.2" + version: 1.15.4 + resolution: "follow-redirects@npm:1.15.4" peerDependenciesMeta: debug: optional: true - checksum: faa66059b66358ba65c234c2f2a37fcec029dc22775f35d9ad6abac56003268baf41e55f9ee645957b32c7d9f62baf1f0b906e68267276f54ec4b4c597c2b190 + checksum: e178d1deff8b23d5d24ec3f7a94cde6e47d74d0dc649c35fc9857041267c12ec5d44650a0c5597ef83056ada9ea6ca0c30e7c4f97dbf07d035086be9e6a5b7b6 languageName: node linkType: hard @@ -10304,15 +10295,6 @@ __metadata: languageName: node linkType: hard -"isomorphic-ws@npm:5.0.0": - version: 5.0.0 - resolution: "isomorphic-ws@npm:5.0.0" - peerDependencies: - ws: "*" - checksum: e20eb2aee09ba96247465fda40c6d22c1153394c0144fa34fe6609f341af4c8c564f60ea3ba762335a7a9c306809349f9b863c8beedf2beea09b299834ad5398 - languageName: node - linkType: hard - "isomorphic-ws@npm:^4.0.1": version: 4.0.1 resolution: "isomorphic-ws@npm:4.0.1" @@ -11427,7 +11409,7 @@ __metadata: "@types/styled-components": ^5.1.23 copy-to-clipboard: ^3.3.1 fs-extra: ^10.1.0 - next: ^13.0.5 + next: ^13.5.1 next-logger: ^3.0.0 prom-client: ^14.2.0 react: ^18.2.0 @@ -11439,7 +11421,7 @@ __metadata: tiny-invariant: ^1.3.1 typescript: 5.1.6 url-loader: ^4.1.1 - viem: ^1.10.3 + viem: ^2.0.6 wagmi: ^0.12.19 languageName: unknown linkType: soft @@ -12278,27 +12260,26 @@ __metadata: languageName: node linkType: hard -"next@npm:^13.0.5": - version: 13.4.19 - resolution: "next@npm:13.4.19" - dependencies: - "@next/env": 13.4.19 - "@next/swc-darwin-arm64": 13.4.19 - "@next/swc-darwin-x64": 13.4.19 - "@next/swc-linux-arm64-gnu": 13.4.19 - "@next/swc-linux-arm64-musl": 13.4.19 - "@next/swc-linux-x64-gnu": 13.4.19 - "@next/swc-linux-x64-musl": 13.4.19 - "@next/swc-win32-arm64-msvc": 13.4.19 - "@next/swc-win32-ia32-msvc": 13.4.19 - "@next/swc-win32-x64-msvc": 13.4.19 - "@swc/helpers": 0.5.1 +"next@npm:^13.5.1": + version: 13.5.6 + resolution: "next@npm:13.5.6" + dependencies: + "@next/env": 13.5.6 + "@next/swc-darwin-arm64": 13.5.6 + "@next/swc-darwin-x64": 13.5.6 + "@next/swc-linux-arm64-gnu": 13.5.6 + "@next/swc-linux-arm64-musl": 13.5.6 + "@next/swc-linux-x64-gnu": 13.5.6 + "@next/swc-linux-x64-musl": 13.5.6 + "@next/swc-win32-arm64-msvc": 13.5.6 + "@next/swc-win32-ia32-msvc": 13.5.6 + "@next/swc-win32-x64-msvc": 13.5.6 + "@swc/helpers": 0.5.2 busboy: 1.6.0 caniuse-lite: ^1.0.30001406 - postcss: 8.4.14 + postcss: 8.4.31 styled-jsx: 5.1.1 watchpack: 2.4.0 - zod: 3.21.4 peerDependencies: "@opentelemetry/api": ^1.1.0 react: ^18.2.0 @@ -12330,7 +12311,7 @@ __metadata: optional: true bin: next: dist/bin/next - checksum: f4873dab8888ed4dae14d36d7cf8dc54cd042695cf7ee41d05e8757f463d11952a594eb066143cc2f7253ea1d41c6efe681cdc3ab8c2fa6eb0815fa5a94de3dc + checksum: c869b0014ae921ada3bf22301985027ec320aebcd6aa9c16e8afbded68bb8def5874cca034c680e8c351a79578f1e514971d02777f6f0a5a1d7290f25970ac0d languageName: node linkType: hard @@ -16033,38 +16014,16 @@ __metadata: languageName: node linkType: hard -"viem@npm:^1.10.3": - version: 1.10.9 - resolution: "viem@npm:1.10.9" - dependencies: - "@adraffy/ens-normalize": 1.9.4 - "@noble/curves": 1.2.0 - "@noble/hashes": 1.3.2 - "@scure/bip32": 1.3.2 - "@scure/bip39": 1.2.1 - "@types/ws": ^8.5.5 - abitype: 0.9.8 - isomorphic-ws: 5.0.0 - ws: 8.13.0 - peerDependencies: - typescript: ">=5.0.4" - peerDependenciesMeta: - typescript: - optional: true - checksum: 7bf53044abff4ff703d518227921d9a56907be744a30750873231a442dcfdee485d9928f337e22ad698a4d0cf25a57194aee9e636a09233169721bf2a6f2eea0 - languageName: node - linkType: hard - -"viem@npm:^1.18.8": - version: 1.18.8 - resolution: "viem@npm:1.18.8" +"viem@npm:^2.0.6": + version: 2.0.6 + resolution: "viem@npm:2.0.6" dependencies: - "@adraffy/ens-normalize": 1.9.4 + "@adraffy/ens-normalize": 1.10.0 "@noble/curves": 1.2.0 "@noble/hashes": 1.3.2 "@scure/bip32": 1.3.2 "@scure/bip39": 1.2.1 - abitype: 0.9.8 + abitype: 0.10.0 isows: 1.0.3 ws: 8.13.0 peerDependencies: @@ -16072,7 +16031,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 735e7debb20592b8c629f54a909c1c4b2f6d5d83ecb85eae15f066df55c9938153be87f7e635d925562ef7c8c600f9778e6f4dc83a76953d2ea1e118914d83db + checksum: eb6d17922c9eba640f4dc41e2f263c38033191da0328d0ff6ce710111368b65326fd7e680600dce1d87e4604c7b1898702bb1cdb333625d9c3cfb552f2739f08 languageName: node linkType: hard @@ -16484,13 +16443,6 @@ __metadata: languageName: node linkType: hard -"zod@npm:3.21.4": - version: 3.21.4 - resolution: "zod@npm:3.21.4" - checksum: f185ba87342ff16f7a06686767c2b2a7af41110c7edf7c1974095d8db7a73792696bcb4a00853de0d2edeb34a5b2ea6a55871bc864227dace682a0a28de33e1f - languageName: node - linkType: hard - "zustand@npm:^4.3.1": version: 4.4.1 resolution: "zustand@npm:4.4.1"