Skip to content

Commit

Permalink
feat: custom chain serializers (#691)
Browse files Browse the repository at this point in the history
* PoC - custom chain serializers

* expose serializeAccessList function

* fix types for adding a custom serializer to the signTransaction function

* add test to show that serializer as argument is used

* fix types to be specific

* export the types to public

* update custom account to reflect that the sign* functions take objects as params

* add changeset

* fix import order

* lint

* refactor

* Update calm-garlics-share.md

---------

Co-authored-by: Aaron <aaron.deruvo@clabs.co>
Co-authored-by: moxey.eth <jakemoxey@gmail.com>
  • Loading branch information
3 people authored Jun 20, 2023
1 parent b510233 commit 6e65789
Show file tree
Hide file tree
Showing 18 changed files with 333 additions and 91 deletions.
5 changes: 5 additions & 0 deletions .changeset/calm-garlics-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": minor
---

Added custom chain serializers via `chain.serializers`.
71 changes: 36 additions & 35 deletions site/docs/accounts/custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ import { getAddress, signMessage, signTransaction } from './sign-utils' // [!cod
const privateKey = '0x...' // [!code focus:13]
const account = toAccount({
address: getAddress(privateKey),
signMessage(message) {
return signMessage(message, privateKey)
async signMessage({ message }) {
return signMessage({ message, privateKey })
},
signTransaction(transaction) {
return signTransaction(transaction, privateKey)
async signTransaction(transaction, { serializer }) {
return signTransaction({ privateKey, transaction, serializer })
},
async signTypedData(typedData) {
return signTypedData({ ...typedData, privateKey })
},
signTypedData(typedData) {
return signTypedData(typedData, privateKey)
}
})

const client = createWalletClient({
Expand All @@ -62,15 +62,15 @@ The Address of the Account.
```ts
const account = toAccount({
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // [!code focus]
signMessage(message) {
return signMessage(message, privateKey)
async signMessage({ message }) {
return signMessage({ message, privateKey })
},
async signTransaction(transaction, { serializer }) {
return signTransaction({ privateKey, transaction, serializer })
},
signTransaction(transaction) {
return signTransaction(transaction, privateKey)
async signTypedData(typedData) {
return signTypedData({ ...typedData, privateKey })
},
signTypedData(typedData) {
return signTypedData(typedData, privateKey)
}
})
```

Expand All @@ -81,15 +81,16 @@ Function to sign a message in [EIP-191 format](https://eips.ethereum.org/EIPS/ei
```ts
const account = toAccount({
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
signMessage(message) { // [!code focus:3]
return signMessage(message, privateKey)

async signMessage({ message }) { // [!code focus:3]
return signMessage({ message, privateKey })
},
signTransaction(transaction) {
return signTransaction(transaction, privateKey)
async signTransaction(transaction, { serializer }) {
return signTransaction({ privateKey, transaction, serializer })
},
async signTypedData(typedData) {
return signTypedData({ ...typedData, privateKey })
},
signTypedData(typedData) {
return signTypedData(typedData, privateKey)
}
})
```

Expand All @@ -100,15 +101,15 @@ Function to sign a transaction.
```ts
const account = toAccount({
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
signMessage(message) {
return signMessage(message, privateKey)
async signMessage({ message }) {
return signMessage({ message, privateKey })
},
async signTransaction(transaction, { serializer }) { // [!code focus:3]
return signTransaction({ privateKey, transaction, serializer })
},
signTransaction(transaction) { // [!code focus:3]
return signTransaction(transaction, privateKey)
async signTypedData(typedData) {
return signTypedData({ ...typedData, privateKey })
},
signTypedData(typedData) {
return signTypedData(typedData, privateKey)
}
})
```

Expand All @@ -119,14 +120,14 @@ Function to sign [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data.
```ts
const account = toAccount({
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
signMessage(message) {
return signMessage(message, privateKey)
async signMessage({ message }) {
return signMessage({ message, privateKey })
},
async signTransaction(transaction, { serializer }) {
return signTransaction({ privateKey, transaction, serializer })
},
signTransaction(transaction) {
return signTransaction(transaction, privateKey)
async signTypedData(typedData) { // [!code focus:3]
return signTypedData({ ...typedData, privateKey })
},
signTypedData(typedData) { // [!code focus:3]
return signTypedData(typedData, privateKey)
}
})
```
4 changes: 2 additions & 2 deletions src/accounts/privateKeyToAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export function privateKeyToAccount(privateKey: Hex): PrivateKeyAccount {
async signMessage({ message }) {
return signMessage({ message, privateKey })
},
async signTransaction(transaction) {
return signTransaction({ privateKey, transaction })
async signTransaction(transaction, { serializer } = {}) {
return signTransaction({ privateKey, transaction, serializer })
},
async signTypedData(typedData) {
return signTypedData({ ...typedData, privateKey })
Expand Down
4 changes: 2 additions & 2 deletions src/accounts/toAccount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('toAccount', () => {
async signMessage() {
return '0x'
},
async signTransaction() {
async signTransaction(_transaction) {
return '0x'
},
async signTypedData() {
Expand All @@ -55,7 +55,7 @@ describe('toAccount', () => {
async signMessage() {
return '0x'
},
async signTransaction() {
async signTransaction(_transaction) {
return '0x'
},
async signTypedData() {
Expand Down
22 changes: 20 additions & 2 deletions src/accounts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import type { HDKey } from '@scure/bip32'
import type { Address, TypedData } from 'abitype'

import type { Hash, Hex, SignableMessage } from '../types/misc.js'
import type { TransactionSerializable } from '../types/transaction.js'
import type {
TransactionSerializable,
TransactionSerialized,
} from '../types/transaction.js'
import type { TypedDataDefinition } from '../types/typedData.js'
import type { IsNarrowable } from '../types/utils.js'
import type { GetTransactionType } from '../utils/transaction/getTransactionType.js'
import type { SerializeTransactionFn } from '../utils/transaction/serializeTransaction.js'

export type Account<TAddress extends Address = Address> =
| JsonRpcAccount<TAddress>
Expand All @@ -14,7 +20,19 @@ export type AccountSource = Address | CustomSource
export type CustomSource = {
address: Address
signMessage: ({ message }: { message: SignableMessage }) => Promise<Hash>
signTransaction: (transaction: TransactionSerializable) => Promise<Hash>
signTransaction: <TTransactionSerializable extends TransactionSerializable>(
transaction: TTransactionSerializable,
args?: {
serializer?: SerializeTransactionFn<TTransactionSerializable>
},
) => Promise<
IsNarrowable<
TransactionSerialized<GetTransactionType<TTransactionSerializable>>,
Hash
> extends true
? TransactionSerialized<GetTransactionType<TTransactionSerializable>>
: Hash
>
signTypedData: <
TTypedData extends TypedData | { [key: string]: unknown },
TPrimaryType extends string = string,
Expand Down
66 changes: 64 additions & 2 deletions src/accounts/utils/signTransaction.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { assertType, describe, expect, test } from 'vitest'
import { assertType, describe, expect, test, vi } from 'vitest'

import { accounts } from '../../_test/constants.js'
import { concatHex, toHex, toRlp } from '../../index.js'
import type {
TransactionSerializable,
TransactionSerializableBase,
Expand All @@ -11,8 +12,8 @@ import type {
TransactionSerializedEIP2930,
TransactionSerializedLegacy,
} from '../../types/transaction.js'
import type { SerializeTransactionFn } from '../../utils/transaction/serializeTransaction.js'
import { parseGwei } from '../../utils/unit/parseGwei.js'

import { signTransaction } from './signTransaction.js'

const base = {
Expand Down Expand Up @@ -240,6 +241,67 @@ describe('eip2930', () => {
})
})

describe('with custom EIP2718 serializer', () => {
type ExampleTransaction = TransactionSerializable & {
type: 'cip42'
chainId: number
additionalField: `0x${string}`
}

test('default', async () => {
const exampleSerializer: SerializeTransactionFn<ExampleTransaction> = vi.fn(
function (transaction) {
const {
chainId,
nonce,
gas,
to,
value,
additionalField,
maxFeePerGas,
maxPriorityFeePerGas,
data,
} = transaction

const serializedTransaction = [
toHex(chainId),
nonce ? toHex(nonce) : '0x',
maxPriorityFeePerGas ? toHex(maxPriorityFeePerGas) : '0x',
maxFeePerGas ? toHex(maxFeePerGas) : '0x',
gas ? toHex(gas) : '0x',
additionalField ?? '0x',
to ?? '0x',
value ? toHex(value) : '0x',
data ?? '0x',
[],
]

return concatHex(['0x08', toRlp(serializedTransaction)])
},
)

const example2718Transaction: ExampleTransaction = {
...base,
type: 'cip42',
additionalField: '0x0000',
chainId: 42240,
}

const signature = await signTransaction({
transaction: example2718Transaction,
privateKey: accounts[0].privateKey,
serializer: exampleSerializer,
})
assertType(signature)

expect(exampleSerializer).toHaveBeenCalledWith(example2718Transaction)

expect(signature).toMatchInlineSnapshot(
'"0x08d282a5008203118080825208820000808080c0"',
)
})
})

describe('legacy', () => {
const baseLegacy = {
...base,
Expand Down
15 changes: 9 additions & 6 deletions src/accounts/utils/signTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import type { Hex } from '../../types/misc.js'
import type {
TransactionSerializable,
TransactionSerialized,
TransactionType,
} from '../../types/transaction.js'
import { keccak256 } from '../../utils/hash/keccak256.js'
import type { GetTransactionType } from '../../utils/transaction/getTransactionType.js'
import { serializeTransaction } from '../../utils/transaction/serializeTransaction.js'
import {
type SerializeTransactionFn,
serializeTransaction,
} from '../../utils/transaction/serializeTransaction.js'

import { sign } from './sign.js'

Expand All @@ -15,23 +17,24 @@ export type SignTransactionArgs<
> = {
privateKey: Hex
transaction: TTransactionSerializable
serializer?: SerializeTransactionFn<TTransactionSerializable>
}
export type SignTransactionReturnType<
TTransactionSerializable extends TransactionSerializable = TransactionSerializable,
TTransactionType extends TransactionType = GetTransactionType<TTransactionSerializable>,
> = TransactionSerialized<TTransactionType>
> = TransactionSerialized<GetTransactionType<TTransactionSerializable>>

export async function signTransaction<
TTransactionSerializable extends TransactionSerializable,
>({
privateKey,
transaction,
serializer = serializeTransaction,
}: SignTransactionArgs<TTransactionSerializable>): Promise<
SignTransactionReturnType<TTransactionSerializable>
> {
const signature = await sign({
hash: keccak256(serializeTransaction(transaction)),
hash: keccak256(serializer(transaction)),
privateKey,
})
return serializeTransaction(transaction, signature)
return serializer(transaction, signature)
}
Loading

1 comment on commit 6e65789

@vercel
Copy link

@vercel vercel bot commented on 6e65789 Jun 20, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.