diff --git a/ironfish/src/rpc/clients/client.ts b/ironfish/src/rpc/clients/client.ts index 43859d590b..31805df973 100644 --- a/ironfish/src/rpc/clients/client.ts +++ b/ironfish/src/rpc/clients/client.ts @@ -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 { @@ -1023,5 +1027,13 @@ export abstract class RpcClient { params, ).waitForEnd() }, + sendTransaction: ( + params: EthSendTransactionRequest, + ): Promise> => { + return this.request( + `${ApiNamespace.eth}/sendTransaction`, + params, + ).waitForEnd() + }, } } diff --git a/ironfish/src/rpc/routes/eth/__fixtures__/sendTransaction.test.ts.fixture b/ironfish/src/rpc/routes/eth/__fixtures__/sendTransaction.test.ts.fixture new file mode 100644 index 0000000000..f11091b7c4 --- /dev/null +++ b/ironfish/src/rpc/routes/eth/__fixtures__/sendTransaction.test.ts.fixture @@ -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 + } + } + ] +} \ No newline at end of file diff --git a/ironfish/src/rpc/routes/eth/index.ts b/ironfish/src/rpc/routes/eth/index.ts index 23c4a7f582..2f33192442 100644 --- a/ironfish/src/rpc/routes/eth/index.ts +++ b/ironfish/src/rpc/routes/eth/index.ts @@ -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' diff --git a/ironfish/src/rpc/routes/eth/sendTransaction.test.ts b/ironfish/src/rpc/routes/eth/sendTransaction.test.ts new file mode 100644 index 0000000000..4e7cf0424f --- /dev/null +++ b/ironfish/src/rpc/routes/eth/sendTransaction.test.ts @@ -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) + }) +}) diff --git a/ironfish/src/rpc/routes/eth/sendTransaction.ts b/ironfish/src/rpc/routes/eth/sendTransaction.ts new file mode 100644 index 0000000000..44ef38db32 --- /dev/null +++ b/ironfish/src/rpc/routes/eth/sendTransaction.ts @@ -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 = 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 = + yup + .object({ + result: yup.string().defined(), + }) + .defined() + +routes.register( + `${ApiNamespace.eth}/sendTransaction`, + EthSendTransactionRequestSchema, + async (request, node): Promise => { + 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'), + }) + }, +)