Add send_transaction2 support (#8)

* add SendTransaction2Args, send_transaction2 rpc method, calls send_transaction2 unit test

* add text-encoding dev dependency to run unit tests

* update api.transact

* minor

* update GetInfoResult

* add transactWithRetry unit test

* add useOldRPC and useOldSendRPC options

* minor

* rm retryTrx, make like cleos, use retry block num and retry irreversible, add assertion, pass retry blocks if not irreversible, make num blocks optional param

* update json rpc unit tests with blocking 10 blocks and blocking irreversible

* update node test, add irreversible check, update to 10 blocks

* update README.md, add '?' for optional params

* use 30 blocks versus 180 for irreversible

* use send_transaction2 rpc by default without retry trx feature

* enable failure trace default, make default send_transaction2 rpc

Co-authored-by: NatPDeveloper <you@example.com>
This commit is contained in:
Nat
2022-08-08 10:28:44 -04:00
committed by GitHub
parent 0255c3c81f
commit 251beab2b2
8 changed files with 294 additions and 7 deletions
+9 -1
View File
@@ -85,6 +85,14 @@ const api = new Api({ rpc, signatureProvider, textDecoder: new TextDecoder(), te
`transact()` is used to sign and push transactions onto the blockchain with an optional configuration object parameter. This parameter can override the default value of `broadcast: true`, and can be used to fill TAPOS fields given `blocksBehind` and `expireSeconds`. Given no configuration options, transactions are expected to be unpacked with TAPOS fields (`expiration`, `ref_block_num`, `ref_block_prefix`) and will automatically be broadcast onto the chain.
With the introduction of Mandel v3.1 the retry transaction feature also adds 5 new optional fields to the configuration object:
- `useOldRPC`: use old RPC `push_transaction`, rather than new RPC send_transaction
- `useOldSendRPC`: use old RPC `/v1/chain/send_transaction`, rather than new RPC `/v1/chain/send_transaction2`
- `returnFailureTrace`: return partial traces on failed transactions
- `retryTrxNumBlocks`: request node to retry transaction until in a block of given height, blocking call
- `retryIrreversible`: request node to retry transaction until it is irreversible or expires, blocking call
```js
(async () => {
const result = await api.transact({
@@ -104,7 +112,7 @@ const api = new Api({ rpc, signatureProvider, textDecoder: new TextDecoder(), te
}]
}, {
blocksBehind: 3,
expireSeconds: 30,
expireSeconds: 30
});
console.dir(result);
})();
+1
View File
@@ -52,6 +52,7 @@
"jest-fetch-mock": "^3.0.3",
"none": "^1.0.0",
"rimraf": "^3.0.2",
"text-encoding": "^0.7.0",
"ts-jest": "^26.5.6",
"ts-loader": "^9.2.3",
"typedoc": "^0.23.5",
+100 -5
View File
@@ -5,7 +5,7 @@
import { AbiProvider, AuthorityProvider, BinaryAbi, CachedAbi, SignatureProvider } from './eosjs-api-interfaces';
import { JsonRpc } from './eosjs-jsonrpc';
import { Abi, GetInfoResult, PushTransactionArgs } from './eosjs-rpc-interfaces';
import { Abi, GetInfoResult, PushTransactionArgs, SendTransaction2Args } from './eosjs-rpc-interfaces';
import * as ser from './eosjs-serialize';
const abiAbi = require('../src/abi.abi.json');
@@ -223,13 +223,39 @@ export class Api {
* Named Parameters:
* * `broadcast`: broadcast this transaction?
* * `sign`: sign this transaction?
* * `useOldRPC`: use old RPC push_transaction, rather than new RPC send_transaction?
* * `useOldSendRPC`: use old RPC /v1/chain/send_transaction, rather than new RPC /v1/chain/send_transaction2?
* * `returnFailureTrace`: return partial traces on failed transactions?
* * `retryTrxNumBlocks`: request node to retry transaction until in a block of given height, blocking call?
* * `retryIrreversible`: request node to retry transaction until it is irreversible or expires, blocking call?
* * If both `blocksBehind` and `expireSeconds` are present,
* then fetch the block which is `blocksBehind` behind head block,
* use it as a reference for TAPoS, and expire the transaction `expireSeconds` after that block's time.
* @returns node response if `broadcast`, `{signatures, serializedTransaction}` if `!broadcast`
*/
public async transact(transaction: any, { broadcast = true, sign = true, blocksBehind, expireSeconds }:
{ broadcast?: boolean; sign?: boolean; blocksBehind?: number; expireSeconds?: number; } = {}): Promise<any> {
public async transact(transaction: any, {
broadcast = true,
sign = true,
blocksBehind,
expireSeconds,
useOldRPC = false,
useOldSendRPC = false,
returnFailureTrace = true,
retryTrxNumBlocks = 0,
retryIrreversible = false
}:
{
broadcast?: boolean;
sign?: boolean;
blocksBehind?: number;
expireSeconds?: number;
useOldRPC?: boolean;
useOldSendRPC?: boolean;
returnFailureTrace?: boolean;
retryTrxNumBlocks?: number;
retryIrreversible?: boolean;
} = {}
): Promise<any> {
let info: GetInfoResult;
if (!this.chainId) {
@@ -237,6 +263,20 @@ export class Api {
this.chainId = info.chain_id;
}
const isRetry = retryIrreversible || retryTrxNumBlocks > 0;
if(isRetry) {
if(info.server_version_string < "v3.1.0") {
throw new Error(`Retry transaction feature unavailable for nodeos :${info.server_version_string}`);
}
if (useOldRPC || useOldSendRPC) {
throw new Error('Retry transaction feature not compatible with old RPC');
}
if(retryIrreversible && retryTrxNumBlocks > 0) {
throw new Error('Must specify retry irreversible or until given block height');
}
}
if (typeof blocksBehind === 'number' && expireSeconds) { // use config fields to generate TAPOS if they exist
if (!info) {
info = await this.rpc.get_info();
@@ -273,12 +313,35 @@ export class Api {
});
}
if (broadcast) {
return this.pushSignedTransaction(pushTransactionArgs);
if(isRetry) {
let params: SendTransaction2Args = {
return_failure_trace: returnFailureTrace,
retry_trx: true,
transaction: pushTransactionArgs
}
if(retryTrxNumBlocks > 0) {
params = {
...params,
retry_trx_num_blocks: retryTrxNumBlocks
}
}
return this.sendSignedTransaction2(params);
} else if(useOldSendRPC) {
return this.sendSignedTransaction(pushTransactionArgs);
} else if(useOldRPC) {
return this.pushSignedTransaction(pushTransactionArgs);
} else {
return this.sendSignedTransaction2({
return_failure_trace: returnFailureTrace,
retry_trx: false,
transaction: pushTransactionArgs
});
}
}
return pushTransactionArgs;
}
/** Broadcast a signed transaction */
/** Broadcast a signed transaction using push_transaction RPC */
public async pushSignedTransaction(
{ signatures, serializedTransaction, serializedContextFreeData }: PushTransactionArgs
): Promise<any> {
@@ -289,6 +352,38 @@ export class Api {
});
}
/** Broadcast a signed transaction using send_transaction RPC */
public async sendSignedTransaction(
{ signatures, serializedTransaction, serializedContextFreeData }: PushTransactionArgs
): Promise<any> {
return this.rpc.send_transaction({
signatures,
serializedTransaction,
serializedContextFreeData
});
}
/** Broadcast a signed transaction using send_transaction2 RPC*/
public async sendSignedTransaction2(
{
return_failure_trace,
retry_trx,
retry_trx_num_blocks,
transaction: { signatures, serializedTransaction, serializedContextFreeData }
}: SendTransaction2Args
): Promise<any> {
return this.rpc.send_transaction2({
return_failure_trace,
retry_trx,
retry_trx_num_blocks,
transaction: {
signatures,
serializedTransaction,
serializedContextFreeData
}
});
}
// eventually break out into TransactionValidator class
private hasRequiredTaposFields({ expiration, ref_block_num, ref_block_prefix }: any): boolean {
return !!(expiration && typeof(ref_block_num) === 'number' && typeof(ref_block_prefix) === 'number');
+23 -1
View File
@@ -5,7 +5,7 @@
import { AbiProvider, AuthorityProvider, AuthorityProviderArgs, BinaryAbi } from './eosjs-api-interfaces';
import { base64ToBinary, convertLegacyPublicKeys } from './eosjs-numeric';
import { GetAbiResult, GetBlockResult, GetCodeResult, GetInfoResult, GetRawCodeAndAbiResult, PushTransactionArgs, GetBlockHeaderStateResult } from "./eosjs-rpc-interfaces" // tslint:disable-line
import { GetAbiResult, GetBlockResult, GetCodeResult, GetInfoResult, GetRawCodeAndAbiResult, PushTransactionArgs, GetBlockHeaderStateResult, SendTransaction2Args } from "./eosjs-rpc-interfaces" // tslint:disable-line
import { RpcError } from './eosjs-rpcerror';
function arrayToHex(data: Uint8Array) {
@@ -212,6 +212,28 @@ export class JsonRpc implements AuthorityProvider, AbiProvider {
});
}
/** Send a serialized transaction2 */
public async send_transaction2(
{
return_failure_trace,
retry_trx,
retry_trx_num_blocks,
transaction: { signatures, serializedTransaction, serializedContextFreeData }
}: SendTransaction2Args
): Promise<any> {
return await this.fetch('/v1/chain/send_transaction2', {
return_failure_trace,
retry_trx,
retry_trx_num_blocks,
transaction: {
signatures,
compression: 0,
packed_context_free_data: arrayToHex(serializedContextFreeData || new Uint8Array(0)),
packed_trx: arrayToHex(serializedTransaction),
}
});
}
/** Raw call to `/v1/db_size/get` */
public async db_size_get() { return await this.fetch('/v1/db_size/get', {}); }
+16
View File
@@ -105,6 +105,14 @@ export interface GetInfoResult {
virtual_block_net_limit: number;
block_cpu_limit: number;
block_net_limit: number;
server_version_string: string;
fork_db_head_block_num: number,
fork_db_head_block_id: string,
server_full_version_string: string,
total_cpu_weight: string,
total_net_weight: string,
earliest_available_block_num: number,
last_irreversible_block_time: string
}
/** Return value of `/v1/chain/get_raw_code_and_abi` */
@@ -120,3 +128,11 @@ export interface PushTransactionArgs {
serializedTransaction: Uint8Array;
serializedContextFreeData?: Uint8Array;
}
/** Arguments for `send_transaction2` */
export interface SendTransaction2Args {
return_failure_trace: boolean,
retry_trx: boolean,
retry_trx_num_blocks?: number,
transaction: PushTransactionArgs
}
+86
View File
@@ -606,6 +606,92 @@ describe('JSON RPC', () => {
expect(fetch).toBeCalledWith(endpoint + expPath, expParams);
});
it('calls send_transaction2 irreversible', async () => {
const expPath = '/v1/chain/send_transaction2';
const signatures = [
'George Washington',
'John Hancock',
'Abraham Lincoln',
];
const serializedTransaction = new Uint8Array([
0, 16, 32, 128, 255,
]);
const expReturn = { data: '12345' };
const callParams = {
return_failure_trace: false,
retry_trx: false,
transaction: {
signatures,
serializedTransaction,
}
};
const expParams = {
body: JSON.stringify({
return_failure_trace: false,
retry_trx: false,
transaction: {
signatures,
compression: 0,
packed_context_free_data: '',
packed_trx: '00102080ff',
}
}),
method: 'POST',
};
fetchMock.once(JSON.stringify(expReturn));
const response = await jsonRpc.send_transaction2(callParams);
expect(response).toEqual(expReturn);
expect(fetch).toBeCalledWith(endpoint + expPath, expParams);
});
it('calls send_transaction2 10 blocks', async () => {
const expPath = '/v1/chain/send_transaction2';
const signatures = [
'George Washington',
'John Hancock',
'Abraham Lincoln',
];
const serializedTransaction = new Uint8Array([
0, 16, 32, 128, 255,
]);
const expReturn = { data: '12345' };
const callParams = {
return_failure_trace: false,
retry_trx: false,
retry_trx_num_blocks: 10,
transaction: {
signatures,
serializedTransaction,
}
};
const expParams = {
body: JSON.stringify({
return_failure_trace: false,
retry_trx: false,
retry_trx_num_blocks: 10,
transaction: {
signatures,
compression: 0,
packed_context_free_data: '',
packed_trx: '00102080ff',
}
}),
method: 'POST',
};
fetchMock.once(JSON.stringify(expReturn));
const response = await jsonRpc.send_transaction2(callParams);
expect(response).toEqual(expReturn);
expect(fetch).toBeCalledWith(endpoint + expPath, expParams);
});
it('calls db_size_get', async () => {
const expPath = '/v1/db_size/get';
const expReturn = { data: '12345' };
+47
View File
@@ -85,6 +85,51 @@ const transactWithoutBroadcast = async () => await api.transact({
blocksBehind: 3,
expireSeconds: 30,
});
const transactWithRetry = async () => await api.transact({
actions: [{
account: 'eosio.token',
name: 'transfer',
authorization: [{
actor: 'bob',
permission: 'active',
}],
data: {
from: 'bob',
to: 'alice',
quantity: '0.0001 SYS',
memo: '',
},
}]
}, {
broadcast: false,
blocksBehind: 3,
expireSeconds: 30,
retryTrxNumBlocks: 10
});
const transactWithRetryIrreversible = async () => await api.transact({
actions: [{
account: 'eosio.token',
name: 'transfer',
authorization: [{
actor: 'bob',
permission: 'active',
}],
data: {
from: 'bob',
to: 'alice',
quantity: '0.0001 SYS',
memo: '',
},
}]
}, {
broadcast: false,
blocksBehind: 3,
expireSeconds: 30,
retryIrreversible: true
});
const broadcastResult = async (signaturesAndPackedTransaction) => await api.pushSignedTransaction(signaturesAndPackedTransaction);
@@ -111,6 +156,8 @@ module.exports = {
transactWithConfig,
transactWithoutConfig,
transactWithoutBroadcast,
transactWithRetry,
transactWithRetryIrreversible,
broadcastResult,
transactShouldFail,
rpcShouldFail
+12
View File
@@ -27,6 +27,18 @@ describe('Node JS environment', () => {
expect(Object.keys(transactionResponse)).toContain('transaction_id');
});
it('retry transaction', async () => {
transactionSignatures = await tests.transactWithRetry();
transactionResponse = await tests.broadcastResult(transactionSignatures);
expect(Object.keys(transactionResponse)).toContain('transaction_id');
});
it('retry transaction irreversible', async () => {
transactionSignatures = await tests.transactWithRetryIrreversible();
transactionResponse = await tests.broadcastResult(transactionSignatures);
expect(Object.keys(transactionResponse)).toContain('transaction_id');
});
it('throws appropriate error message without configuration object or TAPOS in place', async () => {
try {
failedAsPlanned = true;