Skip to content

Commit

Permalink
Add support for sendTransaction error fetching (#59)
Browse files Browse the repository at this point in the history
* Add support for error fetching in sendTransaction

* Bump patch version

* Add support for broadcast type selection

* Add support for typed errors

* Add error ID and log to unknown error message
  • Loading branch information
zivkovicmilos authored Sep 6, 2023
1 parent 4c314cb commit cf2928b
Show file tree
Hide file tree
Showing 12 changed files with 508 additions and 33 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gnolang/tm2-js-client",
"version": "1.0.4",
"version": "1.0.5",
"description": "Tendermint2 JS / TS Client",
"main": "./bin/index.js",
"repository": {
Expand Down
160 changes: 160 additions & 0 deletions src/provider/errors/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import {
GasOverflowErrorMessage,
InsufficientCoinsErrorMessage,
InsufficientFeeErrorMessage,
InsufficientFundsErrorMessage,
InternalErrorMessage,
InvalidAddressErrorMessage,
InvalidCoinsErrorMessage,
InvalidGasWantedErrorMessage,
InvalidPubKeyErrorMessage,
InvalidSequenceErrorMessage,
MemoTooLargeErrorMessage,
NoSignaturesErrorMessage,
OutOfGasErrorMessage,
TooManySignaturesErrorMessage,
TxDecodeErrorMessage,
UnauthorizedErrorMessage,
UnknownAddressErrorMessage,
UnknownRequestErrorMessage,
} from './messages';

class TM2Error extends Error {
public log?: string;

constructor(message: string, log?: string) {
super(message);

this.log = log;
}
}

class InternalError extends TM2Error {
constructor(log?: string) {
super(InternalErrorMessage, log);
}
}

class TxDecodeError extends TM2Error {
constructor(log?: string) {
super(TxDecodeErrorMessage, log);
}
}

class InvalidSequenceError extends TM2Error {
constructor(log?: string) {
super(InvalidSequenceErrorMessage, log);
}
}

class UnauthorizedError extends TM2Error {
constructor(log?: string) {
super(UnauthorizedErrorMessage, log);
}
}

class InsufficientFundsError extends TM2Error {
constructor(log?: string) {
super(InsufficientFundsErrorMessage, log);
}
}

class UnknownRequestError extends TM2Error {
constructor(log?: string) {
super(UnknownRequestErrorMessage, log);
}
}

class InvalidAddressError extends TM2Error {
constructor(log?: string) {
super(InvalidAddressErrorMessage, log);
}
}

class UnknownAddressError extends TM2Error {
constructor(log?: string) {
super(UnknownAddressErrorMessage, log);
}
}

class InvalidPubKeyError extends TM2Error {
constructor(log?: string) {
super(InvalidPubKeyErrorMessage, log);
}
}

class InsufficientCoinsError extends TM2Error {
constructor(log?: string) {
super(InsufficientCoinsErrorMessage, log);
}
}

class InvalidCoinsError extends TM2Error {
constructor(log?: string) {
super(InvalidCoinsErrorMessage, log);
}
}

class InvalidGasWantedError extends TM2Error {
constructor(log?: string) {
super(InvalidGasWantedErrorMessage, log);
}
}

class OutOfGasError extends TM2Error {
constructor(log?: string) {
super(OutOfGasErrorMessage, log);
}
}

class MemoTooLargeError extends TM2Error {
constructor(log?: string) {
super(MemoTooLargeErrorMessage, log);
}
}

class InsufficientFeeError extends TM2Error {
constructor(log?: string) {
super(InsufficientFeeErrorMessage, log);
}
}

class TooManySignaturesError extends TM2Error {
constructor(log?: string) {
super(TooManySignaturesErrorMessage, log);
}
}

class NoSignaturesError extends TM2Error {
constructor(log?: string) {
super(NoSignaturesErrorMessage, log);
}
}

class GasOverflowError extends TM2Error {
constructor(log?: string) {
super(GasOverflowErrorMessage, log);
}
}

export {
TM2Error,
InternalError,
TxDecodeError,
InvalidSequenceError,
UnauthorizedError,
InsufficientFundsError,
UnknownRequestError,
InvalidAddressError,
UnknownAddressError,
InvalidPubKeyError,
InsufficientCoinsError,
InvalidCoinsError,
InvalidGasWantedError,
OutOfGasError,
MemoTooLargeError,
InsufficientFeeError,
TooManySignaturesError,
NoSignaturesError,
GasOverflowError,
};
1 change: 1 addition & 0 deletions src/provider/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './errors';
42 changes: 42 additions & 0 deletions src/provider/errors/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Errors constructed from:
// https://github.com/gnolang/gno/blob/master/tm2/pkg/std/errors.go

const InternalErrorMessage = 'internal error encountered';
const TxDecodeErrorMessage = 'unable to decode tx';
const InvalidSequenceErrorMessage = 'invalid sequence';
const UnauthorizedErrorMessage = 'signature is unauthorized';
const InsufficientFundsErrorMessage = 'insufficient funds';
const UnknownRequestErrorMessage = 'unknown request';
const InvalidAddressErrorMessage = 'invalid address';
const UnknownAddressErrorMessage = 'unknown address';
const InvalidPubKeyErrorMessage = 'invalid pubkey';
const InsufficientCoinsErrorMessage = 'insufficient coins';
const InvalidCoinsErrorMessage = 'invalid coins';
const InvalidGasWantedErrorMessage = 'invalid gas wanted';
const OutOfGasErrorMessage = 'out of gas';
const MemoTooLargeErrorMessage = 'memo too large';
const InsufficientFeeErrorMessage = 'insufficient fee';
const TooManySignaturesErrorMessage = 'too many signatures';
const NoSignaturesErrorMessage = 'no signatures';
const GasOverflowErrorMessage = 'gas overflow';

export {
InternalErrorMessage,
TxDecodeErrorMessage,
InvalidSequenceErrorMessage,
UnauthorizedErrorMessage,
InsufficientFundsErrorMessage,
UnknownRequestErrorMessage,
InvalidAddressErrorMessage,
UnknownAddressErrorMessage,
InvalidPubKeyErrorMessage,
InsufficientCoinsErrorMessage,
InvalidCoinsErrorMessage,
InvalidGasWantedErrorMessage,
OutOfGasErrorMessage,
MemoTooLargeErrorMessage,
InsufficientFeeErrorMessage,
TooManySignaturesErrorMessage,
NoSignaturesErrorMessage,
GasOverflowErrorMessage,
};
1 change: 1 addition & 0 deletions src/provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './utility';
export * from './websocket';
export * from './endpoints';
export * from './provider';
export * from './errors';
55 changes: 43 additions & 12 deletions src/provider/jsonrpc/jsonrpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
ABCIResponse,
BlockInfo,
BlockResult,
BroadcastTxResult,
BroadcastTxSyncResult,
ConsensusParams,
NetworkInfo,
RPCRequest,
Expand All @@ -16,6 +16,8 @@ import { mock } from 'jest-mock-extended';
import { Tx } from '../../proto';
import { sha256 } from '@cosmjs/crypto';
import { CommonEndpoint } from '../endpoints';
import { UnauthorizedErrorMessage } from '../errors/messages';
import { TM2Error } from '../errors';

jest.mock('axios');

Expand Down Expand Up @@ -69,20 +71,49 @@ describe('JSON-RPC Provider', () => {
expect(result).toEqual(mockResult);
});

test('sendTransaction', async () => {
const mockResult: BroadcastTxResult = mock<BroadcastTxResult>();
mockResult.hash = 'hash123';
describe('sendTransaction', () => {
const validResult: BroadcastTxSyncResult = {
error: null,
data: null,
Log: '',
hash: 'hash123',
};

mockedAxios.post.mockResolvedValue({
data: newResponse<BroadcastTxResult>(mockResult),
});
const mockError = '/std.UnauthorizedError';
const mockLog = 'random error message';
const invalidResult: BroadcastTxSyncResult = {
error: {
ABCIErrorKey: mockError,
},
data: null,
Log: mockLog,
hash: '',
};

// Create the provider
const provider = new JSONRPCProvider(mockURL);
const hash = await provider.sendTransaction('encoded tx');
test.each([
[validResult, validResult.hash, '', ''], // no error
[invalidResult, invalidResult.hash, UnauthorizedErrorMessage, mockLog], // error out
])('case %#', async (response, expectedHash, expectedErr, expectedLog) => {
mockedAxios.post.mockResolvedValue({
data: newResponse<BroadcastTxSyncResult>(response),
});

expect(axios.post).toHaveBeenCalled();
expect(hash).toEqual(mockResult.hash);
try {
// Create the provider
const provider = new JSONRPCProvider(mockURL);
const hash = await provider.sendTransaction('encoded tx');

expect(axios.post).toHaveBeenCalled();
expect(hash).toEqual(expectedHash);

if (expectedErr != '') {
fail('expected error');
}
} catch (e) {
expect((e as Error).message).toBe(expectedErr);
expect((e as TM2Error).log).toBe(expectedLog);
}
});
});

test('waitForTransaction', async () => {
Expand Down
70 changes: 65 additions & 5 deletions src/provider/jsonrpc/jsonrpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import {
ABCIResponse,
BlockInfo,
BlockResult,
BroadcastTxResult,
BroadcastTxCommitResult,
BroadcastTxSyncResult,
ConsensusParams,
NetworkInfo,
RPCRequest,
Status,
} from '../types';
import { RestService } from '../../services';
Expand All @@ -24,6 +26,7 @@ import {
TransactionEndpoint,
} from '../endpoints';
import { Tx } from '../../proto';
import { constructRequestError } from '../utility/errors.utility';

/**
* Provider based on JSON-RPC HTTP requests
Expand Down Expand Up @@ -143,12 +146,69 @@ export class JSONRPCProvider implements Provider {
});
}

async sendTransaction(tx: string): Promise<string> {
const response: BroadcastTxResult =
await RestService.post<BroadcastTxResult>(this.baseURL, {
request: newRequest(TransactionEndpoint.BROADCAST_TX_SYNC, [tx]),
async sendTransaction(
tx: string,
endpoint?:
| TransactionEndpoint.BROADCAST_TX_SYNC
| TransactionEndpoint.BROADCAST_TX_COMMIT
): Promise<string> {
const queryEndpoint = endpoint
? endpoint
: TransactionEndpoint.BROADCAST_TX_SYNC;

const request: RPCRequest = newRequest(queryEndpoint, [tx]);

if (queryEndpoint == TransactionEndpoint.BROADCAST_TX_SYNC) {
return this.broadcastTxSync(request);
}

// The endpoint is a commit broadcast
// (it waits for the transaction to be committed) to the chain before returning
return this.broadcastTxCommit(request);
}

private async broadcastTxSync(request: RPCRequest): Promise<string> {
const response: BroadcastTxSyncResult =
await RestService.post<BroadcastTxSyncResult>(this.baseURL, {
request,
});

// Check if there is an immediate tx-broadcast error
// (originating from basic transaction checks like CheckTx)
if (response.error) {
const errType: string = response.error.ABCIErrorKey;
const log: string = response.Log;

throw constructRequestError(errType, log);
}

return response.hash;
}

private async broadcastTxCommit(request: RPCRequest): Promise<string> {
const response: BroadcastTxCommitResult =
await RestService.post<BroadcastTxCommitResult>(this.baseURL, {
request,
});

const { check_tx, deliver_tx } = response;

// Check if there is an immediate tx-broadcast error (in CheckTx)
if (check_tx.ResponseBase.Error) {
const errType: string = check_tx.ResponseBase.Error.ABCIErrorKey;
const log: string = check_tx.ResponseBase.Log;

throw constructRequestError(errType, log);
}

// Check if there is a parsing error with the transaction (in DeliverTx)
if (deliver_tx.ResponseBase.Error) {
const errType: string = deliver_tx.ResponseBase.Error.ABCIErrorKey;
const log: string = deliver_tx.ResponseBase.Log;

throw constructRequestError(errType, log);
}

return response.hash;
}

Expand Down
Loading

0 comments on commit cf2928b

Please sign in to comment.