From 4a89c33c6fcfe2fcea2ab6b56328f968e09905db Mon Sep 17 00:00:00 2001 From: poocart <7067483+poocart@users.noreply.github.com> Date: Wed, 18 Oct 2023 13:33:06 +0300 Subject: [PATCH] Feature/prime sdk as default (#57) * removed via prop, removed etherspot provider type, default provider etherspot prime * removed gas token, added prime sdk estimations (#58) --- CHANGELOG.md | 11 ++ .../hooks/useEtherspotTransactions.test.js | 26 +-- __tests__/hooks/useWalletAddress.test.js | 24 ++- package.json | 2 +- src/components/EtherspotBatch.tsx | 5 +- src/components/EtherspotBatches.tsx | 24 ++- src/hooks/useWalletAddress.ts | 11 +- ...EtherspotTransactionKitContextProvider.tsx | 149 +++++------------- src/types/EtherspotTransactionKit.ts | 38 ++--- 9 files changed, 107 insertions(+), 183 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e3f2cc..d729aff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [0.6.0] - 2023-10-18 + +### Breaking Changes +- Removed `etherspot` wallet type from `useWalletAddress` hook +- Removed `via` prop from `` component +- Default `useWalletAddress` and `` provider is set to `etherspot-prime` +- Removed `gasTokenAddress` prop from `` component due being unsupported in default `etherspot-prime` provider + +### Added +- Added `gasCost` to `etherspot-prime` estimations + ## [0.5.1] - 2023-10-17 ### Added diff --git a/__tests__/hooks/useEtherspotTransactions.test.js b/__tests__/hooks/useEtherspotTransactions.test.js index 6f79691..82a3eee 100644 --- a/__tests__/hooks/useEtherspotTransactions.test.js +++ b/__tests__/hooks/useEtherspotTransactions.test.js @@ -6,7 +6,7 @@ import { useEtherspotTransactions, EtherspotTransactionKit, EtherspotBatches, Et const TestSingleBatchComponent = () => ( - + { test - + { test - - - - - - + { const { result: { current } } = renderHook(() => useEtherspotTransactions(), { wrapper }); - expect(current.batches.length).toBe(6); + expect(current.batches.length).toBe(5); expect(current.batches[0].batches.length).toBe(1); expect(current.batches[0].batches[0].chainId).toBe(123); - expect(current.batches[0].batches[0].gasTokenAddress).toBe('testGasTokenAddress'); expect(current.batches[0].batches[0].transactions.length).toBe(3); expect(current.batches[0].batches[0].transactions[1].to).toBe('0x0'); expect(current.batches[0].batches[0].transactions[1].data).toBe('0xFFF'); @@ -93,13 +83,7 @@ describe('useEtherspotTransactions()', () => { expect(current.batches[0].batches[0].transactions[2].data).toBe('0xa9059cbb0000000000000000000000007f30b1960d5556929b03a0339814fe903c55a347000000000000000000000000000000000000000000000006aaf7c8516d0c0000'); expect(current.batches[0].batches[0].transactions[2].value).toBe(undefined); expect(current.batches[1].skip).toBe(true); - expect(current.batches[2].batches.length).toBe(1); - expect(current.batches[2].batches[0].chainId).toBe(69); - expect(current.batches[2].batches[0].transactions.length).toBe(1); - expect(current.batches[2].batches[0].transactions[0].to).toBe('0x420'); - expect(current.batches[2].batches[0].transactions[0].data).toBe('0x69420'); - expect(current.batches[2].batches[0].transactions[0].value.toJSON()).toStrictEqual({ 'hex': '0x03bd913e6c1df40000', 'type': 'BigNumber' }); - expect(current.batches[3].paymaster).toStrictEqual({ url: 'someUrl', api_key: 'someApiKey' }); + expect(current.batches[2].paymaster).toStrictEqual({ url: 'someUrl', api_key: 'someApiKey' }); }); it('throws an error if within ', () => { diff --git a/__tests__/hooks/useWalletAddress.test.js b/__tests__/hooks/useWalletAddress.test.js index d0c221e..31a0669 100644 --- a/__tests__/hooks/useWalletAddress.test.js +++ b/__tests__/hooks/useWalletAddress.test.js @@ -7,11 +7,23 @@ import { EtherspotTransactionKit, useWalletAddress } from '../../src'; const ethersProvider = new ethers.providers.JsonRpcProvider('http://localhost:8545', 'goerli'); // replace with your node's RPC URL const provider = new ethers.Wallet.createRandom().connect(ethersProvider); -const etherspotAddress = '0x7F30B1960D5556929B03a0339814fE903c55a347'; const etherspotPrimeAddress = '0x07ff85757f5209534EB601E1CA60d72807ECE0bC'; const providerWalletAddress = provider.address; describe('useWalletAddress()', () => { + it('returns default type wallet address if no provided type', async () => { + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useWalletAddress(), { wrapper }); + + await waitFor(() => expect(result.current).not.toBe(undefined)); + expect(result.current).toEqual(etherspotPrimeAddress); + }); + it('returns wallet address by type', async () => { const wrapper = ({ children }) => ( @@ -20,15 +32,11 @@ describe('useWalletAddress()', () => { ); const { result, rerender } = renderHook(({ providerType }) => useWalletAddress(providerType), { - initialProps: { providerType: 'etherspot' }, + initialProps: { providerType: 'etherspot-prime' }, wrapper, }); await waitFor(() => expect(result.current).not.toBe(undefined)); - expect(result.current).toEqual(etherspotAddress); - - rerender({ providerType: 'etherspot-prime' }); - await waitFor(() => expect(result.current).not.toBe(etherspotAddress)); expect(result.current).toEqual(etherspotPrimeAddress); rerender({ providerType: 'provider' }); @@ -44,12 +52,12 @@ describe('useWalletAddress()', () => { ); const { result, rerender } = renderHook(({ providerType }) => useWalletAddress(providerType), { - initialProps: { providerType: 'etherspot' }, + initialProps: { providerType: 'etherspot-prime' }, wrapper, }); await waitFor(() => expect(result.current).not.toBe(undefined)); - expect(result.current).toEqual(etherspotAddress); + expect(result.current).toEqual(etherspotPrimeAddress); rerender({ providerType: 'whatever' }); expect(result.current).toEqual(undefined); diff --git a/package.json b/package.json index a891beb..23464b0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@etherspot/transaction-kit", "description": "React Etherspot Transaction Kit", - "version": "0.5.1", + "version": "0.6.0", "main": "dist/cjs/index.js", "scripts": { "rollup:build": "NODE_OPTIONS=--max-old-space-size=8192 rollup -c", diff --git a/src/components/EtherspotBatch.tsx b/src/components/EtherspotBatch.tsx index fc2a450..df5e977 100644 --- a/src/components/EtherspotBatch.tsx +++ b/src/components/EtherspotBatch.tsx @@ -18,7 +18,7 @@ interface EtherspotBatchProps extends IBatch { children?: React.ReactNode; } -const EtherspotBatch = ({ children, chainId, gasTokenAddress, id: batchId }: EtherspotBatchProps) => { +const EtherspotBatch = ({ children, chainId, id: batchId }: EtherspotBatchProps) => { const context = useContext(EtherspotBatchesContext); const existingBatchContext = useContext(EtherspotBatchContext); const componentId = useId(); @@ -36,7 +36,6 @@ const EtherspotBatch = ({ children, chainId, gasTokenAddress, id: batchId }: Eth const batch = { id: batchId ?? componentId, chainId, - gasTokenAddress, transactions: getObjectSortedByKeys(transactionsPerId), }; @@ -48,7 +47,7 @@ const EtherspotBatch = ({ children, chainId, gasTokenAddress, id: batchId }: Eth return current; }); } - }, [componentId, transactionsPerId, chainId, batchId, gasTokenAddress]); + }, [componentId, transactionsPerId, chainId, batchId]); return ( diff --git a/src/components/EtherspotBatches.tsx b/src/components/EtherspotBatches.tsx index a34c647..4113fe2 100644 --- a/src/components/EtherspotBatches.tsx +++ b/src/components/EtherspotBatches.tsx @@ -1,5 +1,4 @@ import React, { useContext, useEffect, useState } from 'react'; -import { PaymasterApi } from '@etherspot/prime-sdk'; // contexts import EtherspotTransactionKitContext from '../contexts/EtherspotTransactionKitContext'; @@ -21,13 +20,14 @@ type EtherspotBatchesProps = IBatches & { } const EtherspotBatches = (props: EtherspotBatchesProps) => { - let paymaster: PaymasterApi | undefined; - - const { children, skip, onEstimated, onSent, id: batchesId, via } = props; - - if (via === 'etherspot-prime') { - paymaster = props.paymaster; - } + const { + children, + skip, + onEstimated, + onSent, + id: batchesId, + paymaster, + } = props; const context = useContext(EtherspotTransactionKitContext); const existingBatchesContext = useContext(EtherspotBatchesContext); @@ -55,13 +55,9 @@ const EtherspotBatches = (props: EtherspotBatchesProps) => { batches: getObjectSortedByKeys(batchesPerId), onEstimated, onSent, - via, + paymaster, }; - if (groupedBatch.via === 'etherspot-prime') { - groupedBatch = { ...groupedBatch, paymaster }; - } - context.setGroupedBatchesPerId((current) => ({ ...current, [componentId]: groupedBatch })); return () => { @@ -70,7 +66,7 @@ const EtherspotBatches = (props: EtherspotBatchesProps) => { return current; }); } - }, [componentId, batchesPerId, skip, batchesId, via, paymaster]); + }, [componentId, batchesPerId, skip, batchesId, paymaster]); return ( diff --git a/src/hooks/useWalletAddress.ts b/src/hooks/useWalletAddress.ts index d0728db..af758a8 100644 --- a/src/hooks/useWalletAddress.ts +++ b/src/hooks/useWalletAddress.ts @@ -1,6 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; import { useEtherspot } from '@etherspot/react-etherspot'; -import { AccountTypes } from 'etherspot'; // types import { IWalletType } from '../types/EtherspotTransactionKit'; @@ -14,7 +13,7 @@ import useEtherspotTransactions from './useEtherspotTransactions'; * @param chainId {number} - Chain ID * @returns {string | undefined} - wallet address by its type */ -const useWalletAddress = (walletType: IWalletType = 'etherspot', chainId?: number): string | undefined => { +const useWalletAddress = (walletType: IWalletType = 'etherspot-prime', chainId?: number): string | undefined => { const [walletAddress, setWalletAddress] = useState<(string | undefined)>(undefined); const { connect, getSdkForChainId, providerWalletAddress, chainId: defaultChainId } = useEtherspot(); const { getEtherspotPrimeSdkForChainId } = useEtherspotTransactions(); @@ -35,13 +34,7 @@ const useWalletAddress = (walletType: IWalletType = 'etherspot', chainId?: numbe console.warn(`Unable to get SDK for chain ID ${walletAddressChainId}`); } - if (walletType === 'etherspot') { - if (sdkForChainId?.state?.account?.type !== AccountTypes.Contract) { - await connect(walletAddressChainId); - } - - updatedWalletAddress = sdkForChainId?.state?.account?.address; - } else if (walletType === 'etherspot-prime') { + if (walletType === 'etherspot-prime') { const etherspotPrimeSdk = await getEtherspotPrimeSdkForChainId(walletAddressChainId); if (!etherspotPrimeSdk) { console.warn(`Unable to get Etherspot Prime SDK for chain ID ${walletAddressChainId}`); diff --git a/src/providers/EtherspotTransactionKitContextProvider.tsx b/src/providers/EtherspotTransactionKitContextProvider.tsx index 7f76653..39c4108 100644 --- a/src/providers/EtherspotTransactionKitContextProvider.tsx +++ b/src/providers/EtherspotTransactionKitContextProvider.tsx @@ -1,14 +1,12 @@ import { useEtherspot } from '@etherspot/react-etherspot'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { PrimeSdk, isWalletProvider, Web3WalletProvider } from '@etherspot/prime-sdk'; -import { AccountStates } from 'etherspot'; -import { ethers } from 'ethers'; // contexts import EtherspotTransactionKitContext from '../contexts/EtherspotTransactionKitContext'; // utils -import { getObjectSortedByKeys, isTestnetChainId, parseEtherspotErrorMessageIfAvailable } from '../utils/common'; +import { getObjectSortedByKeys } from '../utils/common'; // types import { EstimatedBatch, IBatch, IBatches, IEstimatedBatches, ISentBatches, SentBatch } from '../types/EtherspotTransactionKit'; @@ -23,15 +21,9 @@ let isSdkConnecting: Promise | undefined; let etherspotPrimeSdkPerChain: { [chainId: number]: PrimeSdk } = {}; const EtherspotTransactionKitContextProvider = ({ children }: EtherspotTransactionKitContextProviderProps) => { - const { getSdkForChainId, connect, provider, chainId } = useEtherspot(); + const { provider, chainId } = useEtherspot(); const [groupedBatchesPerId, setGroupedBatchesPerId] = useState>({}); - const connectToSdkForChainIfNeeded = async (chainId: number) => { - isSdkConnecting = connect(chainId); - await isSdkConnecting; - isSdkConnecting = undefined; - } - const estimate = async ( batchesIds?: string[], forSending: boolean = false, @@ -61,58 +53,24 @@ const EtherspotTransactionKitContextProvider = ({ children }: EtherspotTransacti continue; } - if (groupedBatch.via === 'etherspot-prime') { - const etherspotPrimeSdkForChainId = await getEtherspotPrimeSdkForChainId(batchChainId); - if (!etherspotPrimeSdkForChainId) { - estimatedBatches.push({ ...batch, errorMessage: 'Failed to get Etherspot Prime SDK for chain!' }); - continue; - } - - try { - if (!forSending) await etherspotPrimeSdkForChainId.clearUserOpsFromBatch(); - - await Promise.all(batch.transactions.map(async ({ to, value, data }) => { - await etherspotPrimeSdkForChainId.addUserOpsToBatch(({ to, value, data })); - })); - - // TODO: add actual estimation when available on Etherspot Prime - estimatedBatches.push({ ...batch, cost: ethers.BigNumber.from(0) }); - } catch (e) { - estimatedBatches.push({ ...batch, errorMessage: (e instanceof Error && e.message) || 'Failed to estimate!' }); - } - } else { - const sdkForChainId = getSdkForChainId(batchChainId); - - if (!sdkForChainId) { - estimatedBatches.push({ ...batch, errorMessage: 'Failed to get Etherspot SDK for chain!' }); - continue; - } - - await connectToSdkForChainIfNeeded(batchChainId); - - try { - if (!forSending) sdkForChainId.clearGatewayBatch(); - - if (sdkForChainId.state.account.state === AccountStates.UnDeployed) { - await sdkForChainId.batchDeployAccount(); - } - - await Promise.all(batch.transactions.map(async ({ to, value, data }) => { - await sdkForChainId.batchExecuteAccountTransaction(({ to, value, data })); - })); - - const { estimation } = await sdkForChainId.estimateGatewayBatch({ feeToken: batch.gasTokenAddress }); - - const cost = batch.gasTokenAddress - ? estimation.feeAmount - : estimation.estimatedGasPrice.mul(estimation.estimatedGas) - - estimatedBatches.push({ ...batch, cost }); - } catch (e) { - const rawErrorMessage = e instanceof Error && e.message; - const errorMessage = parseEtherspotErrorMessageIfAvailable(rawErrorMessage || 'Failed to estimate!'); - estimatedBatches.push({ ...batch, errorMessage }); - } + const etherspotPrimeSdkForChainId = await getEtherspotPrimeSdkForChainId(batchChainId); + if (!etherspotPrimeSdkForChainId) { + estimatedBatches.push({ ...batch, errorMessage: 'Failed to get Etherspot Prime SDK for chain!' }); + continue; + } + + try { + if (!forSending) await etherspotPrimeSdkForChainId.clearUserOpsFromBatch(); + + await Promise.all(batch.transactions.map(async ({ to, value, data }) => { + await etherspotPrimeSdkForChainId.addUserOpsToBatch(({ to, value, data })); + })); + + const userOp = await etherspotPrimeSdkForChainId.estimate(groupedBatch.paymaster); + const totalGas = await etherspotPrimeSdkForChainId.totalGasEstimated(userOp); + estimatedBatches.push({ ...batch, cost: totalGas, userOp }); + } catch (e) { + estimatedBatches.push({ ...batch, errorMessage: (e instanceof Error && e.message) || 'Failed to estimate!' }); } } @@ -181,21 +139,13 @@ const EtherspotTransactionKitContextProvider = ({ children }: EtherspotTransacti // clear any existing batches before new estimate & send await Promise.all(groupedBatchesToClean.map(async ({ batches = [], - via, }) => Promise.all(batches.map(async (batch) => { const batchChainId = batch.chainId ?? chainId; - if (via === 'etherspot-prime') { - const etherspotPrimeSdkForChainId = await getEtherspotPrimeSdkForChainId(batchChainId); - if (!etherspotPrimeSdkForChainId) return; - await etherspotPrimeSdkForChainId.clearUserOpsFromBatch(); - return; - } + const etherspotPrimeSdkForChainId = await getEtherspotPrimeSdkForChainId(batchChainId); + if (!etherspotPrimeSdkForChainId) return; - const sdkForChainId = getSdkForChainId(batchChainId); - if (!sdkForChainId) return; - - sdkForChainId.clearGatewayBatch(); + await etherspotPrimeSdkForChainId.clearUserOpsFromBatch(); })))); const estimated = await estimate(batchesIds, true); @@ -206,51 +156,32 @@ const EtherspotTransactionKitContextProvider = ({ children }: EtherspotTransacti const sentBatches: SentBatch[] = []; - const via = estimatedBatches.via ?? 'etherspot'; - // send in same order as estimated for (const estimatedBatch of estimatedBatches.estimatedBatches) { const batchChainId = estimatedBatch.chainId ?? chainId // return error message as provided by estimate if (estimatedBatch.errorMessage) { - sentBatches.push({ ...estimatedBatch, via, errorMessage: estimatedBatch.errorMessage }) + sentBatches.push({ ...estimatedBatch, errorMessage: estimatedBatch.errorMessage }) + continue; + } + + const etherspotPrimeSdkForChainId = await getEtherspotPrimeSdkForChainId(batchChainId); + if (!etherspotPrimeSdkForChainId) { + sentBatches.push({ ...estimatedBatch, errorMessage: 'Failed to get Etherspot Prime SDK for chain!' }); + continue; + } + + if (!estimatedBatch.userOp) { + sentBatches.push({ ...estimatedBatch, errorMessage: 'Failed to get estimated UserOp!' }); continue; } - if (estimatedBatches.via === 'etherspot-prime') { - const etherspotPrimeSdkForChainId = await getEtherspotPrimeSdkForChainId(batchChainId); - if (!etherspotPrimeSdkForChainId) { - sentBatches.push({ ...estimatedBatch, via, errorMessage: 'Failed to get Etherspot Prime SDK for chain!' }); - continue; - } - - try { - const estimated = await etherspotPrimeSdkForChainId.estimate(estimatedBatches.paymaster); - const userOpHash = await etherspotPrimeSdkForChainId.send(estimated); - sentBatches.push({ ...estimatedBatch, via, userOpHash }); - } catch (e) { - sentBatches.push({ ...estimatedBatch, via, errorMessage: (e instanceof Error && e.message) || 'Failed to estimate!' }); - } - } else { - const sdkForChainId = getSdkForChainId(batchChainId); - if (!sdkForChainId) { - sentBatches.push({ ...estimatedBatch, via, errorMessage: 'Failed to get Etherspot SDK for chain!' }); - continue; - } - - await connectToSdkForChainIfNeeded(batchChainId); - - try { - // testnets does not have guards - const guarded = !isTestnetChainId(batchChainId); - const { hash: batchHash } = await sdkForChainId.submitGatewayBatch({ guarded }); - sentBatches.push({ ...estimatedBatch, via, batchHash }); - } catch (e) { - const rawErrorMessage = e instanceof Error && e.message; - const errorMessage = parseEtherspotErrorMessageIfAvailable(rawErrorMessage || 'Failed to send!'); - sentBatches.push({ ...estimatedBatch, via, errorMessage }); - } + try { + const userOpHash = await etherspotPrimeSdkForChainId.send(estimatedBatch.userOp); + sentBatches.push({ ...estimatedBatch, userOpHash }); + } catch (e) { + sentBatches.push({ ...estimatedBatch, errorMessage: (e instanceof Error && e.message) || 'Failed to estimate!' }); } } diff --git a/src/types/EtherspotTransactionKit.ts b/src/types/EtherspotTransactionKit.ts index ff38566..330d98f 100644 --- a/src/types/EtherspotTransactionKit.ts +++ b/src/types/EtherspotTransactionKit.ts @@ -1,5 +1,5 @@ import { Fragment, JsonFragment } from '@ethersproject/abi/src.ts/fragments'; -import { BigNumber, BigNumberish } from 'ethers'; +import { BigNumber, BigNumberish, BytesLike } from 'ethers'; import { ExchangeOffer } from 'etherspot'; import { Route } from '@lifi/sdk'; import { PaymasterApi } from '@etherspot/prime-sdk'; @@ -22,41 +22,29 @@ export interface IProviderWalletTransaction { export interface IBatch { id?: string; chainId?: number; - gasTokenAddress?: string; transactions?: ITransaction[]; } export interface EstimatedBatch extends IBatch { errorMessage?: string; cost?: BigNumber; + userOp?: UserOp; } -export interface EtherspotSentBatch extends EstimatedBatch { - via: 'etherspot'; - batchHash?: string; -} export interface EtherspotPrimeSentBatch extends EstimatedBatch { - via: 'etherspot-prime'; userOpHash?: string; } export type SentBatch = { errorMessage?: string; -} & (EtherspotSentBatch | EtherspotPrimeSentBatch); - -export type IBatches = IDefaultBatches - | (IDefaultBatches<'etherspot-prime'> & IEtherspotPrimeBatchesExtra); +} & (EtherspotPrimeSentBatch); -interface IDefaultBatches { +export interface IBatches { id?: string; batches?: IBatch[]; onEstimated?: (estimated: EstimatedBatch[]) => void; onSent?: (sent: SentBatch[]) => void; skip?: boolean; - via?: T; -} - -interface IEtherspotPrimeBatchesExtra { paymaster?: PaymasterApi, } @@ -117,6 +105,20 @@ export interface IProviderWalletTransactionSent { errorMessage?: string; } -export type IBatchesWalletType = 'etherspot' | 'etherspot-prime'; +export type IWalletType = 'provider' | 'etherspot-prime'; + +type EtherspotPromiseOrValue = T | Promise; -export type IWalletType = 'provider' | IBatchesWalletType; +interface UserOp { + sender: EtherspotPromiseOrValue; + nonce: EtherspotPromiseOrValue; + initCode: EtherspotPromiseOrValue; + callData: EtherspotPromiseOrValue; + callGasLimit: EtherspotPromiseOrValue; + verificationGasLimit: EtherspotPromiseOrValue; + preVerificationGas: EtherspotPromiseOrValue; + maxFeePerGas: EtherspotPromiseOrValue; + maxPriorityFeePerGas: EtherspotPromiseOrValue; + paymasterAndData: EtherspotPromiseOrValue; + signature: EtherspotPromiseOrValue; +}