Skip to content

Commit 1a312c7

Browse files
author
Hugh Cunningham
committed
adds eth getAccount rpc for fetching account state
allows the wallet to use its RpcClient to get account state from the node (i.e. balance, nonce, confirmed balance etc.) request and response parameters match spec for eth_getAccount RPC (except that we don't support JSON RPC yet)
1 parent db15072 commit 1a312c7

File tree

7 files changed

+312
-0
lines changed

7 files changed

+312
-0
lines changed

ironfish/src/rpc/clients/client.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ import type {
171171
UseAccountRequest,
172172
UseAccountResponse,
173173
} from '../routes'
174+
import { GetAccountRequest, GetAccountResponse } from '../routes/eth'
174175
import { ApiNamespace } from '../routes/namespaces'
175176

176177
export abstract class RpcClient {
@@ -1001,4 +1002,13 @@ export abstract class RpcClient {
10011002
).waitForEnd()
10021003
},
10031004
}
1005+
1006+
eth = {
1007+
getAccount: (params: GetAccountRequest): Promise<RpcResponseEnded<GetAccountResponse>> => {
1008+
return this.request<GetAccountResponse>(
1009+
`${ApiNamespace.eth}/getAccount`,
1010+
params,
1011+
).waitForEnd()
1012+
},
1013+
}
10041014
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
{
2+
"Route eth/getAccount should fetch account data at the current head": [
3+
{
4+
"value": {
5+
"version": 4,
6+
"id": "d48171f9-8fc3-4a9b-9354-011f77b287f7",
7+
"name": "test",
8+
"spendingKey": "8f894e2e8b634d8c25f503b3fe6ec3893ede3f8701b185bb824f903e55e6232b",
9+
"viewKey": "b937ce5f1d9a3edd4a0e30d3e00a631b860ab17c81fef194d684604ab15ba4ca6266bec75920d9b5b3698a6c4981e94e56bf6a224bdce9eca0a6ef49674d2e91",
10+
"incomingViewKey": "ba9e6c31192c4fd217cbf6142e8ad3a0cff26bcc4422464df47afbe5f3ee8b03",
11+
"outgoingViewKey": "5cea903542b3cf599061c45346c3a2542ca1546f62806ab1129184c78498b81f",
12+
"publicAddress": "4144e12b9dc4f8c53c91e1408641e6c126f939778d1c4652ece7ce2798fc2db9",
13+
"createdAt": {
14+
"hash": {
15+
"type": "Buffer",
16+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
17+
},
18+
"sequence": 1
19+
},
20+
"scanningEnabled": true,
21+
"proofAuthorizingKey": "175edbaf8fa9ee3f2e05aa8c2fcdb70b5abeffed0c5905c0e4d78b3a4d31c500"
22+
},
23+
"head": {
24+
"hash": {
25+
"type": "Buffer",
26+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
27+
},
28+
"sequence": 1
29+
}
30+
},
31+
{
32+
"header": {
33+
"sequence": 2,
34+
"previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86",
35+
"noteCommitment": {
36+
"type": "Buffer",
37+
"data": "base64:lXWYMQoEkbZR+pF7SZE5EiJ4lEEMrBLLAzNZbElmvU0="
38+
},
39+
"transactionCommitment": {
40+
"type": "Buffer",
41+
"data": "base64:V5qNEOUeGW2WXy4saG4LvgKY9Lz1tuuBG5ipPqJxLe0="
42+
},
43+
"target": "9282972777491357380673661573939192202192629606981189395159182914949423",
44+
"randomness": "0",
45+
"timestamp": 1721950965582,
46+
"graffiti": "0000000000000000000000000000000000000000000000000000000000000000",
47+
"noteSize": 4,
48+
"work": "0",
49+
"stateCommitment": {
50+
"type": "Buffer",
51+
"data": "base64:lT/pkKDT9ozPjIb+5e8iLRknYetpuAVFfinYVkVcVyA="
52+
}
53+
},
54+
"transactions": [
55+
{
56+
"type": "Buffer",
57+
"data": "base64:AwAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAaX7MerpGEjjI+hDW6LjsEOTNvInjOZ4YWJiOZsgUCKCRQTh5cKRgWJwHZ4htZIamyX/hUipD2Ur4YVu29cJTMsf7NvW1lEMeYrTfr9V6IliqCfgkqN2VFfFlo2ZvkosHxirkd2SIObl4xXfGgcESoak1ll2CqnXla606hpP7tq4KJ2Y3fenjzZUG17lE5vKML2HMFc/3rXDBVFut/HoXgY0h5joKjRwrcJz1RoVfcx65qLrurqyA8M+zqho2dW5z2MQ7dqfq8MX5cZ4q1OdaV6AKnRYidtM+ar9dAq04s+eKgWcaZ18uh51l6P5s20ujC+3iJepezcv/HVrgRQ8fN30SdDhzD3t58GlVK5sU8QEezClEWC06BaSJz/deYncNV9faHllcbu/srFeGJGsQhWELNq6MT1/kvV6Nra3R5OsR3IN3EHHMIeSgh6h8vhXZvglHn+iqw7eEHvXVh5k6ezI+uY4D6vXzd+rjZJ3gvnfY+hSaVduxA2llbE1kmCeFyiXwQC8/1hrJ2dgrh6ons2l/2QDv/XWqGrtEOnQM+kZcMQPscQljvwuCFaVDifl8jqnTRzVlKS3P8vnkFB8ypgFQn+PextadhR1IQ/c62xwGXB4JYs85ZElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwADMuj/wIg7voruKdWMPLpdBzmE98oUJ7Cm5R6V83Kt/ndaLTtTRaQqPGCCGM52YHx9x2DxR4XSAoHIx/cGYLvgQ="
58+
}
59+
]
60+
}
61+
],
62+
"Route eth/getAccount should fetch account data at past blocks": [
63+
{
64+
"value": {
65+
"version": 4,
66+
"id": "9d875a49-b7d6-4c41-b7b5-7f1e76f2da34",
67+
"name": "test2",
68+
"spendingKey": "93dd77a8f5a796ef840e849f0a7ccf163ad129dea7791fcda08a0742d89cfb5b",
69+
"viewKey": "7327f352231ae43d0ae693656553f63b181df7ee8fbec0092308c65d771d9ac6a7f4f468165961305aa934398fa991c2c3740b211b5ac9a3570f43f1fc5eae53",
70+
"incomingViewKey": "b5171205606518a3254da401b756cdb3917093428b2e01fa89075f074f567f05",
71+
"outgoingViewKey": "d8b12265c21f044c0f1098773d816badc1e73623c12e44ea398d7e37c1feac8c",
72+
"publicAddress": "6ecf210cd81091a718748c4c078faebde7148435ec4f24428dd494f15991195f",
73+
"createdAt": {
74+
"hash": {
75+
"type": "Buffer",
76+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
77+
},
78+
"sequence": 1
79+
},
80+
"scanningEnabled": true,
81+
"proofAuthorizingKey": "431b2bd20dc1287524f4fc715ac60db509cf60e5a14ae0e1f4ae0630a5370a09"
82+
},
83+
"head": {
84+
"hash": {
85+
"type": "Buffer",
86+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
87+
},
88+
"sequence": 1
89+
}
90+
},
91+
{
92+
"header": {
93+
"sequence": 2,
94+
"previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86",
95+
"noteCommitment": {
96+
"type": "Buffer",
97+
"data": "base64:YkCklPqvm0h1UJvl9BOuWEeEpuAaynV0fAPBX8h1fSM="
98+
},
99+
"transactionCommitment": {
100+
"type": "Buffer",
101+
"data": "base64:aW1drWEs5gKdBJA2Oakl+vL3gstkx0g6sGOdZ3mMinc="
102+
},
103+
"target": "9282972777491357380673661573939192202192629606981189395159182914949423",
104+
"randomness": "0",
105+
"timestamp": 1721950966412,
106+
"graffiti": "0000000000000000000000000000000000000000000000000000000000000000",
107+
"noteSize": 4,
108+
"work": "0",
109+
"stateCommitment": {
110+
"type": "Buffer",
111+
"data": "base64:BZv0PplxhjGRuH6r8lnN8WxUiC3jOB+KwyFAJkFqbuw="
112+
}
113+
},
114+
"transactions": [
115+
{
116+
"type": "Buffer",
117+
"data": "base64:AwAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAK9H6ZsEgwYtfBouhmG6+Hf3plwGu59Ov+Q3+m43bT6CvCOijXsIXW8LgDLv2oXBDnCgmLh7e/3180xhFk2c6XodRDGW6P1Q8g1hlqIHppxmMvJQTNHKTKdEwdxIwjYotj6gDuR7usB9Lr0Hin4ZZbkoFyINmS6LhM1v7OmBVwjgMoG5rX1FdMO5kZFt5UMbU210DHFWowBZoputeSSYMAmbDOUPU/Q9zlTFDH9NKMPqFMhtTpliptVQ8E0i+Z/y0t0peV1oDS/VDfXsQnGFnV5M0xSr7PNvZTVr5Y4XqFQCwRlz+8GiblCZ3PFgrI+jfz+Jp7iJbe5dQbNrh6m8oyPow5uNQOupRdz939Nurj0+4vCeiVoflCyMZfcsSuwYPHx5d8yROQe9AcjbCKZ0sSHrbZhQeWI/N8ecTJnYjy5U2zx0UmCFmxMv3hhT7D9Yrx346R15PutP2ZqdWeywU8vadl3+OwHfDGtpOh0BYekHy+YDCM/yxFM2Sx8p4FPlyu2ucidurhIgiYJmI+dGRi9nbAfIKzOU1Sx0Zegvb0gUvb01uSeG9mwUUlPvZHrK1x6ZghQOhmRV2k/VwSamLj5+fT3zhN3A2DdzqmKm+3pOS1rkKzMyxh0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAGFuUmX/JtL7DLH66ahH3fbsPLJ1sQEFZ08KkUfAYu2rl01dgnaSwLKNnd8BDbHM1ux9Igj2Nkj5SiccOMpBtgQ="
118+
}
119+
]
120+
},
121+
{
122+
"header": {
123+
"sequence": 3,
124+
"previousBlockHash": "43B872A575667A00290A818BCE5AEDACAAF4D8577D0156E063D53F665FD6A9DD",
125+
"noteCommitment": {
126+
"type": "Buffer",
127+
"data": "base64:/4U56z8nkWFt/0WCrIh0EOY5sMzFx2Ba+fJa2k1IWnI="
128+
},
129+
"transactionCommitment": {
130+
"type": "Buffer",
131+
"data": "base64:mOMhd2updaX7O8oAlfc4W7B1sAXXt7DikqcUtt3julc="
132+
},
133+
"target": "9255858786337818395603165512831024101510453493377417362192396248796027",
134+
"randomness": "0",
135+
"timestamp": 1721950966953,
136+
"graffiti": "0000000000000000000000000000000000000000000000000000000000000000",
137+
"noteSize": 5,
138+
"work": "0",
139+
"stateCommitment": {
140+
"type": "Buffer",
141+
"data": "base64:oJNfcv4yuJFVz+zFvrSfefSxMKhlR6IXdkFthpYk/QM="
142+
}
143+
},
144+
"transactions": [
145+
{
146+
"type": "Buffer",
147+
"data": "base64:AwAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAc5bGRLhvW2OUIfytN64K2EwI0WvdzsbgsFR3YOqTkeWgRMIyEaXXRBa4WG4/NDnBp0QCfmB4v1WLvF+3YHb3Y6rV9Vq8khkYhTZD1KHgr2iIxb9aEJjONoDCbDrs3sBIcARxGrRqaWTUtXiVjiibW6VIogj7iNA4JtIor5vOLZ8NUplHuiSZUOIreUM+TPVkdD+1MokdEyYSNrhMinl9IuRCAcVZNBV6M2ElTlyOJyiGOwkDkqYOPwOY0PDjYgwX25EMsGZ2RDIcrqGK2A10EVL3abPOHAgQrYoofk371yTSx19fCXQY8A2KVHSDO9QG9YET9IWqYGnjw4I66ZvVqfwZy6I8HBNhEKU/nGgGqE7wGiqrxnNSusvcu52SNlEWELsbBhEmI6fwD5Mje2BAa+HYsPSEjxpGeYjOtuWGudBFDnUPjSFdMnf5RelHeVx936OLOXo23wP3ZLkbBinrx+NGZTCkQBb7JzhEfkqD44xFW3llLAs2TDx0EGVwDOi/dqajm1MyQovsvanfMUhayKb+tUZrcy9iS4YybUuiIB1wLooiP/E10PfJ6Gas+U15IAptUMzxfwRo/9SwVNu3c4J5SK4L5qONLf7N5o9EPCjN6maWh0n340lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAIhywuOD9Y+HGr/38Ojx6SdL/SI4AuIRaWV4Z5LAOEMpLAjeZ4rx96jHOH6+WptyvS8TvnqHzLatK2ZgUSOYoAY="
148+
}
149+
]
150+
}
151+
]
152+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4+
import { Account as EthAccount, Address } from '@ethereumjs/util'
5+
import { useAccountFixture, useMinerBlockFixture } from '../../../testUtilities'
6+
import { createRouteTest } from '../../../testUtilities/routeTest'
7+
import { CurrencyUtils } from '../../../utils'
8+
9+
describe('Route eth/getAccount', () => {
10+
const routeTest = createRouteTest(false)
11+
12+
it('should fetch account data at the current head', async () => {
13+
const { node } = routeTest
14+
node.chain.consensus.parameters.enableEvmDescriptions = 2
15+
16+
const account = await useAccountFixture(node.wallet, 'test')
17+
const address = Address.fromPrivateKey(Buffer.from(account.spendingKey, 'hex'))
18+
const ethAccount = new EthAccount(0n, 10n)
19+
20+
await node.chain.blockchainDb.stateManager.checkpoint()
21+
await node.chain.blockchainDb.stateManager.putAccount(address, ethAccount)
22+
await node.chain.blockchainDb.stateManager.commit()
23+
24+
const block2 = await useMinerBlockFixture(node.chain, 2, account)
25+
26+
expect(block2.header.stateCommitment).toBeDefined()
27+
await expect(node.chain).toAddBlock(block2)
28+
29+
const response = await routeTest.client.eth.getAccount({
30+
address: address.toString(),
31+
blockReference: '2',
32+
})
33+
34+
expect(response.status).toEqual(200)
35+
expect(response.content.balance).toEqual(CurrencyUtils.encode(ethAccount.balance))
36+
expect(response.content.nonce).toEqual(String(ethAccount.nonce))
37+
})
38+
39+
it('should fetch account data at past blocks', async () => {
40+
const { node } = routeTest
41+
node.chain.consensus.parameters.enableEvmDescriptions = 2
42+
43+
const account = await useAccountFixture(node.wallet, 'test2')
44+
const address = Address.fromPrivateKey(Buffer.from(account.spendingKey, 'hex'))
45+
const ethAccount1 = new EthAccount(0n, 1n)
46+
47+
await node.chain.blockchainDb.stateManager.checkpoint()
48+
await node.chain.blockchainDb.stateManager.putAccount(address, ethAccount1)
49+
await node.chain.blockchainDb.stateManager.commit()
50+
51+
const block2 = await useMinerBlockFixture(node.chain, 2, account)
52+
53+
expect(block2.header.stateCommitment).toBeDefined()
54+
await expect(node.chain).toAddBlock(block2)
55+
56+
const ethAccount2 = new EthAccount(0n, 2n)
57+
58+
await node.chain.blockchainDb.stateManager.checkpoint()
59+
await node.chain.blockchainDb.stateManager.putAccount(address, ethAccount2)
60+
await node.chain.blockchainDb.stateManager.commit()
61+
62+
const block3 = await useMinerBlockFixture(node.chain, 3, account)
63+
64+
expect(block3.header.stateCommitment).toBeDefined()
65+
await expect(node.chain).toAddBlock(block3)
66+
67+
const response = await routeTest.client.eth.getAccount({
68+
address: address.toString(),
69+
blockReference: '2',
70+
})
71+
72+
expect(response.status).toEqual(200)
73+
expect(response.content.balance).toEqual(CurrencyUtils.encode(ethAccount1.balance))
74+
expect(response.content.nonce).toEqual(String(ethAccount1.nonce))
75+
})
76+
})
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4+
import { Address, bytesToHex } from '@ethereumjs/util'
5+
import * as yup from 'yup'
6+
import { Assert } from '../../../assert'
7+
import { FullNode } from '../../../node'
8+
import { CurrencyUtils } from '../../../utils'
9+
import { RpcNotFoundError } from '../../adapters'
10+
import { ApiNamespace } from '../namespaces'
11+
import { routes } from '../router'
12+
13+
export type GetAccountRequest = {
14+
address: string
15+
blockReference: string
16+
}
17+
18+
export type GetAccountResponse = {
19+
codeHash: string
20+
storageRoot: string
21+
balance: string
22+
nonce: string
23+
}
24+
25+
export const GetAccountRequestSchema: yup.ObjectSchema<GetAccountRequest> = yup
26+
.object({
27+
address: yup.string().defined().trim(),
28+
blockReference: yup.string().defined().trim(),
29+
})
30+
.defined()
31+
32+
export const GetAccountResponseSchema: yup.ObjectSchema<GetAccountResponse> = yup
33+
.object({
34+
codeHash: yup.string().defined(),
35+
storageRoot: yup.string().defined(),
36+
balance: yup.string().defined(),
37+
nonce: yup.string().defined(),
38+
})
39+
.defined()
40+
41+
routes.register<typeof GetAccountRequestSchema, GetAccountResponse>(
42+
`${ApiNamespace.eth}/getAccount`,
43+
GetAccountRequestSchema,
44+
async (request, node): Promise<void> => {
45+
Assert.isInstanceOf(node, FullNode)
46+
47+
const address = Address.fromString(request.data.address)
48+
49+
// TODO(hughy): parse blockReference as hex string or as tag (e.g., 'latest')
50+
const blockNumber = Number(request.data.blockReference)
51+
const block = await node.chain.getBlockAtSequence(blockNumber)
52+
if (!block) {
53+
throw new RpcNotFoundError(`No block found with reference ${request.data.blockReference}`)
54+
}
55+
56+
const account = await node.chain.evm.getAccount(address, block.header.stateCommitment)
57+
if (!account) {
58+
throw new RpcNotFoundError(`No account found with address ${request.data.address}`)
59+
}
60+
61+
request.end({
62+
codeHash: bytesToHex(account.codeHash),
63+
storageRoot: bytesToHex(account.storageRoot),
64+
balance: CurrencyUtils.renderOre(account.balance),
65+
nonce: account.nonce.toString(),
66+
})
67+
},
68+
)

ironfish/src/rpc/routes/eth/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4+
export * from './getAccount'

ironfish/src/rpc/routes/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ export * from './worker'
1515
export * from './mempool'
1616
export * from './namespaces'
1717
export * from './rpcContext'
18+
export * from './eth'

ironfish/src/rpc/routes/namespaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ export enum ApiNamespace {
1414
worker = 'worker',
1515
rpc = 'rpc',
1616
mempool = 'mempool',
17+
eth = 'eth',
1718
}

0 commit comments

Comments
 (0)