Skip to content

Commit

Permalink
feat: EIP-5792 (#2053)
Browse files Browse the repository at this point in the history
* wip: getCapabilities

* wip: client

* wip: sendCalls

* wip: jsdoc

* wip: getCallsStatus

* wip: decorators

* wip: experimental export

* chore: changeset
  • Loading branch information
jxom authored Apr 2, 2024
1 parent 0782d75 commit e53fe12
Show file tree
Hide file tree
Showing 13 changed files with 1,044 additions and 44 deletions.
5 changes: 5 additions & 0 deletions .changeset/clever-eagles-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": patch
---

Added experimental support for [EIP-5792 `wallet_` methods](https://github.com/ethereum/EIPs/blob/1d759f24e6552a516091bb1fe3361d9ca44d085c/EIPS/eip-5792.md).
154 changes: 154 additions & 0 deletions src/experimental/actions/getCallsStatus.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { expect, test } from 'vitest'
import { accounts, localHttpUrl } from '../../../test/src/constants.js'
import { testClient } from '../../../test/src/utils.js'
import { mine } from '../../actions/index.js'
import { mainnet } from '../../chains/index.js'
import { createClient } from '../../clients/createClient.js'
import { custom } from '../../clients/transports/custom.js'
import { RpcRequestError } from '../../errors/request.js'
import type { WalletGetCallsStatusReceipt } from '../../types/eip1193.js'
import type { Hex } from '../../types/misc.js'
import { getHttpRpcClient, parseEther } from '../../utils/index.js'
import { uid } from '../../utils/uid.js'
import { getCallsStatus } from './getCallsStatus.js'
import { sendCalls } from './sendCalls.js'

type Uid = string
type TxHashes = Hex[]
const calls = new Map<Uid, TxHashes[]>()

const getClient = ({
onRequest,
}: { onRequest({ method, params }: any): void }) =>
createClient({
transport: custom({
async request({ method, params }) {
onRequest({ method, params })

const rpcClient = getHttpRpcClient(localHttpUrl)

if (method === 'wallet_getCallsStatus') {
const hashes = calls.get(params)
if (!hashes) return null
const receipts = await Promise.all(
hashes.map(async (hash) => {
const { result, error } = await rpcClient.request({
body: {
method: 'eth_getTransactionReceipt',
params: [hash],
id: 0,
},
})
if (error)
throw new RpcRequestError({
body: { method, params },
error,
url: localHttpUrl,
})
return {
blockHash: result.blockHash,
blockNumber: result.blockNumber,
gasUsed: result.gasUsed,
logs: result.logs,
status: result.status,
transactionHash: result.transactionHash,
} satisfies WalletGetCallsStatusReceipt
}),
)
return { status: 'CONFIRMED', receipts }
}

if (method === 'wallet_sendCalls') {
const hashes = []
for (const call of params.calls) {
const { result, error } = await rpcClient.request({
body: {
method: 'eth_sendTransaction',
params: [call],
id: 0,
},
})
if (error)
throw new RpcRequestError({
body: { method, params },
error,
url: localHttpUrl,
})
hashes.push(result)
}
const uid_ = uid()
calls.set(uid_, hashes)
return uid_
}

return null
},
}),
})

test('default', async () => {
const requests: unknown[] = []

const client = getClient({
onRequest({ params }) {
requests.push(params)
},
})

const id = await sendCalls(client, {
account: accounts[0].address,
calls: [
{
to: accounts[1].address,
value: parseEther('1'),
},
{
to: accounts[2].address,
},
{
data: '0xcafebabe',
to: accounts[3].address,
value: parseEther('100'),
},
],
chain: mainnet,
})

expect(id).toBeDefined()

await mine(testClient, { blocks: 1 })

const { status, receipts } = await getCallsStatus(client, { id })
expect(status).toMatchInlineSnapshot(`"CONFIRMED"`)
expect(receipts![0].blockHash).toBeDefined()
expect(
receipts?.map((x) => ({ ...x, blockHash: undefined })),
).toMatchInlineSnapshot(`
[
{
"blockHash": undefined,
"blockNumber": 16280771n,
"gasUsed": 21000n,
"logs": [],
"status": "0x1",
"transactionHash": "0x66a7b39a0c4635c2f30cd191d7e1fb0bd370c11dd93199f236c5bdacfc9136b3",
},
{
"blockHash": undefined,
"blockNumber": 16280771n,
"gasUsed": 42000n,
"logs": [],
"status": "0x1",
"transactionHash": "0x5fafca9937b154c21e7ea896c3ca23e5076ab9ca9e466085ae45edffb96c36e7",
},
{
"blockHash": undefined,
"blockNumber": 16280771n,
"gasUsed": 63064n,
"logs": [],
"status": "0x1",
"transactionHash": "0x84f1d37995973fa977fc45eccf3d1ac0cdf666541a7dc2613e9cd3bc356ddfa4",
},
]
`)
})
60 changes: 60 additions & 0 deletions src/experimental/actions/getCallsStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Client } from '../../clients/createClient.js'
import type { Transport } from '../../clients/transports/createTransport.js'
import type { ErrorType } from '../../errors/utils.js'
import type { Account } from '../../types/account.js'
import type { Chain } from '../../types/chain.js'
import type { WalletGetCallsStatusReturnType } from '../../types/eip1193.js'
import type { Prettify } from '../../types/utils.js'
import type { RequestErrorType } from '../../utils/buildRequest.js'
import { hexToBigInt } from '../../utils/encoding/fromHex.js'

export type GetCallsStatusParameters = { id: string }

export type GetCallsStatusReturnType = Prettify<
WalletGetCallsStatusReturnType<bigint>
>

export type GetCallsStatusErrorType = RequestErrorType | ErrorType

/**
* Returns the status of a call batch that was sent via `sendCalls`.
*
* - Docs: https://viem.sh/eip5792/actions/getCallsStatus
* - JSON-RPC Methods: [`wallet_getCallsStatus`](https://eips.ethereum.org/EIPS/eip-5792)
*
* @param client - Client to use
* @returns Status of the calls. {@link GetCallsStatusReturnType}
*
* @example
* import { createWalletClient, custom } from 'viem'
* import { mainnet } from 'viem/chains'
* import { getCallsStatus } from 'viem/wallet'
*
* const client = createWalletClient({
* chain: mainnet,
* transport: custom(window.ethereum),
* })
* const { receipts, status } = await getCallsStatus(client, { id: '0xdeadbeef' })
*/
export async function getCallsStatus<
chain extends Chain | undefined,
account extends Account | undefined = undefined,
>(
client: Client<Transport, chain, account>,
parameters: GetCallsStatusParameters,
): Promise<GetCallsStatusReturnType> {
const { id } = parameters
const { receipts, status } = await client.request({
method: 'wallet_getCallsStatus',
params: id,
})
return {
status,
receipts:
receipts?.map((receipt) => ({
...receipt,
blockNumber: hexToBigInt(receipt.blockNumber),
gasUsed: hexToBigInt(receipt.gasUsed),
})) ?? [],
}
}
49 changes: 49 additions & 0 deletions src/experimental/actions/getCapabilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { expect, test } from 'vitest'
import { createClient } from '../../clients/createClient.js'
import { custom } from '../../clients/transports/custom.js'
import { getCapabilities } from './getCapabilities.js'

const client = createClient({
transport: custom({
async request({ method }) {
if (method === 'wallet_getCapabilities')
return {
'0x2105': {
paymasterService: {
supported: true,
},
sessionKeys: {
supported: true,
},
},
'0x14A34': {
paymasterService: {
supported: true,
},
},
}
return null
},
}),
})

test('default', async () => {
const capabilities = await getCapabilities(client)
expect(capabilities).toMatchInlineSnapshot(`
{
"8453": {
"paymasterService": {
"supported": true,
},
"sessionKeys": {
"supported": true,
},
},
"84532": {
"paymasterService": {
"supported": true,
},
},
}
`)
})
56 changes: 56 additions & 0 deletions src/experimental/actions/getCapabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Client } from '../../clients/createClient.js'
import type { Transport } from '../../clients/transports/createTransport.js'
import type { ErrorType } from '../../errors/utils.js'
import type { Account } from '../../types/account.js'
import type { Chain } from '../../types/chain.js'
import type {
WalletCapabilities,
WalletCapabilitiesRecord,
} from '../../types/eip1193.js'
import type { Prettify } from '../../types/utils.js'
import type { RequestErrorType } from '../../utils/buildRequest.js'

export type GetCapabilitiesReturnType = Prettify<
WalletCapabilitiesRecord<WalletCapabilities, number>
>

export type GetCapabilitiesErrorType = RequestErrorType | ErrorType

/**
* Extract capabilities that a connected wallet supports (e.g. paymasters, session keys, etc).
*
* - Docs: https://viem.sh/eip5792/actions/getCapabilities
* - JSON-RPC Methods: [`wallet_getCapabilities`](https://eips.ethereum.org/EIPS/eip-5792)
*
* @param client - Client to use
* @returns The wallet's capabilities. {@link GetCapabilitiesReturnType}
*
* @example
* import { createWalletClient, custom } from 'viem'
* import { mainnet } from 'viem/chains'
* import { getCapabilities } from 'viem/wallet'
*
* const client = createWalletClient({
* chain: mainnet,
* transport: custom(window.ethereum),
* })
* const capabilities = await getCapabilities(client)
*/
export async function getCapabilities<
chain extends Chain | undefined,
account extends Account | undefined = undefined,
>(
client: Client<Transport, chain, account>,
): Promise<GetCapabilitiesReturnType> {
const capabilities_raw = await client.request({
method: 'wallet_getCapabilities',
})

const capabilities = {} as WalletCapabilitiesRecord<
WalletCapabilities,
number
>
for (const [key, value] of Object.entries(capabilities_raw))
capabilities[Number(key)] = value
return capabilities
}
Loading

0 comments on commit e53fe12

Please sign in to comment.