From 4eedb93aef6fcfaa2e53eac2dc9930e52db61607 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 21 Aug 2023 11:28:10 -0700 Subject: [PATCH] Automatically decode XDR values in `getTransaction` (#129) * Separate response type into status-based interfaces --- CHANGELOG.md | 15 ++- package.json | 2 +- src/server.ts | 101 +++++++++++----- src/soroban_rpc.ts | 43 ++++++- test/unit/server/get_transaction_test.js | 143 +++++++++++++++++++---- yarn.lock | 8 +- 6 files changed, 248 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e1ef740..ad3ad7de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,16 +9,21 @@ A breaking change should be clearly marked in this log. ### Breaking Changes +* The minimum supported NodeJS version is now Node 16. * `Server.prepareTransaction` now returns a `TransactionBuilder` instance rather than an immutable `Transaction`, in order to facilitate modifying your transaction after assembling it alongside the simulation response ([https://github.com/stellar/js-soroban-client/pull/127](#127)). - The intent is to avoid cloning the transaction again (via `TransactionBuilder.cloneFrom`) if you need to modify parameters such as the storage access footprint. - To migrate your code, just call `.build()` on the return value. -* The RPC response schemas for simulation have been upgraded to parse the base64-encoded XDR automatically. The full interface changes are in the pull request ([https://github.com/stellar/js-soroban-client/pull/127](#127)), but succinctly: +* The RPC response schemas for simulation (see `Server.simulateTransaction()`) have been upgraded to parse the base64-encoded XDR automatically. The full interface changes are in the pull request ([https://github.com/stellar/js-soroban-client/pull/127](#127)), but succinctly: - `SimulateTransactionResponse` -> `RawSimulateTransactionResponse` - `SimulateHostFunctionResult` -> `RawSimulateHostFunctionResult` - - Now, `SimulateTransactionResponse` and `SimulateHostFunctionResult` now include the full, decoded XDR structures instead of raw, base64-encoded strings for the relevant fields (e.g. `SimulateTransactionResponse.transactionData` is now an instance of `SorobanDataBuilder`, `events` is now an `xdr.DiagnosticEvent[]` [try out `humanizeEvents` for a friendlier representation of this field]) - - The `SimulateTransactionResponse.results[]` field has been moved to `SimulateTransactionResponse.result?`, since it will always be exactly zero or one result. - -Not all schemas have been broken in this manner in order to facilitate user feedback on this approach. Please add your :+1: or :-1: to [#128](https://github.com/stellar/js-soroban-client/issues/128) to provide your perspective on whether or not we should do this for the other response schemas. + - Now, `SimulateTransactionResponse` and `SimulateHostFunctionResult` include the full, decoded XDR structures instead of raw, base64-encoded strings for the relevant fields (e.g. `SimulateTransactionResponse.transactionData` is now an instance of `SorobanDataBuilder`, `events` is now an `xdr.DiagnosticEvent[]` [try out `humanizeEvents` for a friendlier representation of this field]). + - The `SimulateTransactionResponse.results[]` field has been moved to `SimulateTransactionResponse.result?`, since there will always be exactly zero or one result. +* The RPC response schemas for retrieving transaction details (`Server.getTransaction()`) have been upgraded to parse the base64-encoded XDR automatically. The full interface changes are in the pull request ([https://github.com/stellar/js-soroban-client/pull/129](#129)), but succinctly: + - `GetTransactionResponse` -> `RawGetTransactionResponse` + - All of the `*Xdr` properties are now full, decoded XDR structures. + - There is a new `returnValue` field which is a decoded `xdr.ScVal`, present iff the transaction was a successful Soroban function invocation. + +Not all schemas have been broken in this manner in order to facilitate user feedback on this approach. Please add your :+1: or :-1: to [#128](https://github.com/stellar/js-soroban-client/issues/128) to vote on whether or not we should do this for the other response schemas. ## v0.10.1 diff --git a/package.json b/package.json index 221caf5b..bec4221e 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "@types/chai": "^4.3.5", "@types/detect-node": "^2.0.0", "@types/eventsource": "^1.1.2", - "@types/lodash": "^4.14.192", + "@types/lodash": "^4.14.197", "@types/mocha": "^10.0.1", "@types/node": "^20.4.2", "@types/randombytes": "^2.0.0", diff --git a/src/server.ts b/src/server.ts index ae0b3e45..b93a8f04 100644 --- a/src/server.ts +++ b/src/server.ts @@ -32,8 +32,8 @@ export interface GetEventsRequest { * Specifies the durability namespace of contract-related ledger entries. */ export enum Durability { - Temporary = 'temporary', - Persistent = 'persistent', + Temporary = "temporary", + Persistent = "persistent", } /** @@ -182,7 +182,7 @@ export class Server { ): Promise { // coalesce `contract` param variants to an ScAddress let scAddress: xdr.ScAddress; - if (typeof contract === 'string') { + if (typeof contract === "string") { scAddress = new Contract(contract).address().toScAddress(); } else if (contract instanceof Address) { scAddress = contract.toScAddress(); @@ -211,24 +211,30 @@ export class Server { contract: scAddress, key, durability: xdrDurability, - bodyType: xdr.ContractEntryBodyType.dataEntry() // expirationExtension is internal - }) + bodyType: xdr.ContractEntryBodyType.dataEntry(), // expirationExtension is internal + }), ).toXDR("base64"); - return jsonrpc.post( - this.serverURL.toString(), - "getLedgerEntries", - [contractKey], - ).then(response => { + return jsonrpc + .post( + this.serverURL.toString(), + "getLedgerEntries", + [contractKey], + ) + .then((response) => { const ledgerEntries = response.entries ?? []; - if (ledgerEntries.length !== 1) { - return Promise.reject({ - code: 404, - message: `Contract data not found. Contract: ${Address.fromScAddress(scAddress).toString()}, Key: ${key.toXDR("base64")}, Durability: ${durability}`, - }); - } - return ledgerEntries[0]; - }); + if (ledgerEntries.length !== 1) { + return Promise.reject({ + code: 404, + message: `Contract data not found. Contract: ${Address.fromScAddress( + scAddress, + ).toString()}, Key: ${key.toXDR( + "base64", + )}, Durability: ${durability}`, + }); + } + return ledgerEntries[0]; + }); } /** @@ -287,18 +293,55 @@ export class Server { * * @param {string} hash - The hex-encoded hash of the transaction to check. * - * @returns {Promise} Returns a - * promise to the {@link SorobanRpc.GetTransactionResponse} object - * with the status, result, and other details about the transaction. + * @returns {Promise} Returns a promise to + * the {@link SorobanRpc.GetTransactionResponse} object with the status, + * result, and other details about the transaction. Raw XDR fields are + * parsed into their appropriate structures wherever possible. */ public async getTransaction( hash: string, ): Promise { - return await jsonrpc.post( + const raw = await jsonrpc.post( this.serverURL.toString(), "getTransaction", hash, ); + + let successInfo: Omit< + SorobanRpc.GetSuccessfulTransactionResponse, + keyof SorobanRpc.GetFailedTransactionResponse + > = {} as any; + + if (raw.status === SorobanRpc.GetTransactionStatus.SUCCESS) { + const meta = xdr.TransactionMeta.fromXDR(raw.resultMetaXdr!, "base64"); + successInfo = { + ledger: raw.ledger!, + createdAt: raw.createdAt!, + applicationOrder: raw.applicationOrder!, + feeBump: raw.feeBump!, + envelopeXdr: xdr.TransactionEnvelope.fromXDR( + raw.envelopeXdr!, + "base64", + ), + resultXdr: xdr.TransactionResult.fromXDR(raw.resultXdr!, "base64"), + resultMetaXdr: meta, + ...(meta.switch() === 3 && + meta.v3().sorobanMeta() !== null && { + returnValue: meta.v3().sorobanMeta()?.returnValue(), + }), + }; + } + + const result: SorobanRpc.GetTransactionResponse = { + status: raw.status, + latestLedger: raw.latestLedger, + latestLedgerCloseTime: raw.latestLedgerCloseTime, + oldestLedger: raw.oldestLedger, + oldestLedgerCloseTime: raw.oldestLedgerCloseTime, + ...successInfo, + }; + + return result; } /** @@ -347,8 +390,6 @@ export class Server { // is an ScSymbol and the last is a U32. // // The difficulty comes in matching up the correct integer primitives. - // - // It also means this library will rely on the XDR definitions. return await jsonrpc.postObject(this.serverURL.toString(), "getEvents", { filters: request.filters ?? [], pagination: { @@ -449,11 +490,13 @@ export class Server { public async simulateTransaction( transaction: Transaction | FeeBumpTransaction, ): Promise { - return await jsonrpc.post( - this.serverURL.toString(), - "simulateTransaction", - transaction.toXDR(), - ).then((raw) => parseRawSimulation(raw)); + return await jsonrpc + .post( + this.serverURL.toString(), + "simulateTransaction", + transaction.toXDR(), + ) + .then((raw) => parseRawSimulation(raw)); } /** diff --git a/src/soroban_rpc.ts b/src/soroban_rpc.ts index 2ba796b9..70d272b0 100644 --- a/src/soroban_rpc.ts +++ b/src/soroban_rpc.ts @@ -54,9 +54,48 @@ export namespace SorobanRpc { protocolVersion: string; } - export type GetTransactionStatus = "SUCCESS" | "NOT_FOUND" | "FAILED"; + export enum GetTransactionStatus { + SUCCESS = "SUCCESS", + NOT_FOUND = "NOT_FOUND", + FAILED = "FAILED" + } + + export type GetTransactionResponse = + | GetSuccessfulTransactionResponse + | GetFailedTransactionResponse + | GetMissingTransactionResponse; + + interface GetAnyTransactionResponse { + status: GetTransactionStatus; + latestLedger: number; + latestLedgerCloseTime: number; + oldestLedger: number; + oldestLedgerCloseTime: number; + } + + export interface GetMissingTransactionResponse extends GetAnyTransactionResponse { + status: GetTransactionStatus.NOT_FOUND; + } + + export interface GetFailedTransactionResponse extends GetAnyTransactionResponse { + status: GetTransactionStatus.FAILED; + } + + export interface GetSuccessfulTransactionResponse extends GetAnyTransactionResponse { + status: GetTransactionStatus.SUCCESS; + + ledger: number; + createdAt: number; + applicationOrder: number; + feeBump: boolean; + envelopeXdr: xdr.TransactionEnvelope; + resultXdr: xdr.TransactionResult; + resultMetaXdr: xdr.TransactionMeta; + + returnValue?: xdr.ScVal; // present iff resultMeta is a v3 + } - export interface GetTransactionResponse { + export interface RawGetTransactionResponse { status: GetTransactionStatus; latestLedger: number; latestLedgerCloseTime: number; diff --git a/test/unit/server/get_transaction_test.js b/test/unit/server/get_transaction_test.js index 08472716..0eac532c 100644 --- a/test/unit/server/get_transaction_test.js +++ b/test/unit/server/get_transaction_test.js @@ -1,14 +1,24 @@ +const { + xdr, + Keypair, + Account, + Server, + TransactionBuilder, + nativeToScVal, + XdrLargeInt, +} = SorobanClient; + describe("Server#getTransaction", function () { - let keypair = SorobanClient.Keypair.random(); + let keypair = Keypair.random(); let account = new SorobanClient.Account( keypair.publicKey(), "56199647068161" ); beforeEach(function () { - this.server = new SorobanClient.Server(serverUrl); + this.server = new Server(serverUrl); this.axiosMock = sinon.mock(AxiosClient); - let transaction = new SorobanClient.TransactionBuilder(account, { + let transaction = new TransactionBuilder(account, { fee: 100, networkPassphrase: SorobanClient.Networks.TESTNET, v1: true, @@ -28,6 +38,17 @@ describe("Server#getTransaction", function () { this.transaction = transaction; this.hash = this.transaction.hash().toString("hex"); this.blob = transaction.toEnvelope().toXDR().toString("base64"); + this.prepareAxios = (result) => { + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getTransaction", + params: [this.hash], + }) + .returns(Promise.resolve({ data: { id: 1, result } })); + }; }); afterEach(function () { @@ -36,32 +57,108 @@ describe("Server#getTransaction", function () { }); it("transaction not found", function (done) { - const result = { - status: "NOT_FOUND", - latestLedger: 100, - latestLedgerCloseTime: 12345, - oldestLedger: 50, - oldestLedgerCloseTime: 500, - }; - this.axiosMock - .expects("post") - .withArgs(serverUrl, { - jsonrpc: "2.0", - id: 1, - method: "getTransaction", - params: [this.hash], + const result = makeTxResult("NOT_FOUND"); + this.prepareAxios(result); + + this.server + .getTransaction(this.hash) + .then(function (response) { + expect(response).to.be.deep.equal(result); + done(); }) - .returns(Promise.resolve({ data: { id: 1, result } })); + .catch((err) => done(err)); + }); + + it("transaction success", function (done) { + const result = makeTxResult("SUCCESS", true); + this.prepareAxios(result); - this.server.getTransaction(this.hash).then(function (response) { - expect(response).to.be.deep.equal(result); - done(); + let expected = JSON.parse(JSON.stringify(result)); + [ + ["envelopeXdr", xdr.TransactionEnvelope], + ["resultXdr", xdr.TransactionResult], + ["resultMetaXdr", xdr.TransactionMeta], + ].forEach(([field, struct]) => { + expected[field] = struct.fromXDR(result[field], "base64"); }); + expected.returnValue = expected.resultMetaXdr + .v3() + .sorobanMeta() + .returnValue(); + + this.server + .getTransaction(this.hash) + .then((resp) => { + expect(Object.keys(resp)).to.eql(Object.keys(expected)); + expect(resp).to.eql(expected); + expect(resp.returnValue).to.eql(new XdrLargeInt("u64", 1234).toScVal()); + done(); + }) + .catch((err) => done(err)); }); - xit("transaction pending", function (done) {}); + xit("non-Soroban transaction success", function (done) { + const result = makeTxResult("SUCCESS", false); + this.prepareAxios(result); - xit("transaction success", function (done) {}); + this.server + .getTransaction(this.hash) + .then((resp) => { + expect(resp).to.be.deep.equal(result); + done(); + }) + .catch((err) => done(err)); + }); + xit("transaction pending", function (done) {}); xit("transaction error", function (done) {}); }); + +function makeTxResult(status, addSoroban = true) { + const metaV3 = new xdr.TransactionMeta( + 3, + new xdr.TransactionMetaV3({ + ext: new xdr.ExtensionPoint(0), + txChangesBefore: [], + operations: [], + txChangesAfter: [], + sorobanMeta: new xdr.SorobanTransactionMeta({ + ext: new xdr.ExtensionPoint(0), + events: [], + diagnosticEvents: [], + returnValue: nativeToScVal(1234), + }), + }) + ); + + // only injected in the success case + // + // this data was picked from a random transaction in horizon: + // aa6a8e198abe53c7e852e4870413b29fe9ef04da1415a97a5de1a4ae489e11e2 + const successInfo = { + ledger: 1234, + createdAt: 123456789010, + applicationOrder: 2, + feeBump: false, + envelopeXdr: + "AAAAAgAAAAAT/LQZdYz0FcQ4Xwyg8IM17rkUx3pPCCWLu+SowQ/T+gBLB24poiQa9iwAngAAAAEAAAAAAAAAAAAAAABkwdeeAAAAAAAAAAEAAAABAAAAAC/9E8hDhnktyufVBS5tqA734Yz5XrLX2XNgBgH/YEkiAAAADQAAAAAAAAAAAAA1/gAAAAAv/RPIQ4Z5Lcrn1QUubagO9+GM+V6y19lzYAYB/2BJIgAAAAAAAAAAAAA1/gAAAAQAAAACU0lMVkVSAAAAAAAAAAAAAFDutWuu6S6UPJBrotNSgfmXa27M++63OT7TYn1qjgy+AAAAAVNHWAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AAAACUEFMTEFESVVNAAAAAAAAAFDutWuu6S6UPJBrotNSgfmXa27M++63OT7TYn1qjgy+AAAAAlNJTFZFUgAAAAAAAAAAAABQ7rVrrukulDyQa6LTUoH5l2tuzPvutzk+02J9ao4MvgAAAAAAAAACwQ/T+gAAAEA+ztVEKWlqHXNnqy6FXJeHr7TltHzZE6YZm5yZfzPIfLaqpp+5cyKotVkj3d89uZCQNsKsZI48uoyERLne+VwL/2BJIgAAAEA7323gPSaezVSa7Vi0J4PqsnklDH1oHLqNBLwi5EWo5W7ohLGObRVQZ0K0+ufnm4hcm9J4Cuj64gEtpjq5j5cM", + resultXdr: + "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAANAAAAAAAAAAUAAAACZ4W6fmN63uhVqYRcHET+D2NEtJvhCIYflFh9GqtY+AwAAAACU0lMVkVSAAAAAAAAAAAAAFDutWuu6S6UPJBrotNSgfmXa27M++63OT7TYn1qjgy+AAAYW0toL2gAAAAAAAAAAAAANf4AAAACcgyAkXD5kObNTeRYciLh7R6ES/zzKp0n+cIK3Y6TjBkAAAABU0dYAAAAAABQ7rVrrukulDyQa6LTUoH5l2tuzPvutzk+02J9ao4MvgAAGlGnIJrXAAAAAlNJTFZFUgAAAAAAAAAAAABQ7rVrrukulDyQa6LTUoH5l2tuzPvutzk+02J9ao4MvgAAGFtLaC9oAAAAApmc7UgUBInrDvij8HMSridx2n1w3I8TVEn4sLr1LSpmAAAAAlBBTExBRElVTQAAAAAAAABQ7rVrrukulDyQa6LTUoH5l2tuzPvutzk+02J9ao4MvgAAIUz88EqYAAAAAVNHWAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AABpRpyCa1wAAAAKYUsaaCZ233xB1p+lG7YksShJWfrjsmItbokiR3ifa0gAAAAJTSUxWRVIAAAAAAAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AABv52PPa5wAAAAJQQUxMQURJVU0AAAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AACFM/PBKmAAAAAJnhbp+Y3re6FWphFwcRP4PY0S0m+EIhh+UWH0aq1j4DAAAAAAAAAAAAAA9pAAAAAJTSUxWRVIAAAAAAAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AABv52PPa5wAAAAAv/RPIQ4Z5Lcrn1QUubagO9+GM+V6y19lzYAYB/2BJIgAAAAAAAAAAAAA9pAAAAAA=", + resultMetaXdr: metaV3.toXDR("base64"), + }; + + if (!addSoroban) { + // replace the V3 Soroban meta with a "classic" V2 version + successInfo.resultMetaXdr = + "AAAAAgAAAAIAAAADAtL5awAAAAAAAAAAS0CFMhOtWUKJWerx66zxkxORaiH6/3RUq7L8zspD5RoAAAAAAcm9QAKVkpMAAHpMAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAC0vi5AAAAAGTB02oAAAAAAAAAAQLS+WsAAAAAAAAAAEtAhTITrVlCiVnq8eus8ZMTkWoh+v90VKuy/M7KQ+UaAAAAAAHJvUAClZKTAAB6TQAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAtL5awAAAABkwdd1AAAAAAAAAAEAAAAGAAAAAwLS+VQAAAACAAAAAG4cwu71zHNXx3jHCzRGOIthcnfwRgfN2f/AoHFLLMclAAAAAEySDkgAAAAAAAAAAkJVU0lORVNTAAAAAAAAAAC3JfDeo9vreItKNPoe74EkFIqWybeUQNFvLvURhHtskAAAAAAeQtHTL5f6TAAAXH0AAAAAAAAAAAAAAAAAAAABAtL5awAAAAIAAAAAbhzC7vXMc1fHeMcLNEY4i2Fyd/BGB83Z/8CgcUssxyUAAAAATJIOSAAAAAAAAAACQlVTSU5FU1MAAAAAAAAAALcl8N6j2+t4i0o0+h7vgSQUipbJt5RA0W8u9RGEe2yQAAAAAB5C0dNHf4CAAACLCQAAAAAAAAAAAAAAAAAAAAMC0vlUAAAAAQAAAABuHMLu9cxzV8d4xws0RjiLYXJ38EYHzdn/wKBxSyzHJQAAAAJCVVNJTkVTUwAAAAAAAAAAtyXw3qPb63iLSjT6Hu+BJBSKlsm3lEDRby71EYR7bJAAAAAAAABAL3//////////AAAAAQAAAAEAE3H3TnhnuQAAAAAAAAAAAAAAAAAAAAAAAAABAtL5awAAAAEAAAAAbhzC7vXMc1fHeMcLNEY4i2Fyd/BGB83Z/8CgcUssxyUAAAACQlVTSU5FU1MAAAAAAAAAALcl8N6j2+t4i0o0+h7vgSQUipbJt5RA0W8u9RGEe2yQAAAAAAAAQC9//////////wAAAAEAAAABABNx9J6Z4RkAAAAAAAAAAAAAAAAAAAAAAAAAAwLS+WsAAAAAAAAAAG4cwu71zHNXx3jHCzRGOIthcnfwRgfN2f/AoHFLLMclAAAAH37+zXQCXdRTAAASZAAAApIAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAABbBXKIigAAABhZWyiOAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAtL0awAAAABkwbqrAAAAAAAAAAEC0vlrAAAAAAAAAABuHMLu9cxzV8d4xws0RjiLYXJ38EYHzdn/wKBxSyzHJQAAAB9+/s10Al3UUwAAEmQAAAKSAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAWwVyiIoAAAAYWVsojgAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAALS9GsAAAAAZMG6qwAAAAAAAAAA"; + } + + return { + status, + latestLedger: 100, + latestLedgerCloseTime: 12345, + oldestLedger: 50, + oldestLedgerCloseTime: 500, + ...(status === "SUCCESS" && successInfo), + }; +} diff --git a/yarn.lock b/yarn.lock index 4812db0a..4d390142 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1469,10 +1469,10 @@ resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9" integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA== -"@types/lodash@^4.14.192": - version "4.14.196" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.196.tgz#a7c3d6fc52d8d71328b764e28e080b4169ec7a95" - integrity sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ== +"@types/lodash@^4.14.197": + version "4.14.197" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.197.tgz#e95c5ddcc814ec3e84c891910a01e0c8a378c54b" + integrity sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g== "@types/markdown-it@^12.2.3": version "12.2.3"