feat: Counterfactual signing of smart accounts (#461)

This commit is contained in:
Andrius Bačianskas 2024-02-09 12:18:12 +01:00 committed by GitHub
parent 475847ff12
commit 43d41d40b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 130 additions and 63 deletions

View File

@ -1,2 +1,2 @@
NEXT_PUBLIC_PROJECT_ID=39bc...
NEXT_PUBLIC_RELAY_URL=wss://relay.walletconnect.com
NEXT_PUBLIC_RELAY_URL=wss://relay.walletconnect.com

View File

@ -35,6 +35,13 @@ export const EIP155ChainData: ChainsMap = {
slip44: 60,
testnet: true,
},
"11155111": {
name: "Ethereum Sepolia",
id: "eip155:11155111",
rpc: ["https://gateway.tenderly.co/public/sepolia "],
slip44: 60,
testnet: true,
},
"10": {
name: "Optimism Mainnet",
id: "eip155:10",
@ -138,6 +145,10 @@ export const EIP155Metadata: NamespaceMetadata = {
logo: "/assets/" + "eip155-1.png",
rgb: EIP155Colors.ethereum,
},
"11155111": {
logo: "/assets/" + "eip155-1.png",
rgb: EIP155Colors.ethereum,
},
"10": {
name: "Optimism",
logo: "/assets/" + "eip155-10.png",

View File

@ -24,6 +24,7 @@ export const DEFAULT_MAIN_CHAINS = [
export const DEFAULT_TEST_CHAINS = [
// testnets
"eip155:5",
"eip155:11155111",
"eip155:280",
"eip155:420",
"eip155:80001",

View File

@ -35,6 +35,14 @@ export const rpcProvidersByChainId: RpcProvidersByChainId = {
symbol: "ETH",
},
},
11155111: {
name: "Ethereum Sepolia",
baseURL: WALLETCONNECT_RPC_BASE_URL + "&chainId=eip155:11155111",
token: {
name: "Ether",
symbol: "ETH",
},
},
137: {
name: "Polygon Mainnet",
baseURL: WALLETCONNECT_RPC_BASE_URL + "&chainId=eip155:137",

View File

@ -16,7 +16,7 @@ interface Props {
}
export default function ChainAddressMini({ address }: Props) {
if (!address) return <></>
if (!address || address === 'N/A') return <></>
return (
<>
<Row>

View File

@ -4,11 +4,15 @@ 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
chain: {
chainId: string;
name: string;
logo: string;
rgb: string;
namespace: string;
} | undefined
}
const getKey = (namespace?: string) => {
@ -20,10 +24,11 @@ const getKey = (namespace?: string) => {
}
}
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)
export default function ChainSmartAddressMini({chain}: Props) {
const { address } = useSmartAccount(
getKey(chain?.namespace) as `0x${string}`,
allowedChains.find((c) => c.id.toString() === chain?.chainId.toString()) as Chain
)
if (!address) return <Spinner />
return (
<ChainAddressMini address={address}/>

View File

@ -44,11 +44,6 @@ export default function SmartAccountCard({
setTimeout(() => setCopied(false), 1500)
}
async function onChainChanged(chainId: string, address: string) {
SettingsStore.setActiveChainId(chainId)
await updateSignClientChainId(chainId.toString(), address)
}
async function onCreateSmartAccount() {
try {
if (!isDeployed) {
@ -58,6 +53,11 @@ export default function SmartAccountCard({
console.error(error)
}
}
async function onChainChanged(chainId: string, address: string) {
SettingsStore.setActiveChainId(chainId)
await updateSignClientChainId(chainId.toString(), address)
}
return (
<ChainCard rgb={rgb} flexDirection="row" alignItems="center" flexWrap="wrap">

View File

@ -79,7 +79,7 @@ export const EIP155_TEST_CHAINS: Record<string,EIP155Chain> = {
name: 'Ethereum Sepolia',
logo: '/chain-logos/eip155-1.png',
rgb: '99, 125, 234',
rpc: 'https://rpc.sepolia.org',
rpc: 'https://gateway.tenderly.co/public/sepolia',
namespace: 'eip155',
smartAccountEnabled: true,
},

View File

@ -6,6 +6,7 @@ import { useSnapshot } from "valtio";
import { Hex } from "viem";
import { styledToast } from "@/utils/HelperUtil";
import { TransactionExecutionError } from "viem";
import { SmartAccount } from "permissionless/accounts";
export default function useSmartAccount(signerPrivateKey: Hex, chain: Chain) {
const [loading, setLoading] = useState(false)
@ -33,11 +34,6 @@ export default function useSmartAccount(signerPrivateKey: Hex, chain: Chain) {
}
}, [setLoading])
const deploy = useCallback(async () => {
if (!client) return
execute(client?.deploySmartAccount)
}, [client, execute])
const sendTestTransaction = useCallback(async () => {
if (!client) return
execute(() => client?.sendTransaction({
@ -47,6 +43,11 @@ export default function useSmartAccount(signerPrivateKey: Hex, chain: Chain) {
}))
}, [client, execute])
const deploy = useCallback(async () => {
if (!client) return
execute(client?.deploySmartAccount)
}, [client, execute])
useEffect(() => {
if (!signerPrivateKey || !chain) return
const smartAccountClient = new SmartAccountLib({
@ -58,19 +59,17 @@ export default function useSmartAccount(signerPrivateKey: Hex, chain: Chain) {
}, [signerPrivateKey, smartAccountSponsorshipEnabled, chain])
useEffect(() => {
client?.init()
.then(() => {
setIsDeployed(client?.isDeployed)
setAddress(client?.address)
client?.getAccount()
.then((account: SmartAccount) => {
setAddress(account.address)
})
}, [client, chain])
return {
address,
isDeployed,
deploy,
loading,
sendTestTransaction,
deploy
}
}

View File

@ -39,6 +39,8 @@ export class SmartAccountLib {
throw new Error('A Pimlico API Key is required')
}
console.log('investigate 2', chain, privateKey, sponsored)
this.chain = chain
this.sponsored = sponsored
this.#signerPrivateKey = privateKey

View File

@ -11,12 +11,15 @@ import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils'
import { SignClientTypes } from '@walletconnect/types'
import { getSdkError } from '@walletconnect/utils'
import { providers } from 'ethers'
import { chains } from './SmartAccountUtils'
import { Hex } from 'viem'
import { Chain, allowedChains } from './SmartAccountUtils'
import SettingsStore from '@/store/SettingsStore'
type RequestEventArgs = Omit<SignClientTypes.EventArguments['session_request'], 'verifyContext'>
const getWallet = async (params: any) => {
const typedChains: Record<number, Chain> = chains;
console.log('get wallet params', params)
const chainId = params?.chainId?.split(':')[1]
console.log('chain id', chainId)
@ -28,12 +31,18 @@ const getWallet = async (params: any) => {
const smartAccountEnabledChain = allowedChains.find((chain) => chain.id.toString() === chainId) as Chain
console.log('smart account enabled chain', smartAccountEnabledChain)
const smartAccounts = await Promise.all(Object.values(eip155Wallets).map(async (wallet) => {
console.log('typeed chains', typedChains[chainId])
const smartAccount = new SmartAccountLib({
privateKey: wallet.getPrivateKey() as Hex,
chain: smartAccountEnabledChain,
chain: typedChains[chainId],
sponsored: true, // TODO: Sponsor for now but should be dynamic according to SettingsStore
})
await smartAccount.init()
const isDeployed = await smartAccount.checkIfSmartAccountDeployed()
if (!isDeployed) {
await smartAccount.deploySmartAccount()
}
return smartAccount
}));
@ -46,6 +55,11 @@ const getWallet = async (params: any) => {
export async function approveEIP155Request(requestEvent: RequestEventArgs) {
const { params, id } = requestEvent
const { chainId, request } = params
console.log(requestEvent, chainId, "tests")
SettingsStore.setActiveChainId(chainId)
const wallet = await getWallet(params)
switch (request.method) {

View File

@ -5,6 +5,11 @@ const apiKey = process.env.NEXT_PUBLIC_PIMLICO_KEY
// Types
export const allowedChains = [sepolia, polygonMumbai, goerli] as const
// build chains so I can access them by id
export const chains = allowedChains.reduce((acc, chain) => {
acc[chain.id] = chain
return acc
}, {} as Record<Chain['id'], Chain>)
export type Chain = (typeof allowedChains)[number]
export type UrlConfig = {
chain: Chain
@ -56,7 +61,11 @@ 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 publicRPCUrl = ({ chain }: UrlConfig) => {
console.log("investigate", chain)
return 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) =>

View File

@ -32,11 +32,12 @@ 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 { Chain, allowedChains } from '@/utils/SmartAccountUtils'
import { Hex } from 'viem'
import useSmartAccount from '@/hooks/useSmartAccount'
const StyledText = styled(Text, {
fontWeight: 400
@ -51,7 +52,6 @@ export default function SessionProposalModal() {
// Get proposal data and wallet address from store
const data = useSnapshot(ModalStore.state)
const proposal = data?.data?.proposal as SignClientTypes.EventArguments['session_proposal']
const [isLoadingApprove, setIsLoadingApprove] = useState(false)
const [isLoadingReject, setIsLoadingReject] = useState(false)
console.log('proposal', data.data?.proposal)
@ -170,12 +170,19 @@ export default function SessionProposalModal() {
optional.push(chains)
}
console.log('requestedChains', [...new Set([...required.flat(), ...optional.flat()])])
return [...new Set([...required.flat(), ...optional.flat()])]
}, [proposal])
// the chains that are supported by the wallet from the proposal
const supportedChains = useMemo(
() => requestedChains.map(chain => getChainData(chain!)),
() => requestedChains.map(chain => {
const chainData = getChainData(chain!)
if (!chainData) return null
return chainData
}),
[requestedChains]
)
@ -226,43 +233,50 @@ export default function SessionProposalModal() {
}
}, [])
const namespaces = buildApprovedNamespaces({
proposal: proposal.params,
supportedNamespaces
})
// Hanlde approve action, construct session namespace
const onApprove = useCallback(async () => {
if (proposal) {
setIsLoadingApprove(true)
const namespaces = buildApprovedNamespaces({
proposal: proposal.params,
supportedNamespaces
})
// get keys of namespaces
const namespaceKeys = Object.keys(namespaces)
const [nameSpaceKey] = namespaceKeys
// TODO: improve for multi network
console.log('namespaces', namespaces['eip155'])
const namespaceChains = namespaces['eip155']?.chains?.map((c: string) => c.split(':')[1])
const smartAccountEnabledChains: Chain[] = allowedChains.filter(chain => namespaceChains?.includes(chain.id.toString()))
// We find a request for a chain that is enabled for smart account
if (smartAccountEnabledChains.length) {
const signerAddress = namespaces['eip155'].accounts[0].split(':')[2]
// get chain ids from namespaces
const [chainIds] = namespaceKeys.map(key => namespaces[key].chains)
if (chainIds) {
const allowedChainIds = chainIds.filter(id => {
const chainId = id.replace(`${nameSpaceKey}:`, '')
return allowedChains.map(chain => chain.id.toString()).includes(chainId)
})
console.log('allowedChainIds', allowedChainIds)
const chainIdParsed = allowedChainIds[0].replace(`${nameSpaceKey}:`, '')
const signerAddress = namespaces[nameSpaceKey].accounts[0].split(':')[2]
const wallet = eip155Wallets[signerAddress]
const chain = smartAccountEnabledChains[0]
if (wallet) {
const smartAccountClient = new SmartAccountLib({
privateKey: wallet.getPrivateKey() as Hex,
chain,
sponsored: smartAccountSponsorshipEnabled,
})
await smartAccountClient.init()
const isDeployed = await smartAccountClient.checkIfSmartAccountDeployed()
console.log('isDeployed', isDeployed, smartAccountClient.address)
if (isDeployed) {
namespaces.eip155.accounts = [...namespaces.eip155.accounts, `eip155:${chain.id}:${smartAccountClient.address}`]
}
const chain = allowedChains.find(chain => chain.id.toString() === chainIdParsed)!
const smartAccountClient = new SmartAccountLib({
privateKey: wallet.getPrivateKey() as Hex,
chain: allowedChains.find(chain => chain.id.toString() === chainIdParsed)!,
sponsored: smartAccountSponsorshipEnabled,
})
const smartAccountAddress = await smartAccountClient.getAccount()
if (wallet && smartAccountAddress) {
namespaces.eip155.accounts = [...namespaces.eip155.accounts, `${nameSpaceKey}:${chain.id}:${smartAccountAddress.address}`]
}
console.log('approving namespaces:', namespaces.eip155.accounts)
}
console.log('approving namespaces:', namespaces)
try {
try {
await web3wallet.approveSession({
id: proposal.id,
namespaces
@ -276,7 +290,7 @@ export default function SessionProposalModal() {
}
setIsLoadingApprove(false)
ModalStore.close()
}, [proposal, supportedNamespaces, smartAccountSponsorshipEnabled])
}, [namespaces, proposal, smartAccountSponsorshipEnabled])
// Hanlde reject action
// eslint-disable-next-line react-hooks/rules-of-hooks
@ -339,7 +353,7 @@ export default function SessionProposalModal() {
supportedChains.map((chain, i) => {
return (
<Row key={i}>
<ChainAddressMini key={i} address={getAddress(chain?.namespace)} />
<ChainAddressMini key={i} address={getAddress(chain?.namespace) || 'test'} />
</Row>
)
})}
@ -347,9 +361,13 @@ export default function SessionProposalModal() {
<Row style={{ color: 'GrayText' }}>Smart Accounts</Row>
{smartAccountChains.length &&
smartAccountChains.map((chain, i) => {
if (!chain) {
return <></>
}
return (
<Row key={i}>
<ChainSmartAddressMini namespace={chain?.namespace!} />
<ChainSmartAddressMini chain={chain} />
</Row>
)
})}