Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Commit

Permalink
Merge pull request #963 from EOSIO/read_only_queries
Browse files Browse the repository at this point in the history
Support for Read Only Transactions
  • Loading branch information
Brad Hart authored Jul 1, 2021
2 parents 009def8 + c498592 commit d0ab284
Show file tree
Hide file tree
Showing 15 changed files with 396 additions and 16 deletions.
1 change: 0 additions & 1 deletion .github/eosjs-ci/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ RUN cd cfhello \
&& mkdir build \
&& eosio-cpp -abigen ./cfhello.cpp -o ./build/cfhello.wasm


FROM eosio/eosio:${EOSBRANCH}
ENTRYPOINT ["nodeos", "--data-dir", "/root/.local/share", "-e", "-p", "eosio", "--replay-blockchain", "--plugin", "eosio::producer_plugin", "--plugin", "eosio::producer_api_plugin", "--plugin", "eosio::chain_api_plugin", "--plugin", "eosio::trace_api_plugin", "--trace-no-abis", "--plugin", "eosio::db_size_api_plugin", "--plugin", "eosio::http_plugin", "--http-server-address=0.0.0.0:8888", "--access-control-allow-origin=*", "--contracts-console", "--http-validate-host=false", "--enable-account-queries=true", "--verbose-http-errors", "--max-transaction-time=100"]
WORKDIR /root
Expand Down
5 changes: 5 additions & 0 deletions docs/how-to-guides/01_how-to-submit-a-transaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,8 @@ By providing that function inside `tx.associateContextFree()`, the transaction o

#### Return Values
From nodeos version 2.1, the ability to receive return values from smart contracts to eosjs has been introduced. In the above examples, the `transaction` object will include the values `transaction_id` and the `processed` object. If your smart contract returns values, you will be able to find the values within the `transaction.processed.action_traces` array. The order of the `action_traces` array matches the order of actions in your transaction and within those `action_trace` objects, you can find your deserialized return value for your action in the `return_value` field.

### Read-Only Transactions
From nodeos version 2.2, read-only queries have been introduced to eosjs. Adding `readOnlyTrx` to the `transact` config will send the transaction through the `push_ro_transaction` endpoint in the `chain_api`. The `push_ro_transaction` endpoint does not allow the transaction to make any data changes despite the actions in the transaction. The `push_ro_transaction` endpoint may also be used to call normal actions, but any data changes that action will make will be rolled back.

Adding returnFailureTraces to the transact config enables the return of a trace message if your transaction fails. At this time, this is only available for the `push_ro_transaction` endpoint.
72 changes: 72 additions & 0 deletions docs/troubleshooting/02_rpcerror.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
When a call to the chain_api is performed and fails, it will result in an RPCError object being generated which contains information on why the transaction failed.

The RPCError object will contain a concise error message, for instance 'Invalid transaction'. However additional details can be found in the `details` field and the `json` field. The `json` field holds the complete json response from nodeos. The `details` field specifically holds the error object in the `json` field. The data content of the `json` and `details` vary depending on the endpoint is used to call nodeos. Use the `details` field to quickly find error information.

In the `details` and `json` examples below, you can see that the error message may not contain enough information to discern what caused the action to fail. The error message contains `eosio_assert_message` assertion failure. Looking further at the details you can see an `overdrawn balance` message.
```javascript
RpcError: eosio_assert_message assertion failure
at new RpcError (eosjs-rpcerror.ts:20:13)
at JsonRpc.<anonymous> (eosjs-jsonrpc.ts:90:23)
at step (eosjs-jsonrpc.js:37:23)
at Object.next (eosjs-jsonrpc.js:18:53)
at fulfilled (eosjs-jsonrpc.js:9:58)
at processTicksAndRejections (node:internal/process/task_queues:94:5) {
details: {
code: 3050003,
name: 'eosio_assert_message_exception',
message: 'eosio_assert_message assertion failure',
stack: [
{
context: {
level: 'error',
file: 'cf_system.cpp',
line: 14,
method: 'eosio_assert',
hostname: '',
thread_name: 'nodeos',
timestamp: '2021-06-16T05:26:03.665'
},
format: 'assertion failure with message: ${s}',
data: { s: 'overdrawn balance' }
},
{
context: {
level: 'warn',
file: 'apply_context.cpp',
line: 143,
method: 'exec_one',
hostname: '',
thread_name: 'nodeos',
timestamp: '2021-06-16T05:26:03.665'
},
format: 'pending console output: ${console}',
data: { console: '' }
}
]
},
json: {
head_block_num: 1079,
head_block_id: '00003384ff2dd671472e8290e7ee0fbc00ee1f450ce5c10de0a9c245ab5b5b22',
last_irreversible_block_num: 1070,
last_irreversible_block_id: '00003383946519b67bac1a0f31898826b472d81fd40b7fccb49a2f486bd292d1',
code_hash: '800bb7fedd86155047064bffdaa3c32cca76cda40eb80f5c4a7676c7f57da579',
pending_transactions: [],
result: {
id: '01a0cbb6c0215df53f07ecdcf0fb750a4134938b38a72946a0f6f25cf3f43bcb',
block_num: 1079,
block_time: '2021-06-14T21:13:04.500',
producer_block_id: null,
receipt: null,
elapsed: 189,
net_usage: 137,
scheduled: false,
action_traces: [Array],
account_ram_delta: null,
except: [Object],
error_code: '10000000000000000000',
bill_to_accounts: []
}
},
isFetchError: true
}
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"rimraf": "^3.0.2",
"ts-jest": "^26.5.6",
"ts-loader": "^9.2.3",
"typescript": "^4.3.4",
"typescript": "^4.3.5",
"webpack": "^5.41.1",
"webpack-cli": "^4.7.2"
},
Expand Down
2 changes: 2 additions & 0 deletions src/eosjs-api-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export interface Transaction {
export interface TransactConfig {
broadcast?: boolean;
sign?: boolean;
readOnlyTrx?: boolean;
returnFailureTraces?: boolean;
requiredKeys?: string[];
compression?: boolean;
blocksBehind?: number;
Expand Down
22 changes: 19 additions & 3 deletions src/eosjs-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
GetBlockHeaderStateResult,
GetBlockInfoResult,
GetBlockResult,
ReadOnlyTransactResult,
} from './eosjs-rpc-interfaces';
import * as ser from './eosjs-serialize';

Expand Down Expand Up @@ -274,6 +275,8 @@ export class Api {
* `broadcast`: broadcast this transaction?
* `sign`: sign this transaction?
* `compression`: compress this transaction?
* `readOnlyTrx`: read only transaction?
* `returnFailureTraces`: return failure traces? (only available for read only transactions currently)
*
* If both `blocksBehind` and `expireSeconds` are present,
* then fetch the block which is `blocksBehind` behind head block,
Expand All @@ -287,8 +290,18 @@ export class Api {
*/
public async transact(
transaction: Transaction,
{ broadcast = true, sign = true, requiredKeys, compression, blocksBehind, useLastIrreversible, expireSeconds }:
TransactConfig = {}): Promise<TransactResult|PushTransactionArgs>
{
broadcast = true,
sign = true,
readOnlyTrx,
returnFailureTraces,
requiredKeys,
compression,
blocksBehind,
useLastIrreversible,
expireSeconds
}:
TransactConfig = {}): Promise<TransactResult|ReadOnlyTransactResult|PushTransactionArgs>
{
let info: GetInfoResult;

Expand Down Expand Up @@ -337,6 +350,9 @@ export class Api {
}
if (broadcast) {
let result;
if (readOnlyTrx) {
return this.rpc.push_ro_transaction(pushTransactionArgs, returnFailureTraces) as Promise<ReadOnlyTransactResult>;
}
if (compression) {
return this.pushCompressedSignedTransaction(pushTransactionArgs) as Promise<TransactResult>;
}
Expand Down Expand Up @@ -529,7 +545,7 @@ export class TransactionBuilder {
return this;
}

public async send(config?: TransactConfig): Promise<PushTransactionArgs|TransactResult> {
public async send(config?: TransactConfig): Promise<PushTransactionArgs|ReadOnlyTransactResult|TransactResult> {
const contextFreeDataSet: Uint8Array[] = [];
const contextFreeActions: ser.SerializedAction[] = [];
const actions: ser.SerializedAction[] = this.actions.map((actionBuilder) => actionBuilder.serializedData as ser.SerializedAction);
Expand Down
17 changes: 17 additions & 0 deletions src/eosjs-jsonrpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
GetTableRowsResult,
PushTransactionArgs,
PackedTrx,
ReadOnlyTransactResult,
GetBlockHeaderStateResult,
GetTableByScopeResult,
DBSizeGetResult,
Expand Down Expand Up @@ -85,6 +86,8 @@ export class JsonRpc implements AuthorityProvider, AbiProvider {
json = await response.json();
if (json.processed && json.processed.except) {
throw new RpcError(json);
} else if (json.result && json.result.except) {
throw new RpcError(json);
}
} catch (e) {
e.isFetchError = true;
Expand Down Expand Up @@ -311,6 +314,20 @@ export class JsonRpc implements AuthorityProvider, AbiProvider {
});
}

/** Raw call to `/v1/chain/push_ro_transaction */
public async push_ro_transaction({ signatures, compression = 0, serializedTransaction }: PushTransactionArgs,
returnFailureTraces: boolean = false): Promise<ReadOnlyTransactResult> {
return await this.fetch('/v1/chain/push_ro_transaction', {
transaction: {
signatures,
compression,
packed_context_free_data: arrayToHex(new Uint8Array(0)),
packed_trx: arrayToHex(serializedTransaction),
},
return_failure_traces: returnFailureTraces,
});
}

public async push_transactions(transactions: PushTransactionArgs[]): Promise<TransactResult[]> {
const packedTrxs: PackedTrx[] = transactions.map(({signatures, compression = 0, serializedTransaction, serializedContextFreeData }: PushTransactionArgs) => {
return {
Expand Down
13 changes: 12 additions & 1 deletion src/eosjs-rpc-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* copyright defined in eosjs/LICENSE.txt
*/

import { TransactionReceiptHeader } from './eosjs-api-interfaces';
import { TransactionReceiptHeader, TransactionTrace } from './eosjs-api-interfaces';
import { Authorization } from './eosjs-serialize';

/** Structured format for abis */
Expand Down Expand Up @@ -500,6 +500,17 @@ export interface PushTransactionArgs {
serializedContextFreeData?: Uint8Array;
}

/** Return value of `/v1/chain/push_ro_transaction` */
export interface ReadOnlyTransactResult {
head_block_num: number;
head_block_id: string;
last_irreversible_block_num: number;
last_irreversible_block_id: string;
code_hash: string;
pending_transactions: string[];
result: TransactionTrace;
}

export interface DBSizeIndexCount {
index: string;
row_count: number;
Expand Down
6 changes: 6 additions & 0 deletions src/eosjs-rpcerror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@
export class RpcError extends Error {
/** Detailed error information */
public json: any;
public details: any;

constructor(json: any) {
if (json.error && json.error.details && json.error.details.length && json.error.details[0].message) {
super(json.error.details[0].message);
this.details = json.error.details;
} else if (json.processed && json.processed.except && json.processed.except.message) {
super(json.processed.except.message);
this.details = json.processed.except;
} else if (json.result && json.result.except && json.result.except.message) {
super(json.result.except.message);
this.details = json.result.except;
} else {
super(json.message);
}
Expand Down
32 changes: 32 additions & 0 deletions src/tests/eosjs-jsonrpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,38 @@ describe('JSON RPC', () => {
expect(fetch).toBeCalledWith(endpoint + expPath, expParams);
});

it('calls push_ro_transaction', async () => {
const expPath = '/v1/chain/push_ro_transaction';
const signatures = [
'George Washington',
'John Hancock',
'Abraham Lincoln',
];
const serializedTransaction = new Uint8Array([
0, 16, 32, 128, 255,
]);
const expReturn = { data: '12345' };
const expParams = {
body: JSON.stringify({
transaction: {
signatures,
compression: 0,
packed_context_free_data: '',
packed_trx: '00102080ff'
},
return_failure_traces: false
}),
method: 'POST',
};

fetchMock.once(JSON.stringify(expReturn));

const response = await jsonRpc.push_ro_transaction({ signatures, serializedTransaction });

expect(response).toEqual(expReturn);
expect(fetch).toBeCalledWith(endpoint + expPath, expParams);
});

it('calls send_transaction', async () => {
const expPath = '/v1/chain/send_transaction';
const signatures = [
Expand Down
45 changes: 45 additions & 0 deletions src/tests/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,49 @@ const transactWithReturnValue = async () => {
});
};

const readOnlyQuery = async () => {
return await api.transact({
actions: [{
account: 'readonly',
name: 'get',
authorization: [{
actor: 'readonly',
permission: 'active',
}],
data: {},
}],
}, {
blocksBehind: 3,
expireSeconds: 30,
readOnlyTrx: true,
returnFailureTraces: true,
});
};

const readOnlyFailureTrace = async () => {
return await api.transact({
actions: [{
account: 'eosio.token',
name: 'transfer',
authorization: [{
actor: 'alice',
permission: 'active',
}],
data: {
from: 'alice',
to: 'bob',
quantity: '2000000.0000 SYS',
memo: 'failureTrace',
},
}]
}, {
blocksBehind: 3,
expireSeconds: 30,
readOnlyTrx: true,
returnFailureTraces: true,
});
};

const broadcastResult = async (signaturesAndPackedTransaction) => await api.pushSignedTransaction(signaturesAndPackedTransaction);

const transactShouldFail = async () => await api.transact({
Expand Down Expand Up @@ -210,5 +253,7 @@ module.exports = {
transactWithShorthandTxJsonContextFreeAction,
transactWithShorthandTxJsonContextFreeData,
transactWithReturnValue,
readOnlyQuery,
readOnlyFailureTrace,
rpcShouldFail
};
26 changes: 26 additions & 0 deletions src/tests/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,32 @@ describe('Node JS environment', () => {
expect(transactionResponse.processed.action_traces[0].return_value_data).toEqual(expectedValue);
});

it('confirms the return value of the read-only query', async () => {
const expectedValue = [
{'age': 25, 'gender': 1, 'id': 1, 'name': 'Bob Smith'},
{'age': 42, 'gender': 1, 'id': 3, 'name': 'John Smith'},
{'age': 27, 'gender': 1, 'id': 4, 'name': 'Jack Smith'},
{'age': 20, 'gender': 0, 'id': 2, 'name': 'Alice Smith'},
{'age': 26, 'gender': 0, 'id': 5, 'name': 'Youko Niihara'},
{'age': 18, 'gender': 0, 'id': 6, 'name': 'Rose Lee'},
{'age': 25, 'gender': 0, 'id': 7, 'name': 'Youko Kawakami'},
{'age': 24, 'gender': 0, 'id': 8, 'name': 'Yuu Yamada'}
];
transactionResponse = await tests.readOnlyQuery();
expect(transactionResponse.result.action_traces[0].return_value_data).toEqual(expectedValue);
});

it('returns failure trace for failed transaction', async () => {
let err;
try {
await tests.readOnlyFailureTrace();
} catch (e) {
err = e.details;
}
expect(err.code).toEqual(3050003);
expect(err.stack[0].data.s).toEqual('overdrawn balance');
});

it('throws appropriate error message without configuration object or TAPOS in place', async () => {
try {
failedAsPlanned = true;
Expand Down
Loading

0 comments on commit d0ab284

Please sign in to comment.