diff --git a/.github/eosjs-ci/Dockerfile b/.github/eosjs-ci/Dockerfile index 277738a80..3d34109d3 100644 --- a/.github/eosjs-ci/Dockerfile +++ b/.github/eosjs-ci/Dockerfile @@ -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 diff --git a/docs/how-to-guides/01_how-to-submit-a-transaction.md b/docs/how-to-guides/01_how-to-submit-a-transaction.md index 84d143b93..6b29858ca 100644 --- a/docs/how-to-guides/01_how-to-submit-a-transaction.md +++ b/docs/how-to-guides/01_how-to-submit-a-transaction.md @@ -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. diff --git a/docs/troubleshooting/02_rpcerror.md b/docs/troubleshooting/02_rpcerror.md new file mode 100644 index 000000000..c93709176 --- /dev/null +++ b/docs/troubleshooting/02_rpcerror.md @@ -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. (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 +} +``` diff --git a/package.json b/package.json index 4b9e238f8..ec17ff982 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/eosjs-api-interfaces.ts b/src/eosjs-api-interfaces.ts index b59406b67..20805db5c 100644 --- a/src/eosjs-api-interfaces.ts +++ b/src/eosjs-api-interfaces.ts @@ -89,6 +89,8 @@ export interface Transaction { export interface TransactConfig { broadcast?: boolean; sign?: boolean; + readOnlyTrx?: boolean; + returnFailureTraces?: boolean; requiredKeys?: string[]; compression?: boolean; blocksBehind?: number; diff --git a/src/eosjs-api.ts b/src/eosjs-api.ts index 3d00a1d72..dcd490d6a 100644 --- a/src/eosjs-api.ts +++ b/src/eosjs-api.ts @@ -29,6 +29,7 @@ import { GetBlockHeaderStateResult, GetBlockInfoResult, GetBlockResult, + ReadOnlyTransactResult, } from './eosjs-rpc-interfaces'; import * as ser from './eosjs-serialize'; @@ -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, @@ -287,8 +290,18 @@ export class Api { */ public async transact( transaction: Transaction, - { broadcast = true, sign = true, requiredKeys, compression, blocksBehind, useLastIrreversible, expireSeconds }: - TransactConfig = {}): Promise + { + broadcast = true, + sign = true, + readOnlyTrx, + returnFailureTraces, + requiredKeys, + compression, + blocksBehind, + useLastIrreversible, + expireSeconds + }: + TransactConfig = {}): Promise { let info: GetInfoResult; @@ -337,6 +350,9 @@ export class Api { } if (broadcast) { let result; + if (readOnlyTrx) { + return this.rpc.push_ro_transaction(pushTransactionArgs, returnFailureTraces) as Promise; + } if (compression) { return this.pushCompressedSignedTransaction(pushTransactionArgs) as Promise; } @@ -529,7 +545,7 @@ export class TransactionBuilder { return this; } - public async send(config?: TransactConfig): Promise { + public async send(config?: TransactConfig): Promise { const contextFreeDataSet: Uint8Array[] = []; const contextFreeActions: ser.SerializedAction[] = []; const actions: ser.SerializedAction[] = this.actions.map((actionBuilder) => actionBuilder.serializedData as ser.SerializedAction); diff --git a/src/eosjs-jsonrpc.ts b/src/eosjs-jsonrpc.ts index 374a8a613..67ff80cfb 100644 --- a/src/eosjs-jsonrpc.ts +++ b/src/eosjs-jsonrpc.ts @@ -27,6 +27,7 @@ import { GetTableRowsResult, PushTransactionArgs, PackedTrx, + ReadOnlyTransactResult, GetBlockHeaderStateResult, GetTableByScopeResult, DBSizeGetResult, @@ -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; @@ -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 { + 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 { const packedTrxs: PackedTrx[] = transactions.map(({signatures, compression = 0, serializedTransaction, serializedContextFreeData }: PushTransactionArgs) => { return { diff --git a/src/eosjs-rpc-interfaces.ts b/src/eosjs-rpc-interfaces.ts index 9539e4de3..c82e7d1ba 100644 --- a/src/eosjs-rpc-interfaces.ts +++ b/src/eosjs-rpc-interfaces.ts @@ -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 */ @@ -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; diff --git a/src/eosjs-rpcerror.ts b/src/eosjs-rpcerror.ts index 4924da02b..38cfd389a 100644 --- a/src/eosjs-rpcerror.ts +++ b/src/eosjs-rpcerror.ts @@ -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); } diff --git a/src/tests/eosjs-jsonrpc.test.ts b/src/tests/eosjs-jsonrpc.test.ts index 53e315350..8528aa885 100644 --- a/src/tests/eosjs-jsonrpc.test.ts +++ b/src/tests/eosjs-jsonrpc.test.ts @@ -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 = [ diff --git a/src/tests/node.js b/src/tests/node.js index 98afd5095..c143975d5 100644 --- a/src/tests/node.js +++ b/src/tests/node.js @@ -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({ @@ -210,5 +253,7 @@ module.exports = { transactWithShorthandTxJsonContextFreeAction, transactWithShorthandTxJsonContextFreeData, transactWithReturnValue, + readOnlyQuery, + readOnlyFailureTrace, rpcShouldFail }; diff --git a/src/tests/node.test.ts b/src/tests/node.test.ts index e95c3baeb..266fef88e 100644 --- a/src/tests/node.test.ts +++ b/src/tests/node.test.ts @@ -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; diff --git a/src/tests/type-checks.test.ts b/src/tests/type-checks.test.ts index f04ff0554..b7ac8db56 100644 --- a/src/tests/type-checks.test.ts +++ b/src/tests/type-checks.test.ts @@ -26,6 +26,7 @@ import { GetTableRowsResult, GetTableByScopeResult, PushTransactionArgs, + ReadOnlyTransactResult, AbiBinToJsonResult, TraceApiGetBlockResult, DBSizeGetResult, @@ -889,6 +890,102 @@ describe('Chain API Plugin Endpoints', () => { verifyType(result, transactResult); }); + it('validates return type of push_ro_transaction', async () => { + const transaction: PushTransactionArgs = await api.transact({ + actions: [{ + account: 'readonly', + name: 'get', + authorization: [{ + actor: 'readonly', + permission: 'active', + }], + data: {}, + }], + }, { + sign: true, + broadcast: false, + useLastIrreversible: true, + expireSeconds: 30, + }) as PushTransactionArgs; + const result: ReadOnlyTransactResult = await rpc.push_ro_transaction(transaction); + const readOnlyTransactResult: any = { + 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: { + id: 'string', + block_num: 'number', + block_time: 'string', + 'producer_block_id&': 'string', + 'receipt&': { + status: 'string', + cpu_usage_us: 'number', + net_usage_words: 'number', + }, + elapsed: 'number', + net_usage: 'number', + scheduled: 'boolean', + action_traces: { + action_ordinal: 'number', + creator_action_ordinal: 'number', + closest_unnotified_ancestor_action_ordinal: 'number', + receipt: { + receiver: 'string', + act_digest: 'string', + global_sequence: 'number', + recv_sequence: 'number', + auth_sequence: [ 'string', 'number' ], + code_sequence: 'number', + abi_sequence: 'number', + }, + receiver: 'string', + act: { + account: 'string', + name: 'string', + authorization: { + actor: 'string', + permission: 'string', + }, + 'data?': 'any', + 'hex_data?': 'string', + }, + context_free: 'boolean', + elapsed: 'number', + console: 'string', + trx_id: 'string', + block_num: 'number', + block_time: 'string', + 'producer_block_id&': 'string', + account_ram_deltas: { + account: 'string', + delta: 'number', + }, + account_disk_deltas: { + account: 'string', + delta: 'number', + }, + except: 'any', + 'error_code&': 'number', + 'return_value?': 'any', + 'return_value_hex_data?': 'string', + 'return_value_data?': 'any', + 'inline_traces?': 'any', // ActionTrace, recursive? + }, + 'account_ram_delta&': { + account: 'string', + delta: 'number', + }, + 'except&': 'string', + 'error_code&': 'number', + bill_to_accounts: 'string', + } + }; + verifyType(result, readOnlyTransactResult); + }); + it('validates return type of push_transactions', async () => { const transactionA: PushTransactionArgs = await api.transact({ actions: [{ diff --git a/src/tests/web.html b/src/tests/web.html index 9688ee3bc..42513ea38 100644 --- a/src/tests/web.html +++ b/src/tests/web.html @@ -474,6 +474,57 @@ resultsLabel.innerText = FAILED; return false; } + + const readOnlyQuery = async () => { + return await api.transact({ + actions: [{ + account: 'readonly', + name: 'get', + authorization: [{ + actor: 'readonly', + permission: 'active', + }], + data: {}, + }], + }, { + blocksBehind: 3, + expireSeconds: 30, + readOnlyTrx: true, + }); + } + + const testWithReadOnlyQuery = async (e) => { + const expectedValue = [ + {'id': 1, 'name': 'Bob Smith', 'gender': 1, 'age': 25}, + {'id': 3, 'name': 'John Smith', 'gender': 1, 'age': 42}, + {'id': 4, 'name': 'Jack Smith', 'gender': 1, 'age': 27}, + {'id': 2, 'name': 'Alice Smith', 'gender': 0, 'age': 20,}, + {'id': 5, 'name': 'Youko Niihara', 'gender': 0, 'age': 26}, + {'id': 6, 'name': 'Rose Lee', 'gender': 0, 'age': 18}, + {'id': 7, 'name': 'Youko Kawakami', 'gender': 0, 'age': 25}, + {'id': 8, 'name': 'Yuu Yamada', 'gender': 0, 'age': 24} + ]; + resultsLabel = e.target; + resultsLabel.innerText = EXECUTING; + + try { + transactionResponse = await readOnlyQuery(); + } catch (error) { + resultsLabel.className = 'failed'; + resultsLabel.innerText = FAILED; + console.error('Transact Read Only Query Test Failure: ', error.message); + return false; + } + + if (JSON.stringify(transactionResponse.result.action_traces[0].return_value_data) === JSON.stringify(expectedValue)) { + resultsLabel.className = "success"; + resultsLabel.innerText = SUCCESS; + return true; + } + resultsLabel.className = 'failed'; + resultsLabel.innerText = FAILED; + return false; + } const transactShouldFail = async () => await api.transact({ actions: [{ @@ -564,6 +615,7 @@

Web Build Integration Tests

Transact with .with() Using Tx and Json Abi with Context Free Data

Transact elliptic p256/KeyType.r1 Keys and Signatures

Transact Return Values

+

Read Only Query

Invalid Transaction Throws Error

Invalid Rpc Call Throws Rpc Error

diff --git a/yarn.lock b/yarn.lock index 4376c0b39..4487cf499 100644 --- a/yarn.lock +++ b/yarn.lock @@ -822,9 +822,9 @@ integrity sha512-GdZbRSJ3Cv5fiwT6I0SQ3ckeN2PWNqxd26W9Z2fCK1tGrrasGy4puvNFtnddqH9UJFMQYXxEuuB7B8UK+LLwSg== "@types/prettier@^2.0.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.3.0.tgz#2e8332cc7363f887d32ec5496b207d26ba8052bb" - integrity sha512-hkc1DATxFLQo4VxPDpMH1gCkPpBbpOoJ/4nhuXw4n63/0R6bCpQECj4+K226UJ4JO/eJQz+1mC2I7JsWanAdQw== + version "2.3.1" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.3.1.tgz#54dd88bdc7f49958329666af3779561e47d5dab3" + integrity sha512-NVkb4p4YjI8E3O6+1m8I+8JlMpFZwfSbPGdaw0wXuyPRTEz0SLKwBUWNSO7Maoi8tQMPC8JLZNWkrcKPI7/sLA== "@types/sinonjs__fake-timers@^6.0.2": version "6.0.2" @@ -2287,9 +2287,9 @@ ecurve@1.0.5: bigi "^1.1.0" electron-to-chromium@^1.3.723: - version "1.3.762" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.762.tgz#3fa4e3bcbda539b50e3aa23041627063a5cffe61" - integrity sha512-LehWjRpfPcK8F1Lf/NZoAwWLWnjJVo0SZeQ9j/tvnBWYcT99qDqgo4raAfS2oTKZjPrR/jxruh85DGgDUmywEA== + version "1.3.763" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.763.tgz#93f6f02506d099941f557b9db9ba50b30215bf15" + integrity sha512-UyvEPae0wvzsyNJhVfGeFSOlUkHEze8xSIiExO5tZQ8QTr7obFiJWGk3U4e7afFOJMQJDszqU/3Pk5jtKiaSEg== elliptic@6.5.4, elliptic@^6.5.3: version "6.5.4" @@ -5720,10 +5720,10 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc" - integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew== +typescript@^4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" + integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== union-value@^1.0.0: version "1.0.1"