diff --git a/advanced/wallets/react-wallet-v2/src/components/ChainSmartAddressMini.tsx b/advanced/wallets/react-wallet-v2/src/components/ChainSmartAddressMini.tsx
new file mode 100644
index 0000000..51f0f6b
--- /dev/null
+++ b/advanced/wallets/react-wallet-v2/src/components/ChainSmartAddressMini.tsx
@@ -0,0 +1,31 @@
+import useSmartAccount from '@/hooks/useSmartAccount'
+import { Hex } from 'viem'
+import ChainAddressMini from './ChainAddressMini'
+import { createOrRestoreEIP155Wallet, eip155Wallets } from '@/utils/EIP155WalletUtil'
+import { Spinner } from '@nextui-org/react'
+import { Chain, allowedChains } from '@/utils/SmartAccountUtils'
+import { useSnapshot } from 'valtio'
+import SettingsStore from '@/store/SettingsStore'
+
+interface Props {
+ namespace: string
+}
+
+const getKey = (namespace?: string) => {
+ switch (namespace) {
+ case 'eip155':
+ createOrRestoreEIP155Wallet()
+ const key = Object.values(eip155Wallets)[0]?.getPrivateKey() as Hex
+ return key
+ }
+}
+
+export default function ChainSmartAddressMini({ namespace }: Props) {
+ const { activeChainId } = useSnapshot(SettingsStore.state)
+ const { address } = useSmartAccount(getKey(namespace) as `0x${string}`, allowedChains.find((c) => c.id.toString() === activeChainId) as Chain)
+
+ if (!address) return
+ return (
+
+ )
+}
diff --git a/advanced/wallets/react-wallet-v2/src/components/ModalFooter.tsx b/advanced/wallets/react-wallet-v2/src/components/ModalFooter.tsx
index 5983bfd..fbaf378 100644
--- a/advanced/wallets/react-wallet-v2/src/components/ModalFooter.tsx
+++ b/advanced/wallets/react-wallet-v2/src/components/ModalFooter.tsx
@@ -11,7 +11,6 @@ interface Props {
onReject: () => void
infoBoxCondition?: boolean
infoBoxText?: string
- disabledApprove?: boolean
approveLoader?: LoaderProps
rejectLoader?: LoaderProps
}
@@ -23,7 +22,6 @@ export default function ModalFooter({
rejectLoader,
infoBoxCondition,
infoBoxText,
- disabledApprove
}: Props) {
const { currentRequestVerifyContext } = useSnapshot(SettingsStore.state)
const validation = currentRequestVerifyContext?.verified.validation
@@ -46,13 +44,14 @@ export default function ModalFooter({
{infoBoxText || ''}
)}
-
+
diff --git a/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts b/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts
index 8b79daa..0c16d6b 100644
--- a/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts
+++ b/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts
@@ -8,7 +8,7 @@
*/
export type TEIP155Chain = keyof typeof EIP155_CHAINS
-export type EIP155TestChain = {
+export type EIP155Chain = {
chainId: number
name: string
logo: string
@@ -21,7 +21,7 @@ export type EIP155TestChain = {
/**
* Chains
*/
-export const EIP155_MAINNET_CHAINS = {
+export const EIP155_MAINNET_CHAINS: Record = {
'eip155:1': {
chainId: 1,
name: 'Ethereum',
@@ -64,7 +64,7 @@ export const EIP155_MAINNET_CHAINS = {
}
}
-export const EIP155_TEST_CHAINS: Record = {
+export const EIP155_TEST_CHAINS: Record = {
'eip155:5': {
chainId: 5,
name: 'Ethereum Goerli',
@@ -81,6 +81,7 @@ export const EIP155_TEST_CHAINS: Record = {
rgb: '99, 125, 234',
rpc: 'https://rpc.sepolia.org',
namespace: 'eip155',
+ smartAccountEnabled: true,
},
'eip155:43113': {
chainId: 43113,
@@ -96,7 +97,8 @@ export const EIP155_TEST_CHAINS: Record = {
logo: '/chain-logos/eip155-137.png',
rgb: '130, 71, 229',
rpc: 'https://matic-mumbai.chainstacklabs.com',
- namespace: 'eip155'
+ namespace: 'eip155',
+ smartAccountEnabled: true,
},
'eip155:420': {
chainId: 420,
diff --git a/advanced/wallets/react-wallet-v2/src/hooks/useInitialization.ts b/advanced/wallets/react-wallet-v2/src/hooks/useInitialization.ts
index d96955f..9f378ab 100644
--- a/advanced/wallets/react-wallet-v2/src/hooks/useInitialization.ts
+++ b/advanced/wallets/react-wallet-v2/src/hooks/useInitialization.ts
@@ -49,7 +49,7 @@ export default function useInitialization() {
// restart transport if relayer region changes
const onRelayerRegionChange = useCallback(() => {
try {
- web3wallet.core.relayer.restartTransport(relayerRegionURL)
+ web3wallet?.core?.relayer.restartTransport(relayerRegionURL)
prevRelayerURLValue.current = relayerRegionURL
} catch (err: unknown) {
alert(err)
diff --git a/advanced/wallets/react-wallet-v2/src/hooks/useSmartAccount.ts b/advanced/wallets/react-wallet-v2/src/hooks/useSmartAccount.ts
index a3f3074..38fce46 100644
--- a/advanced/wallets/react-wallet-v2/src/hooks/useSmartAccount.ts
+++ b/advanced/wallets/react-wallet-v2/src/hooks/useSmartAccount.ts
@@ -1,18 +1,24 @@
import { SmartAccountLib } from "@/lib/SmartAccountLib";
-import { styledToast } from "@/utils/HelperUtil";
+import SettingsStore from "@/store/SettingsStore";
+import { Chain, VITALIK_ADDRESS } from "@/utils/SmartAccountUtils";
import { useCallback, useEffect, useState } from "react";
+import { useSnapshot } from "valtio";
+import { Hex } from "viem";
+import { styledToast } from "@/utils/HelperUtil";
import { TransactionExecutionError } from "viem";
-export default function useSmartAccount(signerPrivateKey: `0x${string}`) {
+export default function useSmartAccount(signerPrivateKey: Hex, chain: Chain) {
const [loading, setLoading] = useState(false)
const [client, setClient] = useState();
const [isDeployed, setIsDeployed] = useState(false)
- const [address, setAddress] = useState<`0x${string}`>()
+ const [address, setAddress] = useState()
+ const { smartAccountSponsorshipEnabled } = useSnapshot(SettingsStore.state);
const execute = useCallback(async (callback: () => void) => {
try {
setLoading(true)
- await callback()
+ const res = await callback()
+ console.log('result:', res)
setLoading(false)
}
catch (e) {
@@ -34,13 +40,22 @@ export default function useSmartAccount(signerPrivateKey: `0x${string}`) {
const sendTestTransaction = useCallback(async () => {
if (!client) return
- execute(client?.sendTestTransaction)
+ execute(() => client?.sendTransaction({
+ to: VITALIK_ADDRESS,
+ value: 0n,
+ data: '0x',
+ }))
}, [client, execute])
useEffect(() => {
- const smartAccountClient = new SmartAccountLib(signerPrivateKey, 'goerli')
- setClient(smartAccountClient)
- }, [signerPrivateKey])
+ if (!signerPrivateKey || !chain) return
+ const smartAccountClient = new SmartAccountLib({
+ chain,
+ privateKey: signerPrivateKey,
+ sponsored: smartAccountSponsorshipEnabled,
+ })
+ setClient(smartAccountClient)
+ }, [signerPrivateKey, smartAccountSponsorshipEnabled, chain])
useEffect(() => {
client?.checkIfSmartAccountDeployed()
@@ -48,7 +63,7 @@ export default function useSmartAccount(signerPrivateKey: `0x${string}`) {
setIsDeployed(deployed)
setAddress(client?.address)
})
- }, [client])
+ }, [client, chain])
return {
diff --git a/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts b/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts
index af4f6c2..a860ba2 100644
--- a/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts
+++ b/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts
@@ -117,7 +117,7 @@ export default function useWalletConnectEventsManager(initialized: boolean) {
* Set up WalletConnect event listeners
*****************************************************************************/
useEffect(() => {
- if (initialized) {
+ if (initialized && web3wallet) {
//sign
web3wallet.on('session_proposal', onSessionProposal)
web3wallet.on('session_request', onSessionRequest)
diff --git a/advanced/wallets/react-wallet-v2/src/lib/EIP155Lib.ts b/advanced/wallets/react-wallet-v2/src/lib/EIP155Lib.ts
index 2be3436..87989a0 100644
--- a/advanced/wallets/react-wallet-v2/src/lib/EIP155Lib.ts
+++ b/advanced/wallets/react-wallet-v2/src/lib/EIP155Lib.ts
@@ -39,7 +39,7 @@ export default class EIP155Lib {
return this.wallet.signMessage(message)
}
- _signTypedData(domain: any, types: any, data: any) {
+ _signTypedData(domain: any, types: any, data: any, _primaryType?: string) {
return this.wallet._signTypedData(domain, types, data)
}
diff --git a/advanced/wallets/react-wallet-v2/src/lib/SmartAccountLib.ts b/advanced/wallets/react-wallet-v2/src/lib/SmartAccountLib.ts
index 7d13432..960f3a9 100644
--- a/advanced/wallets/react-wallet-v2/src/lib/SmartAccountLib.ts
+++ b/advanced/wallets/react-wallet-v2/src/lib/SmartAccountLib.ts
@@ -1,134 +1,267 @@
-import { createSmartAccountClient } from 'permissionless'
+import { BundlerActions, BundlerClient, bundlerActions, createSmartAccountClient, getAccountNonce } from 'permissionless'
import { privateKeyToSafeSmartAccount } from 'permissionless/accounts'
import * as chains from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
-import { type Chain, createWalletClient, formatEther, createPublicClient, http } from 'viem'
-import { createPimlicoBundlerClient } from 'permissionless/clients/pimlico'
+import { createWalletClient, formatEther, createPublicClient, http, Address, Hex, PublicClient, createClient, WalletClient } from 'viem'
+import { PimlicoPaymasterClient, createPimlicoPaymasterClient } from 'permissionless/clients/pimlico'
+import { UserOperation } from 'permissionless/types'
+import { providers } from 'ethers'
+import { PimlicoBundlerActions, pimlicoBundlerActions } from 'permissionless/actions/pimlico'
+import { Chain, ENTRYPOINT_ADDRESSES, PAYMASTER_ADDRESSES, USDC_ADDRESSES, VITALIK_ADDRESS, approveUSDCSpendCallData, bundlerUrl, paymasterUrl, publicRPCUrl } from '@/utils/SmartAccountUtils'
-export type SmartAccountEnabledChains = 'sepolia' | 'goerli'
+type SmartAccountLibOptions = {
+ privateKey: `0x${string}`
+ chain: Chain
+ sponsored?: boolean
+};
// -- Helpers -----------------------------------------------------------------
-const bundlerApiKey = process.env.NEXT_PUBLIC_PIMLICO_KEY
+const pimlicoApiKey = process.env.NEXT_PUBLIC_PIMLICO_KEY
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID
// -- Sdk ---------------------------------------------------------------------
export class SmartAccountLib {
public chain: Chain
- private bundlerApiKey: string
- #signerPrivateKey: `0x${string}`;
public isDeployed: boolean = false;
public address?: `0x${string}`;
+ public sponsored: boolean = true;
+
+ private publicClient: PublicClient
+ private paymasterClient: PimlicoPaymasterClient
+ private bundlerClient: BundlerClient & BundlerActions & PimlicoBundlerActions
+ private signerClient: WalletClient
- public constructor(privateKey: `0x${string}`, chain: SmartAccountEnabledChains = 'goerli') {
- if (!bundlerApiKey) {
- throw new Error('Missing required data in SmartAccountSdk')
+ #signerPrivateKey: `0x${string}`;
+
+
+ public constructor({ privateKey, chain, sponsored = true }: SmartAccountLibOptions) {
+ if (!pimlicoApiKey) {
+ throw new Error('A Pimlico API Key is required')
}
- this.bundlerApiKey = bundlerApiKey
- this.chain = chains[chain] as Chain
- this.#signerPrivateKey = privateKey
- }
- // -- Public ------------------------------------------------------------------
+ this.chain = chain
+ this.sponsored = sponsored
+ this.#signerPrivateKey = privateKey
+ this.publicClient = createPublicClient({
+ transport: http(publicRPCUrl({ chain: this.chain }))
+ })
+
+ this.paymasterClient = createPimlicoPaymasterClient({
+ transport: http(paymasterUrl({ chain: this.chain }))
+ })
+
+ this.bundlerClient = createClient({
+ transport: http(bundlerUrl({ chain: this.chain })),
+ chain: this.chain
+ })
+ .extend(bundlerActions)
+ .extend(pimlicoBundlerActions)
+
+ this.signerClient = createWalletClient({
+ account: privateKeyToAccount(this.#signerPrivateKey),
+ chain: this.chain,
+ transport: http(publicRPCUrl({ chain: this.chain }))
+ })
+
+ }
// -- Private -----------------------------------------------------------------
- private getWalletConnectTransport = () => http(
- `https://rpc.walletconnect.com/v1/?chainId=EIP155:${this.chain.id}&projectId=${projectId}`,
- { retryDelay: 1000 }
- );
-
- private getBundlerTransport = () => http(
- `https://api.pimlico.io/v1/${this.chain.name.toLowerCase()}/rpc?apikey=${this.bundlerApiKey}`,
- { retryDelay: 1000 }
- );
-
-
- private getBundlerClient = () => createPimlicoBundlerClient({
- chain: this.chain,
- transport: this.getBundlerTransport()
- })
-
- private getPublicClient = () => createPublicClient({
- chain: this.chain,
- transport: this.getWalletConnectTransport()
- })
-
- private getSignerClient = () => {
- const signerAccount = privateKeyToAccount(this.#signerPrivateKey)
- return createWalletClient({
- account: signerAccount,
+ private getSmartAccountClient = async (
+ sponsorUserOperation?: (args: {
+ userOperation: UserOperation
+ entryPoint: Address
+ }) => Promise
+ ) => {
+ const account = await this.getAccount()
+ return createSmartAccountClient({
+ account,
chain: this.chain,
- transport: this.getWalletConnectTransport()
- })
+ transport: http(bundlerUrl({ chain: this.chain })),
+ sponsorUserOperation: sponsorUserOperation
+ ? sponsorUserOperation
+ : this.sponsored ? this.paymasterClient.sponsorUserOperation : undefined
+ }).extend(pimlicoBundlerActions)
}
- private getSmartAccountClient = async () => {
- const smartAccount = await privateKeyToSafeSmartAccount(this.getPublicClient(), {
- privateKey: this.#signerPrivateKey,
- safeVersion: '1.4.1',
- entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'
- })
-
- return createSmartAccountClient({
- account: smartAccount,
- chain: this.chain,
- transport: this.getBundlerTransport()
+ public getNonce = async () => {
+ const smartAccountClient = await this.getSmartAccountClient()
+ return getAccountNonce(this.publicClient, {
+ sender: smartAccountClient.account.address as Hex,
+ entryPoint: ENTRYPOINT_ADDRESSES[this.chain.name]
})
}
private prefundSmartAccount = async (address: `0x${string}`) => {
- const signerAccountViemClient = this.getSignerClient();
- const publicClient = this.getPublicClient();
- const bundlerClient = this.getBundlerClient();
- const smartAccountBalance = await publicClient.getBalance({ address })
-
+ if (this.sponsored) {
+ return
+ }
+
+ const smartAccountBalance = await this.publicClient.getBalance({ address })
+
console.log(`Smart Account Balance: ${formatEther(smartAccountBalance)} ETH`)
if (smartAccountBalance < 1n) {
console.log(`Smart Account has no balance. Starting prefund`)
- const { fast: fastPrefund } = await bundlerClient.getUserOperationGasPrice()
- const prefundHash = await signerAccountViemClient.sendTransaction({
+ const { fast: fastPrefund } = await this.bundlerClient.getUserOperationGasPrice()
+ const prefundHash = await this.signerClient.sendTransaction({
to: address,
+ chain: this.chain,
+ account: this.signerClient.account!,
value: 10000000000000000n,
maxFeePerGas: fastPrefund.maxFeePerGas,
maxPriorityFeePerGas: fastPrefund.maxPriorityFeePerGas
})
-
- await publicClient.waitForTransactionReceipt({ hash: prefundHash })
+
+ await this.publicClient.waitForTransactionReceipt({ hash: prefundHash })
console.log(`Prefunding Success`)
-
- const newSmartAccountBalance = await publicClient.getBalance({ address })
+
+ const newSmartAccountBalance = await this.publicClient.getBalance({ address })
console.log(
`Smart Account Balance: ${formatEther(newSmartAccountBalance)} ETH`
)
}
}
- // By default first transaction will deploy the smart contract if it hasn't been deployed yet
- public sendTestTransaction = async () => {
- const publicClient = this.getPublicClient();
- const bundlerClient = this.getBundlerClient();
- const smartAccountClient = await this.getSmartAccountClient();
- const { fast: testGas, } = await bundlerClient.getUserOperationGasPrice()
-
- const testHash = await smartAccountClient.sendTransaction({
- to: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' as `0x${string}`,
- value: 0n,
- maxFeePerGas: testGas.maxFeePerGas,
- maxPriorityFeePerGas: testGas.maxPriorityFeePerGas,
- })
-
- console.log(`Sending Test Transaction With Hash: ${testHash}`)
-
- await publicClient.waitForTransactionReceipt({ hash: testHash })
- console.log(`Test Transaction Success`)
+ private getSmartAccountUSDCBalance = async () => {
+ const params = {
+ abi: [
+ {
+ inputs: [{ name: "_owner", type: "address" }],
+ name: "balanceOf",
+ outputs: [{ name: "balance", type: "uint256" }],
+ type: "function",
+ }
+ ],
+ address: USDC_ADDRESSES[this.chain.name] as Hex,
+ functionName: "balanceOf",
+ args: [this.address!]
+ }
+ const usdcBalance = await this.publicClient.readContract(params) as bigint
+ return usdcBalance
}
+ private sponsorUserOperation = async ({ userOperation }: { userOperation: UserOperation }) => {
+ const userOperationWithPaymasterAndData = {
+ ...userOperation,
+ paymasterAndData: PAYMASTER_ADDRESSES[this.chain.name]
+ }
+
+ console.log('Estimating gas limits...', userOperationWithPaymasterAndData)
+
+ const gasLimits = await this.bundlerClient.estimateUserOperationGas({
+ userOperation: userOperationWithPaymasterAndData,
+ entryPoint: ENTRYPOINT_ADDRESSES[this.chain.name]
+ })
+
+ return {
+ ...userOperationWithPaymasterAndData,
+ callGasLimit: gasLimits.callGasLimit,
+ verificationGasLimit: gasLimits.verificationGasLimit,
+ preVerificationGas: gasLimits.preVerificationGas
+ }
+ }
+
+ // -- Public ------------------------------------------------------------------
+ public getAccount = async () =>
+ privateKeyToSafeSmartAccount(this.publicClient, {
+ privateKey: this.#signerPrivateKey,
+ safeVersion: '1.4.1', // simple version
+ entryPoint: ENTRYPOINT_ADDRESSES[this.chain.name], // global entrypoint
+ setupTransactions: [
+ {
+ to: USDC_ADDRESSES[this.chain.name],
+ value: 0n,
+ data: approveUSDCSpendCallData({
+ to: PAYMASTER_ADDRESSES[this.chain.name],
+ amount: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn
+ })
+ }
+ ]
+ })
+
+ static isSmartAccount = async (address: Address, chain: Chain) => {
+ const client = createPublicClient({
+ chain,
+ transport: http(
+ `https://rpc.walletconnect.com/v1/?chainId=EIP155:${chains.goerli.id}&projectId=${projectId}`,
+ { retryDelay: 1000 }
+ )
+ })
+ const bytecode = await client.getBytecode({ address })
+ return Boolean(bytecode)
+ }
+
+ public sendTransaction = async ({
+ to,
+ value,
+ data
+ }: { to: Address; value: bigint; data: Hex }) => {
+ console.log(`Sending Transaction to ${to} with value ${value.toString()} and data ${data}`)
+ const smartAccountClient = await this.getSmartAccountClient()
+ const gasPrices = await smartAccountClient.getUserOperationGasPrice()
+ return smartAccountClient.sendTransaction({
+ to,
+ value,
+ data,
+ maxFeePerGas: gasPrices.fast.maxFeePerGas,
+ maxPriorityFeePerGas: gasPrices.fast.maxPriorityFeePerGas
+ })
+ }
+
+ public signMessage = async (message: string) => {
+ const client = await this.getSmartAccountClient()
+ return client.signMessage({ message })
+ }
+ public _signTypedData = async (domain: any, types: any, data: any, primaryType: any) => {
+ const client = await this.getSmartAccountClient()
+ return client.signTypedData({ account: client.account, domain, types, primaryType, message: data })
+ }
+
+ public connect = async (_provider: providers.JsonRpcProvider) => {
+ return this.getSmartAccountClient()
+ }
+
+ public signTransaction = async (transaction: any) => {
+ const smartAccountClient = await this.getSmartAccountClient()
+ return smartAccountClient.account.signTransaction(transaction)
+ }
+
+ public sendUSDCSponsoredTransaction = async ({
+ to,
+ value,
+ data
+ }: { to: Address; value: bigint; data: Hex }) => {
+ // 1. Check USDC Balance on smart account
+ const usdcBalance = await this.getSmartAccountUSDCBalance()
+
+ if (usdcBalance < 1_000_000n) {
+ throw new Error(
+ `insufficient USDC balance for counterfactual wallet address ${this.address}: ${
+ Number(usdcBalance) / 1000000
+ } USDC, required at least 1 USDC`
+ )
+ }
+
+ const smartAccountClient = await this.getSmartAccountClient(this.sponsorUserOperation)
+ const gasPrices = await smartAccountClient.getUserOperationGasPrice()
+
+ return smartAccountClient.sendTransaction({
+ to,
+ value,
+ data,
+ maxFeePerGas: gasPrices.fast.maxFeePerGas,
+ maxPriorityFeePerGas: gasPrices.fast.maxPriorityFeePerGas
+ })
+ }
+
+
public checkIfSmartAccountDeployed = async () => {
- console.log('checking if deployed')
const smartAccountClient = await this.getSmartAccountClient();
- const publicClient = this.getPublicClient();
- const bytecode = await publicClient.getBytecode({ address: smartAccountClient.account.address })
+ console.log('Checking if deployed', smartAccountClient.account.address, this.chain.name)
+
+ const bytecode = await this.publicClient.getBytecode({ address: smartAccountClient.account.address })
this.isDeployed = Boolean(bytecode)
+
console.log(`Smart Account Deployed: ${this.isDeployed}`)
if (this.isDeployed) {
this.address = smartAccountClient.account.address
@@ -145,7 +278,11 @@ export class SmartAccountLib {
await this.prefundSmartAccount(smartAccountClient.account.address)
// Step 4: Create account by sending test tx
- await this.sendTestTransaction()
+ await this.sendTransaction({
+ to: VITALIK_ADDRESS,
+ value: 0n,
+ data: '0x'
+ })
await this.checkIfSmartAccountDeployed()
console.log(`Account Created`)
}
diff --git a/advanced/wallets/react-wallet-v2/src/pages/_app.tsx b/advanced/wallets/react-wallet-v2/src/pages/_app.tsx
index 8de06ad..8cf3976 100644
--- a/advanced/wallets/react-wallet-v2/src/pages/_app.tsx
+++ b/advanced/wallets/react-wallet-v2/src/pages/_app.tsx
@@ -20,11 +20,11 @@ export default function App({ Component, pageProps }: AppProps) {
useWalletConnectEventsManager(initialized)
useEffect(() => {
if (!initialized) return
- web3wallet.core.relayer.on(RELAYER_EVENTS.connect, () => {
+ web3wallet?.core.relayer.on(RELAYER_EVENTS.connect, () => {
styledToast('Network connection is restored!', 'success')
})
- web3wallet.core.relayer.on(RELAYER_EVENTS.disconnect, () => {
+ web3wallet?.core.relayer.on(RELAYER_EVENTS.disconnect, () => {
styledToast('Network connection lost.', 'error')
})
}, [initialized])
diff --git a/advanced/wallets/react-wallet-v2/src/pages/settings.tsx b/advanced/wallets/react-wallet-v2/src/pages/settings.tsx
index 3c2eaaf..2ade434 100644
--- a/advanced/wallets/react-wallet-v2/src/pages/settings.tsx
+++ b/advanced/wallets/react-wallet-v2/src/pages/settings.tsx
@@ -16,6 +16,7 @@ import { tezosWallets } from '@/utils/TezosWalletUtil'
export default function SettingsPage() {
const {
testNets,
+ smartAccountSponsorshipEnabled,
eip155Address,
cosmosAddress,
solanaAddress,
@@ -53,6 +54,20 @@ export default function SettingsPage() {
+
+ Smart Account Sponsorship (Pimlico)
+
+
+
+ {smartAccountSponsorshipEnabled ? 'Enabled' : 'Disabled'}
+
+
+
+
Relayer Region
diff --git a/advanced/wallets/react-wallet-v2/src/store/SettingsStore.ts b/advanced/wallets/react-wallet-v2/src/store/SettingsStore.ts
index 56edf6a..264654a 100644
--- a/advanced/wallets/react-wallet-v2/src/store/SettingsStore.ts
+++ b/advanced/wallets/react-wallet-v2/src/store/SettingsStore.ts
@@ -20,6 +20,7 @@ interface State {
activeChainId: string
currentRequestVerifyContext?: Verify.Context
sessions: SessionTypes.Struct[]
+ smartAccountSponsorshipEnabled: boolean
}
/**
@@ -39,7 +40,8 @@ const state = proxy({
tezosAddress: '',
kadenaAddress: '',
relayerRegionURL: '',
- sessions: []
+ sessions: [],
+ smartAccountSponsorshipEnabled: false,
})
/**
@@ -103,10 +105,17 @@ const SettingsStore = {
toggleTestNets() {
state.testNets = !state.testNets
if (state.testNets) {
+ state.smartAccountSponsorshipEnabled = true
localStorage.setItem('TEST_NETS', 'YES')
} else {
+ state.smartAccountSponsorshipEnabled = false
localStorage.removeItem('TEST_NETS')
}
+ },
+
+ toggleSmartAccountSponsorship() {
+ if (!state.testNets) return
+ state.smartAccountSponsorshipEnabled = !state.smartAccountSponsorshipEnabled
}
}
diff --git a/advanced/wallets/react-wallet-v2/src/utils/EIP155RequestHandlerUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/EIP155RequestHandlerUtil.ts
index 36b8d61..6cc20b5 100644
--- a/advanced/wallets/react-wallet-v2/src/utils/EIP155RequestHandlerUtil.ts
+++ b/advanced/wallets/react-wallet-v2/src/utils/EIP155RequestHandlerUtil.ts
@@ -1,4 +1,6 @@
import { EIP155_CHAINS, EIP155_SIGNING_METHODS, TEIP155Chain } from '@/data/EIP155Data'
+import EIP155Lib from '@/lib/EIP155Lib'
+import { SmartAccountLib } from '@/lib/SmartAccountLib'
import { eip155Addresses, eip155Wallets } from '@/utils/EIP155WalletUtil'
import {
getSignParamsMessage,
@@ -9,11 +11,41 @@ import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils'
import { SignClientTypes } from '@walletconnect/types'
import { getSdkError } from '@walletconnect/utils'
import { providers } from 'ethers'
+import { Hex } from 'viem'
+import { allowedChains } from './SmartAccountUtils'
type RequestEventArgs = Omit
+
+
+const getWallet = async (params: any) => {
+ const requestParams: Array = params?.request?.params || []
+ const eoaWallet = eip155Wallets[getWalletAddressFromParams(eip155Addresses, params)]
+ if (eoaWallet) {
+ return eoaWallet
+ }
+
+ const deployedSmartAccounts = await Promise.all(Object.values(eip155Wallets).map(async (wallet) => {
+ const smartAccount = new SmartAccountLib({
+ privateKey: wallet.getPrivateKey() as Hex,
+ chain: allowedChains[0], // TODO: FIX FOR MULTI NETWORK
+ sponsored: true, // TODO: Sponsor for now but should be dynamic according to SettingsStore
+ })
+ const isDeployed = await smartAccount.checkIfSmartAccountDeployed()
+ if (isDeployed) {
+ return smartAccount
+ }
+ return null
+ }));
+ const validSmartAccounts = deployedSmartAccounts.filter(Boolean) as Array
+ const smartAccountAddress = getWalletAddressFromParams(validSmartAccounts.map(acc => acc.address!), params)
+
+ return validSmartAccounts.find((smartAccount) => smartAccount?.address === smartAccountAddress) as SmartAccountLib
+}
+
+
export async function approveEIP155Request(requestEvent: RequestEventArgs) {
const { params, id } = requestEvent
const { chainId, request } = params
- const wallet = eip155Wallets[getWalletAddressFromParams(eip155Addresses, params)]
+ const wallet = await getWallet(params)
switch (request.method) {
case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
@@ -32,10 +64,10 @@ export async function approveEIP155Request(requestEvent: RequestEventArgs) {
case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V3:
case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V4:
try {
- const { domain, types, message: data } = getSignTypedDataParamsData(request.params)
+ const { domain, types, message: data, primaryType } = getSignTypedDataParamsData(request.params)
// https://github.com/ethers-io/ethers.js/issues/687#issuecomment-714069471
delete types.EIP712Domain
- const signedData = await wallet._signTypedData(domain, types, data)
+ const signedData = await wallet._signTypedData(domain, types, data, primaryType)
return formatJsonRpcResult(id, signedData)
} catch (error: any) {
console.error(error)
@@ -47,9 +79,10 @@ export async function approveEIP155Request(requestEvent: RequestEventArgs) {
try {
const provider = new providers.JsonRpcProvider(EIP155_CHAINS[chainId as TEIP155Chain].rpc)
const sendTransaction = request.params[0]
- const connectedWallet = wallet.connect(provider)
- const { hash } = await connectedWallet.sendTransaction(sendTransaction)
- return formatJsonRpcResult(id, hash)
+ const connectedWallet = await wallet.connect(provider)
+ const hash = await connectedWallet.sendTransaction(sendTransaction)
+ const receipt = typeof hash === 'string' ? hash : hash?.hash // TODO improve interface
+ return formatJsonRpcResult(id, receipt)
} catch (error: any) {
console.error(error)
alert(error.message)
diff --git a/advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtils.ts b/advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtils.ts
new file mode 100644
index 0000000..4ea710e
--- /dev/null
+++ b/advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtils.ts
@@ -0,0 +1,140 @@
+import { Hex, createPublicClient, encodeFunctionData, http } from "viem"
+import { goerli, polygonMumbai, sepolia } from 'viem/chains'
+
+const apiKey = process.env.NEXT_PUBLIC_PIMLICO_KEY
+
+// Types
+export const allowedChains = [sepolia, polygonMumbai, goerli] as const
+export type Chain = (typeof allowedChains)[number]
+export type UrlConfig = {
+ chain: Chain
+}
+
+// Entrypoints [I think this is constant but JIC]
+export const ENTRYPOINT_ADDRESSES: Record = {
+ Sepolia: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
+ 'Polygon Mumbai': '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
+ 'Goerli': '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'
+}
+
+// Paymasters
+// https://docs.pimlico.io/paymaster/erc20-paymaster/contract-addresses
+export const PAYMASTER_ADDRESSES: Record = {
+ Sepolia: '0x0000000000325602a77416A16136FDafd04b299f',
+ 'Polygon Mumbai': '0x000000000009B901DeC1aaB9389285965F49D387',
+ Goerli: '0xEc43912D8C772A0Eba5a27ea5804Ba14ab502009'
+}
+
+// USDC
+export const USDC_ADDRESSES: Record = {
+ Sepolia: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
+ 'Polygon Mumbai': '0x9999f7fea5938fd3b1e26a12c3f2fb024e194f97',
+ Goerli: '0x07865c6e87b9f70255377e024ace6630c1eaa37f'
+}
+
+// RPC URLs
+export const RPC_URLS: Record = {
+ Sepolia: 'https://rpc.ankr.com/eth_sepolia',
+ 'Polygon Mumbai': 'https://mumbai.rpc.thirdweb.com',
+ Goerli: 'https://ethereum-goerli.publicnode.com'
+}
+
+// Pimlico RPC names
+export const PIMLICO_NETWORK_NAMES: Record = {
+ Sepolia: 'sepolia',
+ 'Polygon Mumbai': 'mumbai',
+ Goerli: 'goerli'
+}
+
+export const FAUCET_URLS: Record = {
+ Sepolia: 'https://sepoliafaucet.com',
+ 'Polygon Mumbai': 'https://faucet.polygon.technology',
+ Goerli: 'https://goerlifaucet.com'
+}
+
+export const USDC_FAUCET_URL = 'https://faucet.circle.com/'
+
+export const VITALIK_ADDRESS = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' as Hex
+
+export const publicRPCUrl = ({ chain }: UrlConfig) => RPC_URLS[chain.name]
+export const paymasterUrl = ({ chain }: UrlConfig) =>
+ `https://api.pimlico.io/v2/${PIMLICO_NETWORK_NAMES[chain.name]}/rpc?apikey=${apiKey}`
+export const bundlerUrl = ({ chain }: UrlConfig) =>
+ `https://api.pimlico.io/v1/${PIMLICO_NETWORK_NAMES[chain.name]}/rpc?apikey=${apiKey}`
+
+
+const publicClient = ({ chain }: UrlConfig) => createPublicClient({
+ transport: http(publicRPCUrl({ chain })),
+})
+
+export const approvePaymasterUSDCSpend = (chain: Chain) => {
+ // Approve paymaster to spend USDC on our behalf
+ const approveData = approveUSDCSpendCallData({
+ to: PAYMASTER_ADDRESSES[chain.name],
+ amount: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn
+ })
+
+ // GENERATE THE CALLDATA FOR USEROP TO SEND TO THE SMART ACCOUNT
+ const dest = USDC_ADDRESSES[chain.name] // Execute tx in USDC contract
+ const value = 0n
+ const data = approveData // Execute approve call
+
+ return generateUserOperationExecuteCallData({ dest, value, data })
+}
+
+export const approveUSDCSpendCallData = ({ to, amount }: { to: Hex, amount: bigint }) => {
+ return encodeFunctionData({
+ abi: [
+ {
+ inputs: [
+ { name: "_spender", type: "address" },
+ { name: "_value", type: "uint256" }
+ ],
+ name: "approve",
+ outputs: [{ name: "", type: "bool" }],
+ payable: false,
+ stateMutability: "nonpayable",
+ type: "function"
+ }
+ ],
+ args: [to, amount]
+ })
+}
+
+// Wraps the call data in the execute function in order to send via UserOperation
+export const generateUserOperationExecuteCallData = ({ dest, data, value }: { dest: Hex, data: Hex, value: bigint }) => {
+ return encodeFunctionData({
+ abi: [
+ {
+ inputs: [
+ { name: "dest", type: "address" },
+ { name: "value", type: "uint256" },
+ { name: "func", type: "bytes" }
+ ],
+ name: "execute",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function"
+ }
+ ],
+ args: [dest, value, data]
+ })
+}
+
+
+export const getUSDCBalance = async ({ address, chain }: { address: Hex, chain: Chain }) => {
+ return publicClient({ chain }).readContract({
+ abi: [
+ {
+ inputs: [{ name: "_owner", type: "address" }],
+ name: "balanceOf",
+ outputs: [{ name: "balance", type: "uint256" }],
+ type: "function",
+ stateMutability: "view"
+ }
+ ],
+ address: USDC_ADDRESSES[chain.name],
+ functionName: "balanceOf",
+ args: [address]
+ })
+}
\ No newline at end of file
diff --git a/advanced/wallets/react-wallet-v2/src/utils/WalletConnectUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/WalletConnectUtil.ts
index 24f1105..b2fea49 100644
--- a/advanced/wallets/react-wallet-v2/src/utils/WalletConnectUtil.ts
+++ b/advanced/wallets/react-wallet-v2/src/utils/WalletConnectUtil.ts
@@ -40,11 +40,11 @@ export async function updateSignClientChainId(chainId: string, address: string)
[namespace]: {
...session.namespaces[namespace],
chains: [
- ...new Set([chainId].concat(Array.from(session.namespaces[namespace].chains || [])))
+ ...new Set([chainId].concat(Array.from(session?.namespaces?.[namespace]?.chains || [])))
],
accounts: [
...new Set(
- [`${chainId}:${address}`].concat(Array.from(session.namespaces[namespace].accounts))
+ [`${chainId}:${address}`].concat(Array.from(session?.namespaces?.[namespace]?.accounts || []))
)
]
}
diff --git a/advanced/wallets/react-wallet-v2/src/views/RequestModal.tsx b/advanced/wallets/react-wallet-v2/src/views/RequestModal.tsx
index cd879ed..64c1251 100644
--- a/advanced/wallets/react-wallet-v2/src/views/RequestModal.tsx
+++ b/advanced/wallets/react-wallet-v2/src/views/RequestModal.tsx
@@ -18,7 +18,6 @@ interface IProps {
intention?: string
infoBoxCondition?: boolean
infoBoxText?: string
- disabledApprove?: boolean
approveLoader?: LoaderProps
rejectLoader?: LoaderProps
}
@@ -32,7 +31,6 @@ export default function RequestModal({
intention,
infoBoxCondition,
infoBoxText,
- disabledApprove
}: IProps) {
const { currentRequestVerifyContext } = useSnapshot(SettingsStore.state)
const isScam = currentRequestVerifyContext?.verified.isScam
@@ -65,14 +63,12 @@ export default function RequestModal({
rejectLoader={rejectLoader}
infoBoxCondition={infoBoxCondition}
infoBoxText={infoBoxText}
- disabledApprove={disabledApprove}
/>
>
)
}, [
approveLoader,
children,
- disabledApprove,
infoBoxCondition,
infoBoxText,
intention,
diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx
index 472763f..e29b6ea 100644
--- a/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx
+++ b/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx
@@ -8,7 +8,7 @@ import CloseIcon from '@mui/icons-material/Close'
import ModalStore from '@/store/ModalStore'
import { cosmosAddresses } from '@/utils/CosmosWalletUtil'
-import { eip155Addresses } from '@/utils/EIP155WalletUtil'
+import { createOrRestoreEIP155Wallet, eip155Addresses, eip155Wallets } from '@/utils/EIP155WalletUtil'
import { polkadotAddresses } from '@/utils/PolkadotWalletUtil'
import { multiversxAddresses } from '@/utils/MultiversxWalletUtil'
import { tronAddresses } from '@/utils/TronWalletUtil'
@@ -18,7 +18,7 @@ import { nearAddresses } from '@/utils/NearWalletUtil'
import { kadenaAddresses } from '@/utils/KadenaWalletUtil'
import { styledToast } from '@/utils/HelperUtil'
import { web3wallet } from '@/utils/WalletConnectUtil'
-import { EIP155_CHAINS, EIP155_SIGNING_METHODS } from '@/data/EIP155Data'
+import { EIP155Chain, EIP155_CHAINS, EIP155_SIGNING_METHODS } from '@/data/EIP155Data'
import { COSMOS_MAINNET_CHAINS, COSMOS_SIGNING_METHODS } from '@/data/COSMOSData'
import { KADENA_CHAINS, KADENA_SIGNING_METHODS } from '@/data/KadenaData'
import { MULTIVERSX_CHAINS, MULTIVERSX_SIGNING_METHODS } from '@/data/MultiversxData'
@@ -31,8 +31,12 @@ import ChainDataMini from '@/components/ChainDataMini'
import ChainAddressMini from '@/components/ChainAddressMini'
import { getChainData } from '@/data/chainsUtil'
import RequestModal from './RequestModal'
+import { SmartAccountLib } from '@/lib/SmartAccountLib'
+import { Hex } from 'viem'
+import ChainSmartAddressMini from '@/components/ChainSmartAddressMini'
import { useSnapshot } from 'valtio'
import SettingsStore from '@/store/SettingsStore'
+import { allowedChains } from '@/utils/SmartAccountUtils'
const StyledText = styled(Text, {
fontWeight: 400
@@ -43,6 +47,7 @@ const StyledSpan = styled('span', {
} as any)
export default function SessionProposalModal() {
+ const { smartAccountSponsorshipEnabled } = useSnapshot(SettingsStore.state)
// Get proposal data and wallet address from store
const data = useSnapshot(ModalStore.state)
const proposal = data?.data?.proposal as SignClientTypes.EventArguments['session_proposal']
@@ -174,6 +179,11 @@ export default function SessionProposalModal() {
[requestedChains]
)
+ const smartAccountChains = useMemo(
+ () => supportedChains.filter(chain =>(chain as any)?.smartAccountEnabled),
+ [supportedChains]
+ )
+
// get required chains that are not supported by the wallet
const notSupportedChains = useMemo(() => {
if (!proposal) return []
@@ -225,6 +235,21 @@ export default function SessionProposalModal() {
supportedNamespaces
})
+ // TODO: improve for multi network
+ console.log('Checking if SA is deployed', eip155Wallets[eip155Addresses[0]])
+ const chainId = namespaces['eip155'].chains?.[0]
+ const smartAccountClient = new SmartAccountLib({
+ privateKey: eip155Wallets[eip155Addresses[0]].getPrivateKey() as Hex,
+ chain: allowedChains.find(chain => chain.id.toString() === chainId)!,
+ sponsored: smartAccountSponsorshipEnabled,
+ })
+ const isDeployed = await smartAccountClient.checkIfSmartAccountDeployed()
+ console.log('isDeployed', isDeployed)
+
+ if (isDeployed) {
+ namespaces.eip155.accounts = [...namespaces.eip155.accounts, `eip155:5:${smartAccountClient.address}`]
+ }
+
console.log('approving namespaces:', namespaces)
try {
@@ -241,7 +266,7 @@ export default function SessionProposalModal() {
}
setIsLoadingApprove(false)
ModalStore.close()
- }, [proposal, supportedNamespaces])
+ }, [proposal, supportedNamespaces, smartAccountSponsorshipEnabled])
// Hanlde reject action
// eslint-disable-next-line react-hooks/rules-of-hooks
@@ -273,7 +298,6 @@ export default function SessionProposalModal() {
rejectLoader={{ active: isLoadingReject }}
infoBoxCondition={notSupportedChains.length > 0}
infoBoxText={`The following required chains are not supported by your wallet - ${notSupportedChains.toString()}`}
- disabledApprove={notSupportedChains.length > 0}
>
@@ -309,6 +333,16 @@ export default function SessionProposalModal() {
)
})}
+
+ Smart Accounts
+ {smartAccountChains.length &&
+ smartAccountChains.map((chain, i) => {
+ return (
+
+
+
+ )
+ })}
diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionSignTypedDataModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionSignTypedDataModal.tsx
index 0fada9f..ec9c01c 100644
--- a/advanced/wallets/react-wallet-v2/src/views/SessionSignTypedDataModal.tsx
+++ b/advanced/wallets/react-wallet-v2/src/views/SessionSignTypedDataModal.tsx
@@ -75,6 +75,8 @@ export default function SessionSignTypedDataModal() {
metadata={requestSession.peer.metadata}
onApprove={onApprove}
onReject={onReject}
+ approveLoader={{ active: isLoadingApprove }}
+ rejectLoader={{ active: isLoadingReject }}
>