From fa33ed8f6788bc0a81c37a90a2660c34c1393dbb Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Thu, 16 Feb 2023 13:40:09 -0500 Subject: [PATCH 01/23] begin rpc reimplementation --- _tasks/download_frame_metadata.ts | 4 +- _tasks/star.ts | 2 +- examples/ink_e2e/main.ts | 2 +- fluent/AddressRune.ts | 6 +- fluent/ClientRune.ts | 4 +- fluent/PublicKeyRune.ts | 8 +- fluent/rpc_method_runes.ts | 2 +- fluent/rpc_runes.ts | 82 +++++------ mod.ts | 2 +- patterns/MultisigRune.ts | 10 +- patterns/consensus/babeBlockAuthor.ts | 13 +- patterns/consensus/preRuntimeDigest.ts | 11 +- patterns/ink/InkMetadataRune.ts | 13 +- providers/frame/FrameProvider.ts | 8 +- providers/frame/FrameProxyProvider.ts | 11 +- rpc/RpcClient.ts | 103 ++++++++++++++ rpc/RpcClientError.ts | 3 + rpc/RpcProvider.ts | 88 ++++++++++++ rpc/client.ts | 186 ------------------------- rpc/known/Readme.md | 1 + rpc/messages.ts | 52 ------- rpc/mod.ts | 10 +- rpc/provider/base.ts | 73 ---------- rpc/provider/errors.ts | 25 ---- rpc/provider/proxy.test.ts | 81 ----------- rpc/provider/proxy.ts | 113 --------------- rpc/provider/smoldot.test.ts | 175 ----------------------- rpc/provider/smoldot.ts | 144 ------------------- rpc/provider/test_util.ts | 18 --- rpc/rpc_messages.ts | 50 +++++++ rpc/{ => todo}/client.test.ts | 0 rpc/todo/client.ts | 186 +++++++++++++++++++++++++ rpc/todo/proxy.test.ts | 81 +++++++++++ rpc/todo/proxy.ts | 113 +++++++++++++++ rpc/todo/smoldot.test.ts | 175 +++++++++++++++++++++++ rpc/todo/smoldot.ts | 144 +++++++++++++++++++ rpc/todo/test_util.ts | 18 +++ util/types.ts | 4 + 38 files changed, 1053 insertions(+), 968 deletions(-) create mode 100644 rpc/RpcClient.ts create mode 100644 rpc/RpcClientError.ts create mode 100644 rpc/RpcProvider.ts delete mode 100644 rpc/client.ts create mode 100644 rpc/known/Readme.md delete mode 100644 rpc/messages.ts delete mode 100644 rpc/provider/base.ts delete mode 100644 rpc/provider/errors.ts delete mode 100644 rpc/provider/proxy.test.ts delete mode 100644 rpc/provider/proxy.ts delete mode 100644 rpc/provider/smoldot.test.ts delete mode 100644 rpc/provider/smoldot.ts delete mode 100644 rpc/provider/test_util.ts create mode 100644 rpc/rpc_messages.ts rename rpc/{ => todo}/client.test.ts (100%) create mode 100644 rpc/todo/client.ts create mode 100644 rpc/todo/proxy.test.ts create mode 100644 rpc/todo/proxy.ts create mode 100644 rpc/todo/smoldot.test.ts create mode 100644 rpc/todo/smoldot.ts create mode 100644 rpc/todo/test_util.ts diff --git a/_tasks/download_frame_metadata.ts b/_tasks/download_frame_metadata.ts index 4d233bd2d..cddccf9e5 100755 --- a/_tasks/download_frame_metadata.ts +++ b/_tasks/download_frame_metadata.ts @@ -34,12 +34,10 @@ Deno.writeTextFileSync(modFilePath, modFileContents, { create: true }) await Promise.all( Object.entries(knownClients).map(async ([name, client]) => { - const r = await client.call(name, "state_getMetadata", []) - if (r instanceof Error) throw r + const r = await client.call("state_getMetadata", []) if (r.error) throw new Error(r.error.message) const outPath = new URL(`_downloaded/${name}.scale`, outDir) console.log(`Downloading ${name} metadata to "${outPath}".`) await Deno.writeTextFile(outPath, r.result) - await client.discard() }), ) diff --git a/_tasks/star.ts b/_tasks/star.ts index 4b272b7b0..6091bf177 100755 --- a/_tasks/star.ts +++ b/_tasks/star.ts @@ -22,7 +22,7 @@ await Deno.writeTextFile(dest, generated) const data: Data = JSON.parse( new TextDecoder().decode( await Deno.run({ - cmd: ["deno", "info", "-r=http://localhost:4646/", "--json", "target/star.ts"], + cmd: ["deno", "info", "--json", "target/star.ts"], stdout: "piped", }) .output(), diff --git a/examples/ink_e2e/main.ts b/examples/ink_e2e/main.ts index b672b5d25..38179bed2 100644 --- a/examples/ink_e2e/main.ts +++ b/examples/ink_e2e/main.ts @@ -39,7 +39,7 @@ if (!address) { } console.log(`Contract address: ${address}`) -const publicKey = AddressRune.from(address, client).publicKey() +const publicKey = AddressRune.from(client, address).publicKey() console.log("Contract public key:", await publicKey.run()) const contract = metadata.instance(client, publicKey) diff --git a/fluent/AddressRune.ts b/fluent/AddressRune.ts index b719a8357..77f13c535 100644 --- a/fluent/AddressRune.ts +++ b/fluent/AddressRune.ts @@ -1,4 +1,3 @@ -import { Client } from "../rpc/mod.ts" import { Rune, RunicArgs } from "../rune/mod.ts" import { ValueRune } from "../rune/ValueRune.ts" import { ss58 } from "../util/mod.ts" @@ -10,7 +9,10 @@ export class AddressRune extends Rune(...[address, client]: RunicArgs) { + static from( + client: ClientRune, + ...[address]: RunicArgs + ) { return Rune.resolve(address).into(AddressRune, Rune.resolve(client).into(ClientRune)) } diff --git a/fluent/ClientRune.ts b/fluent/ClientRune.ts index df0292ced..785a9b7d8 100644 --- a/fluent/ClientRune.ts +++ b/fluent/ClientRune.ts @@ -1,7 +1,7 @@ import * as $ from "../deps/scale.ts" import * as M from "../frame_metadata/mod.ts" import { Event } from "../primitives/mod.ts" -import * as rpc from "../rpc/mod.ts" +import { RpcClient } from "../rpc/mod.ts" import { MetaRune, Rune, RunicArgs, ValueRune } from "../rune/mod.ts" import { HexHash } from "../util/mod.ts" import { BlockRune } from "./BlockRune.ts" @@ -15,7 +15,7 @@ export interface Chain { event: E } -export class ClientRune extends Rune { +export class ClientRune extends Rune { latestBlock = this.block(chain.getBlockHash( this, chain diff --git a/fluent/PublicKeyRune.ts b/fluent/PublicKeyRune.ts index 92112492c..6892dcd68 100644 --- a/fluent/PublicKeyRune.ts +++ b/fluent/PublicKeyRune.ts @@ -1,16 +1,14 @@ -import { Client } from "../rpc/mod.ts" -import { Rune, RunicArgs, ValueRune } from "../rune/mod.ts" +import { Rune, ValueRune } from "../rune/mod.ts" import { ss58 } from "../util/mod.ts" import { AddressRune } from "./AddressRune.ts" -import { ClientRune } from "./ClientRune.ts" +import { Chain, ClientRune } from "./ClientRune.ts" export class PublicKeyRune extends Rune { constructor(_prime: PublicKeyRune["_prime"]) { super(_prime) } - address(...args: RunicArgs) { - const client = RunicArgs.resolve(args)[0].into(ClientRune) + address(client: ClientRune) { return Rune .tuple([client.addressPrefix(), this.into(ValueRune)]) .map(([prefix, publicKey]) => ss58.encode(prefix, publicKey)) diff --git a/fluent/rpc_method_runes.ts b/fluent/rpc_method_runes.ts index 0a7760e8e..75bb17e25 100644 --- a/fluent/rpc_method_runes.ts +++ b/fluent/rpc_method_runes.ts @@ -10,7 +10,7 @@ export namespace state { export const getStorage = rpcCall< [key: known.StorageKey, at?: U.HexHash], known.StorageData | null - >("state_getStorage", true) + >("state_getStorage") export const subscribeStorage = rpcSubscription< [keys: known.StorageKey[]], known.StorageChangeSet diff --git a/fluent/rpc_runes.ts b/fluent/rpc_runes.ts index c09c03d5f..2207aa6de 100644 --- a/fluent/rpc_runes.ts +++ b/fluent/rpc_runes.ts @@ -1,85 +1,66 @@ -import * as rpc from "../rpc/mod.ts" +import { + RpcClient, + RpcClientError, + RpcErrorMessage, + RpcSubscriptionMessage, + WsRpcProvider, +} from "../rpc/mod.ts" import { Batch, MetaRune, Run, Rune, RunicArgs, RunStream } from "../rune/mod.ts" import { ClientRune } from "./ClientRune.ts" -class RunRpcClient extends Run, never> { +class RunRpcClient extends Run { constructor( ctx: Batch, - readonly provider: rpc.Provider, - readonly discoveryValue: DV, + readonly provider: WsRpcProvider, + readonly discoveryValue: string, ) { super(ctx) } - client?: rpc.Client - async _evaluate(): Promise> { - return this.client ??= new rpc.Client(this.provider, this.discoveryValue) - } - - override async cleanup(): Promise { - await this.client?.discard() - super.cleanup() + client?: RpcClient + async _evaluate(): Promise { + return this.client ??= new RpcClient(this.provider, this.discoveryValue) } } -export function rpcClient< - DiscoveryValue, - SendErrorData, - HandlerErrorData, - CloseErrorData, ->( - provider: rpc.Provider, - discoveryValue: DiscoveryValue, -) { +export function rpcClient(provider: WsRpcProvider, discoveryValue: string) { return Rune.new(RunRpcClient, provider, discoveryValue).into(ClientRune) } -export function rpcCall( - method: string, - _nonIdempotent?: boolean, -) { - return (...args: RunicArgs) => { +export function rpcCall(method: string) { + return (...args: RunicArgs) => { return Rune.tuple(args) .map(async ([client, ...params]) => { - // TODO: why do we need to explicitly type this / why is this not being inferred? - const id = client.providerRef.nextId() - const result = await client.call(id, method, params) - if (result instanceof Error) { - throw result - } else if (result.error) { - console.log(result) - throw new RpcServerError(result) - } + const result = await client.call(method, params) + if (result.error) throw new RpcServerError(result) return result.result - }).throws(rpc.ProviderSendError, rpc.ProviderHandlerError, RpcServerError) + }) + .throws(RpcClientError, RpcServerError) } } -class RunRpcSubscription extends RunStream> { +class RunRpcSubscription extends RunStream { constructor( ctx: Batch, - client: rpc.Client, + client: RpcClient, params: unknown[], subscribeMethod: string, unsubscribeMethod: string, ) { super(ctx) - client.subscriptionFactory()( + client.subscription( subscribeMethod, unsubscribeMethod, params, (value) => this.push(value), - this.signal, + this, ) } } export function rpcSubscription() { - return ( - subscribeMethod: string, - unsubscribeMethod: string, - ) => { - return (...args: RunicArgs) => { + return (subscribeMethod: string, unsubscribeMethod: string) => { + return (...args: RunicArgs) => { return Rune.tuple(args) .map(([client, ...params]) => Rune.new(RunRpcSubscription, client, params, subscribeMethod, unsubscribeMethod) @@ -93,15 +74,20 @@ export function rpcSubscription() { throw new RpcServerError(event) } return event.params.result as Result - }).throws(rpc.ProviderSendError, rpc.ProviderHandlerError, RpcServerError) + }) + .throws(RpcClientError, RpcServerError) } } } export class RpcServerError extends Error { override readonly name = "RpcServerError" + code + data - constructor(readonly inner: rpc.msg.ErrorMessage) { - super() + constructor({ error: { code, data, message } }: RpcErrorMessage) { + super(message) + this.code = code + this.data = data } } diff --git a/mod.ts b/mod.ts index 651e3a40b..c99bc9adb 100644 --- a/mod.ts +++ b/mod.ts @@ -3,7 +3,7 @@ export { BitSequence } from "./deps/scale.ts" export * from "./fluent/mod.ts" export * as frame from "./frame_metadata/mod.ts" export * from "./primitives/mod.ts" -export * as rpc from "./rpc/mod.ts" +export * from "./rpc/mod.ts" export * from "./rune/mod.ts" export * from "./scale_info/mod.ts" export { diff --git a/patterns/MultisigRune.ts b/patterns/MultisigRune.ts index 4b6c9c885..bffc1f916 100644 --- a/patterns/MultisigRune.ts +++ b/patterns/MultisigRune.ts @@ -1,7 +1,6 @@ import * as bytes from "../deps/std/bytes.ts" import { Chain, ClientRune } from "../fluent/ClientRune.ts" import { MultiAddress } from "../primitives/mod.ts" -import { Client } from "../rpc/mod.ts" import { Rune, RunicArgs, ValueRune } from "../rune/mod.ts" import { multisigAccountId } from "./multisigAccountId.ts" @@ -35,10 +34,11 @@ export class MultisigRune extends Rune(...[client, multisig]: RunicArgs) { - return Rune - .resolve(multisig) - .into(MultisigRune, Rune.resolve(client).into(ClientRune)) + static from( + client: ClientRune, + ...[multisig]: RunicArgs + ) { + return Rune.resolve(multisig).into(MultisigRune, client) } otherSignatories(...[sender]: RunicArgs) { diff --git a/patterns/consensus/babeBlockAuthor.ts b/patterns/consensus/babeBlockAuthor.ts index 6071de374..2a47c5601 100644 --- a/patterns/consensus/babeBlockAuthor.ts +++ b/patterns/consensus/babeBlockAuthor.ts @@ -1,23 +1,24 @@ import { $preDigest } from "polkadot_dev/types/sp_consensus_babe/digests.ts" -import { ClientRune } from "../../fluent/mod.ts" +import { Chain, ClientRune } from "../../fluent/mod.ts" import { PublicKeyRune } from "../../fluent/mod.ts" -import { Client } from "../../rpc/mod.ts" import { Rune, RunicArgs, ValueRune } from "../../rune/mod.ts" import { HexHash } from "../../util/branded.ts" import { preRuntimeDigest } from "./preRuntimeDigest.ts" -export function babeBlockAuthor(...args: RunicArgs) { - const [client, at] = RunicArgs.resolve(args) +export function babeBlockAuthor( + client: ClientRune, + ...[blockHash]: RunicArgs +) { const validators = client .into(ClientRune) .metadata() .pallet("Session") .storage("Validators") - .entry([], at) + .entry([], blockHash) .unsafeAs() .into(ValueRune) .unhandle(undefined) - const authorityIndex = preRuntimeDigest(...args) + const authorityIndex = preRuntimeDigest(client, blockHash) .map(({ type, value }) => { if (type !== "BABE") return new AuthorRetrievalNotSupportedError() return $preDigest.decode(value) diff --git a/patterns/consensus/preRuntimeDigest.ts b/patterns/consensus/preRuntimeDigest.ts index 90a10ab50..0c00c74a9 100644 --- a/patterns/consensus/preRuntimeDigest.ts +++ b/patterns/consensus/preRuntimeDigest.ts @@ -1,15 +1,16 @@ import { $digestItem, DigestItem } from "polkadot_dev/types/sp_runtime/generic/digest.ts" -import { ClientRune } from "../../fluent/mod.ts" -import { Client } from "../../rpc/mod.ts" +import { Chain, ClientRune } from "../../fluent/mod.ts" import { RunicArgs, ValueRune } from "../../rune/mod.ts" import { HexHash } from "../../util/branded.ts" import { hex } from "../../util/mod.ts" -export function preRuntimeDigest(...args: RunicArgs) { - const [client, at] = RunicArgs.resolve(args) +export function preRuntimeDigest( + client: ClientRune, + ...[blockHash]: RunicArgs +) { return client .into(ClientRune) - .block(at) + .block(blockHash) .header() .into(ValueRune) .access("digest", "logs") diff --git a/patterns/ink/InkMetadataRune.ts b/patterns/ink/InkMetadataRune.ts index e3f776b55..ec37ab371 100644 --- a/patterns/ink/InkMetadataRune.ts +++ b/patterns/ink/InkMetadataRune.ts @@ -1,6 +1,5 @@ import * as $ from "../../deps/scale.ts" import { Chain, ClientRune, CodecRune, ExtrinsicRune, state } from "../../fluent/mod.ts" -import { Client } from "../../rpc/client.ts" import { ArrayRune, Rune, RunicArgs, ValueRune } from "../../rune/mod.ts" import { DeriveCodec } from "../../scale_info/mod.ts" import { hex } from "../../util/mod.ts" @@ -31,7 +30,10 @@ export class InkMetadataRune extends Rune(...[client, jsonText]: RunicArgs) { + static from( + client: ClientRune, + ...[jsonText]: RunicArgs + ) { return Rune .resolve(jsonText) .map((jsonText) => normalize(JSON.parse(jsonText))) @@ -128,10 +130,13 @@ export class InkMetadataRune extends Rune(...[client, publicKey]: RunicArgs) { + instance( + client: ClientRune, + ...[publicKey]: RunicArgs + ) { return Rune .resolve(publicKey) - .into(InkRune, Rune.resolve(client).into(ClientRune), this.as(InkMetadataRune)) + .into(InkRune, client, this.as(InkMetadataRune)) } } diff --git a/providers/frame/FrameProvider.ts b/providers/frame/FrameProvider.ts index 5362803bb..3dbeb7d11 100644 --- a/providers/frame/FrameProvider.ts +++ b/providers/frame/FrameProvider.ts @@ -2,7 +2,7 @@ import { File, FrameCodegen } from "../../codegen/frame/mod.ts" import { posix as path } from "../../deps/std/path.ts" import { $metadata } from "../../frame_metadata/Metadata.ts" import { fromPrefixedHex } from "../../frame_metadata/mod.ts" -import { Client } from "../../rpc/mod.ts" +import { RpcClient } from "../../rpc/mod.ts" import { f, PathInfo, Provider } from "../../server/mod.ts" import { fromPathInfo } from "../../server/PathInfo.ts" import { throwIfError, WeakMemo } from "../../util/mod.ts" @@ -12,7 +12,7 @@ export abstract class FrameProvider extends Provider { codegenCtxsPending: Record> = {} - abstract client(pathInfo: PathInfo): Promise + abstract client(pathInfo: PathInfo): Promise abstract clientFile(pathInfo: PathInfo): Promise async handle(request: Request, pathInfo: PathInfo): Promise { @@ -83,8 +83,8 @@ export abstract class FrameProvider extends Provider { }) } - async clientCall(client: Client, method: string, params: unknown[] = []): Promise { - const result = throwIfError(await client.call(client.providerRef.nextId(), method, params)) + async clientCall(client: RpcClient, method: string, params: unknown[] = []): Promise { + const result = throwIfError(await client.call(method, params)) if (result.error) throw new Error(result.error.message) return result.result as R } diff --git a/providers/frame/FrameProxyProvider.ts b/providers/frame/FrameProxyProvider.ts index ce9cc17dc..88a833732 100644 --- a/providers/frame/FrameProxyProvider.ts +++ b/providers/frame/FrameProxyProvider.ts @@ -1,6 +1,6 @@ import { File } from "../../codegen/frame/mod.ts" import { deferred } from "../../deps/std/async.ts" -import { Client, proxyProvider } from "../../rpc/mod.ts" +import { RpcClient, wsRpcProvider } from "../../rpc/mod.ts" import { PathInfo } from "../../server/mod.ts" import { fromPathInfo } from "../../server/PathInfo.ts" import { FrameProvider } from "./FrameProvider.ts" @@ -60,10 +60,7 @@ export abstract class FrameProxyProvider extends FrameProvider { } async client(pathInfo: PathInfo) { - const url = await this.dynamicUrl(pathInfo) - const client = new Client(proxyProvider, url) - this.env.signal.addEventListener("abort", client.discard) - return client + return new RpcClient(wsRpcProvider, await this.dynamicUrl(pathInfo)) } async clientFile(pathInfo: PathInfo) { @@ -74,9 +71,9 @@ export abstract class FrameProxyProvider extends FrameProvider { export const discoveryValue = "${url}" - export const client = C.rpcClient(C.rpc.proxyProvider, discoveryValue)["_asCodegen"]() + export const client = C.rpcClient(C.wsRpcProvider, discoveryValue)["_asCodegen"]() - export const rawClient = new C.rpc.Client(C.rpc.proxyProvider, discoveryValue) + export const rawClient = new C.RpcClient(C.wsRpcProvider, discoveryValue) `) } } diff --git a/rpc/RpcClient.ts b/rpc/RpcClient.ts new file mode 100644 index 000000000..f112d6e5e --- /dev/null +++ b/rpc/RpcClient.ts @@ -0,0 +1,103 @@ +import { Deferred, deferred } from "../deps/std/async.ts" +import { SignalBearer } from "../util/mod.ts" +import { + RpcErrorMessage, + RpcErrorMessageData, + RpcHandler, + RpcIngressMessage, + RpcMessageId, + RpcNotificationMessage, + RpcOkMessage, +} from "./rpc_messages.ts" +import { WsRpcProvider } from "./RpcProvider.ts" + +export class RpcClient { + conn + + constructor(readonly provider: WsRpcProvider, readonly discovery: string) { + this.conn = (controller: AbortController) => + this.provider.ref(discovery, this.handler, controller) + } + + callResultPendings: Record> = {} + async call( + method: string, + params: unknown[], + ) { + const controller = new AbortController() + const conn = this.conn(controller) + await conn.ready() + const id = conn.currentId++ + const pending = deferred>() + this.callResultPendings[id] = pending + conn.send(id, method, params) + const result = await pending + controller.abort() + return result + } + + subscriptionInitPendings: Record = {} + subscriptions: Record = {} + subscriptionIdByRpcMessageId: Record = {} + async subscription< + NotificationData = unknown, + ErrorData extends RpcErrorMessageData = RpcErrorMessageData, + >( + subscribe: string, + unsubscribe: string, + params: unknown[], + handler: RpcSubscriptionHandler, + { signal }: SignalBearer, + ) { + const providerController = new AbortController() + const conn = this.conn(providerController) + await conn.ready() + const subscribeId = conn.currentId++ + this.subscriptionInitPendings[subscribeId] = handler as RpcSubscriptionHandler + signal.addEventListener("abort", () => { + delete this.subscriptionInitPendings[subscribeId] + const subscriptionId = this.subscriptionIdByRpcMessageId[subscribeId] + if (subscriptionId) { + delete this.subscriptionIdByRpcMessageId[subscribeId] + conn.send(conn.currentId++, unsubscribe, [subscriptionId]) + providerController.abort() + } + }) + conn.send(subscribeId, subscribe, params) + } + + handler = (message: RpcIngressMessage) => { + if (typeof message.id === "number") { + const callResultPending = this.callResultPendings[message.id] + if (callResultPending) { + callResultPending.resolve(message) + delete this.callResultPendings[message.id] + return + } + const subscriptionPending = this.subscriptionInitPendings[message.id] + if (subscriptionPending) { + if (message.error) subscriptionPending(message) + else { + this.subscriptions[message.result] = subscriptionPending + this.subscriptionIdByRpcMessageId[message.id] = message.result + } + delete this.subscriptionInitPendings[message.id] + } + } else if (message.params) this.subscriptions[message.params.subscription]?.(message) + } +} + +export type RpcCallMessage< + OkData = any, + ErrorData extends RpcErrorMessageData = RpcErrorMessageData, +> = RpcOkMessage | RpcErrorMessage + +export type RpcSubscriptionMessage< + NotificationData = any, + ErrorData extends RpcErrorMessageData = RpcErrorMessageData, +> = RpcNotificationMessage | RpcErrorMessage + +export type RpcSubscriptionHandler< + NotificationData = any, + ErrorData extends RpcErrorMessageData = RpcErrorMessageData, +> = RpcHandler> diff --git a/rpc/RpcClientError.ts b/rpc/RpcClientError.ts new file mode 100644 index 000000000..dc43e5795 --- /dev/null +++ b/rpc/RpcClientError.ts @@ -0,0 +1,3 @@ +export class RpcClientError extends Error { + override readonly name = "RpcClientError" +} diff --git a/rpc/RpcProvider.ts b/rpc/RpcProvider.ts new file mode 100644 index 000000000..a21a72bad --- /dev/null +++ b/rpc/RpcProvider.ts @@ -0,0 +1,88 @@ +import { getOrInit, SignalBearer } from "../util/mod.ts" +import { RpcEgressMessage, RpcHandler, RpcMessageId } from "./rpc_messages.ts" +import { RpcClientError } from "./RpcClientError.ts" + +export class WsRpcProvider { + conns = new Map() + + ref(discovery: string, handler: RpcHandler, { signal }: SignalBearer) { + const conn = getOrInit(this.conns, discovery, () => new WsRpcConn(discovery)) + const references = conn.references.get(handler) + if (!references) conn.references.set(handler, 1) + else conn.references.set(handler, references + 1) + signal.addEventListener("abort", () => { + const references = conn.references.get(handler)! + conn.references.set(handler, references - 1) + if (references === 1) { + conn.references.delete(handler) + if (!conn.references.size) { + this.conns.delete(discovery) + conn.close() + } + } + }) + return conn + } +} + +export const wsRpcProvider = new WsRpcProvider() + +export class WsRpcConn { + currentId = 0 + references = new Map() + inner + + constructor(readonly discovery: string) { + this.inner = new WebSocket(discovery) + this.inner.addEventListener("message", (e) => { + const message = JSON.parse(e.data) + for (const reference of this.references.keys()) reference(message) + }) + } + + close() { + this.inner.close() + } + + send(id: RpcMessageId, method: string, params: unknown[]) { + const message: RpcEgressMessage = { + jsonrpc: "2.0", + id, + method, + params, + } + this.inner.send(JSON.stringify(message)) + } + + async ready() { + switch (this.inner.readyState) { + case WebSocket.OPEN: + return + case WebSocket.CONNECTING: { + try { + return await new Promise((resolve, reject) => { + const controller = new AbortController() + this.inner.addEventListener("open", () => { + controller.abort() + resolve() + }, controller) + this.inner.addEventListener("close", () => { + controller.abort() + reject() + }, controller) + this.inner.addEventListener("error", () => { + controller.abort() + reject() + }, controller) + }) + } catch (_e) { + throw new RpcClientError() + } + } + case WebSocket.CLOSING: + case WebSocket.CLOSED: { + throw new RpcClientError() + } + } + } +} diff --git a/rpc/client.ts b/rpc/client.ts deleted file mode 100644 index 25fa9f8e8..000000000 --- a/rpc/client.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { Deferred, deferred } from "../deps/std/async.ts" -import * as U from "../util/mod.ts" -import * as msg from "./messages.ts" -import { Provider, ProviderListener } from "./provider/base.ts" -import { ProviderHandlerError, ProviderSendError } from "./provider/errors.ts" - -export class Client< - DiscoveryValue = any, - SendErrorData = any, - HandlerErrorData = any, - CloseErrorData = any, -> { - providerRef - pendingCalls: Record> = {} - pendingSubscriptions: SubscriptionListeners = {} - activeSubscriptions: SubscriptionListeners = {} - activeSubscriptionByMessageId: Record = {} - - constructor( - readonly provider: Provider, - readonly discoveryValue: DiscoveryValue, - ) { - this.providerRef = provider(discoveryValue, this.listener) - } - - listener: ProviderListener = (e) => { - if (e instanceof ProviderSendError) { - const egressMessageId = e.egressMessage.id - const pendingCall = this.pendingCalls[egressMessageId] - pendingCall?.resolve(e) - delete this.pendingCalls[egressMessageId] - } else if (e instanceof Error) { - for (const id in this.pendingCalls) { - const pendingCall = this.pendingCalls[id]! - pendingCall.resolve(e) - delete this.pendingCalls[id] - this.pendingSubscriptions[id]?.(e) - delete this.pendingSubscriptions[id] - } - for (const id in this.activeSubscriptions) { - this.activeSubscriptions[id]!(e) - delete this.activeSubscriptions[id] - } - } else if (e.id !== undefined) { - const pendingCall = this.pendingCalls[e.id] - pendingCall?.resolve(e) - delete this.pendingCalls[e.id] - if (this.pendingSubscriptions[e.id]) { - if (e.error) { - this.pendingSubscriptions[e.id]!(e) - } else { - this.activeSubscriptions[e.result] = this.pendingSubscriptions[e.id]! - this.activeSubscriptionByMessageId[e.id] = e.result - } - delete this.pendingSubscriptions[e.id] - } - } else if (e.params) { - this.activeSubscriptions[e.params.subscription]?.(e) - } - } - - call: ClientCall = (id, method, params) => { - const waiter = deferred>() - this.pendingCalls[id] = waiter - this.providerRef.send({ - jsonrpc: "2.0", - id, - method, - params, - }) - return waiter - } - - subscriptionFactory = < - Params extends unknown[] = any[], - NotificationData = any, - >(): SubscriptionFactory => - ( - subscribeMethod, - unsubscribeMethod, - params, - listener, - abortSignal, - ) => { - const id = this.providerRef.nextId() - abortSignal.addEventListener("abort", async () => { - delete this.pendingSubscriptions[id] - const activeSubscriptionId = this.activeSubscriptionByMessageId[id] - if (activeSubscriptionId) { - delete this.activeSubscriptions[activeSubscriptionId] - await this.call( - this.providerRef.nextId(), - unsubscribeMethod, - [activeSubscriptionId], - ) - } - delete this.activeSubscriptionByMessageId[id] - }) - this.call(id, subscribeMethod, params).then((x) => { - if (x instanceof Error || x.error) { - listener(x) - } - }) - this.pendingSubscriptions[id] = listener - } - - discard = () => { - this.pendingCalls = {} - this.pendingSubscriptions = {} - this.activeSubscriptions = {} - this.activeSubscriptionByMessageId = {} - return this.providerRef.release() - } -} - -export type ClientCallEvent = - | msg.OkMessage - | msg.ErrorMessage - | ProviderSendError - | ProviderHandlerError - -export type ClientCall = ( - id: number | string, - method: string, - params: unknown[], -) => Promise> - -export type ClientSubscriptionEvent< - Method extends string, - Result, - SendErrorData, - HandlerErrorData, -> = - | msg.NotificationMessage - | msg.ErrorMessage - | ProviderSendError - | ProviderHandlerError - -type SubscriptionListeners = Record< - string, - SubscriptionListener< - any, - any, - SendErrorData, - HandlerErrorData - > -> - -type SubscriptionListener< - SubscribeMethod extends string, - NotificationData, - SendErrorData, - HandlerErrorData, -> = ( - value: ClientSubscriptionEvent< - SubscribeMethod, - NotificationData, - SendErrorData, - HandlerErrorData - >, -) => void - -export type SubscriptionFactory< - Params extends unknown[], - NotificationData, - SendErrorData, - HandlerErrorData, -> = < - SubscribeMethod extends string, ->( - subscribeMethod: SubscribeMethod, - unsubscribeMethod: string, - params: [...Params], - listener: SubscriptionListener< - SubscribeMethod, - NotificationData, - SendErrorData, - HandlerErrorData - >, - abortSignal: AbortSignal, -) => void - -export interface ClientSubscriptionContext { - end: (value?: T) => U.End - endIfError(value: I): U.End> -} diff --git a/rpc/known/Readme.md b/rpc/known/Readme.md new file mode 100644 index 000000000..e192177e2 --- /dev/null +++ b/rpc/known/Readme.md @@ -0,0 +1 @@ +# TODO: delete this dir upon cleanup following RPC codegen (see paritytech/substrate#12939) diff --git a/rpc/messages.ts b/rpc/messages.ts deleted file mode 100644 index 6a50b813e..000000000 --- a/rpc/messages.ts +++ /dev/null @@ -1,52 +0,0 @@ -export interface EgressMessage - extends JsonRpcVersionBearer -{ - method: Method - id: number | string - params: Params -} - -export type IngressMessage = OkMessage | ErrorMessage | NotificationMessage - -export interface OkMessage extends JsonRpcVersionBearer { - id: string | number - result: Result - params?: never - error?: never -} - -export interface ErrorMessage< - Code extends number = number, - Data = any, -> extends JsonRpcVersionBearer { - id: string | number - error: { - code: Code - message: string - data: Data - } - params?: never - result?: never -} - -export interface NotificationMessage - extends JsonRpcVersionBearer -{ - method: Method - id?: never - params: { - subscription: string - result: Result - } - result?: never - error?: never -} - -interface JsonRpcVersionBearer { - jsonrpc: "2.0" -} - -export function parse(raw: string) { - // TODO - return JSON.parse(raw) -} diff --git a/rpc/mod.ts b/rpc/mod.ts index f28315eab..e29eb82ed 100644 --- a/rpc/mod.ts +++ b/rpc/mod.ts @@ -1,7 +1,5 @@ -export * from "./client.ts" export * as known from "./known/mod.ts" -export * as msg from "./messages.ts" -export * from "./provider/base.ts" -export * from "./provider/errors.ts" -export * from "./provider/proxy.ts" -export * from "./provider/smoldot.ts" +export * from "./rpc_messages.ts" +export * from "./RpcClient.ts" +export * from "./RpcClientError.ts" +export * from "./RpcProvider.ts" diff --git a/rpc/provider/base.ts b/rpc/provider/base.ts deleted file mode 100644 index b6d22b194..000000000 --- a/rpc/provider/base.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as U from "../../util/mod.ts" -import * as msg from "../messages.ts" -import { ProviderCloseError, ProviderHandlerError, ProviderSendError } from "./errors.ts" - -/** - * @param discoveryValue the value with which to discover the given chain - * @param listener the callback to which messages and errors should be applied - */ -export type Provider< - DiscoveryValue = any, - SendErrorData = any, - HandlerErrorData = any, - CloseErrorData = any, -> = ( - discoveryValue: DiscoveryValue, - listener: ProviderListener, -) => ProviderRef - -export type ProviderListener = U.Listener< - | msg.IngressMessage - | ProviderSendError - | ProviderHandlerError, - any -> - -export interface ProviderRef { - nextId(): number - send(message: msg.EgressMessage): void - release(): Promise> -} - -export class ProviderConnection { - /** The set of high-level listeners, which accept parsed messages and errors */ - listeners = new Map< - ProviderListener, - ProviderListener - >() - - /** - * @param inner the underlying representation of the connection (such as a WebSocket or smoldot chain) - * @param cleanUp cb to close the connection (`inner`) and free up resources - */ - constructor(readonly inner: Inner, readonly cleanUp: () => void) {} - - addListener = (listener: ProviderListener) => { - if (this.listeners.has(listener)) { - return - } - this.listeners.set( - listener, - listener.bind({ - stop: () => { - this.listeners.delete(listener) - }, - }), - ) - } - - /** - * Execute each listener in sequence - * @param message the message to apply to each listener - */ - forEachListener: ProviderListener = (message) => { - for (const listener of this.listeners.values()) { - listener(message) - } - } -} - -export function nextIdFactory() { - let i = 0 - return () => i++ -} diff --git a/rpc/provider/errors.ts b/rpc/provider/errors.ts deleted file mode 100644 index 24cf4bb90..000000000 --- a/rpc/provider/errors.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { msg } from "../mod.ts" - -export class ProviderSendError extends Error { - override readonly name = "ProviderSendError" - constructor( - override readonly cause: Data, - readonly egressMessage: msg.EgressMessage, - ) { - super() - } -} - -export class ProviderHandlerError extends Error { - override readonly name = "ProviderHandlerError" - constructor(override readonly cause: Data) { - super() - } -} - -export class ProviderCloseError extends Error { - override readonly name = "ProviderCloseError" - constructor(override readonly cause: Data) { - super() - } -} diff --git a/rpc/provider/proxy.test.ts b/rpc/provider/proxy.test.ts deleted file mode 100644 index a0641456b..000000000 --- a/rpc/provider/proxy.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { discoveryValue } from "westend/mod.ts" -import * as A from "../../deps/std/testing/asserts.ts" -import { proxyProvider } from "./proxy.ts" -import { setup } from "./test_util.ts" - -Deno.test({ - name: "Proxy Provider", - async fn(t) { - await t.step({ - name: "send/listen", - async fn() { - const [ref, message] = await setup(proxyProvider, discoveryValue, "system_health", []) - A.assertNotInstanceOf(message, Error) - A.assertExists(message.result) - A.assertNotInstanceOf(await ref.release(), Error) - }, - }) - - await t.step({ - name: "create WebSocket error", - async fn() { - const [ref, message] = await setup( - proxyProvider, - "invalid-endpoint-url", - "system_health", - [], - ) - A.assertInstanceOf(message, Error) - A.assertNotInstanceOf(await ref.release(), Error) - }, - }) - - await t.step({ - name: "close WebSocket while listening", - async fn() { - const server = createWebSocketServer(function() { - this.close() - }) - const [ref, message] = await setup( - proxyProvider, - server.url, - "system_health", - [], - ) - A.assertInstanceOf(message, Error) - A.assertNotInstanceOf(await ref.release(), Error) - server.close() - }, - }) - - await t.step({ - name: "send non-JSON message", - async fn() { - const server = createWebSocketServer() - // make JSON.stringify throw on bigint - const [ref, message] = await setup(proxyProvider, server.url, "system_health", [1n]) - A.assertInstanceOf(message, Error) - A.assertNotInstanceOf(await ref.release(), Error) - server.close() - }, - }) - }, -}) - -function createWebSocketServer(onMessage?: WebSocket["onmessage"]) { - const onmessage = onMessage ?? (() => {}) - const listener = Deno.listen({ port: 0 }) - ;(async () => { - for await (const conn of listener) { - for await (const e of Deno.serveHttp(conn)) { - const { socket, response } = Deno.upgradeWebSocket(e.request) - socket.onmessage = onmessage - e.respondWith(response) - } - } - })() - return { - close: () => listener.close(), - url: `ws://localhost:${(listener.addr as Deno.NetAddr).port}`, - } -} diff --git a/rpc/provider/proxy.ts b/rpc/provider/proxy.ts deleted file mode 100644 index fa9a1f586..000000000 --- a/rpc/provider/proxy.ts +++ /dev/null @@ -1,113 +0,0 @@ -import * as U from "../../util/mod.ts" -import * as msg from "../messages.ts" -import { nextIdFactory, Provider, ProviderConnection, ProviderListener } from "./base.ts" -import { ProviderCloseError, ProviderHandlerError, ProviderSendError } from "./errors.ts" - -/** Global lookup of existing connections */ -const connections = new Map() -type ProxyProviderConnection = ProviderConnection - -const nextId = nextIdFactory() - -export const proxyProvider: Provider = (url, listener) => { - return { - nextId, - send: (message) => { - let conn: ProxyProviderConnection - try { - conn = connection(url, listener) - } catch (error) { - listener(new ProviderHandlerError(error as Event)) - return - } - ;(async () => { - const openError = await ensureWsOpen(conn.inner) - if (openError) { - conn.forEachListener(new ProviderSendError(openError, message)) - return - } - try { - conn.inner.send(JSON.stringify(message)) - } catch (error) { - listener(new ProviderSendError(error as Event, message)) - } - })() - }, - release: () => { - const conn = connections.get(url) - if (!conn) { - return Promise.resolve(undefined) - } - const { cleanUp, listeners, inner } = conn - listeners.delete(listener) - if (!listeners.size) { - connections.delete(url) - cleanUp() - return closeWs(inner) - } - return Promise.resolve(undefined) - }, - } -} - -function connection( - url: string, - listener: ProviderListener, -): ProxyProviderConnection { - const conn = U.getOrInit(connections, url, () => { - const controller = new AbortController() - const ws = new WebSocket(url) - ws.addEventListener("message", (e) => { - conn.forEachListener(msg.parse(e.data)) - }, controller) - ws.addEventListener("error", (e) => { - conn.forEachListener(new ProviderHandlerError(e)) - }, controller) - ws.addEventListener("close", (e) => { - conn.forEachListener(new ProviderHandlerError(e)) - }, controller) - return new ProviderConnection(ws, () => { - controller.abort() - }) - }) - conn.addListener(listener) - return conn -} - -function ensureWsOpen(ws: WebSocket): Promise { - if (ws.readyState === WebSocket.OPEN) { - return Promise.resolve(undefined) - } else if (ws.readyState === WebSocket.CLOSING || ws.readyState === WebSocket.CLOSED) { - return Promise.resolve(new Event("error")) - } else { - return new Promise((resolve) => { - const controller = new AbortController() - ws.addEventListener("open", () => { - controller.abort() - resolve(undefined) - }, controller) - ws.addEventListener("error", (e) => { - controller.abort() - resolve(e) - }, controller) - }) - } -} - -function closeWs(socket: WebSocket): Promise> { - if (socket.readyState === WebSocket.CLOSED) { - return Promise.resolve(undefined) - } - return new Promise>((resolve) => { - const controller = new AbortController() - socket.addEventListener("close", () => { - controller.abort() - resolve(undefined) - }, controller) - socket.addEventListener("error", (e) => { - controller.abort() - resolve(new ProviderCloseError(e)) - }, controller) - socket.close() - }) -} diff --git a/rpc/provider/smoldot.test.ts b/rpc/provider/smoldot.test.ts deleted file mode 100644 index 04e0c1371..000000000 --- a/rpc/provider/smoldot.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { deferred } from "../../deps/std/async.ts" -import { - assertExists, - assertInstanceOf, - assertNotInstanceOf, -} from "../../deps/std/testing/asserts.ts" -import { ProviderListener } from "./base.ts" -import { smoldotProvider } from "./smoldot.ts" -import { setup } from "./test_util.ts" - -Deno.test({ - name: "Smoldot Provider", - sanitizeOps: false, - sanitizeResources: false, - async fn(t) { - await t.step({ - name: "relay chain connection", - async fn() { - const relay = await fetchText( - "https://raw.githubusercontent.com/paritytech/substrate-connect/main/packages/connect/src/connector/specs/polkadot.json", - ) - const pendingSubscriptionId = deferred() - const initialized = deferred() - const unsubscribed = deferred() - const checks: ProviderListener[] = [ - // check for chainHead_unstable_follow subscription - (message) => { - assertNotInstanceOf(message, Error) - assertExists(message.result) - pendingSubscriptionId.resolve(message.result) - }, - // check for chainHead_unstable_follow initialized event - (message) => { - assertNotInstanceOf(message, Error) - assertExists(message.params?.result) - if (message.params?.result.event === "initialized") { - initialized.resolve() - } - }, - // check for chainHead_unstable_unfollow unsubscribe - (message) => { - assertNotInstanceOf(message, Error) - if (message?.result === null) { - unsubscribed.resolve() - } - }, - ] - const provider = smoldotProvider({ chainSpec: { relay } }, (message) => { - if (checks.length > 1) { - checks.shift()!(message) - } else { - checks[0]!(message) - } - }) - provider.send({ - jsonrpc: "2.0", - id: provider.nextId(), - method: "chainHead_unstable_follow", - params: [false], - }) - const subscriptionId = await pendingSubscriptionId - await initialized - provider.send({ - jsonrpc: "2.0", - id: provider.nextId(), - method: "chainHead_unstable_unfollow", - params: [subscriptionId], - }) - await unsubscribed - const providerRelease = await provider.release() - assertNotInstanceOf(providerRelease, Error) - }, - }) - await t.step({ - name: "parachain connection", - async fn() { - const relay = await fetchText( - "https://raw.githubusercontent.com/paritytech/substrate-connect/main/packages/connect/src/connector/specs/westend2.json", - ) - const para = await fetchText( - "https://raw.githubusercontent.com/paritytech/substrate-connect/main/projects/demo/src/assets/westend-westmint.json", - ) - - const pendingSubscriptionId = deferred() - const initialized = deferred() - const unsubscribed = deferred() - const checks: ProviderListener[] = [ - // check for chainHead_unstable_follow subscription - (message) => { - assertNotInstanceOf(message, Error) - assertExists(message.result) - pendingSubscriptionId.resolve(message.result) - }, - // check for chainHead_unstable_follow initialized event - (message) => { - assertNotInstanceOf(message, Error) - assertExists(message.params?.result) - if (message.params?.result.event === "initialized") { - initialized.resolve() - } - }, - // check for chainHead_unstable_unfollow unsubscribe - (message) => { - assertNotInstanceOf(message, Error) - if (message?.result === null) { - unsubscribed.resolve() - } - }, - ] - const provider = smoldotProvider( - { chainSpec: { para, relay } }, - (message) => { - if (checks.length > 1) { - checks.shift()!(message) - } else { - checks[0]!(message) - } - }, - ) - provider.send({ - jsonrpc: "2.0", - id: provider.nextId(), - method: "chainHead_unstable_follow", - params: [false], - }) - const subscriptionId = await pendingSubscriptionId - await initialized - provider.send({ - jsonrpc: "2.0", - id: provider.nextId(), - method: "chainHead_unstable_unfollow", - params: [subscriptionId], - }) - await unsubscribed - const providerRelease = await provider.release() - assertNotInstanceOf(providerRelease, Error) - }, - }) - - await t.step({ - name: "invalid chain spec", - async fn() { - const [ref, message] = await setup( - smoldotProvider, - { chainSpec: { relay: "" } }, - "system_health", - [false], - ) - assertInstanceOf(message, Error) - assertNotInstanceOf(await ref.release(), Error) - }, - }) - await t.step({ - name: "send non-JSON", - async fn() { - const relay = await fetchText( - "https://raw.githubusercontent.com/paritytech/substrate-connect/main/packages/connect/src/connector/specs/polkadot.json", - ) - const [ref, message] = await setup( - smoldotProvider, - { chainSpec: { relay } }, - "system_health", - // make JSON.stringify to throw - [1n], - ) - assertInstanceOf(message, Error) - assertNotInstanceOf(await ref.release(), Error) - }, - }) - }, -}) - -async function fetchText(url: string) { - return (await fetch(url)).text() -} diff --git a/rpc/provider/smoldot.ts b/rpc/provider/smoldot.ts deleted file mode 100644 index 4d487c2a2..000000000 --- a/rpc/provider/smoldot.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { - AddChainError, - AlreadyDestroyedError, - CrashError, - JsonRpcDisabledError, - MalformedJsonRpcError, - QueueFullError, - start, -} from "../../deps/smoldot.ts" -import { Chain, Client, ClientOptions } from "../../deps/smoldot/client.d.ts" -import { deferred } from "../../deps/std/async.ts" -import * as msg from "../messages.ts" -import { nextIdFactory, Provider, ProviderConnection, ProviderListener } from "./base.ts" -import { ProviderCloseError, ProviderHandlerError, ProviderSendError } from "./errors.ts" - -type SmoldotSendErrorData = - | AlreadyDestroyedError - | CrashError - | JsonRpcDisabledError - | MalformedJsonRpcError - | QueueFullError -type SmoldotHandlerErrorData = - | AlreadyDestroyedError - | CrashError - | JsonRpcDisabledError - | AddChainError -type SmoldotCloseErrorData = AlreadyDestroyedError | CrashError - -let client: undefined | Client -const connections = new Map() -class SmoldotProviderConnection - extends ProviderConnection -{} - -const nextId = nextIdFactory() - -export interface SmoldotProviderProps { - chainSpec: { - relay: string - para?: string - } - // TODO: support deferring closing (how / what heuristic?) - deferClosing?: boolean -} - -export const smoldotProvider: Provider< - SmoldotProviderProps, - SmoldotSendErrorData, - SmoldotHandlerErrorData, - SmoldotCloseErrorData -> = (props, listener) => { - return { - nextId, - send: (message) => { - ;(async () => { - let conn: SmoldotProviderConnection - try { - conn = await connection(props, listener) - } catch (error) { - listener(new ProviderHandlerError(error as SmoldotHandlerErrorData)) - return - } - try { - conn.inner.sendJsonRpc(JSON.stringify(message)) - } catch (error) { - listener(new ProviderSendError(error as SmoldotSendErrorData, message)) - } - })() - }, - release: () => { - const conn = connections.get(props) - if (!conn) { - return Promise.resolve(undefined) - } - const { cleanUp, listeners, inner } = conn - listeners.delete(listener) - if (!listeners.size) { - connections.delete(props) - cleanUp() - try { - // TODO: utilize `deferClosing` prop once we flesh out approach - inner.remove() - } catch (e) { - return Promise.resolve(new ProviderCloseError(e as SmoldotCloseErrorData)) - } - } - return Promise.resolve(undefined) - }, - } -} - -async function connection( - props: SmoldotProviderProps, - listener: ProviderListener, -): Promise { - if (!client) { - client = start( - { - forbidTcp: true, - forbidNonLocalWs: true, - cpuRateLimit: .25, - } as ClientOptions, - ) - } - let conn = connections.get(props) - if (!conn) { - let inner: Chain - if (props.chainSpec.para) { - const relayChainConnection = await client.addChain({ - chainSpec: props.chainSpec.relay, - disableJsonRpc: true, - }) - inner = await client.addChain({ - chainSpec: props.chainSpec.para, - potentialRelayChains: [relayChainConnection], - }) - } else { - inner = await client.addChain({ chainSpec: props.chainSpec.relay }) - } - const stopListening = deferred() - conn = new SmoldotProviderConnection(inner, () => stopListening.resolve()) - connections.set(props, conn) - ;(async () => { - while (true) { - try { - const response = await Promise.race([ - stopListening, - inner.nextJsonRpcResponse(), - ]) - if (!response) { - break - } - const message = msg.parse(response) - conn!.forEachListener(message) - } catch (e) { - conn!.forEachListener(new ProviderHandlerError(e as SmoldotHandlerErrorData)) - break - } - } - })() - } - conn.addListener(listener) - return conn -} diff --git a/rpc/provider/test_util.ts b/rpc/provider/test_util.ts deleted file mode 100644 index 217535487..000000000 --- a/rpc/provider/test_util.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Provider, ProviderRef } from "./base.ts" - -export function setup( - provider: Provider, - discoveryValue: any, - method: string, - params: unknown[], -): Promise<[ProviderRef, any]> { - return new Promise((resolve) => { - const providerRef = provider(discoveryValue, (message) => resolve([providerRef, message])) - providerRef.send({ - jsonrpc: "2.0", - id: providerRef.nextId(), - method, - params, - }) - }) -} diff --git a/rpc/rpc_messages.ts b/rpc/rpc_messages.ts new file mode 100644 index 000000000..d85a2b458 --- /dev/null +++ b/rpc/rpc_messages.ts @@ -0,0 +1,50 @@ +export interface RpcEgressMessage extends RpcVersionBearer, RpcMessageIdBearer { + method: string + params: any[] +} + +export type RpcHandler = ( + message: Message, +) => void + +export type RpcIngressMessage = RpcOkMessage | RpcErrorMessage | RpcNotificationMessage + +export interface RpcOkMessage extends RpcVersionBearer, RpcMessageIdBearer { + result: OkData + params?: never + error?: never +} + +export interface RpcErrorMessage + extends RpcVersionBearer, RpcMessageIdBearer +{ + error: ErrorData + params?: never + result?: never +} +export interface RpcErrorMessageData { + code: Code + message: string + data: Data +} + +export interface RpcNotificationMessage extends RpcVersionBearer { + method: string // we could narrow, but it's not all that useful + id?: never + params: { + subscription: string + result: NotificationData + } + result?: never + error?: never +} + +export type RpcVersion = "2.0" +interface RpcVersionBearer { + jsonrpc: RpcVersion +} + +export type RpcMessageId = number | string +export interface RpcMessageIdBearer { + id: RpcMessageId +} diff --git a/rpc/client.test.ts b/rpc/todo/client.test.ts similarity index 100% rename from rpc/client.test.ts rename to rpc/todo/client.test.ts diff --git a/rpc/todo/client.ts b/rpc/todo/client.ts new file mode 100644 index 000000000..b5fe60356 --- /dev/null +++ b/rpc/todo/client.ts @@ -0,0 +1,186 @@ +// import { Deferred, deferred } from "../deps/std/async.ts" +// import * as U from "../util/mod.ts" +// import * as msg from "./messages.ts" +// import { Provider, ProviderListener } from "./provider/base.ts" +// import { ProviderHandlerError, ProviderSendError } from "./provider/errors.ts" + +// export class Client< +// DiscoveryValue = any, +// SendErrorData = any, +// HandlerErrorData = any, +// CloseErrorData = any, +// > { +// providerRef +// pendingCalls: Record> = {} +// pendingSubscriptions: SubscriptionListeners = {} +// activeSubscriptions: SubscriptionListeners = {} +// activeSubscriptionByMessageId: Record = {} + +// constructor( +// readonly provider: Provider, +// readonly discoveryValue: DiscoveryValue, +// ) { +// this.providerRef = provider(discoveryValue, this.listener) +// } + +// listener: ProviderListener = (e) => { +// if (e instanceof ProviderSendError) { +// const egressMessageId = e.egressMessage.id +// const pendingCall = this.pendingCalls[egressMessageId] +// pendingCall?.resolve(e) +// delete this.pendingCalls[egressMessageId] +// } else if (e instanceof Error) { +// for (const id in this.pendingCalls) { +// const pendingCall = this.pendingCalls[id]! +// pendingCall.resolve(e) +// delete this.pendingCalls[id] +// this.pendingSubscriptions[id]?.(e) +// delete this.pendingSubscriptions[id] +// } +// for (const id in this.activeSubscriptions) { +// this.activeSubscriptions[id]!(e) +// delete this.activeSubscriptions[id] +// } +// } else if (e.id !== undefined) { +// const pendingCall = this.pendingCalls[e.id] +// pendingCall?.resolve(e) +// delete this.pendingCalls[e.id] +// if (this.pendingSubscriptions[e.id]) { +// if (e.error) { +// this.pendingSubscriptions[e.id]!(e) +// } else { +// this.activeSubscriptions[e.result] = this.pendingSubscriptions[e.id]! +// this.activeSubscriptionByMessageId[e.id] = e.result +// } +// delete this.pendingSubscriptions[e.id] +// } +// } else if (e.params) { +// this.activeSubscriptions[e.params.subscription]?.(e) +// } +// } + +// call: ClientCall = (id, method, params) => { +// const waiter = deferred>() +// this.pendingCalls[id] = waiter +// this.providerRef.send({ +// jsonrpc: "2.0", +// id, +// method, +// params, +// }) +// return waiter +// } + +// subscriptionFactory = < +// Params extends unknown[] = any[], +// NotificationData = any, +// >(): SubscriptionFactory => +// ( +// subscribeMethod, +// unsubscribeMethod, +// params, +// listener, +// abortSignal, +// ) => { +// const id = this.providerRef.nextId() +// abortSignal.addEventListener("abort", async () => { +// delete this.pendingSubscriptions[id] +// const activeSubscriptionId = this.activeSubscriptionByMessageId[id] +// if (activeSubscriptionId) { +// delete this.activeSubscriptions[activeSubscriptionId] +// await this.call( +// this.providerRef.nextId(), +// unsubscribeMethod, +// [activeSubscriptionId], +// ) +// } +// delete this.activeSubscriptionByMessageId[id] +// }) +// this.call(id, subscribeMethod, params).then((x) => { +// if (x instanceof Error || x.error) { +// listener(x) +// } +// }) +// this.pendingSubscriptions[id] = listener +// } + +// discard = () => { +// this.pendingCalls = {} +// this.pendingSubscriptions = {} +// this.activeSubscriptions = {} +// this.activeSubscriptionByMessageId = {} +// return this.providerRef.release() +// } +// } + +// export type ClientCallEvent = +// | msg.OkMessage +// | msg.ErrorMessage +// | ProviderSendError +// | ProviderHandlerError + +// export type ClientCall = ( +// id: number | string, +// method: string, +// params: unknown[], +// ) => Promise> + +// export type ClientSubscriptionEvent< +// Method extends string, +// Result, +// SendErrorData, +// HandlerErrorData, +// > = +// | msg.NotificationMessage +// | msg.ErrorMessage +// | ProviderSendError +// | ProviderHandlerError + +// type SubscriptionListeners = Record< +// string, +// SubscriptionListener< +// any, +// any, +// SendErrorData, +// HandlerErrorData +// > +// > + +// type SubscriptionListener< +// SubscribeMethod extends string, +// NotificationData, +// SendErrorData, +// HandlerErrorData, +// > = ( +// value: ClientSubscriptionEvent< +// SubscribeMethod, +// NotificationData, +// SendErrorData, +// HandlerErrorData +// >, +// ) => void + +// export type SubscriptionFactory< +// Params extends unknown[], +// NotificationData, +// SendErrorData, +// HandlerErrorData, +// > = < +// SubscribeMethod extends string, +// >( +// subscribeMethod: SubscribeMethod, +// unsubscribeMethod: string, +// params: [...Params], +// listener: SubscriptionListener< +// SubscribeMethod, +// NotificationData, +// SendErrorData, +// HandlerErrorData +// >, +// abortSignal: AbortSignal, +// ) => void + +// export interface ClientSubscriptionContext { +// end: (value?: T) => U.End +// endIfError(value: I): U.End> +// } diff --git a/rpc/todo/proxy.test.ts b/rpc/todo/proxy.test.ts new file mode 100644 index 000000000..5984824d8 --- /dev/null +++ b/rpc/todo/proxy.test.ts @@ -0,0 +1,81 @@ +// import { discoveryValue } from "westend/mod.ts" +// import * as A from "../../deps/std/testing/asserts.ts" +// import { proxyProvider } from "./proxy.ts" +// import { setup } from "./test_util.ts" + +// Deno.test({ +// name: "Proxy Provider", +// async fn(t) { +// await t.step({ +// name: "send/listen", +// async fn() { +// const [ref, message] = await setup(proxyProvider, discoveryValue, "system_health", []) +// A.assertNotInstanceOf(message, Error) +// A.assertExists(message.result) +// A.assertNotInstanceOf(await ref.release(), Error) +// }, +// }) + +// await t.step({ +// name: "create WebSocket error", +// async fn() { +// const [ref, message] = await setup( +// proxyProvider, +// "invalid-endpoint-url", +// "system_health", +// [], +// ) +// A.assertInstanceOf(message, Error) +// A.assertNotInstanceOf(await ref.release(), Error) +// }, +// }) + +// await t.step({ +// name: "close WebSocket while listening", +// async fn() { +// const server = createWebSocketServer(function() { +// this.close() +// }) +// const [ref, message] = await setup( +// proxyProvider, +// server.url, +// "system_health", +// [], +// ) +// A.assertInstanceOf(message, Error) +// A.assertNotInstanceOf(await ref.release(), Error) +// server.close() +// }, +// }) + +// await t.step({ +// name: "send non-JSON message", +// async fn() { +// const server = createWebSocketServer() +// // make JSON.stringify throw on bigint +// const [ref, message] = await setup(proxyProvider, server.url, "system_health", [1n]) +// A.assertInstanceOf(message, Error) +// A.assertNotInstanceOf(await ref.release(), Error) +// server.close() +// }, +// }) +// }, +// }) + +// function createWebSocketServer(onMessage?: WebSocket["onmessage"]) { +// const onmessage = onMessage ?? (() => {}) +// const listener = Deno.listen({ port: 0 }) +// ;(async () => { +// for await (const conn of listener) { +// for await (const e of Deno.serveHttp(conn)) { +// const { socket, response } = Deno.upgradeWebSocket(e.request) +// socket.onmessage = onmessage +// e.respondWith(response) +// } +// } +// })() +// return { +// close: () => listener.close(), +// url: `ws://localhost:${(listener.addr as Deno.NetAddr).port}`, +// } +// } diff --git a/rpc/todo/proxy.ts b/rpc/todo/proxy.ts new file mode 100644 index 000000000..89e43a47a --- /dev/null +++ b/rpc/todo/proxy.ts @@ -0,0 +1,113 @@ +// import * as U from "../../util/mod.ts" +// import * as msg from "../messages.ts" +// import { nextIdFactory, Provider, ProviderConnection, ProviderListener } from "./base.ts" +// import { ProviderCloseError, ProviderHandlerError, ProviderSendError } from "./errors.ts" + +// /** Global lookup of existing connections */ +// const connections = new Map() +// type ProxyProviderConnection = ProviderConnection + +// const nextId = nextIdFactory() + +// export const proxyProvider: Provider = (url, listener) => { +// return { +// nextId, +// send: (message) => { +// let conn: ProxyProviderConnection +// try { +// conn = connection(url, listener) +// } catch (error) { +// listener(new ProviderHandlerError(error as Event)) +// return +// } +// ;(async () => { +// const openError = await ensureWsOpen(conn.inner) +// if (openError) { +// conn.forEachListener(new ProviderSendError(openError, message)) +// return +// } +// try { +// conn.inner.send(JSON.stringify(message)) +// } catch (error) { +// listener(new ProviderSendError(error as Event, message)) +// } +// })() +// }, +// release: () => { +// const conn = connections.get(url) +// if (!conn) { +// return Promise.resolve(undefined) +// } +// const { cleanUp, listeners, inner } = conn +// listeners.delete(listener) +// if (!listeners.size) { +// connections.delete(url) +// cleanUp() +// return closeWs(inner) +// } +// return Promise.resolve(undefined) +// }, +// } +// } + +// function connection( +// url: string, +// listener: ProviderListener, +// ): ProxyProviderConnection { +// const conn = U.getOrInit(connections, url, () => { +// const controller = new AbortController() +// const ws = new WebSocket(url) +// ws.addEventListener("message", (e) => { +// conn.forEachListener(msg.parse(e.data)) +// }, controller) +// ws.addEventListener("error", (e) => { +// conn.forEachListener(new ProviderHandlerError(e)) +// }, controller) +// ws.addEventListener("close", (e) => { +// conn.forEachListener(new ProviderHandlerError(e)) +// }, controller) +// return new ProviderConnection(ws, () => { +// controller.abort() +// }) +// }) +// conn.addListener(listener) +// return conn +// } + +// function ensureWsOpen(ws: WebSocket): Promise { +// if (ws.readyState === WebSocket.OPEN) { +// return Promise.resolve(undefined) +// } else if (ws.readyState === WebSocket.CLOSING || ws.readyState === WebSocket.CLOSED) { +// return Promise.resolve(new Event("error")) +// } else { +// return new Promise((resolve) => { +// const controller = new AbortController() +// ws.addEventListener("open", () => { +// controller.abort() +// resolve(undefined) +// }, controller) +// ws.addEventListener("error", (e) => { +// controller.abort() +// resolve(e) +// }, controller) +// }) +// } +// } + +// function closeWs(socket: WebSocket): Promise> { +// if (socket.readyState === WebSocket.CLOSED) { +// return Promise.resolve(undefined) +// } +// return new Promise>((resolve) => { +// const controller = new AbortController() +// socket.addEventListener("close", () => { +// controller.abort() +// resolve(undefined) +// }, controller) +// socket.addEventListener("error", (e) => { +// controller.abort() +// resolve(new ProviderCloseError(e)) +// }, controller) +// socket.close() +// }) +// } diff --git a/rpc/todo/smoldot.test.ts b/rpc/todo/smoldot.test.ts new file mode 100644 index 000000000..12f13953e --- /dev/null +++ b/rpc/todo/smoldot.test.ts @@ -0,0 +1,175 @@ +// import { deferred } from "../../deps/std/async.ts" +// import { +// assertExists, +// assertInstanceOf, +// assertNotInstanceOf, +// } from "../../deps/std/testing/asserts.ts" +// import { ProviderListener } from "./base.ts" +// import { smoldotProvider } from "./smoldot.ts" +// import { setup } from "./test_util.ts" + +// Deno.test({ +// name: "Smoldot Provider", +// sanitizeOps: false, +// sanitizeResources: false, +// async fn(t) { +// await t.step({ +// name: "relay chain connection", +// async fn() { +// const relay = await fetchText( +// "https://raw.githubusercontent.com/paritytech/substrate-connect/main/packages/connect/src/connector/specs/polkadot.json", +// ) +// const pendingSubscriptionId = deferred() +// const initialized = deferred() +// const unsubscribed = deferred() +// const checks: ProviderListener[] = [ +// // check for chainHead_unstable_follow subscription +// (message) => { +// assertNotInstanceOf(message, Error) +// assertExists(message.result) +// pendingSubscriptionId.resolve(message.result) +// }, +// // check for chainHead_unstable_follow initialized event +// (message) => { +// assertNotInstanceOf(message, Error) +// assertExists(message.params?.result) +// if (message.params?.result.event === "initialized") { +// initialized.resolve() +// } +// }, +// // check for chainHead_unstable_unfollow unsubscribe +// (message) => { +// assertNotInstanceOf(message, Error) +// if (message?.result === null) { +// unsubscribed.resolve() +// } +// }, +// ] +// const provider = smoldotProvider({ chainSpec: { relay } }, (message) => { +// if (checks.length > 1) { +// checks.shift()!(message) +// } else { +// checks[0]!(message) +// } +// }) +// provider.send({ +// jsonrpc: "2.0", +// id: provider.nextId(), +// method: "chainHead_unstable_follow", +// params: [false], +// }) +// const subscriptionId = await pendingSubscriptionId +// await initialized +// provider.send({ +// jsonrpc: "2.0", +// id: provider.nextId(), +// method: "chainHead_unstable_unfollow", +// params: [subscriptionId], +// }) +// await unsubscribed +// const providerRelease = await provider.release() +// assertNotInstanceOf(providerRelease, Error) +// }, +// }) +// await t.step({ +// name: "parachain connection", +// async fn() { +// const relay = await fetchText( +// "https://raw.githubusercontent.com/paritytech/substrate-connect/main/packages/connect/src/connector/specs/westend2.json", +// ) +// const para = await fetchText( +// "https://raw.githubusercontent.com/paritytech/substrate-connect/main/projects/demo/src/assets/westend-westmint.json", +// ) + +// const pendingSubscriptionId = deferred() +// const initialized = deferred() +// const unsubscribed = deferred() +// const checks: ProviderListener[] = [ +// // check for chainHead_unstable_follow subscription +// (message) => { +// assertNotInstanceOf(message, Error) +// assertExists(message.result) +// pendingSubscriptionId.resolve(message.result) +// }, +// // check for chainHead_unstable_follow initialized event +// (message) => { +// assertNotInstanceOf(message, Error) +// assertExists(message.params?.result) +// if (message.params?.result.event === "initialized") { +// initialized.resolve() +// } +// }, +// // check for chainHead_unstable_unfollow unsubscribe +// (message) => { +// assertNotInstanceOf(message, Error) +// if (message?.result === null) { +// unsubscribed.resolve() +// } +// }, +// ] +// const provider = smoldotProvider( +// { chainSpec: { para, relay } }, +// (message) => { +// if (checks.length > 1) { +// checks.shift()!(message) +// } else { +// checks[0]!(message) +// } +// }, +// ) +// provider.send({ +// jsonrpc: "2.0", +// id: provider.nextId(), +// method: "chainHead_unstable_follow", +// params: [false], +// }) +// const subscriptionId = await pendingSubscriptionId +// await initialized +// provider.send({ +// jsonrpc: "2.0", +// id: provider.nextId(), +// method: "chainHead_unstable_unfollow", +// params: [subscriptionId], +// }) +// await unsubscribed +// const providerRelease = await provider.release() +// assertNotInstanceOf(providerRelease, Error) +// }, +// }) + +// await t.step({ +// name: "invalid chain spec", +// async fn() { +// const [ref, message] = await setup( +// smoldotProvider, +// { chainSpec: { relay: "" } }, +// "system_health", +// [false], +// ) +// assertInstanceOf(message, Error) +// assertNotInstanceOf(await ref.release(), Error) +// }, +// }) +// await t.step({ +// name: "send non-JSON", +// async fn() { +// const relay = await fetchText( +// "https://raw.githubusercontent.com/paritytech/substrate-connect/main/packages/connect/src/connector/specs/polkadot.json", +// ) +// const [ref, message] = await setup( +// smoldotProvider, +// { chainSpec: { relay } }, +// "system_health", +// // make JSON.stringify to throw +// [1n], +// ) +// assertInstanceOf(message, Error) +// assertNotInstanceOf(await ref.release(), Error) +// }, +// }) +// }, +// }) + +// async function fetchText(url: string) { +// return (await fetch(url)).text() +// } diff --git a/rpc/todo/smoldot.ts b/rpc/todo/smoldot.ts new file mode 100644 index 000000000..3d87a690c --- /dev/null +++ b/rpc/todo/smoldot.ts @@ -0,0 +1,144 @@ +// import { +// AddChainError, +// AlreadyDestroyedError, +// CrashError, +// JsonRpcDisabledError, +// MalformedJsonRpcError, +// QueueFullError, +// start, +// } from "../../deps/smoldot.ts" +// import { Chain, Client, ClientOptions } from "../../deps/smoldot/client.d.ts" +// import { deferred } from "../../deps/std/async.ts" +// import * as msg from "../messages.ts" +// import { nextIdFactory, Provider, ProviderConnection, ProviderListener } from "./base.ts" +// import { ProviderCloseError, ProviderHandlerError, ProviderSendError } from "./errors.ts" + +// type SmoldotSendErrorData = +// | AlreadyDestroyedError +// | CrashError +// | JsonRpcDisabledError +// | MalformedJsonRpcError +// | QueueFullError +// type SmoldotHandlerErrorData = +// | AlreadyDestroyedError +// | CrashError +// | JsonRpcDisabledError +// | AddChainError +// type SmoldotCloseErrorData = AlreadyDestroyedError | CrashError + +// let client: undefined | Client +// const connections = new Map() +// class SmoldotProviderConnection +// extends ProviderConnection +// {} + +// const nextId = nextIdFactory() + +// export interface SmoldotProviderProps { +// chainSpec: { +// relay: string +// para?: string +// } +// // TODO: support deferring closing (how / what heuristic?) +// deferClosing?: boolean +// } + +// export const smoldotProvider: Provider< +// SmoldotProviderProps, +// SmoldotSendErrorData, +// SmoldotHandlerErrorData, +// SmoldotCloseErrorData +// > = (props, listener) => { +// return { +// nextId, +// send: (message) => { +// ;(async () => { +// let conn: SmoldotProviderConnection +// try { +// conn = await connection(props, listener) +// } catch (error) { +// listener(new ProviderHandlerError(error as SmoldotHandlerErrorData)) +// return +// } +// try { +// conn.inner.sendJsonRpc(JSON.stringify(message)) +// } catch (error) { +// listener(new ProviderSendError(error as SmoldotSendErrorData, message)) +// } +// })() +// }, +// release: () => { +// const conn = connections.get(props) +// if (!conn) { +// return Promise.resolve(undefined) +// } +// const { cleanUp, listeners, inner } = conn +// listeners.delete(listener) +// if (!listeners.size) { +// connections.delete(props) +// cleanUp() +// try { +// // TODO: utilize `deferClosing` prop once we flesh out approach +// inner.remove() +// } catch (e) { +// return Promise.resolve(new ProviderCloseError(e as SmoldotCloseErrorData)) +// } +// } +// return Promise.resolve(undefined) +// }, +// } +// } + +// async function connection( +// props: SmoldotProviderProps, +// listener: ProviderListener, +// ): Promise { +// if (!client) { +// client = start( +// { +// forbidTcp: true, +// forbidNonLocalWs: true, +// cpuRateLimit: .25, +// } as ClientOptions, +// ) +// } +// let conn = connections.get(props) +// if (!conn) { +// let inner: Chain +// if (props.chainSpec.para) { +// const relayChainConnection = await client.addChain({ +// chainSpec: props.chainSpec.relay, +// disableJsonRpc: true, +// }) +// inner = await client.addChain({ +// chainSpec: props.chainSpec.para, +// potentialRelayChains: [relayChainConnection], +// }) +// } else { +// inner = await client.addChain({ chainSpec: props.chainSpec.relay }) +// } +// const stopListening = deferred() +// conn = new SmoldotProviderConnection(inner, () => stopListening.resolve()) +// connections.set(props, conn) +// ;(async () => { +// while (true) { +// try { +// const response = await Promise.race([ +// stopListening, +// inner.nextJsonRpcResponse(), +// ]) +// if (!response) { +// break +// } +// const message = msg.parse(response) +// conn!.forEachListener(message) +// } catch (e) { +// conn!.forEachListener(new ProviderHandlerError(e as SmoldotHandlerErrorData)) +// break +// } +// } +// })() +// } +// conn.addListener(listener) +// return conn +// } diff --git a/rpc/todo/test_util.ts b/rpc/todo/test_util.ts new file mode 100644 index 000000000..d298723e4 --- /dev/null +++ b/rpc/todo/test_util.ts @@ -0,0 +1,18 @@ +// import { Provider, ProviderRef } from "./base.ts" + +// export function setup( +// provider: Provider, +// discoveryValue: any, +// method: string, +// params: unknown[], +// ): Promise<[ProviderRef, any]> { +// return new Promise((resolve) => { +// const providerRef = provider(discoveryValue, (message) => resolve([providerRef, message])) +// providerRef.send({ +// jsonrpc: "2.0", +// id: providerRef.nextId(), +// method, +// params, +// }) +// }) +// } diff --git a/util/types.ts b/util/types.ts index 7570aff41..bd57d51bf 100644 --- a/util/types.ts +++ b/util/types.ts @@ -1 +1,5 @@ export type PromiseOr = T | Promise + +export interface SignalBearer { + signal: AbortSignal +} From 79035d0119c5d85a9023914c884901f35c2491df Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Thu, 16 Feb 2023 14:24:21 -0500 Subject: [PATCH 02/23] generalize rpc providers continued --- fluent/ClientRune.ts | 2 +- fluent/rpc_runes.ts | 67 +++++++++++++---------------- providers/frame/FrameProvider.ts | 18 +++++--- rpc/RpcClient.ts | 6 +-- rpc/RpcClientError.ts | 3 -- rpc/RpcProvider.ts | 74 ++++++-------------------------- rpc/errors.ts | 18 ++++++++ rpc/mod.ts | 3 +- rpc/wsRpcProvider.ts | 64 +++++++++++++++++++++++++++ 9 files changed, 141 insertions(+), 114 deletions(-) delete mode 100644 rpc/RpcClientError.ts create mode 100644 rpc/errors.ts create mode 100644 rpc/wsRpcProvider.ts diff --git a/fluent/ClientRune.ts b/fluent/ClientRune.ts index 785a9b7d8..0fe3b8f28 100644 --- a/fluent/ClientRune.ts +++ b/fluent/ClientRune.ts @@ -15,7 +15,7 @@ export interface Chain { event: E } -export class ClientRune extends Rune { +export class ClientRune extends Rune, U> { latestBlock = this.block(chain.getBlockHash( this, chain diff --git a/fluent/rpc_runes.ts b/fluent/rpc_runes.ts index 2207aa6de..8a90aa2a5 100644 --- a/fluent/rpc_runes.ts +++ b/fluent/rpc_runes.ts @@ -1,37 +1,38 @@ import { RpcClient, RpcClientError, - RpcErrorMessage, + RpcProvider, + RpcServerError, RpcSubscriptionMessage, - WsRpcProvider, } from "../rpc/mod.ts" import { Batch, MetaRune, Run, Rune, RunicArgs, RunStream } from "../rune/mod.ts" import { ClientRune } from "./ClientRune.ts" -class RunRpcClient extends Run { +class RunRpcClient extends Run, never> { constructor( ctx: Batch, - readonly provider: WsRpcProvider, - readonly discoveryValue: string, + readonly provider: RpcProvider, + readonly discoveryValue: D, ) { super(ctx) } - client?: RpcClient - async _evaluate(): Promise { + client?: RpcClient + async _evaluate(): Promise> { return this.client ??= new RpcClient(this.provider, this.discoveryValue) } } -export function rpcClient(provider: WsRpcProvider, discoveryValue: string) { - return Rune.new(RunRpcClient, provider, discoveryValue).into(ClientRune) +export function rpcClient(provider: RpcProvider, discoveryValue: D) { + return Rune.new(RunRpcClient, provider, discoveryValue).into(ClientRune) } -export function rpcCall(method: string) { - return (...args: RunicArgs) => { - return Rune.tuple(args) +export function rpcCall(method: string) { + return (...args: RunicArgs, ...params: Params]>) => { + return Rune + .tuple(args) .map(async ([client, ...params]) => { - const result = await client.call(method, params) + const result = await client.call(method, params) if (result.error) throw new RpcServerError(result) return result.result }) @@ -39,16 +40,18 @@ export function rpcCall(method: string) { } } -class RunRpcSubscription extends RunStream { +class RunRpcSubscription + extends RunStream> +{ constructor( ctx: Batch, - client: RpcClient, + client: RpcClient, params: unknown[], subscribeMethod: string, unsubscribeMethod: string, ) { super(ctx) - client.subscription( + client.subscription( subscribeMethod, unsubscribeMethod, params, @@ -58,36 +61,26 @@ class RunRpcSubscription extends RunStream { } } -export function rpcSubscription() { +export function rpcSubscription() { return (subscribeMethod: string, unsubscribeMethod: string) => { - return (...args: RunicArgs) => { + return (...args: RunicArgs, ...params: Params]>) => { return Rune.tuple(args) .map(([client, ...params]) => - Rune.new(RunRpcSubscription, client, params, subscribeMethod, unsubscribeMethod) + Rune.new( + RunRpcSubscription, + client, + params, + subscribeMethod, + unsubscribeMethod, + ) ) .into(MetaRune) .flat() .map((event) => { - if (event instanceof Error) { - throw event - } else if (event.error) { - throw new RpcServerError(event) - } - return event.params.result as Result + if (event.error) throw new RpcServerError(event) + return event.params.result }) .throws(RpcClientError, RpcServerError) } } } - -export class RpcServerError extends Error { - override readonly name = "RpcServerError" - code - data - - constructor({ error: { code, data, message } }: RpcErrorMessage) { - super(message) - this.code = code - this.data = data - } -} diff --git a/providers/frame/FrameProvider.ts b/providers/frame/FrameProvider.ts index 3dbeb7d11..dac6b04c8 100644 --- a/providers/frame/FrameProvider.ts +++ b/providers/frame/FrameProvider.ts @@ -2,17 +2,17 @@ import { File, FrameCodegen } from "../../codegen/frame/mod.ts" import { posix as path } from "../../deps/std/path.ts" import { $metadata } from "../../frame_metadata/Metadata.ts" import { fromPrefixedHex } from "../../frame_metadata/mod.ts" -import { RpcClient } from "../../rpc/mod.ts" +import { RpcClient, RpcServerError } from "../../rpc/mod.ts" import { f, PathInfo, Provider } from "../../server/mod.ts" import { fromPathInfo } from "../../server/PathInfo.ts" -import { throwIfError, WeakMemo } from "../../util/mod.ts" +import { WeakMemo } from "../../util/mod.ts" export abstract class FrameProvider extends Provider { generatorId = "frame" codegenCtxsPending: Record> = {} - abstract client(pathInfo: PathInfo): Promise + abstract client(pathInfo: PathInfo): Promise> abstract clientFile(pathInfo: PathInfo): Promise async handle(request: Request, pathInfo: PathInfo): Promise { @@ -83,9 +83,13 @@ export abstract class FrameProvider extends Provider { }) } - async clientCall(client: RpcClient, method: string, params: unknown[] = []): Promise { - const result = throwIfError(await client.call(method, params)) - if (result.error) throw new Error(result.error.message) - return result.result as R + async clientCall( + client: RpcClient, + method: string, + params: unknown[] = [], + ): Promise { + const result = await client.call(method, params) + if (result.error) throw new RpcServerError(result) + return result.result } } diff --git a/rpc/RpcClient.ts b/rpc/RpcClient.ts index f112d6e5e..a22d413c3 100644 --- a/rpc/RpcClient.ts +++ b/rpc/RpcClient.ts @@ -9,12 +9,12 @@ import { RpcNotificationMessage, RpcOkMessage, } from "./rpc_messages.ts" -import { WsRpcProvider } from "./RpcProvider.ts" +import { RpcProvider } from "./RpcProvider.ts" -export class RpcClient { +export class RpcClient { conn - constructor(readonly provider: WsRpcProvider, readonly discovery: string) { + constructor(readonly provider: RpcProvider, readonly discovery: D) { this.conn = (controller: AbortController) => this.provider.ref(discovery, this.handler, controller) } diff --git a/rpc/RpcClientError.ts b/rpc/RpcClientError.ts deleted file mode 100644 index dc43e5795..000000000 --- a/rpc/RpcClientError.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class RpcClientError extends Error { - override readonly name = "RpcClientError" -} diff --git a/rpc/RpcProvider.ts b/rpc/RpcProvider.ts index a21a72bad..179129119 100644 --- a/rpc/RpcProvider.ts +++ b/rpc/RpcProvider.ts @@ -1,12 +1,13 @@ import { getOrInit, SignalBearer } from "../util/mod.ts" -import { RpcEgressMessage, RpcHandler, RpcMessageId } from "./rpc_messages.ts" -import { RpcClientError } from "./RpcClientError.ts" +import { RpcHandler, RpcMessageId } from "./rpc_messages.ts" -export class WsRpcProvider { - conns = new Map() +export class RpcProvider { + conns = new Map>() - ref(discovery: string, handler: RpcHandler, { signal }: SignalBearer) { - const conn = getOrInit(this.conns, discovery, () => new WsRpcConn(discovery)) + constructor(readonly init: (discovery: D) => RpcConn) {} + + ref(discovery: D, handler: RpcHandler, { signal }: SignalBearer) { + const conn = getOrInit(this.conns, discovery, () => this.init(discovery)) const references = conn.references.get(handler) if (!references) conn.references.set(handler, 1) else conn.references.set(handler, references + 1) @@ -25,64 +26,13 @@ export class WsRpcProvider { } } -export const wsRpcProvider = new WsRpcProvider() - -export class WsRpcConn { +export abstract class RpcConn { currentId = 0 references = new Map() - inner - - constructor(readonly discovery: string) { - this.inner = new WebSocket(discovery) - this.inner.addEventListener("message", (e) => { - const message = JSON.parse(e.data) - for (const reference of this.references.keys()) reference(message) - }) - } - - close() { - this.inner.close() - } - send(id: RpcMessageId, method: string, params: unknown[]) { - const message: RpcEgressMessage = { - jsonrpc: "2.0", - id, - method, - params, - } - this.inner.send(JSON.stringify(message)) - } + abstract inner: I - async ready() { - switch (this.inner.readyState) { - case WebSocket.OPEN: - return - case WebSocket.CONNECTING: { - try { - return await new Promise((resolve, reject) => { - const controller = new AbortController() - this.inner.addEventListener("open", () => { - controller.abort() - resolve() - }, controller) - this.inner.addEventListener("close", () => { - controller.abort() - reject() - }, controller) - this.inner.addEventListener("error", () => { - controller.abort() - reject() - }, controller) - }) - } catch (_e) { - throw new RpcClientError() - } - } - case WebSocket.CLOSING: - case WebSocket.CLOSED: { - throw new RpcClientError() - } - } - } + abstract close(): void + abstract send(id: RpcMessageId, method: string, params: unknown): void + abstract ready(): Promise } diff --git a/rpc/errors.ts b/rpc/errors.ts new file mode 100644 index 000000000..af8653c79 --- /dev/null +++ b/rpc/errors.ts @@ -0,0 +1,18 @@ +import { RpcErrorMessage } from "./rpc_messages.ts" + +export class RpcClientError extends Error { + override readonly name = "RpcClientError" +} + +export class RpcServerError extends Error { + override readonly name = "RpcServerError" + code + data + + // TODO: accept init `EgressMessage`? + constructor({ error: { code, data, message } }: RpcErrorMessage) { + super(message, { cause: message }) + this.code = code + this.data = data + } +} diff --git a/rpc/mod.ts b/rpc/mod.ts index e29eb82ed..a452724f5 100644 --- a/rpc/mod.ts +++ b/rpc/mod.ts @@ -1,5 +1,6 @@ +export * from "./errors.ts" export * as known from "./known/mod.ts" export * from "./rpc_messages.ts" export * from "./RpcClient.ts" -export * from "./RpcClientError.ts" export * from "./RpcProvider.ts" +export * from "./wsRpcProvider.ts" diff --git a/rpc/wsRpcProvider.ts b/rpc/wsRpcProvider.ts new file mode 100644 index 000000000..a05ab4344 --- /dev/null +++ b/rpc/wsRpcProvider.ts @@ -0,0 +1,64 @@ +import { RpcClientError } from "./errors.ts" +import { RpcEgressMessage, RpcMessageId } from "./rpc_messages.ts" +import { RpcConn, RpcProvider } from "./RpcProvider.ts" + +export const wsRpcProvider = new RpcProvider((discovery: string) => new WsRpcConn(discovery)) + +export class WsRpcConn extends RpcConn { + inner + + constructor(readonly discovery: string) { + super() + this.inner = new WebSocket(discovery) + this.inner.addEventListener("message", (e) => { + const message = JSON.parse(e.data) + for (const reference of this.references.keys()) reference(message) + }) + } + + close() { + this.inner.close() + } + + send(id: RpcMessageId, method: string, params: unknown[]) { + const message: RpcEgressMessage = { + jsonrpc: "2.0", + id, + method, + params, + } + this.inner.send(JSON.stringify(message)) + } + + async ready() { + switch (this.inner.readyState) { + case WebSocket.OPEN: + return + case WebSocket.CONNECTING: { + try { + return await new Promise((resolve, reject) => { + const controller = new AbortController() + this.inner.addEventListener("open", () => { + controller.abort() + resolve() + }, controller) + this.inner.addEventListener("close", () => { + controller.abort() + reject() + }, controller) + this.inner.addEventListener("error", () => { + controller.abort() + reject() + }, controller) + }) + } catch (_e) { + throw new RpcClientError() + } + } + case WebSocket.CLOSING: + case WebSocket.CLOSED: { + throw new RpcClientError() + } + } + } +} From df0afd30d1712dcc5d7adb26ba068b721c13e7e4 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Thu, 16 Feb 2023 15:04:17 -0500 Subject: [PATCH 03/23] more cleanup --- mod.ts | 14 +- rpc/RpcClient.ts | 3 +- rpc/mod.ts | 8 +- rpc/{RpcProvider.ts => provider/base.ts} | 4 +- rpc/provider/mod.ts | 5 + rpc/provider/smoldot.ts | 87 +++++++++++ rpc/{wsRpcProvider.ts => provider/ws.ts} | 6 +- rpc/todo/client.test.ts | 170 --------------------- rpc/todo/client.ts | 186 ----------------------- rpc/todo/proxy.test.ts | 81 ---------- rpc/todo/proxy.ts | 113 -------------- rpc/todo/smoldot.test.ts | 175 --------------------- rpc/todo/smoldot.ts | 144 ------------------ rpc/todo/test_util.ts | 18 --- 14 files changed, 112 insertions(+), 902 deletions(-) rename rpc/{RpcProvider.ts => provider/base.ts} (89%) create mode 100644 rpc/provider/mod.ts create mode 100644 rpc/provider/smoldot.ts rename rpc/{wsRpcProvider.ts => provider/ws.ts} (90%) delete mode 100644 rpc/todo/client.test.ts delete mode 100644 rpc/todo/client.ts delete mode 100644 rpc/todo/proxy.test.ts delete mode 100644 rpc/todo/proxy.ts delete mode 100644 rpc/todo/smoldot.test.ts delete mode 100644 rpc/todo/smoldot.ts delete mode 100644 rpc/todo/test_util.ts diff --git a/mod.ts b/mod.ts index c99bc9adb..9ccd2460f 100644 --- a/mod.ts +++ b/mod.ts @@ -1,11 +1,6 @@ export * as $ from "./deps/scale.ts" export { BitSequence } from "./deps/scale.ts" -export * from "./fluent/mod.ts" export * as frame from "./frame_metadata/mod.ts" -export * from "./primitives/mod.ts" -export * from "./rpc/mod.ts" -export * from "./rune/mod.ts" -export * from "./scale_info/mod.ts" export { alice, aliceStash, @@ -23,5 +18,12 @@ export { type Listener, Sr25519, ss58, - throwIfError, } from "./util/mod.ts" + +// moderate --exclude deps frame_metadata main.ts patterns providers server util + +export * from "./fluent/mod.ts" +export * from "./primitives/mod.ts" +export * from "./rpc/mod.ts" +export * from "./rune/mod.ts" +export * from "./scale_info/mod.ts" diff --git a/rpc/RpcClient.ts b/rpc/RpcClient.ts index a22d413c3..57765134a 100644 --- a/rpc/RpcClient.ts +++ b/rpc/RpcClient.ts @@ -1,5 +1,6 @@ import { Deferred, deferred } from "../deps/std/async.ts" import { SignalBearer } from "../util/mod.ts" +import { RpcProvider } from "./provider/base.ts" import { RpcErrorMessage, RpcErrorMessageData, @@ -9,7 +10,6 @@ import { RpcNotificationMessage, RpcOkMessage, } from "./rpc_messages.ts" -import { RpcProvider } from "./RpcProvider.ts" export class RpcClient { conn @@ -66,6 +66,7 @@ export class RpcClient { conn.send(subscribeId, subscribe, params) } + // TODO: error handling handler = (message: RpcIngressMessage) => { if (typeof message.id === "number") { const callResultPending = this.callResultPendings[message.id] diff --git a/rpc/mod.ts b/rpc/mod.ts index a452724f5..ecdc5f612 100644 --- a/rpc/mod.ts +++ b/rpc/mod.ts @@ -1,6 +1,8 @@ -export * from "./errors.ts" export * as known from "./known/mod.ts" + +// moderate --exclude known + +export * from "./errors.ts" +export * from "./provider/mod.ts" export * from "./rpc_messages.ts" export * from "./RpcClient.ts" -export * from "./RpcProvider.ts" -export * from "./wsRpcProvider.ts" diff --git a/rpc/RpcProvider.ts b/rpc/provider/base.ts similarity index 89% rename from rpc/RpcProvider.ts rename to rpc/provider/base.ts index 179129119..90754f47c 100644 --- a/rpc/RpcProvider.ts +++ b/rpc/provider/base.ts @@ -1,5 +1,5 @@ -import { getOrInit, SignalBearer } from "../util/mod.ts" -import { RpcHandler, RpcMessageId } from "./rpc_messages.ts" +import { getOrInit, SignalBearer } from "../../util/mod.ts" +import { RpcHandler, RpcMessageId } from "../rpc_messages.ts" export class RpcProvider { conns = new Map>() diff --git a/rpc/provider/mod.ts b/rpc/provider/mod.ts new file mode 100644 index 000000000..85bd2b2df --- /dev/null +++ b/rpc/provider/mod.ts @@ -0,0 +1,5 @@ +// moderate + +export * from "./base.ts" +export * from "./smoldot.ts" +export * from "./ws.ts" diff --git a/rpc/provider/smoldot.ts b/rpc/provider/smoldot.ts new file mode 100644 index 000000000..97ad1f519 --- /dev/null +++ b/rpc/provider/smoldot.ts @@ -0,0 +1,87 @@ +import { start } from "../../deps/smoldot.ts" +import { Chain, Client, ClientOptions } from "../../deps/smoldot/client.d.ts" +import { deferred } from "../../deps/std/async.ts" +import { RpcEgressMessage, RpcMessageId } from "../rpc_messages.ts" +import { RpcConn, RpcProvider } from "./base.ts" + +// TODO: fix the many possible race conditions + +export interface SmoldotDiscovery { + relayChainSpec: string + parachainSpec?: string +} + +export const smoldotRpcProvider = new RpcProvider((discovery: SmoldotDiscovery) => + new SmoldotRpcConn(discovery) +) + +let client: undefined | Client + +export class SmoldotRpcConn extends RpcConn> { + inner + listening + stopListening + + constructor(readonly discovery: SmoldotDiscovery) { + super() + if (!client) { + client = start({ + forbidTcp: true, + forbidNonLocalWs: true, + cpuRateLimit: .25, + } as ClientOptions) + } + if (discovery.parachainSpec) { + const { parachainSpec } = discovery + const relayChain = client.addChain({ + chainSpec: discovery.relayChainSpec, + disableJsonRpc: true, + }) + this.inner = (async () => { + return client.addChain({ + chainSpec: parachainSpec, + potentialRelayChains: [await relayChain], + }) + })() + } else { + this.inner = client.addChain({ chainSpec: discovery.relayChainSpec }) + } + this.listening = deferred() + this.stopListening = () => this.listening.resolve() + this.startListening() + } + + async startListening() { + const inner = await this.inner + while (true) { + try { + const response = await Promise.race([ + this.listening, + inner.nextJsonRpcResponse(), + ]) + if (!response) break + const message = JSON.parse(response) + for (const reference of this.references.keys()) reference(message) + } catch (_e) {} + } + } + + async close() { + ;(await this.inner).remove() + } + + async send(id: RpcMessageId, method: string, params: unknown[]) { + const inner = await this.inner + const message: RpcEgressMessage = { + jsonrpc: "2.0", + id, + method, + params, + } + inner.sendJsonRpc(JSON.stringify(message)) + } + + async ready() { + await this.inner + } +} diff --git a/rpc/wsRpcProvider.ts b/rpc/provider/ws.ts similarity index 90% rename from rpc/wsRpcProvider.ts rename to rpc/provider/ws.ts index a05ab4344..a49ce4e08 100644 --- a/rpc/wsRpcProvider.ts +++ b/rpc/provider/ws.ts @@ -1,6 +1,6 @@ -import { RpcClientError } from "./errors.ts" -import { RpcEgressMessage, RpcMessageId } from "./rpc_messages.ts" -import { RpcConn, RpcProvider } from "./RpcProvider.ts" +import { RpcClientError } from "../errors.ts" +import { RpcEgressMessage, RpcMessageId } from "../rpc_messages.ts" +import { RpcConn, RpcProvider } from "./base.ts" export const wsRpcProvider = new RpcProvider((discovery: string) => new WsRpcConn(discovery)) diff --git a/rpc/todo/client.test.ts b/rpc/todo/client.test.ts deleted file mode 100644 index 3fe001860..000000000 --- a/rpc/todo/client.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -/* -import { discoveryValue } from "polkadot_dev/mod.ts" -import * as A from "../deps/std/testing/asserts.ts" -import * as U from "../util/mod.ts" -import { - Client, - known, - msg, - nextIdFactory, - Provider, - ProviderHandlerError, - ProviderListener, - ProviderSendError, - proxyProvider, -} from "./mod.ts" - -Deno.test({ - name: "RPC Client", - async fn(t) { - const client = new Client(proxyProvider, discoveryValue) - - await t.step({ - name: "call", - sanitizeOps: false, - sanitizeResources: false, - async fn() { - const metadata = await client.call(client.providerRef.nextId(), "state_getMetadata", []) - A.assertNotInstanceOf(metadata, Error) - A.assert(!metadata.error) - A.assertExists(metadata.result) - }, - }) - - await t.step({ - name: "subscribe", - sanitizeOps: false, - sanitizeResources: false, - async fn() { - const events: msg.NotificationMessage<"chain_subscribeAllHeads", known.Header>[] = [] - const stoppedSubscriptionId = await client.subscriptionFactory<[], known.Header>()( - "chain_subscribeAllHeads", - "chain_unsubscribeAllHeads", - [], - (ctx) => { - let i = 0 - return (e) => { - A.assertNotInstanceOf(e, Error) - A.assert(!e.error) - A.assertExists(e.params.result.parentHash) - events.push(e) - if (i === 2) { - return ctx.end(e.params.subscription) - } - i++ - return - } - }, - ) - A.assertEquals(events.length, 3) - A.assert(typeof stoppedSubscriptionId === "string") - }, - }) - - await client.discard() - - await t.step({ - name: "call general error", - async fn() { - const { client, emitEvent } = createMockClient() - const id = client.providerRef.nextId() - const pending = client.call(id, null!, null!) - emitEvent(new ProviderSendError(null!, dummy(id))) - A.assertInstanceOf(await pending, Error) - assertClientCleanup(client) - }, - }) - - await t.step({ - name: "call error", - async fn() { - const { client, emitEvent } = createMockClient() - const id = client.providerRef.nextId() - const pending = client.call(id, null!, null!) - emitEvent(new ProviderSendError(null!, dummy(id))) - A.assertInstanceOf(await pending, ProviderSendError) - assertClientCleanup(client) - }, - }) - - await t.step({ - name: "subscribe general error", - async fn() { - const { client, emitEvent } = createMockClient() - const message = { id: 0 } as msg.EgressMessage - const pending = client - .subscriptionFactory()(null!, null!, null!, ({ end }) => () => end()) - emitEvent(new ProviderSendError(null!, message)) - A.assertEquals(await pending, undefined) - assertClientCleanup(client) - }, - }) - - await t.step({ - name: "subscribe error after subscribing", - async fn() { - const { client, emitEvent } = createMockClient() - const pending = client - .subscriptionFactory()(null!, null!, null!, ({ end }) => (e) => end(e)) - const result = "$$$" - emitEvent({ id: 0, result } as unknown as msg.OkMessage) - emitEvent(new ProviderHandlerError(null!)) - emitEvent({ id: 1, result: true } as unknown as msg.OkMessage) - A.assertInstanceOf(await pending, ProviderHandlerError) - assertClientCleanup(client) - }, - }) - - await t.step({ - name: "subscribe error subscribing", - async fn() { - const { client, emitEvent } = createMockClient() - const pending = client - .subscriptionFactory()(null!, null!, null!, ({ end }) => (e) => end(e)) - const toEmit = { - id: 0, - error: { - message: "some error", - }, - } as unknown as msg.ErrorMessage - emitEvent(toEmit) - A.assertEquals(U.throwIfError(await pending), toEmit) - assertClientCleanup(client) - }, - }) - }, -}) - -function assertClientCleanup(client: Client) { - A.assertEquals(client.pendingCalls, {}) - A.assertEquals(client.pendingSubscriptions, {}) - A.assertEquals(client.activeSubscriptions, {}) - A.assertEquals(client.activeSubscriptionByMessageId, {}) -} - -function createMockClient() { - let listener: ProviderListener - const nextId = nextIdFactory() - const providerMockFactory: Provider = (_discoveryValue, clientListener) => { - listener = clientListener - return { - nextId, - send: () => {}, - release: () => Promise.resolve(undefined), - } - } - return { - client: new Client(providerMockFactory, null!), - emitEvent: listener!, - } -} - -function dummy(id: number): msg.EgressMessage { - return { - id, - method: null!, - params: null!, - } as any -} - -*/ diff --git a/rpc/todo/client.ts b/rpc/todo/client.ts deleted file mode 100644 index b5fe60356..000000000 --- a/rpc/todo/client.ts +++ /dev/null @@ -1,186 +0,0 @@ -// import { Deferred, deferred } from "../deps/std/async.ts" -// import * as U from "../util/mod.ts" -// import * as msg from "./messages.ts" -// import { Provider, ProviderListener } from "./provider/base.ts" -// import { ProviderHandlerError, ProviderSendError } from "./provider/errors.ts" - -// export class Client< -// DiscoveryValue = any, -// SendErrorData = any, -// HandlerErrorData = any, -// CloseErrorData = any, -// > { -// providerRef -// pendingCalls: Record> = {} -// pendingSubscriptions: SubscriptionListeners = {} -// activeSubscriptions: SubscriptionListeners = {} -// activeSubscriptionByMessageId: Record = {} - -// constructor( -// readonly provider: Provider, -// readonly discoveryValue: DiscoveryValue, -// ) { -// this.providerRef = provider(discoveryValue, this.listener) -// } - -// listener: ProviderListener = (e) => { -// if (e instanceof ProviderSendError) { -// const egressMessageId = e.egressMessage.id -// const pendingCall = this.pendingCalls[egressMessageId] -// pendingCall?.resolve(e) -// delete this.pendingCalls[egressMessageId] -// } else if (e instanceof Error) { -// for (const id in this.pendingCalls) { -// const pendingCall = this.pendingCalls[id]! -// pendingCall.resolve(e) -// delete this.pendingCalls[id] -// this.pendingSubscriptions[id]?.(e) -// delete this.pendingSubscriptions[id] -// } -// for (const id in this.activeSubscriptions) { -// this.activeSubscriptions[id]!(e) -// delete this.activeSubscriptions[id] -// } -// } else if (e.id !== undefined) { -// const pendingCall = this.pendingCalls[e.id] -// pendingCall?.resolve(e) -// delete this.pendingCalls[e.id] -// if (this.pendingSubscriptions[e.id]) { -// if (e.error) { -// this.pendingSubscriptions[e.id]!(e) -// } else { -// this.activeSubscriptions[e.result] = this.pendingSubscriptions[e.id]! -// this.activeSubscriptionByMessageId[e.id] = e.result -// } -// delete this.pendingSubscriptions[e.id] -// } -// } else if (e.params) { -// this.activeSubscriptions[e.params.subscription]?.(e) -// } -// } - -// call: ClientCall = (id, method, params) => { -// const waiter = deferred>() -// this.pendingCalls[id] = waiter -// this.providerRef.send({ -// jsonrpc: "2.0", -// id, -// method, -// params, -// }) -// return waiter -// } - -// subscriptionFactory = < -// Params extends unknown[] = any[], -// NotificationData = any, -// >(): SubscriptionFactory => -// ( -// subscribeMethod, -// unsubscribeMethod, -// params, -// listener, -// abortSignal, -// ) => { -// const id = this.providerRef.nextId() -// abortSignal.addEventListener("abort", async () => { -// delete this.pendingSubscriptions[id] -// const activeSubscriptionId = this.activeSubscriptionByMessageId[id] -// if (activeSubscriptionId) { -// delete this.activeSubscriptions[activeSubscriptionId] -// await this.call( -// this.providerRef.nextId(), -// unsubscribeMethod, -// [activeSubscriptionId], -// ) -// } -// delete this.activeSubscriptionByMessageId[id] -// }) -// this.call(id, subscribeMethod, params).then((x) => { -// if (x instanceof Error || x.error) { -// listener(x) -// } -// }) -// this.pendingSubscriptions[id] = listener -// } - -// discard = () => { -// this.pendingCalls = {} -// this.pendingSubscriptions = {} -// this.activeSubscriptions = {} -// this.activeSubscriptionByMessageId = {} -// return this.providerRef.release() -// } -// } - -// export type ClientCallEvent = -// | msg.OkMessage -// | msg.ErrorMessage -// | ProviderSendError -// | ProviderHandlerError - -// export type ClientCall = ( -// id: number | string, -// method: string, -// params: unknown[], -// ) => Promise> - -// export type ClientSubscriptionEvent< -// Method extends string, -// Result, -// SendErrorData, -// HandlerErrorData, -// > = -// | msg.NotificationMessage -// | msg.ErrorMessage -// | ProviderSendError -// | ProviderHandlerError - -// type SubscriptionListeners = Record< -// string, -// SubscriptionListener< -// any, -// any, -// SendErrorData, -// HandlerErrorData -// > -// > - -// type SubscriptionListener< -// SubscribeMethod extends string, -// NotificationData, -// SendErrorData, -// HandlerErrorData, -// > = ( -// value: ClientSubscriptionEvent< -// SubscribeMethod, -// NotificationData, -// SendErrorData, -// HandlerErrorData -// >, -// ) => void - -// export type SubscriptionFactory< -// Params extends unknown[], -// NotificationData, -// SendErrorData, -// HandlerErrorData, -// > = < -// SubscribeMethod extends string, -// >( -// subscribeMethod: SubscribeMethod, -// unsubscribeMethod: string, -// params: [...Params], -// listener: SubscriptionListener< -// SubscribeMethod, -// NotificationData, -// SendErrorData, -// HandlerErrorData -// >, -// abortSignal: AbortSignal, -// ) => void - -// export interface ClientSubscriptionContext { -// end: (value?: T) => U.End -// endIfError(value: I): U.End> -// } diff --git a/rpc/todo/proxy.test.ts b/rpc/todo/proxy.test.ts deleted file mode 100644 index 5984824d8..000000000 --- a/rpc/todo/proxy.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -// import { discoveryValue } from "westend/mod.ts" -// import * as A from "../../deps/std/testing/asserts.ts" -// import { proxyProvider } from "./proxy.ts" -// import { setup } from "./test_util.ts" - -// Deno.test({ -// name: "Proxy Provider", -// async fn(t) { -// await t.step({ -// name: "send/listen", -// async fn() { -// const [ref, message] = await setup(proxyProvider, discoveryValue, "system_health", []) -// A.assertNotInstanceOf(message, Error) -// A.assertExists(message.result) -// A.assertNotInstanceOf(await ref.release(), Error) -// }, -// }) - -// await t.step({ -// name: "create WebSocket error", -// async fn() { -// const [ref, message] = await setup( -// proxyProvider, -// "invalid-endpoint-url", -// "system_health", -// [], -// ) -// A.assertInstanceOf(message, Error) -// A.assertNotInstanceOf(await ref.release(), Error) -// }, -// }) - -// await t.step({ -// name: "close WebSocket while listening", -// async fn() { -// const server = createWebSocketServer(function() { -// this.close() -// }) -// const [ref, message] = await setup( -// proxyProvider, -// server.url, -// "system_health", -// [], -// ) -// A.assertInstanceOf(message, Error) -// A.assertNotInstanceOf(await ref.release(), Error) -// server.close() -// }, -// }) - -// await t.step({ -// name: "send non-JSON message", -// async fn() { -// const server = createWebSocketServer() -// // make JSON.stringify throw on bigint -// const [ref, message] = await setup(proxyProvider, server.url, "system_health", [1n]) -// A.assertInstanceOf(message, Error) -// A.assertNotInstanceOf(await ref.release(), Error) -// server.close() -// }, -// }) -// }, -// }) - -// function createWebSocketServer(onMessage?: WebSocket["onmessage"]) { -// const onmessage = onMessage ?? (() => {}) -// const listener = Deno.listen({ port: 0 }) -// ;(async () => { -// for await (const conn of listener) { -// for await (const e of Deno.serveHttp(conn)) { -// const { socket, response } = Deno.upgradeWebSocket(e.request) -// socket.onmessage = onmessage -// e.respondWith(response) -// } -// } -// })() -// return { -// close: () => listener.close(), -// url: `ws://localhost:${(listener.addr as Deno.NetAddr).port}`, -// } -// } diff --git a/rpc/todo/proxy.ts b/rpc/todo/proxy.ts deleted file mode 100644 index 89e43a47a..000000000 --- a/rpc/todo/proxy.ts +++ /dev/null @@ -1,113 +0,0 @@ -// import * as U from "../../util/mod.ts" -// import * as msg from "../messages.ts" -// import { nextIdFactory, Provider, ProviderConnection, ProviderListener } from "./base.ts" -// import { ProviderCloseError, ProviderHandlerError, ProviderSendError } from "./errors.ts" - -// /** Global lookup of existing connections */ -// const connections = new Map() -// type ProxyProviderConnection = ProviderConnection - -// const nextId = nextIdFactory() - -// export const proxyProvider: Provider = (url, listener) => { -// return { -// nextId, -// send: (message) => { -// let conn: ProxyProviderConnection -// try { -// conn = connection(url, listener) -// } catch (error) { -// listener(new ProviderHandlerError(error as Event)) -// return -// } -// ;(async () => { -// const openError = await ensureWsOpen(conn.inner) -// if (openError) { -// conn.forEachListener(new ProviderSendError(openError, message)) -// return -// } -// try { -// conn.inner.send(JSON.stringify(message)) -// } catch (error) { -// listener(new ProviderSendError(error as Event, message)) -// } -// })() -// }, -// release: () => { -// const conn = connections.get(url) -// if (!conn) { -// return Promise.resolve(undefined) -// } -// const { cleanUp, listeners, inner } = conn -// listeners.delete(listener) -// if (!listeners.size) { -// connections.delete(url) -// cleanUp() -// return closeWs(inner) -// } -// return Promise.resolve(undefined) -// }, -// } -// } - -// function connection( -// url: string, -// listener: ProviderListener, -// ): ProxyProviderConnection { -// const conn = U.getOrInit(connections, url, () => { -// const controller = new AbortController() -// const ws = new WebSocket(url) -// ws.addEventListener("message", (e) => { -// conn.forEachListener(msg.parse(e.data)) -// }, controller) -// ws.addEventListener("error", (e) => { -// conn.forEachListener(new ProviderHandlerError(e)) -// }, controller) -// ws.addEventListener("close", (e) => { -// conn.forEachListener(new ProviderHandlerError(e)) -// }, controller) -// return new ProviderConnection(ws, () => { -// controller.abort() -// }) -// }) -// conn.addListener(listener) -// return conn -// } - -// function ensureWsOpen(ws: WebSocket): Promise { -// if (ws.readyState === WebSocket.OPEN) { -// return Promise.resolve(undefined) -// } else if (ws.readyState === WebSocket.CLOSING || ws.readyState === WebSocket.CLOSED) { -// return Promise.resolve(new Event("error")) -// } else { -// return new Promise((resolve) => { -// const controller = new AbortController() -// ws.addEventListener("open", () => { -// controller.abort() -// resolve(undefined) -// }, controller) -// ws.addEventListener("error", (e) => { -// controller.abort() -// resolve(e) -// }, controller) -// }) -// } -// } - -// function closeWs(socket: WebSocket): Promise> { -// if (socket.readyState === WebSocket.CLOSED) { -// return Promise.resolve(undefined) -// } -// return new Promise>((resolve) => { -// const controller = new AbortController() -// socket.addEventListener("close", () => { -// controller.abort() -// resolve(undefined) -// }, controller) -// socket.addEventListener("error", (e) => { -// controller.abort() -// resolve(new ProviderCloseError(e)) -// }, controller) -// socket.close() -// }) -// } diff --git a/rpc/todo/smoldot.test.ts b/rpc/todo/smoldot.test.ts deleted file mode 100644 index 12f13953e..000000000 --- a/rpc/todo/smoldot.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -// import { deferred } from "../../deps/std/async.ts" -// import { -// assertExists, -// assertInstanceOf, -// assertNotInstanceOf, -// } from "../../deps/std/testing/asserts.ts" -// import { ProviderListener } from "./base.ts" -// import { smoldotProvider } from "./smoldot.ts" -// import { setup } from "./test_util.ts" - -// Deno.test({ -// name: "Smoldot Provider", -// sanitizeOps: false, -// sanitizeResources: false, -// async fn(t) { -// await t.step({ -// name: "relay chain connection", -// async fn() { -// const relay = await fetchText( -// "https://raw.githubusercontent.com/paritytech/substrate-connect/main/packages/connect/src/connector/specs/polkadot.json", -// ) -// const pendingSubscriptionId = deferred() -// const initialized = deferred() -// const unsubscribed = deferred() -// const checks: ProviderListener[] = [ -// // check for chainHead_unstable_follow subscription -// (message) => { -// assertNotInstanceOf(message, Error) -// assertExists(message.result) -// pendingSubscriptionId.resolve(message.result) -// }, -// // check for chainHead_unstable_follow initialized event -// (message) => { -// assertNotInstanceOf(message, Error) -// assertExists(message.params?.result) -// if (message.params?.result.event === "initialized") { -// initialized.resolve() -// } -// }, -// // check for chainHead_unstable_unfollow unsubscribe -// (message) => { -// assertNotInstanceOf(message, Error) -// if (message?.result === null) { -// unsubscribed.resolve() -// } -// }, -// ] -// const provider = smoldotProvider({ chainSpec: { relay } }, (message) => { -// if (checks.length > 1) { -// checks.shift()!(message) -// } else { -// checks[0]!(message) -// } -// }) -// provider.send({ -// jsonrpc: "2.0", -// id: provider.nextId(), -// method: "chainHead_unstable_follow", -// params: [false], -// }) -// const subscriptionId = await pendingSubscriptionId -// await initialized -// provider.send({ -// jsonrpc: "2.0", -// id: provider.nextId(), -// method: "chainHead_unstable_unfollow", -// params: [subscriptionId], -// }) -// await unsubscribed -// const providerRelease = await provider.release() -// assertNotInstanceOf(providerRelease, Error) -// }, -// }) -// await t.step({ -// name: "parachain connection", -// async fn() { -// const relay = await fetchText( -// "https://raw.githubusercontent.com/paritytech/substrate-connect/main/packages/connect/src/connector/specs/westend2.json", -// ) -// const para = await fetchText( -// "https://raw.githubusercontent.com/paritytech/substrate-connect/main/projects/demo/src/assets/westend-westmint.json", -// ) - -// const pendingSubscriptionId = deferred() -// const initialized = deferred() -// const unsubscribed = deferred() -// const checks: ProviderListener[] = [ -// // check for chainHead_unstable_follow subscription -// (message) => { -// assertNotInstanceOf(message, Error) -// assertExists(message.result) -// pendingSubscriptionId.resolve(message.result) -// }, -// // check for chainHead_unstable_follow initialized event -// (message) => { -// assertNotInstanceOf(message, Error) -// assertExists(message.params?.result) -// if (message.params?.result.event === "initialized") { -// initialized.resolve() -// } -// }, -// // check for chainHead_unstable_unfollow unsubscribe -// (message) => { -// assertNotInstanceOf(message, Error) -// if (message?.result === null) { -// unsubscribed.resolve() -// } -// }, -// ] -// const provider = smoldotProvider( -// { chainSpec: { para, relay } }, -// (message) => { -// if (checks.length > 1) { -// checks.shift()!(message) -// } else { -// checks[0]!(message) -// } -// }, -// ) -// provider.send({ -// jsonrpc: "2.0", -// id: provider.nextId(), -// method: "chainHead_unstable_follow", -// params: [false], -// }) -// const subscriptionId = await pendingSubscriptionId -// await initialized -// provider.send({ -// jsonrpc: "2.0", -// id: provider.nextId(), -// method: "chainHead_unstable_unfollow", -// params: [subscriptionId], -// }) -// await unsubscribed -// const providerRelease = await provider.release() -// assertNotInstanceOf(providerRelease, Error) -// }, -// }) - -// await t.step({ -// name: "invalid chain spec", -// async fn() { -// const [ref, message] = await setup( -// smoldotProvider, -// { chainSpec: { relay: "" } }, -// "system_health", -// [false], -// ) -// assertInstanceOf(message, Error) -// assertNotInstanceOf(await ref.release(), Error) -// }, -// }) -// await t.step({ -// name: "send non-JSON", -// async fn() { -// const relay = await fetchText( -// "https://raw.githubusercontent.com/paritytech/substrate-connect/main/packages/connect/src/connector/specs/polkadot.json", -// ) -// const [ref, message] = await setup( -// smoldotProvider, -// { chainSpec: { relay } }, -// "system_health", -// // make JSON.stringify to throw -// [1n], -// ) -// assertInstanceOf(message, Error) -// assertNotInstanceOf(await ref.release(), Error) -// }, -// }) -// }, -// }) - -// async function fetchText(url: string) { -// return (await fetch(url)).text() -// } diff --git a/rpc/todo/smoldot.ts b/rpc/todo/smoldot.ts deleted file mode 100644 index 3d87a690c..000000000 --- a/rpc/todo/smoldot.ts +++ /dev/null @@ -1,144 +0,0 @@ -// import { -// AddChainError, -// AlreadyDestroyedError, -// CrashError, -// JsonRpcDisabledError, -// MalformedJsonRpcError, -// QueueFullError, -// start, -// } from "../../deps/smoldot.ts" -// import { Chain, Client, ClientOptions } from "../../deps/smoldot/client.d.ts" -// import { deferred } from "../../deps/std/async.ts" -// import * as msg from "../messages.ts" -// import { nextIdFactory, Provider, ProviderConnection, ProviderListener } from "./base.ts" -// import { ProviderCloseError, ProviderHandlerError, ProviderSendError } from "./errors.ts" - -// type SmoldotSendErrorData = -// | AlreadyDestroyedError -// | CrashError -// | JsonRpcDisabledError -// | MalformedJsonRpcError -// | QueueFullError -// type SmoldotHandlerErrorData = -// | AlreadyDestroyedError -// | CrashError -// | JsonRpcDisabledError -// | AddChainError -// type SmoldotCloseErrorData = AlreadyDestroyedError | CrashError - -// let client: undefined | Client -// const connections = new Map() -// class SmoldotProviderConnection -// extends ProviderConnection -// {} - -// const nextId = nextIdFactory() - -// export interface SmoldotProviderProps { -// chainSpec: { -// relay: string -// para?: string -// } -// // TODO: support deferring closing (how / what heuristic?) -// deferClosing?: boolean -// } - -// export const smoldotProvider: Provider< -// SmoldotProviderProps, -// SmoldotSendErrorData, -// SmoldotHandlerErrorData, -// SmoldotCloseErrorData -// > = (props, listener) => { -// return { -// nextId, -// send: (message) => { -// ;(async () => { -// let conn: SmoldotProviderConnection -// try { -// conn = await connection(props, listener) -// } catch (error) { -// listener(new ProviderHandlerError(error as SmoldotHandlerErrorData)) -// return -// } -// try { -// conn.inner.sendJsonRpc(JSON.stringify(message)) -// } catch (error) { -// listener(new ProviderSendError(error as SmoldotSendErrorData, message)) -// } -// })() -// }, -// release: () => { -// const conn = connections.get(props) -// if (!conn) { -// return Promise.resolve(undefined) -// } -// const { cleanUp, listeners, inner } = conn -// listeners.delete(listener) -// if (!listeners.size) { -// connections.delete(props) -// cleanUp() -// try { -// // TODO: utilize `deferClosing` prop once we flesh out approach -// inner.remove() -// } catch (e) { -// return Promise.resolve(new ProviderCloseError(e as SmoldotCloseErrorData)) -// } -// } -// return Promise.resolve(undefined) -// }, -// } -// } - -// async function connection( -// props: SmoldotProviderProps, -// listener: ProviderListener, -// ): Promise { -// if (!client) { -// client = start( -// { -// forbidTcp: true, -// forbidNonLocalWs: true, -// cpuRateLimit: .25, -// } as ClientOptions, -// ) -// } -// let conn = connections.get(props) -// if (!conn) { -// let inner: Chain -// if (props.chainSpec.para) { -// const relayChainConnection = await client.addChain({ -// chainSpec: props.chainSpec.relay, -// disableJsonRpc: true, -// }) -// inner = await client.addChain({ -// chainSpec: props.chainSpec.para, -// potentialRelayChains: [relayChainConnection], -// }) -// } else { -// inner = await client.addChain({ chainSpec: props.chainSpec.relay }) -// } -// const stopListening = deferred() -// conn = new SmoldotProviderConnection(inner, () => stopListening.resolve()) -// connections.set(props, conn) -// ;(async () => { -// while (true) { -// try { -// const response = await Promise.race([ -// stopListening, -// inner.nextJsonRpcResponse(), -// ]) -// if (!response) { -// break -// } -// const message = msg.parse(response) -// conn!.forEachListener(message) -// } catch (e) { -// conn!.forEachListener(new ProviderHandlerError(e as SmoldotHandlerErrorData)) -// break -// } -// } -// })() -// } -// conn.addListener(listener) -// return conn -// } diff --git a/rpc/todo/test_util.ts b/rpc/todo/test_util.ts deleted file mode 100644 index d298723e4..000000000 --- a/rpc/todo/test_util.ts +++ /dev/null @@ -1,18 +0,0 @@ -// import { Provider, ProviderRef } from "./base.ts" - -// export function setup( -// provider: Provider, -// discoveryValue: any, -// method: string, -// params: unknown[], -// ): Promise<[ProviderRef, any]> { -// return new Promise((resolve) => { -// const providerRef = provider(discoveryValue, (message) => resolve([providerRef, message])) -// providerRef.send({ -// jsonrpc: "2.0", -// id: providerRef.nextId(), -// method, -// params, -// }) -// }) -// } From e671df62390bdd5746f3e9230f2ff44ccfd07f4f Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Thu, 16 Feb 2023 15:11:17 -0500 Subject: [PATCH 04/23] tiny tweak --- _tasks/download_frame_metadata.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/_tasks/download_frame_metadata.ts b/_tasks/download_frame_metadata.ts index cddccf9e5..506afb4f8 100755 --- a/_tasks/download_frame_metadata.ts +++ b/_tasks/download_frame_metadata.ts @@ -2,6 +2,7 @@ import { rawClient as kusama } from "kusama/client.ts" import { rawClient as polkadot } from "polkadot/client.ts" import { rawClient as rococo } from "rococo/client.ts" import { rawClient as westend } from "westend/client.ts" +import { RpcServerError } from "../rpc/mod.ts" const knownClients = { kusama, polkadot, westend, rococo } @@ -34,10 +35,10 @@ Deno.writeTextFileSync(modFilePath, modFileContents, { create: true }) await Promise.all( Object.entries(knownClients).map(async ([name, client]) => { - const r = await client.call("state_getMetadata", []) - if (r.error) throw new Error(r.error.message) + const result = await client.call("state_getMetadata", []) + if (result.error) throw new RpcServerError(result) const outPath = new URL(`_downloaded/${name}.scale`, outDir) console.log(`Downloading ${name} metadata to "${outPath}".`) - await Deno.writeTextFile(outPath, r.result) + await Deno.writeTextFile(outPath, result.result) }), ) From 13f27cfc7cf7471f1f14869a9a2128b41d3241f0 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Thu, 16 Feb 2023 15:31:32 -0500 Subject: [PATCH 05/23] get rid of signal bearer --- fluent/rpc_runes.ts | 2 +- rpc/{RpcClient.ts => client.ts} | 14 ++++++-------- rpc/errors.ts | 2 +- rpc/{rpc_messages.ts => messages.ts} | 0 rpc/mod.ts | 4 ++-- rpc/provider/base.ts | 6 +++--- rpc/provider/smoldot.ts | 3 ++- rpc/provider/ws.ts | 2 +- util/types.ts | 4 ---- 9 files changed, 16 insertions(+), 21 deletions(-) rename rpc/{RpcClient.ts => client.ts} (91%) rename rpc/{rpc_messages.ts => messages.ts} (100%) diff --git a/fluent/rpc_runes.ts b/fluent/rpc_runes.ts index 8a90aa2a5..718148e4a 100644 --- a/fluent/rpc_runes.ts +++ b/fluent/rpc_runes.ts @@ -56,7 +56,7 @@ class RunRpcSubscription unsubscribeMethod, params, (value) => this.push(value), - this, + this.signal, ) } } diff --git a/rpc/RpcClient.ts b/rpc/client.ts similarity index 91% rename from rpc/RpcClient.ts rename to rpc/client.ts index 57765134a..b75d16a08 100644 --- a/rpc/RpcClient.ts +++ b/rpc/client.ts @@ -1,6 +1,4 @@ import { Deferred, deferred } from "../deps/std/async.ts" -import { SignalBearer } from "../util/mod.ts" -import { RpcProvider } from "./provider/base.ts" import { RpcErrorMessage, RpcErrorMessageData, @@ -9,14 +7,14 @@ import { RpcMessageId, RpcNotificationMessage, RpcOkMessage, -} from "./rpc_messages.ts" +} from "./messages.ts" +import { RpcProvider } from "./provider/base.ts" export class RpcClient { conn constructor(readonly provider: RpcProvider, readonly discovery: D) { - this.conn = (controller: AbortController) => - this.provider.ref(discovery, this.handler, controller) + this.conn = (signal: AbortSignal) => this.provider.ref(discovery, this.handler, signal) } callResultPendings: Record> = {} @@ -25,7 +23,7 @@ export class RpcClient { params: unknown[], ) { const controller = new AbortController() - const conn = this.conn(controller) + const conn = this.conn(controller.signal) await conn.ready() const id = conn.currentId++ const pending = deferred>() @@ -47,10 +45,10 @@ export class RpcClient { unsubscribe: string, params: unknown[], handler: RpcSubscriptionHandler, - { signal }: SignalBearer, + signal: AbortSignal, ) { const providerController = new AbortController() - const conn = this.conn(providerController) + const conn = this.conn(providerController.signal) await conn.ready() const subscribeId = conn.currentId++ this.subscriptionInitPendings[subscribeId] = handler as RpcSubscriptionHandler diff --git a/rpc/errors.ts b/rpc/errors.ts index af8653c79..df8ca1de6 100644 --- a/rpc/errors.ts +++ b/rpc/errors.ts @@ -1,4 +1,4 @@ -import { RpcErrorMessage } from "./rpc_messages.ts" +import { RpcErrorMessage } from "./messages.ts" export class RpcClientError extends Error { override readonly name = "RpcClientError" diff --git a/rpc/rpc_messages.ts b/rpc/messages.ts similarity index 100% rename from rpc/rpc_messages.ts rename to rpc/messages.ts diff --git a/rpc/mod.ts b/rpc/mod.ts index ecdc5f612..9f13ae676 100644 --- a/rpc/mod.ts +++ b/rpc/mod.ts @@ -2,7 +2,7 @@ export * as known from "./known/mod.ts" // moderate --exclude known +export * from "./client.ts" export * from "./errors.ts" +export * from "./messages.ts" export * from "./provider/mod.ts" -export * from "./rpc_messages.ts" -export * from "./RpcClient.ts" diff --git a/rpc/provider/base.ts b/rpc/provider/base.ts index 90754f47c..36e7c2584 100644 --- a/rpc/provider/base.ts +++ b/rpc/provider/base.ts @@ -1,12 +1,12 @@ -import { getOrInit, SignalBearer } from "../../util/mod.ts" -import { RpcHandler, RpcMessageId } from "../rpc_messages.ts" +import { getOrInit } from "../../util/mod.ts" +import { RpcHandler, RpcMessageId } from "../messages.ts" export class RpcProvider { conns = new Map>() constructor(readonly init: (discovery: D) => RpcConn) {} - ref(discovery: D, handler: RpcHandler, { signal }: SignalBearer) { + ref(discovery: D, handler: RpcHandler, signal: AbortSignal) { const conn = getOrInit(this.conns, discovery, () => this.init(discovery)) const references = conn.references.get(handler) if (!references) conn.references.set(handler, 1) diff --git a/rpc/provider/smoldot.ts b/rpc/provider/smoldot.ts index 97ad1f519..1d0bd9d27 100644 --- a/rpc/provider/smoldot.ts +++ b/rpc/provider/smoldot.ts @@ -1,7 +1,7 @@ import { start } from "../../deps/smoldot.ts" import { Chain, Client, ClientOptions } from "../../deps/smoldot/client.d.ts" import { deferred } from "../../deps/std/async.ts" -import { RpcEgressMessage, RpcMessageId } from "../rpc_messages.ts" +import { RpcEgressMessage, RpcMessageId } from "../messages.ts" import { RpcConn, RpcProvider } from "./base.ts" // TODO: fix the many possible race conditions @@ -67,6 +67,7 @@ export class SmoldotRpcConn extends RpcConn> { } async close() { + this.stopListening() ;(await this.inner).remove() } diff --git a/rpc/provider/ws.ts b/rpc/provider/ws.ts index a49ce4e08..b41a95117 100644 --- a/rpc/provider/ws.ts +++ b/rpc/provider/ws.ts @@ -1,5 +1,5 @@ import { RpcClientError } from "../errors.ts" -import { RpcEgressMessage, RpcMessageId } from "../rpc_messages.ts" +import { RpcEgressMessage, RpcMessageId } from "../messages.ts" import { RpcConn, RpcProvider } from "./base.ts" export const wsRpcProvider = new RpcProvider((discovery: string) => new WsRpcConn(discovery)) diff --git a/util/types.ts b/util/types.ts index bd57d51bf..7570aff41 100644 --- a/util/types.ts +++ b/util/types.ts @@ -1,5 +1 @@ export type PromiseOr = T | Promise - -export interface SignalBearer { - signal: AbortSignal -} From e016ff30752d6437af797f8e4fea5292cb0d708f Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Thu, 16 Feb 2023 16:48:47 -0500 Subject: [PATCH 06/23] clean up rpc api --- fluent/ClientRune.ts | 1 + fluent/rpc_runes.ts | 10 +++---- providers/frame/FrameProxyProvider.ts | 8 +++--- rpc/{provider/base.ts => ConnRefCounter.ts} | 24 +++++----------- rpc/client.ts | 11 ++++++-- rpc/conn/base.ts | 11 ++++++++ rpc/{provider => conn}/mod.ts | 0 rpc/{provider => conn}/smoldot.ts | 31 +++++++-------------- rpc/{provider => conn}/ws.ts | 14 ++-------- rpc/messages.ts | 11 ++++++++ rpc/mod.ts | 3 +- 11 files changed, 62 insertions(+), 62 deletions(-) rename rpc/{provider/base.ts => ConnRefCounter.ts} (50%) create mode 100644 rpc/conn/base.ts rename rpc/{provider => conn}/mod.ts (100%) rename rpc/{provider => conn}/smoldot.ts (69%) rename rpc/{provider => conn}/ws.ts (80%) diff --git a/fluent/ClientRune.ts b/fluent/ClientRune.ts index 0fe3b8f28..de28bd6f1 100644 --- a/fluent/ClientRune.ts +++ b/fluent/ClientRune.ts @@ -15,6 +15,7 @@ export interface Chain { event: E } +// TODO: do we want to represent the discovery value and conn type within the type system? export class ClientRune extends Rune, U> { latestBlock = this.block(chain.getBlockHash( this, diff --git a/fluent/rpc_runes.ts b/fluent/rpc_runes.ts index 718148e4a..723bb10bd 100644 --- a/fluent/rpc_runes.ts +++ b/fluent/rpc_runes.ts @@ -1,7 +1,7 @@ import { RpcClient, RpcClientError, - RpcProvider, + RpcConnCtor, RpcServerError, RpcSubscriptionMessage, } from "../rpc/mod.ts" @@ -11,7 +11,7 @@ import { ClientRune } from "./ClientRune.ts" class RunRpcClient extends Run, never> { constructor( ctx: Batch, - readonly provider: RpcProvider, + readonly connCtor: RpcConnCtor, readonly discoveryValue: D, ) { super(ctx) @@ -19,12 +19,12 @@ class RunRpcClient extends Run, never> { client?: RpcClient async _evaluate(): Promise> { - return this.client ??= new RpcClient(this.provider, this.discoveryValue) + return this.client ??= new RpcClient(this.connCtor, this.discoveryValue) } } -export function rpcClient(provider: RpcProvider, discoveryValue: D) { - return Rune.new(RunRpcClient, provider, discoveryValue).into(ClientRune) +export function rpcClient(connCtor: RpcConnCtor, discoveryValue: D) { + return Rune.new(RunRpcClient, connCtor, discoveryValue).into(ClientRune) } export function rpcCall(method: string) { diff --git a/providers/frame/FrameProxyProvider.ts b/providers/frame/FrameProxyProvider.ts index 88a833732..774569434 100644 --- a/providers/frame/FrameProxyProvider.ts +++ b/providers/frame/FrameProxyProvider.ts @@ -1,6 +1,6 @@ import { File } from "../../codegen/frame/mod.ts" import { deferred } from "../../deps/std/async.ts" -import { RpcClient, wsRpcProvider } from "../../rpc/mod.ts" +import { RpcClient, WsRpcConn } from "../../rpc/mod.ts" import { PathInfo } from "../../server/mod.ts" import { fromPathInfo } from "../../server/PathInfo.ts" import { FrameProvider } from "./FrameProvider.ts" @@ -60,7 +60,7 @@ export abstract class FrameProxyProvider extends FrameProvider { } async client(pathInfo: PathInfo) { - return new RpcClient(wsRpcProvider, await this.dynamicUrl(pathInfo)) + return new RpcClient(WsRpcConn, await this.dynamicUrl(pathInfo)) } async clientFile(pathInfo: PathInfo) { @@ -71,9 +71,9 @@ export abstract class FrameProxyProvider extends FrameProvider { export const discoveryValue = "${url}" - export const client = C.rpcClient(C.wsRpcProvider, discoveryValue)["_asCodegen"]() + export const client = C.rpcClient(C.WsRpcConn, discoveryValue)["_asCodegen"]() - export const rawClient = new C.RpcClient(C.wsRpcProvider, discoveryValue) + export const rawClient = new C.RpcClient(C.WsRpcConn, discoveryValue) `) } } diff --git a/rpc/provider/base.ts b/rpc/ConnRefCounter.ts similarity index 50% rename from rpc/provider/base.ts rename to rpc/ConnRefCounter.ts index 36e7c2584..0dc62735b 100644 --- a/rpc/provider/base.ts +++ b/rpc/ConnRefCounter.ts @@ -1,13 +1,14 @@ -import { getOrInit } from "../../util/mod.ts" -import { RpcHandler, RpcMessageId } from "../messages.ts" +import { getOrInit } from "../util/mod.ts" +import { RpcConn, RpcConnCtor } from "./conn/mod.ts" +import { RpcHandler } from "./messages.ts" -export class RpcProvider { - conns = new Map>() +export class ConnRefCounter { + conns = new Map() - constructor(readonly init: (discovery: D) => RpcConn) {} + constructor(readonly connCtor: RpcConnCtor) {} ref(discovery: D, handler: RpcHandler, signal: AbortSignal) { - const conn = getOrInit(this.conns, discovery, () => this.init(discovery)) + const conn = getOrInit(this.conns, discovery, () => new this.connCtor(discovery)) const references = conn.references.get(handler) if (!references) conn.references.set(handler, 1) else conn.references.set(handler, references + 1) @@ -25,14 +26,3 @@ export class RpcProvider { return conn } } - -export abstract class RpcConn { - currentId = 0 - references = new Map() - - abstract inner: I - - abstract close(): void - abstract send(id: RpcMessageId, method: string, params: unknown): void - abstract ready(): Promise -} diff --git a/rpc/client.ts b/rpc/client.ts index b75d16a08..a5a9d186c 100644 --- a/rpc/client.ts +++ b/rpc/client.ts @@ -1,4 +1,7 @@ import { Deferred, deferred } from "../deps/std/async.ts" +import { getOrInit } from "../util/mod.ts" +import { RpcConnCtor } from "./conn/mod.ts" +import { ConnRefCounter } from "./ConnRefCounter.ts" import { RpcErrorMessage, RpcErrorMessageData, @@ -8,13 +11,15 @@ import { RpcNotificationMessage, RpcOkMessage, } from "./messages.ts" -import { RpcProvider } from "./provider/base.ts" + +const connRefCounters = new WeakMap, ConnRefCounter>() export class RpcClient { conn - constructor(readonly provider: RpcProvider, readonly discovery: D) { - this.conn = (signal: AbortSignal) => this.provider.ref(discovery, this.handler, signal) + constructor(readonly connCtor: RpcConnCtor, readonly discovery: D) { + const connRefCounter = getOrInit(connRefCounters, connCtor, () => new ConnRefCounter(connCtor)) + this.conn = (signal: AbortSignal) => connRefCounter.ref(discovery, this.handler, signal) } callResultPendings: Record> = {} diff --git a/rpc/conn/base.ts b/rpc/conn/base.ts new file mode 100644 index 000000000..fbc782a26 --- /dev/null +++ b/rpc/conn/base.ts @@ -0,0 +1,11 @@ +import { RpcHandler, RpcMessageId } from "../messages.ts" + +export type RpcConnCtor = new(discovery: D) => RpcConn +export abstract class RpcConn { + currentId = 0 + references = new Map() + + abstract close(): void + abstract send(id: RpcMessageId, method: string, params: unknown): void + abstract ready(): Promise +} diff --git a/rpc/provider/mod.ts b/rpc/conn/mod.ts similarity index 100% rename from rpc/provider/mod.ts rename to rpc/conn/mod.ts diff --git a/rpc/provider/smoldot.ts b/rpc/conn/smoldot.ts similarity index 69% rename from rpc/provider/smoldot.ts rename to rpc/conn/smoldot.ts index 1d0bd9d27..1ef701f14 100644 --- a/rpc/provider/smoldot.ts +++ b/rpc/conn/smoldot.ts @@ -1,8 +1,8 @@ import { start } from "../../deps/smoldot.ts" -import { Chain, Client, ClientOptions } from "../../deps/smoldot/client.d.ts" +import { Client, ClientOptions } from "../../deps/smoldot/client.d.ts" import { deferred } from "../../deps/std/async.ts" import { RpcEgressMessage, RpcMessageId } from "../messages.ts" -import { RpcConn, RpcProvider } from "./base.ts" +import { RpcConn } from "./base.ts" // TODO: fix the many possible race conditions @@ -11,14 +11,10 @@ export interface SmoldotDiscovery { parachainSpec?: string } -export const smoldotRpcProvider = new RpcProvider((discovery: SmoldotDiscovery) => - new SmoldotRpcConn(discovery) -) - let client: undefined | Client -export class SmoldotRpcConn extends RpcConn> { - inner +export class SmoldotRpcConn extends RpcConn { + chainPending listening stopListening @@ -37,14 +33,14 @@ export class SmoldotRpcConn extends RpcConn> { chainSpec: discovery.relayChainSpec, disableJsonRpc: true, }) - this.inner = (async () => { + this.chainPending = (async () => { return client.addChain({ chainSpec: parachainSpec, potentialRelayChains: [await relayChain], }) })() } else { - this.inner = client.addChain({ chainSpec: discovery.relayChainSpec }) + this.chainPending = client.addChain({ chainSpec: discovery.relayChainSpec }) } this.listening = deferred() this.stopListening = () => this.listening.resolve() @@ -52,7 +48,7 @@ export class SmoldotRpcConn extends RpcConn> { } async startListening() { - const inner = await this.inner + const inner = await this.chainPending while (true) { try { const response = await Promise.race([ @@ -68,21 +64,14 @@ export class SmoldotRpcConn extends RpcConn> { async close() { this.stopListening() - ;(await this.inner).remove() + ;(await this.chainPending).remove() } async send(id: RpcMessageId, method: string, params: unknown[]) { - const inner = await this.inner - const message: RpcEgressMessage = { - jsonrpc: "2.0", - id, - method, - params, - } - inner.sendJsonRpc(JSON.stringify(message)) + ;(await this.chainPending).sendJsonRpc(RpcEgressMessage.fmt(id, method, params)) } async ready() { - await this.inner + await this.chainPending } } diff --git a/rpc/provider/ws.ts b/rpc/conn/ws.ts similarity index 80% rename from rpc/provider/ws.ts rename to rpc/conn/ws.ts index b41a95117..9a6b54ca8 100644 --- a/rpc/provider/ws.ts +++ b/rpc/conn/ws.ts @@ -1,10 +1,8 @@ import { RpcClientError } from "../errors.ts" import { RpcEgressMessage, RpcMessageId } from "../messages.ts" -import { RpcConn, RpcProvider } from "./base.ts" +import { RpcConn } from "./base.ts" -export const wsRpcProvider = new RpcProvider((discovery: string) => new WsRpcConn(discovery)) - -export class WsRpcConn extends RpcConn { +export class WsRpcConn extends RpcConn { inner constructor(readonly discovery: string) { @@ -21,13 +19,7 @@ export class WsRpcConn extends RpcConn { } send(id: RpcMessageId, method: string, params: unknown[]) { - const message: RpcEgressMessage = { - jsonrpc: "2.0", - id, - method, - params, - } - this.inner.send(JSON.stringify(message)) + this.inner.send(RpcEgressMessage.fmt(id, method, params)) } async ready() { diff --git a/rpc/messages.ts b/rpc/messages.ts index d85a2b458..2003714ba 100644 --- a/rpc/messages.ts +++ b/rpc/messages.ts @@ -2,6 +2,17 @@ export interface RpcEgressMessage extends RpcVersionBearer, RpcMessageIdBearer { method: string params: any[] } +export namespace RpcEgressMessage { + export function fmt(id: RpcMessageId, method: string, params: unknown[]) { + const message: RpcEgressMessage = { + jsonrpc: "2.0", + id, + method, + params, + } + return JSON.stringify(message) + } +} export type RpcHandler = ( message: Message, diff --git a/rpc/mod.ts b/rpc/mod.ts index 9f13ae676..9ff71ae56 100644 --- a/rpc/mod.ts +++ b/rpc/mod.ts @@ -3,6 +3,7 @@ export * as known from "./known/mod.ts" // moderate --exclude known export * from "./client.ts" +export * from "./conn/mod.ts" +export * from "./ConnRefCounter.ts" export * from "./errors.ts" export * from "./messages.ts" -export * from "./provider/mod.ts" From f00407ab1f252771ba5cd57270d6e4553d79bc96 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 17 Feb 2023 10:32:31 -0500 Subject: [PATCH 07/23] more cleanup --- rpc/ConnRefCounter.ts | 28 -------------- rpc/ConnsRefCounter.ts | 28 ++++++++++++++ rpc/{client.ts => RpcClient.ts} | 59 +++++++++++++++++------------- rpc/conn/base.ts | 13 ++++--- rpc/conn/smoldot.ts | 37 +++++++++---------- rpc/conn/ws.ts | 37 +++++++++---------- rpc/errors.ts | 18 --------- rpc/mod.ts | 7 ++-- rpc/{messages.ts => rpc_common.ts} | 29 ++++++++++++--- words.txt | 3 +- 10 files changed, 134 insertions(+), 125 deletions(-) delete mode 100644 rpc/ConnRefCounter.ts create mode 100644 rpc/ConnsRefCounter.ts rename rpc/{client.ts => RpcClient.ts} (64%) delete mode 100644 rpc/errors.ts rename rpc/{messages.ts => rpc_common.ts} (70%) diff --git a/rpc/ConnRefCounter.ts b/rpc/ConnRefCounter.ts deleted file mode 100644 index 0dc62735b..000000000 --- a/rpc/ConnRefCounter.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { getOrInit } from "../util/mod.ts" -import { RpcConn, RpcConnCtor } from "./conn/mod.ts" -import { RpcHandler } from "./messages.ts" - -export class ConnRefCounter { - conns = new Map() - - constructor(readonly connCtor: RpcConnCtor) {} - - ref(discovery: D, handler: RpcHandler, signal: AbortSignal) { - const conn = getOrInit(this.conns, discovery, () => new this.connCtor(discovery)) - const references = conn.references.get(handler) - if (!references) conn.references.set(handler, 1) - else conn.references.set(handler, references + 1) - signal.addEventListener("abort", () => { - const references = conn.references.get(handler)! - conn.references.set(handler, references - 1) - if (references === 1) { - conn.references.delete(handler) - if (!conn.references.size) { - this.conns.delete(discovery) - conn.close() - } - } - }) - return conn - } -} diff --git a/rpc/ConnsRefCounter.ts b/rpc/ConnsRefCounter.ts new file mode 100644 index 000000000..db69a62d4 --- /dev/null +++ b/rpc/ConnsRefCounter.ts @@ -0,0 +1,28 @@ +import { getOrInit } from "../util/mod.ts" +import { RpcConn, RpcConnCtor } from "./conn/mod.ts" +import { RpcHandler } from "./rpc_common.ts" + +export class ConnsRefCounter { + conns = new Map() + + constructor(readonly connCtor: RpcConnCtor) {} + + ref(discoveryValue: D, handler: RpcHandler, signal: AbortSignal) { + const conn = getOrInit(this.conns, discoveryValue, () => new this.connCtor(discoveryValue)) + const references = conn.handlerReferenceCount.get(handler) + if (!references) conn.handlerReferenceCount.set(handler, 1) + else conn.handlerReferenceCount.set(handler, references + 1) + signal.addEventListener("abort", () => { + const references = conn.handlerReferenceCount.get(handler)! + conn.handlerReferenceCount.set(handler, references - 1) + if (references === 1) { + conn.handlerReferenceCount.delete(handler) + if (!conn.handlerReferenceCount.size) { + this.conns.delete(discoveryValue) + conn.close() + } + } + }) + return conn + } +} diff --git a/rpc/client.ts b/rpc/RpcClient.ts similarity index 64% rename from rpc/client.ts rename to rpc/RpcClient.ts index a5a9d186c..e822501e6 100644 --- a/rpc/client.ts +++ b/rpc/RpcClient.ts @@ -1,7 +1,7 @@ import { Deferred, deferred } from "../deps/std/async.ts" import { getOrInit } from "../util/mod.ts" import { RpcConnCtor } from "./conn/mod.ts" -import { ConnRefCounter } from "./ConnRefCounter.ts" +import { ConnsRefCounter } from "./ConnsRefCounter.ts" import { RpcErrorMessage, RpcErrorMessageData, @@ -10,16 +10,20 @@ import { RpcMessageId, RpcNotificationMessage, RpcOkMessage, -} from "./messages.ts" +} from "./rpc_common.ts" -const connRefCounters = new WeakMap, ConnRefCounter>() +const connRefCounters = new WeakMap, ConnsRefCounter>() export class RpcClient { conn - constructor(readonly connCtor: RpcConnCtor, readonly discovery: D) { - const connRefCounter = getOrInit(connRefCounters, connCtor, () => new ConnRefCounter(connCtor)) - this.conn = (signal: AbortSignal) => connRefCounter.ref(discovery, this.handler, signal) + constructor(readonly connCtor: RpcConnCtor, readonly discoveryValue: D) { + const connRefCounter = getOrInit( + connRefCounters, + connCtor, + () => new ConnsRefCounter(connCtor), + ) + this.conn = (signal: AbortSignal) => connRefCounter.ref(discoveryValue, this.handler, signal) } callResultPendings: Record> = {} @@ -40,9 +44,9 @@ export class RpcClient { } subscriptionInitPendings: Record = {} - subscriptions: Record = {} + subscriptionHandlers: Record = {} subscriptionIdByRpcMessageId: Record = {} - async subscription< + subscription< NotificationData = unknown, ErrorData extends RpcErrorMessageData = RpcErrorMessageData, >( @@ -52,21 +56,23 @@ export class RpcClient { handler: RpcSubscriptionHandler, signal: AbortSignal, ) { - const providerController = new AbortController() - const conn = this.conn(providerController.signal) - await conn.ready() - const subscribeId = conn.currentId++ - this.subscriptionInitPendings[subscribeId] = handler as RpcSubscriptionHandler - signal.addEventListener("abort", () => { - delete this.subscriptionInitPendings[subscribeId] - const subscriptionId = this.subscriptionIdByRpcMessageId[subscribeId] - if (subscriptionId) { - delete this.subscriptionIdByRpcMessageId[subscribeId] - conn.send(conn.currentId++, unsubscribe, [subscriptionId]) - providerController.abort() - } - }) - conn.send(subscribeId, subscribe, params) + const controller = new AbortController() + const conn = this.conn(controller.signal) + ;(async () => { + await conn.ready() + const subscribeId = conn.currentId++ + this.subscriptionInitPendings[subscribeId] = handler as RpcSubscriptionHandler + signal.addEventListener("abort", () => { + delete this.subscriptionInitPendings[subscribeId] + const subscriptionId = this.subscriptionIdByRpcMessageId[subscribeId] + if (subscriptionId) { + delete this.subscriptionIdByRpcMessageId[subscribeId] + conn.send(conn.currentId++, unsubscribe, [subscriptionId]) + controller.abort() + } + }) + conn.send(subscribeId, subscribe, params) + })() } // TODO: error handling @@ -82,12 +88,13 @@ export class RpcClient { if (subscriptionPending) { if (message.error) subscriptionPending(message) else { - this.subscriptions[message.result] = subscriptionPending + this.subscriptionHandlers[message.result] = subscriptionPending this.subscriptionIdByRpcMessageId[message.id] = message.result } delete this.subscriptionInitPendings[message.id] } - } else if (message.params) this.subscriptions[message.params.subscription]?.(message) + } else if (message.params) this.subscriptionHandlers[message.params.subscription]?.(message) + // TODO: log message about fallthrough if in `--dbg` } } @@ -99,7 +106,7 @@ export type RpcCallMessage< export type RpcSubscriptionMessage< NotificationData = any, ErrorData extends RpcErrorMessageData = RpcErrorMessageData, -> = RpcNotificationMessage | RpcErrorMessage +> = RpcNotificationMessage | RpcErrorMessage export type RpcSubscriptionHandler< NotificationData = any, diff --git a/rpc/conn/base.ts b/rpc/conn/base.ts index fbc782a26..c687bb2a3 100644 --- a/rpc/conn/base.ts +++ b/rpc/conn/base.ts @@ -1,11 +1,14 @@ -import { RpcHandler, RpcMessageId } from "../messages.ts" +import { RpcHandler, RpcMessageId } from "../rpc_common.ts" + +export type RpcConnCtor = new(discoveryValue: D) => RpcConn -export type RpcConnCtor = new(discovery: D) => RpcConn export abstract class RpcConn { currentId = 0 - references = new Map() + handlerReferenceCount = new Map() - abstract close(): void - abstract send(id: RpcMessageId, method: string, params: unknown): void abstract ready(): Promise + + abstract send(id: RpcMessageId, method: string, params: unknown): void + + abstract close(): void } diff --git a/rpc/conn/smoldot.ts b/rpc/conn/smoldot.ts index 1ef701f14..357a7647b 100644 --- a/rpc/conn/smoldot.ts +++ b/rpc/conn/smoldot.ts @@ -1,12 +1,12 @@ import { start } from "../../deps/smoldot.ts" import { Client, ClientOptions } from "../../deps/smoldot/client.d.ts" import { deferred } from "../../deps/std/async.ts" -import { RpcEgressMessage, RpcMessageId } from "../messages.ts" +import { RpcEgressMessage, RpcMessageId } from "../rpc_common.ts" import { RpcConn } from "./base.ts" // TODO: fix the many possible race conditions -export interface SmoldotDiscovery { +export interface SmoldotRpcConnProps { relayChainSpec: string parachainSpec?: string } @@ -18,7 +18,7 @@ export class SmoldotRpcConn extends RpcConn { listening stopListening - constructor(readonly discovery: SmoldotDiscovery) { + constructor(readonly props: SmoldotRpcConnProps) { super() if (!client) { client = start({ @@ -27,51 +27,50 @@ export class SmoldotRpcConn extends RpcConn { cpuRateLimit: .25, } as ClientOptions) } - if (discovery.parachainSpec) { - const { parachainSpec } = discovery + if (props.parachainSpec) { const relayChain = client.addChain({ - chainSpec: discovery.relayChainSpec, + chainSpec: props.relayChainSpec, disableJsonRpc: true, }) + const { parachainSpec } = props this.chainPending = (async () => { return client.addChain({ chainSpec: parachainSpec, potentialRelayChains: [await relayChain], }) })() - } else { - this.chainPending = client.addChain({ chainSpec: discovery.relayChainSpec }) - } + } else this.chainPending = client.addChain({ chainSpec: props.relayChainSpec }) this.listening = deferred() this.stopListening = () => this.listening.resolve() this.startListening() } async startListening() { - const inner = await this.chainPending + const chain = await this.chainPending while (true) { try { const response = await Promise.race([ this.listening, - inner.nextJsonRpcResponse(), + chain.nextJsonRpcResponse(), ]) if (!response) break const message = JSON.parse(response) - for (const reference of this.references.keys()) reference(message) + for (const handler of this.handlerReferenceCount.keys()) handler(message) } catch (_e) {} } } - async close() { - this.stopListening() - ;(await this.chainPending).remove() + async ready() { + await this.chainPending } - async send(id: RpcMessageId, method: string, params: unknown[]) { - ;(await this.chainPending).sendJsonRpc(RpcEgressMessage.fmt(id, method, params)) + send(id: RpcMessageId, method: string, params: unknown[]) { + this.chainPending + .then((chain) => chain.sendJsonRpc(RpcEgressMessage.fmt(id, method, params))) } - async ready() { - await this.chainPending + close() { + this.stopListening() + this.chainPending.then((chain) => chain.remove()) } } diff --git a/rpc/conn/ws.ts b/rpc/conn/ws.ts index 9a6b54ca8..ebe817142 100644 --- a/rpc/conn/ws.ts +++ b/rpc/conn/ws.ts @@ -1,44 +1,35 @@ -import { RpcClientError } from "../errors.ts" -import { RpcEgressMessage, RpcMessageId } from "../messages.ts" +import { RpcClientError, RpcEgressMessage, RpcMessageId } from "../rpc_common.ts" import { RpcConn } from "./base.ts" export class WsRpcConn extends RpcConn { - inner + chain - constructor(readonly discovery: string) { + constructor(readonly url: string) { super() - this.inner = new WebSocket(discovery) - this.inner.addEventListener("message", (e) => { + this.chain = new WebSocket(url) + this.chain.addEventListener("message", (e) => { const message = JSON.parse(e.data) - for (const reference of this.references.keys()) reference(message) + for (const handler of this.handlerReferenceCount.keys()) handler(message) }) } - close() { - this.inner.close() - } - - send(id: RpcMessageId, method: string, params: unknown[]) { - this.inner.send(RpcEgressMessage.fmt(id, method, params)) - } - async ready() { - switch (this.inner.readyState) { + switch (this.chain.readyState) { case WebSocket.OPEN: return case WebSocket.CONNECTING: { try { return await new Promise((resolve, reject) => { const controller = new AbortController() - this.inner.addEventListener("open", () => { + this.chain.addEventListener("open", () => { controller.abort() resolve() }, controller) - this.inner.addEventListener("close", () => { + this.chain.addEventListener("close", () => { controller.abort() reject() }, controller) - this.inner.addEventListener("error", () => { + this.chain.addEventListener("error", () => { controller.abort() reject() }, controller) @@ -53,4 +44,12 @@ export class WsRpcConn extends RpcConn { } } } + + send(id: RpcMessageId, method: string, params: unknown[]) { + this.chain.send(RpcEgressMessage.fmt(id, method, params)) + } + + close() { + this.chain.close() + } } diff --git a/rpc/errors.ts b/rpc/errors.ts deleted file mode 100644 index df8ca1de6..000000000 --- a/rpc/errors.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { RpcErrorMessage } from "./messages.ts" - -export class RpcClientError extends Error { - override readonly name = "RpcClientError" -} - -export class RpcServerError extends Error { - override readonly name = "RpcServerError" - code - data - - // TODO: accept init `EgressMessage`? - constructor({ error: { code, data, message } }: RpcErrorMessage) { - super(message, { cause: message }) - this.code = code - this.data = data - } -} diff --git a/rpc/mod.ts b/rpc/mod.ts index 9ff71ae56..e14dce283 100644 --- a/rpc/mod.ts +++ b/rpc/mod.ts @@ -2,8 +2,7 @@ export * as known from "./known/mod.ts" // moderate --exclude known -export * from "./client.ts" export * from "./conn/mod.ts" -export * from "./ConnRefCounter.ts" -export * from "./errors.ts" -export * from "./messages.ts" +export * from "./ConnsRefCounter.ts" +export * from "./rpc_common.ts" +export * from "./RpcClient.ts" diff --git a/rpc/messages.ts b/rpc/rpc_common.ts similarity index 70% rename from rpc/messages.ts rename to rpc/rpc_common.ts index 2003714ba..7605992f6 100644 --- a/rpc/messages.ts +++ b/rpc/rpc_common.ts @@ -14,12 +14,12 @@ export namespace RpcEgressMessage { } } +export type RpcIngressMessage = RpcOkMessage | RpcErrorMessage | RpcNotificationMessage + export type RpcHandler = ( - message: Message, + e: Message, ) => void -export type RpcIngressMessage = RpcOkMessage | RpcErrorMessage | RpcNotificationMessage - export interface RpcOkMessage extends RpcVersionBearer, RpcMessageIdBearer { result: OkData params?: never @@ -39,8 +39,10 @@ export interface RpcErrorMessageData { data: Data } -export interface RpcNotificationMessage extends RpcVersionBearer { - method: string // we could narrow, but it's not all that useful +export interface RpcNotificationMessage + extends RpcVersionBearer +{ + method: Method id?: never params: { subscription: string @@ -59,3 +61,20 @@ export type RpcMessageId = number | string export interface RpcMessageIdBearer { id: RpcMessageId } + +export class RpcClientError extends Error { + override readonly name = "RpcClientError" +} + +export class RpcServerError extends Error { + override readonly name = "RpcServerError" + code + data + + // TODO: accept init `EgressMessage`? + constructor({ error: { code, data, message } }: RpcErrorMessage) { + super(message, { cause: message }) + this.code = code + this.data = data + } +} diff --git a/words.txt b/words.txt index d8cd68cfc..9f01e2527 100644 --- a/words.txt +++ b/words.txt @@ -36,11 +36,12 @@ codespaces combinators concat contextfree +curto contramap +counteds creds cummon curto -Curto darwinia datahighway datetime From e95268f17d8932e87833933b3635201dbb038926 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 17 Feb 2023 10:37:01 -0500 Subject: [PATCH 08/23] more cleanup --- fluent/rpc_runes.ts | 10 +++++----- rpc/RpcClient.ts | 9 ++++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/fluent/rpc_runes.ts b/fluent/rpc_runes.ts index 723bb10bd..b313c4cd2 100644 --- a/fluent/rpc_runes.ts +++ b/fluent/rpc_runes.ts @@ -40,8 +40,8 @@ export function rpcCall(method: string) { } } -class RunRpcSubscription - extends RunStream> +class RunRpcSubscription + extends RunStream> { constructor( ctx: Batch, @@ -51,7 +51,7 @@ class RunRpcSubscription unsubscribeMethod: string, ) { super(ctx) - client.subscription( + client.subscription( subscribeMethod, unsubscribeMethod, params, @@ -62,12 +62,12 @@ class RunRpcSubscription } export function rpcSubscription() { - return (subscribeMethod: string, unsubscribeMethod: string) => { + return (subscribeMethod: Method, unsubscribeMethod: string) => { return (...args: RunicArgs, ...params: Params]>) => { return Rune.tuple(args) .map(([client, ...params]) => Rune.new( - RunRpcSubscription, + RunRpcSubscription, client, params, subscribeMethod, diff --git a/rpc/RpcClient.ts b/rpc/RpcClient.ts index e822501e6..b57c0c90c 100644 --- a/rpc/RpcClient.ts +++ b/rpc/RpcClient.ts @@ -47,13 +47,14 @@ export class RpcClient { subscriptionHandlers: Record = {} subscriptionIdByRpcMessageId: Record = {} subscription< + Method extends string = string, NotificationData = unknown, ErrorData extends RpcErrorMessageData = RpcErrorMessageData, >( subscribe: string, unsubscribe: string, params: unknown[], - handler: RpcSubscriptionHandler, + handler: RpcSubscriptionHandler, signal: AbortSignal, ) { const controller = new AbortController() @@ -104,11 +105,13 @@ export type RpcCallMessage< > = RpcOkMessage | RpcErrorMessage export type RpcSubscriptionMessage< + Method extends string = string, NotificationData = any, ErrorData extends RpcErrorMessageData = RpcErrorMessageData, -> = RpcNotificationMessage | RpcErrorMessage +> = RpcNotificationMessage | RpcErrorMessage export type RpcSubscriptionHandler< + Method extends string = string, NotificationData = any, ErrorData extends RpcErrorMessageData = RpcErrorMessageData, -> = RpcHandler> +> = RpcHandler> From c33fd5b7b183b93beea4d66db90ca72744e44a7d Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 17 Feb 2023 10:37:49 -0500 Subject: [PATCH 09/23] readd -r to star task --- _tasks/star.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_tasks/star.ts b/_tasks/star.ts index 6091bf177..4b272b7b0 100755 --- a/_tasks/star.ts +++ b/_tasks/star.ts @@ -22,7 +22,7 @@ await Deno.writeTextFile(dest, generated) const data: Data = JSON.parse( new TextDecoder().decode( await Deno.run({ - cmd: ["deno", "info", "--json", "target/star.ts"], + cmd: ["deno", "info", "-r=http://localhost:4646/", "--json", "target/star.ts"], stdout: "piped", }) .output(), From bc2707a20e1216870547c01f0ff58c70d565f64f Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 17 Feb 2023 10:50:25 -0500 Subject: [PATCH 10/23] words --- words.txt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/words.txt b/words.txt index 9f01e2527..cf8de1b8b 100644 --- a/words.txt +++ b/words.txt @@ -1,3 +1,5 @@ +lxkf +Tpeyg abortable acala ajun @@ -36,9 +38,7 @@ codespaces combinators concat contextfree -curto contramap -counteds creds cummon curto @@ -104,7 +104,6 @@ litentry lldb lparachain ltex -Lxkf mathchain matklad merkle @@ -201,7 +200,6 @@ tnkr todos tomaka tostring -Tpeyg transcoders trufflehog trufflesecurity From 13ac2e825a38c70517404375908a3474ff38a8c3 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 17 Feb 2023 10:51:52 -0500 Subject: [PATCH 11/23] words --- words.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/words.txt b/words.txt index cf8de1b8b..abf333ea4 100644 --- a/words.txt +++ b/words.txt @@ -1,5 +1,3 @@ -lxkf -Tpeyg abortable acala ajun @@ -104,6 +102,7 @@ litentry lldb lparachain ltex +lxkf mathchain matklad merkle @@ -200,6 +199,7 @@ tnkr todos tomaka tostring +tpeyg transcoders trufflehog trufflesecurity From ad0558d0bf8e2c6e56ac48ab09bf124d56a73e4a Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 17 Feb 2023 11:30:49 -0500 Subject: [PATCH 12/23] fixing xcm example --- zombienets/statemine.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/zombienets/statemine.toml b/zombienets/statemine.toml index 782f50f8d..f905ab5cf 100644 --- a/zombienets/statemine.toml +++ b/zombienets/statemine.toml @@ -12,6 +12,14 @@ validator = true name = "bob" validator = true +[[relaychain.nodes]] +name = "charlie" +validator = true + +[[relaychain.nodes]] +name = "dave" +validator = true + [[parachains]] id = 1000 cumulus_based = true From ca64d988a98f0cf237f1943adba1005f5c4ad52b Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Sat, 18 Feb 2023 11:36:40 -0500 Subject: [PATCH 13/23] cleanup --- rpc/ConnsRefCounter.ts | 14 +++++++------- rpc/RpcClient.ts | 8 ++++---- rpc/conn/base.ts | 8 ++++++-- rpc/conn/smoldot.ts | 3 +-- rpc/conn/ws.ts | 18 +++++++++--------- rpc/rpc_common.ts | 2 +- 6 files changed, 28 insertions(+), 25 deletions(-) diff --git a/rpc/ConnsRefCounter.ts b/rpc/ConnsRefCounter.ts index db69a62d4..fbb1beed8 100644 --- a/rpc/ConnsRefCounter.ts +++ b/rpc/ConnsRefCounter.ts @@ -9,15 +9,15 @@ export class ConnsRefCounter { ref(discoveryValue: D, handler: RpcHandler, signal: AbortSignal) { const conn = getOrInit(this.conns, discoveryValue, () => new this.connCtor(discoveryValue)) - const references = conn.handlerReferenceCount.get(handler) - if (!references) conn.handlerReferenceCount.set(handler, 1) - else conn.handlerReferenceCount.set(handler, references + 1) + const references = conn.handlerReferenceCounts.get(handler) + if (!references) conn.handlerReferenceCounts.set(handler, 1) + else conn.handlerReferenceCounts.set(handler, references + 1) signal.addEventListener("abort", () => { - const references = conn.handlerReferenceCount.get(handler)! - conn.handlerReferenceCount.set(handler, references - 1) + const references = conn.handlerReferenceCounts.get(handler)! + conn.handlerReferenceCounts.set(handler, references - 1) if (references === 1) { - conn.handlerReferenceCount.delete(handler) - if (!conn.handlerReferenceCount.size) { + conn.handlerReferenceCounts.delete(handler) + if (!conn.handlerReferenceCounts.size) { this.conns.delete(discoveryValue) conn.close() } diff --git a/rpc/RpcClient.ts b/rpc/RpcClient.ts index b57c0c90c..7c9cb4370 100644 --- a/rpc/RpcClient.ts +++ b/rpc/RpcClient.ts @@ -23,7 +23,8 @@ export class RpcClient { connCtor, () => new ConnsRefCounter(connCtor), ) - this.conn = (signal: AbortSignal) => connRefCounter.ref(discoveryValue, this.handler, signal) + this.conn = (signal: AbortSignal) => + connRefCounter.ref(discoveryValue, (e) => this.handler(e), signal) } callResultPendings: Record> = {} @@ -76,8 +77,7 @@ export class RpcClient { })() } - // TODO: error handling - handler = (message: RpcIngressMessage) => { + handler(message: RpcIngressMessage) { if (typeof message.id === "number") { const callResultPending = this.callResultPendings[message.id] if (callResultPending) { @@ -95,7 +95,7 @@ export class RpcClient { delete this.subscriptionInitPendings[message.id] } } else if (message.params) this.subscriptionHandlers[message.params.subscription]?.(message) - // TODO: log message about fallthrough if in `--dbg` + else throw new Error(Deno.inspect(message)) // ... for now } } diff --git a/rpc/conn/base.ts b/rpc/conn/base.ts index c687bb2a3..46557aa3d 100644 --- a/rpc/conn/base.ts +++ b/rpc/conn/base.ts @@ -1,14 +1,18 @@ -import { RpcHandler, RpcMessageId } from "../rpc_common.ts" +import { RpcHandler, RpcIngressMessage, RpcMessageId } from "../rpc_common.ts" export type RpcConnCtor = new(discoveryValue: D) => RpcConn export abstract class RpcConn { currentId = 0 - handlerReferenceCount = new Map() + handlerReferenceCounts = new Map() abstract ready(): Promise abstract send(id: RpcMessageId, method: string, params: unknown): void abstract close(): void + + push(message: RpcIngressMessage) { + for (const handler of this.handlerReferenceCounts.keys()) handler(message) + } } diff --git a/rpc/conn/smoldot.ts b/rpc/conn/smoldot.ts index 357a7647b..c4eb1010f 100644 --- a/rpc/conn/smoldot.ts +++ b/rpc/conn/smoldot.ts @@ -54,8 +54,7 @@ export class SmoldotRpcConn extends RpcConn { chain.nextJsonRpcResponse(), ]) if (!response) break - const message = JSON.parse(response) - for (const handler of this.handlerReferenceCount.keys()) handler(message) + this.push(JSON.parse(response)) } catch (_e) {} } } diff --git a/rpc/conn/ws.ts b/rpc/conn/ws.ts index ebe817142..7202ca91c 100644 --- a/rpc/conn/ws.ts +++ b/rpc/conn/ws.ts @@ -7,9 +7,10 @@ export class WsRpcConn extends RpcConn { constructor(readonly url: string) { super() this.chain = new WebSocket(url) - this.chain.addEventListener("message", (e) => { - const message = JSON.parse(e.data) - for (const handler of this.handlerReferenceCount.keys()) handler(message) + this.chain.addEventListener("message", (e) => this.push(JSON.parse(e.data))) + this.chain.addEventListener("error", (e) => { // TODO: recovery + console.log(e) + Deno.exit(1) }) } @@ -25,14 +26,13 @@ export class WsRpcConn extends RpcConn { controller.abort() resolve() }, controller) - this.chain.addEventListener("close", () => { - controller.abort() - reject() - }, controller) - this.chain.addEventListener("error", () => { + this.chain.addEventListener("close", throw_, controller) + this.chain.addEventListener("error", throw_, controller) + + function throw_() { controller.abort() reject() - }, controller) + } }) } catch (_e) { throw new RpcClientError() diff --git a/rpc/rpc_common.ts b/rpc/rpc_common.ts index 7605992f6..88266f65c 100644 --- a/rpc/rpc_common.ts +++ b/rpc/rpc_common.ts @@ -17,7 +17,7 @@ export namespace RpcEgressMessage { export type RpcIngressMessage = RpcOkMessage | RpcErrorMessage | RpcNotificationMessage export type RpcHandler = ( - e: Message, + message: Message, ) => void export interface RpcOkMessage extends RpcVersionBearer, RpcMessageIdBearer { From 54cd04315dedf428dcb21ce7fed7434eab5f0e2b Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Mon, 20 Feb 2023 11:02:09 -0500 Subject: [PATCH 14/23] t6 cleanup --- examples/foo.ts | 14 ++++++ providers/frame/FrameProvider.ts | 40 +++++++++------ providers/frame/FrameProxyProvider.ts | 7 +-- rpc/{RpcClient.ts => Connection.ts} | 72 ++++++++++++++++++--------- rpc/ConnsRefCounter.ts | 28 ----------- rpc/conn/base.ts | 18 ------- rpc/conn/mod.ts | 5 -- rpc/mod.ts | 6 +-- rpc/{conn => }/smoldot.ts | 14 +++--- rpc/{conn => }/ws.ts | 8 +-- util/withSignal.ts | 8 +++ 11 files changed, 113 insertions(+), 107 deletions(-) create mode 100644 examples/foo.ts rename rpc/{RpcClient.ts => Connection.ts} (70%) delete mode 100644 rpc/ConnsRefCounter.ts delete mode 100644 rpc/conn/base.ts delete mode 100644 rpc/conn/mod.ts rename rpc/{conn => }/smoldot.ts (81%) rename rpc/{conn => }/ws.ts (87%) create mode 100644 util/withSignal.ts diff --git a/examples/foo.ts b/examples/foo.ts new file mode 100644 index 000000000..3c50a1e8a --- /dev/null +++ b/examples/foo.ts @@ -0,0 +1,14 @@ +import { WsConnection } from "../rpc/mod.ts" + +const controller = new AbortController() +const client = WsConnection.connect("ws://localhost:4646/frame/dev/polkadot/@x", controller.signal) + +await client.call("state_getMetadata", []) + +await client.call("state_getMetadata", []) + +controller.abort() + +console.log("hmm") + +self.addEventListener("unload", () => console.log("exiting inner")) diff --git a/providers/frame/FrameProvider.ts b/providers/frame/FrameProvider.ts index dac6b04c8..249aed7da 100644 --- a/providers/frame/FrameProvider.ts +++ b/providers/frame/FrameProvider.ts @@ -2,17 +2,18 @@ import { File, FrameCodegen } from "../../codegen/frame/mod.ts" import { posix as path } from "../../deps/std/path.ts" import { $metadata } from "../../frame_metadata/Metadata.ts" import { fromPrefixedHex } from "../../frame_metadata/mod.ts" -import { RpcClient, RpcServerError } from "../../rpc/mod.ts" +import { Connection, RpcServerError } from "../../rpc/mod.ts" import { f, PathInfo, Provider } from "../../server/mod.ts" import { fromPathInfo } from "../../server/PathInfo.ts" import { WeakMemo } from "../../util/mod.ts" +import { withSignal } from "../../util/withSignal.ts" export abstract class FrameProvider extends Provider { generatorId = "frame" codegenCtxsPending: Record> = {} - abstract client(pathInfo: PathInfo): Promise> + abstract client(pathInfo: PathInfo, signal: AbortSignal): Promise abstract clientFile(pathInfo: PathInfo): Promise async handle(request: Request, pathInfo: PathInfo): Promise { @@ -48,9 +49,11 @@ export abstract class FrameProvider extends Provider { // TODO: memo async latestVersion(pathInfo: PathInfo) { - const client = await this.client(pathInfo) - const version = await this.clientCall(client, "system_version", []) - return this.normalizeRuntimeVersion(version) + return await withSignal(async (signal) => { + const client = await this.client(pathInfo, signal) + const version = await this.clientCall(client, "system_version", []) + return this.normalizeRuntimeVersion(version) + }) } normalizeRuntimeVersion(version: string) { @@ -71,20 +74,25 @@ export abstract class FrameProvider extends Provider { } async getMetadata(pathInfo: PathInfo) { - return this.env.cache.get(`${this.cacheKey(pathInfo)}/metadata`, $metadata, async () => { - if (pathInfo.vRuntime !== await this.latestVersion(pathInfo)) { - throw f.serverError("Cannot get metadata for old runtime version") - } - const client = await this.client(pathInfo) - const metadata = fromPrefixedHex( - await this.clientCall(client, "state_getMetadata", []), - ) - return metadata - }) + return this.env.cache.get( + `${this.cacheKey(pathInfo)}/metadata`, + $metadata, + () => + withSignal(async (signal) => { + if (pathInfo.vRuntime !== await this.latestVersion(pathInfo)) { + throw f.serverError("Cannot get metadata for old runtime version") + } + const client = await this.client(pathInfo, signal) + const metadata = fromPrefixedHex( + await this.clientCall(client, "state_getMetadata", []), + ) + return metadata + }), + ) } async clientCall( - client: RpcClient, + client: Connection, method: string, params: unknown[] = [], ): Promise { diff --git a/providers/frame/FrameProxyProvider.ts b/providers/frame/FrameProxyProvider.ts index 774569434..0caee721f 100644 --- a/providers/frame/FrameProxyProvider.ts +++ b/providers/frame/FrameProxyProvider.ts @@ -1,6 +1,6 @@ import { File } from "../../codegen/frame/mod.ts" import { deferred } from "../../deps/std/async.ts" -import { RpcClient, WsRpcConn } from "../../rpc/mod.ts" +import { WsConnection } from "../../rpc/mod.ts" import { PathInfo } from "../../server/mod.ts" import { fromPathInfo } from "../../server/PathInfo.ts" import { FrameProvider } from "./FrameProvider.ts" @@ -17,6 +17,7 @@ export abstract class FrameProxyProvider extends FrameProvider { } async proxyWs(request: Request, pathInfo: PathInfo) { + console.log("new connection") const url = await this.dynamicUrl(pathInfo) const server = new WebSocket(url) const { socket: client, response } = Deno.upgradeWebSocket(request) @@ -59,8 +60,8 @@ export abstract class FrameProxyProvider extends FrameProvider { ).toString() } - async client(pathInfo: PathInfo) { - return new RpcClient(WsRpcConn, await this.dynamicUrl(pathInfo)) + async client(pathInfo: PathInfo, signal: AbortSignal) { + return WsConnection.connect(await this.dynamicUrl(pathInfo), signal) } async clientFile(pathInfo: PathInfo) { diff --git a/rpc/RpcClient.ts b/rpc/Connection.ts similarity index 70% rename from rpc/RpcClient.ts rename to rpc/Connection.ts index 7c9cb4370..97fa9baca 100644 --- a/rpc/RpcClient.ts +++ b/rpc/Connection.ts @@ -1,7 +1,5 @@ import { Deferred, deferred } from "../deps/std/async.ts" import { getOrInit } from "../util/mod.ts" -import { RpcConnCtor } from "./conn/mod.ts" -import { ConnsRefCounter } from "./ConnsRefCounter.ts" import { RpcErrorMessage, RpcErrorMessageData, @@ -12,33 +10,62 @@ import { RpcOkMessage, } from "./rpc_common.ts" -const connRefCounters = new WeakMap, ConnsRefCounter>() +const connectionMemos = new Map Connection, Map>() -export class RpcClient { - conn +export abstract class Connection { + currentId = 0 + references = 0 - constructor(readonly connCtor: RpcConnCtor, readonly discoveryValue: D) { - const connRefCounter = getOrInit( - connRefCounters, - connCtor, - () => new ConnsRefCounter(connCtor), - ) - this.conn = (signal: AbortSignal) => - connRefCounter.ref(discoveryValue, (e) => this.handler(e), signal) + signal + #controller + constructor() { + this.#controller = new AbortController() + this.signal = this.#controller.signal } + static connect( + this: new(discovery: D) => Connection, + discovery: D, + signal: AbortSignal, + ) { + const memo = getOrInit(connectionMemos, this, () => new Map()) + return getOrInit(memo, discovery, () => { + const connection = new this(discovery) + connection.ref(signal) + connection.signal.addEventListener("abort", () => { + memo.delete(connection) + }) + return connection + }) + } + + ref(signal: AbortSignal) { + this.references++ + signal.addEventListener("abort", () => { + if (!--this.references) { + this.#controller.abort() + this.close() + } + }) + } + + abstract ready(): Promise + + abstract send(id: RpcMessageId, method: string, params: unknown): void + + protected abstract close(): void + callResultPendings: Record> = {} async call( method: string, params: unknown[], ) { const controller = new AbortController() - const conn = this.conn(controller.signal) - await conn.ready() - const id = conn.currentId++ + await this.ready() + const id = this.currentId++ const pending = deferred>() this.callResultPendings[id] = pending - conn.send(id, method, params) + this.send(id, method, params) const result = await pending controller.abort() return result @@ -59,25 +86,24 @@ export class RpcClient { signal: AbortSignal, ) { const controller = new AbortController() - const conn = this.conn(controller.signal) ;(async () => { - await conn.ready() - const subscribeId = conn.currentId++ + await this.ready() + const subscribeId = this.currentId++ this.subscriptionInitPendings[subscribeId] = handler as RpcSubscriptionHandler signal.addEventListener("abort", () => { delete this.subscriptionInitPendings[subscribeId] const subscriptionId = this.subscriptionIdByRpcMessageId[subscribeId] if (subscriptionId) { delete this.subscriptionIdByRpcMessageId[subscribeId] - conn.send(conn.currentId++, unsubscribe, [subscriptionId]) + this.send(this.currentId++, unsubscribe, [subscriptionId]) controller.abort() } }) - conn.send(subscribeId, subscribe, params) + this.send(subscribeId, subscribe, params) })() } - handler(message: RpcIngressMessage) { + handle(message: RpcIngressMessage) { if (typeof message.id === "number") { const callResultPending = this.callResultPendings[message.id] if (callResultPending) { diff --git a/rpc/ConnsRefCounter.ts b/rpc/ConnsRefCounter.ts deleted file mode 100644 index fbb1beed8..000000000 --- a/rpc/ConnsRefCounter.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { getOrInit } from "../util/mod.ts" -import { RpcConn, RpcConnCtor } from "./conn/mod.ts" -import { RpcHandler } from "./rpc_common.ts" - -export class ConnsRefCounter { - conns = new Map() - - constructor(readonly connCtor: RpcConnCtor) {} - - ref(discoveryValue: D, handler: RpcHandler, signal: AbortSignal) { - const conn = getOrInit(this.conns, discoveryValue, () => new this.connCtor(discoveryValue)) - const references = conn.handlerReferenceCounts.get(handler) - if (!references) conn.handlerReferenceCounts.set(handler, 1) - else conn.handlerReferenceCounts.set(handler, references + 1) - signal.addEventListener("abort", () => { - const references = conn.handlerReferenceCounts.get(handler)! - conn.handlerReferenceCounts.set(handler, references - 1) - if (references === 1) { - conn.handlerReferenceCounts.delete(handler) - if (!conn.handlerReferenceCounts.size) { - this.conns.delete(discoveryValue) - conn.close() - } - } - }) - return conn - } -} diff --git a/rpc/conn/base.ts b/rpc/conn/base.ts deleted file mode 100644 index 46557aa3d..000000000 --- a/rpc/conn/base.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { RpcHandler, RpcIngressMessage, RpcMessageId } from "../rpc_common.ts" - -export type RpcConnCtor = new(discoveryValue: D) => RpcConn - -export abstract class RpcConn { - currentId = 0 - handlerReferenceCounts = new Map() - - abstract ready(): Promise - - abstract send(id: RpcMessageId, method: string, params: unknown): void - - abstract close(): void - - push(message: RpcIngressMessage) { - for (const handler of this.handlerReferenceCounts.keys()) handler(message) - } -} diff --git a/rpc/conn/mod.ts b/rpc/conn/mod.ts deleted file mode 100644 index 85bd2b2df..000000000 --- a/rpc/conn/mod.ts +++ /dev/null @@ -1,5 +0,0 @@ -// moderate - -export * from "./base.ts" -export * from "./smoldot.ts" -export * from "./ws.ts" diff --git a/rpc/mod.ts b/rpc/mod.ts index e14dce283..863c047ef 100644 --- a/rpc/mod.ts +++ b/rpc/mod.ts @@ -2,7 +2,7 @@ export * as known from "./known/mod.ts" // moderate --exclude known -export * from "./conn/mod.ts" -export * from "./ConnsRefCounter.ts" +export * from "./Connection.ts" export * from "./rpc_common.ts" -export * from "./RpcClient.ts" +export * from "./smoldot.ts" +export * from "./ws.ts" diff --git a/rpc/conn/smoldot.ts b/rpc/smoldot.ts similarity index 81% rename from rpc/conn/smoldot.ts rename to rpc/smoldot.ts index c4eb1010f..a77526042 100644 --- a/rpc/conn/smoldot.ts +++ b/rpc/smoldot.ts @@ -1,8 +1,8 @@ -import { start } from "../../deps/smoldot.ts" -import { Client, ClientOptions } from "../../deps/smoldot/client.d.ts" -import { deferred } from "../../deps/std/async.ts" -import { RpcEgressMessage, RpcMessageId } from "../rpc_common.ts" -import { RpcConn } from "./base.ts" +import { start } from "../deps/smoldot.ts" +import { Client, ClientOptions } from "../deps/smoldot/client.d.ts" +import { deferred } from "../deps/std/async.ts" +import { Connection } from "./Connection.ts" +import { RpcEgressMessage, RpcMessageId } from "./rpc_common.ts" // TODO: fix the many possible race conditions @@ -13,7 +13,7 @@ export interface SmoldotRpcConnProps { let client: undefined | Client -export class SmoldotRpcConn extends RpcConn { +export class SmoldotConnection extends Connection { chainPending listening stopListening @@ -54,7 +54,7 @@ export class SmoldotRpcConn extends RpcConn { chain.nextJsonRpcResponse(), ]) if (!response) break - this.push(JSON.parse(response)) + this.handle(JSON.parse(response)) } catch (_e) {} } } diff --git a/rpc/conn/ws.ts b/rpc/ws.ts similarity index 87% rename from rpc/conn/ws.ts rename to rpc/ws.ts index 7202ca91c..ebca2e376 100644 --- a/rpc/conn/ws.ts +++ b/rpc/ws.ts @@ -1,13 +1,13 @@ -import { RpcClientError, RpcEgressMessage, RpcMessageId } from "../rpc_common.ts" -import { RpcConn } from "./base.ts" +import { Connection } from "./Connection.ts" +import { RpcClientError, RpcEgressMessage, RpcMessageId } from "./rpc_common.ts" -export class WsRpcConn extends RpcConn { +export class WsConnection extends Connection { chain constructor(readonly url: string) { super() this.chain = new WebSocket(url) - this.chain.addEventListener("message", (e) => this.push(JSON.parse(e.data))) + this.chain.addEventListener("message", (e) => this.handle(JSON.parse(e.data))) this.chain.addEventListener("error", (e) => { // TODO: recovery console.log(e) Deno.exit(1) diff --git a/util/withSignal.ts b/util/withSignal.ts new file mode 100644 index 000000000..0b54dfd70 --- /dev/null +++ b/util/withSignal.ts @@ -0,0 +1,8 @@ +export async function withSignal(cb: (signal: AbortSignal) => Promise) { + const controller = new AbortController() + try { + return await cb(controller.signal) + } finally { + controller.abort() + } +} From a3df2f9cf5e256569c6e439a1dff7651a620f0d0 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Mon, 20 Feb 2023 12:00:45 -0500 Subject: [PATCH 15/23] t6 cleanup --- fluent/AddressRune.ts | 13 +--- fluent/BlockRune.ts | 12 ++-- fluent/ChainRune.ts | 70 +++++++++++++++++++++ fluent/ClientRune.ts | 63 ------------------- fluent/ConnectionRune.ts | 79 +++++++++++++++++++++++ fluent/ExtrinsicRune.ts | 44 +++++++------ fluent/ExtrinsicStatusRune.ts | 8 +-- fluent/MetadataRune.ts | 4 +- fluent/PublicKeyRune.ts | 8 +-- fluent/SignedExtrinsicRune.ts | 13 ++-- fluent/StorageRune.ts | 9 ++- fluent/mod.ts | 5 +- fluent/rpc_method_runes.ts | 64 ------------------- fluent/rpc_runes.ts | 86 -------------------------- patterns/MultisigRune.ts | 8 +-- patterns/consensus/babeBlockAuthor.ts | 6 +- patterns/consensus/preRuntimeDigest.ts | 6 +- patterns/ink/InkMetadataRune.ts | 10 +-- patterns/ink/InkRune.ts | 4 +- rpc/known/author.ts | 26 ++++---- rpc/known/babe.ts | 6 +- rpc/known/beefy.ts | 15 ++--- rpc/known/chain.ts | 37 +++++------ rpc/known/childstate.ts | 18 +++--- rpc/known/contracts.ts | 12 ++-- rpc/known/framesystem.ts | 12 ++-- rpc/known/grandpa.ts | 27 ++++---- rpc/known/mmr.ts | 8 +-- rpc/known/mod.ts | 40 ++++++++++++ rpc/known/offchain.ts | 8 +-- rpc/known/payment.ts | 2 +- rpc/known/smoldot.ts | 40 ++++++------ rpc/known/state.ts | 60 ++++++++---------- rpc/known/statemigration.ts | 6 +- rpc/known/sync.ts | 6 +- rpc/known/system.ts | 38 ++++++------ rpc/known/utils.ts | 7 ++- 37 files changed, 424 insertions(+), 456 deletions(-) create mode 100644 fluent/ChainRune.ts delete mode 100644 fluent/ClientRune.ts create mode 100644 fluent/ConnectionRune.ts delete mode 100644 fluent/rpc_method_runes.ts delete mode 100644 fluent/rpc_runes.ts diff --git a/fluent/AddressRune.ts b/fluent/AddressRune.ts index 77f13c535..1629afcbe 100644 --- a/fluent/AddressRune.ts +++ b/fluent/AddressRune.ts @@ -1,21 +1,14 @@ -import { Rune, RunicArgs } from "../rune/mod.ts" +import { Rune } from "../rune/mod.ts" import { ValueRune } from "../rune/ValueRune.ts" import { ss58 } from "../util/mod.ts" -import { Chain, ClientRune } from "./ClientRune.ts" +import { Chain, ChainRune } from "./ChainRune.ts" import { PublicKeyRune } from "./PublicKeyRune.ts" export class AddressRune extends Rune { - constructor(_prime: AddressRune["_prime"], readonly client: ClientRune) { + constructor(_prime: AddressRune["_prime"], readonly chain: ChainRune) { super(_prime) } - static from( - client: ClientRune, - ...[address]: RunicArgs - ) { - return Rune.resolve(address).into(AddressRune, Rune.resolve(client).into(ClientRune)) - } - publicKey() { return this .into(ValueRune) diff --git a/fluent/BlockRune.ts b/fluent/BlockRune.ts index 9956e320c..06355ef8a 100644 --- a/fluent/BlockRune.ts +++ b/fluent/BlockRune.ts @@ -3,13 +3,13 @@ import { known } from "../rpc/mod.ts" import { ArrayRune, Rune } from "../rune/mod.ts" import { ValueRune } from "../rune/ValueRune.ts" import { hex, HexHash } from "../util/mod.ts" -import { Chain, ClientRune } from "./ClientRune.ts" +import { Chain, ChainRune } from "./ChainRune.ts" import { CodecRune } from "./CodecRune.ts" export class BlockRune extends Rune { constructor( _prime: BlockRune["_prime"], - readonly client: ClientRune, + readonly chain: ChainRune, readonly hash: Rune, ) { super(_prime) @@ -24,7 +24,7 @@ export class BlockRune extends Rune extends Rune() + .unsafeAs[] | undefined>() .into(ValueRune) .unhandle(undefined) - .rehandle(undefined, () => Rune.constant([])) + .rehandle(undefined, () => Rune.constant[]>([])) } } diff --git a/fluent/ChainRune.ts b/fluent/ChainRune.ts new file mode 100644 index 000000000..be70b2fda --- /dev/null +++ b/fluent/ChainRune.ts @@ -0,0 +1,70 @@ +import * as $ from "../deps/scale.ts" +import * as M from "../frame_metadata/mod.ts" +import { Event } from "../primitives/mod.ts" +import { Connection } from "../rpc/mod.ts" +import { Rune, RunicArgs, ValueRune } from "../rune/mod.ts" +import { HexHash } from "../util/mod.ts" +import { BlockRune } from "./BlockRune.ts" +import { ConnectionRune } from "./ConnectionRune.ts" +import { ExtrinsicRune } from "./ExtrinsicRune.ts" +import { MetadataRune } from "./MetadataRune.ts" + +export interface Chain { + connection: Connection + _call?: $.Codec + _event?: $.Codec +} + +export namespace Chain { + export type Call = C extends Chain ? Call : never + export type Event = C extends Chain ? Event : never +} + +// TODO: do we want to represent the discovery value and conn type within the type system? +export class ChainRune extends Rune { + connection = this.into(ValueRune).access("connection").into(ConnectionRune) + + latestBlock = this.block( + this.connection + .call( + "chain_getBlockHash", + this.connection + .subscribe("chain_subscribeNewHeads", "chain_unsubscribeNewHeads") + .access("number"), + ) + .unsafeAs(), + ) + + block(...[blockHash]: RunicArgs) { + return this.connection.call("chain_getBlock", blockHash) + .unhandle(null) + .into(BlockRune, this, Rune.resolve(blockHash)) + } + + metadata(...[blockHash]: RunicArgs) { + return this.connection.call("state_getMetadata", blockHash) + .map(M.fromPrefixedHex) + .throws($.ScaleError) + .into(MetadataRune, this) + } + + extrinsic(...args: RunicArgs]>) { + const [call] = RunicArgs.resolve(args) + return call.into(ExtrinsicRune, this.as(ChainRune)) + } + + addressPrefix() { + return this + .metadata() + .pallet("System") + .const("SS58Prefix") + .decoded + .unsafeAs() + } + + chainVersion = this.connection.call("system_version") + + private _asCodegen() { + return this as any as ChainRune + } +} diff --git a/fluent/ClientRune.ts b/fluent/ClientRune.ts deleted file mode 100644 index de28bd6f1..000000000 --- a/fluent/ClientRune.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as $ from "../deps/scale.ts" -import * as M from "../frame_metadata/mod.ts" -import { Event } from "../primitives/mod.ts" -import { RpcClient } from "../rpc/mod.ts" -import { MetaRune, Rune, RunicArgs, ValueRune } from "../rune/mod.ts" -import { HexHash } from "../util/mod.ts" -import { BlockRune } from "./BlockRune.ts" -import { ExtrinsicRune } from "./ExtrinsicRune.ts" -import { MetadataRune } from "./MetadataRune.ts" -import { chain, state } from "./rpc_method_runes.ts" -import { rpcCall } from "./rpc_runes.ts" - -export interface Chain { - call: C - event: E -} - -// TODO: do we want to represent the discovery value and conn type within the type system? -export class ClientRune extends Rune, U> { - latestBlock = this.block(chain.getBlockHash( - this, - chain - .subscribeNewHeads(this.as(ClientRune)) - .map((header) => Rune.constant(header)) - .into(MetaRune) - .flatMap((headers) => headers.into(ValueRune)) - .access("number"), - )) - - block(...[blockHash]: RunicArgs) { - return chain - .getBlock(this.as(Rune), blockHash) - .into(BlockRune, this, Rune.resolve(blockHash)) - } - - metadata(...[blockHash]: RunicArgs) { - return state - .getMetadata(this.as(Rune), blockHash) - .map(M.fromPrefixedHex) - .throws($.ScaleError) - .into(MetadataRune, this) - } - - extrinsic(...args: RunicArgs) { - const [call] = RunicArgs.resolve(args) - return call.into(ExtrinsicRune, this.as(ClientRune)) - } - - addressPrefix() { - return this - .metadata() - .pallet("System") - .const("SS58Prefix") - .decoded - .unsafeAs() - } - - chainVersion = rpcCall<[], string>("system_version")(this.as(Rune)) - - private _asCodegen() { - return this as any as ClientRune - } -} diff --git a/fluent/ConnectionRune.ts b/fluent/ConnectionRune.ts new file mode 100644 index 000000000..0a4ad9445 --- /dev/null +++ b/fluent/ConnectionRune.ts @@ -0,0 +1,79 @@ +import { Calls, Subscription, Subscriptions } from "../rpc/known/mod.ts" +import { Connection, RpcClientError, RpcServerError, RpcSubscriptionMessage } from "../rpc/mod.ts" +import { Batch, MetaRune, Run, Rune, RunicArgs, RunStream } from "../rune/mod.ts" + +class RunConnection extends Run { + constructor(ctx: Batch, readonly initConnection: (signal: AbortSignal) => Connection) { + super(ctx) + } + + connection?: Connection + async _evaluate(): Promise { + return this.connection ??= this.initConnection(this.signal) + } +} + +export function connection(init: (signal: AbortSignal) => Connection) { + return Rune.new(RunConnection, init).into(ConnectionRune) +} + +export class ConnectionRune extends Rune { + call( + callMethod: K, + ...args: RunicArgs]> + ) { + return Rune + .tuple([this.as(Rune), ...args]) + .map(async ([client, ...params]) => { + const result = await client.call>(callMethod, params) + if (result.error) throw new RpcServerError(result) + return result.result + }) + .throws(RpcClientError, RpcServerError) + } + + subscribe( + subscribeMethod: K, + unsubscribeMethod: Subscription.UnsubscribeMethod>, + ...args: RunicArgs]> + ) { + return Rune.tuple([this, ...args]) + .map(([client, ...params]) => + Rune.new( + RunRpcSubscription>>, + client, + params, + subscribeMethod, + unsubscribeMethod, + ) + ) + .into(MetaRune) + .flat() + .map((event) => { + if (event.error) throw new RpcServerError(event) + return event.params.result + }) + .throws(RpcClientError, RpcServerError) + } +} + +class RunRpcSubscription + extends RunStream> +{ + constructor( + ctx: Batch, + connection: Connection, + params: unknown[], + subscribeMethod: string, + unsubscribeMethod: string, + ) { + super(ctx) + connection.subscription( + subscribeMethod, + unsubscribeMethod, + params, + (value) => this.push(value), + this.signal, + ) + } +} diff --git a/fluent/ExtrinsicRune.ts b/fluent/ExtrinsicRune.ts index 396047919..5a33bd1e2 100644 --- a/fluent/ExtrinsicRune.ts +++ b/fluent/ExtrinsicRune.ts @@ -4,9 +4,9 @@ import { Rune, RunicArgs, ValueRune } from "../rune/mod.ts" import { Era, era } from "../scale_info/mod.ts" import { Blake2_256 } from "../util/hashers.ts" import * as U from "../util/mod.ts" -import { Chain, ClientRune } from "./ClientRune.ts" +import { HexHash } from "../util/mod.ts" +import { Chain, ChainRune } from "./ChainRune.ts" import { CodecRune } from "./CodecRune.ts" -import { chain, payment, system } from "./rpc_method_runes.ts" import { SignedExtrinsicRune } from "./SignedExtrinsicRune.ts" export interface ExtrinsicSender { @@ -22,12 +22,12 @@ export interface SignedExtrinsicProps { tip?: bigint } -export class ExtrinsicRune extends Rune { +export class ExtrinsicRune extends Rune, U> { hash - constructor(_prime: ExtrinsicRune["_prime"], readonly client: ClientRune) { + constructor(_prime: ExtrinsicRune["_prime"], readonly chain: ChainRune) { super(_prime) - const metadata = this.client.metadata() + const metadata = this.chain.metadata() this.hash = Rune.rec({ metadata, deriveCodec: metadata.deriveCodec }) .map((x) => Blake2_256.$hash(M.$call(x))) .into(CodecRune) @@ -36,7 +36,7 @@ export class ExtrinsicRune extends Rune(_props: RunicArgs) { const props = RunicArgs.resolve(_props) - const metadata = this.client.metadata() + const metadata = this.chain.metadata() const System = metadata.pallet("System") const addrPrefix = System.const("SS58Prefix").decoded.unsafeAs() const $extrinsic = Rune.rec({ @@ -52,19 +52,23 @@ export class ExtrinsicRune extends Rune { - switch (sender.address.type) { - case "Id": { - return U.ss58.encode(addrPrefix, sender.address.value) + const senderSs58 = Rune + .tuple([addrPrefix, props.sender]) + .map(([addrPrefix, sender]) => { + switch (sender.address.type) { + case "Id": { + return U.ss58.encode(addrPrefix, sender.address.value) + } + default: { + throw new Error("unimplemented") + } } - default: { - throw new Error("unimplemented") - } - } - }).throws(U.ss58.InvalidPublicKeyLengthError, U.ss58.InvalidNetworkPrefixError) + }) + .throws(U.ss58.InvalidPublicKeyLengthError, U.ss58.InvalidNetworkPrefixError) // TODO: handle props.nonce resolving to undefined - const nonce = props.nonce ?? system.accountNextIndex(this.client, senderSs58) - const genesisHashHex = chain.getBlockHash(this.client, 0) + const nonce = props.nonce ?? this.chain.connection.call("system_accountNextIndex", senderSs58) + const genesisHashHex = this.chain.connection.call("chain_getBlockHash", 0).unsafeAs() + .into(ValueRune) const genesisHash = genesisHashHex.map(U.hex.decode) const checkpointHash = Rune.tuple([props.checkpoint, genesisHashHex]).map(([a, b]) => a ?? b) .map(U.hex.decode) @@ -83,11 +87,11 @@ export class ExtrinsicRune extends Rune extends Rune ({ ...rest, weight: { diff --git a/fluent/ExtrinsicStatusRune.ts b/fluent/ExtrinsicStatusRune.ts index c920c95f9..afa26d650 100644 --- a/fluent/ExtrinsicStatusRune.ts +++ b/fluent/ExtrinsicStatusRune.ts @@ -2,7 +2,7 @@ import { known } from "../rpc/mod.ts" import { MetaRune, Rune, RunicArgs, ValueRune } from "../rune/mod.ts" import { Hex } from "../util/mod.ts" import { BlockRune } from "./BlockRune.ts" -import { Chain } from "./ClientRune.ts" +import { Chain } from "./ChainRune.ts" import { SignedExtrinsicRune } from "./SignedExtrinsicRune.ts" export class ExtrinsicStatusRune @@ -37,9 +37,9 @@ export class ExtrinsicStatusRune ) .singular() .unhandle(NeverInBlockError) - return this.extrinsic.client + return this.extrinsic.chain .block(hash) - .into(BlockRune, this.extrinsic.client, hash) + .into(BlockRune, this.extrinsic.chain, hash) } finalized() { @@ -51,7 +51,7 @@ export class ExtrinsicStatusRune ) .singular() .unhandle(NeverFinalizedError) - return this.extrinsic.client.block(hash) + return this.extrinsic.chain.block(hash) } txEvents() { diff --git a/fluent/MetadataRune.ts b/fluent/MetadataRune.ts index fef72bacc..6e5512c2e 100644 --- a/fluent/MetadataRune.ts +++ b/fluent/MetadataRune.ts @@ -1,12 +1,12 @@ import * as M from "../frame_metadata/mod.ts" import { Rune, RunicArgs, ValueRune } from "../rune/mod.ts" import { DeriveCodec, Ty } from "../scale_info/mod.ts" -import { Chain, ClientRune } from "./ClientRune.ts" +import { Chain, ChainRune } from "./ChainRune.ts" import { CodecRune } from "./CodecRune.ts" import { PalletRune } from "./PalletRune.ts" export class MetadataRune extends Rune { - constructor(_prime: MetadataRune["_prime"], readonly client: ClientRune) { + constructor(_prime: MetadataRune["_prime"], readonly chain: ChainRune) { super(_prime) } diff --git a/fluent/PublicKeyRune.ts b/fluent/PublicKeyRune.ts index 6892dcd68..0ccd91327 100644 --- a/fluent/PublicKeyRune.ts +++ b/fluent/PublicKeyRune.ts @@ -1,17 +1,17 @@ import { Rune, ValueRune } from "../rune/mod.ts" import { ss58 } from "../util/mod.ts" import { AddressRune } from "./AddressRune.ts" -import { Chain, ClientRune } from "./ClientRune.ts" +import { Chain, ChainRune } from "./ChainRune.ts" export class PublicKeyRune extends Rune { constructor(_prime: PublicKeyRune["_prime"]) { super(_prime) } - address(client: ClientRune) { + address(chain: ChainRune) { return Rune - .tuple([client.addressPrefix(), this.into(ValueRune)]) + .tuple([chain.addressPrefix(), this.into(ValueRune)]) .map(([prefix, publicKey]) => ss58.encode(prefix, publicKey)) - .into(AddressRune, client) + .into(AddressRune, chain) } } diff --git a/fluent/SignedExtrinsicRune.ts b/fluent/SignedExtrinsicRune.ts index fc979c4bf..0398ea138 100644 --- a/fluent/SignedExtrinsicRune.ts +++ b/fluent/SignedExtrinsicRune.ts @@ -1,11 +1,10 @@ import { Rune, ValueRune } from "../rune/mod.ts" import { hex } from "../util/mod.ts" -import { Chain, ClientRune } from "./ClientRune.ts" +import { Chain, ChainRune } from "./ChainRune.ts" import { ExtrinsicStatusRune } from "./ExtrinsicStatusRune.ts" -import { author } from "./rpc_method_runes.ts" export class SignedExtrinsicRune extends Rune { - constructor(_prime: SignedExtrinsicRune["_prime"], readonly client: ClientRune) { + constructor(_prime: SignedExtrinsicRune["_prime"], readonly chain: ChainRune) { super(_prime) } @@ -16,7 +15,13 @@ export class SignedExtrinsicRune extends Run sent() { return this .hex() - .map((hex) => author.submitAndWatchExtrinsic(this.client as ClientRune, hex)) + .map((hex) => + this.chain.connection.subscribe( + "author_submitAndWatchExtrinsic", + "author_unwatchExtrinsic", + hex, + ) + ) .into(ExtrinsicStatusRune, this) } } diff --git a/fluent/StorageRune.ts b/fluent/StorageRune.ts index 18e59bba5..fc1ad1b75 100644 --- a/fluent/StorageRune.ts +++ b/fluent/StorageRune.ts @@ -4,7 +4,6 @@ import { Rune, RunicArgs, ValueRune } from "../rune/mod.ts" import * as U from "../util/mod.ts" import { CodecRune } from "./CodecRune.ts" import { PalletRune } from "./PalletRune.ts" -import { state } from "./rpc_method_runes.ts" export class StorageRune extends Rune { $key @@ -22,8 +21,8 @@ export class StorageRune extends Rune< entryRaw(...[key, blockHash]: RunicArgs) { const storageKey = this.$key.encoded(key).map(U.hex.encode) - return state - .getStorage(this.pallet.metadata.client, storageKey, blockHash) + return this.pallet.metadata.chain.connection + .call("state_getStorage", storageKey, blockHash) .unhandle(null) .rehandle(null, () => Rune.constant(undefined)) } @@ -55,8 +54,8 @@ export class StorageRune extends Rune< .map(U.hex.encode) .rehandle(undefined), ) - const keysEncoded = state.getKeysPaged( - this.pallet.metadata.client, + const keysEncoded = this.pallet.metadata.chain.connection.call( + "state_getKeysPaged", storageKey, count, startKey, diff --git a/fluent/mod.ts b/fluent/mod.ts index 9420d0734..ddfea5819 100644 --- a/fluent/mod.ts +++ b/fluent/mod.ts @@ -2,15 +2,14 @@ export * from "./AddressRune.ts" export * from "./BlockRune.ts" -export * from "./ClientRune.ts" +export * from "./ChainRune.ts" export * from "./CodecRune.ts" +export * from "./ConnectionRune.ts" export * from "./ConstRune.ts" export * from "./ExtrinsicRune.ts" export * from "./ExtrinsicStatusRune.ts" export * from "./MetadataRune.ts" export * from "./PalletRune.ts" export * from "./PublicKeyRune.ts" -export * from "./rpc_method_runes.ts" -export * from "./rpc_runes.ts" export * from "./SignedExtrinsicRune.ts" export * from "./StorageRune.ts" diff --git a/fluent/rpc_method_runes.ts b/fluent/rpc_method_runes.ts deleted file mode 100644 index 75bb17e25..000000000 --- a/fluent/rpc_method_runes.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { known } from "../rpc/mod.ts" -import { HexHash } from "../util/branded.ts" -import * as U from "../util/mod.ts" -import { rpcCall, rpcSubscription } from "./rpc_runes.ts" - -// TODO: generate the following? -export namespace state { - export const getMetadata = rpcCall<[at?: U.HexHash], U.HexHash>("state_getMetadata") - export const call = rpcCall<[method: string, data: U.Hex], U.HexHash>("state_call") - export const getStorage = rpcCall< - [key: known.StorageKey, at?: U.HexHash], - known.StorageData | null - >("state_getStorage") - export const subscribeStorage = rpcSubscription< - [keys: known.StorageKey[]], - known.StorageChangeSet - >()( - "state_subscribeStorage", - "state_unsubscribeStorage", - ) - export const getKeysPaged = rpcCall< - [prefix: known.StorageKey, count: number, startKey?: known.StorageKey, at?: U.HexHash], - known.StorageKey[] - >("state_getKeysPaged") -} -export namespace chain { - export const subscribeNewHeads = rpcSubscription<[], known.Header>()( - "chain_subscribeNewHeads", - "chain_unsubscribeNewHeads", - ) - export const getBlock = rpcCall<[hash?: U.HexHash], known.SignedBlock>("chain_getBlock") - export const getBlockHash = rpcCall<[height?: known.ListOrValue], U.HexHash>( - "chain_getBlockHash", - ) - export const getHeader = rpcCall<[hash?: U.HexHash], known.Header>("chain_getHeader") - export const getFinalizedHead = rpcCall<[], HexHash>("chain_getFinalizedHead") -} -export namespace system { - export const accountNextIndex = rpcCall<[accountId: known.AccountId], number>( - "system_accountNextIndex", - ) - export const version = rpcCall<[hash?: U.HexHash], string>("system_version") -} -export namespace author { - export const submitAndWatchExtrinsic = rpcSubscription< - [extrinsic: U.Hex], - known.TransactionStatus - >()( - "author_submitAndWatchExtrinsic", - "author_unwatchExtrinsic", - ) - export const submitExtrinsic = rpcCall<[extrinsic: U.Hex], U.Hash>("author_submitExtrinsic") -} -export const methods = rpcCall<[], string[]>("rpc_methods") -export namespace payment { - export const queryInfo = rpcCall<[extrinsic: U.Hex, at?: U.HexHash], known.RuntimeDispatchInfo>( - "payment_queryInfo", - ) -} -export namespace sync { - export namespace state { - export const genSyncSpec = rpcCall<[boolean], known.ChainSpec>("sync_state_genSyncSpec") - } -} diff --git a/fluent/rpc_runes.ts b/fluent/rpc_runes.ts deleted file mode 100644 index b313c4cd2..000000000 --- a/fluent/rpc_runes.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { - RpcClient, - RpcClientError, - RpcConnCtor, - RpcServerError, - RpcSubscriptionMessage, -} from "../rpc/mod.ts" -import { Batch, MetaRune, Run, Rune, RunicArgs, RunStream } from "../rune/mod.ts" -import { ClientRune } from "./ClientRune.ts" - -class RunRpcClient extends Run, never> { - constructor( - ctx: Batch, - readonly connCtor: RpcConnCtor, - readonly discoveryValue: D, - ) { - super(ctx) - } - - client?: RpcClient - async _evaluate(): Promise> { - return this.client ??= new RpcClient(this.connCtor, this.discoveryValue) - } -} - -export function rpcClient(connCtor: RpcConnCtor, discoveryValue: D) { - return Rune.new(RunRpcClient, connCtor, discoveryValue).into(ClientRune) -} - -export function rpcCall(method: string) { - return (...args: RunicArgs, ...params: Params]>) => { - return Rune - .tuple(args) - .map(async ([client, ...params]) => { - const result = await client.call(method, params) - if (result.error) throw new RpcServerError(result) - return result.result - }) - .throws(RpcClientError, RpcServerError) - } -} - -class RunRpcSubscription - extends RunStream> -{ - constructor( - ctx: Batch, - client: RpcClient, - params: unknown[], - subscribeMethod: string, - unsubscribeMethod: string, - ) { - super(ctx) - client.subscription( - subscribeMethod, - unsubscribeMethod, - params, - (value) => this.push(value), - this.signal, - ) - } -} - -export function rpcSubscription() { - return (subscribeMethod: Method, unsubscribeMethod: string) => { - return (...args: RunicArgs, ...params: Params]>) => { - return Rune.tuple(args) - .map(([client, ...params]) => - Rune.new( - RunRpcSubscription, - client, - params, - subscribeMethod, - unsubscribeMethod, - ) - ) - .into(MetaRune) - .flat() - .map((event) => { - if (event.error) throw new RpcServerError(event) - return event.params.result - }) - .throws(RpcClientError, RpcServerError) - } - } -} diff --git a/patterns/MultisigRune.ts b/patterns/MultisigRune.ts index bffc1f916..3cd15cb03 100644 --- a/patterns/MultisigRune.ts +++ b/patterns/MultisigRune.ts @@ -1,5 +1,5 @@ import * as bytes from "../deps/std/bytes.ts" -import { Chain, ClientRune } from "../fluent/ClientRune.ts" +import { Chain, ChainRune } from "../fluent/ChainRune.ts" import { MultiAddress } from "../primitives/mod.ts" import { Rune, RunicArgs, ValueRune } from "../rune/mod.ts" import { multisigAccountId } from "./multisigAccountId.ts" @@ -28,14 +28,14 @@ export class MultisigRune extends Rune["_prime"], readonly client: ClientRune) { + constructor(_prime: MultisigRune["_prime"], readonly chain: ChainRune) { super(_prime) this.pallet = this.client.metadata().pallet("Multisig") this.storage = this.pallet.storage("Multisigs") } static from( - client: ClientRune, + chain: ChainRune, ...[multisig]: RunicArgs ) { return Rune.resolve(multisig).into(MultisigRune, client) @@ -102,7 +102,7 @@ export class MultisigRune extends Rune - multisig.into(MultisigRune, client.into(ClientRune)) + multisig.into(MultisigRune, client.into(ChainRune)) .proposal(callHash) .unsafeAs<{ when: unknown }>() .into(ValueRune) diff --git a/patterns/consensus/babeBlockAuthor.ts b/patterns/consensus/babeBlockAuthor.ts index 2a47c5601..8b074577b 100644 --- a/patterns/consensus/babeBlockAuthor.ts +++ b/patterns/consensus/babeBlockAuthor.ts @@ -1,16 +1,16 @@ import { $preDigest } from "polkadot_dev/types/sp_consensus_babe/digests.ts" -import { Chain, ClientRune } from "../../fluent/mod.ts" +import { Chain, ChainRune } from "../../fluent/mod.ts" import { PublicKeyRune } from "../../fluent/mod.ts" import { Rune, RunicArgs, ValueRune } from "../../rune/mod.ts" import { HexHash } from "../../util/branded.ts" import { preRuntimeDigest } from "./preRuntimeDigest.ts" export function babeBlockAuthor( - client: ClientRune, + chain: ChainRune, ...[blockHash]: RunicArgs ) { const validators = client - .into(ClientRune) + .into(ChainRune) .metadata() .pallet("Session") .storage("Validators") diff --git a/patterns/consensus/preRuntimeDigest.ts b/patterns/consensus/preRuntimeDigest.ts index 0c00c74a9..21ad7f680 100644 --- a/patterns/consensus/preRuntimeDigest.ts +++ b/patterns/consensus/preRuntimeDigest.ts @@ -1,15 +1,15 @@ import { $digestItem, DigestItem } from "polkadot_dev/types/sp_runtime/generic/digest.ts" -import { Chain, ClientRune } from "../../fluent/mod.ts" +import { Chain, ChainRune } from "../../fluent/mod.ts" import { RunicArgs, ValueRune } from "../../rune/mod.ts" import { HexHash } from "../../util/branded.ts" import { hex } from "../../util/mod.ts" export function preRuntimeDigest( - client: ClientRune, + chain: ChainRune, ...[blockHash]: RunicArgs ) { return client - .into(ClientRune) + .into(ChainRune) .block(blockHash) .header() .into(ValueRune) diff --git a/patterns/ink/InkMetadataRune.ts b/patterns/ink/InkMetadataRune.ts index ec37ab371..84e709a8c 100644 --- a/patterns/ink/InkMetadataRune.ts +++ b/patterns/ink/InkMetadataRune.ts @@ -1,5 +1,5 @@ import * as $ from "../../deps/scale.ts" -import { Chain, ClientRune, CodecRune, ExtrinsicRune, state } from "../../fluent/mod.ts" +import { Chain, ChainRune, CodecRune, ExtrinsicRune, state } from "../../fluent/mod.ts" import { ArrayRune, Rune, RunicArgs, ValueRune } from "../../rune/mod.ts" import { DeriveCodec } from "../../scale_info/mod.ts" import { hex } from "../../util/mod.ts" @@ -22,7 +22,7 @@ export interface InstantiateProps { export class InkMetadataRune extends Rune { deriveCodec - constructor(_prime: InkMetadataRune["_prime"], readonly client: ClientRune) { + constructor(_prime: InkMetadataRune["_prime"], readonly chain: ChainRune) { super(_prime) this.deriveCodec = this .into(ValueRune) @@ -31,13 +31,13 @@ export class InkMetadataRune extends Rune( - client: ClientRune, + chain: ChainRune, ...[jsonText]: RunicArgs ) { return Rune .resolve(jsonText) .map((jsonText) => normalize(JSON.parse(jsonText))) - .into(InkMetadataRune, Rune.resolve(client).into(ClientRune)) + .into(InkMetadataRune, Rune.resolve(client).into(ChainRune)) } salt() { @@ -131,7 +131,7 @@ export class InkMetadataRune extends Rune( - client: ClientRune, + chain: ChainRune, ...[publicKey]: RunicArgs ) { return Rune diff --git a/patterns/ink/InkRune.ts b/patterns/ink/InkRune.ts index 7aa9c4231..8ccd1dca7 100644 --- a/patterns/ink/InkRune.ts +++ b/patterns/ink/InkRune.ts @@ -1,5 +1,5 @@ import { equals } from "../../deps/std/bytes.ts" -import { Chain, ClientRune, CodecRune, ExtrinsicRune, state } from "../../fluent/mod.ts" +import { Chain, ChainRune, CodecRune, ExtrinsicRune, state } from "../../fluent/mod.ts" import { Event } from "../../primitives/mod.ts" import { Rune, RunicArgs, ValueRune } from "../../rune/mod.ts" import { hex } from "../../util/mod.ts" @@ -18,7 +18,7 @@ export interface MsgProps { export class InkRune extends Rune { constructor( _prime: InkRune["_prime"], - readonly client: ClientRune, + readonly chain: ChainRune, readonly contract: InkMetadataRune, ) { super(_prime) diff --git a/rpc/known/author.ts b/rpc/known/author.ts index 5483b2e97..b41f52973 100644 --- a/rpc/known/author.ts +++ b/rpc/known/author.ts @@ -1,4 +1,4 @@ -import { Hash, Hex, RpcResult, SerdeEnum, Subscription } from "./utils.ts" +import { Hash, Hex, SerdeEnum, Subscription } from "./utils.ts" // https://github.com/paritytech/substrate/blob/e0ccd00/client/transaction-pool/api/src/lib.rs#L104 /** @@ -102,28 +102,31 @@ export type ExtrinsicOrHash = SerdeEnum<{ }> // https://github.com/paritytech/substrate/blob/e0ccd00/client/rpc-api/src/author/mod.rs#L30 -export type AuthorRpc = { +export type AuthorCalls = { /** Submit hex-encoded extrinsic for inclusion in block. */ - author_submitExtrinsic(extrinsic: Hex): RpcResult + author_submitExtrinsic(extrinsic: Hex): Hash /** Insert a key into the keystore. */ - author_insertKey(keyType: string, suri: string, publicKey: Hex): RpcResult + author_insertKey(keyType: string, suri: string, publicKey: Hex): null /** Generate new session keys and returns the corresponding public keys. */ - author_rotateKeys(): RpcResult + author_rotateKeys(): Hex /** * Checks if the keystore has private keys for the given session public keys. * `sessionKeys` is the SCALE encoded session keys object from the runtime. * Returns `true` iff all private keys could be found. */ - author_hasSessionKeys(sessionsKeys: Hex): RpcResult + author_hasSessionKeys(sessionsKeys: Hex): boolean /** * Checks if the keystore has private keys for the given public key and key type. * Returns `true` if a private key could be found. */ - author_hasKey(pubKey: Hex, keyType: string): RpcResult + author_hasKey(pubKey: Hex, keyType: string): boolean /** Returns all pending extrinsics, potentially grouped by sender. */ - author_pendingExtrinsics(): RpcResult + author_pendingExtrinsics(): Hex[] /** Remove given extrinsic from the pool and temporarily ban it to prevent reimporting. */ - author_removeExtrinsic(extrinsics: ExtrinsicOrHash[]): RpcResult // todo + author_removeExtrinsic(extrinsics: ExtrinsicOrHash[]): Hex[] // todo +} + +export type AuthorSubscriptions = { /** * Submit an extrinsic to watch. * @@ -132,8 +135,5 @@ export type AuthorRpc = { */ author_submitAndWatchExtrinsic( extrinsic: Hex, - ): RpcResult> - author_unwatchExtrinsic( - subscription: Subscription<"author_submitAndWatchExtrinsic", TransactionStatus>, - ): RpcResult + ): Subscription<"author_unwatchExtrinsic", TransactionStatus> } diff --git a/rpc/known/babe.ts b/rpc/known/babe.ts index b148a651d..5a4b975a1 100644 --- a/rpc/known/babe.ts +++ b/rpc/known/babe.ts @@ -1,4 +1,4 @@ -import { AccountId, RpcResult } from "./utils.ts" +import { AccountId } from "./utils.ts" // https://github.com/paritytech/substrate/blob/9b01569/client/consensus/babe/rpc/src/lib.rs#L154 /** Holds information about the `slot`'s that can be claimed by a given key. */ @@ -12,10 +12,10 @@ export interface EpochAuthorship { } // https://github.com/paritytech/substrate/blob/9b01569/client/consensus/babe/rpc/src/lib.rs#L44 -export type BabeRpc = { +export type BabeCalls = { /** * Returns data about which slots (primary or secondary) can be claimed in * the current epoch with the keys in the keystore. */ - babe_epochAuthorship(): RpcResult> + babe_epochAuthorship(): Record } diff --git a/rpc/known/beefy.ts b/rpc/known/beefy.ts index 2f1a418d7..0c13ad399 100644 --- a/rpc/known/beefy.ts +++ b/rpc/known/beefy.ts @@ -1,11 +1,7 @@ -import { Hash, Hex, RpcResult, Subscription } from "./utils.ts" +import { Hash, Hex, Subscription } from "./utils.ts" // https://github.com/paritytech/substrate/blob/317808a/client/beefy/rpc/src/lib.rs#L84 -export type BeefyRpc = { - /** Returns the block most recently finalized by BEEFY, alongside side its justification. */ - beefy_subscribeJustifications(): RpcResult< - Subscription<"beefy_subscribeJustifications", Hex> - > +export type BeefyCalls = { /** * Returns hash of the latest BEEFY finalized block as seen by this client. * @@ -13,5 +9,10 @@ export type BeefyRpc = { * in the network or if the client is still initializing or syncing with the network. * In such case an error would be returned. */ - beefy_getFinalizedHead(): RpcResult + beefy_getFinalizedHead(): Hash +} + +export type BeefySubscriptions = { + /** Returns the block most recently finalized by BEEFY, alongside side its justification. */ + beefy_subscribeJustifications(): Subscription<"beefy_unsubscribeJustifications", Hex> } diff --git a/rpc/known/chain.ts b/rpc/known/chain.ts index 7983cea65..7b5e8e6b4 100644 --- a/rpc/known/chain.ts +++ b/rpc/known/chain.ts @@ -1,4 +1,4 @@ -import { Hash, Hex, ListOrValue, NumberOrHex, RpcResult, Subscription } from "./utils.ts" +import { Hash, Hex, ListOrValue, NumberOrHex, Subscription } from "./utils.ts" // https://github.com/paritytech/substrate/blob/0ba251c/primitives/runtime/src/generic/digest.rs /** Generic header digest. */ @@ -40,36 +40,29 @@ export interface Block { } // https://github.com/paritytech/substrate/blob/934fbfd/client/rpc-api/src/chain/mod.rs#L27 -export type ChainRpc = { +export type ChainCalls = { /** Get header. */ - chain_getHeader(hash?: Hash): RpcResult
+ chain_getHeader(hash?: Hash): Header | null /** Get header and body of a relay chain block. */ - chain_getBlock(hash?: Hash): RpcResult + chain_getBlock(hash?: Hash): SignedBlock | null /** * Get hash of the n-th block in the canon chain. * * By default returns latest block hash. */ - chain_getBlockHash(height?: ListOrValue): RpcResult> - chain_getHead: ChainRpc["chain_getBlockHash"] + chain_getBlockHash(height?: ListOrValue): ListOrValue + chain_getHead: ChainCalls["chain_getBlockHash"] /** Get hash of the last finalized block in the canon chain. */ - chain_getFinalizedHead(): RpcResult - chain_getFinalisedHead: ChainRpc["chain_getFinalizedHead"] + chain_getFinalizedHead(): Hash + chain_getFinalisedHead: ChainCalls["chain_getFinalizedHead"] +} + +export type ChainSubscriptions = { /** All head subscription. */ - chain_subscribeAllHeads(): RpcResult> - chain_unsubscribeAllHeads( - subscription: Subscription<"chain_subscribeAllHeads", Header>, - ): RpcResult + chain_subscribeAllHeads(): Subscription<"chain_unsubscribeAllHeads", Header> /** New head subscription. */ - chain_subscribeNewHeads(): RpcResult> - chain_unsubscribeNewHeads( - subscription: Subscription<"chain_subscribeAllHeads", Header>, - ): RpcResult + chain_subscribeNewHeads(): Subscription<"chain_unsubscribeNewHeads", Header> /** Finalized head subscription. */ - chain_subscribeFinalizedHeads(): RpcResult> - chain_unsubscribeFinalizedHeads( - subscription: Subscription<"chain_subscribeAllHeads", Header>, - ): RpcResult - chain_subscribeFinalisedHeads: ChainRpc["chain_subscribeFinalizedHeads"] - chain_unsubscribeFinalisedHeads: ChainRpc["chain_unsubscribeFinalizedHeads"] + chain_subscribeFinalizedHeads(): Subscription<"chain_unsubscribeAllHeads", Header> + chain_subscribeFinalisedHeads: ChainSubscriptions["chain_subscribeFinalizedHeads"] } diff --git a/rpc/known/childstate.ts b/rpc/known/childstate.ts index 13e6ddc5a..043d99e2a 100644 --- a/rpc/known/childstate.ts +++ b/rpc/known/childstate.ts @@ -1,4 +1,4 @@ -import { Hash, Hex, RpcResult } from "./utils.ts" +import { Hash, Hex } from "./utils.ts" // https://github.com/paritytech/substrate/blob/4d04aba/primitives/storage/src/lib.rs export type StorageKey = Hex @@ -14,7 +14,7 @@ export interface ReadProof { } // https://github.com/paritytech/substrate/blob/934fbfd/client/rpc-api/src/child_state/mod.rs#L29 -export type ChildStateRpc = { +export type ChildStateCalls = { /** * Returns the keys with prefix from a child storage, leave empty to get all the keys * @deprecated [2.0.0] Please use `getKeysPaged` with proper paging support @@ -23,7 +23,7 @@ export type ChildStateRpc = { childStorageKey: PrefixedStorageKey, prefix: StorageKey, hash?: Hash, - ): RpcResult + ): StorageKey[] /** * Returns the keys with prefix from a child storage with pagination support. * Up to `count` keys will be returned. @@ -35,35 +35,35 @@ export type ChildStateRpc = { count: number, startKey?: StorageKey, hash?: Hash, - ): RpcResult + ): StorageKey[] /** Returns a child storage entry at a specific block's state. */ childState_getStorage( childStorageKey: PrefixedStorageKey, key: StorageKey, hash?: Hash, - ): RpcResult + ): StorageData | null /** Returns child storage entries for multiple keys at a specific block's state. */ childState_getStorageEntries( childStorageKey: PrefixedStorageKey, keys: StorageKey[], hash?: Hash, - ): RpcResult<(StorageData | null)[]> + ): (StorageData | null)[] /** Returns the hash of a child storage entry at a block's state. */ childState_getStorageHash( childStorageKey: PrefixedStorageKey, key: StorageKey, hash?: Hash, - ): RpcResult + ): Hash | null /** Returns the size of a child storage entry at a block's state. */ childState_getStorageSize( childStorageKey: PrefixedStorageKey, key: StorageKey, hash?: Hash, - ): RpcResult + ): number | null /** Returns proof of storage for child key entries at a specific block's state. */ state_getChildReadProof( childStorageKey: PrefixedStorageKey, keys: StorageKey[], hash?: Hash, - ): RpcResult + ): ReadProof } diff --git a/rpc/known/contracts.ts b/rpc/known/contracts.ts index 608fab5bc..cde6084de 100644 --- a/rpc/known/contracts.ts +++ b/rpc/known/contracts.ts @@ -1,4 +1,4 @@ -import { AccountId, Hash, Hex, NumberOrHex, RpcResult, SerdeEnum, SerdeResult } from "./utils.ts" +import { AccountId, Hash, Hex, NumberOrHex, SerdeEnum, SerdeResult } from "./utils.ts" // https://github.com/paritytech/substrate/blob/0246883/frame/contracts/rpc/src/lib.rs#L92 /** A struct that encodes RPC parameters required for a call to a smart-contract. */ @@ -220,7 +220,7 @@ export interface InstantiateReturnValue { } // https://github.com/paritytech/substrate/blob/0246883/frame/contracts/rpc/src/lib.rs#L127 -export type ContractsRpc = { +export type ContractsCalls = { /** * Executes a call to a contract. * @@ -233,7 +233,7 @@ export type ContractsRpc = { contracts_call( callRequest: CallRequest, at?: Hash, - ): RpcResult>> + ): ContractResult> /** * Instantiate a new contract. * @@ -244,7 +244,7 @@ export type ContractsRpc = { */ contracts_instantiate( instantiateRequest: InstantiateRequest, - ): RpcResult>> + ): ContractResult> /** * Upload new code without instantiating a contract from it. * @@ -256,7 +256,7 @@ export type ContractsRpc = { contracts_upload_code( uploadRequest: CodeUploadRequest, at?: Hash, - ): RpcResult> + ): SerdeResult /** * Returns the value under a specified storage `key` in a contract given by `address` param, * or `None` if it is not set. @@ -265,5 +265,5 @@ export type ContractsRpc = { accountId: AccountId, key: Hex, aat?: Hash, - ): RpcResult + ): Hex | null } diff --git a/rpc/known/framesystem.ts b/rpc/known/framesystem.ts index b9e1d8079..d98e3e511 100644 --- a/rpc/known/framesystem.ts +++ b/rpc/known/framesystem.ts @@ -1,7 +1,7 @@ -import { AccountId, Hash, Hex, RpcResult } from "./utils.ts" +import { AccountId, Hash, Hex } from "./utils.ts" // https://github.com/paritytech/substrate/blob/eddf888/utils/frame/rpc/system/src/lib.rs#L41 -export type FrameSystemRpc = { +export type FrameSystemCalls = { /** * Returns the next valid index (aka nonce) for given account. * @@ -9,9 +9,9 @@ export type FrameSystemRpc = { * currently in the pool and if no transactions are found in the pool * it fallbacks to query the index from the runtime (aka. state nonce). */ - system_accountNextIndex(account: AccountId): RpcResult - account_nextIndex: FrameSystemRpc["system_accountNextIndex"] + system_accountNextIndex(account: AccountId): number + account_nextIndex: FrameSystemCalls["system_accountNextIndex"] /** Dry run an extrinsic at a given block. Return SCALE encoded ApplyExtrinsicResult. */ - system_dryRun(extrinsic: Hex, at?: Hash): RpcResult - system_dryRunAt: FrameSystemRpc["system_dryRun"] + system_dryRun(extrinsic: Hex, at?: Hash): Hex + system_dryRunAt: FrameSystemCalls["system_dryRun"] } diff --git a/rpc/known/grandpa.ts b/rpc/known/grandpa.ts index 92e751f36..8eeeb42fb 100644 --- a/rpc/known/grandpa.ts +++ b/rpc/known/grandpa.ts @@ -1,4 +1,4 @@ -import { Hex, RpcResult, Subscription } from "./utils.ts" +import { Hex, Subscription } from "./utils.ts" // https://github.com/paritytech/substrate/blob/0ba251c/client/finality-grandpa/rpc/src/report.rs#L116 /** @@ -40,25 +40,26 @@ export type JustificationNotification = Hex export type EncodedFinalityProof = Hex // https://github.com/paritytech/substrate/blob/9b01569/client/finality-grandpa/rpc/src/lib.rs#L48 -export type GrandpaRpc = { +export type GrandpaCalls = { /** * Returns the state of the current best round state as well as the * ongoing background rounds. */ - grandpa_roundState(): RpcResult + grandpa_roundState(): ReportedRoundStates + /** + * Prove finality for the given block number by returning the Justification for the last block + * in the set and all the intermediary headers to link them together. + */ + grandpa_proveFinality(block: number): EncodedFinalityProof | null +} + +export type GrandpaSubscriptions = { /** * Returns the block most recently finalized by Grandpa, alongside * side its justification. */ - grandpa_subscribeJustifications(): RpcResult< - Subscription<"grandpa_subscribeJustifications", JustificationNotification> + grandpa_subscribeJustifications(): Subscription< + "grandpa_unsubscribeJustifications", + JustificationNotification > - grandpa_unsubscribeJustifications( - subscription: Subscription<"grandpa_subscribeJustifications", JustificationNotification>, - ): void - /** - * Prove finality for the given block number by returning the Justification for the last block - * in the set and all the intermediary headers to link them together. - */ - grandpa_proveFinality(block: number): RpcResult } diff --git a/rpc/known/mmr.ts b/rpc/known/mmr.ts index f42bb62b4..52c2dfdc1 100644 --- a/rpc/known/mmr.ts +++ b/rpc/known/mmr.ts @@ -1,4 +1,4 @@ -import { Hash, Hex, RpcResult } from "./utils.ts" +import { Hash, Hex } from "./utils.ts" // https://github.com/paritytech/substrate/blob/6c5ac31/primitives/merkle-mountain-range/src/lib.rs#L37 /** @@ -33,7 +33,7 @@ export interface LeafBatchProof { } // https://github.com/paritytech/substrate/blob/eddf888/frame/merkle-mountain-range/rpc/src/lib.rs#L99 -export type MmrRpc = { +export type MmrCalls = { /** * Generate MMR proof for given leaf index. * @@ -44,7 +44,7 @@ export type MmrRpc = { * Returns the (full) leaf itself and a proof for this leaf (compact encoding, i.e. hash of * the leaf). Both parameters are SCALE-encoded. */ - mmr_generateProof(leafIndex: LeafIndex, at?: Hash): RpcResult + mmr_generateProof(leafIndex: LeafIndex, at?: Hash): LeafProof /** * Generate MMR proof for the given leaf indices. * @@ -57,5 +57,5 @@ export type MmrRpc = { * The order of entries in the `leaves` field of the returned struct * is the same as the order of the entries in `leaf_indices` supplied */ - mmr_generateBatchProof(leafIndices: LeafIndex[], at?: Hash): RpcResult + mmr_generateBatchProof(leafIndices: LeafIndex[], at?: Hash): LeafBatchProof } diff --git a/rpc/known/mod.ts b/rpc/known/mod.ts index 01fde4271..dbdf09e34 100644 --- a/rpc/known/mod.ts +++ b/rpc/known/mod.ts @@ -1,3 +1,43 @@ +import { AuthorCalls, AuthorSubscriptions } from "./author.ts" +import { BabeCalls } from "./babe.ts" +import { BeefyCalls, BeefySubscriptions } from "./beefy.ts" +import { ChainCalls, ChainSubscriptions } from "./chain.ts" +import { ChildStateCalls } from "./childstate.ts" +import { ContractsCalls } from "./contracts.ts" +import { FrameSystemCalls } from "./framesystem.ts" +import { GrandpaCalls, GrandpaSubscriptions } from "./grandpa.ts" +import { MmrCalls } from "./mmr.ts" +import { OffchainCalls } from "./offchain.ts" +import { TransactionPaymentCalls } from "./payment.ts" +import { StateCalls, StateSubscriptions } from "./state.ts" +import { StateMigrationCalls } from "./statemigration.ts" +import { SyncCalls } from "./sync.ts" +import { SystemCalls } from "./system.ts" + +export type Calls = + & AuthorCalls + & BabeCalls + & BeefyCalls + & ChainCalls + & ChildStateCalls + & ContractsCalls + & FrameSystemCalls + & GrandpaCalls + & MmrCalls + & OffchainCalls + & StateCalls + & StateMigrationCalls + & SyncCalls + & SystemCalls + & TransactionPaymentCalls + +export type Subscriptions = + & AuthorSubscriptions + & BeefySubscriptions + & ChainSubscriptions + & GrandpaSubscriptions + & StateSubscriptions + export * from "./author.ts" export * from "./babe.ts" export * from "./beefy.ts" diff --git a/rpc/known/offchain.ts b/rpc/known/offchain.ts index d53bd3043..652a9621d 100644 --- a/rpc/known/offchain.ts +++ b/rpc/known/offchain.ts @@ -1,4 +1,4 @@ -import { Hex, RpcResult, SerdeEnum } from "./utils.ts" +import { Hex, SerdeEnum } from "./utils.ts" /** A type of supported crypto. */ export type StorageKind = SerdeEnum<{ @@ -21,9 +21,9 @@ export type StorageKind = SerdeEnum<{ }> // https://github.com/paritytech/substrate/blob/7d233c2/client/rpc-api/src/offchain/mod.rs#L28 -export type OffchainRpc = { +export type OffchainCalls = { /** Set offchain local storage under given key and prefix. */ - offchain_localStorageSet(kind: StorageKind, key: Hex, value: Hex): RpcResult + offchain_localStorageSet(kind: StorageKind, key: Hex, value: Hex): null /** Get offchain local storage under given key and prefix. */ - offchain_localStorageGet(kind: StorageKind, key: Hex): RpcResult + offchain_localStorageGet(kind: StorageKind, key: Hex): Hex | null } diff --git a/rpc/known/payment.ts b/rpc/known/payment.ts index 115dd3c1d..8abfd21b6 100644 --- a/rpc/known/payment.ts +++ b/rpc/known/payment.ts @@ -83,7 +83,7 @@ export interface InclusionFee { } // https://github.com/paritytech/substrate/blob/eddf888/frame/transaction-payment/rpc/src/lib.rs#L41 -export type TransactionPaymentApi = { +export type TransactionPaymentCalls = { payment_queryInfo(extrinsic: Hex, at?: Hash): RuntimeDispatchInfo payment_queryFeeDetails(extrinsic: Hex, at?: Hash): FeeDetails } diff --git a/rpc/known/smoldot.ts b/rpc/known/smoldot.ts index 2435f5274..aeb81a1b6 100644 --- a/rpc/known/smoldot.ts +++ b/rpc/known/smoldot.ts @@ -1,4 +1,4 @@ -import { Hash, Hex, RpcResult, Subscription } from "./utils.ts" +import { Hash, Hex, Subscription } from "./utils.ts" export type NetworkConfig = { totalAttempts: number @@ -76,41 +76,41 @@ export type SmoldotRpc = { followSubscription: string, hash: Hash, networkConfig?: NetworkConfig, - ): RpcResult + ): string chainHead_unstable_call( followSubscription: string, hash: Hash, fn: string, callParameters: Hex, networkConfig?: NetworkConfig, - ): RpcResult + ): string chainHead_unstable_follow( runtimeUpdates: boolean, - ): RpcResult> - chainHead_unstable_genesisHash(): RpcResult - chainHead_unstable_header(followSubscription: string, hash: Hash): RpcResult - chainHead_unstable_stopBody(subscription: string): RpcResult - chainHead_unstable_stopCall(subscription: string): RpcResult - chainHead_unstable_stopStorage(subscription: string): RpcResult + ): Subscription<"chainHead_unstable_follow", ChainHeadUnstableFollowEvent> + chainHead_unstable_genesisHash(): Hash + chainHead_unstable_header(followSubscription: string, hash: Hash): Hex + chainHead_unstable_stopBody(subscription: string): void + chainHead_unstable_stopCall(subscription: string): void + chainHead_unstable_stopStorage(subscription: string): void chainHead_unstable_storage( followSubscription: string, hash: Hash, key: Hex, childKey?: Hex, networkConfig?: NetworkConfig, - ): RpcResult - chainHead_unstable_unfollow(followSubscription: string): RpcResult - chainHead_unstable_unpin(followSubscription: string, hash: Hash): RpcResult - chainSpec_unstable_chainName(): RpcResult - chainSpec_unstable_genesisHash(): RpcResult - chainSpec_unstable_properties(): RpcResult> - sudo_unstable_p2pDiscover(multiaddr: string): RpcResult - sudo_unstable_version(): RpcResult + ): string + chainHead_unstable_unfollow(followSubscription: string): void + chainHead_unstable_unpin(followSubscription: string, hash: Hash): void + chainSpec_unstable_chainName(): string + chainSpec_unstable_genesisHash(): Hash + chainSpec_unstable_properties(): Record + sudo_unstable_p2pDiscover(multiaddr: string): void + sudo_unstable_version(): string transaction_unstable_submitAndWatch( transaction: Hex, - ): RpcResult> - transaction_unstable_unwatch(subscription: string): RpcResult - chainHead_unstable_finalizedDatabase(maxSizeBytes?: bigint): RpcResult + ): Subscription<"transaction_unstable_submitAndWatch", TransactionWatchEvent> + transaction_unstable_unwatch(subscription: string): void + chainHead_unstable_finalizedDatabase(maxSizeBytes?: bigint): string } // TODO: do we even care about narrowing error code? diff --git a/rpc/known/state.ts b/rpc/known/state.ts index 408f1e729..ddad94a50 100644 --- a/rpc/known/state.ts +++ b/rpc/known/state.ts @@ -1,5 +1,5 @@ import { ReadProof, StorageData, StorageKey } from "./childstate.ts" -import { Hash, Hex, RpcResult, SerdeEnum, Subscription } from "./utils.ts" +import { Hash, Hex, SerdeEnum, Subscription } from "./utils.ts" // https://github.com/paritytech/substrate/blob/01a3ad65/primitives/version/src/lib.rs#L161 /** @@ -160,17 +160,17 @@ export interface Span { } // https://github.com/paritytech/substrate/blob/28ac0a8/client/rpc-api/src/state/mod.rs#L35 -export type StateRpc = { +export type StateCalls = { /** Call a contract at a block's state. */ - state_call(name: string, bytes: Hex, at?: Hash): RpcResult - state_callAt: StateRpc["state_call"] + state_call(name: string, bytes: Hex, at?: Hash): Hex + state_callAt: StateCalls["state_call"] /** * Returns the keys with prefix, leave empty to get all the keys. * @deprecated [2.0.0] Please use `getKeysPaged` with proper paging support */ - state_getKeys(prefix: StorageKey, at?: Hash): RpcResult + state_getKeys(prefix: StorageKey, at?: Hash): StorageKey[] /** Returns the keys with prefix, leave empty to get all the keys */ - state_getPairs(prefix: StorageKey, at?: Hash): RpcResult<[StorageKey, StorageData][]> + state_getPairs(prefix: StorageKey, at?: Hash): [StorageKey, StorageData][] /** * Returns the keys with prefix with pagination support. * Up to `count` keys will be returned. @@ -181,22 +181,22 @@ export type StateRpc = { count: number, startKey?: StorageKey, at?: Hash, - ): RpcResult - state_getKeysPagedAt: StateRpc["state_getKeysPaged"] + ): StorageKey[] + state_getKeysPagedAt: StateCalls["state_getKeysPaged"] /** Returns a storage entry at a specific block's state. */ - state_getStorage(key: StorageKey, at?: Hash): RpcResult - state_getStorageAt: StateRpc["state_getStorage"] + state_getStorage(key: StorageKey, at?: Hash): StorageData | null + state_getStorageAt: StateCalls["state_getStorage"] /** Returns the hash of a storage entry at a block's state. */ - state_getStorageHash(key: StorageKey, at?: Hash): RpcResult - state_getStorageHashAt: StateRpc["state_getStorageHash"] + state_getStorageHash(key: StorageKey, at?: Hash): Hash | null + state_getStorageHashAt: StateCalls["state_getStorageHash"] /** Returns the size of a storage entry at a block's state. */ - state_getStorageSize(key: StorageKey, at?: Hash): RpcResult - state_getStorageSizeAt: StateRpc["state_getStorageSize"] + state_getStorageSize(key: StorageKey, at?: Hash): number | null + state_getStorageSizeAt: StateCalls["state_getStorageSize"] /** Returns the runtime metadata as an opaque blob. */ - state_getMetadata(at?: Hash): RpcResult + state_getMetadata(at?: Hash): Hex /** Get the runtime version. */ - state_getRuntimeVersion(at?: Hash): RpcResult - chain_getRuntimeVersion: StateRpc["state_getRuntimeVersion"] + state_getRuntimeVersion(at?: Hash): RuntimeVersion + chain_getRuntimeVersion: StateCalls["state_getRuntimeVersion"] /** * Query historical storage entries (by key) starting from a block given as the second * parameter. @@ -204,32 +204,26 @@ export type StateRpc = { * NOTE This first returned result contains the initial state of storage for all keys. * Subsequent values in the vector represent changes to the previous state (diffs). */ - state_queryStorage(keys: StorageKey[], block: Hash, at?: Hash): RpcResult + state_queryStorage(keys: StorageKey[], block: Hash, at?: Hash): StorageChangeSet[] /** Query storage entries (by key) starting at block hash given as the second parameter. */ - state_queryStorageAt(keys: StorageKey[], at?: Hash): RpcResult + state_queryStorageAt(keys: StorageKey[], at?: Hash): StorageChangeSet[] /** Returns proof of storage entries at a specific block's state. */ - state_getReadProof(keys: StorageKey[], at?: Hash): RpcResult + state_getReadProof(keys: StorageKey[], at?: Hash): ReadProof +} + +export type StateSubscriptions = { /** New runtime version subscription */ - state_subscribeRuntimeVersion(): RpcResult< - Subscription<"state_subscribeRuntimeVersion", RuntimeVersion> - > - state_unsubscribeRuntimeVersion( - subscription: Subscription<"state_subscribeRuntimeVersion", RuntimeVersion>, - ): RpcResult - chain_subscribeRuntimeVersion: StateRpc["state_subscribeRuntimeVersion"] - chain_unsubscribeRuntimeVersion: StateRpc["state_unsubscribeRuntimeVersion"] + state_subscribeRuntimeVersion(): Subscription<"state_unsubscribeRuntimeVersion", RuntimeVersion> + chain_subscribeRuntimeVersion: StateSubscriptions["state_subscribeRuntimeVersion"] /** New storage subscription */ state_subscribeStorage( keys: StorageKey[] | null, - ): RpcResult> - state_unsubscribeStorage( - subscription: Subscription<"state_subscribeStorage", StorageChangeSet>, - ): RpcResult + ): Subscription<"state_unsubscribeStorage", StorageChangeSet> /** See https://paritytech.github.io/substrate/master/sc_rpc_api/state/trait.StateApiServer.html#tymethod.trace_block */ state_traceBlock( block: Hash, targets?: string, storageKeys?: string, methods?: string, - ): RpcResult + ): TraceBlockResponse } diff --git a/rpc/known/statemigration.ts b/rpc/known/statemigration.ts index de8c5a18c..d28e739e4 100644 --- a/rpc/known/statemigration.ts +++ b/rpc/known/statemigration.ts @@ -1,4 +1,4 @@ -import { Hash, RpcResult } from "./utils.ts" +import { Hash } from "./utils.ts" // https://github.com/paritytech/substrate/blob/00cc5f1/utils/frame/rpc/state-trie-migration-rpc/src/lib.rs#L106 export interface MigrationStatusResult { @@ -7,7 +7,7 @@ export interface MigrationStatusResult { } // https://github.com/paritytech/substrate/blob/00cc5f1/utils/frame/rpc/state-trie-migration-rpc/src/lib.rs#L113 -export type StateMigrationRpc = { +export type StateMigrationCalls = { /** * Check current migration state. * @@ -15,5 +15,5 @@ export type StateMigrationRpc = { * won't change any state. Nonetheless it is a VERY costly call that should be * only exposed to trusted peers. */ - state_trieMigrationStatus(at?: Hash): RpcResult + state_trieMigrationStatus(at?: Hash): MigrationStatusResult } diff --git a/rpc/known/sync.ts b/rpc/known/sync.ts index 79c8c9bb5..1f0eb0a1c 100644 --- a/rpc/known/sync.ts +++ b/rpc/known/sync.ts @@ -1,5 +1,5 @@ import { ChainType } from "./system.ts" -import { Hash, Hex, RpcResult } from "./utils.ts" +import { Hash, Hex } from "./utils.ts" // https://github.com/paritytech/substrate/blob/a7ba55d3/client/chain-spec/src/chain_spec.rs#L161 /** A configuration of a client. Does not include runtime storage initialization. */ @@ -64,8 +64,8 @@ export interface LightSyncState { } // https://github.com/paritytech/substrate/blob/eddf8883/client/sync-state-rpc/src/lib.rs#L128 -export type SyncRpc = { +export type SyncCalls = { // https://github.com/paritytech/substrate/blob/eddf8883/client/sync-state-rpc/src/lib.rs#L131 /** Returns the JSON serialized chainspec running the node, with a sync state. */ - system_gen_sync_spec(raw: boolean): RpcResult + system_gen_sync_spec(raw: boolean): ChainSpec } diff --git a/rpc/known/system.ts b/rpc/known/system.ts index e1210971d..b3d0e430a 100644 --- a/rpc/known/system.ts +++ b/rpc/known/system.ts @@ -1,4 +1,4 @@ -import { Hash, RpcResult, SerdeEnum } from "./utils.ts" +import { Hash, SerdeEnum } from "./utils.ts" // https://github.com/paritytech/substrate/blob/57e3486/client/chain-spec/src/lib.rs#L198 /** @@ -66,17 +66,17 @@ export interface SyncState { } // https://github.com/paritytech/substrate/blob/e0ccd00/client/rpc-api/src/system/mod.rs#L33 -export type SystemRpc = { +export type SystemCalls = { /** Get the node's implementation name. Plain old string. */ - system_name(): RpcResult + system_name(): string /** Get the node implementation's version. Should be a semver string. */ - system_version(): RpcResult + system_version(): string /** Get the chain's name. Given as a string identifier. */ - system_chain(): RpcResult + system_chain(): string /** Get the chain's type. */ - system_chainType(): RpcResult + system_chainType(): ChainType /** Get a custom set of properties as a JSON object, defined in the chain spec. */ - system_properties(): RpcResult> + system_properties(): Record /** * Return health status of the node. * @@ -84,18 +84,18 @@ export type SystemRpc = { * - connected to some peers (unless running in dev mode) * - not performing a major sync */ - system_health(): RpcResult + system_health(): Health /** Returns the base58-encoded PeerId of the node. */ - system_localPeerId(): RpcResult + system_localPeerId(): string /** * Returns the multi-addresses that the local node is listening on * * The addresses include a trailing `/p2p/` with the local PeerId, and are thus suitable to * be passed to `addReservedPeer` or as a bootnode address for example. */ - system_localListenAddresses(): RpcResult + system_localListenAddresses(): string[] /** Returns currently connected peers */ - system_peers(): RpcResult + system_peers(): PeerInfo[] /** * Returns current state of the network. * @@ -104,7 +104,7 @@ export type SystemRpc = { */ // TODO: the future of this call is uncertain: https://github.com/paritytech/substrate/issues/1890 // https://github.com/paritytech/substrate/issues/5541 - system_networkState(): RpcResult + system_networkState(): unknown /** * Adds a reserved peer. Returns the empty string or an error. The string * parameter should encode a `p2p` multiaddr. @@ -112,21 +112,21 @@ export type SystemRpc = { * `/ip4/198.51.100.19/tcp/30333/p2p/QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV` * is an example of a valid, passing multiaddr with PeerId attached. */ - system_addReservedPeer(peer: string): RpcResult + system_addReservedPeer(peer: string): void /** * Remove a reserved peer. Returns the empty string or an error. The string * should encode only the PeerId e.g. `QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV`. */ - system_removeReservedPeer(peerId: string): RpcResult + system_removeReservedPeer(peerId: string): void /** Returns the list of reserved peers */ - system_reservedPeers(): RpcResult + system_reservedPeers(): string[] /** Returns the roles the node is running as. */ - system_nodeRoles(): RpcResult + system_nodeRoles(): NodeRole[] /** * Returns the state of the syncing of the node: starting block, current best block, highest * known block. */ - system_syncState(): RpcResult + system_syncState(): SyncState /** * Adds the supplied directives to the current log filter * @@ -134,7 +134,7 @@ export type SystemRpc = { * * `sync=debug,state=trace` */ - system_addLogFilter(directives: string): RpcResult + system_addLogFilter(directives: string): void /** Resets the log filter to Substrate defaults */ - system_resetLogFilter(): RpcResult + system_resetLogFilter(): void } diff --git a/rpc/known/utils.ts b/rpc/known/utils.ts index 7106511a8..e1799b530 100644 --- a/rpc/known/utils.ts +++ b/rpc/known/utils.ts @@ -10,7 +10,10 @@ export type Hex = U.Hex export type Hash = U.HexHash export type SubId = string export type AccountId = string -export type Subscription = string & { _subscription: [T, U] } +export type Subscription = [unsubscribe: T, result: U] +export namespace Subscription { + export type UnsubscribeMethod = T extends Subscription ? U : never + export type Result = T extends Subscription ? U : never +} export type NumberOrHex = U.Hex | number export type ListOrValue = T | T[] -export type RpcResult = T From dd85af3a91fc6eed7fa7df8e8c99c13046683fe5 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Mon, 20 Feb 2023 12:41:27 -0500 Subject: [PATCH 16/23] fixing errors --- fluent/ChainRune.ts | 2 +- patterns/MultisigRune.ts | 111 +++++++++++++------------ patterns/consensus/babeBlockAuthor.ts | 7 +- patterns/consensus/preRuntimeDigest.ts | 3 +- patterns/ink/InkMetadataRune.ts | 27 +++--- patterns/ink/InkRune.ts | 11 +-- 6 files changed, 87 insertions(+), 74 deletions(-) diff --git a/fluent/ChainRune.ts b/fluent/ChainRune.ts index be70b2fda..bce566459 100644 --- a/fluent/ChainRune.ts +++ b/fluent/ChainRune.ts @@ -9,7 +9,7 @@ import { ConnectionRune } from "./ConnectionRune.ts" import { ExtrinsicRune } from "./ExtrinsicRune.ts" import { MetadataRune } from "./MetadataRune.ts" -export interface Chain { +export interface Chain { connection: Connection _call?: $.Codec _event?: $.Codec diff --git a/patterns/MultisigRune.ts b/patterns/MultisigRune.ts index 3cd15cb03..fc1badd9b 100644 --- a/patterns/MultisigRune.ts +++ b/patterns/MultisigRune.ts @@ -1,12 +1,14 @@ +import { types } from "polkadot_dev/mod.ts" import * as bytes from "../deps/std/bytes.ts" import { Chain, ChainRune } from "../fluent/ChainRune.ts" +import { ExtrinsicRune } from "../fluent/mod.ts" import { MultiAddress } from "../primitives/mod.ts" import { Rune, RunicArgs, ValueRune } from "../rune/mod.ts" import { multisigAccountId } from "./multisigAccountId.ts" -export interface MultisigRatifyProps { +export interface MultisigRatifyProps { sender: MultiAddress - call: unknown + call: Chain.Call } export interface MultisigVoteProps { @@ -19,6 +21,7 @@ export interface Multisig { threshold: number } +// TODO: swap out `Chain` constraints upon subset gen issue resolution... same for other patterns export class MultisigRune extends Rune { threshold = this.into(ValueRune).access("threshold") address = this.into(ValueRune).map(({ signatories, threshold }) => @@ -30,17 +33,10 @@ export class MultisigRune extends Rune["_prime"], readonly chain: ChainRune) { super(_prime) - this.pallet = this.client.metadata().pallet("Multisig") + this.pallet = this.chain.metadata().pallet("Multisig") this.storage = this.pallet.storage("Multisigs") } - static from( - chain: ChainRune, - ...[multisig]: RunicArgs - ) { - return Rune.resolve(multisig).into(MultisigRune, client) - } - otherSignatories(...[sender]: RunicArgs) { return Rune .tuple([this.into(ValueRune).access("signatories"), sender]) @@ -49,58 +45,69 @@ export class MultisigRune extends Rune({ sender, call: _call }: RunicArgs) { - const call = this.client.extrinsic(_call) - const maxWeight = call.feeEstimate().access("weight") - return this.client.extrinsic(Rune.rec({ - type: "Multisig", - value: Rune.rec({ - type: "asMulti", - threshold: this.threshold, - call, - otherSignatories: this.otherSignatories(sender), - storeCall: false, - maxWeight, - maybeTimepoint: this.maybeTimepoint(call.hash), - }), - })) + ratify({ sender, call: call_ }: RunicArgs>) { + const call = Rune + .resolve(call_) + .unsafeAs>() + .into(ExtrinsicRune, this.chain) + return Rune + .rec({ + type: "Multisig", + value: Rune.rec({ + type: "asMulti", + threshold: this.threshold, + call, + otherSignatories: this.otherSignatories(sender), + storeCall: false, + maxWeight: call.feeEstimate().access("weight"), + maybeTimepoint: this.maybeTimepoint(call.hash), + }), + }) + .unsafeAs>() + .into(ExtrinsicRune, this.chain) } approve({ sender, callHash }: RunicArgs) { - return this.client.extrinsic(Rune.rec({ - type: "Multisig", - value: Rune.rec({ - type: "approveAsMulti", - threshold: this.threshold, - callHash, - otherSignatories: this.otherSignatories(sender), - storeCall: false, - // TODO: is this right? - maxWeight: { - refTime: 0n, - proofSize: 0n, - }, - maybeTimepoint: this.maybeTimepoint(callHash), - }), - })) + return Rune + .rec({ + type: "Multisig", + value: Rune.rec({ + type: "approveAsMulti", + threshold: this.threshold, + callHash, + otherSignatories: this.otherSignatories(sender), + storeCall: false, + // TODO: is this right? + maxWeight: { + refTime: 0n, + proofSize: 0n, + }, + maybeTimepoint: this.maybeTimepoint(callHash), + }), + }) + .unsafeAs>() + .into(ExtrinsicRune, this.chain) } cancel({ sender, callHash }: RunicArgs) { - return this.client.extrinsic(Rune.rec({ - type: "Multisig", - value: Rune.rec({ - type: "cancelAsMulti", - threshold: this.threshold, - callHash, - otherSignatories: this.otherSignatories(sender), - timepoint: this.maybeTimepoint(callHash).map((x) => x ?? new NoProposalError()), - }), - })) + return Rune + .rec({ + type: "Multisig", + value: Rune.rec({ + type: "cancelAsMulti", + threshold: this.threshold, + callHash, + otherSignatories: this.otherSignatories(sender), + timepoint: this.maybeTimepoint(callHash).map((x) => x ?? new NoProposalError()), + }), + }) + .unsafeAs>() + .into(ExtrinsicRune, this.chain) } private maybeTimepoint(...[callHash]: RunicArgs) { return Rune.captureUnhandled( - [this, this.client, callHash], + [this, this.chain, callHash], (multisig, client, callHash) => multisig.into(MultisigRune, client.into(ChainRune)) .proposal(callHash) diff --git a/patterns/consensus/babeBlockAuthor.ts b/patterns/consensus/babeBlockAuthor.ts index 8b074577b..b7b5586d5 100644 --- a/patterns/consensus/babeBlockAuthor.ts +++ b/patterns/consensus/babeBlockAuthor.ts @@ -9,8 +9,7 @@ export function babeBlockAuthor( chain: ChainRune, ...[blockHash]: RunicArgs ) { - const validators = client - .into(ChainRune) + const validators = chain .metadata() .pallet("Session") .storage("Validators") @@ -18,7 +17,7 @@ export function babeBlockAuthor( .unsafeAs() .into(ValueRune) .unhandle(undefined) - const authorityIndex = preRuntimeDigest(client, blockHash) + const authorityIndex = preRuntimeDigest(chain, blockHash) .map(({ type, value }) => { if (type !== "BABE") return new AuthorRetrievalNotSupportedError() return $preDigest.decode(value) @@ -31,7 +30,7 @@ export function babeBlockAuthor( .map(([validators, authorityIndex]) => validators[authorityIndex]) .unhandle(undefined) .into(PublicKeyRune) - .address(client) + .address(chain) } export class AuthorRetrievalNotSupportedError extends Error { diff --git a/patterns/consensus/preRuntimeDigest.ts b/patterns/consensus/preRuntimeDigest.ts index 21ad7f680..90cbb4f09 100644 --- a/patterns/consensus/preRuntimeDigest.ts +++ b/patterns/consensus/preRuntimeDigest.ts @@ -8,8 +8,7 @@ export function preRuntimeDigest( chain: ChainRune, ...[blockHash]: RunicArgs ) { - return client - .into(ChainRune) + return chain .block(blockHash) .header() .into(ValueRune) diff --git a/patterns/ink/InkMetadataRune.ts b/patterns/ink/InkMetadataRune.ts index 84e709a8c..a57173162 100644 --- a/patterns/ink/InkMetadataRune.ts +++ b/patterns/ink/InkMetadataRune.ts @@ -1,5 +1,5 @@ import * as $ from "../../deps/scale.ts" -import { Chain, ChainRune, CodecRune, ExtrinsicRune, state } from "../../fluent/mod.ts" +import { Chain, ChainRune, CodecRune, ExtrinsicRune } from "../../fluent/mod.ts" import { ArrayRune, Rune, RunicArgs, ValueRune } from "../../rune/mod.ts" import { DeriveCodec } from "../../scale_info/mod.ts" import { hex } from "../../util/mod.ts" @@ -37,7 +37,7 @@ export class InkMetadataRune extends Rune normalize(JSON.parse(jsonText))) - .into(InkMetadataRune, Rune.resolve(client).into(ChainRune)) + .into(InkMetadataRune, chain) } salt() { @@ -109,14 +109,20 @@ export class InkMetadataRune extends Rune - state - .call(this.client, "ContractsApi_instantiate", instantiateArgs.map(hex.encode)) - .map((result) => $contractsApiInstantiateResult.decode(hex.decode(result))) - .access("gasRequired")) + .rehandle( + undefined, + () => + this.chain.connection.call( + "state_call", + "ContractsApi_instantiate", + instantiateArgs.map(hex.encode), + ) + .map((result) => $contractsApiInstantiateResult.decode(hex.decode(result))) + .access("gasRequired"), + ) return Rune .rec({ - type: "Contracts" as const, + type: "Contracts", value: Rune.rec({ type: "instantiateWithCode", value, @@ -127,7 +133,8 @@ export class InkMetadataRune extends Rune>() + .into(ExtrinsicRune, this.chain) } instance( @@ -136,7 +143,7 @@ export class InkMetadataRune extends Rune extends Rune $contractsApiCallResult.decode(hex.decode(result))) } @@ -86,7 +86,8 @@ export class InkRune extends Rune>() + .into(ExtrinsicRune, this.chain) } filterContractEvents = (...[events]: RunicArgs) => { @@ -102,7 +103,7 @@ export class InkRune extends Rune(...[failRuntimeEvent]: RunicArgs) => { - const metadata = this.client.metadata() + const metadata = this.chain.metadata() const $error = metadata.codec( metadata .pallet("Contracts") From a939fd627e299762892d105ab5d08310f5ba0b5f Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Mon, 20 Feb 2023 13:51:56 -0500 Subject: [PATCH 17/23] fix errors --- _tasks/download_frame_metadata.ts | 20 +++++++++----------- codegen/frame/FrameCodegen.ts | 10 +++++----- codegen/frame/pallet.ts | 6 +++--- examples/block_author.ts | 4 ++-- examples/block_events.ts | 4 ++-- examples/block_extrinsics.ts | 4 ++-- examples/dynamic/balance.ts | 4 ++-- examples/foo.ts | 14 -------------- examples/ink_e2e/main.ts | 12 ++++++------ examples/metadata.ts | 4 ++-- examples/multisig_transfer.ts | 14 ++++++++------ examples/watch.ts | 4 ++-- examples/xcm_asset_teleportation.ts | 7 ++++--- fluent/ChainRune.ts | 2 +- fluent/ConnectionRune.ts | 7 ++++++- patterns/MultisigRune.ts | 5 ++--- patterns/ink/InkMetadataRune.ts | 1 + providers/frame/FrameProvider.ts | 12 ++++++------ providers/frame/FrameProxyProvider.ts | 10 ++++------ rpc/Connection.ts | 2 +- 20 files changed, 68 insertions(+), 78 deletions(-) delete mode 100644 examples/foo.ts diff --git a/_tasks/download_frame_metadata.ts b/_tasks/download_frame_metadata.ts index 506afb4f8..a7129e8d1 100755 --- a/_tasks/download_frame_metadata.ts +++ b/_tasks/download_frame_metadata.ts @@ -1,12 +1,11 @@ -import { rawClient as kusama } from "kusama/client.ts" -import { rawClient as polkadot } from "polkadot/client.ts" -import { rawClient as rococo } from "rococo/client.ts" -import { rawClient as westend } from "westend/client.ts" -import { RpcServerError } from "../rpc/mod.ts" +import { chain as kusama } from "kusama/mod.ts" +import { chain as polkadot } from "polkadot/mod.ts" +import { chain as rococo } from "rococo/mod.ts" +import { chain as westend } from "westend/mod.ts" -const knownClients = { kusama, polkadot, westend, rococo } +const knownChains = { kusama, polkadot, westend, rococo } -const names = Object.keys(knownClients) +const names = Object.keys(knownChains) const outDir = new URL("../frame_metadata/_downloaded", import.meta.url) try { Deno.removeSync(outDir, { recursive: true }) @@ -34,11 +33,10 @@ async function download(name: string) { Deno.writeTextFileSync(modFilePath, modFileContents, { create: true }) await Promise.all( - Object.entries(knownClients).map(async ([name, client]) => { - const result = await client.call("state_getMetadata", []) - if (result.error) throw new RpcServerError(result) + Object.entries(knownChains).map(async ([name, chain]) => { + const result = await chain.connection.call("state_getMetadata").run() const outPath = new URL(`_downloaded/${name}.scale`, outDir) console.log(`Downloading ${name} metadata to "${outPath}".`) - await Deno.writeTextFile(outPath, result.result) + await Deno.writeTextFile(outPath, result) }), ) diff --git a/codegen/frame/FrameCodegen.ts b/codegen/frame/FrameCodegen.ts index 4e30c31f2..a95d1fad3 100644 --- a/codegen/frame/FrameCodegen.ts +++ b/codegen/frame/FrameCodegen.ts @@ -8,7 +8,7 @@ import { typeVisitor } from "./typeVisitor.ts" export interface FrameCodegenProps { metadata: Metadata - clientFile: File + chainFile: File } export class FrameCodegen { @@ -20,9 +20,9 @@ export class FrameCodegen { typeVisitor typeFiles = new Map() - constructor({ metadata, clientFile }: FrameCodegenProps) { + constructor({ metadata, chainFile }: FrameCodegenProps) { this.metadata = metadata - this.clientFile = clientFile + this.clientFile = chainFile this.typeVisitor = typeVisitor(this) @@ -33,7 +33,7 @@ export class FrameCodegen { this.files.set("codecs.ts", codecs(this)) - this.files.set("client.ts", clientFile) + this.files.set("chain.ts", chainFile) const callTy = Object .fromEntries(this.metadata.extrinsic.ty.params.map((x) => [x.name.toLowerCase(), x.ty])) @@ -60,7 +60,7 @@ export class FrameCodegen { this.typeVisitor.visit(eventTy) }> - export * from "./client.ts" + export * from "./chain.ts" export * as types from "./types/mod.ts" ${palletNamespaceExports} diff --git a/codegen/frame/pallet.ts b/codegen/frame/pallet.ts index 00c7d9f9b..5448d84d2 100644 --- a/codegen/frame/pallet.ts +++ b/codegen/frame/pallet.ts @@ -14,12 +14,12 @@ export function pallet(ctx: FrameCodegen, pallet: Pallet) { import * as codecs from "./codecs.ts" import { $ } from "./capi.ts" import * as C from "./capi.ts" - import { client } from "./client.ts" + import { chain } from "./chain.ts" `] for (const entry of pallet.storage?.entries ?? []) { items.push( makeDocComment(entry.docs) - + `export const ${entry.name} = client.metadata()` + + `export const ${entry.name} = chain.metadata()` + `.pallet(${S.string(pallet.name)})` + `.storage(${S.string(entry.name)})` + `["_asCodegenStorage"](${ @@ -39,7 +39,7 @@ export function pallet(ctx: FrameCodegen, pallet: Pallet) { items.push( makeDocComment(call.docs) + `export function ${type}(...args: Parameters>): C.ExtrinsicRune, Chain>` - + `{ return client.extrinsic(C.Rune.rec({ type: ${ + + `{ return chain.extrinsic(C.Rune.rec({ type: ${ S.string(pallet.name) }, value: ${typeName}(...args) })) }`, ) diff --git a/examples/block_author.ts b/examples/block_author.ts index 5040c647f..65d24ef25 100644 --- a/examples/block_author.ts +++ b/examples/block_author.ts @@ -1,6 +1,6 @@ import { babeBlockAuthor } from "capi/patterns/consensus/mod.ts" -import { client } from "polkadot/mod.ts" +import { chain } from "polkadot/mod.ts" -const result = await babeBlockAuthor(client, client.latestBlock.hash).run() +const result = await babeBlockAuthor(chain, chain.latestBlock.hash).run() console.log(result) diff --git a/examples/block_events.ts b/examples/block_events.ts index 5cd707d91..395f6e01e 100644 --- a/examples/block_events.ts +++ b/examples/block_events.ts @@ -1,5 +1,5 @@ -import { client } from "polkadot/mod.ts" +import { chain } from "polkadot/mod.ts" -const result = await client.latestBlock.events().run() +const result = await chain.latestBlock.events().run() console.log(result) diff --git a/examples/block_extrinsics.ts b/examples/block_extrinsics.ts index 9dbb2ba7c..168bbaf0e 100644 --- a/examples/block_extrinsics.ts +++ b/examples/block_extrinsics.ts @@ -1,5 +1,5 @@ -import { client } from "polkadot/mod.ts" +import { chain } from "polkadot/mod.ts" -const result = await client.latestBlock.extrinsics().run() +const result = await chain.latestBlock.extrinsics().run() console.log(result) diff --git a/examples/dynamic/balance.ts b/examples/dynamic/balance.ts index 7b90db572..c478195c1 100644 --- a/examples/dynamic/balance.ts +++ b/examples/dynamic/balance.ts @@ -1,7 +1,7 @@ import { alice } from "capi" -import { client } from "polkadot_dev/mod.ts" +import { chain } from "polkadot_dev/mod.ts" -const result = await client +const result = await chain .metadata() .pallet("System") .storage("Account") diff --git a/examples/foo.ts b/examples/foo.ts deleted file mode 100644 index 3c50a1e8a..000000000 --- a/examples/foo.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { WsConnection } from "../rpc/mod.ts" - -const controller = new AbortController() -const client = WsConnection.connect("ws://localhost:4646/frame/dev/polkadot/@x", controller.signal) - -await client.call("state_getMetadata", []) - -await client.call("state_getMetadata", []) - -controller.abort() - -console.log("hmm") - -self.addEventListener("unload", () => console.log("exiting inner")) diff --git a/examples/ink_e2e/main.ts b/examples/ink_e2e/main.ts index 38179bed2..458603f8e 100644 --- a/examples/ink_e2e/main.ts +++ b/examples/ink_e2e/main.ts @@ -1,14 +1,14 @@ -import { AddressRune, alice } from "capi" +import { AddressRune, alice, Rune } from "capi" import { InkMetadataRune, instantiationEventIntoPublicKey, isInstantiatedEvent, } from "capi/patterns/ink/mod.ts" -import { client } from "zombienet/rococo_contracts.toml/collator/@latest/mod.ts" +import { chain } from "zombienet/rococo_contracts.toml/collator/@latest/mod.ts" import { parse } from "../../deps/std/flags.ts" export const metadata = InkMetadataRune.from( - client, + chain, Deno.readTextFileSync("examples/ink_e2e/metadata.json"), ) @@ -34,15 +34,15 @@ if (!address) { ) .unhandle(FailedToFindContractInstantiatedError) .pipe(instantiationEventIntoPublicKey) - .address(client) + .address(chain) .run() } console.log(`Contract address: ${address}`) -const publicKey = AddressRune.from(client, address).publicKey() +const publicKey = Rune.resolve(address).into(AddressRune, chain).publicKey() console.log("Contract public key:", await publicKey.run()) -const contract = metadata.instance(client, publicKey) +const contract = metadata.instance(chain, publicKey) console.log("Get:", await contract.call({ sender, method: "get" }).run()) diff --git a/examples/metadata.ts b/examples/metadata.ts index 6e42ca284..12e118af8 100644 --- a/examples/metadata.ts +++ b/examples/metadata.ts @@ -1,5 +1,5 @@ -import { client } from "polkadot_dev/mod.ts" +import { chain } from "polkadot_dev/mod.ts" -const result = await client.metadata().run() +const result = await chain.metadata().run() console.log(result) diff --git a/examples/multisig_transfer.ts b/examples/multisig_transfer.ts index 50a52bd98..8a549bb1c 100644 --- a/examples/multisig_transfer.ts +++ b/examples/multisig_transfer.ts @@ -1,13 +1,15 @@ -import { alice, bob, charlie, dave, ValueRune } from "capi" +import { alice, bob, charlie, dave, Rune, ValueRune } from "capi" import { MultisigRune } from "capi/patterns/MultisigRune.ts" -import { Balances, client, System } from "polkadot_dev/mod.ts" +import { Balances, chain, System } from "polkadot_dev/mod.ts" // TODO: utilize type exposed from capi/patterns/MultisigRune.ts (when we enable client env specificity) import { MultiAddress } from "polkadot_dev/types/sp_runtime/multiaddress.ts" -const multisig = MultisigRune.from(client, { - signatories: [alice.publicKey, bob.publicKey, charlie.publicKey], - threshold: 2, -}) +const multisig = Rune + .constant({ + signatories: [alice.publicKey, bob.publicKey, charlie.publicKey], + threshold: 2, + }) + .into(MultisigRune, chain) // Read dave's initial balance (to-be changed by the call) console.log("Dave initial balance:", await System.Account.entry([dave.publicKey]).run()) diff --git a/examples/watch.ts b/examples/watch.ts index 6246c9caf..8b17ef663 100644 --- a/examples/watch.ts +++ b/examples/watch.ts @@ -1,7 +1,7 @@ import { Rune } from "capi" -import { client, Timestamp } from "polkadot/mod.ts" +import { chain, Timestamp } from "polkadot/mod.ts" -const block = client.latestBlock +const block = chain.latestBlock const extrinsics = block.extrinsics() const events = block.events() const now = Timestamp.Now.entry([], block.hash) diff --git a/examples/xcm_asset_teleportation.ts b/examples/xcm_asset_teleportation.ts index f5ca039fd..c88021f8a 100644 --- a/examples/xcm_asset_teleportation.ts +++ b/examples/xcm_asset_teleportation.ts @@ -1,8 +1,8 @@ -import { alice, Rune } from "capi" +import { alice, Rune, ValueRune } from "capi" import { types, XcmPallet } from "zombienet/statemine.toml/alice/@latest/mod.ts" import { Event as XcmPalletEvent } from "zombienet/statemine.toml/alice/@latest/types/pallet_xcm/pallet.ts" import { RuntimeEvent as AliceRuntimeEvent } from "zombienet/statemine.toml/alice/@latest/types/rococo_runtime/mod.ts" -import { client, System } from "zombienet/statemine.toml/collator/@latest/mod.ts" +import { chain, System } from "zombienet/statemine.toml/collator/@latest/mod.ts" import { Event as ParachainSystemEvent } from "zombienet/statemine.toml/collator/@latest/types/cumulus_pallet_parachain_system/pallet.ts" import { RuntimeEvent as CollatorRuntimeEvent } from "zombienet/statemine.toml/collator/@latest/types/statemine_runtime.ts" @@ -59,7 +59,8 @@ const initiatedEvent = XcmPallet .log("Initiated event:") const processedEvent = System.Events - .entry([], client.latestBlock.hash) + .entry([], chain.latestBlock.hash) + .into(ValueRune) .map((events) => events ?.find((e) => diff --git a/fluent/ChainRune.ts b/fluent/ChainRune.ts index bce566459..277bb6c52 100644 --- a/fluent/ChainRune.ts +++ b/fluent/ChainRune.ts @@ -9,7 +9,7 @@ import { ConnectionRune } from "./ConnectionRune.ts" import { ExtrinsicRune } from "./ExtrinsicRune.ts" import { MetadataRune } from "./MetadataRune.ts" -export interface Chain { +export interface Chain { connection: Connection _call?: $.Codec _event?: $.Codec diff --git a/fluent/ConnectionRune.ts b/fluent/ConnectionRune.ts index 0a4ad9445..8a808397b 100644 --- a/fluent/ConnectionRune.ts +++ b/fluent/ConnectionRune.ts @@ -1,6 +1,7 @@ import { Calls, Subscription, Subscriptions } from "../rpc/known/mod.ts" import { Connection, RpcClientError, RpcServerError, RpcSubscriptionMessage } from "../rpc/mod.ts" -import { Batch, MetaRune, Run, Rune, RunicArgs, RunStream } from "../rune/mod.ts" +import { Batch, MetaRune, Run, Rune, RunicArgs, RunStream, ValueRune } from "../rune/mod.ts" +import { ChainRune } from "./ChainRune.ts" class RunConnection extends Run { constructor(ctx: Batch, readonly initConnection: (signal: AbortSignal) => Connection) { @@ -55,6 +56,10 @@ export class ConnectionRune extends Rune { }) .throws(RpcClientError, RpcServerError) } + + chain() { + return this.into(ValueRune).map((x) => ({ connection: x })).into(ChainRune) + } } class RunRpcSubscription diff --git a/patterns/MultisigRune.ts b/patterns/MultisigRune.ts index fc1badd9b..4677ae285 100644 --- a/patterns/MultisigRune.ts +++ b/patterns/MultisigRune.ts @@ -1,4 +1,3 @@ -import { types } from "polkadot_dev/mod.ts" import * as bytes from "../deps/std/bytes.ts" import { Chain, ChainRune } from "../fluent/ChainRune.ts" import { ExtrinsicRune } from "../fluent/mod.ts" @@ -108,8 +107,8 @@ export class MultisigRune extends Rune(...[callHash]: RunicArgs) { return Rune.captureUnhandled( [this, this.chain, callHash], - (multisig, client, callHash) => - multisig.into(MultisigRune, client.into(ChainRune)) + (multisig, chain, callHash) => + multisig.into(MultisigRune, chain.into(ChainRune)) .proposal(callHash) .unsafeAs<{ when: unknown }>() .into(ValueRune) diff --git a/patterns/ink/InkMetadataRune.ts b/patterns/ink/InkMetadataRune.ts index a57173162..8232a765e 100644 --- a/patterns/ink/InkMetadataRune.ts +++ b/patterns/ink/InkMetadataRune.ts @@ -30,6 +30,7 @@ export class InkMetadataRune extends Rune( chain: ChainRune, ...[jsonText]: RunicArgs diff --git a/providers/frame/FrameProvider.ts b/providers/frame/FrameProvider.ts index 249aed7da..b177d14b0 100644 --- a/providers/frame/FrameProvider.ts +++ b/providers/frame/FrameProvider.ts @@ -13,8 +13,8 @@ export abstract class FrameProvider extends Provider { codegenCtxsPending: Record> = {} - abstract client(pathInfo: PathInfo, signal: AbortSignal): Promise - abstract clientFile(pathInfo: PathInfo): Promise + abstract connect(pathInfo: PathInfo, signal: AbortSignal): Promise + abstract chainFile(pathInfo: PathInfo): Promise async handle(request: Request, pathInfo: PathInfo): Promise { const { vRuntime, filePath } = pathInfo @@ -50,7 +50,7 @@ export abstract class FrameProvider extends Provider { // TODO: memo async latestVersion(pathInfo: PathInfo) { return await withSignal(async (signal) => { - const client = await this.client(pathInfo, signal) + const client = await this.connect(pathInfo, signal) const version = await this.clientCall(client, "system_version", []) return this.normalizeRuntimeVersion(version) }) @@ -68,8 +68,8 @@ export abstract class FrameProvider extends Provider { codegen(pathInfo: PathInfo) { return this.codegenMemo.run(this.cacheKey(pathInfo), async () => { const metadata = await this.getMetadata(pathInfo) - const clientFile = await this.clientFile(pathInfo) - return new FrameCodegen({ metadata, clientFile }) + const chainFile = await this.chainFile(pathInfo) + return new FrameCodegen({ metadata, chainFile }) }) } @@ -82,7 +82,7 @@ export abstract class FrameProvider extends Provider { if (pathInfo.vRuntime !== await this.latestVersion(pathInfo)) { throw f.serverError("Cannot get metadata for old runtime version") } - const client = await this.client(pathInfo, signal) + const client = await this.connect(pathInfo, signal) const metadata = fromPrefixedHex( await this.clientCall(client, "state_getMetadata", []), ) diff --git a/providers/frame/FrameProxyProvider.ts b/providers/frame/FrameProxyProvider.ts index 0caee721f..5d9b7f401 100644 --- a/providers/frame/FrameProxyProvider.ts +++ b/providers/frame/FrameProxyProvider.ts @@ -17,7 +17,6 @@ export abstract class FrameProxyProvider extends FrameProvider { } async proxyWs(request: Request, pathInfo: PathInfo) { - console.log("new connection") const url = await this.dynamicUrl(pathInfo) const server = new WebSocket(url) const { socket: client, response } = Deno.upgradeWebSocket(request) @@ -60,11 +59,11 @@ export abstract class FrameProxyProvider extends FrameProvider { ).toString() } - async client(pathInfo: PathInfo, signal: AbortSignal) { + async connect(pathInfo: PathInfo, signal: AbortSignal) { return WsConnection.connect(await this.dynamicUrl(pathInfo), signal) } - async clientFile(pathInfo: PathInfo) { + async chainFile(pathInfo: PathInfo) { const url = this.staticUrl(pathInfo) return new File(` import * as C from "./capi.ts" @@ -72,9 +71,8 @@ export abstract class FrameProxyProvider extends FrameProvider { export const discoveryValue = "${url}" - export const client = C.rpcClient(C.WsRpcConn, discoveryValue)["_asCodegen"]() - - export const rawClient = new C.RpcClient(C.WsRpcConn, discoveryValue) + export const chain = C.connection((signal) => C.WsConnection.connect(discoveryValue, signal)) + .chain()["_asCodegen"]() `) } } diff --git a/rpc/Connection.ts b/rpc/Connection.ts index 97fa9baca..d7f92d896 100644 --- a/rpc/Connection.ts +++ b/rpc/Connection.ts @@ -33,7 +33,7 @@ export abstract class Connection { const connection = new this(discovery) connection.ref(signal) connection.signal.addEventListener("abort", () => { - memo.delete(connection) + memo.delete(discovery) }) return connection }) From 4cb34b2b2e6680e4bbcc4a67d85b540b9ade9ad8 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Mon, 20 Feb 2023 14:54:27 -0500 Subject: [PATCH 18/23] rebase and fix type errors --- examples/ink_e2e/main.ts | 8 ++++---- patterns/ink/InkMetadata.ts | 4 ++++ patterns/ink/InkMetadataRune.ts | 11 ----------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/examples/ink_e2e/main.ts b/examples/ink_e2e/main.ts index 458603f8e..fdb4c8b1e 100644 --- a/examples/ink_e2e/main.ts +++ b/examples/ink_e2e/main.ts @@ -3,14 +3,14 @@ import { InkMetadataRune, instantiationEventIntoPublicKey, isInstantiatedEvent, + parse as parseMetadata, } from "capi/patterns/ink/mod.ts" import { chain } from "zombienet/rococo_contracts.toml/collator/@latest/mod.ts" import { parse } from "../../deps/std/flags.ts" -export const metadata = InkMetadataRune.from( - chain, - Deno.readTextFileSync("examples/ink_e2e/metadata.json"), -) +export const metadata = Rune + .resolve(parseMetadata(Deno.readTextFileSync("examples/ink_e2e/metadata.json"))) + .into(InkMetadataRune, chain) const sender = alice.publicKey diff --git a/patterns/ink/InkMetadata.ts b/patterns/ink/InkMetadata.ts index 63dd5c61b..f9bc7bb4d 100644 --- a/patterns/ink/InkMetadata.ts +++ b/patterns/ink/InkMetadata.ts @@ -171,3 +171,7 @@ function normalizeFields(fields: any[]) { return { ty, ...rest } }) } + +export function parse(jsonText: string) { + return normalize(JSON.parse(jsonText)) +} diff --git a/patterns/ink/InkMetadataRune.ts b/patterns/ink/InkMetadataRune.ts index 8232a765e..d85421c7f 100644 --- a/patterns/ink/InkMetadataRune.ts +++ b/patterns/ink/InkMetadataRune.ts @@ -30,17 +30,6 @@ export class InkMetadataRune extends Rune( - chain: ChainRune, - ...[jsonText]: RunicArgs - ) { - return Rune - .resolve(jsonText) - .map((jsonText) => normalize(JSON.parse(jsonText))) - .into(InkMetadataRune, chain) - } - salt() { return Rune.constant(crypto.getRandomValues(new Uint8Array(4))) } From 884499f5dec20f2939b12b5e37e5da6f54c6ec1f Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Mon, 20 Feb 2023 17:00:08 -0500 Subject: [PATCH 19/23] fix lint error --- patterns/ink/InkMetadataRune.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/ink/InkMetadataRune.ts b/patterns/ink/InkMetadataRune.ts index d85421c7f..30718cfbe 100644 --- a/patterns/ink/InkMetadataRune.ts +++ b/patterns/ink/InkMetadataRune.ts @@ -3,7 +3,7 @@ import { Chain, ChainRune, CodecRune, ExtrinsicRune } from "../../fluent/mod.ts" import { ArrayRune, Rune, RunicArgs, ValueRune } from "../../rune/mod.ts" import { DeriveCodec } from "../../scale_info/mod.ts" import { hex } from "../../util/mod.ts" -import { Callable, InkMetadata, normalize } from "./InkMetadata.ts" +import { Callable, InkMetadata } from "./InkMetadata.ts" import { InkRune } from "./InkRune.ts" import { $contractsApiInstantiateArgs, $contractsApiInstantiateResult, Weight } from "./known.ts" From f586ddc13493e05c2944f3bfd92d79a1cc8fec76 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Mon, 20 Feb 2023 17:36:27 -0500 Subject: [PATCH 20/23] rpc cleanup --- codegen/frame/FrameCodegen.ts | 4 +- fluent/ConnectionRune.ts | 37 +++++++-------- providers/frame/FrameProvider.ts | 44 +++++++++--------- providers/frame/zombienet.ts | 1 + rpc/Connection.ts | 78 +++++++++---------------------- rpc/mod.ts | 2 +- rpc/rpc_common.ts | 80 -------------------------------- rpc/rpc_messages.ts | 73 +++++++++++++++++++++++++++++ rpc/smoldot.ts | 18 +++---- rpc/ws.ts | 28 +++++------ 10 files changed, 162 insertions(+), 203 deletions(-) delete mode 100644 rpc/rpc_common.ts create mode 100644 rpc/rpc_messages.ts diff --git a/codegen/frame/FrameCodegen.ts b/codegen/frame/FrameCodegen.ts index a95d1fad3..cdfe1bfa6 100644 --- a/codegen/frame/FrameCodegen.ts +++ b/codegen/frame/FrameCodegen.ts @@ -15,14 +15,14 @@ export class FrameCodegen { files = new Map() metadata - clientFile + chainFile typeVisitor typeFiles = new Map() constructor({ metadata, chainFile }: FrameCodegenProps) { this.metadata = metadata - this.clientFile = chainFile + this.chainFile = chainFile this.typeVisitor = typeVisitor(this) diff --git a/fluent/ConnectionRune.ts b/fluent/ConnectionRune.ts index 8a808397b..23d17de93 100644 --- a/fluent/ConnectionRune.ts +++ b/fluent/ConnectionRune.ts @@ -1,20 +1,21 @@ import { Calls, Subscription, Subscriptions } from "../rpc/known/mod.ts" -import { Connection, RpcClientError, RpcServerError, RpcSubscriptionMessage } from "../rpc/mod.ts" +import { Connection, ConnectionError, RpcSubscriptionMessage, ServerError } from "../rpc/mod.ts" import { Batch, MetaRune, Run, Rune, RunicArgs, RunStream, ValueRune } from "../rune/mod.ts" +import { PromiseOr } from "../util/mod.ts" import { ChainRune } from "./ChainRune.ts" class RunConnection extends Run { - constructor(ctx: Batch, readonly initConnection: (signal: AbortSignal) => Connection) { + constructor(ctx: Batch, readonly initConnection: (signal: AbortSignal) => PromiseOr) { super(ctx) } connection?: Connection async _evaluate(): Promise { - return this.connection ??= this.initConnection(this.signal) + return this.connection ??= await this.initConnection(this.signal) } } -export function connection(init: (signal: AbortSignal) => Connection) { +export function connection(init: (signal: AbortSignal) => PromiseOr) { return Rune.new(RunConnection, init).into(ConnectionRune) } @@ -25,12 +26,12 @@ export class ConnectionRune extends Rune { ) { return Rune .tuple([this.as(Rune), ...args]) - .map(async ([client, ...params]) => { - const result = await client.call>(callMethod, params) - if (result.error) throw new RpcServerError(result) - return result.result + .map(async ([connection, ...params]) => { + const result = await connection.call(callMethod, params) + if (result.error) throw new ServerError(result) + return result.result as ReturnType }) - .throws(RpcClientError, RpcServerError) + .throws(ConnectionError, ServerError) } subscribe( @@ -39,10 +40,10 @@ export class ConnectionRune extends Rune { ...args: RunicArgs]> ) { return Rune.tuple([this, ...args]) - .map(([client, ...params]) => + .map(([connection, ...params]) => Rune.new( - RunRpcSubscription>>, - client, + RunRpcSubscription, + connection, params, subscribeMethod, unsubscribeMethod, @@ -51,10 +52,10 @@ export class ConnectionRune extends Rune { .into(MetaRune) .flat() .map((event) => { - if (event.error) throw new RpcServerError(event) - return event.params.result + if (event.error) throw new ServerError(event) + return event.params.result as Subscription.Result> }) - .throws(RpcClientError, RpcServerError) + .throws(ConnectionError, ServerError) } chain() { @@ -62,9 +63,7 @@ export class ConnectionRune extends Rune { } } -class RunRpcSubscription - extends RunStream> -{ +class RunRpcSubscription extends RunStream { constructor( ctx: Batch, connection: Connection, @@ -73,7 +72,7 @@ class RunRpcSubscription unsubscribeMethod: string, ) { super(ctx) - connection.subscription( + connection.subscription( subscribeMethod, unsubscribeMethod, params, diff --git a/providers/frame/FrameProvider.ts b/providers/frame/FrameProvider.ts index b177d14b0..bc59e0ae8 100644 --- a/providers/frame/FrameProvider.ts +++ b/providers/frame/FrameProvider.ts @@ -2,7 +2,7 @@ import { File, FrameCodegen } from "../../codegen/frame/mod.ts" import { posix as path } from "../../deps/std/path.ts" import { $metadata } from "../../frame_metadata/Metadata.ts" import { fromPrefixedHex } from "../../frame_metadata/mod.ts" -import { Connection, RpcServerError } from "../../rpc/mod.ts" +import { Connection, ServerError } from "../../rpc/mod.ts" import { f, PathInfo, Provider } from "../../server/mod.ts" import { fromPathInfo } from "../../server/PathInfo.ts" import { WeakMemo } from "../../util/mod.ts" @@ -49,11 +49,8 @@ export abstract class FrameProvider extends Provider { // TODO: memo async latestVersion(pathInfo: PathInfo) { - return await withSignal(async (signal) => { - const client = await this.connect(pathInfo, signal) - const version = await this.clientCall(client, "system_version", []) - return this.normalizeRuntimeVersion(version) - }) + const version = await this.call(pathInfo, "system_version", []) + return this.normalizeRuntimeVersion(version) } normalizeRuntimeVersion(version: string) { @@ -77,27 +74,28 @@ export abstract class FrameProvider extends Provider { return this.env.cache.get( `${this.cacheKey(pathInfo)}/metadata`, $metadata, - () => - withSignal(async (signal) => { - if (pathInfo.vRuntime !== await this.latestVersion(pathInfo)) { - throw f.serverError("Cannot get metadata for old runtime version") - } - const client = await this.connect(pathInfo, signal) - const metadata = fromPrefixedHex( - await this.clientCall(client, "state_getMetadata", []), - ) - return metadata - }), + async () => { + if (pathInfo.vRuntime !== await this.latestVersion(pathInfo)) { + throw f.serverError("Cannot get metadata for old runtime version") + } + const metadata = fromPrefixedHex( + await this.call(pathInfo, "state_getMetadata", []), + ) + return metadata + }, ) } - async clientCall( - client: Connection, + async call( + pathInfo: PathInfo, method: string, params: unknown[] = [], - ): Promise { - const result = await client.call(method, params) - if (result.error) throw new RpcServerError(result) - return result.result + ): Promise { + return withSignal(async (signal) => { + const connection = await this.connect(pathInfo, signal) + const result = await connection.call(method, params) + if (result.error) throw new ServerError(result) + return result.result as T + }) } } diff --git a/providers/frame/zombienet.ts b/providers/frame/zombienet.ts index f16223851..9b116768e 100644 --- a/providers/frame/zombienet.ts +++ b/providers/frame/zombienet.ts @@ -37,6 +37,7 @@ export class ZombienetProvider extends FrameProxyProvider { urlMemo = new PermanentMemo() dynamicUrl(pathInfo: PathInfo) { const { target } = pathInfo + if (!target) throw new Error("Missing target") return this.urlMemo.run(target, async () => { const targetParts = splitLast("/", target) if (!targetParts) throw new Error("Failed to parse zombienet target") diff --git a/rpc/Connection.ts b/rpc/Connection.ts index d7f92d896..5815d811d 100644 --- a/rpc/Connection.ts +++ b/rpc/Connection.ts @@ -1,23 +1,15 @@ import { Deferred, deferred } from "../deps/std/async.ts" import { getOrInit } from "../util/mod.ts" -import { - RpcErrorMessage, - RpcErrorMessageData, - RpcHandler, - RpcIngressMessage, - RpcMessageId, - RpcNotificationMessage, - RpcOkMessage, -} from "./rpc_common.ts" +import { RpcCallMessage, RpcIngressMessage, RpcSubscriptionHandler } from "./rpc_messages.ts" const connectionMemos = new Map Connection, Map>() export abstract class Connection { - currentId = 0 + nextId = 0 references = 0 - signal #controller + constructor() { this.#controller = new AbortController() this.signal = this.#controller.signal @@ -51,52 +43,44 @@ export abstract class Connection { abstract ready(): Promise - abstract send(id: RpcMessageId, method: string, params: unknown): void + abstract send( + id: number, + method: string, + params: unknown, + ): void protected abstract close(): void - callResultPendings: Record> = {} - async call( - method: string, - params: unknown[], - ) { - const controller = new AbortController() + callResultPendings: Record> = {} + async call(method: string, params: unknown[]) { await this.ready() - const id = this.currentId++ - const pending = deferred>() + const id = this.nextId++ + const pending = deferred() this.callResultPendings[id] = pending this.send(id, method, params) - const result = await pending - controller.abort() - return result + return await pending } - subscriptionInitPendings: Record = {} - subscriptionHandlers: Record = {} - subscriptionIdByRpcMessageId: Record = {} - subscription< - Method extends string = string, - NotificationData = unknown, - ErrorData extends RpcErrorMessageData = RpcErrorMessageData, - >( + subscriptionInitPendings: Record = {} + subscriptionHandlers: Record = {} + subscriptionIdByRpcMessageId: Record = {} + subscription( subscribe: string, unsubscribe: string, params: unknown[], - handler: RpcSubscriptionHandler, + handler: RpcSubscriptionHandler, signal: AbortSignal, ) { - const controller = new AbortController() ;(async () => { await this.ready() - const subscribeId = this.currentId++ + const subscribeId = this.nextId++ this.subscriptionInitPendings[subscribeId] = handler as RpcSubscriptionHandler signal.addEventListener("abort", () => { delete this.subscriptionInitPendings[subscribeId] const subscriptionId = this.subscriptionIdByRpcMessageId[subscribeId] if (subscriptionId) { delete this.subscriptionIdByRpcMessageId[subscribeId] - this.send(this.currentId++, unsubscribe, [subscriptionId]) - controller.abort() + this.send(this.nextId++, unsubscribe, [subscriptionId]) } }) this.send(subscribeId, subscribe, params) @@ -115,8 +99,9 @@ export abstract class Connection { if (subscriptionPending) { if (message.error) subscriptionPending(message) else { - this.subscriptionHandlers[message.result] = subscriptionPending - this.subscriptionIdByRpcMessageId[message.id] = message.result + const subscriptionId = message.result as string + this.subscriptionHandlers[subscriptionId] = subscriptionPending + this.subscriptionIdByRpcMessageId[message.id] = subscriptionId } delete this.subscriptionInitPendings[message.id] } @@ -124,20 +109,3 @@ export abstract class Connection { else throw new Error(Deno.inspect(message)) // ... for now } } - -export type RpcCallMessage< - OkData = any, - ErrorData extends RpcErrorMessageData = RpcErrorMessageData, -> = RpcOkMessage | RpcErrorMessage - -export type RpcSubscriptionMessage< - Method extends string = string, - NotificationData = any, - ErrorData extends RpcErrorMessageData = RpcErrorMessageData, -> = RpcNotificationMessage | RpcErrorMessage - -export type RpcSubscriptionHandler< - Method extends string = string, - NotificationData = any, - ErrorData extends RpcErrorMessageData = RpcErrorMessageData, -> = RpcHandler> diff --git a/rpc/mod.ts b/rpc/mod.ts index 863c047ef..5ccfcf493 100644 --- a/rpc/mod.ts +++ b/rpc/mod.ts @@ -3,6 +3,6 @@ export * as known from "./known/mod.ts" // moderate --exclude known export * from "./Connection.ts" -export * from "./rpc_common.ts" +export * from "./rpc_messages.ts" export * from "./smoldot.ts" export * from "./ws.ts" diff --git a/rpc/rpc_common.ts b/rpc/rpc_common.ts deleted file mode 100644 index 88266f65c..000000000 --- a/rpc/rpc_common.ts +++ /dev/null @@ -1,80 +0,0 @@ -export interface RpcEgressMessage extends RpcVersionBearer, RpcMessageIdBearer { - method: string - params: any[] -} -export namespace RpcEgressMessage { - export function fmt(id: RpcMessageId, method: string, params: unknown[]) { - const message: RpcEgressMessage = { - jsonrpc: "2.0", - id, - method, - params, - } - return JSON.stringify(message) - } -} - -export type RpcIngressMessage = RpcOkMessage | RpcErrorMessage | RpcNotificationMessage - -export type RpcHandler = ( - message: Message, -) => void - -export interface RpcOkMessage extends RpcVersionBearer, RpcMessageIdBearer { - result: OkData - params?: never - error?: never -} - -export interface RpcErrorMessage - extends RpcVersionBearer, RpcMessageIdBearer -{ - error: ErrorData - params?: never - result?: never -} -export interface RpcErrorMessageData { - code: Code - message: string - data: Data -} - -export interface RpcNotificationMessage - extends RpcVersionBearer -{ - method: Method - id?: never - params: { - subscription: string - result: NotificationData - } - result?: never - error?: never -} - -export type RpcVersion = "2.0" -interface RpcVersionBearer { - jsonrpc: RpcVersion -} - -export type RpcMessageId = number | string -export interface RpcMessageIdBearer { - id: RpcMessageId -} - -export class RpcClientError extends Error { - override readonly name = "RpcClientError" -} - -export class RpcServerError extends Error { - override readonly name = "RpcServerError" - code - data - - // TODO: accept init `EgressMessage`? - constructor({ error: { code, data, message } }: RpcErrorMessage) { - super(message, { cause: message }) - this.code = code - this.data = data - } -} diff --git a/rpc/rpc_messages.ts b/rpc/rpc_messages.ts new file mode 100644 index 000000000..0501002aa --- /dev/null +++ b/rpc/rpc_messages.ts @@ -0,0 +1,73 @@ +export interface RpcEgressMessage extends RpcBaseMessage { + id: number + method: string + params: unknown[] +} +export namespace RpcEgressMessage { + export function fmt(id: number, method: string, params: unknown[]) { + const message: RpcEgressMessage = { + jsonrpc: "2.0", + id, + method, + params, + } + return JSON.stringify(message) + } +} + +export type RpcIngressMessage = RpcOkMessage | RpcErrorMessage | RpcNotificationMessage + +export type RpcCallMessage = RpcOkMessage | RpcErrorMessage +export type RpcSubscriptionMessage = RpcNotificationMessage | RpcErrorMessage + +export type RpcSubscriptionHandler = (message: RpcSubscriptionMessage) => void + +export interface RpcOkMessage extends RpcBaseMessage { + id: number + result: unknown + params?: never + error?: never +} + +export interface RpcErrorMessage extends RpcBaseMessage { + id: number + error: { + code: number + message: string + data: unknown + } + params?: never + result?: never +} + +export interface RpcNotificationMessage extends RpcBaseMessage { + method: string + id?: never + params: { + subscription: string + result: unknown + } + result?: never + error?: never +} + +interface RpcBaseMessage { + jsonrpc: "2.0" +} + +export class ConnectionError extends Error { + override readonly name = "ConnectionError" +} + +export class ServerError extends Error { + override readonly name = "ServerError" + code + data + + // TODO: accept init `EgressMessage`? + constructor({ error: { code, data, message } }: RpcErrorMessage) { + super(message, { cause: message }) + this.code = code + this.data = data + } +} diff --git a/rpc/smoldot.ts b/rpc/smoldot.ts index a77526042..8a440f235 100644 --- a/rpc/smoldot.ts +++ b/rpc/smoldot.ts @@ -2,7 +2,7 @@ import { start } from "../deps/smoldot.ts" import { Client, ClientOptions } from "../deps/smoldot/client.d.ts" import { deferred } from "../deps/std/async.ts" import { Connection } from "./Connection.ts" -import { RpcEgressMessage, RpcMessageId } from "./rpc_common.ts" +import { RpcEgressMessage } from "./rpc_messages.ts" // TODO: fix the many possible race conditions @@ -14,7 +14,7 @@ export interface SmoldotRpcConnProps { let client: undefined | Client export class SmoldotConnection extends Connection { - chainPending + smoldotChainPending listening stopListening @@ -33,20 +33,20 @@ export class SmoldotConnection extends Connection { disableJsonRpc: true, }) const { parachainSpec } = props - this.chainPending = (async () => { + this.smoldotChainPending = (async () => { return client.addChain({ chainSpec: parachainSpec, potentialRelayChains: [await relayChain], }) })() - } else this.chainPending = client.addChain({ chainSpec: props.relayChainSpec }) + } else this.smoldotChainPending = client.addChain({ chainSpec: props.relayChainSpec }) this.listening = deferred() this.stopListening = () => this.listening.resolve() this.startListening() } async startListening() { - const chain = await this.chainPending + const chain = await this.smoldotChainPending while (true) { try { const response = await Promise.race([ @@ -60,16 +60,16 @@ export class SmoldotConnection extends Connection { } async ready() { - await this.chainPending + await this.smoldotChainPending } - send(id: RpcMessageId, method: string, params: unknown[]) { - this.chainPending + send(id: number, method: string, params: unknown[]) { + this.smoldotChainPending .then((chain) => chain.sendJsonRpc(RpcEgressMessage.fmt(id, method, params))) } close() { this.stopListening() - this.chainPending.then((chain) => chain.remove()) + this.smoldotChainPending.then((chain) => chain.remove()) } } diff --git a/rpc/ws.ts b/rpc/ws.ts index ebca2e376..79ebaf1b6 100644 --- a/rpc/ws.ts +++ b/rpc/ws.ts @@ -1,33 +1,33 @@ import { Connection } from "./Connection.ts" -import { RpcClientError, RpcEgressMessage, RpcMessageId } from "./rpc_common.ts" +import { ConnectionError, RpcEgressMessage } from "./rpc_messages.ts" export class WsConnection extends Connection { - chain + ws constructor(readonly url: string) { super() - this.chain = new WebSocket(url) - this.chain.addEventListener("message", (e) => this.handle(JSON.parse(e.data))) - this.chain.addEventListener("error", (e) => { // TODO: recovery + this.ws = new WebSocket(url) + this.ws.addEventListener("message", (e) => this.handle(JSON.parse(e.data))) + this.ws.addEventListener("error", (e) => { // TODO: recovery console.log(e) Deno.exit(1) }) } async ready() { - switch (this.chain.readyState) { + switch (this.ws.readyState) { case WebSocket.OPEN: return case WebSocket.CONNECTING: { try { return await new Promise((resolve, reject) => { const controller = new AbortController() - this.chain.addEventListener("open", () => { + this.ws.addEventListener("open", () => { controller.abort() resolve() }, controller) - this.chain.addEventListener("close", throw_, controller) - this.chain.addEventListener("error", throw_, controller) + this.ws.addEventListener("close", throw_, controller) + this.ws.addEventListener("error", throw_, controller) function throw_() { controller.abort() @@ -35,21 +35,21 @@ export class WsConnection extends Connection { } }) } catch (_e) { - throw new RpcClientError() + throw new ConnectionError() } } case WebSocket.CLOSING: case WebSocket.CLOSED: { - throw new RpcClientError() + throw new ConnectionError("WebSocket already closed") } } } - send(id: RpcMessageId, method: string, params: unknown[]) { - this.chain.send(RpcEgressMessage.fmt(id, method, params)) + send(id: number, method: string, params: unknown[]) { + this.ws.send(RpcEgressMessage.fmt(id, method, params)) } close() { - this.chain.close() + this.ws.close() } } From 3940ec625fa840d37efaf51d51dcee7cec259b07 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Mon, 20 Feb 2023 17:44:06 -0500 Subject: [PATCH 21/23] rpc handler cleanup --- rpc/Connection.ts | 52 ++++++++++++++++------------------------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/rpc/Connection.ts b/rpc/Connection.ts index 5815d811d..18a846ca2 100644 --- a/rpc/Connection.ts +++ b/rpc/Connection.ts @@ -61,51 +61,33 @@ export abstract class Connection { return await pending } - subscriptionInitPendings: Record = {} subscriptionHandlers: Record = {} - subscriptionIdByRpcMessageId: Record = {} - subscription( + async subscription( subscribe: string, unsubscribe: string, params: unknown[], handler: RpcSubscriptionHandler, signal: AbortSignal, ) { - ;(async () => { - await this.ready() - const subscribeId = this.nextId++ - this.subscriptionInitPendings[subscribeId] = handler as RpcSubscriptionHandler - signal.addEventListener("abort", () => { - delete this.subscriptionInitPendings[subscribeId] - const subscriptionId = this.subscriptionIdByRpcMessageId[subscribeId] - if (subscriptionId) { - delete this.subscriptionIdByRpcMessageId[subscribeId] - this.send(this.nextId++, unsubscribe, [subscriptionId]) - } - }) - this.send(subscribeId, subscribe, params) - })() + const message = await this.call(subscribe, params) + if (signal.aborted) return + if (message.error) return handler(message) + const subscriptionId = message.result as string + this.subscriptionHandlers[subscriptionId] = handler + signal.addEventListener("abort", () => { + delete this.subscriptionHandlers[subscriptionId] + this.send(this.nextId++, unsubscribe, [subscriptionId]) + }) } handle(message: RpcIngressMessage) { if (typeof message.id === "number") { - const callResultPending = this.callResultPendings[message.id] - if (callResultPending) { - callResultPending.resolve(message) - delete this.callResultPendings[message.id] - return - } - const subscriptionPending = this.subscriptionInitPendings[message.id] - if (subscriptionPending) { - if (message.error) subscriptionPending(message) - else { - const subscriptionId = message.result as string - this.subscriptionHandlers[subscriptionId] = subscriptionPending - this.subscriptionIdByRpcMessageId[message.id] = subscriptionId - } - delete this.subscriptionInitPendings[message.id] - } - } else if (message.params) this.subscriptionHandlers[message.params.subscription]?.(message) - else throw new Error(Deno.inspect(message)) // ... for now + this.callResultPendings[message.id]?.resolve(message) + delete this.callResultPendings[message.id] + } else if (message.params) { + this.subscriptionHandlers[message.params.subscription]?.(message) + } else { + throw new Error(Deno.inspect(message)) // ... for now + } } } From b6d09d7ecf9201f123d5ca375d216b998054460d Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Mon, 20 Feb 2023 17:53:42 -0500 Subject: [PATCH 22/23] misc cleanup --- examples/xcm_asset_teleportation.ts | 10 ++++------ mod.ts | 2 +- patterns/mod.ts | 4 ---- 3 files changed, 5 insertions(+), 11 deletions(-) delete mode 100644 patterns/mod.ts diff --git a/examples/xcm_asset_teleportation.ts b/examples/xcm_asset_teleportation.ts index c88021f8a..1824fd3c2 100644 --- a/examples/xcm_asset_teleportation.ts +++ b/examples/xcm_asset_teleportation.ts @@ -60,13 +60,11 @@ const initiatedEvent = XcmPallet const processedEvent = System.Events .entry([], chain.latestBlock.hash) - .into(ValueRune) .map((events) => - events - ?.find((e) => - CollatorRuntimeEvent.isParachainSystem(e.event) - && ParachainSystemEvent.isDownwardMessagesProcessed(e.event.value) - ) + events?.find((e) => + CollatorRuntimeEvent.isParachainSystem(e.event) + && ParachainSystemEvent.isDownwardMessagesProcessed(e.event.value) + ) ) .filter((event) => !!event) .log("Processed events:") diff --git a/mod.ts b/mod.ts index 9ccd2460f..dc1cf042f 100644 --- a/mod.ts +++ b/mod.ts @@ -20,7 +20,7 @@ export { ss58, } from "./util/mod.ts" -// moderate --exclude deps frame_metadata main.ts patterns providers server util +// moderate --exclude frame_metadata main.ts providers server util export * from "./fluent/mod.ts" export * from "./primitives/mod.ts" diff --git a/patterns/mod.ts b/patterns/mod.ts deleted file mode 100644 index 78e009325..000000000 --- a/patterns/mod.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * as consensus from "./consensus/mod.ts" -export * as ink from "./ink/mod.ts" -export * from "./multisigAccountId.ts" -export * from "./MultisigRune.ts" From 48fd8e9f4d342008272b477bdd443ad472f3f84d Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Mon, 20 Feb 2023 17:54:34 -0500 Subject: [PATCH 23/23] misc cleanup --- examples/xcm_asset_teleportation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/xcm_asset_teleportation.ts b/examples/xcm_asset_teleportation.ts index 1824fd3c2..96b9800c9 100644 --- a/examples/xcm_asset_teleportation.ts +++ b/examples/xcm_asset_teleportation.ts @@ -1,4 +1,4 @@ -import { alice, Rune, ValueRune } from "capi" +import { alice, Rune } from "capi" import { types, XcmPallet } from "zombienet/statemine.toml/alice/@latest/mod.ts" import { Event as XcmPalletEvent } from "zombienet/statemine.toml/alice/@latest/types/pallet_xcm/pallet.ts" import { RuntimeEvent as AliceRuntimeEvent } from "zombienet/statemine.toml/alice/@latest/types/rococo_runtime/mod.ts"