Standardizing error handling in send_transaction2 (#123)

* Standardizing error handling in send_transaction2

* v1.2.0-rc2

* Update package.json
This commit is contained in:
Aaron Cox
2026-04-11 20:24:53 -07:00
committed by GitHub
parent 189442b1c6
commit feeb1def9e
7 changed files with 266 additions and 6 deletions
+3 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@wharfkit/antelope",
"description": "Library for working with Antelope powered blockchains.",
"version": "1.1.1",
"version": "1.2.0",
"homepage": "https://github.com/wharfkit/antelope",
"license": "BSD-3-Clause-No-Military-License",
"main": "lib/antelope.js",
@@ -58,5 +58,6 @@
"tsconfig-paths": "^4.1.0",
"typedoc": "^0.23.10",
"typescript": "^4.1.2"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
+36 -2
View File
@@ -1,4 +1,5 @@
import {APIClient} from '../client'
import {APIClient, APIError} from '../client'
import {APIResponse} from '../provider'
import {
BlockIdType,
@@ -46,6 +47,7 @@ import {
SendTransaction2Options,
SendTransaction2Response,
SendTransactionResponse,
SendTransactionResponseExceptionStack,
TableIndexType,
TableIndexTypes,
} from './types'
@@ -53,6 +55,14 @@ import {
import {ABISerializableConstructor, ABISerializableType, Serializer} from '../../serializer'
import {isInstanceOf} from '../../utils'
function resolveStackFormat(entry: SendTransactionResponseExceptionStack): string {
if (!entry.format) return ''
if (!entry.data) return entry.format
return entry.format.replace(/\$\{(\w+)\}/g, (_, key) =>
entry.data[key] !== undefined ? String(entry.data[key]) : `\${${key}}`
)
}
export class ChainAPI {
constructor(private client: APIClient) {}
@@ -227,7 +237,7 @@ export class ChainAPI {
if (!isInstanceOf(tx, PackedTransaction)) {
tx = PackedTransaction.fromSigned(SignedTransaction.from(tx))
}
return this.client.call<SendTransaction2Response>({
const response = await this.client.call<SendTransaction2Response>({
path: '/v1/chain/send_transaction2',
params: {
return_failure_trace: true,
@@ -237,6 +247,30 @@ export class ChainAPI {
...options,
},
})
if (response && response.processed && response.processed.except) {
const except = response.processed.except
const synthesized: APIResponse = {
status: 202,
headers: {},
text: JSON.stringify(response),
json: {
...response,
error: {
code: except.code || 0,
name: except.name || 'transaction_exception',
what: except.message || 'Transaction failed',
details: (except.stack || []).map((s) => ({
message: resolveStackFormat(s) || '',
file: (s.context && s.context.file) || '',
line_number: (s.context && s.context.line) || 0,
method: (s.context && s.context.method) || '',
})),
},
},
}
throw new APIError('/v1/chain/send_transaction2', synthesized)
}
return response
}
async get_table_rows<Index extends TableIndexType = Name>(
+3 -1
View File
@@ -476,8 +476,10 @@ export interface SendTransaction2Response {
id: string
block_num: number
block_time: string
receipt: {status: string; cpu_usage_us: number; net_usage_words: number}
receipt: {status: string; cpu_usage_us: number; net_usage_words: number} | null
elapsed: number
except?: SendTransactionResponseException
error_code?: string
net_usage: number
scheduled: boolean
action_traces: any[]
+35
View File
@@ -618,6 +618,41 @@ suite('api v1', function () {
assert.equal(result.transaction_id, transaction.id.hexString)
})
test('chain send_transaction2 (failure detection)', async function () {
const provider = jungle4.provider as MockProvider
provider.setContext(this.test!.title)
const info = await jungle4.v1.chain.get_info()
const header = info.getTransactionHeader(90)
const transfer = Transfer.from({
from: 'corecorecore',
to: 'nonexistent1',
quantity: '0.0001 EOS',
memo: 'this should fail',
})
const action = Action.from({
authorization: [{actor: 'corecorecore', permission: 'active'}],
account: 'eosio.token',
name: 'transfer',
data: transfer,
})
const transaction = Transaction.from({...header, actions: [action]})
const signedTransaction = await signMockTransaction(transaction, info)
try {
await jungle4.v1.chain.send_transaction2(signedTransaction)
assert.fail('should have thrown')
} catch (error) {
assert.equal(error instanceof APIError, true)
const apiError = error as APIError
assert.equal(apiError.name, 'eosio_assert_message_exception')
assert.equal(apiError.code, 3050003)
assert.isAbove(apiError.details.length, 0)
assert.isDefined(apiError.response.json.processed)
assert.isDefined(apiError.response.json.processed.except)
} finally {
provider.setContext('')
}
})
test('chain get_table_rows (untyped)', async function () {
const res = await eos.v1.chain.get_table_rows({
code: 'eosio.token',
@@ -0,0 +1,37 @@
{
"api": "https://jungle4.api.eosnation.io",
"headers": {
"access-control-allow-headers": "Origin, X-Requested-With, Content-Type, Accept",
"access-control-allow-methods": "GET, POST, HEAD, OPTIONS",
"access-control-allow-origin": "*",
"connection": "close",
"content-length": "966",
"content-type": "application/json",
"date": "Fri, 20 Feb 2026 18:42:11 GMT",
"server": "nginx/1.28.2"
},
"status": 200,
"json": {
"server_version": "42b514a1",
"chain_id": "73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d",
"head_block_num": 251509646,
"last_irreversible_block_num": 251509644,
"last_irreversible_block_id": "0efdbb8c537db74dea9eb0ba6bceafc4f0f1879b2d462aeda58619d54e2ce8da",
"head_block_id": "0efdbb8efd8f422850484a14cbf4cfb90e37e99a10e4aa3fa7f010721432cdfc",
"head_block_time": "2026-02-20T18:42:11.500",
"head_block_producer": "gorillapower",
"virtual_block_cpu_limit": 200000000,
"virtual_block_net_limit": 1048576000,
"block_cpu_limit": 200000,
"block_net_limit": 1048576,
"server_version_string": "v1.2.1",
"fork_db_head_block_num": 251509646,
"fork_db_head_block_id": "0efdbb8efd8f422850484a14cbf4cfb90e37e99a10e4aa3fa7f010721432cdfc",
"server_full_version_string": "v1.2.1-42b514a1566f46a3c652e5025911b295214d3ce4-dirty",
"total_cpu_weight": "120571940060499",
"total_net_weight": "117540133617123",
"earliest_available_block_num": 1,
"last_irreversible_block_time": "2026-02-20T18:42:10.500"
},
"text": "{\"server_version\":\"42b514a1\",\"chain_id\":\"73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d\",\"head_block_num\":251509646,\"last_irreversible_block_num\":251509644,\"last_irreversible_block_id\":\"0efdbb8c537db74dea9eb0ba6bceafc4f0f1879b2d462aeda58619d54e2ce8da\",\"head_block_id\":\"0efdbb8efd8f422850484a14cbf4cfb90e37e99a10e4aa3fa7f010721432cdfc\",\"head_block_time\":\"2026-02-20T18:42:11.500\",\"head_block_producer\":\"gorillapower\",\"virtual_block_cpu_limit\":200000000,\"virtual_block_net_limit\":1048576000,\"block_cpu_limit\":200000,\"block_net_limit\":1048576,\"server_version_string\":\"v1.2.1\",\"fork_db_head_block_num\":251509646,\"fork_db_head_block_id\":\"0efdbb8efd8f422850484a14cbf4cfb90e37e99a10e4aa3fa7f010721432cdfc\",\"server_full_version_string\":\"v1.2.1-42b514a1566f46a3c652e5025911b295214d3ce4-dirty\",\"total_cpu_weight\":\"120571940060499\",\"total_net_weight\":\"117540133617123\",\"earliest_available_block_num\":1,\"last_irreversible_block_time\":\"2026-02-20T18:42:10.500\"}"
}
@@ -0,0 +1,146 @@
{
"api": "https://jungle4.api.eosnation.io",
"headers": {
"access-control-allow-headers": "Origin, X-Requested-With, Content-Type, Accept",
"access-control-allow-methods": "GET, POST, HEAD, OPTIONS",
"access-control-allow-origin": "*",
"connection": "close",
"content-length": "2586",
"content-type": "application/json",
"date": "Fri, 20 Feb 2026 18:42:11 GMT",
"server": "nginx/1.28.2"
},
"status": 202,
"json": {
"transaction_id": "21a37ffb8f996491a77953426e8da621b8b01c06cce5a80bbe8464408bae0185",
"processed": {
"id": "21a37ffb8f996491a77953426e8da621b8b01c06cce5a80bbe8464408bae0185",
"block_num": 251509647,
"block_time": "2026-02-20T18:42:12.000",
"producer_block_id": null,
"receipt": null,
"elapsed": 214,
"net_usage": 135,
"scheduled": false,
"action_traces": [
{
"action_ordinal": 1,
"creator_action_ordinal": 0,
"closest_unnotified_ancestor_action_ordinal": 0,
"receipt": null,
"receiver": "eosio.token",
"act": {
"account": "eosio.token",
"name": "transfer",
"authorization": [
{
"actor": "corecorecore",
"permission": "active"
}
],
"data": {
"from": "corecorecore",
"to": "nonexistent1",
"quantity": "0.0001 EOS",
"memo": "this should fail"
},
"hex_data": "a02e45ea52a42e4510f25419bbae269d010000000000000004454f530000000010746869732073686f756c64206661696c"
},
"context_free": false,
"elapsed": 74,
"console": "",
"trx_id": "21a37ffb8f996491a77953426e8da621b8b01c06cce5a80bbe8464408bae0185",
"block_num": 251509647,
"block_time": "2026-02-20T18:42:12.000",
"producer_block_id": null,
"account_ram_deltas": [],
"except": {
"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": "2026-02-20T18:42:11.892"
},
"format": "assertion failure with message: ${s}",
"data": {
"s": "to account does not exist"
}
},
{
"context": {
"level": "warn",
"file": "apply_context.cpp",
"line": 134,
"method": "exec_one",
"hostname": "",
"thread_name": "nodeos",
"timestamp": "2026-02-20T18:42:11.892"
},
"format": "${receiver} <= ${account}::${action} pending console output: ${console}",
"data": {
"console": "",
"account": "eosio.token",
"action": "transfer",
"receiver": "eosio.token"
}
}
]
},
"error_code": "10000000000000000000",
"return_value_hex_data": ""
}
],
"account_ram_delta": null,
"except": {
"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": "2026-02-20T18:42:11.892"
},
"format": "assertion failure with message: ${s}",
"data": {
"s": "to account does not exist"
}
},
{
"context": {
"level": "warn",
"file": "apply_context.cpp",
"line": 134,
"method": "exec_one",
"hostname": "",
"thread_name": "nodeos",
"timestamp": "2026-02-20T18:42:11.892"
},
"format": "${receiver} <= ${account}::${action} pending console output: ${console}",
"data": {
"console": "",
"account": "eosio.token",
"action": "transfer",
"receiver": "eosio.token"
}
}
]
},
"error_code": "10000000000000000000"
}
},
"text": "{\"transaction_id\":\"21a37ffb8f996491a77953426e8da621b8b01c06cce5a80bbe8464408bae0185\",\"processed\":{\"id\":\"21a37ffb8f996491a77953426e8da621b8b01c06cce5a80bbe8464408bae0185\",\"block_num\":251509647,\"block_time\":\"2026-02-20T18:42:12.000\",\"producer_block_id\":null,\"receipt\":null,\"elapsed\":214,\"net_usage\":135,\"scheduled\":false,\"action_traces\":[{\"action_ordinal\":1,\"creator_action_ordinal\":0,\"closest_unnotified_ancestor_action_ordinal\":0,\"receipt\":null,\"receiver\":\"eosio.token\",\"act\":{\"account\":\"eosio.token\",\"name\":\"transfer\",\"authorization\":[{\"actor\":\"corecorecore\",\"permission\":\"active\"}],\"data\":{\"from\":\"corecorecore\",\"to\":\"nonexistent1\",\"quantity\":\"0.0001 EOS\",\"memo\":\"this should fail\"},\"hex_data\":\"a02e45ea52a42e4510f25419bbae269d010000000000000004454f530000000010746869732073686f756c64206661696c\"},\"context_free\":false,\"elapsed\":74,\"console\":\"\",\"trx_id\":\"21a37ffb8f996491a77953426e8da621b8b01c06cce5a80bbe8464408bae0185\",\"block_num\":251509647,\"block_time\":\"2026-02-20T18:42:12.000\",\"producer_block_id\":null,\"account_ram_deltas\":[],\"except\":{\"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\":\"2026-02-20T18:42:11.892\"},\"format\":\"assertion failure with message: ${s}\",\"data\":{\"s\":\"to account does not exist\"}},{\"context\":{\"level\":\"warn\",\"file\":\"apply_context.cpp\",\"line\":134,\"method\":\"exec_one\",\"hostname\":\"\",\"thread_name\":\"nodeos\",\"timestamp\":\"2026-02-20T18:42:11.892\"},\"format\":\"${receiver} <= ${account}::${action} pending console output: ${console}\",\"data\":{\"console\":\"\",\"account\":\"eosio.token\",\"action\":\"transfer\",\"receiver\":\"eosio.token\"}}]},\"error_code\":\"10000000000000000000\",\"return_value_hex_data\":\"\"}],\"account_ram_delta\":null,\"except\":{\"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\":\"2026-02-20T18:42:11.892\"},\"format\":\"assertion failure with message: ${s}\",\"data\":{\"s\":\"to account does not exist\"}},{\"context\":{\"level\":\"warn\",\"file\":\"apply_context.cpp\",\"line\":134,\"method\":\"exec_one\",\"hostname\":\"\",\"thread_name\":\"nodeos\",\"timestamp\":\"2026-02-20T18:42:11.892\"},\"format\":\"${receiver} <= ${account}::${action} pending console output: ${console}\",\"data\":{\"console\":\"\",\"account\":\"eosio.token\",\"action\":\"transfer\",\"receiver\":\"eosio.token\"}}]},\"error_code\":\"10000000000000000000\"}}"
}
+6 -1
View File
@@ -10,12 +10,17 @@ import {APIMethods, APIProvider, Bytes, Checksum160, FetchProvider} from '$lib'
export class MockProvider implements APIProvider {
recordProvider = new FetchProvider(this.api, {fetch, headers: this.reqHeaders})
private context: string = ''
constructor(private api: string = 'https://jungle4.greymass.com', private reqHeaders = {}) {}
setContext(name: string) {
this.context = name
}
getFilename(path: string, params?: unknown) {
const digest = Checksum160.hash(
Bytes.from(this.api + path + (params ? JSON.stringify(params) : ''), 'utf8')
Bytes.from(this.api + path + this.context + (params ? JSON.stringify(params) : ''), 'utf8')
).hexString
return joinPath(__dirname, '../data', digest + '.json')
}