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:
parent
46bfda7ecf
commit
9439c9af54
@ -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}/>
|
||||||
|
)
|
||||||
|
}
|
@ -11,7 +11,6 @@ interface Props {
|
|||||||
onReject: () => void
|
onReject: () => void
|
||||||
infoBoxCondition?: boolean
|
infoBoxCondition?: boolean
|
||||||
infoBoxText?: string
|
infoBoxText?: string
|
||||||
disabledApprove?: boolean
|
|
||||||
approveLoader?: LoaderProps
|
approveLoader?: LoaderProps
|
||||||
rejectLoader?: LoaderProps
|
rejectLoader?: LoaderProps
|
||||||
}
|
}
|
||||||
@ -23,7 +22,6 @@ export default function ModalFooter({
|
|||||||
rejectLoader,
|
rejectLoader,
|
||||||
infoBoxCondition,
|
infoBoxCondition,
|
||||||
infoBoxText,
|
infoBoxText,
|
||||||
disabledApprove
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { currentRequestVerifyContext } = useSnapshot(SettingsStore.state)
|
const { currentRequestVerifyContext } = useSnapshot(SettingsStore.state)
|
||||||
const validation = currentRequestVerifyContext?.verified.validation
|
const validation = currentRequestVerifyContext?.verified.validation
|
||||||
@ -46,13 +44,14 @@ export default function ModalFooter({
|
|||||||
<span>{infoBoxText || ''}</span>
|
<span>{infoBoxText || ''}</span>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
<Row justify="space-between">
|
<Row justify="space-between" align='center'>
|
||||||
<Button
|
<Button
|
||||||
auto
|
auto
|
||||||
flat
|
flat
|
||||||
style={{ color: 'white', backgroundColor: 'grey' }}
|
style={{ color: 'white', backgroundColor: 'grey' }}
|
||||||
onPress={onReject}
|
onPress={onReject}
|
||||||
data-testid="session-reject-button"
|
data-testid="session-reject-button"
|
||||||
|
disabled={rejectLoader?.active}
|
||||||
>
|
>
|
||||||
{rejectLoader && rejectLoader.active ? (
|
{rejectLoader && rejectLoader.active ? (
|
||||||
<Loading size="md" type="points" color={rejectLoader.color || 'white'} />
|
<Loading size="md" type="points" color={rejectLoader.color || 'white'} />
|
||||||
@ -64,7 +63,7 @@ export default function ModalFooter({
|
|||||||
auto
|
auto
|
||||||
flat
|
flat
|
||||||
color={approveButtonColor}
|
color={approveButtonColor}
|
||||||
disabled={disabledApprove}
|
disabled={approveLoader?.active}
|
||||||
onPress={onApprove}
|
onPress={onApprove}
|
||||||
data-testid="session-approve-button"
|
data-testid="session-approve-button"
|
||||||
>
|
>
|
||||||
|
@ -5,9 +5,10 @@ import { updateSignClientChainId } from '@/utils/WalletConnectUtil'
|
|||||||
import { Avatar, Button, Text, Tooltip, Loading } from '@nextui-org/react'
|
import { Avatar, Button, Text, Tooltip, Loading } from '@nextui-org/react'
|
||||||
import { eip155Wallets } from '@/utils/EIP155WalletUtil'
|
import { eip155Wallets } from '@/utils/EIP155WalletUtil'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState } from 'react'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import useSmartAccount from '@/hooks/useSmartAccount'
|
import useSmartAccount from '@/hooks/useSmartAccount'
|
||||||
|
import { Chain, FAUCET_URLS, allowedChains } from '@/utils/SmartAccountUtils'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string
|
name: string
|
||||||
@ -28,13 +29,14 @@ export default function SmartAccountCard({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const { activeChainId } = useSnapshot(SettingsStore.state)
|
const { activeChainId } = useSnapshot(SettingsStore.state)
|
||||||
|
const chain = allowedChains.find((c) => c.id.toString() === chainId.split(':')[1]) as Chain
|
||||||
const {
|
const {
|
||||||
deploy,
|
deploy,
|
||||||
isDeployed,
|
isDeployed,
|
||||||
address: smartAccountAddress,
|
address: smartAccountAddress,
|
||||||
loading,
|
loading,
|
||||||
sendTestTransaction,
|
sendTestTransaction,
|
||||||
} = useSmartAccount(eip155Wallets[address].getPrivateKey() as `0x${string}`)
|
} = useSmartAccount(eip155Wallets[address].getPrivateKey() as `0x${string}`, chain)
|
||||||
|
|
||||||
function onCopy() {
|
function onCopy() {
|
||||||
navigator?.clipboard?.writeText(address)
|
navigator?.clipboard?.writeText(address)
|
||||||
@ -56,9 +58,7 @@ export default function SmartAccountCard({
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFaucetUrl = () => `https://${name?.toLowerCase()?.replace('ethereum', '')?.trim()}faucet.com`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChainCard rgb={rgb} flexDirection="row" alignItems="center" flexWrap="wrap">
|
<ChainCard rgb={rgb} flexDirection="row" alignItems="center" flexWrap="wrap">
|
||||||
<Avatar src={logo} />
|
<Avatar src={logo} />
|
||||||
@ -124,7 +124,7 @@ export default function SmartAccountCard({
|
|||||||
disabled={!isActiveChain || loading}
|
disabled={!isActiveChain || loading}
|
||||||
size="sm"
|
size="sm"
|
||||||
css={{ marginTop: 20, width: '100%' }}
|
css={{ marginTop: 20, width: '100%' }}
|
||||||
onClick={() => window.open(getFaucetUrl(), '_blank')}
|
onClick={() => window.open(FAUCET_URLS[chain?.name], '_blank')}
|
||||||
>
|
>
|
||||||
{name} Faucet
|
{name} Faucet
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
export type TEIP155Chain = keyof typeof EIP155_CHAINS
|
export type TEIP155Chain = keyof typeof EIP155_CHAINS
|
||||||
|
|
||||||
export type EIP155TestChain = {
|
export type EIP155Chain = {
|
||||||
chainId: number
|
chainId: number
|
||||||
name: string
|
name: string
|
||||||
logo: string
|
logo: string
|
||||||
@ -21,7 +21,7 @@ export type EIP155TestChain = {
|
|||||||
/**
|
/**
|
||||||
* Chains
|
* Chains
|
||||||
*/
|
*/
|
||||||
export const EIP155_MAINNET_CHAINS = {
|
export const EIP155_MAINNET_CHAINS: Record<string, EIP155Chain> = {
|
||||||
'eip155:1': {
|
'eip155:1': {
|
||||||
chainId: 1,
|
chainId: 1,
|
||||||
name: 'Ethereum',
|
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': {
|
'eip155:5': {
|
||||||
chainId: 5,
|
chainId: 5,
|
||||||
name: 'Ethereum Goerli',
|
name: 'Ethereum Goerli',
|
||||||
@ -81,6 +81,7 @@ export const EIP155_TEST_CHAINS: Record<string,EIP155TestChain> = {
|
|||||||
rgb: '99, 125, 234',
|
rgb: '99, 125, 234',
|
||||||
rpc: 'https://rpc.sepolia.org',
|
rpc: 'https://rpc.sepolia.org',
|
||||||
namespace: 'eip155',
|
namespace: 'eip155',
|
||||||
|
smartAccountEnabled: true,
|
||||||
},
|
},
|
||||||
'eip155:43113': {
|
'eip155:43113': {
|
||||||
chainId: 43113,
|
chainId: 43113,
|
||||||
@ -96,7 +97,8 @@ export const EIP155_TEST_CHAINS: Record<string,EIP155TestChain> = {
|
|||||||
logo: '/chain-logos/eip155-137.png',
|
logo: '/chain-logos/eip155-137.png',
|
||||||
rgb: '130, 71, 229',
|
rgb: '130, 71, 229',
|
||||||
rpc: 'https://matic-mumbai.chainstacklabs.com',
|
rpc: 'https://matic-mumbai.chainstacklabs.com',
|
||||||
namespace: 'eip155'
|
namespace: 'eip155',
|
||||||
|
smartAccountEnabled: true,
|
||||||
},
|
},
|
||||||
'eip155:420': {
|
'eip155:420': {
|
||||||
chainId: 420,
|
chainId: 420,
|
||||||
|
@ -49,7 +49,7 @@ export default function useInitialization() {
|
|||||||
// restart transport if relayer region changes
|
// restart transport if relayer region changes
|
||||||
const onRelayerRegionChange = useCallback(() => {
|
const onRelayerRegionChange = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
web3wallet.core.relayer.restartTransport(relayerRegionURL)
|
web3wallet?.core?.relayer.restartTransport(relayerRegionURL)
|
||||||
prevRelayerURLValue.current = relayerRegionURL
|
prevRelayerURLValue.current = relayerRegionURL
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
alert(err)
|
alert(err)
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
import { SmartAccountLib } from "@/lib/SmartAccountLib";
|
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 { useCallback, useEffect, useState } from "react";
|
||||||
|
import { useSnapshot } from "valtio";
|
||||||
|
import { Hex } from "viem";
|
||||||
|
import { styledToast } from "@/utils/HelperUtil";
|
||||||
import { TransactionExecutionError } from "viem";
|
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 [loading, setLoading] = useState(false)
|
||||||
const [client, setClient] = useState<SmartAccountLib>();
|
const [client, setClient] = useState<SmartAccountLib>();
|
||||||
const [isDeployed, setIsDeployed] = useState(false)
|
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) => {
|
const execute = useCallback(async (callback: () => void) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
await callback()
|
const res = await callback()
|
||||||
|
console.log('result:', res)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
@ -34,13 +40,22 @@ export default function useSmartAccount(signerPrivateKey: `0x${string}`) {
|
|||||||
|
|
||||||
const sendTestTransaction = useCallback(async () => {
|
const sendTestTransaction = useCallback(async () => {
|
||||||
if (!client) return
|
if (!client) return
|
||||||
execute(client?.sendTestTransaction)
|
execute(() => client?.sendTransaction({
|
||||||
|
to: VITALIK_ADDRESS,
|
||||||
|
value: 0n,
|
||||||
|
data: '0x',
|
||||||
|
}))
|
||||||
}, [client, execute])
|
}, [client, execute])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const smartAccountClient = new SmartAccountLib(signerPrivateKey, 'goerli')
|
if (!signerPrivateKey || !chain) return
|
||||||
setClient(smartAccountClient)
|
const smartAccountClient = new SmartAccountLib({
|
||||||
}, [signerPrivateKey])
|
chain,
|
||||||
|
privateKey: signerPrivateKey,
|
||||||
|
sponsored: smartAccountSponsorshipEnabled,
|
||||||
|
})
|
||||||
|
setClient(smartAccountClient)
|
||||||
|
}, [signerPrivateKey, smartAccountSponsorshipEnabled, chain])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
client?.checkIfSmartAccountDeployed()
|
client?.checkIfSmartAccountDeployed()
|
||||||
@ -48,7 +63,7 @@ export default function useSmartAccount(signerPrivateKey: `0x${string}`) {
|
|||||||
setIsDeployed(deployed)
|
setIsDeployed(deployed)
|
||||||
setAddress(client?.address)
|
setAddress(client?.address)
|
||||||
})
|
})
|
||||||
}, [client])
|
}, [client, chain])
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -117,7 +117,7 @@ export default function useWalletConnectEventsManager(initialized: boolean) {
|
|||||||
* Set up WalletConnect event listeners
|
* Set up WalletConnect event listeners
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialized) {
|
if (initialized && web3wallet) {
|
||||||
//sign
|
//sign
|
||||||
web3wallet.on('session_proposal', onSessionProposal)
|
web3wallet.on('session_proposal', onSessionProposal)
|
||||||
web3wallet.on('session_request', onSessionRequest)
|
web3wallet.on('session_request', onSessionRequest)
|
||||||
|
@ -39,7 +39,7 @@ export default class EIP155Lib {
|
|||||||
return this.wallet.signMessage(message)
|
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)
|
return this.wallet._signTypedData(domain, types, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,134 +1,267 @@
|
|||||||
import { createSmartAccountClient } from 'permissionless'
|
import { BundlerActions, BundlerClient, bundlerActions, createSmartAccountClient, getAccountNonce } from 'permissionless'
|
||||||
import { privateKeyToSafeSmartAccount } from 'permissionless/accounts'
|
import { privateKeyToSafeSmartAccount } from 'permissionless/accounts'
|
||||||
import * as chains from 'viem/chains'
|
import * as chains from 'viem/chains'
|
||||||
import { privateKeyToAccount } from 'viem/accounts'
|
import { privateKeyToAccount } from 'viem/accounts'
|
||||||
import { type Chain, createWalletClient, formatEther, createPublicClient, http } from 'viem'
|
import { createWalletClient, formatEther, createPublicClient, http, Address, Hex, PublicClient, createClient, WalletClient } from 'viem'
|
||||||
import { createPimlicoBundlerClient } from 'permissionless/clients/pimlico'
|
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 -----------------------------------------------------------------
|
// -- 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
|
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID
|
||||||
|
|
||||||
// -- Sdk ---------------------------------------------------------------------
|
// -- Sdk ---------------------------------------------------------------------
|
||||||
export class SmartAccountLib {
|
export class SmartAccountLib {
|
||||||
public chain: Chain
|
public chain: Chain
|
||||||
private bundlerApiKey: string
|
|
||||||
#signerPrivateKey: `0x${string}`;
|
|
||||||
public isDeployed: boolean = false;
|
public isDeployed: boolean = false;
|
||||||
public address?: `0x${string}`;
|
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') {
|
#signerPrivateKey: `0x${string}`;
|
||||||
if (!bundlerApiKey) {
|
|
||||||
throw new Error('Missing required data in SmartAccountSdk')
|
|
||||||
|
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 -----------------------------------------------------------------
|
||||||
private getWalletConnectTransport = () => http(
|
private getSmartAccountClient = async (
|
||||||
`https://rpc.walletconnect.com/v1/?chainId=EIP155:${this.chain.id}&projectId=${projectId}`,
|
sponsorUserOperation?: (args: {
|
||||||
{ retryDelay: 1000 }
|
userOperation: UserOperation
|
||||||
);
|
entryPoint: Address
|
||||||
|
}) => Promise<UserOperation>
|
||||||
private getBundlerTransport = () => http(
|
) => {
|
||||||
`https://api.pimlico.io/v1/${this.chain.name.toLowerCase()}/rpc?apikey=${this.bundlerApiKey}`,
|
const account = await this.getAccount()
|
||||||
{ retryDelay: 1000 }
|
return createSmartAccountClient({
|
||||||
);
|
account,
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
chain: this.chain,
|
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 () => {
|
public getNonce = async () => {
|
||||||
const smartAccount = await privateKeyToSafeSmartAccount(this.getPublicClient(), {
|
const smartAccountClient = await this.getSmartAccountClient()
|
||||||
privateKey: this.#signerPrivateKey,
|
return getAccountNonce(this.publicClient, {
|
||||||
safeVersion: '1.4.1',
|
sender: smartAccountClient.account.address as Hex,
|
||||||
entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'
|
entryPoint: ENTRYPOINT_ADDRESSES[this.chain.name]
|
||||||
})
|
|
||||||
|
|
||||||
return createSmartAccountClient({
|
|
||||||
account: smartAccount,
|
|
||||||
chain: this.chain,
|
|
||||||
transport: this.getBundlerTransport()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private prefundSmartAccount = async (address: `0x${string}`) => {
|
private prefundSmartAccount = async (address: `0x${string}`) => {
|
||||||
const signerAccountViemClient = this.getSignerClient();
|
if (this.sponsored) {
|
||||||
const publicClient = this.getPublicClient();
|
return
|
||||||
const bundlerClient = this.getBundlerClient();
|
}
|
||||||
const smartAccountBalance = await publicClient.getBalance({ address })
|
|
||||||
|
const smartAccountBalance = await this.publicClient.getBalance({ address })
|
||||||
|
|
||||||
console.log(`Smart Account Balance: ${formatEther(smartAccountBalance)} ETH`)
|
console.log(`Smart Account Balance: ${formatEther(smartAccountBalance)} ETH`)
|
||||||
if (smartAccountBalance < 1n) {
|
if (smartAccountBalance < 1n) {
|
||||||
console.log(`Smart Account has no balance. Starting prefund`)
|
console.log(`Smart Account has no balance. Starting prefund`)
|
||||||
const { fast: fastPrefund } = await bundlerClient.getUserOperationGasPrice()
|
const { fast: fastPrefund } = await this.bundlerClient.getUserOperationGasPrice()
|
||||||
const prefundHash = await signerAccountViemClient.sendTransaction({
|
const prefundHash = await this.signerClient.sendTransaction({
|
||||||
to: address,
|
to: address,
|
||||||
|
chain: this.chain,
|
||||||
|
account: this.signerClient.account!,
|
||||||
value: 10000000000000000n,
|
value: 10000000000000000n,
|
||||||
maxFeePerGas: fastPrefund.maxFeePerGas,
|
maxFeePerGas: fastPrefund.maxFeePerGas,
|
||||||
maxPriorityFeePerGas: fastPrefund.maxPriorityFeePerGas
|
maxPriorityFeePerGas: fastPrefund.maxPriorityFeePerGas
|
||||||
})
|
})
|
||||||
|
|
||||||
await publicClient.waitForTransactionReceipt({ hash: prefundHash })
|
await this.publicClient.waitForTransactionReceipt({ hash: prefundHash })
|
||||||
console.log(`Prefunding Success`)
|
console.log(`Prefunding Success`)
|
||||||
|
|
||||||
const newSmartAccountBalance = await publicClient.getBalance({ address })
|
const newSmartAccountBalance = await this.publicClient.getBalance({ address })
|
||||||
console.log(
|
console.log(
|
||||||
`Smart Account Balance: ${formatEther(newSmartAccountBalance)} ETH`
|
`Smart Account Balance: ${formatEther(newSmartAccountBalance)} ETH`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// By default first transaction will deploy the smart contract if it hasn't been deployed yet
|
private getSmartAccountUSDCBalance = async () => {
|
||||||
public sendTestTransaction = async () => {
|
const params = {
|
||||||
const publicClient = this.getPublicClient();
|
abi: [
|
||||||
const bundlerClient = this.getBundlerClient();
|
{
|
||||||
const smartAccountClient = await this.getSmartAccountClient();
|
inputs: [{ name: "_owner", type: "address" }],
|
||||||
const { fast: testGas, } = await bundlerClient.getUserOperationGasPrice()
|
name: "balanceOf",
|
||||||
|
outputs: [{ name: "balance", type: "uint256" }],
|
||||||
const testHash = await smartAccountClient.sendTransaction({
|
type: "function",
|
||||||
to: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' as `0x${string}`,
|
}
|
||||||
value: 0n,
|
],
|
||||||
maxFeePerGas: testGas.maxFeePerGas,
|
address: USDC_ADDRESSES[this.chain.name] as Hex,
|
||||||
maxPriorityFeePerGas: testGas.maxPriorityFeePerGas,
|
functionName: "balanceOf",
|
||||||
})
|
args: [this.address!]
|
||||||
|
}
|
||||||
console.log(`Sending Test Transaction With Hash: ${testHash}`)
|
const usdcBalance = await this.publicClient.readContract(params) as bigint
|
||||||
|
return usdcBalance
|
||||||
await publicClient.waitForTransactionReceipt({ hash: testHash })
|
|
||||||
console.log(`Test Transaction Success`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 () => {
|
public checkIfSmartAccountDeployed = async () => {
|
||||||
console.log('checking if deployed')
|
|
||||||
const smartAccountClient = await this.getSmartAccountClient();
|
const smartAccountClient = await this.getSmartAccountClient();
|
||||||
const publicClient = this.getPublicClient();
|
console.log('Checking if deployed', smartAccountClient.account.address, this.chain.name)
|
||||||
const bytecode = await publicClient.getBytecode({ address: smartAccountClient.account.address })
|
|
||||||
|
const bytecode = await this.publicClient.getBytecode({ address: smartAccountClient.account.address })
|
||||||
this.isDeployed = Boolean(bytecode)
|
this.isDeployed = Boolean(bytecode)
|
||||||
|
|
||||||
console.log(`Smart Account Deployed: ${this.isDeployed}`)
|
console.log(`Smart Account Deployed: ${this.isDeployed}`)
|
||||||
if (this.isDeployed) {
|
if (this.isDeployed) {
|
||||||
this.address = smartAccountClient.account.address
|
this.address = smartAccountClient.account.address
|
||||||
@ -145,7 +278,11 @@ export class SmartAccountLib {
|
|||||||
await this.prefundSmartAccount(smartAccountClient.account.address)
|
await this.prefundSmartAccount(smartAccountClient.account.address)
|
||||||
|
|
||||||
// Step 4: Create account by sending test tx
|
// Step 4: Create account by sending test tx
|
||||||
await this.sendTestTransaction()
|
await this.sendTransaction({
|
||||||
|
to: VITALIK_ADDRESS,
|
||||||
|
value: 0n,
|
||||||
|
data: '0x'
|
||||||
|
})
|
||||||
await this.checkIfSmartAccountDeployed()
|
await this.checkIfSmartAccountDeployed()
|
||||||
console.log(`Account Created`)
|
console.log(`Account Created`)
|
||||||
}
|
}
|
||||||
|
@ -20,11 +20,11 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||||||
useWalletConnectEventsManager(initialized)
|
useWalletConnectEventsManager(initialized)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialized) return
|
if (!initialized) return
|
||||||
web3wallet.core.relayer.on(RELAYER_EVENTS.connect, () => {
|
web3wallet?.core.relayer.on(RELAYER_EVENTS.connect, () => {
|
||||||
styledToast('Network connection is restored!', 'success')
|
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')
|
styledToast('Network connection lost.', 'error')
|
||||||
})
|
})
|
||||||
}, [initialized])
|
}, [initialized])
|
||||||
|
@ -16,6 +16,7 @@ import { tezosWallets } from '@/utils/TezosWalletUtil'
|
|||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const {
|
const {
|
||||||
testNets,
|
testNets,
|
||||||
|
smartAccountSponsorshipEnabled,
|
||||||
eip155Address,
|
eip155Address,
|
||||||
cosmosAddress,
|
cosmosAddress,
|
||||||
solanaAddress,
|
solanaAddress,
|
||||||
@ -53,6 +54,20 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
<Divider y={2} />
|
<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">
|
<Row justify="space-between" align="center">
|
||||||
<Text h4 css={{ marginBottom: '$5' }}>
|
<Text h4 css={{ marginBottom: '$5' }}>
|
||||||
Relayer Region
|
Relayer Region
|
||||||
|
@ -20,6 +20,7 @@ interface State {
|
|||||||
activeChainId: string
|
activeChainId: string
|
||||||
currentRequestVerifyContext?: Verify.Context
|
currentRequestVerifyContext?: Verify.Context
|
||||||
sessions: SessionTypes.Struct[]
|
sessions: SessionTypes.Struct[]
|
||||||
|
smartAccountSponsorshipEnabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,7 +40,8 @@ const state = proxy<State>({
|
|||||||
tezosAddress: '',
|
tezosAddress: '',
|
||||||
kadenaAddress: '',
|
kadenaAddress: '',
|
||||||
relayerRegionURL: '',
|
relayerRegionURL: '',
|
||||||
sessions: []
|
sessions: [],
|
||||||
|
smartAccountSponsorshipEnabled: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -103,10 +105,17 @@ const SettingsStore = {
|
|||||||
toggleTestNets() {
|
toggleTestNets() {
|
||||||
state.testNets = !state.testNets
|
state.testNets = !state.testNets
|
||||||
if (state.testNets) {
|
if (state.testNets) {
|
||||||
|
state.smartAccountSponsorshipEnabled = true
|
||||||
localStorage.setItem('TEST_NETS', 'YES')
|
localStorage.setItem('TEST_NETS', 'YES')
|
||||||
} else {
|
} else {
|
||||||
|
state.smartAccountSponsorshipEnabled = false
|
||||||
localStorage.removeItem('TEST_NETS')
|
localStorage.removeItem('TEST_NETS')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleSmartAccountSponsorship() {
|
||||||
|
if (!state.testNets) return
|
||||||
|
state.smartAccountSponsorshipEnabled = !state.smartAccountSponsorshipEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { EIP155_CHAINS, EIP155_SIGNING_METHODS, TEIP155Chain } from '@/data/EIP155Data'
|
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 { eip155Addresses, eip155Wallets } from '@/utils/EIP155WalletUtil'
|
||||||
import {
|
import {
|
||||||
getSignParamsMessage,
|
getSignParamsMessage,
|
||||||
@ -9,11 +11,41 @@ import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils'
|
|||||||
import { SignClientTypes } from '@walletconnect/types'
|
import { SignClientTypes } from '@walletconnect/types'
|
||||||
import { getSdkError } from '@walletconnect/utils'
|
import { getSdkError } from '@walletconnect/utils'
|
||||||
import { providers } from 'ethers'
|
import { providers } from 'ethers'
|
||||||
|
import { Hex } from 'viem'
|
||||||
|
import { allowedChains } from './SmartAccountUtils'
|
||||||
type RequestEventArgs = Omit<SignClientTypes.EventArguments['session_request'], 'verifyContext'>
|
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) {
|
export async function approveEIP155Request(requestEvent: RequestEventArgs) {
|
||||||
const { params, id } = requestEvent
|
const { params, id } = requestEvent
|
||||||
const { chainId, request } = params
|
const { chainId, request } = params
|
||||||
const wallet = eip155Wallets[getWalletAddressFromParams(eip155Addresses, params)]
|
const wallet = await getWallet(params)
|
||||||
|
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
|
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_V3:
|
||||||
case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V4:
|
case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V4:
|
||||||
try {
|
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
|
// https://github.com/ethers-io/ethers.js/issues/687#issuecomment-714069471
|
||||||
delete types.EIP712Domain
|
delete types.EIP712Domain
|
||||||
const signedData = await wallet._signTypedData(domain, types, data)
|
const signedData = await wallet._signTypedData(domain, types, data, primaryType)
|
||||||
return formatJsonRpcResult(id, signedData)
|
return formatJsonRpcResult(id, signedData)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@ -47,9 +79,10 @@ export async function approveEIP155Request(requestEvent: RequestEventArgs) {
|
|||||||
try {
|
try {
|
||||||
const provider = new providers.JsonRpcProvider(EIP155_CHAINS[chainId as TEIP155Chain].rpc)
|
const provider = new providers.JsonRpcProvider(EIP155_CHAINS[chainId as TEIP155Chain].rpc)
|
||||||
const sendTransaction = request.params[0]
|
const sendTransaction = request.params[0]
|
||||||
const connectedWallet = wallet.connect(provider)
|
const connectedWallet = await wallet.connect(provider)
|
||||||
const { hash } = await connectedWallet.sendTransaction(sendTransaction)
|
const hash = await connectedWallet.sendTransaction(sendTransaction)
|
||||||
return formatJsonRpcResult(id, hash)
|
const receipt = typeof hash === 'string' ? hash : hash?.hash // TODO improve interface
|
||||||
|
return formatJsonRpcResult(id, receipt)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
alert(error.message)
|
alert(error.message)
|
||||||
|
140
advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtils.ts
Normal file
140
advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtils.ts
Normal 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]
|
||||||
|
})
|
||||||
|
}
|
@ -40,11 +40,11 @@ export async function updateSignClientChainId(chainId: string, address: string)
|
|||||||
[namespace]: {
|
[namespace]: {
|
||||||
...session.namespaces[namespace],
|
...session.namespaces[namespace],
|
||||||
chains: [
|
chains: [
|
||||||
...new Set([chainId].concat(Array.from(session.namespaces[namespace].chains || [])))
|
...new Set([chainId].concat(Array.from(session?.namespaces?.[namespace]?.chains || [])))
|
||||||
],
|
],
|
||||||
accounts: [
|
accounts: [
|
||||||
...new Set(
|
...new Set(
|
||||||
[`${chainId}:${address}`].concat(Array.from(session.namespaces[namespace].accounts))
|
[`${chainId}:${address}`].concat(Array.from(session?.namespaces?.[namespace]?.accounts || []))
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ interface IProps {
|
|||||||
intention?: string
|
intention?: string
|
||||||
infoBoxCondition?: boolean
|
infoBoxCondition?: boolean
|
||||||
infoBoxText?: string
|
infoBoxText?: string
|
||||||
disabledApprove?: boolean
|
|
||||||
approveLoader?: LoaderProps
|
approveLoader?: LoaderProps
|
||||||
rejectLoader?: LoaderProps
|
rejectLoader?: LoaderProps
|
||||||
}
|
}
|
||||||
@ -32,7 +31,6 @@ export default function RequestModal({
|
|||||||
intention,
|
intention,
|
||||||
infoBoxCondition,
|
infoBoxCondition,
|
||||||
infoBoxText,
|
infoBoxText,
|
||||||
disabledApprove
|
|
||||||
}: IProps) {
|
}: IProps) {
|
||||||
const { currentRequestVerifyContext } = useSnapshot(SettingsStore.state)
|
const { currentRequestVerifyContext } = useSnapshot(SettingsStore.state)
|
||||||
const isScam = currentRequestVerifyContext?.verified.isScam
|
const isScam = currentRequestVerifyContext?.verified.isScam
|
||||||
@ -65,14 +63,12 @@ export default function RequestModal({
|
|||||||
rejectLoader={rejectLoader}
|
rejectLoader={rejectLoader}
|
||||||
infoBoxCondition={infoBoxCondition}
|
infoBoxCondition={infoBoxCondition}
|
||||||
infoBoxText={infoBoxText}
|
infoBoxText={infoBoxText}
|
||||||
disabledApprove={disabledApprove}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}, [
|
}, [
|
||||||
approveLoader,
|
approveLoader,
|
||||||
children,
|
children,
|
||||||
disabledApprove,
|
|
||||||
infoBoxCondition,
|
infoBoxCondition,
|
||||||
infoBoxText,
|
infoBoxText,
|
||||||
intention,
|
intention,
|
||||||
|
@ -8,7 +8,7 @@ import CloseIcon from '@mui/icons-material/Close'
|
|||||||
|
|
||||||
import ModalStore from '@/store/ModalStore'
|
import ModalStore from '@/store/ModalStore'
|
||||||
import { cosmosAddresses } from '@/utils/CosmosWalletUtil'
|
import { cosmosAddresses } from '@/utils/CosmosWalletUtil'
|
||||||
import { eip155Addresses } from '@/utils/EIP155WalletUtil'
|
import { createOrRestoreEIP155Wallet, eip155Addresses, eip155Wallets } from '@/utils/EIP155WalletUtil'
|
||||||
import { polkadotAddresses } from '@/utils/PolkadotWalletUtil'
|
import { polkadotAddresses } from '@/utils/PolkadotWalletUtil'
|
||||||
import { multiversxAddresses } from '@/utils/MultiversxWalletUtil'
|
import { multiversxAddresses } from '@/utils/MultiversxWalletUtil'
|
||||||
import { tronAddresses } from '@/utils/TronWalletUtil'
|
import { tronAddresses } from '@/utils/TronWalletUtil'
|
||||||
@ -18,7 +18,7 @@ import { nearAddresses } from '@/utils/NearWalletUtil'
|
|||||||
import { kadenaAddresses } from '@/utils/KadenaWalletUtil'
|
import { kadenaAddresses } from '@/utils/KadenaWalletUtil'
|
||||||
import { styledToast } from '@/utils/HelperUtil'
|
import { styledToast } from '@/utils/HelperUtil'
|
||||||
import { web3wallet } from '@/utils/WalletConnectUtil'
|
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 { COSMOS_MAINNET_CHAINS, COSMOS_SIGNING_METHODS } from '@/data/COSMOSData'
|
||||||
import { KADENA_CHAINS, KADENA_SIGNING_METHODS } from '@/data/KadenaData'
|
import { KADENA_CHAINS, KADENA_SIGNING_METHODS } from '@/data/KadenaData'
|
||||||
import { MULTIVERSX_CHAINS, MULTIVERSX_SIGNING_METHODS } from '@/data/MultiversxData'
|
import { MULTIVERSX_CHAINS, MULTIVERSX_SIGNING_METHODS } from '@/data/MultiversxData'
|
||||||
@ -31,8 +31,12 @@ import ChainDataMini from '@/components/ChainDataMini'
|
|||||||
import ChainAddressMini from '@/components/ChainAddressMini'
|
import ChainAddressMini from '@/components/ChainAddressMini'
|
||||||
import { getChainData } from '@/data/chainsUtil'
|
import { getChainData } from '@/data/chainsUtil'
|
||||||
import RequestModal from './RequestModal'
|
import RequestModal from './RequestModal'
|
||||||
|
import { SmartAccountLib } from '@/lib/SmartAccountLib'
|
||||||
|
import { Hex } from 'viem'
|
||||||
|
import ChainSmartAddressMini from '@/components/ChainSmartAddressMini'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import SettingsStore from '@/store/SettingsStore'
|
import SettingsStore from '@/store/SettingsStore'
|
||||||
|
import { allowedChains } from '@/utils/SmartAccountUtils'
|
||||||
|
|
||||||
const StyledText = styled(Text, {
|
const StyledText = styled(Text, {
|
||||||
fontWeight: 400
|
fontWeight: 400
|
||||||
@ -43,6 +47,7 @@ const StyledSpan = styled('span', {
|
|||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
export default function SessionProposalModal() {
|
export default function SessionProposalModal() {
|
||||||
|
const { smartAccountSponsorshipEnabled } = useSnapshot(SettingsStore.state)
|
||||||
// Get proposal data and wallet address from store
|
// Get proposal data and wallet address from store
|
||||||
const data = useSnapshot(ModalStore.state)
|
const data = useSnapshot(ModalStore.state)
|
||||||
const proposal = data?.data?.proposal as SignClientTypes.EventArguments['session_proposal']
|
const proposal = data?.data?.proposal as SignClientTypes.EventArguments['session_proposal']
|
||||||
@ -174,6 +179,11 @@ export default function SessionProposalModal() {
|
|||||||
[requestedChains]
|
[requestedChains]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const smartAccountChains = useMemo(
|
||||||
|
() => supportedChains.filter(chain =>(chain as any)?.smartAccountEnabled),
|
||||||
|
[supportedChains]
|
||||||
|
)
|
||||||
|
|
||||||
// get required chains that are not supported by the wallet
|
// get required chains that are not supported by the wallet
|
||||||
const notSupportedChains = useMemo(() => {
|
const notSupportedChains = useMemo(() => {
|
||||||
if (!proposal) return []
|
if (!proposal) return []
|
||||||
@ -225,6 +235,21 @@ export default function SessionProposalModal() {
|
|||||||
supportedNamespaces
|
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)
|
console.log('approving namespaces:', namespaces)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -241,7 +266,7 @@ export default function SessionProposalModal() {
|
|||||||
}
|
}
|
||||||
setIsLoadingApprove(false)
|
setIsLoadingApprove(false)
|
||||||
ModalStore.close()
|
ModalStore.close()
|
||||||
}, [proposal, supportedNamespaces])
|
}, [proposal, supportedNamespaces, smartAccountSponsorshipEnabled])
|
||||||
|
|
||||||
// Hanlde reject action
|
// Hanlde reject action
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
@ -273,7 +298,6 @@ export default function SessionProposalModal() {
|
|||||||
rejectLoader={{ active: isLoadingReject }}
|
rejectLoader={{ active: isLoadingReject }}
|
||||||
infoBoxCondition={notSupportedChains.length > 0}
|
infoBoxCondition={notSupportedChains.length > 0}
|
||||||
infoBoxText={`The following required chains are not supported by your wallet - ${notSupportedChains.toString()}`}
|
infoBoxText={`The following required chains are not supported by your wallet - ${notSupportedChains.toString()}`}
|
||||||
disabledApprove={notSupportedChains.length > 0}
|
|
||||||
>
|
>
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
@ -309,6 +333,16 @@ export default function SessionProposalModal() {
|
|||||||
</Row>
|
</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>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Row style={{ color: 'GrayText' }} justify="flex-end">
|
<Row style={{ color: 'GrayText' }} justify="flex-end">
|
||||||
|
@ -75,6 +75,8 @@ export default function SessionSignTypedDataModal() {
|
|||||||
metadata={requestSession.peer.metadata}
|
metadata={requestSession.peer.metadata}
|
||||||
onApprove={onApprove}
|
onApprove={onApprove}
|
||||||
onReject={onReject}
|
onReject={onReject}
|
||||||
|
approveLoader={{ active: isLoadingApprove }}
|
||||||
|
rejectLoader={{ active: isLoadingReject }}
|
||||||
>
|
>
|
||||||
<RequesDetailsCard chains={[chainId ?? '']} protocol={requestSession.relay.protocol} />
|
<RequesDetailsCard chains={[chainId ?? '']} protocol={requestSession.relay.protocol} />
|
||||||
<Divider y={1} />
|
<Divider y={1} />
|
||||||
|
Loading…
Reference in New Issue
Block a user