Skip to content

Commit

Permalink
feat: parseTransactionCelo (#1035)
Browse files Browse the repository at this point in the history
* add parseTransactionCelo for parsing out serialized transaction types specific to celo chain.

* refactor

---------

Co-authored-by: Aaron <aaron.deruvo@clabs.co>
Co-authored-by: moxey.eth <jakemoxey@gmail.com>
  • Loading branch information
3 people authored Aug 21, 2023
1 parent ed96577 commit 7981fa9
Show file tree
Hide file tree
Showing 8 changed files with 462 additions and 46 deletions.
5 changes: 5 additions & 0 deletions .changeset/cyan-bears-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": minor
---

Added `parseTransactionCelo` to the `viem/chains/utils` entrypoint.
222 changes: 222 additions & 0 deletions src/chains/celo/parsers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { expect, test } from 'vitest'

import { accounts } from '../../_test/constants.js'
import {
parseEther,
parseGwei,
parseTransaction as parseTransaction_,
serializeTransaction,
toRlp,
} from '../../index.js'
import { parseTransactionCelo } from './parsers.js'
import { serializeTransactionCelo } from './serializers.js'
import type { TransactionSerializableCIP42 } from './types.js'

test('should be able to parse a cip42 transaction', () => {
const signedTransaction =
'0x7cf84682a4ec80847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a808094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c0'

expect(parseTransactionCelo(signedTransaction)).toMatchInlineSnapshot(`
{
"chainId": 42220,
"feeCurrency": "0x765de816845861e75a25fca122bb6898b8b1282a",
"maxFeePerGas": 2000000000n,
"maxPriorityFeePerGas": 2000000000n,
"to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"type": "cip42",
"value": 1000000000000000000n,
}
`)
})

const transaction = {
chainId: 1,
gas: 21001n,
maxFeePerGas: parseGwei('2'),
maxPriorityFeePerGas: parseGwei('2'),
to: accounts[3].address,
nonce: 785,
value: parseEther('1'),
}

test('should return same result as standard parser when not CIP42', () => {
const serialized = serializeTransaction(transaction)

expect(parseTransactionCelo(serialized)).toEqual(
parseTransaction_(serialized),
)
})

test('should parse a CIP42 transaction with gatewayFee', () => {
const transactionWithGatewayFee = {
...transaction,
chainId: 42270,
gatewayFee: parseEther('0.1'),
gatewayFeeRecipient: accounts[1].address,
}

const serialized = serializeTransactionCelo(transactionWithGatewayFee)

expect(parseTransactionCelo(serialized)).toMatchInlineSnapshot(`
{
"chainId": 42270,
"gas": 21001n,
"gatewayFee": 100000000000000000n,
"gatewayFeeRecipient": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
"maxFeePerGas": 2000000000n,
"maxPriorityFeePerGas": 2000000000n,
"nonce": 785,
"to": "0x90f79bf6eb2c4f870365e785982e1f101e93b906",
"type": "cip42",
"value": 1000000000000000000n,
}
`)
})

test('should parse a CIP42 transaction with access list', () => {
const transactionWithAccessList: TransactionSerializableCIP42 = {
feeCurrency: '0x765de816845861e75a25fca122bb6898b8b1282a',
...transaction,
chainId: 42270,
accessList: [
{
address: '0x0000000000000000000000000000000000000000',
storageKeys: [
'0x0000000000000000000000000000000000000000000000000000000000000001',
'0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe',
],
},
],
}

const serialized = serializeTransactionCelo(transactionWithAccessList)

expect(parseTransactionCelo(serialized)).toMatchInlineSnapshot(`
{
"accessList": [
{
"address": "0x0000000000000000000000000000000000000000",
"storageKeys": [
"0x0000000000000000000000000000000000000000000000000000000000000001",
"0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe",
],
},
],
"chainId": 42270,
"feeCurrency": "0x765de816845861e75a25fca122bb6898b8b1282a",
"gas": 21001n,
"maxFeePerGas": 2000000000n,
"maxPriorityFeePerGas": 2000000000n,
"nonce": 785,
"to": "0x90f79bf6eb2c4f870365e785982e1f101e93b906",
"type": "cip42",
"value": 1000000000000000000n,
}
`)
})

test('should parse a CIP42 transaction with data as 0x', () => {
const transactionWithData: TransactionSerializableCIP42 = {
feeCurrency: '0x765de816845861e75a25fca122bb6898b8b1282a',
...transaction,
chainId: 42270,
data: '0x',
}

const serialized = serializeTransactionCelo(transactionWithData)

expect(parseTransactionCelo(serialized)).toMatchInlineSnapshot(`
{
"chainId": 42270,
"feeCurrency": "0x765de816845861e75a25fca122bb6898b8b1282a",
"gas": 21001n,
"maxFeePerGas": 2000000000n,
"maxPriorityFeePerGas": 2000000000n,
"nonce": 785,
"to": "0x90f79bf6eb2c4f870365e785982e1f101e93b906",
"type": "cip42",
"value": 1000000000000000000n,
}
`)
})

test('should parse a CIP42 transaction with data', () => {
const transactionWithData: TransactionSerializableCIP42 = {
...transaction,
feeCurrency: '0x765de816845861e75a25fca122bb6898b8b1282a',
chainId: 42270,
data: '0x1234',
}

const serialized = serializeTransactionCelo(transactionWithData)

expect(parseTransactionCelo(serialized)).toMatchInlineSnapshot(`
{
"chainId": 42270,
"data": "0x1234",
"feeCurrency": "0x765de816845861e75a25fca122bb6898b8b1282a",
"gas": 21001n,
"maxFeePerGas": 2000000000n,
"maxPriorityFeePerGas": 2000000000n,
"nonce": 785,
"to": "0x90f79bf6eb2c4f870365e785982e1f101e93b906",
"type": "cip42",
"value": 1000000000000000000n,
}
`)
})

test('invalid transaction (all missing)', () => {
expect(() =>
parseTransactionCelo(`0x7c${toRlp([]).slice(2)}`),
).toThrowErrorMatchingInlineSnapshot(`
"Invalid serialized transaction of type \\"cip42\\" was provided.
Serialized Transaction: \\"0x7cc0\\"
Missing Attributes: chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gas, feeCurrency, to, gatewayFeeRecipient, gatewayFee, value, data, accessList
Version: viem@1.0.2"
`)
})

test('invalid transaction (some missing)', () => {
expect(() =>
parseTransactionCelo(`0x7c${toRlp(['0x0', '0x1']).slice(2)}`),
).toThrowErrorMatchingInlineSnapshot(`
"Invalid serialized transaction of type \\"cip42\\" was provided.
Serialized Transaction: \\"0x7cc20001\\"
Missing Attributes: maxPriorityFeePerGas, maxFeePerGas, gas, feeCurrency, to, gatewayFeeRecipient, gatewayFee, value, data, accessList
Version: viem@1.0.2"
`)
})

test('invalid transaction (missing signature)', () => {
expect(() =>
parseTransactionCelo(
`0x7c${toRlp([
'0x',
'0x',
'0x',
'0x',
'0x',
'0x',
'0x',
'0x',
'0x',
'0x',
'0x',
'0x',
'0x',
]).slice(2)}`,
),
).toThrowErrorMatchingInlineSnapshot(`
"Invalid serialized transaction of type \\"cip42\\" was provided.
Serialized Transaction: \\"0x7ccd80808080808080808080808080\\"
Missing Attributes: r, s
Version: viem@1.0.2"
`)
})
123 changes: 123 additions & 0 deletions src/chains/celo/parsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { InvalidSerializedTransactionError } from '../../errors/transaction.js'
import type { Hex } from '../../types/misc.js'
import { isHex } from '../../utils/data/isHex.js'
import { sliceHex } from '../../utils/data/slice.js'
import { hexToBigInt, hexToNumber } from '../../utils/encoding/fromHex.js'
import type { RecursiveArray } from '../../utils/encoding/toRlp.js'
import type { GetSerializedTransactionType } from '../../utils/transaction/getSerializedTransactionType.js'
import {
type ParseTransactionReturnType,
parseAccessList,
parseTransaction,
toTransactionArray,
} from '../../utils/transaction/parseTransaction.js'
import { assertTransactionCIP42 } from './serializers.js'
import type {
CeloTransactionSerialized,
CeloTransactionType,
TransactionSerializableCIP42,
TransactionSerializedCIP42,
} from './types.js'

export type ParseTransactionCeloReturnType<
TSerialized extends CeloTransactionSerialized = CeloTransactionSerialized,
TType extends CeloTransactionType = GetSerializedTransactionType<TSerialized>,
> = TSerialized extends TransactionSerializedCIP42
? TransactionSerializableCIP42
: ParseTransactionReturnType<TSerialized, TType>

export function parseTransactionCelo<
TSerialized extends CeloTransactionSerialized,
>(
serializedTransaction: TSerialized,
): ParseTransactionCeloReturnType<TSerialized> {
const serializedType = sliceHex(serializedTransaction, 0, 1)

if (serializedType === '0x7c')
return parseTransactionCIP42(
serializedTransaction as TransactionSerializedCIP42,
) as ParseTransactionCeloReturnType<TSerialized>

return parseTransaction(
serializedTransaction,
) as ParseTransactionCeloReturnType<TSerialized>
}

function parseTransactionCIP42(
serializedTransaction: TransactionSerializedCIP42,
): TransactionSerializableCIP42 {
const transactionArray = toTransactionArray(serializedTransaction)

const [
chainId,
nonce,
maxPriorityFeePerGas,
maxFeePerGas,
gas,
feeCurrency,
gatewayFeeRecipient,
gatewayFee,
to,
value,
data,
accessList,
v,
r,
s,
] = transactionArray

if (transactionArray.length !== 15 && transactionArray.length !== 12) {
throw new InvalidSerializedTransactionError({
attributes: {
chainId,
nonce,
maxPriorityFeePerGas,
maxFeePerGas,
gas,
feeCurrency,
to,
gatewayFeeRecipient,
gatewayFee,
value,
data,
accessList,
...(transactionArray.length > 12
? {
v,
r,
s,
}
: {}),
},
serializedTransaction,
type: 'cip42',
})
}

const transaction: Partial<TransactionSerializableCIP42> = {
chainId: hexToNumber(chainId as Hex),
type: 'cip42',
}

if (isHex(to) && to !== '0x') transaction.to = to
if (isHex(gas) && gas !== '0x') transaction.gas = hexToBigInt(gas)
if (isHex(data) && data !== '0x') transaction.data = data
if (isHex(nonce) && nonce !== '0x') transaction.nonce = hexToNumber(nonce)
if (isHex(value) && value !== '0x') transaction.value = hexToBigInt(value)
if (isHex(feeCurrency) && feeCurrency !== '0x')
transaction.feeCurrency = feeCurrency
if (isHex(gatewayFeeRecipient) && gatewayFeeRecipient !== '0x')
transaction.gatewayFeeRecipient = gatewayFeeRecipient
if (isHex(gatewayFee) && gatewayFee !== '0x')
transaction.gatewayFee = hexToBigInt(gatewayFee)
if (isHex(maxFeePerGas) && maxFeePerGas !== '0x')
transaction.maxFeePerGas = hexToBigInt(maxFeePerGas)
if (isHex(maxPriorityFeePerGas) && maxPriorityFeePerGas !== '0x')
transaction.maxPriorityFeePerGas = hexToBigInt(maxPriorityFeePerGas)
if (accessList.length !== 0 && accessList !== '0x')
transaction.accessList = parseAccessList(accessList as RecursiveArray<Hex>)

assertTransactionCIP42(transaction as TransactionSerializableCIP42)

return transaction as TransactionSerializableCIP42
}
Loading

1 comment on commit 7981fa9

@vercel
Copy link

@vercel vercel bot commented on 7981fa9 Aug 21, 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.