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: assert that client chain matches wallet's active chain #223

Merged
merged 2 commits into from
Mar 16, 2023
Merged
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/sixty-ties-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": patch
---

Added an assertion in `sendTransaction` & `writeContract` to check that the client chain matches the wallet's current chain.
1 change: 1 addition & 0 deletions site/docs/actions/wallet/sendTransaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ const hash = await walletClient.sendTransaction({
### chain (optional)

- **Type:** [`Chain`](/docs/glossary/types#chain)
- **Default:** `walletClient.chain`

The target chain. If there is a mismatch between the wallet's current chain & the target chain, an error will be thrown if `assertChain` is truthy.

Expand Down
15 changes: 15 additions & 0 deletions site/docs/clients/wallet.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,21 @@ const hash = await client.sendTransaction({ // [!code focus:5]

## Parameters

### chain (optional)

- **Type:** [Chain](/docs/glossary/types#chain)

The [Chain](/docs/clients/chains) of the Wallet Client.

Used in the [`sendTransaction`](/docs/actions/wallet/sendTransaction) & [`writeContract`](/docs/contract/writeContract) Actions to assert that the chain matches the wallet's active chain.

```ts
const client = createWalletClient({
chain: mainnet, // [!code focus]
transport: custom(window.ethereum)
})
```

### key (optional)

- **Type:** `string`
Expand Down
43 changes: 43 additions & 0 deletions site/docs/contract/writeContract.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,49 @@ await walletClient.writeContract({
})
```

### assertChain (optional)
Copy link
Member Author

Choose a reason for hiding this comment

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

Adding missing docs on writeContract.


- **Type:** `boolean`
- **Default:** `true`

Throws an error if `chain` does not match the current wallet chain.

Defaults to `true`, but you can turn this off if your dapp is primarily multi-chain.

```ts
import { optimism } from 'viem/chains' // [!code focus]

await walletClient.writeContract({
address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2',
abi: wagmiAbi,
functionName: 'mint',
args: [69420],
assertChain: false, // [!code focus]
chain: optimism, // [!code focus]
})
```

### chain (optional)

- **Type:** [`Chain`](/docs/glossary/types#chain)
- **Default:** `walletClient.chain`

The target chain. If there is a mismatch between the wallet's current chain & the target chain, an error will be thrown if `assertChain` is truthy.

The chain is also used to infer its request type (e.g. the Celo chain has a `gatewayFee` that you can pass through to `sendTransaction`).

```ts
import { optimism } from 'viem/chains' // [!code focus]

await walletClient.writeContract({
address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2',
abi: wagmiAbi,
functionName: 'mint',
args: [69420],
chain: optimism, // [!code focus]
})
```

### gasPrice (optional)

- **Type:** `bigint`
Expand Down
52 changes: 50 additions & 2 deletions src/actions/wallet/sendTransaction.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { describe, expect, test } from 'vitest'

import { accounts, publicClient, testClient, walletClient } from '../../_test'
import {
accounts,
localHttpUrl,
publicClient,
testClient,
walletClient,
} from '../../_test'
import { celo, defineChain, localhost, mainnet, optimism } from '../../chains'
import { getAccount, hexToNumber, parseEther, parseGwei } from '../../utils'
import { getBalance, getBlock, getTransaction } from '..'
import { mine, setBalance, setNextBlockBaseFeePerGas } from '../test'

import { sendTransaction } from './sendTransaction'
import { anvilChain, getLocalAccount } from '../../_test/utils'
import { createWalletClient, http } from '../../clients'

const sourceAccount = accounts[0]
const targetAccount = accounts[1]
Expand Down Expand Up @@ -120,6 +127,32 @@ test.skip('sends transaction w/ no value', async () => {
).toBeLessThan(sourceAccount.balance)
})

test('client chain mismatch', async () => {
const walletClient = createWalletClient({
chain: optimism,
transport: http(localHttpUrl),
})
await expect(() =>
sendTransaction(walletClient, {
account: getAccount(sourceAccount.address),
to: targetAccount.address,
value: parseEther('1'),
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be good to pass in some kind of autoSwitch: true here that would automatically switch chains first if connected to wrong chain. Made a discussion here

}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
"The current chain of the wallet (id: 1) does not match the target chain for the transaction (id: 10 – Optimism).

Current Chain ID: 1
Expected Chain ID: 10 – Optimism

Request Arguments:
from: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
to: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8
value: 1 ETH

Version: [email protected]"
`)
})

describe('args: gas', () => {
test('sends transaction', async () => {
await setup()
Expand Down Expand Up @@ -416,6 +449,21 @@ describe('args: chain', async () => {
).toBeDefined
})

test('args: assertChain', async () => {
const walletClient = createWalletClient({
chain: optimism,
transport: http(localHttpUrl),
})
expect(
await sendTransaction(walletClient, {
assertChain: false,
account: getAccount(sourceAccount.address),
to: targetAccount.address,
value: parseEther('1'),
}),
).toBeDefined
})

test('chain mismatch', async () => {
await expect(() =>
sendTransaction(walletClient, {
Expand All @@ -425,7 +473,7 @@ describe('args: chain', async () => {
value: parseEther('1'),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
"The current chain (id: 1) does not match the chain passed to the request (id: 10 – Optimism).
"The current chain of the wallet (id: 1) does not match the target chain for the transaction (id: 10 – Optimism).

Current Chain ID: 1
Expected Chain ID: 10 – Optimism
Expand Down
5 changes: 3 additions & 2 deletions src/actions/wallet/sendTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ export type SendTransactionParameters<TChain extends Chain = Chain> =
export type SendTransactionReturnType = Hash

export async function sendTransaction<TChain extends Chain>(
client: WalletClient,
client: WalletClient<any, any>,
args: SendTransactionParameters<TChain>,
): Promise<SendTransactionReturnType> {
const {
account,
chain,
chain = client.chain,
accessList,
assertChain = true,
data,
Expand All @@ -62,6 +62,7 @@ export async function sendTransaction<TChain extends Chain>(
value,
...rest
} = args

try {
assertRequest(args)

Expand Down
23 changes: 22 additions & 1 deletion src/clients/createWalletClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,28 @@ describe('transports', () => {
expect(client).toMatchInlineSnapshot(`
{
"addChain": [Function],
"chain": undefined,
"chain": {
"id": 1337,
"name": "Localhost",
"nativeCurrency": {
"decimals": 18,
"name": "Ether",
"symbol": "ETH",
},
"network": "localhost",
"rpcUrls": {
"default": {
"http": [
"http://127.0.0.1:8545",
],
},
"public": {
"http": [
"http://127.0.0.1:8545",
],
},
},
},
"deployContract": [Function],
"getAddresses": [Function],
"getChainId": [Function],
Expand Down
2 changes: 2 additions & 0 deletions src/clients/createWalletClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function createWalletClient<
TTransport extends Transport,
TChain extends Chain,
>({
chain,
transport,
key = 'wallet',
name = 'Wallet Client',
Expand All @@ -42,6 +43,7 @@ export function createWalletClient<
true
> {
const client = createClient({
chain,
key,
name,
pollingInterval,
Expand Down
2 changes: 1 addition & 1 deletion src/errors/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class ChainMismatchError extends BaseError {
currentChainId,
}: { chain: Chain; currentChainId: number }) {
super(
`The current chain (id: ${currentChainId}) does not match the chain passed to the request (id: ${chain.id} – ${chain.name}).`,
`The current chain of the wallet (id: ${currentChainId}) does not match the target chain for the transaction (id: ${chain.id} – ${chain.name}).`,
{
metaMessages: [
`Current Chain ID: ${currentChainId}`,
Expand Down