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:
@@ -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);
|
||||
})();
|
||||
|
||||
@@ -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
@@ -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
@@ -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', {}); }
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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' };
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user