feat: Smart Account relay connection + paymaster base (#351)

* feat: added Goerli smart accounts

* chore: remove viem version modifier. Split useSmartAccount usage into multiliune

* feat: fix error handling

* feat: added smart wallets to proposal modal

* feat: added paymasters to be able to freely transact on testnet with Pimlico sponsorship. Serialize SA addresses in relay response

* feat: adapted SmartWalletLib interface to be able to use same methods as EIP155Lib. Hooked up all operations on EIP155RequestHandler to smart accounts.

* chore: remove logs

* feat: added spinner to modal footer. Use it when handling eip155 requests

* feat: added sponsorship toggle in settings

* feat: upgraded to permissionless@0.0.16

* fix: conflicts after merge

* chore: unify loaders

* chore: kristoph refactor + multi chain support

* fix: chain Id issue. remove unused logs
This commit is contained in:
tomiir 2024-02-01 16:23:32 -06:00 committed by GitHub
parent 46bfda7ecf
commit 9439c9af54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 544 additions and 131 deletions

View File

@ -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 <Spinner />
return (
<ChainAddressMini address={address}/>
)
}

View File

@ -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({
<span>{infoBoxText || ''}</span>
</Row>
)}
<Row justify="space-between">
<Row justify="space-between" align='center'>
<Button
auto
flat
style={{ color: 'white', backgroundColor: 'grey' }}
onPress={onReject}
data-testid="session-reject-button"
disabled={rejectLoader?.active}
>
{rejectLoader && rejectLoader.active ? (
<Loading size="md" type="points" color={rejectLoader.color || 'white'} />
@ -64,7 +63,7 @@ export default function ModalFooter({
auto
flat
color={approveButtonColor}
disabled={disabledApprove}
disabled={approveLoader?.active}
onPress={onApprove}
data-testid="session-approve-button"
>

View File

@ -5,9 +5,10 @@ import { updateSignClientChainId } from '@/utils/WalletConnectUtil'
import { Avatar, Button, Text, Tooltip, Loading } from '@nextui-org/react'
import { eip155Wallets } from '@/utils/EIP155WalletUtil'
import Image from 'next/image'
import { useState, useEffect } from 'react'
import { useState } from 'react'
import { useSnapshot } from 'valtio'
import useSmartAccount from '@/hooks/useSmartAccount'
import { Chain, FAUCET_URLS, allowedChains } from '@/utils/SmartAccountUtils'
interface Props {
name: string
@ -28,13 +29,14 @@ export default function SmartAccountCard({
}: Props) {
const [copied, setCopied] = useState(false)
const { activeChainId } = useSnapshot(SettingsStore.state)
const chain = allowedChains.find((c) => c.id.toString() === chainId.split(':')[1]) as Chain
const {
deploy,
isDeployed,
address: smartAccountAddress,
loading,
sendTestTransaction,
} = useSmartAccount(eip155Wallets[address].getPrivateKey() as `0x${string}`)
} = useSmartAccount(eip155Wallets[address].getPrivateKey() as `0x${string}`, chain)
function onCopy() {
navigator?.clipboard?.writeText(address)
@ -57,8 +59,6 @@ export default function SmartAccountCard({
}
}
const getFaucetUrl = () => `https://${name?.toLowerCase()?.replace('ethereum', '')?.trim()}faucet.com`
return (
<ChainCard rgb={rgb} flexDirection="row" alignItems="center" flexWrap="wrap">
<Avatar src={logo} />
@ -124,7 +124,7 @@ export default function SmartAccountCard({
disabled={!isActiveChain || loading}
size="sm"
css={{ marginTop: 20, width: '100%' }}
onClick={() => window.open(getFaucetUrl(), '_blank')}
onClick={() => window.open(FAUCET_URLS[chain?.name], '_blank')}
>
{name} Faucet
</Button>

View File

@ -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<string, EIP155Chain> = {
'eip155:1': {
chainId: 1,
name: 'Ethereum',
@ -64,7 +64,7 @@ export const EIP155_MAINNET_CHAINS = {
}
}
export const EIP155_TEST_CHAINS: Record<string,EIP155TestChain> = {
export const EIP155_TEST_CHAINS: Record<string,EIP155Chain> = {
'eip155:5': {
chainId: 5,
name: 'Ethereum Goerli',
@ -81,6 +81,7 @@ export const EIP155_TEST_CHAINS: Record<string,EIP155TestChain> = {
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<string,EIP155TestChain> = {
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,

View File

@ -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)

View File

@ -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<SmartAccountLib>();
const [isDeployed, setIsDeployed] = useState(false)
const [address, setAddress] = useState<`0x${string}`>()
const [address, setAddress] = useState<Hex>()
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')
if (!signerPrivateKey || !chain) return
const smartAccountClient = new SmartAccountLib({
chain,
privateKey: signerPrivateKey,
sponsored: smartAccountSponsorshipEnabled,
})
setClient(smartAccountClient)
}, [signerPrivateKey])
}, [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 {

View File

@ -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)

View File

@ -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)
}

View File

@ -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;
public constructor(privateKey: `0x${string}`, chain: SmartAccountEnabledChains = 'goerli') {
if (!bundlerApiKey) {
throw new Error('Missing required data in SmartAccountSdk')
private publicClient: PublicClient
private paymasterClient: PimlicoPaymasterClient
private bundlerClient: BundlerClient & BundlerActions & PimlicoBundlerActions
private signerClient: WalletClient
#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.chain = chain
this.sponsored = sponsored
this.#signerPrivateKey = privateKey
}
this.publicClient = createPublicClient({
transport: http(publicRPCUrl({ chain: this.chain }))
})
// -- Public ------------------------------------------------------------------
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({
private getSmartAccountClient = async (
sponsorUserOperation?: (args: {
userOperation: UserOperation
entryPoint: Address
}) => Promise<UserOperation>
) => {
const account = await this.getAccount()
return createSmartAccountClient({
account,
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,
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`)
}

View File

@ -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])

View File

@ -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() {
<Divider y={2} />
<Text h4 css={{ marginBottom: '$5' }}>
Smart Account Sponsorship (Pimlico)
</Text>
<Row justify="space-between" align="center">
<Switch
checked={smartAccountSponsorshipEnabled}
onChange={SettingsStore.toggleSmartAccountSponsorship}
data-testid="settings-toggle-smart-account-sponsorship"
/>
<Text>{smartAccountSponsorshipEnabled ? 'Enabled' : 'Disabled'}</Text>
</Row>
<Divider y={2} />
<Row justify="space-between" align="center">
<Text h4 css={{ marginBottom: '$5' }}>
Relayer Region

View File

@ -20,6 +20,7 @@ interface State {
activeChainId: string
currentRequestVerifyContext?: Verify.Context
sessions: SessionTypes.Struct[]
smartAccountSponsorshipEnabled: boolean
}
/**
@ -39,7 +40,8 @@ const state = proxy<State>({
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
}
}

View File

@ -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<SignClientTypes.EventArguments['session_request'], 'verifyContext'>
const getWallet = async (params: any) => {
const requestParams: Array<any> = 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<SmartAccountLib>
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)

View File

@ -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<Chain['name'], Hex> = {
Sepolia: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
'Polygon Mumbai': '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
'Goerli': '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'
}
// Paymasters
// https://docs.pimlico.io/paymaster/erc20-paymaster/contract-addresses
export const PAYMASTER_ADDRESSES: Record<Chain['name'], Hex> = {
Sepolia: '0x0000000000325602a77416A16136FDafd04b299f',
'Polygon Mumbai': '0x000000000009B901DeC1aaB9389285965F49D387',
Goerli: '0xEc43912D8C772A0Eba5a27ea5804Ba14ab502009'
}
// USDC
export const USDC_ADDRESSES: Record<Chain['name'], Hex> = {
Sepolia: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
'Polygon Mumbai': '0x9999f7fea5938fd3b1e26a12c3f2fb024e194f97',
Goerli: '0x07865c6e87b9f70255377e024ace6630c1eaa37f'
}
// RPC URLs
export const RPC_URLS: Record<Chain['name'], string> = {
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<Chain['name'], string> = {
Sepolia: 'sepolia',
'Polygon Mumbai': 'mumbai',
Goerli: 'goerli'
}
export const FAUCET_URLS: Record<Chain['name'], string> = {
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]
})
}

View File

@ -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 || []))
)
]
}

View File

@ -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,

View File

@ -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}
>
<Row>
<Col>
@ -309,6 +333,16 @@ export default function SessionProposalModal() {
</Row>
)
})}
<Row style={{ color: 'GrayText' }}>Smart Accounts</Row>
{smartAccountChains.length &&
smartAccountChains.map((chain, i) => {
return (
<Row key={i}>
<ChainSmartAddressMini namespace={chain?.namespace!} />
</Row>
)
})}
</Grid>
<Grid>
<Row style={{ color: 'GrayText' }} justify="flex-end">

View File

@ -75,6 +75,8 @@ export default function SessionSignTypedDataModal() {
metadata={requestSession.peer.metadata}
onApprove={onApprove}
onReject={onReject}
approveLoader={{ active: isLoadingApprove }}
rejectLoader={{ active: isLoadingReject }}
>
<RequesDetailsCard chains={[chainId ?? '']} protocol={requestSession.relay.protocol} />
<Divider y={1} />