From c7b59e6489de56eb0de234321e2b1d166cd3c710 Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Thu, 2 Nov 2023 15:16:52 -0700 Subject: [PATCH 1/3] value is an ScVal, not an event :facepalm: --- src/soroban/parsers.ts | 2 +- src/soroban/soroban_rpc.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/soroban/parsers.ts b/src/soroban/parsers.ts index 429030903..739bc3bcc 100644 --- a/src/soroban/parsers.ts +++ b/src/soroban/parsers.ts @@ -27,7 +27,7 @@ export function parseRawEvents( ...evt, contractId: new Contract(evt.contractId), topic: evt.topic.map((topic) => xdr.ScVal.fromXDR(topic, 'base64')), - value: xdr.DiagnosticEvent.fromXDR(evt.value.xdr, 'base64') + value: xdr.ScVal.fromXDR(evt.value.xdr, 'base64') }; }) }; diff --git a/src/soroban/soroban_rpc.ts b/src/soroban/soroban_rpc.ts index bb1bbd3e6..3a0278d64 100644 --- a/src/soroban/soroban_rpc.ts +++ b/src/soroban/soroban_rpc.ts @@ -146,7 +146,7 @@ export namespace Api { interface EventResponse extends BaseEventResponse { contractId: Contract; topic: xdr.ScVal[]; - value: xdr.DiagnosticEvent; + value: xdr.ScVal; } export interface RawGetEventsResponse { From ebe042366afa47b4fb4e3f37e504a3e5b41044ee Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Thu, 2 Nov 2023 15:48:57 -0700 Subject: [PATCH 2/3] Actually run tests and actually test things :facepalm: --- test/unit/contract_spec.js | 428 ++++++++++---------- test/unit/server/soroban/get_events_test.js | 83 ++-- 2 files changed, 264 insertions(+), 247 deletions(-) diff --git a/test/unit/contract_spec.js b/test/unit/contract_spec.js index b73e03b38..a16fbd30f 100644 --- a/test/unit/contract_spec.js +++ b/test/unit/contract_spec.js @@ -6,239 +6,251 @@ const publicKey = "GCBVOLOM32I7OD5TWZQCIXCXML3TK56MDY7ZMTAILIBQHHKPCVU42XYW"; const addr = Address.fromString(publicKey); let SPEC; before(() => { - SPEC = new ContractSpec(spec); + SPEC = new ContractSpec(spec); }); it("throws if no entries", () => { - expect(() => new ContractSpec([])).to.throw(/Contract spec must have at least one entry/i); + expect(() => new ContractSpec([])).to.throw( + /Contract spec must have at least one entry/i, + ); }); describe("Can round trip custom types", function () { - function getResultType(funcName) { - let fn = SPEC.findEntry(funcName).value(); - if (!(fn instanceof xdr.ScSpecFunctionV0)) { - throw new Error("Not a function"); - } - if (fn.outputs().length === 0) { - return xdr.ScSpecTypeDef.scSpecTypeVoid(); - } - return fn.outputs()[0]; + function getResultType(funcName) { + let fn = SPEC.findEntry(funcName).value(); + if (!(fn instanceof xdr.ScSpecFunctionV0)) { + throw new Error("Not a function"); } - function roundtrip(funcName, input, typeName) { - let type = getResultType(funcName); - let ty = typeName ?? funcName; - let obj = {}; - obj[ty] = input; - let scVal = SPEC.funcArgsToScVals(funcName, obj)[0]; - let result = SPEC.scValToNative(scVal, type); - expect(result).deep.equal(input); + if (fn.outputs().length === 0) { + return xdr.ScSpecTypeDef.scSpecTypeVoid(); } - it("u32", () => { - roundtrip("u32_", 1); + return fn.outputs()[0]; + } + function roundtrip(funcName, input, typeName) { + let type = getResultType(funcName); + let ty = typeName ?? funcName; + let obj = {}; + obj[ty] = input; + let scVal = SPEC.funcArgsToScVals(funcName, obj)[0]; + let result = SPEC.scValToNative(scVal, type); + expect(result).deep.equal(input); + } + it("u32", () => { + roundtrip("u32_", 1); + }); + it("i32", () => { + roundtrip("i32_", -1); + }); + it("i64", () => { + roundtrip("i64_", 1n); + }); + it("strukt", () => { + roundtrip("strukt", { a: 0, b: true, c: "hello" }); + }); + describe("simple", () => { + it("first", () => { + const simple = { tag: "First", values: undefined }; + roundtrip("simple", simple); }); - it("i32", () => { - roundtrip("i32_", -1); + it("simple second", () => { + const simple = { tag: "Second", values: undefined }; + roundtrip("simple", simple); }); - it("i64", () => { - roundtrip("i64_", 1n); + it("simple third", () => { + const simple = { tag: "Third", values: undefined }; + roundtrip("simple", simple); }); - it("strukt", () => { - roundtrip("strukt", { a: 0, b: true, c: "hello" }); - }); - describe("simple", () => { - it("first", () => { - const simple = { tag: "First", values: undefined }; - roundtrip("simple", simple); - }); - it("simple second", () => { - const simple = { tag: "Second", values: undefined }; - roundtrip("simple", simple); - }); - it("simple third", () => { - const simple = { tag: "Third", values: undefined }; - roundtrip("simple", simple); - }); - }); - describe("complex", () => { - it("struct", () => { - const complex = { - tag: "Struct", - values: [{ a: 0, b: true, c: "hello" }], - }; - roundtrip("complex", complex); - }); - it("tuple", () => { - const complex = { - tag: "Tuple", - values: [ - [ - { a: 0, b: true, c: "hello" }, - { tag: "First", values: undefined }, - ], - ], - }; - roundtrip("complex", complex); - }); - it("enum", () => { - const complex = { - tag: "Enum", - values: [{ tag: "First", values: undefined }], - }; - roundtrip("complex", complex); - }); - it("asset", () => { - const complex = { tag: "Asset", values: [addr, 1n] }; - roundtrip("complex", complex); - }); - it("void", () => { - const complex = { tag: "Void", values: undefined }; - roundtrip("complex", complex); - }); - }); - it("addresse", () => { - roundtrip("addresse", addr); - }); - it("bytes", () => { - const bytes = Buffer.from("hello"); - roundtrip("bytes", bytes); - }); - it("bytes_n", () => { - const bytes_n = Buffer.from("123456789"); // what's the correct way to construct bytes_n? - roundtrip("bytes_n", bytes_n); - }); - it("card", () => { - const card = 11; - roundtrip("card", card); - }); - it("boolean", () => { - roundtrip("boolean", true); - }); - it("not", () => { - roundtrip("boolean", false); - }); - it("i128", () => { - roundtrip("i128", -1n); - }); - it("u128", () => { - roundtrip("u128", 1n); - }); - it("map", () => { - const map = new Map(); - map.set(1, true); - map.set(2, false); - roundtrip("map", map); - map.set(3, "hahaha"); - expect(() => roundtrip("map", map)).to.throw(/invalid type scSpecTypeBool specified for string value/i); - }); - it("vec", () => { - const vec = [1, 2, 3]; - roundtrip("vec", vec); + }); + describe("complex", () => { + it("struct", () => { + const complex = { + tag: "Struct", + values: [{ a: 0, b: true, c: "hello" }], + }; + roundtrip("complex", complex); }); it("tuple", () => { - const tuple = ["hello", 1]; - roundtrip("tuple", tuple); - }); - it("option", () => { - roundtrip("option", 1); - roundtrip("option", undefined); - }); - it("u256", () => { - roundtrip("u256", 1n); - expect(() => roundtrip("u256", -1n)).to.throw(/expected a positive value, got: -1/i); + const complex = { + tag: "Tuple", + values: [ + [ + { a: 0, b: true, c: "hello" }, + { tag: "First", values: undefined }, + ], + ], + }; + roundtrip("complex", complex); }); - it("i256", () => { - roundtrip("i256", -1n); + it("enum", () => { + const complex = { + tag: "Enum", + values: [{ tag: "First", values: undefined }], + }; + roundtrip("complex", complex); }); - it("string", () => { - roundtrip("string", "hello"); + it("asset", () => { + const complex = { tag: "Asset", values: [addr, 1n] }; + roundtrip("complex", complex); }); - it("tuple_strukt", () => { - const arg = [ - { a: 0, b: true, c: "hello" }, - { tag: "First", values: undefined }, - ]; - roundtrip("tuple_strukt", arg); + it("void", () => { + const complex = { tag: "Void", values: undefined }; + roundtrip("complex", complex); }); + }); + it("addresse", () => { + roundtrip("addresse", addr); + }); + it("bytes", () => { + const bytes = Buffer.from("hello"); + roundtrip("bytes", bytes); + }); + it("bytes_n", () => { + const bytes_n = Buffer.from("123456789"); // what's the correct way to construct bytes_n? + roundtrip("bytes_n", bytes_n); + }); + it("card", () => { + const card = 11; + roundtrip("card", card); + }); + it("boolean", () => { + roundtrip("boolean", true); + }); + it("not", () => { + roundtrip("boolean", false); + }); + it("i128", () => { + roundtrip("i128", -1n); + }); + it("u128", () => { + roundtrip("u128", 1n); + }); + it("map", () => { + const map = new Map(); + map.set(1, true); + map.set(2, false); + roundtrip("map", map); + map.set(3, "hahaha"); + expect(() => roundtrip("map", map)).to.throw( + /invalid type scSpecTypeBool specified for string value/i, + ); + }); + it("vec", () => { + const vec = [1, 2, 3]; + roundtrip("vec", vec); + }); + it("tuple", () => { + const tuple = ["hello", 1]; + roundtrip("tuple", tuple); + }); + it("option", () => { + roundtrip("option", 1); + roundtrip("option", undefined); + }); + it("u256", () => { + roundtrip("u256", 1n); + expect(() => roundtrip("u256", -1n)).to.throw( + /expected a positive value, got: -1/i, + ); + }); + it("i256", () => { + roundtrip("i256", -1n); + }); + it("string", () => { + roundtrip("string", "hello"); + }); + it("tuple_strukt", () => { + const arg = [ + { a: 0, b: true, c: "hello" }, + { tag: "First", values: undefined }, + ]; + roundtrip("tuple_strukt", arg); + }); }); describe("parsing and building ScVals", function () { - it("Can parse entries", function () { - let spec = new ContractSpec([GIGA_MAP, func]); - let fn = spec.findEntry("giga_map"); - let gigaMap = spec.findEntry("GigaMap"); - expect(gigaMap).deep.equal(GIGA_MAP); - expect(fn).deep.equal(func); - }); + it("Can parse entries", function () { + let spec = new ContractSpec([GIGA_MAP, func]); + let fn = spec.findEntry("giga_map"); + let gigaMap = spec.findEntry("GigaMap"); + expect(gigaMap).deep.equal(GIGA_MAP); + expect(fn).deep.equal(func); + }); }); -export const GIGA_MAP = xdr.ScSpecEntry.scSpecEntryUdtStructV0(new xdr.ScSpecUdtStructV0({ +export const GIGA_MAP = xdr.ScSpecEntry.scSpecEntryUdtStructV0( + new xdr.ScSpecUdtStructV0({ doc: "This is a kitchen sink of all the types", lib: "", name: "GigaMap", fields: [ - new xdr.ScSpecUdtStructFieldV0({ - doc: "", - name: "bool", - type: xdr.ScSpecTypeDef.scSpecTypeBool(), - }), - new xdr.ScSpecUdtStructFieldV0({ - doc: "", - name: "i128", - type: xdr.ScSpecTypeDef.scSpecTypeI128(), - }), - new xdr.ScSpecUdtStructFieldV0({ - doc: "", - name: "u128", - type: xdr.ScSpecTypeDef.scSpecTypeU128(), - }), - new xdr.ScSpecUdtStructFieldV0({ - doc: "", - name: "i256", - type: xdr.ScSpecTypeDef.scSpecTypeI256(), - }), - new xdr.ScSpecUdtStructFieldV0({ - doc: "", - name: "u256", - type: xdr.ScSpecTypeDef.scSpecTypeU256(), - }), - new xdr.ScSpecUdtStructFieldV0({ - doc: "", - name: "i32", - type: xdr.ScSpecTypeDef.scSpecTypeI32(), - }), - new xdr.ScSpecUdtStructFieldV0({ - doc: "", - name: "u32", - type: xdr.ScSpecTypeDef.scSpecTypeU32(), - }), - new xdr.ScSpecUdtStructFieldV0({ - doc: "", - name: "i64", - type: xdr.ScSpecTypeDef.scSpecTypeI64(), - }), - new xdr.ScSpecUdtStructFieldV0({ - doc: "", - name: "u64", - type: xdr.ScSpecTypeDef.scSpecTypeU64(), - }), - new xdr.ScSpecUdtStructFieldV0({ - doc: "", - name: "symbol", - type: xdr.ScSpecTypeDef.scSpecTypeSymbol(), - }), - new xdr.ScSpecUdtStructFieldV0({ - doc: "", - name: "string", - type: xdr.ScSpecTypeDef.scSpecTypeString(), - }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "bool", + type: xdr.ScSpecTypeDef.scSpecTypeBool(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "i128", + type: xdr.ScSpecTypeDef.scSpecTypeI128(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "u128", + type: xdr.ScSpecTypeDef.scSpecTypeU128(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "i256", + type: xdr.ScSpecTypeDef.scSpecTypeI256(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "u256", + type: xdr.ScSpecTypeDef.scSpecTypeU256(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "i32", + type: xdr.ScSpecTypeDef.scSpecTypeI32(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "u32", + type: xdr.ScSpecTypeDef.scSpecTypeU32(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "i64", + type: xdr.ScSpecTypeDef.scSpecTypeI64(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "u64", + type: xdr.ScSpecTypeDef.scSpecTypeU64(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "symbol", + type: xdr.ScSpecTypeDef.scSpecTypeSymbol(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "string", + type: xdr.ScSpecTypeDef.scSpecTypeString(), + }), ], -})); -const GIGA_MAP_TYPE = xdr.ScSpecTypeDef.scSpecTypeUdt(new xdr.ScSpecTypeUdt({ name: "GigaMap" })); -let func = xdr.ScSpecEntry.scSpecEntryFunctionV0(new xdr.ScSpecFunctionV0({ + }), +); +const GIGA_MAP_TYPE = xdr.ScSpecTypeDef.scSpecTypeUdt( + new xdr.ScSpecTypeUdt({ name: "GigaMap" }), +); +let func = xdr.ScSpecEntry.scSpecEntryFunctionV0( + new xdr.ScSpecFunctionV0({ doc: "Kitchen Sink", name: "giga_map", inputs: [ - new xdr.ScSpecFunctionInputV0({ - doc: "", - name: "giga_map", - type: GIGA_MAP_TYPE, - }), + new xdr.ScSpecFunctionInputV0({ + doc: "", + name: "giga_map", + type: GIGA_MAP_TYPE, + }), ], outputs: [GIGA_MAP_TYPE], -})); + }), +); diff --git a/test/unit/server/soroban/get_events_test.js b/test/unit/server/soroban/get_events_test.js index 57c6a5241..83212dd02 100644 --- a/test/unit/server/soroban/get_events_test.js +++ b/test/unit/server/soroban/get_events_test.js @@ -1,4 +1,4 @@ -const { SorobanRpc } = StellarSdk; +const { nativeToScVal, SorobanRpc } = StellarSdk; const { Server, AxiosClient } = StellarSdk.SorobanRpc; describe("Server#getEvents", function () { @@ -40,6 +40,7 @@ describe("Server#getEvents", function () { latestLedger: 1, events: filterEvents(getEventsResponseFixture, "*/*"), }; + expect(result.events).to.not.have.lengthOf(0, JSON.stringify(result)); setupMock( this.axiosMock, @@ -76,9 +77,10 @@ describe("Server#getEvents", function () { latestLedger: 1, events: filterEvents( getEventsResponseFixture, - "AAAABQAAAAh0cmFuc2Zlcg==/AAAAAQB6Mcc=", + `${topicVals[0]}/${topicVals[1]}`, ), }; + expect(result.events).to.not.have.lengthOf(0, JSON.stringify(result)); setupMock( this.axiosMock, @@ -86,7 +88,7 @@ describe("Server#getEvents", function () { startLedger: "1", filters: [ { - topics: [["AAAABQAAAAh0cmFuc2Zlcg==", "AAAAAQB6Mcc="]], + topics: [[topicVals[0], topicVals[1]]], }, ], pagination: {}, @@ -99,12 +101,12 @@ describe("Server#getEvents", function () { startLedger: 1, filters: [ { - topics: [["AAAABQAAAAh0cmFuc2Zlcg==", "AAAAAQB6Mcc="]], + topics: [[topicVals[0], topicVals[1]]], }, ], }) .then(function (response) { - expect(response).to.be.deep.equal(result); + expect(response).to.be.deep.equal(parseEvents(result)); done(); }) .catch(done); @@ -112,20 +114,21 @@ describe("Server#getEvents", function () { it("can build mixed filters", function (done) { let result = { - latestLedger: 1, + latestLedger: 3, events: filterEventsByLedger( - filterEvents(getEventsResponseFixture, "AAAABQAAAAh0cmFuc2Zlcg==/*"), - 1, + filterEvents(getEventsResponseFixture, `${topicVals[0]}/*`), + 2, ), }; + expect(result.events).to.not.have.lengthOf(0, JSON.stringify(result)); setupMock( this.axiosMock, { - startLedger: "1", + startLedger: "2", filters: [ { - topics: [["AAAABQAAAAh0cmFuc2Zlcg==", "*"]], + topics: [[topicVals[0], "*"]], }, ], pagination: {}, @@ -135,15 +138,15 @@ describe("Server#getEvents", function () { this.server .getEvents({ - startLedger: 1, + startLedger: 2, filters: [ { - topics: [["AAAABQAAAAh0cmFuc2Zlcg==", "*"]], + topics: [[topicVals[0], "*"]], }, ], }) .then(function (response) { - expect(response).to.be.deep.equal(result); + expect(response).to.be.deep.equal(parseEvents(result)); done(); }) .catch(done); @@ -151,12 +154,13 @@ describe("Server#getEvents", function () { it("can paginate", function (done) { let result = { - latestLedger: 1, + latestLedger: 3, events: filterEventsByLedger( filterEvents(getEventsResponseFixture, "*/*"), - 1, + 2, ), }; + expect(result.events).to.not.have.lengthOf(0, JSON.stringify(result)); setupMock( this.axiosMock, @@ -185,7 +189,7 @@ describe("Server#getEvents", function () { limit: 10, }) .then(function (response) { - expect(response).to.be.deep.equal(result); + expect(response).to.be.deep.equal(parseEvents(result)); done(); }) .catch(done); @@ -193,16 +197,17 @@ describe("Server#getEvents", function () { }); function filterEvents(events, filter) { + const parts = filter.split("/"); return events.filter( (e, i) => - e.topic.length == filter.length && - e.topic.every((s, j) => s === filter[j] || s === "*"), + e.topic.length == parts.length && + e.topic.every((s, j) => s === parts[j] || parts[j] === "*"), ); } function filterEventsByLedger(events, start) { return events.filter((e) => { - return e.ledger.parseInt() >= start; + return parseInt(e.ledger) >= start; }); } @@ -219,67 +224,67 @@ function setupMock(axiosMock, params, result) { } function parseEvents(result) { - return { - ...result, - events: result.events.map(SorobanRpc.parseRawEvents), - }; + return SorobanRpc.parseRawEvents(result); } +const contractId = "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"; +const topicVals = [ + nativeToScVal("transfer", { type: "symbol" }).toXDR("base64"), + nativeToScVal(contractId, { type: "address" }).toXDR("base64"), + nativeToScVal(1234).toXDR("base64"), +]; +let eventVal = nativeToScVal("wassup").toXDR("base64"); let getEventsResponseFixture = [ { type: "system", ledger: "1", ledgerClosedAt: "2022-11-16T16:10:41Z", - contractId: - "e3e82a76cc316f6289fd1ffbdf315da0f2c6be9582b84b9983a402f02ea0fff7", + contractId, id: "0164090849041387521-0000000003", pagingToken: "164090849041387521-3", - topic: ["AAAABQAAAAh0cmFuc2Zlcg==", "AAAAAQB6Mcc="], inSuccessfulContractCall: true, + topic: topicVals.slice(0, 2), value: { - xdr: "AAAABQAAAApHaWJNb255UGxzAAA=", + xdr: eventVal, }, }, { type: "contract", ledger: "2", ledgerClosedAt: "2022-11-16T16:10:41Z", - contractId: - "e3e82a76cc316f6289fd1ffbdf315da0f2c6be9582b84b9983a402f02ea0fff7", + contractId, id: "0164090849041387521-0000000003", pagingToken: "164090849041387521-3", - topic: ["AAAAAQB6Mcc=", "AAAABQAAAAh0cmFuc2Zlcg=="], inSuccessfulContractCall: true, + topic: topicVals.slice(0, 2), value: { - xdr: "AAAABQAAAApHaWJNb255UGxzAAA=", + xdr: eventVal, }, }, { type: "diagnostic", ledger: "2", ledgerClosedAt: "2022-11-16T16:10:41Z", - contractId: - "a3e82a76cc316f6289fd1ffbdf315da0f2c6be9582b84b9983a402f02ea0fff7", + contractId, id: "0164090849041387521-0000000003", pagingToken: "164090849041387521-3", inSuccessfulContractCall: true, - topic: ["AAAAAQB6Mcc="], + topic: [topicVals[0]], value: { - xdr: "AAAABQAAAApHaWJNb255UGxzAAA=", + xdr: eventVal, }, }, { type: "contract", ledger: "3", ledgerClosedAt: "2022-12-14T01:01:20Z", - contractId: - "6ebe0114ae15f72f187f05d06dcb66b22bd97218755c9b4646b034ab961fc1d5", + contractId, id: "0000000171798695936-0000000001", pagingToken: "0000000171798695936-0000000001", inSuccessfulContractCall: true, - topic: ["AAAABQAAAAdDT1VOVEVSAA==", "AAAABQAAAAlpbmNyZW1lbnQAAAA="], + topic: topicVals, value: { - xdr: "AAAAAQAAAAE=", + xdr: eventVal, }, }, ]; From 8501ad58f36854adeecf41b317f19d5c250c398d Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Thu, 2 Nov 2023 15:51:09 -0700 Subject: [PATCH 3/3] Add changelog entry --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d4a25e3..bd4ee2264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ A breaking change will get clearly marked in this log. ## Unreleased ### Fixed -* The `SorobanRpc.Server.getTransaction` method will now return the full response when encountering a `FAILED` transaction result ([#872](https://github.com/stellar/js-stellar-sdk/pull/872)). +* The `SorobanRpc.Server.getTransaction()` method will now return the full response when encountering a `FAILED` transaction result ([#872](https://github.com/stellar/js-stellar-sdk/pull/872)). +* The `SorobanRpc.Server.getEvents()` method will correctly parse the event value (which is an `xdr.ScVal` rather than an `xdr.DiagnosticEvent`, see the modified `SorobanRpc.Api.EventResponse.value`; [#876](https://github.com/stellar/js-stellar-sdk/pull/876)). ## [v11.0.0-beta.5](https://github.com/stellar/js-stellar-sdk/compare/v11.0.0-beta.4...v11.0.0-beta.5)