-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(jellyfish-api-core, jellyfish-transaction): add evmTx (#2099)
<!-- 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
1 parent
24a4043
commit 595a0e5
Showing
11 changed files
with
374 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
170
packages/jellyfish-api-core/__tests__/category/evm/evmTx.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' })) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
77 changes: 77 additions & 0 deletions
77
packages/jellyfish-transaction/__tests__/script/dftx/dftx_evm/EvmTx.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.