diff --git a/README.md b/README.md index b8cb8b63..4d2a8094 100644 --- a/README.md +++ b/README.md @@ -572,10 +572,11 @@ typewriter.state: StorageView; `typewriter.state` resolves the top-level Solidity `state` variable by one proxy layer. It reads the runtime's local revm-backed storage mirror, so accepted state -is visible before onchain inclusion and leaf accesses return promises. +is visible before onchain inclusion. Leaf accesses are synchronous and return +values. ```ts -const balance = await typewriter.state.balances[accountID]; +const balance = typewriter.state.balances[accountID]; ``` `typewriter.state` reflects locally accepted state, which can be ahead of what is `included` or `finalized` onchain. diff --git a/apps/order-book/src/index.ts b/apps/order-book/src/index.ts index 24e52cd0..83d0af37 100644 --- a/apps/order-book/src/index.ts +++ b/apps/order-book/src/index.ts @@ -80,9 +80,9 @@ function json(value: unknown, init?: ResponseInit): Response { }); } -async function accountCredentials(account: Hex) { +function accountCredentials(account: Hex) { const credentials = app.accounts[account].credentials; - const length = await credentials.length; + const length = credentials.length; const out: { expiration: number; keyType: number; @@ -91,29 +91,23 @@ async function accountCredentials(account: Hex) { }[] = []; for (let i = 0; i < length; i++) { const credential = credentials[i]; - const [expiration, keyType, permissions, publicKey] = await Promise.all([ - credential.expiration, - credential.keyType, - credential.permissions, - credential.publicKey, - ]); out.push({ - expiration: Number(expiration), - keyType: Number(keyType), - permissions: String(permissions), - publicKey: publicKey as Hex, + expiration: Number(credential.expiration), + keyType: Number(credential.keyType), + permissions: String(credential.permissions), + publicKey: credential.publicKey as Hex, }); } return out; } -async function accountExists(account: Hex): Promise { - return (await app.accounts[account].credentials.length) > 0; +function accountExists(account: Hex): boolean { + return app.accounts[account].credentials.length > 0; } -async function accountOrders(account: Hex, instrumentId?: number) { +function accountOrders(account: Hex, instrumentId?: number) { const orders = app.state.accounts[account].orders; - const length = await orders.length; + const length = orders.length; const out: { orderId: number; quantity: string; @@ -124,14 +118,7 @@ async function accountOrders(account: Hex, instrumentId?: number) { }[] = []; for (let i = 0; i < length; i++) { const order = orders[i]; - const [quantity, orderInstrumentId, price, tickVolume, side] = - await Promise.all([ - order.quantity, - order.instrumentId, - order.price, - order.tickVolume, - order.side, - ]); + const orderInstrumentId = order.instrumentId; if ( instrumentId !== undefined && Number(orderInstrumentId) !== instrumentId @@ -140,53 +127,48 @@ async function accountOrders(account: Hex, instrumentId?: number) { } out.push({ orderId: i, - quantity: String(quantity), + quantity: String(order.quantity), instrumentId: Number(orderInstrumentId), - price: String(price), - tickVolume: Number(tickVolume), - side: Number(side) as 0 | 1, + price: String(order.price), + tickVolume: Number(order.tickVolume), + side: Number(order.side) as 0 | 1, }); } return out; } -async function accountBalances(account: Hex): Promise> { +function accountBalances(account: Hex): Record { const balanceProxy = app.state.accounts[account].balances; const assets = Object.keys(balanceProxy) as Address[]; const balances: Record = {}; for (const asset of assets) { - balances[asset] = String(await balanceProxy[asset]); + balances[asset] = String(balanceProxy[asset]); } return balances; } -async function accountNonces(account: Hex): Promise> { +function accountNonces(account: Hex): Record { const nonceProxy = app.accounts[account].nonces; const keys = Object.keys(nonceProxy); const nonces: Record = {}; for (const key of keys) { - nonces[key] = String(await nonceProxy[key as `${number}`]); + nonces[key] = String(nonceProxy[key as `${number}`]); } return nonces; } -async function getPrices( - instrumentId: number, - side: "bids" | "asks", -): Promise { +function getPrices(instrumentId: number, side: "bids" | "asks"): number[] { const instrument = app.state.instruments[`${instrumentId}`]; const prices: number[] = []; - let price = Number( - await (side === "asks" ? instrument.bestAsk : instrument.bestBid), - ); + let price = Number(side === "asks" ? instrument.bestAsk : instrument.bestBid); while (price !== 0) { prices.push(price); - price = Number(await instrument[side][`${price}`].next); + price = Number(instrument[side][`${price}`].next); } return prices; } -async function summarizePrice( +function summarizePrice( instrumentId: number, bidPrices: number[], askPrices: number[], @@ -196,7 +178,7 @@ async function summarizePrice( for (const price of bidPrices) { const remainingQuantity = - await app.state.instruments[`${instrumentId}`].bids[`${price}`] + app.state.instruments[`${instrumentId}`].bids[`${price}`] .remainingQuantity; if (remainingQuantity > 0n) { bestBid = price; @@ -205,7 +187,7 @@ async function summarizePrice( } for (const price of askPrices) { const remainingQuantity = - await app.state.instruments[`${instrumentId}`].asks[`${price}`] + app.state.instruments[`${instrumentId}`].asks[`${price}`] .remainingQuantity; if (remainingQuantity > 0n) { bestAsk = price; @@ -226,24 +208,22 @@ async function summarizePrice( }; } -async function tickAt( +function tickAt( instrumentId: number, side: "bids" | "asks", priceQ32: number, -): Promise<{ +): { quantity: string; remainingQuantity: string; volume: number; -} | null> { +} | null { const tick = app.state.instruments[String(instrumentId) as `${number}`][side][ String(priceQ32) as `${number}` ]; - const [quantity, remainingQuantity, volume] = await Promise.all([ - tick.quantity, - tick.remainingQuantity, - tick.volume, - ]); + const quantity = tick.quantity; + const remainingQuantity = tick.remainingQuantity; + const volume = tick.volume; if (quantity === 0n && remainingQuantity === 0n && Number(volume) === 0) { return null; } @@ -347,12 +327,12 @@ serve({ }, }, "/api/account/:id/orders": { - GET: async (req) => { + GET: (req) => { const account = req.params.id as Hex; if (account === null) { return json({ error: "Invalid account address" }, { status: 400 }); } - if (!(await accountExists(account))) { + if (!accountExists(account)) { return json({ error: "account not found" }, { status: 404 }); } const instrumentIdParam = new URL(req.url).searchParams.get( @@ -362,16 +342,16 @@ serve({ instrumentIdParam !== null && /^\d+$/.test(instrumentIdParam) ? Number(instrumentIdParam) : undefined; - return json({ orders: await accountOrders(account, instrumentId) }); + return json({ orders: accountOrders(account, instrumentId) }); }, }, "/api/account/:id/exists": { - GET: async (req) => { + GET: (req) => { const account = req.params.id as Hex; if (account === null) { return json({ exists: false, hasKeys: false }); } - const exists = await accountExists(account); + const exists = accountExists(account); return json({ exists, hasKeys: exists }); }, }, @@ -381,22 +361,19 @@ serve({ if (account === null) { return json({ error: "Invalid account address" }, { status: 400 }); } - if (!(await accountExists(account))) { + if (!accountExists(account)) { return json({ error: "account not found" }, { status: 404 }); } - const [credentials, nonces, orders, balances, mutations] = - await Promise.all([ - accountCredentials(account), - accountNonces(account), - accountOrders(account), - accountBalances(account), - selectMutationsByAccount( - readerDb, - app.schema, - account, - ACCOUNT_MUTATION_HISTORY_LIMIT, - ), - ]); + const credentials = accountCredentials(account); + const nonces = accountNonces(account); + const orders = accountOrders(account); + const balances = accountBalances(account); + const mutations = await selectMutationsByAccount( + readerDb, + app.schema, + account, + ACCOUNT_MUTATION_HISTORY_LIMIT, + ); return json({ address: account, credentials, @@ -408,7 +385,7 @@ serve({ }, }, "/api/balances": { - GET: async (req) => { + GET: (req) => { const account = new URL(req.url).searchParams.get( "account", ) as Hex | null; @@ -418,15 +395,15 @@ serve({ { status: 400 }, ); } - if (!(await accountExists(account))) { + if (!accountExists(account)) { return json({ error: "account not found" }, { status: 404 }); } - const balances = await accountBalances(account); + const balances = accountBalances(account); return json({ account, balances }); }, }, "/api/price": { - GET: async (req) => { + GET: (req) => { const instrumentIdParam = new URL(req.url).searchParams.get( "instrumentId", ); @@ -437,13 +414,13 @@ serve({ ); } const instrumentId = Number(instrumentIdParam); - const bidPrices = await getPrices(instrumentId, "bids"); - const askPrices = await getPrices(instrumentId, "asks"); - return json(await summarizePrice(instrumentId, bidPrices, askPrices)); + const bidPrices = getPrices(instrumentId, "bids"); + const askPrices = getPrices(instrumentId, "asks"); + return json(summarizePrice(instrumentId, bidPrices, askPrices)); }, }, "/api/depth": { - GET: async (req) => { + GET: (req) => { const instrumentIdParam = new URL(req.url).searchParams.get( "instrumentId", ); @@ -454,13 +431,9 @@ serve({ ); } const instrumentId = Number(instrumentIdParam); - const bidPrices = await getPrices(instrumentId, "bids"); - const askPrices = await getPrices(instrumentId, "asks"); - const summary = await summarizePrice( - instrumentId, - bidPrices, - askPrices, - ); + const bidPrices = getPrices(instrumentId, "bids"); + const askPrices = getPrices(instrumentId, "asks"); + const summary = summarizePrice(instrumentId, bidPrices, askPrices); const bidTotals: Record = {}; const askTotals: Record = {}; for (const bp of [1, 5, 25] as const) { @@ -470,18 +443,16 @@ serve({ for (const price of bidPrices) { if (price >= summary.price * (1 - bp / 10_000)) { const remainingQuantity = - await app.state.instruments[`${instrumentId}`].bids[ - `${price}` - ].remainingQuantity; + app.state.instruments[`${instrumentId}`].bids[`${price}`] + .remainingQuantity; bidTotal += remainingQuantity; } } for (const price of askPrices) { if (price <= summary.price * (1 + bp / 10_000)) { const remainingQuantity = - await app.state.instruments[`${instrumentId}`].asks[ - `${price}` - ].remainingQuantity; + app.state.instruments[`${instrumentId}`].asks[`${price}`] + .remainingQuantity; askTotal += remainingQuantity; } } @@ -507,20 +478,18 @@ serve({ { status: 400 }, ); } - const ticks = await Promise.all( - body.queries.map((query) => - tickAt( - body.instrumentId as number, - query.side === "buy" ? "bids" : "asks", - Number(query.priceQ32), - ), + const ticks = body.queries.map((query) => + tickAt( + body.instrumentId as number, + query.side === "buy" ? "bids" : "asks", + Number(query.priceQ32), ), ); return json({ ticks }); }, }, "/api/estimate-market-order": { - GET: async (req) => { + GET: (req) => { const url = new URL(req.url); const instrumentId = url.searchParams.get("instrumentId"); const side = url.searchParams.get("side"); @@ -536,7 +505,7 @@ serve({ ); } const parsedInstrumentId = Number(instrumentId); - const prices = await getPrices( + const prices = getPrices( parsedInstrumentId, side === "buy" ? "asks" : "bids", ); @@ -547,7 +516,7 @@ serve({ for (const price of prices) { if (remaining === 0n) break; const available = - await app.state.instruments[`${parsedInstrumentId}`][ + app.state.instruments[`${parsedInstrumentId}`][ side === "buy" ? "asks" : "bids" ][`${price}`].remainingQuantity; if (available === 0n) continue; @@ -567,7 +536,7 @@ serve({ }, }, "/api/estimate-fill-to-price": { - GET: async (req) => { + GET: (req) => { const url = new URL(req.url); const instrumentId = url.searchParams.get("instrumentId"); const side = url.searchParams.get("side"); @@ -584,7 +553,7 @@ serve({ } const parsedInstrumentId = Number(instrumentId); const limitPrice = Number(priceQ32); - const prices = await getPrices( + const prices = getPrices( parsedInstrumentId, side === "buy" ? "asks" : "bids", ); @@ -594,7 +563,7 @@ serve({ for (const price of prices) { if (side === "buy" ? price > limitPrice : price < limitPrice) break; const remainingQuantity = - await app.state.instruments[`${parsedInstrumentId}`][ + app.state.instruments[`${parsedInstrumentId}`][ side === "buy" ? "asks" : "bids" ][`${price}`].remainingQuantity; if (remainingQuantity === 0n) continue; diff --git a/apps/token/src/app.test.ts b/apps/token/src/app.test.ts index d3bb6d23..f6aa9d9e 100644 --- a/apps/token/src/app.test.ts +++ b/apps/token/src/app.test.ts @@ -112,13 +112,11 @@ test("smoke: native accounts mint and transfer settle onchain", async () => { ); expect(acceptedMutationIds).toHaveLength(4); - expect(await typewriter.state.totalSupply).toBe(100n); - expect(await typewriter.state.balances[user.accountID]).toBe(60n); - expect(await typewriter.state.balances[recipient.accountID]).toBe(40n); - expect(await typewriter.accounts[user.accountID].nonces["0"]).toBe(2n); - expect(await typewriter.accounts[user.accountID].activeCredentials).toBe( - 1n, - ); + expect(typewriter.state.totalSupply).toBe(100n); + expect(typewriter.state.balances[user.accountID]).toBe(60n); + expect(typewriter.state.balances[recipient.accountID]).toBe(40n); + expect(typewriter.accounts[user.accountID].nonces["0"]).toBe(2n); + expect(typewriter.accounts[user.accountID].activeCredentials).toBe(1n); const deadlineMs = Date.now() + 5_000; while (includedMutationIds.size < 4) { diff --git a/apps/token/src/index.ts b/apps/token/src/index.ts index e7392a99..59ef985d 100644 --- a/apps/token/src/index.ts +++ b/apps/token/src/index.ts @@ -59,10 +59,10 @@ const server = Bun.serve({ }, "/api/accountIds": () => jsonResponse(Object.keys(typewriter.state.balances)), - "/api/account/:accountID": async (req) => { + "/api/account/:accountID": (req) => { const accountID = req.params.accountID as Hex; return jsonResponse({ - balance: await typewriter.state.balances[accountID], + balance: typewriter.state.balances[accountID], }); }, "/api/mutation/:id/status": { diff --git a/packages/typewriter-evm/src/index.test.ts b/packages/typewriter-evm/src/index.test.ts index 93bc88bb..e6af7db1 100644 --- a/packages/typewriter-evm/src/index.test.ts +++ b/packages/typewriter-evm/src/index.test.ts @@ -15,7 +15,7 @@ const REVERTING_ADDR = "0x0000000000000000000000000000000000000bad" as const; const CALLER = "0x000000000000000000000000000000000000ca11" as const; import { expect, test } from "bun:test"; -import { Effect, Exit } from "effect"; +import { Effect, Exit, Fiber, Scheduler } from "effect"; import { Hex } from "ox"; import { createStorageView } from "storage-layout"; import { TEST_PUBLIC_CLIENT } from "../test/setup"; @@ -832,9 +832,9 @@ test("an interrupted execute does not desync subsequent calls", async () => { }); test("concurrent execute submissions serialize into sequential journals", async () => { - // createEVM serializes calls through a semaphore, so executes submitted - // concurrently run one at a time and each opens the next sequential - // journal id rather than racing or interleaving. + // Each native call blocks the thread, so executes submitted concurrently + // run one at a time and each opens the next sequential journal id rather + // than racing or interleaving. const program = Effect.gen(function* () { const evm = yield* createEVM(); yield* evm.init(initWithCounter); @@ -852,3 +852,26 @@ test("concurrent execute submissions serialize into sequential journals", async const journalIds = results.map((r) => r.journal_id!).sort((a, b) => a - b); expect(journalIds).toEqual([1, 2, 3, 4, 5]); }); + +test("runSync completes while another call is paused mid-call", async () => { + // A fiber can yield to the scheduler at any op. Nothing in a call may wait + // on another call, or runSync on a storage read fails with AsyncFiberError. + const program = Effect.gen(function* () { + const evm = yield* createEVM(); + yield* evm.init(initWithCounter); + const read = evm.readStorage({ address: COUNTER_ADDR, slots: ["0x0"] }); + // A budget below 3 yields before any progress, so the fiber never ends. + for (let ops = 3; ops <= 32; ops++) { + // Yield after `ops` ops so the forked call pauses at each point in it. + const fiber = Effect.runFork( + read.pipe(Effect.provideService(Scheduler.MaxOpsBeforeYield, ops)), + ); + expect(Effect.runSync(read)).toEqual( + yield* Effect.promise(() => Effect.runPromise(read)), + ); + yield* Fiber.join(fiber); + } + }); + + await Effect.runPromise(Effect.scoped(program)); +}); diff --git a/packages/typewriter-evm/src/index.ts b/packages/typewriter-evm/src/index.ts index 949e2faf..5945db71 100644 --- a/packages/typewriter-evm/src/index.ts +++ b/packages/typewriter-evm/src/index.ts @@ -1,6 +1,4 @@ -// Effect-native client for the typewriter-evm native addon. - -import { Data, Effect, Ref, type Scope, Semaphore } from "effect"; +import { Data, Effect, Ref, type Scope } from "effect"; import type { BlockParams, ExecuteParams, @@ -103,61 +101,60 @@ function loadNativeAddon(): NativeAddon { } } -// Creates one native EVM harness per createEVM. A semaphore (capacity 1) -// serializes calls so the harness only ever sees one request at a time. +// Creates one native EVM harness per createEVM. Every method is one +// synchronous native call, which blocks the thread, so the harness only ever +// sees one request at a time without a lock. Nothing in a call suspends, so +// `Effect.runSync` on a call always completes. Keep native methods synchronous: +// an async native method needs calls to be serialized again. export const createEVM = (): Effect.Effect => Effect.acquireRelease( Effect.gen(function* () { const native = new (loadNativeAddon().NativeEvm)(); - const sem = yield* Semaphore.make(1); - const locked = sem.withPermits(1); const idCounterRef = yield* Ref.make(1); const call = ( method: Request["method"], build: (id: number) => Request, ): Effect.Effect => - locked( - Effect.gen(function* () { - const id = yield* Ref.getAndUpdate(idCounterRef, (n) => n + 1); - const req = build(id); - - const responseJson = yield* Effect.try({ - try: () => native.call(JSON.stringify(req)), - catch: (e) => - new EvmCrashed({ message: "native call failed", cause: e }), + Effect.gen(function* () { + const id = yield* Ref.getAndUpdate(idCounterRef, (n) => n + 1); + const req = build(id); + + const responseJson = yield* Effect.try({ + try: () => native.call(JSON.stringify(req)), + catch: (e) => + new EvmCrashed({ message: "native call failed", cause: e }), + }); + + const res = yield* Effect.try({ + try: () => JSON.parse(responseJson) as Response, + catch: (e) => + new EvmProtocolError({ + message: `invalid response JSON: ${responseJson}`, + cause: e, + }), + }); + if (res.id !== id) { + const responseError = res.ok ? undefined : res.error; + // Mismatches often mean the TypeScript client and native addon + // are out of sync. + return yield* new EvmProtocolError({ + message: + `response id mismatch for ${method}: expected ${id}, got ${res.id}` + + (responseError === undefined + ? "" + : `; response error: ${responseError}`), }); - - const res = yield* Effect.try({ - try: () => JSON.parse(responseJson) as Response, - catch: (e) => - new EvmProtocolError({ - message: `invalid response JSON: ${responseJson}`, - cause: e, - }), + } + if (!res.ok) { + return yield* new EvmCallError({ + method, + message: `${method} failed: ${res.error}`, + cause: res.error, }); - if (res.id !== id) { - const responseError = res.ok ? undefined : res.error; - // Mismatches often mean the TypeScript client and native addon - // are out of sync. - return yield* new EvmProtocolError({ - message: - `response id mismatch for ${method}: expected ${id}, got ${res.id}` + - (responseError === undefined - ? "" - : `; response error: ${responseError}`), - }); - } - if (!res.ok) { - return yield* new EvmCallError({ - method, - message: `${method} failed: ${res.error}`, - cause: res.error, - }); - } - return res.result as T; - }), - ); + } + return res.result as T; + }); const evm: EVM = { init: (params) => diff --git a/packages/typewriter/README.md b/packages/typewriter/README.md index b8cb8b63..4d2a8094 100644 --- a/packages/typewriter/README.md +++ b/packages/typewriter/README.md @@ -572,10 +572,11 @@ typewriter.state: StorageView; `typewriter.state` resolves the top-level Solidity `state` variable by one proxy layer. It reads the runtime's local revm-backed storage mirror, so accepted state -is visible before onchain inclusion and leaf accesses return promises. +is visible before onchain inclusion. Leaf accesses are synchronous and return +values. ```ts -const balance = await typewriter.state.balances[accountID]; +const balance = typewriter.state.balances[accountID]; ``` `typewriter.state` reflects locally accepted state, which can be ahead of what is `included` or `finalized` onchain. diff --git a/packages/typewriter/src/config.test.ts b/packages/typewriter/src/config.test.ts index 35ec1e20..67c2b7bb 100644 --- a/packages/typewriter/src/config.test.ts +++ b/packages/typewriter/src/config.test.ts @@ -119,12 +119,12 @@ test("buildInternalApp constructs a serializable manifest", () => { test("Typewriter exposes each storage root at one property layer", () => { const app = createTypewriterTest(); - type Root = StorageView; + type Root = StorageView; expectTypeOf(app.state).toEqualTypeOf(); expectTypeOf(app.accounts).toEqualTypeOf(); - expectTypeOf().toEqualTypeOf>(); - expectTypeOf().toEqualTypeOf>(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); }); test("Typewriter mutation input uses concrete authorization", () => { diff --git a/packages/typewriter/src/index.ts b/packages/typewriter/src/index.ts index 5a6f0928..6c02b56e 100644 --- a/packages/typewriter/src/index.ts +++ b/packages/typewriter/src/index.ts @@ -26,7 +26,7 @@ type StorageRootProperty< storageConfig extends StorageConfig, name extends "accounts" | "state", > = - StorageView extends infer root + StorageView extends infer root ? name extends keyof root ? root[name] : never diff --git a/packages/typewriter/src/runtime.test.ts b/packages/typewriter/src/runtime.test.ts index 37bdc5ef..20367ebf 100644 --- a/packages/typewriter/src/runtime.test.ts +++ b/packages/typewriter/src/runtime.test.ts @@ -147,8 +147,8 @@ test("runtime loads persisted root state before returning", async () => { ); const runtime = yield* createRuntimeEffect(app); - const state = runtime.state as { total: Promise }; - return yield* Effect.promise(() => state.total); + const state = runtime.state as { total: bigint }; + return state.total; }).pipe(Effect.provide(layerRuntimeServices(address))), ), ); @@ -398,9 +398,9 @@ test("batch ordering executes credit before a submitted debit", async () => { concurrency: "unbounded", }); const state = runtime.state as { - balances: Record | undefined>; + balances: Record; }; - return yield* Effect.promise(() => state.balances[accountID]!); + return state.balances[accountID]!; }), ); @@ -480,9 +480,9 @@ test("runtime uses native credentials for P256 and WebAuthn mutations", async () yield* runtime.execute(addWebAuthn); yield* runtime.execute(webAuthnCredit); const state = runtime.state as { - balances: Record | undefined>; + balances: Record; }; - return yield* Effect.promise(() => state.balances[accountID]!); + return state.balances[accountID]!; }), ); diff --git a/packages/typewriter/src/runtime.ts b/packages/typewriter/src/runtime.ts index 713057e9..cd5f3f63 100644 --- a/packages/typewriter/src/runtime.ts +++ b/packages/typewriter/src/runtime.ts @@ -331,8 +331,8 @@ export function createRevmRevertError( export function createRuntimeState(app: InternalApp): Effect.Effect< { evm: EVM; - accounts: StorageView[string]; - state: StorageView[string]; + accounts: StorageView[string]; + state: StorageView[string]; /** * The keccak256 preimages the storage view enumerates mapping keys from. * Add the preimages of each accepted mutation. @@ -407,10 +407,10 @@ export function createRuntimeState(app: InternalApp): Effect.Effect< const storageProxy = createStorageView( app.storageLayout, - async (slots) => { + (slots) => { const missingSlots = slots.filter((slot) => !slotCache.has(slot)); if (missingSlots.length > 0) { - const fetched = await Effect.runPromise( + const fetched = Effect.runSync( evm.readStorage({ address: app.address, slots: missingSlots }), ); for (const [slot, value] of Object.entries(fetched) as [ diff --git a/packages/typewriter/src/typewriter.ts b/packages/typewriter/src/typewriter.ts index 32516a26..59820cf5 100644 --- a/packages/typewriter/src/typewriter.ts +++ b/packages/typewriter/src/typewriter.ts @@ -31,7 +31,7 @@ type StorageRootProperty< storageConfig extends StorageConfig, name extends "accounts" | "state", > = - StorageView extends infer root + StorageView extends infer root ? name extends keyof root ? root[name] : never @@ -84,8 +84,8 @@ export type RuntimeTypewriter< export type InternalRuntimeTypewriter< sequencingConfig extends SequencingConfig = SequencingConfig, > = { - readonly state: StorageView[string]; - readonly accounts: StorageView[string]; + readonly state: StorageView[string]; + readonly accounts: StorageView[string]; readonly schema: TypewriterSchema; execute(submitted: { name: string;