Skip to content

Commit

Permalink
feat: initial callContract implementation (#31)
Browse files Browse the repository at this point in the history
* feat: callContract

* chore: changeset

* cleanup

* cleanup

* cleanup

* format
  • Loading branch information
jxom committed Feb 1, 2023
1 parent f99b996 commit 1f65640
Show file tree
Hide file tree
Showing 56 changed files with 2,803 additions and 177 deletions.
5 changes: 5 additions & 0 deletions .changeset/moody-gorillas-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": patch
---

Added initial `callContract` implementation
1 change: 1 addition & 0 deletions src/actions/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ test('exports actions', () => {
{
"addChain": [Function],
"call": [Function],
"callContract": [Function],
"createBlockFilter": [Function],
"createPendingTransactionFilter": [Function],
"dropTransaction": [Function],
Expand Down
3 changes: 3 additions & 0 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export {
call,
callContract,
createBlockFilter,
createPendingTransactionFilter,
estimateGas,
Expand All @@ -25,6 +26,8 @@ export {
} from './public'
export type {
CallArgs,
CallContractArgs,
CallContractResponse,
CallResponse,
CreateBlockFilterResponse,
CreatePendingTransactionFilterResponse,
Expand Down
2 changes: 2 additions & 0 deletions src/actions/public/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Chain, Formatter } from '../../chains'
import type { PublicClient } from '../../clients'
import { InvalidGasArgumentsError } from '../../errors'
import type {
Address,
BlockTag,
Hex,
MergeIntersectionProperties,
Expand All @@ -21,6 +22,7 @@ export type CallArgs<TChain extends Chain = Chain> = FormattedCall<
TransactionRequestFormatter<TChain>
> & {
chain?: TChain
from?: Address
} & (
| {
/** The balance of the account at a block number. */
Expand Down
28 changes: 28 additions & 0 deletions src/actions/public/callContract.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Contract } from 'ethers'
import { bench, describe } from 'vitest'

import {
ethersProvider,
publicClient,
wagmiContractConfig,
} from '../../../test'

import { callContract } from './callContract'

describe('Call Contract', () => {
bench('viem: `callContract`', async () => {
await callContract(publicClient, {
...wagmiContractConfig,
functionName: 'totalSupply',
})
})

bench('ethers: `callStatic`', async () => {
const wagmi = new Contract(
wagmiContractConfig.address,
wagmiContractConfig.abi,
ethersProvider,
)
await wagmi.callStatic.mint(1)
})
})
284 changes: 284 additions & 0 deletions src/actions/public/callContract.test.ts

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions src/actions/public/callContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Abi } from 'abitype'

import type { Chain, Formatter } from '../../chains'
import type { PublicClient } from '../../clients'
import type {
Address,
ExtractArgsFromAbi,
ExtractResultFromAbi,
ExtractFunctionNameFromAbi,
GetValue,
} from '../../types'
import {
EncodeFunctionDataArgs,
decodeFunctionResult,
encodeFunctionData,
getContractError,
} from '../../utils'
import { call, CallArgs, FormattedCall } from './call'

export type FormattedCallContract<
TFormatter extends Formatter | undefined = Formatter,
> = FormattedCall<TFormatter>

export type CallContractArgs<
TChain extends Chain = Chain,
TAbi extends Abi | readonly unknown[] = Abi,
TFunctionName extends string = any,
> = Omit<CallArgs<TChain>, 'from' | 'to' | 'data' | 'value'> & {
address: Address
abi: TAbi
from?: Address
functionName: ExtractFunctionNameFromAbi<TAbi, TFunctionName>
value?: GetValue<TAbi, TFunctionName, CallArgs<TChain>['value']>
} & ExtractArgsFromAbi<TAbi, TFunctionName>

export type CallContractResponse<
TAbi extends Abi | readonly unknown[] = Abi,
TFunctionName extends string = string,
> = ExtractResultFromAbi<TAbi, TFunctionName>

export async function callContract<
TChain extends Chain,
TAbi extends Abi = Abi,
TFunctionName extends string = any,
>(
client: PublicClient,
{
abi,
address,
args,
functionName,
...callRequest
}: CallContractArgs<TChain, TAbi, TFunctionName>,
): Promise<CallContractResponse<TAbi, TFunctionName>> {
const calldata = encodeFunctionData({
abi,
args,
functionName,
} as unknown as EncodeFunctionDataArgs<TAbi, TFunctionName>)
try {
const { data } = await call(client, {
data: calldata,
to: address,
...callRequest,
} as unknown as CallArgs<TChain>)
return decodeFunctionResult({
abi,
functionName,
data: data || '0x',
}) as CallContractResponse<TAbi, TFunctionName>
} catch (err) {
throw getContractError(err, {
abi,
address,
args,
functionName,
sender: callRequest.from,
})
}
}
1 change: 1 addition & 0 deletions src/actions/public/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ test('exports actions', () => {
expect(actions).toMatchInlineSnapshot(`
{
"call": [Function],
"callContract": [Function],
"createBlockFilter": [Function],
"createPendingTransactionFilter": [Function],
"estimateGas": [Function],
Expand Down
7 changes: 7 additions & 0 deletions src/actions/public/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
export { call } from './call'
export type { CallArgs, CallResponse, FormattedCall } from './call'

export { callContract } from './callContract'
export type {
CallContractArgs,
CallContractResponse,
FormattedCallContract,
} from './callContract'

export { createPendingTransactionFilter } from './createPendingTransactionFilter'
export type { CreatePendingTransactionFilterResponse } from './createPendingTransactionFilter'

Expand Down
12 changes: 8 additions & 4 deletions src/errors/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ export class AbiDecodingDataSizeInvalidError extends BaseError {
}
}

export class AbiDecodingZeroDataError extends BaseError {
name = 'AbiDecodingZeroDataError'
constructor() {
super('Cannot decode zero data ("0x") with ABI parameters.')
}
}

export class AbiEncodingArrayLengthMismatchError extends BaseError {
name = 'AbiEncodingArrayLengthMismatchError'
constructor({
Expand Down Expand Up @@ -220,15 +227,12 @@ export class InvalidArrayError extends BaseError {

export class InvalidDefinitionTypeError extends BaseError {
name = 'InvalidDefinitionTypeError'
constructor(type: string, { docsPath }: { docsPath: string }) {
constructor(type: string) {
super(
[
`"${type}" is not a valid definition type.`,
'Valid types: "function", "event", "error"',
].join('\n'),
{
docsPath,
},
)
}
}
3 changes: 2 additions & 1 deletion src/errors/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @ts-ignore
import pkg from '../../package.json'
import { stringify } from '../utils/stringify'

/* c8 ignore next */
const version = process.env.TEST ? '1.0.2' : pkg.version
Expand Down Expand Up @@ -42,7 +43,7 @@ export class BaseError extends Error {
...(args.cause &&
!(args.cause instanceof BaseError) &&
Object.keys(args.cause).length > 0
? [`Internal Error: ${JSON.stringify(args.cause)}`]
? [`Internal Error: ${stringify(args.cause)}`]
: []),
].join('\n')

Expand Down
125 changes: 125 additions & 0 deletions src/errors/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Abi } from 'abitype'
import { Address } from '../types'
import { BaseError } from './base'

export class ContractMethodExecutionError extends BaseError {
abi?: Abi
args?: unknown[]
contractAddress?: Address
formattedArgs?: string
functionName?: string
reason?: string
sender?: Address

name = 'ContractMethodExecutionError'

constructor(
message?: string,
{
abi,
args,
cause,
contractAddress,
formattedArgs,
functionName,
functionWithParams,
sender,
}: {
abi?: Abi
args?: any
cause?: Error
contractAddress?: Address
formattedArgs?: string
functionName?: string
functionWithParams?: string
sender?: Address
} = {},
) {
super(
[
message,
' ',
sender && `Sender: ${sender}`,
contractAddress &&
`Contract: ${
/* c8 ignore start */
process.env.TEST
? '0x0000000000000000000000000000000000000000'
: contractAddress
/* c8 ignore end */
}`,
functionWithParams && `Function: ${functionWithParams}`,
formattedArgs &&
`Arguments: ${[...Array(functionName?.length ?? 0).keys()]
.map(() => ' ')
.join('')}${formattedArgs}`,
]
.filter(Boolean)
.join('\n'),
{
cause,
},
)
if (message) this.reason = message
this.abi = abi
this.args = args
this.contractAddress = contractAddress
this.functionName = functionName
this.sender = sender
}
}

export class ContractMethodZeroDataError extends BaseError {
abi?: Abi
args?: unknown[]
contractAddress?: Address
functionName?: string
functionWithParams?: string

name = 'ContractMethodZeroDataError'

constructor({
abi,
args,
cause,
contractAddress,
functionName,
functionWithParams,
}: {
abi?: Abi
args?: any
cause?: Error
contractAddress?: Address
functionName?: string
functionWithParams?: string
} = {}) {
super(
[
`The contract method "${functionName}" returned no data ("0x"). This could be due to any of the following:`,
`- The contract does not have the function "${functionName}",`,
'- The parameters passed to the contract function may be invalid, or',
'- The address is not a contract.',
' ',
contractAddress &&
`Contract: ${
/* c8 ignore start */
process.env.TEST
? '0x0000000000000000000000000000000000000000'
: contractAddress
/* c8 ignore end */
}`,
functionWithParams && `Function: ${functionWithParams}`,
functionWithParams && ` > "0x"`,
]
.filter(Boolean)
.join('\n'),
{
cause,
},
)
this.abi = abi
this.args = args
this.contractAddress = contractAddress
this.functionName = functionName
}
}
6 changes: 6 additions & 0 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export {
AbiConstructorNotFoundError,
AbiConstructorParamsNotFoundError,
AbiDecodingDataSizeInvalidError,
AbiDecodingZeroDataError,
AbiEncodingArrayLengthMismatchError,
AbiEncodingLengthMismatchError,
AbiErrorInputsNotFoundError,
Expand All @@ -23,6 +24,11 @@ export { BaseError } from './base'

export { BlockNotFoundError } from './block'

export {
ContractMethodExecutionError,
ContractMethodZeroDataError,
} from './contract'

export { SizeExceedsPaddingSizeError } from './data'

export {
Expand Down
Loading

3 comments on commit 1f65640

@vercel
Copy link

@vercel vercel bot commented on 1f65640 Feb 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

viem-benchmark – ./playgrounds/benchmark

viem-benchmark.vercel.app
viem-benchmark-git-main-wagmi-dev.vercel.app
viem-benchmark-wagmi-dev.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 1f65640 Feb 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

viem-playground – ./playgrounds/dev

viem-playground.vercel.app
viem-playground-wagmi-dev.vercel.app
viem-playground-git-main-wagmi-dev.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 1f65640 Feb 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

viem-site – ./site

viem-site-git-main-wagmi-dev.vercel.app
viem-site.vercel.app
viem-site-wagmi-dev.vercel.app

Please sign in to comment.