Skip to content

Commit 07f9e0d

Browse files
normalize transaction chain id to handle non-standard chain ids and r… (#1411)
Co-authored-by: goosewobbler <432005+goosewobbler@users.noreply.github.com>
1 parent 45ec2df commit 07f9e0d

File tree

6 files changed

+161
-78
lines changed

6 files changed

+161
-78
lines changed

main/api/http.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import http, { IncomingMessage, ServerResponse } from 'http'
22
import log from 'electron-log'
3+
import { isHexString } from '@ethereumjs/util'
34

45
import provider from '../provider'
56
import accounts from '../accounts'
@@ -86,10 +87,20 @@ const handler = (req: IncomingMessage, res: ServerResponse) => {
8687
)
8788

8889
const origin = parseOrigin(req.headers.origin)
89-
const { payload } = updateOrigin(rawPayload, origin)
90+
const { payload, chainId } = updateOrigin(rawPayload, origin)
9091

9192
extendSession(payload._origin)
9293

94+
if (!isHexString(chainId)) {
95+
const error = {
96+
message: `Invalid chain id (${rawPayload.chainId}), chain id must be hex-prefixed string`,
97+
code: -1
98+
}
99+
100+
res.writeHead(401, { 'Content-Type': 'application/json' })
101+
return res.end(JSON.stringify({ id: payload.id, jsonrpc: payload.jsonrpc, error }))
102+
}
103+
93104
if (protectedMethods.indexOf(payload.method) > -1 && !(await isTrusted(payload))) {
94105
let error = { message: `Permission denied, approve ${origin} in Frame to continue`, code: 4001 }
95106
// Review

main/api/ws.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { IncomingMessage, Server } from 'http'
12
import WebSocket from 'ws'
23
import { v4 as uuid } from 'uuid'
34
import log from 'electron-log'
5+
import { isHexString } from '@ethereumjs/util'
46

57
import store from '../store'
68
import provider from '../provider'
@@ -17,7 +19,6 @@ import {
1719
} from './origins'
1820
import validPayload from './validPayload'
1921
import protectedMethods from './protectedMethods'
20-
import { IncomingMessage, Server } from 'http'
2122

2223
const logTraffic = process.env.LOG_TRAFFIC
2324

@@ -98,6 +99,14 @@ const handler = (socket: FrameWebSocket, req: IncomingMessage) => {
9899

99100
const { payload, chainId } = updateOrigin(rawPayload, origin, rawPayload.__extensionConnecting)
100101

102+
if (!isHexString(chainId)) {
103+
const error = {
104+
message: `Invalid chain id (${rawPayload.chainId}), chain id must be hex-prefixed string`,
105+
code: -1
106+
}
107+
return res({ id: rawPayload.id, jsonrpc: rawPayload.jsonrpc, error })
108+
}
109+
101110
if (!rawPayload.__extensionConnecting) {
102111
extendSession(payload._origin)
103112
}

main/provider/index.ts

+59-54
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import accounts, {
1919
} from '../accounts'
2020
import Chains, { Chain } from '../chains'
2121
import { getSignerType, Type as SignerType } from '../../resources/domain/signer'
22-
import { TransactionData } from '../../resources/domain/transaction'
22+
import { normalizeChainId, TransactionData } from '../../resources/domain/transaction'
2323
import { populate as populateTransaction, maxFee, classifyTransaction } from '../transaction'
2424
import FrameAccount from '../accounts/Account'
2525
import { capitalize } from '../../resources/utils'
@@ -449,14 +449,23 @@ export class Provider extends EventEmitter {
449449
}
450450

451451
async fillTransaction(newTx: RPC.SendTransaction.TxParams, cb: Callback<TransactionMetadata>) {
452-
if (!newTx) return cb(new Error('No transaction data'))
452+
if (!newTx) {
453+
return cb(new Error('No transaction data'))
454+
}
455+
456+
const connection = this.connection.connections['ethereum'][parseInt(newTx.chainId, 16)]
457+
const chainConnected = connection && (connection.primary?.connected || connection.secondary?.connected)
458+
459+
if (!chainConnected) {
460+
return cb(new Error(`Chain ${newTx.chainId} not connected`))
461+
}
453462

454463
try {
455464
const approvals: RequiredApproval[] = []
456465
const accountId = (accounts.current() || {}).id
457466
const rawTx = getRawTx(newTx, accountId)
458467
const gas = gasFees(rawTx)
459-
const chainConfig = this.connection.connections['ethereum'][parseInt(rawTx.chainId)].chainConfig
468+
const { chainConfig } = connection
460469

461470
const estimateGasLimit = async () => {
462471
try {
@@ -497,69 +506,65 @@ export class Provider extends EventEmitter {
497506
}
498507

499508
sendTransaction(payload: RPC.SendTransaction.Request, res: RPCRequestCallback, targetChain: Chain) {
500-
const txParams = payload.params[0]
501-
const payloadChain = payload.chainId
502-
const txChain = txParams.chainId
503-
504-
if (payloadChain && txChain && parseInt(payloadChain, 16) !== parseInt(txChain, 16)) {
505-
return resError(
506-
`Chain for transaction (${txChain}) does not match request target chain (${payloadChain})`,
507-
payload,
508-
res
509-
)
510-
}
509+
try {
510+
const txParams = payload.params[0]
511+
const payloadChain = payload.chainId
511512

512-
const newTx = {
513-
...txParams,
514-
chainId: txChain || payloadChain || addHexPrefix(targetChain.id.toString(16))
515-
}
513+
const normalizedTx = normalizeChainId(txParams, payloadChain ? parseInt(payloadChain, 16) : undefined)
514+
const tx = {
515+
...normalizedTx,
516+
chainId: normalizedTx.chainId || payloadChain || addHexPrefix(targetChain.id.toString(16))
517+
}
516518

517-
const currentAccount = accounts.current()
519+
const currentAccount = accounts.current()
518520

519-
log.verbose(`sendTransaction(${JSON.stringify(newTx)}`)
521+
log.verbose(`sendTransaction(${JSON.stringify(tx)}`)
520522

521-
this.fillTransaction(newTx, (err, transactionMetadata) => {
522-
if (err) {
523-
resError(err, payload, res)
524-
} else {
525-
const txMetadata = transactionMetadata as TransactionMetadata
526-
const from = txMetadata.tx.from
523+
this.fillTransaction(tx, (err, transactionMetadata) => {
524+
if (err) {
525+
resError(err, payload, res)
526+
} else {
527+
const txMetadata = transactionMetadata as TransactionMetadata
528+
const from = txMetadata.tx.from
527529

528-
if (!currentAccount || !hasAddress(currentAccount, from)) {
529-
return resError('Transaction is not from currently selected account', payload, res)
530-
}
530+
if (!currentAccount || !hasAddress(currentAccount, from)) {
531+
return resError('Transaction is not from currently selected account', payload, res)
532+
}
531533

532-
const handlerId = this.addRequestHandler(res)
534+
const handlerId = this.addRequestHandler(res)
533535

534-
const { feesUpdated, recipientType, ...data } = txMetadata.tx
536+
const { feesUpdated, recipientType, ...data } = txMetadata.tx
535537

536-
const unclassifiedReq = {
537-
handlerId,
538-
type: 'transaction',
539-
data,
540-
payload,
541-
account: (currentAccount as FrameAccount).id,
542-
origin: payload._origin,
543-
approvals: [],
544-
feesUpdatedByUser: false,
545-
recipientType,
546-
recognizedActions: []
547-
} as Omit<TransactionRequest, 'classification'>
538+
const unclassifiedReq = {
539+
handlerId,
540+
type: 'transaction',
541+
data,
542+
payload,
543+
account: (currentAccount as FrameAccount).id,
544+
origin: payload._origin,
545+
approvals: [],
546+
feesUpdatedByUser: false,
547+
recipientType,
548+
recognizedActions: []
549+
} as Omit<TransactionRequest, 'classification'>
548550

549-
const classification = classifyTransaction(unclassifiedReq)
551+
const classification = classifyTransaction(unclassifiedReq)
550552

551-
const req = {
552-
...unclassifiedReq,
553-
classification
554-
}
553+
const req = {
554+
...unclassifiedReq,
555+
classification
556+
}
555557

556-
accounts.addRequest(req, res)
558+
accounts.addRequest(req, res)
557559

558-
txMetadata.approvals.forEach((approval) => {
559-
currentAccount?.addRequiredApproval(req, approval.type, approval.data)
560-
})
561-
}
562-
})
560+
txMetadata.approvals.forEach((approval) => {
561+
currentAccount?.addRequiredApproval(req, approval.type, approval.data)
562+
})
563+
}
564+
})
565+
} catch (e) {
566+
resError((e as Error).message, payload, res)
567+
}
563568
}
564569

565570
getTransactionByHash(payload: RPCRequestPayload, cb: RPCRequestCallback, targetChain: Chain) {

resources/domain/transaction/index.ts

+31
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { JsonTx } from '@ethereumjs/tx'
2+
import { addHexPrefix, isHexString } from '@ethereumjs/util'
23

34
export enum GasFeesSource {
45
Dapp = 'Dapp',
@@ -23,3 +24,33 @@ export function typeSupportsBaseFee(type: string) {
2324
export function usesBaseFee(rawTx: TransactionData) {
2425
return typeSupportsBaseFee(rawTx.type)
2526
}
27+
28+
function parseChainId(chainId: string) {
29+
if (isHexString(chainId)) {
30+
return parseInt(chainId, 16)
31+
}
32+
33+
return Number(chainId)
34+
}
35+
36+
// TODO: move this into requests parsing module
37+
export function normalizeChainId(tx: RPC.SendTransaction.TxParams, targetChain?: number) {
38+
if (!tx.chainId) return tx
39+
40+
const chainId = parseChainId(tx.chainId)
41+
42+
if (!chainId) {
43+
throw new Error(`Chain for transaction (${tx.chainId}) is not a hex-prefixed string`)
44+
}
45+
46+
if (targetChain && targetChain !== chainId) {
47+
throw new Error(
48+
`Chain for transaction (${tx.chainId}) does not match request target chain (${targetChain})`
49+
)
50+
}
51+
52+
return {
53+
...tx,
54+
chainId: addHexPrefix(chainId.toString(16))
55+
}
56+
}

test/main/provider/index.test.js

+3-21
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,9 @@ describe('#send', () => {
807807
})
808808

809809
connection.connections.ethereum[chainId] = {
810+
primary: {
811+
connected: true
812+
},
810813
chainConfig: chainConfig(chainId, chainId === 1 ? 'london' : 'istanbul')
811814
}
812815
})
@@ -886,27 +889,6 @@ describe('#send', () => {
886889
})
887890

888891
describe('replacing gas fees', () => {
889-
beforeEach(() => {
890-
const chainIds = [1, 137]
891-
892-
chainIds.forEach((chainId) => {
893-
store.set('main.networksMeta.ethereum', chainId, 'gas', {
894-
price: {
895-
selected: 'standard',
896-
levels: { slow: '', standard: '', fast: gweiToHex(30), asap: '', custom: '' },
897-
fees: {
898-
maxPriorityFeePerGas: gweiToHex(1),
899-
maxBaseFeePerGas: gweiToHex(8)
900-
}
901-
}
902-
})
903-
904-
connection.connections.ethereum[chainId] = {
905-
chainConfig: chainConfig(chainId, chainId === 1 ? 'london' : 'istanbul')
906-
}
907-
})
908-
})
909-
910892
it('adds a 10% gas buffer when replacing a legacy transaction', (done) => {
911893
tx.type = '0x0'
912894
tx.chainId = addHexPrefix((137).toString(16))

test/resources/domain/transaction/index.test.js

+46-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { typeSupportsBaseFee, usesBaseFee } from '../../../../resources/domain/transaction'
1+
import {
2+
normalizeChainId,
3+
parseChainId,
4+
typeSupportsBaseFee,
5+
usesBaseFee
6+
} from '../../../../resources/domain/transaction'
27

38
describe('#typeSupportsBaseFee', () => {
49
it('does not support a base fee for type 0', () => {
@@ -39,3 +44,43 @@ describe('#usesBaseFee', () => {
3944
expect(usesBaseFee(tx)).toBe(true)
4045
})
4146
})
47+
48+
describe('#normalizeChainId', () => {
49+
it('does not modify a transaction with no chain id', () => {
50+
const tx = { to: '0xframe' }
51+
52+
expect(normalizeChainId(tx)).toStrictEqual(tx)
53+
})
54+
55+
it('normalizes a hex-prefixed chain id', () => {
56+
const tx = { to: '0xframe', chainId: '0xa' }
57+
58+
expect(normalizeChainId(tx)).toStrictEqual({ to: '0xframe', chainId: '0xa' })
59+
})
60+
61+
it('does not handle a hex chain id with no prefix', () => {
62+
const tx = { to: '0xframe', chainId: 'a' }
63+
64+
expect(() => normalizeChainId(tx)).toThrowError(/chain for transaction.*is not a hex-prefixed string/i)
65+
})
66+
67+
it('normalizes a numeric chain id', () => {
68+
const tx = { to: '0xframe', chainId: 14 }
69+
70+
expect(normalizeChainId(tx)).toStrictEqual({ to: '0xframe', chainId: '0xe' })
71+
})
72+
73+
it('normalizes a numeric string chain id', () => {
74+
const tx = { to: '0xframe', chainId: '100' }
75+
76+
expect(normalizeChainId(tx)).toStrictEqual({ to: '0xframe', chainId: '0x64' })
77+
})
78+
79+
it('does not allow a chain id that does not match the target chain', () => {
80+
const tx = { to: '0xframe', chainId: '0xa' }
81+
82+
expect(() => normalizeChainId(tx, 11)).toThrowError(
83+
/chain for transaction.*does not match request target chain/i
84+
)
85+
})
86+
})

0 commit comments

Comments
 (0)