Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: useSignTransaction and useSendRawTransaction #4330

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rich-berries-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wagmi/core": minor
---

Added `sendRawTransaction` and `signTransaction` actions.
5 changes: 5 additions & 0 deletions .changeset/strong-eels-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wagmi": minor
---

Added `useSendRawTransaction` and `useSignTransaction` hooks.
50 changes: 50 additions & 0 deletions packages/core/src/actions/sendRawTransaction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { config, transactionHashRegex } from '@wagmi/test'
import { parseEther } from 'viem'
import { beforeEach, expect, test } from 'vitest'

import { connect } from './connect.js'
import { disconnect } from './disconnect.js'
import { sendRawTransaction } from './sendRawTransaction.js'
import { signTransaction } from './signTransaction.js'

const connector = config.connectors[0]!

beforeEach(async () => {
if (config.state.current === connector.uid)
await disconnect(config, { connector })
})

test('default', async () => {
await connect(config, { connector })
const serializedTransaction = await signTransaction(config, {
to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
value: parseEther('0.01'),
})

expect(
sendRawTransaction(config, {
serializedTransaction,
}),
).resolves.toMatch(transactionHashRegex)
})

test('behavior: connector not connected', async () => {
await connect(config, { connector })

const serializedTransaction = await signTransaction(config, {
to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
value: parseEther('0.01'),
})

await expect(
sendRawTransaction(config, {
connector: config.connectors[1],
serializedTransaction,
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
[ConnectorNotConnectedError: Connector not connected.

Version: @wagmi/core@x.y.z]
`)
await disconnect(config, { connector })
})
61 changes: 61 additions & 0 deletions packages/core/src/actions/sendRawTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
type SendRawTransactionErrorType as viem_SendRawTransactionErrorType,
type SendRawTransactionParameters as viem_SendRawTransactionParameters,
type SendRawTransactionReturnType as viem_SendRawTransactionReturnType,
sendRawTransaction as viem_sendRawTransaction,
} from 'viem/actions'

import type { Config } from '../createConfig.js'
import type { BaseErrorType, ErrorType } from '../errors/base.js'
import type {
ChainIdParameter,
ConnectorParameter,
} from '../types/properties.js'
import type { Compute } from '../types/utils.js'
import { getAction } from '../utils/getAction.js'
import {
type GetConnectorClientErrorType,
getConnectorClient,
} from './getConnectorClient.js'

export type SendRawTransactionParameters<
config extends Config = Config,
chainId extends
config['chains'][number]['id'] = config['chains'][number]['id'],
> = Compute<
viem_SendRawTransactionParameters &
ChainIdParameter<config, chainId> &
ConnectorParameter
>

export type SendRawTransactionReturnType = viem_SendRawTransactionReturnType

export type SendRawTransactionErrorType =
// getConnectorClient()
| GetConnectorClientErrorType
// base
| BaseErrorType
| ErrorType
// viem
| viem_SendRawTransactionErrorType

/** https://wagmi.sh/core/api/actions/sendRawTransaction */
export async function sendRawTransaction<
config extends Config,
chainId extends config['chains'][number]['id'],
>(
config: config,
parameters: SendRawTransactionParameters<config, chainId>,
): Promise<SendRawTransactionReturnType> {
const { chainId, connector, ...rest } = parameters

const client = await getConnectorClient(config, { chainId, connector })

const action = getAction(
client,
viem_sendRawTransaction,
'sendRawTransaction',
)

return action(rest)
}
50 changes: 50 additions & 0 deletions packages/core/src/actions/signTransaction.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { http, parseEther } from 'viem'
import { celo, mainnet } from 'viem/chains'
import { expectTypeOf, test } from 'vitest'

import { createConfig } from '../createConfig.js'
import {
type SignTransactionParameters,
signTransaction,
} from './signTransaction.js'

test('chain formatters', () => {
const config = createConfig({
chains: [mainnet, celo],
transports: { [celo.id]: http(), [mainnet.id]: http() },
})

type Result = SignTransactionParameters<typeof config>
expectTypeOf<Result>().toMatchTypeOf<{
chainId?: typeof celo.id | typeof mainnet.id | undefined
feeCurrency?: `0x${string}` | undefined
}>()
signTransaction(config, {
to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
value: parseEther('0.01'),
feeCurrency: '0x',
})

type Result2 = SignTransactionParameters<typeof config, typeof celo.id>
expectTypeOf<Result2>().toMatchTypeOf<{
feeCurrency?: `0x${string}` | undefined
}>()
signTransaction(config, {
chainId: celo.id,
to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
value: parseEther('0.01'),
feeCurrency: '0x',
})

type Result3 = SignTransactionParameters<typeof config, typeof mainnet.id>
expectTypeOf<Result3>().not.toMatchTypeOf<{
feeCurrency?: `0x${string}` | undefined
}>()
signTransaction(config, {
chainId: mainnet.id,
to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
value: parseEther('0.01'),
// @ts-expect-error
feeCurrency: '0x',
})
})
84 changes: 84 additions & 0 deletions packages/core/src/actions/signTransaction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { config, privateKey } from '@wagmi/test'
import { type TransactionRequestBase, parseEther } from 'viem'
import { beforeEach, expect, test } from 'vitest'

import { privateKeyToAccount } from 'viem/accounts'
import { connect } from './connect.js'
import { disconnect } from './disconnect.js'
import { signTransaction } from './signTransaction.js'

const connector = config.connectors[0]!

const base = {
from: '0x0000000000000000000000000000000000000000',
gas: 21000n,
nonce: 785,
} satisfies TransactionRequestBase

beforeEach(async () => {
if (config.state.current === connector.uid)
await disconnect(config, { connector })
})

test('default', async () => {
await connect(config, { connector })
await expect(
signTransaction(config, {
...base,
to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
value: parseEther('0.01'),
}),
).resolves.toMatchInlineSnapshot(
'"0x02f870018203118085065e22cad982520894d2135cfb216b74109775236e36d4b433f1df507b872386f26fc1000080c080a0af0d6c8691aae5ecfe11b40f69ea580980175ce3a242b431f65c6192c5f59663a0016d0a36a9b3100da6a45d818a4261d64ad5276d07f6313e816777705e619b91"',
)
await disconnect(config, { connector })
})

test('behavior: connector not connected', async () => {
await connect(config, { connector })
await expect(
signTransaction(config, {
...base,
connector: config.connectors[1],
to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
value: parseEther('0.01'),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
[ConnectorNotConnectedError: Connector not connected.

Version: @wagmi/core@x.y.z]
`)
await disconnect(config, { connector })
})

test('behavior: account does not exist on connector', async () => {
await connect(config, { connector })
await expect(
signTransaction(config, {
...base,
account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
value: parseEther('0.01'),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
[ConnectorAccountNotFoundError: Account "0xA0Cf798816D4b9b9866b5330EEa46a18382f251e" not found for connector "Mock Connector".

Version: @wagmi/core@x.y.z]
`)
await disconnect(config, { connector })
})

test('behavior: local account', async () => {
const account = privateKeyToAccount(privateKey)
await expect(
signTransaction(config, {
...base,
account,
type: 'eip1559',
to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
value: parseEther('0.01'),
}),
).resolves.toMatchInlineSnapshot(
'"0x02f8690182031180808094d2135cfb216b74109775236e36d4b433f1df507b872386f26fc1000080c001a05df802f592b42ca82e8e9fb9ef0a923ef4d090234fd80dd83d980c09948ab5dea03b845f9d0602a1a11fb319f68d77d658d471c6661733179094bd313a4c1b61aa"',
)
})
112 changes: 112 additions & 0 deletions packages/core/src/actions/signTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type {
Account,
Chain,
Client,
TransactionRequest,
SignTransactionErrorType as viem_SignTransactionErrorType,
SignTransactionParameters as viem_SignTransactionParameters,
SignTransactionReturnType as viem_SignTransactionReturnType,
} from 'viem'
import {
estimateGas as viem_estimateGas,
signTransaction as viem_signTransaction,
} from 'viem/actions'

import type { Config } from '../createConfig.js'
import type { BaseErrorType, ErrorType } from '../errors/base.js'
import type { SelectChains } from '../types/chain.js'
import type {
ChainIdParameter,
ConnectorParameter,
} from '../types/properties.js'
import type { Compute } from '../types/utils.js'
import { getAction } from '../utils/getAction.js'
import { getAccount } from './getAccount.js'
import {
type GetConnectorClientErrorType,
getConnectorClient,
} from './getConnectorClient.js'

export type SignTransactionParameters<
config extends Config = Config,
chainId extends
config['chains'][number]['id'] = config['chains'][number]['id'],
///
chains extends readonly Chain[] = SelectChains<config, chainId>,
> = {
[key in keyof chains]: Compute<
Omit<
viem_SignTransactionParameters<chains[key], Account, chains[key]>,
'chain' | 'gas'
> &
ChainIdParameter<config, chainId> &
ConnectorParameter
>
}[number] & {
/** Gas provided for transaction execution, or `null` to skip the prelude gas estimation. */
gas?: TransactionRequest['gas'] | null
}

export type SignTransactionReturnType = viem_SignTransactionReturnType

export type SignTransactionErrorType =
// getConnectorClient()
| GetConnectorClientErrorType
// base
| BaseErrorType
| ErrorType
// viem
| viem_SignTransactionErrorType

/** https://wagmi.sh/core/api/actions/signTransaction */
export async function signTransaction<
config extends Config,
chainId extends config['chains'][number]['id'],
>(
config: config,
parameters: SignTransactionParameters<config, chainId>,
): Promise<SignTransactionReturnType> {
const { account, chainId, connector, gas: gas_, ...rest } = parameters

let client: Client
if (typeof account === 'object' && account.type === 'local')
client = config.getClient({ chainId })
else
client = await getConnectorClient(config, { account, chainId, connector })

const { connector: activeConnector } = getAccount(config)

const gas = await (async () => {
// Skip gas estimation if `data` doesn't exist (not a contract interaction).
if (!('data' in parameters) || !parameters.data) return undefined

// Skip gas estimation if connector supports simulation.
if ((connector ?? activeConnector)?.supportsSimulation) return undefined

// Skip gas estimation if `null` is provided.
if (gas_ === null) return undefined

// Run gas estimation if no value is provided.
if (gas_ === undefined) {
const action = getAction(client, viem_estimateGas, 'estimateGas')
return action({
...(rest as any),
account,
chain: chainId ? { id: chainId } : null,
})
}

// Use provided gas value.
return gas_
})()

const action = getAction(client, viem_signTransaction, 'signTransaction')
const signature = await action({
...(rest as any),
...(account ? { account } : {}),
gas,
chain: chainId ? { id: chainId } : null,
})

return signature
}
2 changes: 2 additions & 0 deletions packages/core/src/exports/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ test('exports', () => {
"readContract",
"readContracts",
"reconnect",
"sendRawTransaction",
"sendTransaction",
"signMessage",
"signTransaction",
"signTypedData",
"simulateContract",
"switchAccount",
Expand Down
Loading
Loading