Skip to content

Commit e347153

Browse files
authored
evm create transaction rpc (#5242)
1 parent dc473a7 commit e347153

File tree

5 files changed

+235
-1
lines changed

5 files changed

+235
-1
lines changed

ironfish/src/rpc/routes/wallet/__fixtures__/createTransaction.test.ts.fixture

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,5 +1060,113 @@
10601060
}
10611061
]
10621062
}
1063+
],
1064+
"Route wallet/createTransaction should generate a valid transaction with EVM description": [
1065+
{
1066+
"value": {
1067+
"version": 4,
1068+
"id": "ecfdb63b-d264-4fb9-9d05-6a405f77d3ae",
1069+
"name": "existingAccount",
1070+
"spendingKey": "0f7f735d6429d77890ccd030456fa06b000eb99ea280ed7be26ccbe91cb5a701",
1071+
"viewKey": "1cbdc5d4351d73a3a18c05ed77f6dd43635b7ba5b0aad5d9e4303cc8aac6d2d552d217e9f9090a175a864e438a743cd2f134cc192d11502d0a67728ecc0755c4",
1072+
"incomingViewKey": "b130e2f19e0ba894d09ed31f62d1ce20741440a611453fe63e30c67dbfe1b605",
1073+
"outgoingViewKey": "ac850bc3411e50bb664411029eb3baabfffd5898bb30c30d11996bb454ab6330",
1074+
"publicAddress": "0b74a0b2f65349a44497d6cd155c656ec621bf5ab0aef6fc99d1f08cf1109deb",
1075+
"createdAt": {
1076+
"hash": {
1077+
"type": "Buffer",
1078+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
1079+
},
1080+
"sequence": 1
1081+
},
1082+
"scanningEnabled": true,
1083+
"proofAuthorizingKey": "a11c99d533a1dc27355135fbdede47746612e62d6c8000b0b97f11bb94ea380b"
1084+
},
1085+
"head": {
1086+
"hash": {
1087+
"type": "Buffer",
1088+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
1089+
},
1090+
"sequence": 1
1091+
}
1092+
},
1093+
{
1094+
"header": {
1095+
"sequence": 2,
1096+
"previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86",
1097+
"noteCommitment": {
1098+
"type": "Buffer",
1099+
"data": "base64:RxOyZDvVy6f0auHJrHAag49ziyNHsIuwpMCFab6/YDs="
1100+
},
1101+
"transactionCommitment": {
1102+
"type": "Buffer",
1103+
"data": "base64:AtEe3j8aJqnKNpg8CGUKjIOibOHM+DCB9AX/9Z6CNBA="
1104+
},
1105+
"target": "9282972777491357380673661573939192202192629606981189395159182914949423",
1106+
"randomness": "0",
1107+
"timestamp": 1723568831818,
1108+
"graffiti": "0000000000000000000000000000000000000000000000000000000000000000",
1109+
"noteSize": 4,
1110+
"work": "0"
1111+
},
1112+
"transactions": [
1113+
{
1114+
"type": "Buffer",
1115+
"data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAP8V6q1BQrywYZIbwdFc3voqd2ND0BdAG8G68Rgdlvqyrlbe4ZOujI7Pzi6nv32POD4xnuF2iYtEzzUCvhgFfp9ZHFXcs12M0RWvVrSWvKqaC2cyFQ1j2ShIR52YN1l2OIlJXKvM52GJhIQd/m55B5zuSawSfvm++WTiEzI9ImXUFfP5F+hVzzyzGZTT2njxBp0xtBLuZTla3I6yzP4jVhds11yWVJWrYiBDq4WZvuFa1+zFqjmj8BUU06fRSLbNNmZFGhZ/ux3SynrrCZcm03rm9IyJxQCG7lzOG8+3XQvKgbnqohI4LV/iHDdk2+v3inTpjKWLmjtATa8m7wekmB3BLAPV7L/HDcCboNYkH7m2rKHiOEfFNvTLZIdhXCJVnlcf/YMpUQZlZTzspLZ6ldvHUIg7jKhPTNCterQ11h8Yb3WXrwFAXFmV6eXFiq0LriWCoXcMtlnM47feZKyEDsznWFaBYadeAXVRVWka4C3AmM3ednKkfROTtarXUWj9KS8mD/HIb4SCHmm5JlBXJG+IsjegW7a8qlFKlhQC1+KiMhHgsPxgG6nLjdLr5/QLWWTrhFqO4qKG1PkDhpYmPJ8ra1FLPgBNTW6W46tNfdRvc2h8eYc1qYklyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw7F67Sp1eTOBBGpglBgiRka1arXBgfWvgBAMFsoMyayjgbYrSvWQQVfI6G13ol+sBCyvdOhYuC8iU/XIrC6y6BQ=="
1116+
}
1117+
]
1118+
},
1119+
{
1120+
"header": {
1121+
"sequence": 3,
1122+
"previousBlockHash": "F5A70BD9C5474C6380415DDACD90C030DCFE011F40B9F08B326B7920908969E1",
1123+
"noteCommitment": {
1124+
"type": "Buffer",
1125+
"data": "base64:Mr85jsWkZ9ofJ40H3tdCqO7x3kAcJkwQw13C9YmYdEY="
1126+
},
1127+
"transactionCommitment": {
1128+
"type": "Buffer",
1129+
"data": "base64:N/BasxMEEzyBeEm1AMr0a1Nce8HcOjJA7NjjRmBvqtw="
1130+
},
1131+
"target": "9255858786337818395603165512831024101510453493377417362192396248796027",
1132+
"randomness": "0",
1133+
"timestamp": 1723568832132,
1134+
"graffiti": "0000000000000000000000000000000000000000000000000000000000000000",
1135+
"noteSize": 5,
1136+
"work": "0"
1137+
},
1138+
"transactions": [
1139+
{
1140+
"type": "Buffer",
1141+
"data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAAx9A7sg8ysWo7L+q8KoUP25gFg30tMlGw7fpIYNEuVmCttpfFGsqe/aUxijXNZWKvlPFnO3xvGEdOLrVjJcRdJ/EYsXAEhpbGpuTzqDcUqGk37MCk7Hv0YjJ4Cpx2V9sYRG4KG2HFqzwps0IanDf0Gnfg2FGebgmQf1Qvm8lkaYDIk8S211CRxrKgbbgky0PGqpQiSvWF2gy8iJIeE/R8utYgjJimVVSvngbo9LeqgyMMB876xvWNsrffM3rt+YyjZXQQSCKlouofJdionftmhgyxsdZC+owuyfBJ5a7n90lZi3YzGP7NdEFbK4Jbu9AimbbwmIjNOqbb2of4p8OJ8NCbWupU3tifULfS7hhZJAlCb8JYevJIHm59bpdhjcEfOsrDLlg65SmEGuqvOnjjfYPD3hZQO0qzcCAqgLx+jlzSgWKJJbPSzzf1EyLrUEpPJIOIruj4CiqEWNxdm2tQ1htKxHNIb4a9wu5EEaKk/4vLrcw7+M6ElobZjla5hwbxvXRQTWw3CySz+eqYwhbc26uDknskyOVh4YECJk4fyYJd/Wso/bTFVnDdEYHeqolEcg/PXASWzKi8eyQl6mdm7brGQFWE3YK15K1jG1VMhh+MTZDEJhrfklyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwwWVMbhdsmW4O06MUYf2qTQ1EEngj8hmN/BpJt1UrbS2nYo1BjQknQE3drqVC7CwSJ2HUY/vyZRJHVcGGk2yMAA=="
1142+
}
1143+
]
1144+
},
1145+
{
1146+
"header": {
1147+
"sequence": 4,
1148+
"previousBlockHash": "1988B096DB3C5A6E5878D378AE16B11329D242EE0CCC5BD6E1A52C86D11563C4",
1149+
"noteCommitment": {
1150+
"type": "Buffer",
1151+
"data": "base64:XqDZQY5DcEVXgtxwSUf0n/qcyOS3nABMlst7BHQaJxA="
1152+
},
1153+
"transactionCommitment": {
1154+
"type": "Buffer",
1155+
"data": "base64:lsoV7EJOhy0V9Rc2irvFU5iVsvmUc5Pvmztc+ttwhNA="
1156+
},
1157+
"target": "9228823284279306817296266184515742822248210830185427859262273659833347",
1158+
"randomness": "0",
1159+
"timestamp": 1723568832443,
1160+
"graffiti": "0000000000000000000000000000000000000000000000000000000000000000",
1161+
"noteSize": 6,
1162+
"work": "0"
1163+
},
1164+
"transactions": [
1165+
{
1166+
"type": "Buffer",
1167+
"data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAu31EuMR2dhMNlXAmMWYdCjUb1AjUckEHENgDiOsvmeOLtqmPc1h4sOwXcXdBQGjDZcLcUAIodd0FBkMfSlQAax5xRniRarGWe5orgE6QMvCr8HZFoRkZJN/FcfW3uq7q+AH65kZJzUcpSCmUrfgKi90ThXCbZfMZGbdNh3ufLLYO/Z8d2TnKalbs/4qcvJLvp7hgRUswoTnYJbMcFVoYir/ZdIOeTJA6eeqKjsbmf4yyTFMW4+tdk0Ta6/mYAllgxxHU41KxiFJMKwXn+bJq5cctZlld9CM5guHq9GnHPeeym2OfZAtFsExcVLuq5fzZJf8bpcXnufYuaq4SGfsR5AVkSD+u5h6s/OckC5V+9FJdxKe/1e/Fw9jv0OZq5zY9RRCanKXZziVp1JIT4lF8cF5qttqPFM34ETgiBy6AFPAg1OfDCmdb0VcMgMBaRxNkfCW6L8w/YSDAWlrUunxOlvUeZxfobp5bWMeRIKuLHjzH4oVdZaNMWT1lbIm4uLyVg3KrPw4P0E9sbgKrjgFA+vdMxLkjYj6IRQK8roVuWU8hd8Cbupsh0XEi2xQ4b48j5431vnu2hkz/hYpK0xdjZzvFmCS+9vEE9HXGJPm3oVxuZ3FD7EaqMElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwHDLNZab/s/9JQKP/lUH4Q5ffyjaKtmkGNzDn3BtXKnLGU4IPo8NAnY080XleZPx9qjxIdf0VH8N+tVaVh2CsBQ=="
1168+
}
1169+
]
1170+
}
10631171
]
10641172
}

ironfish/src/rpc/routes/wallet/createTransaction.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
44

55
import { Asset, MEMO_LENGTH } from '@ironfish/rust-nodejs'
6+
import { Assert } from '../../../assert'
7+
import { Consensus } from '../../../consensus'
8+
import { TransactionVersion } from '../../../primitives'
69
import { RawTransactionSerde } from '../../../primitives/rawTransaction'
710
import { useAccountFixture, useMinerBlockFixture } from '../../../testUtilities'
811
import { createRouteTest } from '../../../testUtilities/routeTest'
@@ -168,6 +171,64 @@ describe('Route wallet/createTransaction', () => {
168171
expect(rawTransaction.fee).toBeGreaterThan(0n)
169172
})
170173

174+
it('should generate a valid transaction with EVM description', async () => {
175+
const sender = await useAccountFixture(routeTest.node.wallet, 'existingAccount')
176+
177+
for (let i = 0; i < 3; ++i) {
178+
const block = await useMinerBlockFixture(
179+
routeTest.chain,
180+
undefined,
181+
sender,
182+
routeTest.node.wallet,
183+
)
184+
185+
await expect(routeTest.node.chain).toAddBlock(block)
186+
187+
await routeTest.node.wallet.scan()
188+
}
189+
190+
const mockGetActiveTransactionVersion = jest
191+
.spyOn(Consensus.prototype, 'getActiveTransactionVersion')
192+
.mockImplementation(() => TransactionVersion.V3)
193+
194+
const response = await routeTest.client.wallet.createTransaction({
195+
account: 'existingAccount',
196+
outputs: [],
197+
evm: {
198+
nonce: '1',
199+
gasPrice: '20000000000',
200+
gasLimit: '21000',
201+
to: 'b794f5ea0ba39494ce839613fffba74279579268',
202+
value: '1000000000000000000',
203+
data: '0x',
204+
privateIron: '0',
205+
publicIron: '0',
206+
},
207+
fee: undefined,
208+
feeRate: '200',
209+
})
210+
211+
expect(response.status).toBe(200)
212+
expect(response.content.transaction).toBeDefined()
213+
214+
const rawTransactionBytes = Buffer.from(response.content.transaction, 'hex')
215+
const rawTransaction = RawTransactionSerde.deserialize(rawTransactionBytes)
216+
217+
expect(rawTransaction.evm).toBeDefined()
218+
Assert.isNotNull(rawTransaction.evm)
219+
expect(rawTransaction.evm.nonce).toBe(BigInt(1))
220+
expect(rawTransaction.evm.gasPrice).toBe(BigInt(20000000000))
221+
expect(rawTransaction.evm.gasLimit).toBe(BigInt(21000))
222+
expect(rawTransaction.evm.to.toString('hex')).toBe(
223+
'b794f5ea0ba39494ce839613fffba74279579268',
224+
)
225+
expect(rawTransaction.evm.value).toBe(BigInt(1000000000000000000))
226+
expect(rawTransaction.evm.data.toString('hex')).toBe('')
227+
expect(rawTransaction.evm.privateIron).toBe(BigInt(0))
228+
expect(rawTransaction.evm.publicIron).toBe(BigInt(0))
229+
mockGetActiveTransactionVersion.mockRestore()
230+
})
231+
171232
it('should create transaction if fee and fee rate are empty', async () => {
172233
const sender = await useAccountFixture(routeTest.node.wallet, 'existingAccount')
173234

ironfish/src/rpc/routes/wallet/createTransaction.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import * as yup from 'yup'
1111
import { Assert } from '../../../assert'
1212
import { RawTransactionSerde } from '../../../primitives/rawTransaction'
13-
import { CurrencyUtils, YupUtils } from '../../../utils'
13+
import { CurrencyUtils, EthUtils, YupUtils } from '../../../utils'
1414
import { Wallet } from '../../../wallet'
1515
import { NotEnoughFundsError } from '../../../wallet/errors'
1616
import { RPC_ERROR_CODES, RpcValidationError } from '../../adapters/errors'
@@ -39,6 +39,19 @@ export type CreateTransactionRequest = {
3939
assetId: string
4040
value: string
4141
}[]
42+
evm?: {
43+
nonce: string
44+
gasPrice: string
45+
gasLimit: string
46+
to: string
47+
value: string
48+
data: string
49+
privateIron: string
50+
publicIron: string
51+
v?: string
52+
r?: string
53+
s?: string
54+
}
4255
fee?: string | null
4356
feeRate?: string | null
4457
expiration?: number
@@ -93,6 +106,22 @@ export const CreateTransactionRequestSchema: yup.ObjectSchema<CreateTransactionR
93106
.defined(),
94107
)
95108
.optional(),
109+
evm: yup
110+
.object({
111+
nonce: yup.string().defined(),
112+
gasPrice: yup.string().defined(),
113+
gasLimit: yup.string().defined(),
114+
to: yup.string().defined(),
115+
value: yup.string().defined(),
116+
data: yup.string().defined(),
117+
publicIron: yup.string().defined(),
118+
privateIron: yup.string().defined(),
119+
v: yup.string(),
120+
r: yup.string(),
121+
s: yup.string(),
122+
})
123+
.optional()
124+
.default(undefined),
96125
fee: YupUtils.currency({ min: 1n }).nullable().optional(),
97126
feeRate: YupUtils.currency({ min: 1n }).nullable().optional(),
98127
expiration: yup.number().optional(),
@@ -218,6 +247,28 @@ routes.register<typeof CreateTransactionRequestSchema, CreateTransactionResponse
218247
}
219248
}
220249

250+
if (request.data.evm) {
251+
// if 0x is present, remove it
252+
const evm = request.data.evm
253+
const to = EthUtils.remove0x(evm.to)
254+
const data = EthUtils.remove0x(evm.data)
255+
const r = evm.r ? EthUtils.remove0x(evm.r) : undefined
256+
const s = evm.s ? EthUtils.remove0x(evm.s) : undefined
257+
params.evm = {
258+
nonce: BigInt(evm.nonce),
259+
gasPrice: BigInt(evm.gasPrice),
260+
gasLimit: BigInt(evm.gasLimit),
261+
to: Buffer.from(to, 'hex'),
262+
value: BigInt(evm.value),
263+
data: Buffer.from(data, 'hex'),
264+
privateIron: BigInt(evm.privateIron),
265+
publicIron: BigInt(evm.publicIron),
266+
v: evm.v ? BigInt(evm.v) : undefined,
267+
r: r ? Buffer.from(r, 'hex') : undefined,
268+
s: s ? Buffer.from(s, 'hex') : undefined,
269+
}
270+
}
271+
221272
try {
222273
const transaction = await node.wallet.createTransaction(params)
223274
const serialized = RawTransactionSerde.serialize(transaction)

ironfish/src/utils/eth.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
5+
function prefix0x(data: string): string {
6+
return data.startsWith('0x') ? data : `0x${data}`
7+
}
8+
9+
function remove0x(data: string): string {
10+
return data.startsWith('0x') ? data.slice(2) : data
11+
}
12+
13+
export const EthUtils = { prefix0x, remove0x }

ironfish/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export * from './compactblock'
1414
export * from './currency'
1515
export * from './enums'
1616
export * from './error'
17+
export * from './eth'
1718
export * from './file'
1819
export * from './decimalUtils'
1920
export * from './graffiti'

0 commit comments

Comments
 (0)