Skip to content

Commit

Permalink
eth/sendTransaction RPC (WIP) (#5268)
Browse files Browse the repository at this point in the history
* Add send transaction RPC

* Fix lint
  • Loading branch information
danield9tqh committed Aug 15, 2024
1 parent 6367504 commit aa24089
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 0 deletions.
12 changes: 12 additions & 0 deletions ironfish/src/rpc/clients/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ import {
SendRawTransactionRequest,
SendRawTransactionResponse,
} from '../routes/eth'
import {
EthSendTransactionRequest,
EthSendTransactionResponse,
} from '../routes/eth/sendTransaction'
import { ApiNamespace } from '../routes/namespaces'

export abstract class RpcClient {
Expand Down Expand Up @@ -1023,5 +1027,13 @@ export abstract class RpcClient {
params,
).waitForEnd()
},
sendTransaction: (
params: EthSendTransactionRequest,
): Promise<RpcResponseEnded<EthSendTransactionResponse>> => {
return this.request<EthSendTransactionResponse>(
`${ApiNamespace.eth}/sendTransaction`,
params,
).waitForEnd()
},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"Route eth/sendRawTransaction should construct evm transaction and submit to node": [
{
"value": {
"version": 4,
"id": "a564b402-05d4-409c-8870-38123af5df55",
"name": "sender",
"spendingKey": "0292797f0960c31dea187f003077e6b50060a0fb4c353ea7489686594db820fe",
"viewKey": "c935c8075cc794bdc1828d9b8ba0f7c337ded09ccfd87408f9bb659d35cd583663a66a64ea5346dd90ba8e789835ac2eb3897def8f3e5f827706a8c30b4133e5",
"incomingViewKey": "5081a2f748587124c358263159f9dc37dca2be3c1435ec7bc2582400991bc802",
"outgoingViewKey": "ba97180b44cbe448bcc9d2e353df8a777972ce39b802f452389957b09111c53d",
"publicAddress": "800243d1a8f126222e1a7e1a5d3c3ea1a8a960b173695176b29be6fc71847e37",
"createdAt": {
"hash": {
"type": "Buffer",
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
},
"sequence": 1
},
"scanningEnabled": true,
"proofAuthorizingKey": "ce16da48daf6e5aebaa68baa7bb9cde2e37da86187253da1eee60dc4931e6d00"
},
"head": {
"hash": {
"type": "Buffer",
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
},
"sequence": 1
}
}
],
"Route eth/sendRawTransaction should construct an evm shield transaction and submit to node": [
{
"value": {
"version": 4,
"id": "b8e50242-f166-457f-957b-2e11cf8ba92a",
"name": "ifReceivingAccount",
"spendingKey": "21044d211270de5755ce9117384db1803e70f2abbf0339c1b969cf6293f07440",
"viewKey": "edbc41e9b9354e90a0bd85f72b01155b940cff688de69c87565b89297a7b25140d58fa27d5104bb9ac5c64effa1e92dc2a9943991221a96af3022a28806cdd9f",
"incomingViewKey": "fb25acfbace7285463cc7b50d9553d7818f21f4355df252175d2eeff05c26c02",
"outgoingViewKey": "7b7ef391d01fdc30834009acc2e768cef636d78f4cf67f1fb99997096dbd546b",
"publicAddress": "9fa0f27a65d94e3f80dbb1c00a8d1ff14c33869f30cb3ed9412ac802146d9f8f",
"createdAt": {
"hash": {
"type": "Buffer",
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
},
"sequence": 1
},
"scanningEnabled": true,
"proofAuthorizingKey": "97e9cf89bc242e78aa8505854411b59e7b76f5fef3115907c44e8bdbdb7fe503"
},
"head": {
"hash": {
"type": "Buffer",
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
},
"sequence": 1
}
}
]
}
1 change: 1 addition & 0 deletions ironfish/src/rpc/routes/eth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
export * from './getAccount'
export * from './sendRawTransaction'
export * from './sendTransaction'
64 changes: 64 additions & 0 deletions ironfish/src/rpc/routes/eth/sendTransaction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { Account as EthAccount, Address } from '@ethereumjs/util'
import { ethers } from 'ethers'
import { Assert } from '../../../assert'
import { ContractArtifact, GLOBAL_CONTRACT_ADDRESS } from '../../../evm'
import { useAccountFixture } from '../../../testUtilities'
import { createRouteTest } from '../../../testUtilities/routeTest'

describe('Route eth/sendRawTransaction', () => {
const routeTest = createRouteTest()
const globalContract = new ethers.Interface(ContractArtifact.abi)

it('should construct evm transaction and submit to node', async () => {
const senderIf = await useAccountFixture(routeTest.node.wallet, 'sender')

const evmPrivateKey = Uint8Array.from(Buffer.from(senderIf.spendingKey || '', 'hex'))

const evmSenderAddress = Address.fromPrivateKey(evmPrivateKey)
const senderAccount = new EthAccount(BigInt(0), 500_000_000n)

await routeTest.node.chain.blockchainDb.stateManager.checkpoint()
await routeTest.node.chain.blockchainDb.stateManager.putAccount(
evmSenderAddress,
senderAccount,
)
await routeTest.node.chain.blockchainDb.stateManager.commit()

const evmAccount = await routeTest.node.chain.blockchainDb.stateManager.getAccount(
evmSenderAddress,
)
Assert.isNotUndefined(evmAccount)

const response = await routeTest.client.eth.sendTransaction({
nonce: '0x0',
to: evmSenderAddress.toString(),
from: evmSenderAddress.toString(),
value: '0xEE6B280', // 250000000
})

expect(response.status).toEqual(200)
})

it('should construct an evm shield transaction and submit to node', async () => {
const { wallet } = routeTest

const ifReceivingAccount = await useAccountFixture(wallet, 'ifReceivingAccount')

const encodedFunctionData = globalContract.encodeFunctionData('shield', [
Buffer.from(ifReceivingAccount.publicAddress, 'hex'),
2n,
500n,
])

const response = await routeTest.client.eth.sendTransaction({
to: GLOBAL_CONTRACT_ADDRESS.toString(),
from: ifReceivingAccount.ethAddress!.toString(),
data: encodedFunctionData,
})

expect(response.status).toEqual(200)
})
})
109 changes: 109 additions & 0 deletions ironfish/src/rpc/routes/eth/sendTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { LegacyTransaction } from '@ethereumjs/tx'
import { Address } from '@ethereumjs/util'
import * as yup from 'yup'
import { Assert } from '../../../assert'
import { GLOBAL_IF_ACCOUNT } from '../../../evm'
import { FullNode } from '../../../node'
import { legacyTransactionToEvmDescription } from '../../../primitives'
import { EthUtils } from '../../../utils/eth'
import { AssertSpending } from '../../../wallet/account/account'
import { ApiNamespace } from '../namespaces'
import { routes } from '../router'

export type EthSendTransactionRequest = {
from: string
to?: string
gas?: string
gasPrice?: string
value?: string
data?: string
nonce?: string
}

export type EthSendTransactionResponse = {
result: string
}

export const EthSendTransactionRequestSchema: yup.ObjectSchema<EthSendTransactionRequest> = yup
.object({
from: yup.string().defined(),
to: yup.string().optional(),
gas: yup.string().optional(),
gasPrice: yup.string().optional(),
value: yup.string().optional(),
data: yup.string().optional(),
nonce: yup.string().optional(),
})
.defined()

export const EthSendTransactionResponseSchema: yup.ObjectSchema<EthSendTransactionResponse> =
yup
.object({
result: yup.string().defined(),
})
.defined()

routes.register<typeof EthSendTransactionRequestSchema, EthSendTransactionResponse>(
`${ApiNamespace.eth}/sendTransaction`,
EthSendTransactionRequestSchema,
async (request, node): Promise<void> => {
Assert.isInstanceOf(node, FullNode)

const account = node.wallet.listAccounts().find((a) => a.ethAddress === request.data.from)
Assert.isNotUndefined(account, 'Account not found')
AssertSpending(account)

const ethAccount = account.ethAddress
? await node.chain.evm.getAccount(
Address.fromString(EthUtils.prefix0x(account.ethAddress)),
)
: undefined

const nonce = request.data.nonce
? BigInt(request.data.nonce)
: ethAccount?.nonce ?? BigInt(0)

const gas = request.data.gas ? BigInt(request.data.gas) : 1000000n
const gasPrice = request.data.gasPrice ? BigInt(request.data.gasPrice) : 0n
const value = request.data.value ? BigInt(request.data.value) : undefined

const ethTransaction = new LegacyTransaction({
nonce: nonce,
to: request.data.to,
gasLimit: gas,
gasPrice: gasPrice,
value: value,
data: request.data.data,
})

const signed = ethTransaction.sign(Buffer.from(account.spendingKey, 'hex'))

const evmDescription = legacyTransactionToEvmDescription(signed)
const result = await node.chain.evm.simulateTx({ tx: signed })
const events = result.events
Assert.isNotUndefined(events)

const raw = await node.wallet.createEvmTransaction({
evm: evmDescription,
evmEvents: events,
account: account,
})

// TODO: This is pretty hacky to figure out which key to post with
const unshields = events.filter((e) => e.name === 'unshield')
const spendingKey =
unshields.length > 0 ? account.spendingKey : GLOBAL_IF_ACCOUNT.spendingKey

const { transaction } = await node.wallet.post({
transaction: raw,
spendingKey: spendingKey,
})

request.end({
result: Buffer.from(transaction.hash()).toString('hex'),
})
},
)

0 comments on commit aa24089

Please sign in to comment.