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

Support for Read Only Transactions #963

Merged
merged 14 commits into from
Jul 1, 2021
8 changes: 7 additions & 1 deletion .github/eosjs-ci/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ RUN git clone https://github.com/EOSIO/eos \
&& cd eos \
&& git checkout develop

RUN git clone https://github.com/EOSIO/eosio.cdt \
&& cd eosio.cdt \
&& git checkout develop \
&& mkdir build && cd build && mkdir read_only_query_tests && cd .. \
&& eosio-cpp -abigen ./tests/unit/test_contracts/read_only_query_tests.cpp -o ./build/read_only_query_tests/read_only_query_tests.wasm

RUN git clone https://github.com/EOSIO/eosio.contracts \
&& cd eosio.contracts \
&& git checkout develop \
Expand All @@ -34,13 +40,13 @@ 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::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
RUN mkdir -p "/opt/eosio/bin/contracts"
COPY --from=contracts /root/eos/contracts/contracts/eosio.bios/bin/* /opt/eosio/bin/contracts/eosio.bios/
COPY --from=contracts /root/eos/contracts/contracts/eosio.boot/bin/* /opt/eosio/bin/contracts/eosio.boot/
COPY --from=contracts /root/eosio.cdt/build/read_only_query_tests/* /opt/eosio/bin/contracts/read_only_query_tests/
COPY --from=contracts /root/eosio.contracts/build/ /opt/eosio/bin/contracts
COPY --from=contracts /root/key-value-example-app/contracts/kv_todo/build/* /opt/eosio/bin/contracts/kv_todo/
COPY --from=contracts /root/return-values-example-app/contracts/action_return_value/build/* /opt/eosio/bin/contracts/action_return_value/
Expand Down
7 changes: 7 additions & 0 deletions .github/eosjs-ci/scripts/deploy_contracts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ cleos create account eosio eosio.vpay $SYSTEM_ACCOUNT_PUBLIC_KEY
cleos create account eosio eosio.rex $SYSTEM_ACCOUNT_PUBLIC_KEY
cleos create account eosio eosio.token $SYSTEM_ACCOUNT_PUBLIC_KEY
cleos create account eosio returnvalue $SYSTEM_ACCOUNT_PUBLIC_KEY
cleos create account eosio readonly $EXAMPLE_ACCOUNT_PUBLIC_KEY
cleos create account eosio todo $SYSTEM_ACCOUNT_PUBLIC_KEY
cleos create account eosio bob $EXAMPLE_ACCOUNT_PUBLIC_KEY
cleos create account eosio alice $EXAMPLE_ACCOUNT_PUBLIC_KEY
Expand Down Expand Up @@ -214,6 +215,10 @@ sleep 1s
setabi returnvalue $CONTRACTS_DIR/action_return_value/action_return_value.abi
setcode returnvalue $CONTRACTS_DIR/action_return_value/action_return_value.wasm

sleep 1s
setabi readonly $CONTRACTS_DIR/read_only_query_tests/read_only_query_tests.abi
setcode readonly $CONTRACTS_DIR/read_only_query_tests/read_only_query_tests.wasm

sleep 1s
setabi eosio.token $CONTRACTS_DIR/eosio.token/eosio.token.abi
setcode eosio.token $CONTRACTS_DIR/eosio.token/eosio.token.wasm
Expand All @@ -229,6 +234,8 @@ cleos push action todo upsert '["bf581bee-9f2c-447b-94ad-78e4984b6f51", "todo",
cleos push action todo upsert '["b7b0d09d-a82b-44d9-b067-3bae2d02917e", "todo", "Start Blockchain", false]' -p todo@active
cleos push action todo upsert '["ac8acfe7-cd4e-4d22-8400-218b697a4517", "todo", "Deploy Hello World Contract", false]' -p todo@active

cleos push action readonly setup '[]' -p readonly@active

echo "All done initializing the blockchain"

if [[ -z $NODEOS_RUNNING ]]; then
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
Adding `readOnlyTrx` to the `transact` config will send the transaction through the `push_ro_transaction` endpoint in the `chain_api`. This endpoint will ensure that the transaction will not make any changes despite the actions in the transaction. It is typically useful for doing queries using actions but normal actions will also work with this endpoint, but they won't make changes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe:

This endpoint will ensure that the transaction will not make any changes despite the actions in the transaction. It is typically useful for doing queries using actions but normal actions will also work with this endpoint, but they won't make changes.

to ->

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, though the actions will not make any data changes. => what if you call an action that does make changes - is this rolled back? Perhaps explicitly state what happens in this case <=

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • maybe start the ### Read-Only Transactions paragraph with:
    From nodeos version 2.2, read-only queries have been introduced to eosjs. Adding readOnlyTrx to the transact config ... etc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe:

This endpoint will ensure that the transaction will not make any changes despite the actions in the transaction. It is typically useful for doing queries using actions but normal actions will also work with this endpoint, but they won't make changes.

to ->

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, though the actions will not make any data changes. => what if you call an action that does make changes - is this rolled back? Perhaps explicitly state what happens in this case <=

It is rolled back, so I changed your adjustments slightly to state that.


Adding `returnFailureTraces` to the `transact` config will return a trace of the failure if your transaction fails. At this time, this is only available for read-only transactions.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe:

Adding returnFailureTraces to the transact config will return a trace of the failure if your transaction fails. At this time, this is only available for read-only transactions.

to =>

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 read-only transactions. => what happens if you add this to a push_ro_transaction that is calling a non read only action - is nothing returned?.

Copy link
Contributor Author

@bradlhart bradlhart Jul 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed your changes slightly to state that it 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 to inform why the transaction failed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe:

When a call to the chain_api is performed and fails, it will result in an RPCError object being generated to inform why the transaction failed.

to =>

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 error with the concise message, for instance 'Invalid transaction', but additional details can be found in both the `details` field and the `json` field. The `json` field holds the entire json response from `nodeos` while the `details` field specifically holds the error object in the `json`. This is typically in different places of the `json` depending on what endpoint is used in `nodeos` so the `details` field is available to quickly see the error information without needing to search through the `json` object.
Copy link
Contributor

@halsaphi halsaphi Jun 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe

The RPCError object will error with the concise message, for instance 'Invalid transaction', but additional details can be found in both the details field and the json field. The json field holds the entire json response from nodeos while the details field specifically holds the error object in the json. This is typically in different places of the json depending on what endpoint is used in nodeos so the details field is available to quickly see the error information without needing to search through the json object.

to =>

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 ``detailsvary depending on the endpoint is used to callnodeos`. Use the `details` field to quickly find error information.


As you can see below, the concise error message might not give enough information to really discern the issue. While the error has `eosio_assert_message assertion failure` as the concise error, in the `details` field the `overdrawn balance` error mentioned. It's important to note that the format of the `details` object can be different depending on what endpoint is used.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe

As you can see below, the concise error message might not give enough information to really discern the issue. While the error has eosio_assert_message assertion failure as the concise error, in the details field the overdrawn balance error mentioned. It's important to note that the format of the details object can be different depending on what endpoint is used.

to =>

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.

It's important to note that the format of the details object can be different depending on what endpoint is used. => removed this bit as it is mentioned above. If you wanted to emphasise (or I have misunderstood) pwerhaps
=> Please note that the format of the details object will differ dependant on which endpoint is used.

=> as an additional thing - I think you choose the endpoint with the readOnlyTrx flag, is it worth mentioning the other endpoint explicitly and/or showing an example from calls to both endpoints -assuming there are two endpoints.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a large amount of different responses from nodeos, unfortunately it won't be easy to state all of them.

```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: 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
18 changes: 18 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 All @@ -93,6 +96,7 @@ export class JsonRpc implements AuthorityProvider, AbiProvider {
if (!response.ok) {
throw new RpcError(json);
}
// check each return value for every action for an error and call `new RpcError`?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this comment

return json;
}

Expand Down Expand Up @@ -311,6 +315,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
};
Loading