Skip to content

Commit

Permalink
feat(jellyfish-api-core, jellyfish-transaction): add evmTx (#2099)
Browse files Browse the repository at this point in the history
<!--  Thanks for sending a pull request! -->

#### What this PR does / why we need it:
Adds support for creation of EVM transaction.

#### Which issue(s) does this PR fixes?:
<!--
(Optional) Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes #

#### Additional comments?:

---------

Co-authored-by: canonbrother <[email protected]>
  • Loading branch information
lykalabrada and canonbrother authored Jun 28, 2023
1 parent 24a4043 commit 595a0e5
Show file tree
Hide file tree
Showing 11 changed files with 374 additions and 0 deletions.
33 changes: 33 additions & 0 deletions docs/node/CATEGORIES/18-evm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
id: evm
title: EVM API
sidebar_label: EVM API
slug: /jellyfish/api/evm
---

```js
import {JsonRpcClient} from '@defichain/jellyfish-api-jsonrpc'
const client = new JsonRpcClient('http://foo:bar@localhost:8554')

// Using client.evm.
const something = await client.evm.method()
```

## EVM

Creates an EVM transaction submitted to local node and network.

```ts title="client.evm.evmTx()"
interface evm {
evmTx (options: EvmTxOptions): Promise<string>
}

interface EvmTxOptions {
from: string
nonce: number
gasPrice: number
gasLimit: number
to: string
value: BigNumber
data?: string
}
170 changes: 170 additions & 0 deletions packages/jellyfish-api-core/__tests__/category/evm/evmTx.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@

import { MasterNodeRegTestContainer } from '@defichain/testcontainers'
import { Testing } from '@defichain/jellyfish-testing'
import { RpcApiError } from '@defichain/jellyfish-api-core/dist/index'
import { ContainerAdapterClient } from '../../container_adapter_client'
import { TransferDomainType } from '../../../src/category/account'
import BigNumber from 'bignumber.js'

describe('EVMTX', () => {
let dfiAddress: string, ethAddress: string, toEthAddress: string
const container = new MasterNodeRegTestContainer()
const client = new ContainerAdapterClient(container)
const testing = Testing.create(container)
const amount = {
ONE: 1,
HUNDRED: 100
}
const txGas = {
gasPrice: 21,
gasLimit: 21000
}

beforeAll(async () => {
await container.start()
await container.waitForWalletCoinbaseMaturity()
await testing.rpc.masternode.setGov({
ATTRIBUTES: { 'v0/params/feature/evm': 'true' }
})
await container.generate(1)
dfiAddress = await container.call('getnewaddress')
await container.call('utxostoaccount', [{ [dfiAddress]: '105@DFI' }])
await container.generate(1)
ethAddress = await container.call('getnewaddress', ['', 'eth'])
toEthAddress = await container.call('getnewaddress', ['', 'eth'])
})

afterAll(async () => {
await container.stop()
})

it('should verify that feature/evm gov attribute is set', async () => {
const attributes = await testing.rpc.masternode.getGov('ATTRIBUTES')
expect(attributes.ATTRIBUTES['v0/params/feature/evm']).toStrictEqual('true')
})

it('should successfully create a new EVM transaction', async () => {
const balanceDFIAddressBefore: Record<string, BigNumber> = await client.call('getaccount', [dfiAddress, {}, true], 'bignumber')
const dvmToEvmTransfer = [
{
src: {
address: dfiAddress,
amount: `${amount.HUNDRED}@DFI`,
domain: TransferDomainType.DVM
},
dst: {
address: ethAddress,
amount: `${amount.HUNDRED}@DFI`,
domain: TransferDomainType.EVM
}
}
]
await container.call('transferdomain', [dvmToEvmTransfer])
await container.generate(1)

const balanceDFIAddressAfter: Record<string, BigNumber> = await client.call('getaccount', [dfiAddress, {}, true], 'bignumber')
expect(balanceDFIAddressAfter['0']).toStrictEqual(balanceDFIAddressBefore['0'].minus(amount.HUNDRED))

const evmTxHash = await client.evm.evmtx({
from: ethAddress,
to: toEthAddress,
value: new BigNumber(amount.ONE),
nonce: 0,
...txGas
})
await container.generate(1)

const blockHash: string = await client.blockchain.getBestBlockHash()
const txs = await client.blockchain.getBlock(blockHash, 1)
expect(txs.tx[1]).toStrictEqual(evmTxHash)
})

it('should successfully create a new EVM transaction with optional data', async () => {
const evmTxHash = await client.evm.evmtx({
from: ethAddress,
to: toEthAddress,
value: new BigNumber(amount.ONE),
data: 'ad33eb89000000000000000000000000a218a0ea9a888e3f6e2dffdf4066885f596f07bf', // random methodId 0xad33eb89, random tokenAddr 000000000000000000000000a218a0ea9a888e3f6e2dffdf4066885f596f07bf
nonce: 1,
...txGas
})
await container.generate(1)

const blockHash: string = await client.blockchain.getBestBlockHash()
const txs = await client.blockchain.getBlock(blockHash, 1)
expect(txs.tx[1]).toStrictEqual(evmTxHash)
})

it('should fail creation of evmtx when data input is not hex', async () => {
await expect(client.evm.evmtx({
from: ethAddress,
to: toEthAddress,
value: new BigNumber(amount.ONE),
nonce: 2,
data: '1234abcnothex',
...txGas
})).rejects.toThrow(new RpcApiError({ code: -8, method: 'evmtx', message: 'Input param expected to be in hex format' }))
})

it('should fail creation of evmtx when amount is not valid', async () => {
await expect(client.evm.evmtx({
from: ethAddress,
to: toEthAddress,
value: new BigNumber(Number.MAX_VALUE),
nonce: 2,
...txGas
})).rejects.toThrow(new RpcApiError({ code: -3, method: 'evmtx', message: 'Invalid amount' }))
})

it('should fail creation of evmtx when from address is not a valid ethereum address', async () => {
await expect(client.evm.evmtx({
from: dfiAddress,
to: ethAddress,
value: new BigNumber(amount.ONE),
nonce: 2,
...txGas
})).rejects.toThrow(new RpcApiError({ code: -8, method: 'evmtx', message: 'from address not an Ethereum address' }))
})

it('should fail creation of evmtx when to address is not a valid ethereum address', async () => {
await expect(client.evm.evmtx({
from: ethAddress,
to: dfiAddress,
value: new BigNumber(amount.ONE),
nonce: 2,
...txGas
})).rejects.toThrow(new RpcApiError({ code: -8, method: 'evmtx', message: 'to address not an Ethereum address' }))
})

it('should fail creation of evmtx when nonce is not valid (already used)', async () => {
await expect(client.evm.evmtx({
from: ethAddress,
to: toEthAddress,
value: new BigNumber(amount.ONE),
nonce: 0,
...txGas
})).rejects.toThrow(RpcApiError)
})

it('should fail creation of evmtx when gas price is not valid', async () => {
await expect(client.evm.evmtx({
from: ethAddress,
to: toEthAddress,
value: new BigNumber(amount.ONE),
nonce: 2,
gasPrice: Number.MAX_VALUE,
gasLimit: txGas.gasLimit
})).rejects.toThrow(new RpcApiError({ code: -1, method: 'evmtx', message: 'JSON integer out of range' }))
})

it('should fail creation of evmtx when gas limit is not valid', async () => {
await expect(client.evm.evmtx({
from: ethAddress,
to: toEthAddress,
value: new BigNumber(amount.ONE),
nonce: 2,
gasPrice: txGas.gasPrice,
gasLimit: Number.MAX_VALUE
})).rejects.toThrow(new RpcApiError({ code: -1, method: 'evmtx', message: 'JSON integer out of range' }))
})
})
37 changes: 37 additions & 0 deletions packages/jellyfish-api-core/src/category/evm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ApiClient, BigNumber } from '../.'

/**
* EVM RPCs for DeFi Blockchain
*/
export class Evm {
private readonly client: ApiClient

constructor (client: ApiClient) {
this.client = client
}

/**
* Creates an EVM transaction submitted to local node and network
* @param {string} from
* @param {number} nonce
* @param {number} gasPrice
* @param {number} gasLimit
* @param {string} to
* @param {BigNumber} value
* @param {string} [data]
* @returns {Promise<string>}
*/
async evmtx ({ from, nonce, gasPrice, gasLimit, to, value, data }: EvmTxOptions): Promise<string> {
return await this.client.call('evmtx', [from, nonce, gasPrice, gasLimit, to, value, data], 'bignumber')
}
}

export interface EvmTxOptions {
from: string
nonce: number
gasPrice: number
gasLimit: number
to: string
value: BigNumber
data?: string
}
3 changes: 3 additions & 0 deletions packages/jellyfish-api-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Spv } from './category/spv'
import { Misc } from './category/misc'
import { Loan } from './category/loan'
import { Vault } from './category/vault'
import { Evm } from './category/evm'

export * from '@defichain/jellyfish-json'

Expand All @@ -35,6 +36,7 @@ export * as spv from './category/spv'
export * as icxorderbook from './category/icxorderbook'
export * as misc from './category/misc'
export * as loan from './category/loan'
export * as evm from './category/evm'

/**
* A protocol agnostic DeFiChain node client, RPC calls are separated into their category.
Expand All @@ -57,6 +59,7 @@ export abstract class ApiClient {
public readonly misc = new Misc(this)
public readonly loan = new Loan(this)
public readonly vault = new Vault(this)
public readonly evm = new Evm(this)

/**
* A promise based procedure call handling
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { SmartBuffer } from 'smart-buffer'
import { CEvmTx, EvmTx, OP_DEFI_TX } from '../../../../src/script/dftx'
import { toBuffer, toOPCodes } from '../../../../src/script/_buffer'
import { OP_CODES } from '../../../../src'

it('should bi-directional buffer-object-buffer', () => {
const fixtures = [
/**
6a - OP_RETURN
4c - OP_PUSHDATA1
74 - length
44665478 - dftx
39 - txtype
6e - length
f86c808504e3b292008252089494c2acef73afe7409220af7aab48f0add9e4e7ee880de0b6b3a76400008026a0be7b6f57bf2c838a48fa6e666945994b3b6f386c92f1523667c8c50f753e63c0a018226da7216224a0217bbe186a456eb943ca8bcbef3d7421d544d6fcb9ab2479 -- signed evm tx
*/
'6a4c7444665478396ef86c808504e3b292008252089494c2acef73afe7409220af7aab48f0add9e4e7ee880de0b6b3a76400008026a0be7b6f57bf2c838a48fa6e666945994b3b6f386c92f1523667c8c50f753e63c0a018226da7216224a0217bbe186a456eb943ca8bcbef3d7421d544d6fcb9ab2479',
'6a4c7444665478396ef86c018504e3b2920082520894585bd64cf6574abf77f216efc894940e87cad5b1880de0b6b3a76400008026a035e16fa88aa4a1d01990c3b6acb0e6d869fe4d5960992490814f65e063ba3ed7a0401a72637c21501313e1a5c8b03cd1031ea7823085c6a8f6d388caf078027d7b'
]

fixtures.forEach(hex => {
const stack = toOPCodes(
SmartBuffer.fromBuffer(Buffer.from(hex, 'hex'))
)
const buffer = toBuffer(stack)
expect(buffer.toString('hex')).toStrictEqual(hex)
expect((stack[1] as OP_DEFI_TX).tx.type).toStrictEqual(0x39)
})
})

const evmTxData: Array<{ header: string, data: string, evmTx: EvmTx }> = [
{
// data with context
header: '6a4c744466547839', // OP_RETURN(6a) OP_PUSHDATA1(4c) (length 74) CDfTx.SIGNATURE(44665478) CEvmTx.OP_CODE(39)
data: '6ef86c808504e3b292008252089494c2acef73afe7409220af7aab48f0add9e4e7ee880de0b6b3a76400008026a0be7b6f57bf2c838a48fa6e666945994b3b6f386c92f1523667c8c50f753e63c0a018226da7216224a0217bbe186a456eb943ca8bcbef3d7421d544d6fcb9ab2479',
evmTx: {
raw: 'f86c808504e3b292008252089494c2acef73afe7409220af7aab48f0add9e4e7ee880de0b6b3a76400008026a0be7b6f57bf2c838a48fa6e666945994b3b6f386c92f1523667c8c50f753e63c0a018226da7216224a0217bbe186a456eb943ca8bcbef3d7421d544d6fcb9ab2479'
}
},
{
header: '6a4c744466547839', // OP_RETURN(6a) OP_PUSHDATA1(4c) (length 74) CDfTx.SIGNATURE(44665478) CEvmTx.OP_CODE(39)
data: '6ef86c018504e3b2920082520894585bd64cf6574abf77f216efc894940e87cad5b1880de0b6b3a76400008026a035e16fa88aa4a1d01990c3b6acb0e6d869fe4d5960992490814f65e063ba3ed7a0401a72637c21501313e1a5c8b03cd1031ea7823085c6a8f6d388caf078027d7b',
evmTx: {
raw: 'f86c018504e3b2920082520894585bd64cf6574abf77f216efc894940e87cad5b1880de0b6b3a76400008026a035e16fa88aa4a1d01990c3b6acb0e6d869fe4d5960992490814f65e063ba3ed7a0401a72637c21501313e1a5c8b03cd1031ea7823085c6a8f6d388caf078027d7b'
}
}
]

describe.each(evmTxData)('should craft and compose dftx',
({ header, data, evmTx }: { header: string, data: string, evmTx: EvmTx }) => {
it('should craft dftx with OP_CODES._() for evm tx', () => {
const stack = [
OP_CODES.OP_RETURN,
OP_CODES.OP_DEFI_TX_EVM_TX(evmTx)
]

const buffer = toBuffer(stack)
expect(buffer.toString('hex')).toStrictEqual(header + data)
})

describe('Composable', () => {
it('should compose from buffer to composable', () => {
const buffer = SmartBuffer.fromBuffer(Buffer.from(data, 'hex'))
const composable = new CEvmTx(buffer)

expect(composable.toObject()).toStrictEqual(evmTx)
})

it('should compose from composable to buffer', () => {
const composable = new CEvmTx(evmTx)
const buffer = new SmartBuffer()
composable.toBuffer(buffer)

expect(buffer.toBuffer().toString('hex')).toStrictEqual(data)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,10 @@ describe('All mapped OP_CODES are setup properly: (static, hex, num, asm)', () =
expectOPCode(script.OP_CODES.OP_SHA1, script.OP_SHA1, 'OP_SHA1', 0xa7, 'a7')
})

it('OP_SHA3', () => {
expectOPCode(script.OP_CODES.OP_SHA3, script.OP_SHA3, 'OP_SHA3', 0xc0, 'c0')
})

it('OP_SHA256', () => {
expectOPCode(script.OP_CODES.OP_SHA256, script.OP_SHA256, 'OP_SHA256', 0xa8, 'a8')
})
Expand Down
9 changes: 9 additions & 0 deletions packages/jellyfish-transaction/src/script/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ export class OP_SHA1 extends StaticCode {
}
}

/**
* The input is hashed using SHA-3.
*/
export class OP_SHA3 extends StaticCode {
constructor () {
super(0xc0, 'OP_SHA3')
}
}

/**
* The input is hashed using SHA-256.
*/
Expand Down
3 changes: 3 additions & 0 deletions packages/jellyfish-transaction/src/script/dftx/dftx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ import {
CPlaceAuctionBid,
PlaceAuctionBid
} from './dftx_vault'
import { CEvmTx, EvmTx } from './dftx_evmtx'

/**
* DeFi Transaction
Expand Down Expand Up @@ -298,6 +299,8 @@ export class CDfTx extends ComposableBuffer<DfTx<any>> {
return compose<PaybackLoanV2>(CPaybackLoanV2.OP_NAME, d => new CPaybackLoanV2(d))
case CPlaceAuctionBid.OP_CODE:
return compose<PlaceAuctionBid>(CPlaceAuctionBid.OP_NAME, d => new CPlaceAuctionBid(d))
case CEvmTx.OP_CODE:
return compose<EvmTx>(CEvmTx.OP_NAME, d => new CEvmTx(d))
default:
return compose<DeFiOpUnmapped>(CDeFiOpUnmapped.OP_NAME, d => new CDeFiOpUnmapped(d))
}
Expand Down
Loading

0 comments on commit 595a0e5

Please sign in to comment.