diff --git a/.github/workflows/deno_deploy.yml b/.github/workflows/deno_deploy.yml new file mode 100644 index 000000000..96042459b --- /dev/null +++ b/.github/workflows/deno_deploy.yml @@ -0,0 +1,19 @@ +name: Deno Deploy +on: push +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v3 + - uses: denoland/setup-deno@9db7f66e8e16b5699a514448ce994936c63f0d54 # v1.1.0 + with: + deno-version: v1.x + - run: deno task run _tasks/gen_deploy.ts + - name: Deploy to Deno Deploy + uses: denoland/deployctl@v1 + with: + project: capi-dev + entrypoint: target/deploy.ts diff --git a/.github/workflows/example.yml b/.github/workflows/example.yml index 7c1a9932e..90706efbd 100644 --- a/.github/workflows/example.yml +++ b/.github/workflows/example.yml @@ -31,4 +31,5 @@ jobs: key: ${{ runner.os }}-deno-${{ hashFiles('lock.json', 'deps/**/*.ts') }} - name: Setup Polkadot uses: ./.github/actions/setup-polkadot + - run: deno task codegen - run: deno task run ${{ matrix.example_path }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 243c27b3c..9a6e3d48c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,5 +12,6 @@ jobs: - name: Setup polkadot uses: ./.github/actions/setup-polkadot - run: deno task lint + - run: deno task codegen - run: deno task star - run: deno task test diff --git a/.vscode/settings.json b/.vscode/settings.json index 3e05a2fd4..b4a3c7251 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,14 @@ }, "deno.codeLens.testArgs": ["--no-check=remote", "-A", "-L=info"], "deno.config": "./deno.jsonc", + "deno.importMap": "./import_map_cache.json", + "deno.suggest.imports.hosts": { + "https://deno.land": true, + "https://x.nest.land": true, + "https://crux.land": true, + "http://localhost:5646": true, + "https://capi.dev": true + }, "deno.enable": true, "deno.lint": true, "editor.defaultFormatter": "dprint.dprint", diff --git a/_tasks/codegen.ts b/_tasks/codegen.ts new file mode 100644 index 000000000..e31f6a1d1 --- /dev/null +++ b/_tasks/codegen.ts @@ -0,0 +1,21 @@ +import { LocalCapiCodegenServer } from "../codegen/server/local.ts" +import * as fs from "../deps/std/fs.ts" + +await fs.emptyDir("target/codegen/generated") +const port = 5646 +const server = new LocalCapiCodegenServer() +server.listen(port) + +await Deno.run({ + cmd: [ + "deno", + "cache", + "--no-lock", + "--import-map", + "import_map_localhost.json", + `--reload=http://localhost:${port}/`, + "examples/mod.ts", + ], +}).status() + +server.abortController.abort() diff --git a/_tasks/gen_deploy.ts b/_tasks/gen_deploy.ts new file mode 100644 index 000000000..d5dab965b --- /dev/null +++ b/_tasks/gen_deploy.ts @@ -0,0 +1,18 @@ +import { getModuleIndex, getSha } from "../codegen/server/git_utils.ts" +import { ensureDir } from "../deps/std/fs.ts" + +const sha = await getSha() +const index = await getModuleIndex() + +await ensureDir("target") +await Deno.writeTextFile( + "target/deploy.ts", + ` +import { DenoDeployCodegenServer } from "../codegen/server/deploy.ts" + +new DenoDeployCodegenServer( + "sha:${sha}", + ${JSON.stringify(index)}, +).listen(80) +`, +) diff --git a/codegen.ts b/codegen.ts deleted file mode 100644 index 8311a9117..000000000 --- a/codegen.ts +++ /dev/null @@ -1,61 +0,0 @@ -// TODO: prettier messaging & help screens -import { codegen } from "./codegen/mod.ts" -import { parse } from "./deps/std/flags.ts" -import * as C from "./mod.ts" -import * as T from "./test_util/mod.ts" -import * as U from "./util/mod.ts" - -const args = parse(Deno.args, { - string: ["src", "out", "import", "dev"], - boolean: ["help"], - default: { - import: "https://deno.land/x/capi/mod.ts", - }, - alias: { - src: ["s"], - dev: ["d"], - out: ["o"], - help: ["h", "?"], - }, -}) - -if (args.help) help() - -if (!args.out) { - throw new Error("Must specify `out`") -} - -let metadata: C.M.Metadata -if (args.src && args.dev) { - throw Error("Cannot specify both `src` and `dev`") -} else if (args.src) { - if (args.src.endsWith(".scale")) { - metadata = C.M.fromPrefixedHex(await Deno.readTextFile(args.src)) - } else { - const client = C.rpcClient(C.rpc.proxyProvider, args.src) - metadata = U.throwIfError(await C.metadata(client)().run()) - } -} else if (args.dev) { - if (!T.isRuntimeName(args.dev)) { - throw new T.InvalidRuntimeSpecifiedError(args.dev) - } - const client = T[args.dev as T.RuntimeName] - metadata = U.throwIfError(await C.metadata(client)().run()) -} else { - throw new Error("Please specify either `src` or `dev`") -} -await run(metadata, args.out) - -function run(metadata: C.M.Metadata, out: string) { - return codegen({ - importSpecifier: args.import, - metadata, - }) - .write(out) -} - -// TODO: do we handle help differently depending on what flags were specified? -function help(): never { - console.log("Usage: codegen -s= -o=") - Deno.exit() -} diff --git a/codegen/Files.ts b/codegen/Files.ts index e97704823..8a512b325 100644 --- a/codegen/Files.ts +++ b/codegen/Files.ts @@ -1,32 +1,9 @@ import { tsFormatter } from "../deps/dprint.ts" -import * as path from "../deps/std/path.ts" -import { S } from "./utils.ts" -export type File = { getContent: () => S } -export class Files extends Map { - async write(outDir: string) { - const errors = [] - try { - await Deno.remove(outDir, { recursive: true }) - } catch (e) { - if (!(e instanceof Deno.errors.NotFound)) { - throw e - } - } - await Deno.mkdir(outDir, { recursive: true }) - for (const [relativePath, file] of this.entries()) { - const outputPath = path.join(outDir, relativePath) - const content = S.toString(file.getContent()) - try { - const formatted = tsFormatter.formatText("gen.ts", content) - await Deno.writeTextFile(outputPath, formatted) - } catch (e) { - await Deno.writeTextFile(outputPath, content) - errors.push(e) - } - } - if (errors.length) { - throw errors - } +export class Files extends Map string> { + getFormatted(key: string) { + const file = this.get(key) + if (!file) return undefined + return tsFormatter.formatText(key, file()) } } diff --git a/codegen/codecVisitor.ts b/codegen/codecVisitor.ts deleted file mode 100644 index 19006e97c..000000000 --- a/codegen/codecVisitor.ts +++ /dev/null @@ -1,185 +0,0 @@ -import * as M from "../frame_metadata/mod.ts" -import { Files } from "./Files.ts" -import { CodegenProps } from "./mod.ts" -import { Decl, getCodecPath, getName, getRawCodecPath, S } from "./utils.ts" - -export function createCodecVisitor( - props: CodegenProps, - decls: Decl[], - typeVisitor: M.TyVisitor, - files: Files, -) { - const { tys } = props.metadata - const namespaceImports = new Set() - const codecs: S[] = [] - files.set("codecs.ts", { - getContent: () => [ - "\n", - [ - "import { ChainError, BitSequence, Era, $, $era, $null } from", - S.string(props.importSpecifier), - ], - [`import type * as t from "./mod.ts"`], - ...codecs, - [ - "export const _all: $.AnyCodec[] =", - S.array(props.metadata.tys.map((ty) => getName(getRawCodecPath(ty)))), - ], - ], - }) - - return new M.TyVisitor(tys, { - unitStruct(ty) { - return addCodecDecl(ty, "$null") - }, - wrapperStruct(ty, inner) { - return addCodecDecl(ty, this.visit(inner)) - }, - tupleStruct(ty, members) { - return addCodecDecl(ty, ["$.tuple(", members.map((x) => [this.visit(x), ","]), ")"]) - }, - objectStruct(ty) { - return addCodecDecl( - ty, - [ - "$.object(", - ty.fields.map( - (x) => [S.array([S.string(x.name!), this.visit(x.ty)]), ","], - ), - ")", - ], - ) - }, - option(ty, some) { - return addCodecDecl(ty, ["$.option(", this.visit(some), ")"]) - }, - result(ty, ok, err) { - return addCodecDecl(ty, ["$.result(", this.visit(ok), ",", [ - "$.instance(ChainError<", - fixType(typeVisitor.visit(err)), - `>, ["value", `, - this.visit(err), - "])", - ], ")"]) - }, - never(ty) { - return addCodecDecl(ty, "$.never") - }, - stringUnion(ty) { - return addCodecDecl(ty, [ - "$.stringUnion(", - S.object(...ty.members.map((x): [S, S] => [x.index, S.string(x.name)])), - ")", - ]) - }, - taggedUnion(ty) { - return addCodecDecl( - ty, - [ - `$.taggedUnion("type",`, - S.object( - ...ty.members.map(({ fields, name: type, index }): [S, S] => { - let props: S[] - if (fields.length === 0) { - props = [] - } else if (fields[0]!.name === undefined) { - // Tuple variant - const value = fields.length === 1 - ? this.visit(fields[0]!.ty) - : ["$.tuple(", fields.map((f) => [this.visit(f.ty), ","]), ")"] - props = [S.array([S.string("value"), value])] - } else { - // Object variant - props = fields.map((field) => - S.array([ - S.string(field.name!), - this.visit(field.ty), - ]) - ) - } - return [index, S.array([S.string(type), ...props])] - }), - ), - ")", - ], - ) - }, - uint8Array(ty) { - return addCodecDecl(ty, "$.uint8Array") - }, - array(ty) { - return addCodecDecl(ty, ["$.array(", this.visit(ty.typeParam), ")"]) - }, - sizedUint8Array(ty) { - return addCodecDecl(ty, `$.sizedUint8Array(${ty.len})`) - }, - sizedArray(ty) { - return addCodecDecl(ty, ["$.sizedArray(", this.visit(ty.typeParam), ",", ty.len, ")"]) - }, - primitive(ty) { - return addCodecDecl(ty, getCodecPath(tys, ty)!) - }, - compact(ty) { - return addCodecDecl(ty, ["$.compact(", this.visit(ty.typeParam), ")"]) - }, - bitSequence(ty) { - return addCodecDecl(ty, "$.bitSequence") - }, - map(ty, key, val) { - return addCodecDecl(ty, ["$.map(", this.visit(key), ",", this.visit(val), ")"]) - }, - set(ty, val) { - return addCodecDecl(ty, ["$.set(", this.visit(val), ")"]) - }, - era(ty) { - return addCodecDecl(ty, "$era") - }, - lenPrefixedWrapper(ty, inner) { - return addCodecDecl(ty, ["$.lenPrefixed(", this.visit(inner), ")"]) - }, - circular(ty) { - return ["$.deferred(() =>", getName(getRawCodecPath(ty)), ")"] - }, - }) - - function addCodecDecl(ty: M.Ty, value: S) { - const rawPath = getRawCodecPath(ty) - if (ty.path.length > 1) { - namespaceImports.add(ty.path[0]!) - } - codecs.push([ - ["export const", getName(rawPath)], - ": $.Codec<", - fixType(typeVisitor.visit(ty)), - "> =", - value, - ]) - const path = getCodecPath(tys, ty) - // Deduplicate -- metadata has redundant entries (e.g. pallet_collective::RawOrigin) - if (path !== rawPath && path !== value && !decls.some((x) => x.path === path)) { - decls.push({ - path, - code: [ - ["export const", getName(path)], - ": $.Codec<", - typeVisitor.visit(ty), - "> =", - rawPath, - ], - }) - } - return getName(rawPath) - } - - /** - * Prefix generated types with `t.` - * e.g. `[Compact, foo.Bar, Uint8Array]` -> `[t.Compact, t.foo.Bar, Uint8Array]` - */ - function fixType(type: S) { - return S.toString(type).replace( - // Matches paths (`a.b.c`) that either contain a `.`, or are a number type (either `u123` or `Compact`) - /\b([\w\$]+\.[\w\.$]+|u\d+|Compact)\b/g, - (x) => "t." + x, - ) - } -} diff --git a/codegen/codecVisitor.test.ts b/codegen/codecs.test.ts similarity index 59% rename from codegen/codecVisitor.test.ts rename to codegen/codecs.test.ts index f569be4ad..20f99048a 100644 --- a/codegen/codecVisitor.test.ts +++ b/codegen/codecs.test.ts @@ -1,27 +1,29 @@ import { Codec } from "../deps/scale.ts" -import * as path from "../deps/std/path.ts" import { assertEquals } from "../deps/std/testing/asserts.ts" import * as M from "../frame_metadata/mod.ts" -import * as C from "../mod.ts" import * as testClients from "../test_util/clients/mod.ts" -import * as U from "../util/mod.ts" -import { codegen } from "./mod.ts" +import { InMemoryCache } from "./server/cache.ts" +import { LocalCapiCodegenServer } from "./server/local.ts" +import { highlighterPromise } from "./server/server.ts" -const currentDir = path.dirname(path.fromFileUrl(import.meta.url)) -const codegenTestDir = path.join(currentDir, "../target/codegen") +await highlighterPromise -for (const [runtime, client] of Object.entries(testClients)) { +for (const runtime of Object.keys(testClients)) { Deno.test(runtime, async () => { - const metadata = U.throwIfError(await C.metadata(client)().run()) - const outDir = path.join(codegenTestDir, runtime) - await codegen({ - importSpecifier: "../../../mod.ts", - metadata, - }).write(outDir) - const codegened = await import(path.toFileUrl(path.join(outDir, "mod.ts")).toString()) + let port: number + const server = new LocalCapiCodegenServer() + server.cache = new InMemoryCache(server.abortController.signal) + server.listen(0, (x) => port = x.port) + const chainUrl = `dev:${runtime}` + const version = await server.latestChainVersion(chainUrl) + const metadata = await server.metadata(chainUrl, version) + const codegened = await import( + `http://localhost:${port!}/@local/proxy/${chainUrl}/@${version}/codecs.ts` + ) + server.abortController.abort() const deriveCodec = M.DeriveCodec(metadata.tys) const derivedCodecs = metadata.tys.map(deriveCodec) - const codegenCodecs = codegened._metadata.types + const codegenCodecs = codegened._all const origInspect = Codec.prototype["_inspect"]! let inspecting = 0 Codec.prototype["_inspect"] = function(inspect) { diff --git a/codegen/genCodecs.ts b/codegen/genCodecs.ts new file mode 100644 index 000000000..f1f3dafe1 --- /dev/null +++ b/codegen/genCodecs.ts @@ -0,0 +1,151 @@ +import * as M from "../frame_metadata/mod.ts" +import { normalizeCase } from "../util/case.ts" +import { CodegenProps } from "./mod.ts" +import { S } from "./utils.ts" + +export function genCodecs(props: CodegenProps, typeVisitor: M.TyVisitor) { + const { tys } = props.metadata + const namespaceImports = new Set() + + let file = `\ +import { $, C } from "./capi.ts" +import type * as types from "./types/mod.ts" + +` + + const visitor = new M.TyVisitor(tys, { + unitStruct(ty) { + return addCodecDecl(ty, "C.$null") + }, + wrapperStruct(ty, inner) { + return addCodecDecl(ty, this.visit(inner)) + }, + tupleStruct(ty, members) { + return addCodecDecl(ty, `$.tuple(${members.map((x) => this.visit(x)).join(", ")})`) + }, + objectStruct(ty) { + return addCodecDecl( + ty, + `$.object(${ + ty.fields.map((x) => + S.array([ + S.string(normalizeCase(x.name!)), + this.visit(x.ty), + ]) + ).join(", ") + })`, + ) + }, + option(ty, some) { + return addCodecDecl(ty, `$.option(${this.visit(some)})`) + }, + result(ty, ok, err) { + return addCodecDecl( + ty, + `$.result(${ + this.visit(ok) + }, $.instance(C.ChainError<$.Native>, ["value", ${this.visit(err)}]))`, + ) + }, + never(ty) { + return addCodecDecl(ty, "$.never") + }, + stringUnion(ty) { + return addCodecDecl( + ty, + `$.stringUnion(${ + S.object( + ...ty.members.map(( + x, + ): [string, string] => [`${x.index}`, S.string(normalizeCase(x.name))]), + ) + })`, + ) + }, + taggedUnion(ty) { + return addCodecDecl( + ty, + `$.taggedUnion("type", ${ + S.object( + ...ty.members.map(({ fields, name, index }): [string, string] => { + const type = normalizeCase(name) + let props: string[] + if (fields.length === 0) { + props = [] + } else if (fields[0]!.name === undefined) { + // Tuple variant + const value = fields.length === 1 + ? this.visit(fields[0]!.ty) + : `$.tuple(${fields.map((f) => this.visit(f.ty)).join(", ")})` + props = [S.array([S.string("value"), value])] + } else { + // Object variant + props = fields.map((field) => + S.array([ + S.string(normalizeCase(field.name!)), + this.visit(field.ty), + ]) + ) + } + return [`${index}`, S.array([S.string(type), ...props])] + }), + ) + })`, + ) + }, + uint8Array(ty) { + return addCodecDecl(ty, "$.uint8Array") + }, + array(ty) { + return addCodecDecl(ty, `$.array(${this.visit(ty.typeParam)})`) + }, + sizedUint8Array(ty) { + return addCodecDecl(ty, `$.sizedUint8Array(${ty.len})`) + }, + sizedArray(ty) { + return addCodecDecl(ty, `$.sizedArray(${this.visit(ty.typeParam)}, ${ty.len})`) + }, + primitive(ty) { + return addCodecDecl(ty, ty.kind === "char" ? "$.str" : "$." + ty.kind) + }, + compact(ty) { + return addCodecDecl(ty, `$.compact(${this.visit(ty.typeParam)})`) + }, + bitSequence(ty) { + return addCodecDecl(ty, "$.bitSequence") + }, + map(ty, key, val) { + return addCodecDecl(ty, `$.map(${this.visit(key)}, ${this.visit(val)})`) + }, + set(ty, val) { + return addCodecDecl(ty, `$.set(${this.visit(val)})`) + }, + era(ty) { + return addCodecDecl(ty, "C.$era") + }, + lenPrefixedWrapper(ty, inner) { + return addCodecDecl(ty, `$.lenPrefixed(${this.visit(inner)})`) + }, + circular(ty) { + return `$.deferred(() => $${ty.id})` + }, + }) + + for (const ty of props.metadata.tys) { + visitor.visit(ty) + } + + file += `export const _all: $.AnyCodec[] = ${ + S.array(props.metadata.tys.map((ty) => `$${ty.id}`)) + }` + + return file + + function addCodecDecl(ty: M.Ty, value: string) { + if (ty.path.length > 1) { + namespaceImports.add(ty.path[0]!) + } + file += `export const $${ty.id}: $.Codec<${typeVisitor.visit(ty)}> = ${value}\n\n` + return `$${ty.id}` + } +} diff --git a/codegen/genMetadata.ts b/codegen/genMetadata.ts index 3aa11981c..1b232e734 100644 --- a/codegen/genMetadata.ts +++ b/codegen/genMetadata.ts @@ -1,7 +1,14 @@ import * as M from "../frame_metadata/mod.ts" -import { Decl, getPath, getRawCodecPath, makeDocComment, S } from "./utils.ts" +import { hex } from "../mod.ts" +import { normalizeCase } from "../util/case.ts" +import { Files } from "./Files.ts" +import { getRawCodecPath, makeDocComment, S } from "./utils.ts" -export function genMetadata(metadata: M.Metadata, decls: Decl[]) { +export function genMetadata( + metadata: M.Metadata, + typeVisitor: M.TyVisitor, + files: Files, +) { const { tys, extrinsic, pallets } = metadata const isUnitVisitor = new M.TyVisitor(tys, { @@ -25,80 +32,103 @@ export function genMetadata(metadata: M.Metadata, decls: Decl[]) { circular: () => false, }) - decls.push({ - path: "_metadata.extrinsic", - code: [ - "export const extrinsic =", - S.object( - ["version", extrinsic.version], - ["extras", getExtrasCodec(extrinsic.signedExtensions.map((x) => [x.ident, x.ty]))], - [ - "additional", - getExtrasCodec(extrinsic.signedExtensions.map((x) => [x.ident, x.additionalSigned])), - ], - ), - ], - }) + const { + signature: signatureTy, + call: callTy, + address: addressTy, + } = Object.fromEntries( + extrinsic.ty.params.map((x) => [x.name.toLowerCase(), x.ty]), + ) + for (const pallet of pallets) { - for (const entry of pallet.storage?.entries ?? []) { - decls.push({ - path: `${pallet.name}.${entry.name}`, - code: [ - makeDocComment(entry.docs), - `export const ${entry.name} =`, - S.object( - ["type", S.string(entry.type)], - ["modifier", S.string(entry.modifier)], - [ - "hashers", - entry.type === "Map" ? JSON.stringify(entry.hashers) : "[]", - ], - [ - "key", + files.set(`pallets/${pallet.name}.ts`, () => { + const items = [ + `\ +import type * as types from "../types/mod.ts" +import * as codecs from "../codecs.ts" +import { $, C, client } from "../capi.ts" +`, + ] + for (const entry of pallet.storage?.entries ?? []) { + items.push( + makeDocComment(entry.docs) + + `export const ${entry.name} = new C.fluent.Storage(${[ + "client", + S.string(entry.type), + S.string(entry.modifier), + S.string(pallet.name), + S.string(entry.name), entry.type === "Map" ? entry.hashers.length === 1 - ? ["$.tuple(", getRawCodecPath(entry.key), ")"] + ? `$.tuple(${getRawCodecPath(entry.key)})` : getRawCodecPath(entry.key) - : "[]", - ], - ["value", getRawCodecPath(entry.value)], - ), - ], - }) - } - if (pallet.calls) { - const ty = pallet.calls as M.Ty & M.UnionTyDef - const isStringUnion = ty.members.every((x) => !x.fields.length) - for (const call of ty.members) { - const typeName = isStringUnion ? S.string(call.name) : getPath(tys, ty)! + "." + call.name - const [params, data]: [S, S] = call.fields.length - ? call.fields[0]!.name - ? [`value: Omit<${typeName}, "type">`, ["{ ...value, type:", S.string(call.name), "}"]] - : [[call.fields.length > 1 ? "..." : "", `value: ${typeName}["value"]`], [ - "{ ...value, type:", - S.string(call.name), - "}", - ]] - : ["", isStringUnion ? S.string(call.name) : S.object(["type", S.string(call.name)])] - decls.push({ - path: `${pallet.name}.${call.name}`, - code: [ - makeDocComment(call.docs), - "export function", - call.name, - ["(", params, ")"], - [":", typeName], - ["{ return", data, "}"], - ], - }) + : "$.tuple()", + getRawCodecPath(entry.value), + ]})`, + ) + } + if (pallet.calls) { + const ty = pallet.calls as M.Ty & M.UnionTyDef + const isStringUnion = ty.members.every((x) => !x.fields.length) + for (const call of ty.members) { + const type = normalizeCase(call.name) + const typeName = typeVisitor.visit(ty)! + "." + type + const [params, data]: [string, string] = call.fields.length + ? call.fields[0]!.name + ? [`value: Omit<${typeName}, "type">`, `{ ...value, type: ${S.string(type)} }`] + : [ + `${call.fields.length > 1 ? "..." : ""}value: ${typeName}["value"]`, + `{ ...value, type: ${S.string(type)} }`, + ] + : ["", isStringUnion ? S.string(type) : S.object(["type", S.string(type)])] + items.push( + makeDocComment(call.docs) + + `export function ${type}(${params}): ${typeVisitor.visit(callTy!)}` + + `{ return { type: ${S.string(pallet.name)}, value: ${data} } }`, + ) + } } - } + for (const constant of pallet.constants) { + items.push( + makeDocComment(constant.docs) + + `export const ${constant.name}: ${ + typeVisitor.visit(constant.ty) + } = codecs.$${constant.ty.id}.decode(C.hex.decode(${ + S.string(hex.encode(constant.value)) + } as C.Hex))`, + ) + constant.value + } + return items.join("\n\n") + }) } - decls.push({ - path: "_metadata.types", - code: "export const types = _codec._all", - }) + files.set( + "pallets/mod.ts", + () => + pallets.map((x) => x.name).sort().map((x) => `export * as ${x} from "./${x}.ts"`).join("\n"), + ) + + files.set("extrinsic.ts", () => ` +import { $, C, client } from "./capi.ts" +import * as codecs from "./codecs.ts" +import type * as types from "./types/mod.ts" + +const _extrinsic = ${ + S.object( + ["version", `${extrinsic.version}`], + ["extras", getExtrasCodec(extrinsic.signedExtensions.map((x) => [x.ident, x.ty]))], + [ + "additional", + getExtrasCodec(extrinsic.signedExtensions.map((x) => [x.ident, x.additionalSigned])), + ], + ["call", getRawCodecPath(callTy!)], + ["address", getRawCodecPath(addressTy!)], + ["signature", getRawCodecPath(signatureTy!)], + ) + } +export const extrinsic = C.extrinsic(client); +`) function getExtrasCodec(xs: [string, M.Ty][]) { return S.array( diff --git a/codegen/mod.ts b/codegen/mod.ts index b87efad97..bf41e99e1 100644 --- a/codegen/mod.ts +++ b/codegen/mod.ts @@ -1,39 +1,43 @@ import * as M from "../frame_metadata/mod.ts" -import { createCodecVisitor } from "./codecVisitor.ts" import { Files } from "./Files.ts" +import { genCodecs } from "./genCodecs.ts" import { genMetadata } from "./genMetadata.ts" import { createTypeVisitor } from "./typeVisitor.ts" -import { Decl, printDecls, S } from "./utils.ts" +import { S } from "./utils.ts" export interface CodegenProps { metadata: M.Metadata importSpecifier: string + clientDecl: string } export function codegen(props: CodegenProps): Files { - const decls: Decl[] = [] const files = new Files() - decls.push({ - path: "_", - code: [ - "\n", - [ - "import { ChainError, BitSequence, Era, $ } from", - S.string(props.importSpecifier), - ], - [`import * as _codec from "./codecs.ts"`], - [`export { _metadata }`], - ], - }) - const typeVisitor = createTypeVisitor(props, decls) - const codecVisitor = createCodecVisitor(props, decls, typeVisitor, files) - for (const ty of props.metadata.tys) { - typeVisitor.visit(ty) - codecVisitor.visit(ty) - } - genMetadata(props.metadata, decls) - files.set("mod.ts", { - getContent: () => printDecls(decls), - }) + + const typeVisitor = createTypeVisitor(props, files) + + files.set("codecs.ts", () => genCodecs(props, typeVisitor)) + + genMetadata(props.metadata, typeVisitor, files) + + files.set( + "capi.ts", + () => + `\ +export { $ } from ${S.string(props.importSpecifier)} +export * as C from ${S.string(props.importSpecifier)} +${props.clientDecl} +`, + ) + + files.set("mod.ts", () => + `\ +export * as pallets from "./pallets/mod.ts" +export * as types from "./types/mod.ts" +export * as codecs from "./codecs.ts" + +export * from "./extrinsic.ts" +`) + return files } diff --git a/codegen/server/cache.ts b/codegen/server/cache.ts new file mode 100644 index 000000000..29e5d1b81 --- /dev/null +++ b/codegen/server/cache.ts @@ -0,0 +1,82 @@ +import * as $ from "../../deps/scale.ts" +import * as fs from "../../deps/std/fs.ts" +import * as path from "../../deps/std/path.ts" +import { getOrInit, PermanentMemo, TimedMemo, WeakMemo } from "../../util/mod.ts" + +export abstract class Cache { + constructor(readonly signal: AbortSignal) { + this.stringMemo = new TimedMemo(-1, this.signal) + } + + abstract _getRaw(key: string, init: () => Promise): Promise + + rawMemo = new WeakMemo() + getRaw(key: string, init: () => Promise): Promise { + return this.rawMemo.run(key, () => this._getRaw(key, init)) + } + + decodedMemo = new Map<$.AnyCodec, WeakMemo>() + get(key: string, $value: $.Codec, init: () => Promise): Promise { + const memo = getOrInit(this.decodedMemo, $value, () => new WeakMemo()) as WeakMemo + return memo.run(key, async () => { + let value: T | undefined + const raw = await this.getRaw(key, async () => $value.encode(value = await init())) + value ??= $value.decode(raw) + return value + }) + } + + stringMemo + getString(key: string, ttl: number, init: () => Promise): Promise { + return this.stringMemo.run(key, async () => { + let value: string | undefined + const raw = await this.getRaw(key, async () => new TextEncoder().encode(value = await init())) + value ??= new TextDecoder().decode(raw) + return value + }, ttl) + } + + abstract _list(prefix: string): Promise + + listMemo = new WeakMemo() + list(prefix: string) { + return this.listMemo.run(prefix, () => this._list(prefix)) + } +} + +export class FsCache extends Cache { + constructor(readonly location: string, signal: AbortSignal) { + super(signal) + } + + async _getRaw(key: string, init: () => Promise) { + const file = path.join(this.location, key) + try { + return await Deno.readFile(file) + } catch (e) { + if (!(e instanceof Deno.errors.NotFound)) throw e + const content = await init() + await fs.ensureDir(path.dirname(file)) + await Deno.writeFile(file, content) + return content + } + } + + async _list(prefix: string): Promise { + const result = [] + for await (const entry of Deno.readDir(path.join(this.location, prefix))) { + result.push(entry.name) + } + return result + } +} + +export class InMemoryCache extends Cache { + memo = new PermanentMemo() + async _getRaw(key: string, init: () => Promise): Promise { + return this.memo.run(key, init) + } + async _list(): Promise { + throw new Error("unimplemented") + } +} diff --git a/codegen/server/capi_repo.ts b/codegen/server/capi_repo.ts new file mode 100644 index 000000000..c056f13f6 --- /dev/null +++ b/codegen/server/capi_repo.ts @@ -0,0 +1,96 @@ +import { PermanentMemo, TimedMemo } from "../../util/memo.ts" +import { SHA_ABBREV_LENGTH } from "./git_utils.ts" +import { CodegenServer } from "./server.ts" + +const TAGS_TTL = 60_000 // 1 minute +const BRANCHES_TTL = 60_000 // 1 minute + +export const GITHUB_API_REPO = "https://api.github.com/repos/paritytech/capi" + +export const R_TAG_VERSION = /^v?(\d+\.\d+\.\d+[^\/]*)$/ +export const R_REF_VERSION = /^ref:([^\/]*)$/ +export const R_SHA_VERSION = /^sha:([0-9a-f]+)$/ + +export abstract class CapiCodegenServer extends CodegenServer { + async normalizeVersion(version: string) { + const tagMatch = R_TAG_VERSION.exec(version) + if (tagMatch) return "v" + tagMatch[1] + if (R_REF_VERSION.test(version)) { + const shaVersion = (await this.branches())[version] + if (!shaVersion) throw this.e404() + return shaVersion + } + const shaMatch = R_SHA_VERSION.exec(version) + if (shaMatch) { + const sha = await this.fullSha(shaMatch[1]!) + return `sha:${sha.slice(0, SHA_ABBREV_LENGTH)}` + } + throw this.e404() + } + + moduleFileUrl(version: string, path: string) { + if (this.local && version === this.version) { + return new URL("../.." + path, import.meta.url).toString() + } + if (R_REF_VERSION.test(version)) { + return `https://deno.land/x/capi@${version}${path}` + } + const shaMatch = R_SHA_VERSION.exec(version) + if (shaMatch) { + const [, sha] = shaMatch + return `https://raw.githubusercontent.com/paritytech/capi/${sha}${path}` + } + throw new Error("expected normalized version") + } + + async versionSuggestions(): Promise { + return [ + ...new Set((await Promise.all([ + this.local ? [this.version] : [], + this.tags(), + this.branches().then(Object.keys), + ])).flat()), + ] + } + + tagsMemo = new TimedMemo(TAGS_TTL, this.abortController.signal) + tags() { + return this.tagsMemo.run(null, async () => { + return (await json("https://apiland.deno.dev/completions/items/capi/")).items + }) + } + + branchesMemo = new TimedMemo>( + BRANCHES_TTL, + this.abortController.signal, + ) + branches() { + return this.branchesMemo.run(null, async () => { + const refs: GithubRef[] = await json( + `${GITHUB_API_REPO}/git/matching-refs/heads`, + ) + return Object.fromEntries(refs.map((ref) => [ + `ref:${ref.ref.slice("refs/heads/".length).replace(/\//g, ":")}`, + `sha:${ref.object.sha.slice(0, SHA_ABBREV_LENGTH)}`, + ])) + }) + } + + fullShaMemo = new PermanentMemo() + fullSha(sha: string) { + return this.fullShaMemo.run(sha, async () => { + return (await json(`${GITHUB_API_REPO}/commits/${sha}`)).sha + }) + } +} + +export async function json(url: string) { + const response = await fetch(url) + if (!response.ok) throw new Error(`${url}: invalid response`) + return await response.json() +} + +export interface GithubRef { + ref: string + object: { sha: string } +} diff --git a/codegen/server/deploy.ts b/codegen/server/deploy.ts new file mode 100644 index 000000000..7a61edd5a --- /dev/null +++ b/codegen/server/deploy.ts @@ -0,0 +1,84 @@ +import { PermanentMemo } from "../../util/memo.ts" +import { + CapiCodegenServer, + GITHUB_API_REPO, + GithubRef, + json, + R_REF_VERSION, + R_SHA_VERSION, +} from "./capi_repo.ts" +import { S3Cache } from "./s3.ts" + +const DENO_DEPLOY_USER_ID = 75045203 + +const PRODUCTION_HOST = "capi.dev" + +export class DenoDeployCodegenServer extends CapiCodegenServer { + moduleIndex + cache = new S3Cache({ + accessKeyID: Deno.env.get("S3_ACCESS_KEY")!, + secretKey: Deno.env.get("S3_SECRET_KEY")!, + region: Deno.env.get("S3_REGION")!, + bucket: Deno.env.get("S3_BUCKET")!, + }, this.abortController.signal) + local = false + + constructor(readonly version: string, moduleIndex: string[]) { + super() + this.moduleIndex = async () => moduleIndex + } + + async defaultVersion(request: Request) { + if (new URL(request.url).host === PRODUCTION_HOST) { + return (await this.tags())[0]! + } + return this.version + } + + deploymentUrlMemo = new PermanentMemo() + async deploymentUrl(version: string) { + const fullSha = await this.versionSha(version) + return this.deploymentUrlMemo.run(fullSha, async () => { + const deployments: GithubDeployment[] = await json( + `${GITHUB_API_REPO}/deployments?sha=${fullSha}`, + ) + const deployment = deployments.find((x) => x.creator.id === DENO_DEPLOY_USER_ID) + if (!deployment) throw this.e404() + const statuses: GithubStatus[] = await json(deployment.statuses_url) + const url = statuses.map((x) => x.environment_url).find((x) => x) + if (!url) throw this.e404() + return url + }) + } + + async versionSha(version: string) { + if (R_REF_VERSION.test(version)) { + return this.tagSha(version) + } + const shaMatch = R_SHA_VERSION.exec(version) + if (shaMatch) { + return await this.fullSha(shaMatch[1]!) + } + throw new Error("expected normalized version") + } + + tagShaMemo = new PermanentMemo() + tagSha(tag: string) { + return this.fullShaMemo.run(tag, async () => { + const refs: GithubRef[] = await json( + `${GITHUB_API_REPO}/git/matching-refs/tags/${tag}`, + ) + if (!refs[0]) throw this.e404() + return refs[0].object.sha + }) + } +} + +interface GithubDeployment { + creator: { id: number } + statuses_url: string +} + +interface GithubStatus { + environment_url?: string +} diff --git a/codegen/server/git_utils.ts b/codegen/server/git_utils.ts new file mode 100644 index 000000000..f81beec1f --- /dev/null +++ b/codegen/server/git_utils.ts @@ -0,0 +1,21 @@ +export const SHA_ABBREV_LENGTH = 8 + +export async function getModuleIndex() { + const cmd = Deno.run({ + cmd: ["git", "ls-files"], + stdout: "piped", + }) + if (!(await cmd.status()).success) throw new Error("git ls-files failed") + const output = new TextDecoder().decode(await cmd.output()) + return output.split("\n").filter((x) => x.endsWith(".ts")) +} + +export async function getSha() { + const cmd = Deno.run({ + cmd: ["git", "rev-parse", "@"], + stdout: "piped", + }) + if (!(await cmd.status()).success) throw new Error("git rev-parse failed") + const output = new TextDecoder().decode(await cmd.output()) + return output.slice(0, SHA_ABBREV_LENGTH) +} diff --git a/codegen/server/local.ts b/codegen/server/local.ts new file mode 100644 index 000000000..2081c49fc --- /dev/null +++ b/codegen/server/local.ts @@ -0,0 +1,59 @@ +import { PermanentMemo } from "../../util/memo.ts" +import { Cache, FsCache } from "./cache.ts" +import { CapiCodegenServer } from "./capi_repo.ts" +import { getModuleIndex } from "./git_utils.ts" + +const R_DENO_LAND_URL = /^https:\/\/deno\.land\/x\/capi@(v[^\/]+)\// +const R_GITHUB_URL = /^https:\/\/raw\.githubusercontent\.com\/paritytech\/capi\/([0-9a-f]+)\// + +export class LocalCapiCodegenServer extends CapiCodegenServer { + version + cache: Cache = new FsCache("target/codegen", this.abortController.signal) + local = true + + constructor(version?: string) { + super() + this.version = version ?? this.detectVersion() + } + + detectVersion() { + const url = import.meta.url + if (url.startsWith("file://")) return "local" + const denoMatch = R_DENO_LAND_URL.exec(url) + if (denoMatch) { + const [, version] = denoMatch + return version! + } + const githubMatch = R_GITHUB_URL.exec(url) + if (githubMatch) { + const [, sha] = githubMatch + return `sha:${sha}` + } + throw new Error("Could not detect version from url " + url) + } + + async defaultVersion() { + return this.version + } + + deploymentUrlMemo = new PermanentMemo() + async deploymentUrl(version: string) { + return this.deploymentUrlMemo.run(version, async () => { + const mod = await import(this.moduleFileUrl(version, "/codegen/server/local.ts")) + const Server = mod.LocalCapiCodegenServer as typeof LocalCapiCodegenServer + const server = new Server(version) + this.abortController.signal.addEventListener("abort", () => { + server.abortController.abort() + }) + let port: number + server.listen(0, (x) => port = x.port) + return `http://localhost:${port!}` + }) + } + + moduleIndex = getModuleIndex +} + +if (import.meta.main) { + new LocalCapiCodegenServer().listen(5646) +} diff --git a/codegen/server/s3.ts b/codegen/server/s3.ts new file mode 100644 index 000000000..6bd4556c5 --- /dev/null +++ b/codegen/server/s3.ts @@ -0,0 +1,25 @@ +import { S3Bucket, S3BucketConfig } from "https://deno.land/x/s3@0.5.0/mod.ts" +import { Cache } from "./cache.ts" + +export class S3Cache extends Cache { + bucket + constructor(config: S3BucketConfig, signal: AbortSignal) { + super(signal) + this.bucket = new S3Bucket(config) + } + + async _getRaw(key: string, init: () => Promise) { + const result = await this.bucket.getObject(key) + if (result) { + return new Uint8Array(await new Response(result.body).arrayBuffer()) + } + const value = await init() + await this.bucket.putObject(key, value) + return value + } + + async _list(prefix: string): Promise { + const result = await this.bucket.listObjects({ prefix }) + return result?.contents?.map((object) => object.key!.slice(prefix.length)) ?? [] + } +} diff --git a/codegen/server/server.ts b/codegen/server/server.ts new file mode 100644 index 000000000..df7296280 --- /dev/null +++ b/codegen/server/server.ts @@ -0,0 +1,460 @@ +import { serve } from "https://deno.land/std@0.165.0/http/server.ts" +import { escapeHtml } from "https://deno.land/x/escape@1.4.2/mod.ts" +import * as shiki from "https://esm.sh/shiki@0.11.1?bundle" +import * as $ from "../../deps/scale.ts" +import * as C from "../../mod.ts" +import * as T from "../../test_util/mod.ts" +import * as U from "../../util/mod.ts" +import { TimedMemo } from "../../util/mod.ts" +import { Files } from "../Files.ts" +import { codegen } from "../mod.ts" +import { Cache } from "./cache.ts" + +shiki.setCDN("https://unpkg.com/shiki/") +export const highlighterPromise = shiki.getHighlighter({ theme: "github-dark", langs: ["ts"] }) + +const LOCAL_CHAINS = [ + "dev:polkadot", + "dev:westend", + "dev:rococo", + "dev:kusama", +] + +const SUGGESTED_CHAIN_URLS = [ + "wss:rpc.polkadot.io", + "wss:kusama-rpc.polkadot.io", + // "wss://acala-polkadot.api.onfinality.io/public-ws/", + "wss:rococo-contracts-rpc.polkadot.io", + "wss:wss.api.moonbeam.network", + "wss:statemint-rpc.polkadot.io", + "wss:para.subsocial.network", + "wss:westend-rpc.polkadot.io", +] + +const LATEST_CHAIN_VERSION_TTL = 600_000 // 10 minutes +const CHAIN_FILE_TTL = 60_000 // 1 minute +const RENDERED_HTML_TTL = 60_000 // 1 minute + +const R_WITH_CAPI_VERSION = /^\/@([^\/]+)(\/.*)?$/ +const R_WITH_CHAIN_URL = /^\/proxy\/(dev:\w+|wss?:[^\/]+)\/(?:@([^\/]+)\/)?(.*)$/ + +const $index = $.array($.str) + +export abstract class CodegenServer { + abstract version: string + abstract cache: Cache + abstract local: boolean + abstract moduleIndex(): Promise + abstract defaultVersion(request: Request): Promise + abstract normalizeVersion(version: string): Promise + abstract deploymentUrl(version: string): Promise + abstract versionSuggestions(): Promise + abstract moduleFileUrl(version: string, path: string): string + + abortController = new AbortController() + + listen( + port: number, + onListen?: (params: { + hostname: string + port: number + }) => void, + ) { + return serve((req) => + this.root(req).catch((e) => { + if (e instanceof Response) return e + return new Response(Deno.inspect(e), { status: 500 }) + }), { port, signal: this.abortController.signal, onListen }) + } + + async root(request: Request): Promise { + const fullPath = new URL(request.url).pathname + if (fullPath === "/.well-known/deno-import-intellisense.json") { + return this.autocomplete(fullPath) + } + const versionMatch = R_WITH_CAPI_VERSION.exec(fullPath) + if (!versionMatch) { + const defaultVersion = await this.defaultVersion(request) + return this.redirect(request, `/@${defaultVersion}${fullPath}`) + } + const [, version, path] = versionMatch + if (version !== this.version) { + return this.delegateRequest(request, version!, path ?? "/") + } + if (!path) return this.redirect(request, `/@${version}/`) + if (path === "/") { + return this.landingPage() + } + if (path.startsWith("/proxy/")) { + const match = R_WITH_CHAIN_URL.exec(path) + if (!match) return this.e404() + const [, chainUrl, chainVersion, file] = match + return this.chainFile(request, chainUrl!, chainVersion, file!) + } + if (path.startsWith("/autocomplete/")) { + return this.autocomplete(path) + } + return this.moduleFile(request, path) + } + + landingPage() { + return this.html(`
capi@${this.version}
`) + } + + async chainFile( + request: Request, + chainUrl: string, + chainVersion: string | undefined, + filePath: string, + ) { + if (!chainVersion) { + const latestChainVersion = await this.latestChainVersion(chainUrl) + return this.redirect( + request, + `/@${this.version}/proxy/${chainUrl}/@${latestChainVersion}/${filePath}`, + ) + } + if (chainVersion !== this.normalizeChainVersion(chainVersion)) { + return this.redirect( + request, + `/@${this.version}/proxy/${chainUrl}/@${ + this.normalizeChainVersion(chainVersion) + }/${filePath}`, + ) + } + return this.ts(request, () => + this.cache.getString( + `generated/@${this.version}/${chainUrl}/@${chainVersion}/${filePath}`, + CHAIN_FILE_TTL, + async () => { + const files = await this.files(chainUrl, chainVersion) + const content = files.getFormatted(filePath) + if (content == null) throw this.e404() + return content + }, + )) + } + + filesMemo = new Map() + async files(chainUrl: string, chainVersion: string) { + const metadata = await this.metadata(chainUrl, chainVersion) + return U.getOrInit(this.filesMemo, metadata, () => { + return codegen({ + metadata, + clientDecl: chainUrl.startsWith("dev:") + ? ` +import { LocalClientEffect } from ${JSON.stringify(`/@${this.version}/test_util/local.ts`)} +export const client = new LocalClientEffect(${JSON.stringify(chainUrl.slice(4))}) + ` + : ` +import * as C from ${JSON.stringify(`/@${this.version}/mod.ts`)} +export const client = C.rpc.rpcClient(C.rpc.proxyProvider, ${JSON.stringify(chainUrl)}) + `, + importSpecifier: `/@${this.version}/mod.ts`, + }) + }) + } + + async chainIndex(chainUrl: string, chainVersion: string) { + return await this.cache.get( + `generated/@${this.version}/${chainUrl}/@${chainVersion}/_index`, + $index, + async () => { + const files = await this.files(chainUrl, chainVersion) + return [...files.keys()] + }, + ) + } + + async autocomplete(path: string) { + if (path === "/.well-known/deno-import-intellisense.json") { + return this.json({ + version: 2, + registries: [ + { + schema: "/:version(@[^/]*)?/:file*", + variables: [ + { key: "version", url: "/autocomplete/version" }, + { key: "file", url: "/${version}/autocomplete/moduleFile/${file}" }, + ], + }, + { + schema: + "/:version(@[^/]*)/:_proxy(proxy)/:chainUrl(dev:\\w*|wss?:[^/]*)/:chainVersion(@[^/]+)/:file*", + variables: [ + { key: "version", url: "/autocomplete/version" }, + { key: "_proxy", url: "/autocomplete/null" }, + { key: "chainUrl", url: "/${version}/autocomplete/chainUrl/${chainUrl}" }, + { + key: "chainVersion", + url: "/${version}/autocomplete/chainVersion/${chainUrl}/${chainVersion}", + }, + { + key: "file", + url: "/${version}/autocomplete/chainFile/${chainUrl}/${chainVersion}/${file}", + }, + ], + }, + ], + }) + } + const parts = path.slice(1).split("/") + if (parts[0] !== "autocomplete") return this.e404() + if (parts[1] === "null") { + return this.json({ items: [] }) + } + if (parts[1] === "version") { + const versions = await this.versionSuggestions() + const items = versions.map((v) => "@" + v) + return this.json({ items, preselect: items[0] }) + } + if (parts[1] === "chainUrl") { + return this.json({ + items: [ + ...(this.local ? LOCAL_CHAINS : []), + ...SUGGESTED_CHAIN_URLS, + ], + }) + } + if (parts[1] === "chainVersion") { + const chainUrl = parts[2]! + const latest = await this.latestChainVersion(chainUrl) + const other = await this.cache.list(`metadata/${chainUrl}/`) + const versions = [...new Set([latest, ...other])] + const items = versions.map((v) => "@" + v) + return this.json({ items, preselect: items[0] }) + } + if (parts[1] === "moduleFile") { + if (parts[2] === "proxy" || parts[2]?.startsWith("@")) return this.json({ items: [] }) + const index = await this.moduleIndex() + const result = this.autocompleteIndex(index, parts.slice(2)) + if (!parts[3]) { + result.items.unshift("proxy/") + result.preselect = "proxy/" + } + return this.json(result) + } + if (parts[1] === "chainFile") { + const chainUrl = parts[2]! + const chainVersion = parts[3]?.slice(1) || await this.latestChainVersion(chainUrl) + const index = await this.chainIndex(chainUrl, chainVersion) + return this.json(this.autocompleteIndex(index, parts.slice(4))) + } + return this.e404() + } + + autocompleteIndex(index: string[], partial: string[]) { + let dir = partial.join("/") + "/" + let result + while (true) { + if (dir === "/") dir = "" + result = [ + ...new Set( + index.filter((x) => x.startsWith(dir)).map((x) => + dir + x.slice(dir.length).replace(/\/.*$/, "/") + ), + ), + ].sort((a, b) => + (+(b === dir + "mod.ts") - +(a === dir + "mod.ts")) + || (+b.endsWith("/") - +a.endsWith("/")) + || (a < b ? -1 : 1) + ) + if (!result.length && dir) { + dir = dir.replace(/[^\/]+\/$/, "") + continue + } + break + } + return { items: result, isIncomplete: true, preselect: dir + "mod.ts" } + } + + json(body: unknown) { + return new Response(JSON.stringify(body), { + headers: { + "Content-Type": "application/json", + }, + }) + } + + acceptsHtml(request: Request) { + return request.headers.get("Accept")?.split(",").includes("text/html") ?? false + } + + async ts( + request: Request, + body: () => Promise, + path = new URL(request.url).pathname, + ) { + if (!this.acceptsHtml(request)) { + return new Response(await body(), { + headers: { + "Content-Type": "application/typescript", + }, + }) + } + const html = await this.cache.getString( + `rendered/${this.version}/${path}.html`, + RENDERED_HTML_TTL, + async () => { + const content = await body() + const highlighter = await highlighterPromise + const tokens = highlighter.codeToThemedTokens(content, "ts") + let codeContent = "" + for (const line of tokens) { + codeContent += `` + for (const token of line) { + if ( + token.explanation?.every((value) => + value.scopes.some((scope) => + scope.scopeName === "meta.export.ts" || scope.scopeName === "meta.import.ts" + ) + && value.scopes.some((scope) => scope.scopeName === "string.quoted.double.ts") + ) + ) { + codeContent += `${escapeHtml(token.content)}` + } else { + codeContent += `${ + escapeHtml(token.content) + }` + } + } + codeContent += "\n" + } + return `\ + + +

${path}

+
${codeContent}
+ +` + }, + ) + return this.html(html) + } + + html(html: string) { + return new Response(html, { + headers: { + "Content-Type": "text/html", + }, + }) + } + + e404() { + return new Response("404", { status: 404 }) + } + + async redirect(request: Request, path: string) { + if (path.endsWith(".ts") && this.acceptsHtml(request)) { + if (path.startsWith("/")) { + return this.root(new Request(new URL(path, request.url), { headers: request.headers })) + } + return this.ts(request, async () => { + const response = await fetch(path) + if (!response.ok) throw response + return await response.text() + }, path) + } + if (path.startsWith("file://")) { + return await fetch(path) + } + return new Response(null, { + status: 302, + headers: { + Location: path, + }, + }) + } + + latestChainVersionMemo = new TimedMemo( + LATEST_CHAIN_VERSION_TTL, + this.abortController.signal, + ) + async latestChainVersion(chainUrl: string) { + return this.latestChainVersionMemo.run(chainUrl, async () => { + const client = this.client(chainUrl) + const chainVersion = U.throwIfError( + await C.rpcCall("system_version")(client)().as().next(this.normalizeChainVersion) + .run(), + ) + return chainVersion + }) + } + + metadata(chainUrl: string, version: string) { + return this.cache.get(`metadata/${chainUrl}/${version}`, C.M.$metadata, async () => { + const client = this.client(chainUrl) + const [chainVersion, metadata] = U.throwIfError( + await C.Z.ls( + C.rpcCall("system_version")(client)().as().next(this.normalizeChainVersion), + C.metadata(client)(), + ).run(), + ) + if (this.normalizeChainVersion(version) !== chainVersion) { + console.log(version, chainVersion) + throw new Error("Outdated version") + } + return metadata + }) + } + + client(chainUrl: string) { + if (chainUrl.startsWith("dev:")) { + if (!this.local) throw new Error("Dev chains are not supported") + const runtime = chainUrl.slice("dev:".length) + if (!T.isRuntimeName(runtime)) { + throw new T.InvalidRuntimeSpecifiedError(runtime) + } + return T[runtime] + } else { + return C.rpcClient(C.rpc.proxyProvider, chainUrl.replace(/:/, "://")) + } + } + + normalizeChainVersion(version: string) { + if (!version.startsWith("v")) version = "v" + version + if (version.includes("-")) version = version.split("-")[0]! + return version + } + + async moduleFile(request: Request, path: string) { + return this.redirect(request, this.moduleFileUrl(this.version, path)) + } + + async delegateRequest(request: Request, version: string, path: string): Promise { + const normalizedVersion = await this.normalizeVersion(version) + if (normalizedVersion !== version) { + return this.redirect(request, `/@${normalizedVersion}${path}`) + } + const url = await this.deploymentUrl(version) + return await fetch(new URL(new URL(request.url).pathname, url), { headers: request.headers }) + } +} diff --git a/codegen/typeVisitor.ts b/codegen/typeVisitor.ts index 12467b222..75f850955 100644 --- a/codegen/typeVisitor.ts +++ b/codegen/typeVisitor.ts @@ -1,147 +1,334 @@ +import { posix as pathPosix } from "../deps/std/path.ts" import * as M from "../frame_metadata/mod.ts" +import { normalizeCase } from "../util/case.ts" +import { getOrInit } from "../util/map.ts" +import { Files } from "./Files.ts" import { CodegenProps } from "./mod.ts" -import { Decl, getName, getPath, makeDocComment, S } from "./utils.ts" +import { makeDocComment, S } from "./utils.ts" -export function createTypeVisitor(props: CodegenProps, decls: Decl[]) { +class TypeFile { + reexports = new Set() + types = new Map() + get ext() { + return this.reexports.size ? "/mod.ts" : ".ts" + } +} + +export function createTypeVisitor(props: CodegenProps, files: Files) { const { tys } = props.metadata - return new M.TyVisitor(tys, { - unitStruct(ty) { - return addTypeDecl(ty, "null") + const paths = new Map() + const typeFiles = new Map() + addPath("types.Compact", { type: "Compact" } as M.Ty) + + const visitor = new M.TyVisitor(tys, { + unitStruct(_ty) { + return "null" }, - wrapperStruct(ty, inner) { - if (ty.path[0] === "Cow") return this.visit(inner) - return addTypeDecl(ty, this.visit(inner)) + wrapperStruct(_ty, inner) { + return this.visit(inner) }, - tupleStruct(ty, members) { - return addTypeDecl(ty, S.array(members.map((x) => this.visit(x)))) + tupleStruct(_ty, members) { + return S.array(members.map((x) => this.visit(x))) }, objectStruct(ty) { - return addInterfaceDecl( - ty, - S.object( - ...ty.fields.map( - (x) => [makeDocComment(x.docs), x.name!, this.visit(x.ty)] as const, - ), + return S.object( + ...ty.fields.map( + (x) => [makeDocComment(x.docs), normalizeCase(x.name!), this.visit(x.ty)] as const, ), ) }, option(_ty, some) { - return [this.visit(some), "| undefined"] + return `${this.visit(some)} | undefined` }, result(_ty, ok, err) { - return [this.visit(ok), "|", ["ChainError<", this.visit(err), ">"]] + return `${this.visit(ok)} | C.ChainError<${this.visit(err)}>` }, - never(ty) { - return addTypeDecl(ty, "never") + never() { + return "never" }, stringUnion(ty) { - return addTypeDecl(ty, [ty.members.map((x) => ["|", S.string(x.name)])]) - }, - taggedUnion(ty) { - const path = getPath(tys, ty)! - const name = getName(path) - decls.push({ - path, - code: [ - makeDocComment(ty.docs), - ["export type", name, "="], - ty.members.map(({ fields, name: type, docs }) => { - let props: [comment: S, name: S, type: S][] - if (fields.length === 0) { - props = [] - } else if (fields[0]!.name === undefined) { - // Tuple variant - const value = fields.length === 1 - ? this.visit(fields[0]!.ty) - : S.array(fields.map((f) => this.visit(f.ty))) - props = [["", "value", value]] - } else { - // Object variant - props = fields.map((field, i) => [ - makeDocComment(field.docs), - field.name || i, - this.visit(field.ty), - ]) - } - decls.push({ - path: path + "." + type, - code: [ - makeDocComment(docs), - ["export interface", type], - S.object( - ["type", S.string(type)], - ...props, - ), - ], - }) - return ["|", path, ".", type] - }), - ], - }) - return path + return ty.members.map((x) => S.string(normalizeCase(x.name))).join(" | ") }, - uint8Array(ty) { - return addTypeDecl(ty, "Uint8Array") + taggedUnion: undefined!, + uint8Array() { + return "Uint8Array" }, array(ty) { - return addTypeDecl(ty, ["Array<", this.visit(ty.typeParam), ">"]) + return `Array<${this.visit(ty.typeParam)}>` }, - sizedUint8Array(ty) { - return addTypeDecl(ty, "Uint8Array") // TODO: consider `& { length: L }` + sizedUint8Array() { + return "Uint8Array" // TODO: consider `& { length: L }` }, sizedArray(ty) { - return addTypeDecl(ty, S.array(Array(ty.len).fill(this.visit(ty.typeParam)))) + return S.array(Array(ty.len).fill(this.visit(ty.typeParam))) }, primitive(ty) { - if (ty.kind === "char") return addTypeDecl(ty, "string") + if (ty.kind === "char") return "string" if (ty.kind === "bool") return "boolean" if (ty.kind === "str") return "string" - if (+ty.kind.slice(1) < 64) return addTypeDecl(ty, "number") - return addTypeDecl(ty, "bigint") + if (+ty.kind.slice(1) < 64) return "number" + return "bigint" }, compact(ty) { - decls.push({ path: "Compact", code: "export type Compact = T" }) - return ["Compact<", this.visit(ty.typeParam), ">"] + return `types.Compact<${this.visit(ty.typeParam)}>` }, - bitSequence(ty) { - return addTypeDecl(ty, "BitSequence") + bitSequence() { + return "$.BitSequence" }, map(_ty, key, val) { - return ["Map<", this.visit(key), ",", this.visit(val), ">"] + return `Map<${this.visit(key)}, ${this.visit(val)}>` }, set(_ty, val) { - return ["Set<", this.visit(val), ">"] + return `Set<${this.visit(val)}>` }, era() { - return "Era" + return "C.Era" }, lenPrefixedWrapper(_ty, inner) { return this.visit(inner) }, circular(ty) { - return getPath(tys, ty) || this._visit(ty) + return getPath(ty) ?? this._visit(ty) + }, + all(ty) { + return getPath(ty) ?? undefined }, }) - function addTypeDecl(ty: M.Ty, value: S) { - const path = getPath(tys, ty) - if (path && path !== value) { - decls.push({ - path, - code: [makeDocComment(ty.docs), ["export type", getName(path)], "=", value], - }) + for (const ty of tys) { + visitor.visit(ty) + } + + for (const [path, typeFile] of typeFiles) { + const filePath = path + typeFile.ext + files.set(filePath, () => { + let file = "" + if (path !== "types") { + file += `import type * as types from ${S.string(importPath(filePath, "types/mod.ts"))}\n` + } + file += `import * as codecs from ${S.string(importPath(filePath, "codecs.ts"))}\n` + file += `import { $, C } from ${S.string(importPath(filePath, "capi.ts"))}\n` + file += "\n" + for (const reexport of [...typeFile.reexports].sort()) { + const otherFile = typeFiles.get(path + "/" + reexport)! + file += `export * as ${reexport} from "./${reexport}${otherFile.ext}"\n` + } + file += "\n" + for ( + const [path, ty] of [...typeFile.types].sort((a, b) => + a[0] > b[0] ? 1 : a[0] < b[0] ? -1 : 0 + ) + ) { + file += createTypeDecl(path, ty) + "\n\n" + } + return file + }) + } + + return visitor + + function getPath(ty: M.Ty): string | null { + return getOrInit(paths, ty, () => { + if ( + ty.type === "Struct" && ty.fields.length === 1 && ty.params.some((x) => x.ty !== undefined) + ) { + return null + } + let path = _getPath(ty) + if (path) { + path = "types." + path + addPath(path, ty) + } + return path + }) + + function _getPath(ty: M.Ty): string | null { + if (ty.type === "Primitive") { + return (ty.kind === "bool" || ty.kind === "str" ? null : ty.kind) + } + if (ty.type === "Compact") { + return null + } + if ( + [ + "Option", + "Result", + "Cow", + "BTreeMap", + "BTreeSet", + "Era", + "WrapperOpaque", + "WrapperKeepOpaque", + ].includes(ty.path.at(-1)!) + ) { + return null + } + const baseName = ty.path.join(".") + if (!baseName) return null + return baseName + ty.params.map((p, i) => { + if ( + p.ty === undefined || tys.every((x) => + x.path.length !== ty.path.length + || !x.path.every((x, i) => x === ty.path[i]) + || x.params[i]!.ty === p.ty + ) + ) { + return "" + } + const x = _getPath(p.ty) + if (x === null) throw new Error("was null") + return ".$$" + x + }).join("") } - return path || value } - function addInterfaceDecl(ty: M.Ty, value: S) { - const path = getPath(tys, ty) - if (path && path !== value) { - decls.push({ - path, - code: [makeDocComment(ty.docs), ["export interface", getName(path)], value], - }) + function addPath(path: string, ty: M.Ty) { + let pair = split(path.replace(/\./g, "/")) + if (!pair) throw new Error("addPath called with orphan") + getOrInit(typeFiles, pair[0], () => new TypeFile()).types.set(path, ty) + while ((pair = split(pair[0]))) { + getOrInit(typeFiles, pair[0], () => new TypeFile()).reexports.add(pair[1]) + } + + function split(path: string): [string, string] | null { + const i = path.lastIndexOf("/") + if (i === -1) return null + return [path.slice(0, i), path.slice(i + 1)] } - return path || value } + + function createTypeDecl(path: string, ty: M.Ty) { + const name = path.slice(path.lastIndexOf(".") + 1) + const docs = makeDocComment(ty.docs) + + const fallback = (key: keyof M.TyVisitorMethods) => + (...args: any) => { + return `\ +${docs} +export type ${name} = ${(visitor[key] as any)!(...args)} +` + } + + const codec = ty.type === "Compact" ? "" : `\ +export const $${name[0]!.toLowerCase()}${name.slice(1)}: $.Codec<${ + ty.type === "Primitive" ? name : path + }> = codecs.$${ty.id} +` + + return codec + new M.TyVisitor(tys, { + unitStruct() { + return `\ +${docs} +export type ${name} = null +${docs} +export function ${name}(){ return null } +` + }, + wrapperStruct(ty, inner) { + return `\ +${docs} +export type ${name} = ${visitor.wrapperStruct(ty, inner)} +${docs} +export function ${name}(value: ${path}){ return value } +` + }, + tupleStruct(ty, members) { + return `\ +${docs} +export type ${name} = ${visitor.tupleStruct(ty, members)} +${docs} +export function ${name}(...value: ${path}){ return value } +` + }, + objectStruct(ty) { + return `\ +${docs} +export interface ${name} ${visitor.objectStruct(ty)} +${docs} +export function ${name}(value: ${path}){ return value } +` + }, + option: null!, + result: null!, + never: fallback("never"), + stringUnion: fallback("stringUnion"), + taggedUnion(ty) { + const factories: string[] = [] + const types: string[] = [] + const union: string[] = [] + for (const { fields, name, docs } of ty.members) { + const type = normalizeCase(name) + const memberPath = path + "." + type + let props: [comment: string, name: string, type: string][] + let factory: [params: string, result: string] + if (fields.length === 0) { + props = [] + factory = ["", ""] + } else if (fields[0]!.name === undefined) { + // Tuple variant + const value = fields.length === 1 + ? visitor.visit(fields[0]!.ty) + : S.array(fields.map((f) => visitor.visit(f.ty))) + props = [["", "value", value]] + factory = [ + `${fields.length === 1 ? "" : "..."}value: ${memberPath}["value"]`, + "value", + ] + } else { + // Object variant + props = fields.map((field) => [ + makeDocComment(field.docs), + normalizeCase(field.name!), + visitor.visit(field.ty), + ]) + factory = [`value: Omit<${memberPath}, "type">`, "...value"] + } + factories.push( + makeDocComment(docs) + + `export function ${type} (${factory[0]}): ${memberPath}` + + `{ return { type: ${S.string(type)}, ${factory[1]} } }`, + ) + types.push( + makeDocComment(docs) + + `export interface ${type}` + + S.object( + ["type", S.string(type)], + ...props, + ), + ) + union.push(`| ${memberPath}`) + } + return `\ +${docs} +export type ${name} = ${union.join(" ")} +export namespace ${name} { ${ + [ + ...types, + ...factories, + ].join("\n") + } } +` + }, + uint8Array: fallback("uint8Array"), + array: fallback("array"), + sizedUint8Array: fallback("sizedUint8Array"), + sizedArray: fallback("sizedArray"), + primitive: fallback("primitive"), + compact() { + return `export type Compact = T\n` + }, + bitSequence: fallback("bitSequence"), + map: fallback("map"), + set: fallback("set"), + era: null!, + lenPrefixedWrapper: null!, + circular: null!, + }).visit(ty).trim() + } +} + +function importPath(from: string, to: string) { + let path = pathPosix.relative(pathPosix.dirname("/" + from), "/" + to) + if (!path.startsWith(".")) path = "./" + path + return path } diff --git a/codegen/utils.ts b/codegen/utils.ts index 25899b89d..86e80c49c 100644 --- a/codegen/utils.ts +++ b/codegen/utils.ts @@ -1,112 +1,27 @@ import * as M from "../frame_metadata/mod.ts" -export type S = string | number | S[] - export namespace S { - export function array(items: S[]): S { - return ["[", items.map((x) => [x, ","]), "]"] + export function array(items: string[]): string { + return `[${items}]` } export function object( - ...items: (readonly [doc: S, prop: S, val: S] | readonly [prop: S, val: S])[] - ): S { - return ["{", items.map((x) => [x.slice(0, -1), ":", x.at(-1)!, ","]), "}"] + ...items: + (readonly [doc: string, prop: string, val: string] | readonly [prop: string, val: string])[] + ): string { + return `{${items.map((x) => [...x.slice(0, -1), ":", x.at(-1)!].join(""))}}` } - export function string(value: string): S { + export function string(value: string): string { return JSON.stringify(value) } - export function toString(value: S): string { - if (!(value instanceof Array)) return value.toString() - const parts = value.map(S.toString) - return parts.map((x) => x.trim()).join(parts.some((x) => x.includes("\n")) ? "\n" : " ").trim() - } -} - -export type Decl = { path: string; code: S } - -export function getPath(tys: M.Ty[], ty: M.Ty): string | null { - if (ty.type === "Struct" && ty.fields.length === 1 && ty.params.length) return null - return _getName(ty) - - function _getName(ty: M.Ty): string | null { - if (ty.type === "Primitive") { - return ty.kind - } - if (ty.type === "Compact") { - return null - } - if (ty.path.at(-1) === "Era") return "Era" - if (["Option", "Result", "Cow", "BTreeMap", "BTreeSet"].includes(ty.path[0]!)) return null - const baseName = ty.path.join(".") - if (!baseName) return null - return baseName + ty.params.map((p, i) => { - if (p.ty === undefined) return "" - if (tys.every((x) => x.path.join(".") !== baseName || x.params[i]!.ty === p.ty)) { - return "" - } - return ".$$" + (_getName(p.ty) ?? p.ty) - }).join("") - } -} - -export function getName(path: string) { - return path.split(".").at(-1)! } -export function makeDocComment(docs: string[]) { +export function makeDocComment(docs: string[] = []) { docs = docs.map((x) => x.replace(/^\s*\n\s*|\s*\n\s*$/, "").replace(/\s*\n\s*/g, " ")) if (!docs.length) return "" if (docs.length === 1) return `/** ${docs[0]!.trim()} */\n` - return `/**\n * ${docs.join("\n * ")}\n */` + return `/**\n * ${docs.join("\n * ")}\n */\n` } export function getRawCodecPath(ty: M.Ty) { - return `_codec.$${ty.id}` -} - -export function getCodecPath(tys: M.Ty[], ty: M.Ty) { - if (ty.type === "Primitive") { - return ty.kind === "char" ? "$.str" : "$." + ty.kind - } - const path = getPath(tys, ty) - if (path === null) return getRawCodecPath(ty) - const parts = path.split(".") - return [ - ...parts.slice(0, -1), - "$" + parts.at(-1)![0]!.toLowerCase() + parts.at(-1)!.slice(1), - ].join(".") -} - -export function printDecls(decls: Decl[]) { - const namespaces: Record = {} - const done: Decl[] = [] - for (const { path, code } of decls) { - if (path.includes(".")) { - const [ns, ...rest] = path.split(".") - ;(namespaces[ns!] ??= []).push({ path: rest.join("."), code }) - } else { - done.push({ path, code }) - } - } - for (const ns in namespaces) { - done.push({ - path: ns, - code: [ - [ns.startsWith("_") ? "" : "export", "namespace", ns, "{"], - printDecls(namespaces[ns]!), - "}", - ], - }) - } - // sort by path, _s first - done.sort((a, b) => - a.path.startsWith("_") !== b.path.startsWith("_") - ? a.path.startsWith("_") ? -1 : 1 - : a.path < b.path - ? -1 - : a.path > b.path - ? 1 - : 0 - ) - // Deduplicate -- metadata has redundant entries (e.g. pallet_collective::RawOrigin) - return [...new Set(done.map((x) => S.toString(x.code)))].join("\n") + return `codecs.$${ty.id}` } diff --git a/compat/mod.ts b/compat/mod.ts deleted file mode 100644 index 5d64e843c..000000000 --- a/compat/mod.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./pjs.ts" diff --git a/compat/pjs.ts b/compat/pjs.ts deleted file mode 100644 index 688663efc..000000000 --- a/compat/pjs.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { KeyringPair } from "../deps/polkadot/keyring/types.ts" -import { MultiAddress, Signature, Signer } from "../frame_metadata/Extrinsic.ts" - -export function multiAddressFromKeypair(keypair: KeyringPair): MultiAddress { - return MultiAddress.fromId(keypair.publicKey) -} - -export function signerFromKeypair(keypair: KeyringPair): Signer { - const type = ((): Signature["type"] => { - switch (keypair.type) { - case "sr25519": { - return "Sr25519" - } - case "ed25519": { - return "Ed25519" - } - default: { - // TODO - return null! - } - } - })() - return (message) => ({ - type, - value: keypair.sign(message), - }) -} diff --git a/cspell.json b/cspell.json index cc1958a38..856043707 100644 --- a/cspell.json +++ b/cspell.json @@ -16,6 +16,7 @@ "deno.lock", "frame_metadata/raw_erc20_metadata.json", "target", - "**/__snapshots__/*.snap" + "**/__snapshots__/*.snap", + "codegen/_output" ] } diff --git a/deno.jsonc b/deno.jsonc index ee0957da3..180068858 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -9,7 +9,7 @@ }, "lint": { "files": { - "exclude": ["target"], + "exclude": ["target", "codegen/_output"], "include": ["."] }, "rules": { @@ -18,25 +18,29 @@ "no-explicit-any", "no-namespace", "no-empty", - "no-extra-semi" + "no-extra-semi", + "ban-types", + "require-await" ], "tags": ["recommended"] } }, "include": ["."], "tasks": { - "run": "deno run -A --no-lock", + "run": "deno run -A --no-lock --import-map=import_map_cache.json", "run:browser": "deno task run test_util/ctx.ts -- deno task run _tasks/run_browser.ts", "debug": "deno task run --inspect-brk", "download:frame_metadata": "deno task run _tasks/download_frame_metadata.ts", "udd": "deno task star && deno task run https://deno.land/x/udd@0.5.0/main.ts target/star.ts", "dnt": "deno task run _tasks/dnt.ts", - "star": "deno task run _tasks/star.ts && deno cache --check --no-lock target/star.ts", + "star": "deno task run _tasks/star.ts && deno cache --check --no-lock --import-map=import_map_cache.json target/star.ts", "lint": "deno lint", + "codegen": "deno task run _tasks/codegen.ts", "test": "deno task run test_util/ctx.ts -- deno test -A --no-lock -L=info --ignore=target --parallel", "test:update": "deno task test -- -- --update", "mdbook:watch": "mdbook watch -o", "bench": "deno bench -A --no-lock", + "moderate": "deno task run https://deno.land/x/moderate@0.0.5/mod.ts && dprint fmt", "polkagen": "deno task run codegen.ts -d=polkadot -o=target/polkagen --import=../../mod.ts" } } diff --git a/deps/capi_crypto_wrappers.ts b/deps/capi_crypto_wrappers.ts new file mode 100644 index 000000000..842506c81 --- /dev/null +++ b/deps/capi_crypto_wrappers.ts @@ -0,0 +1 @@ +export * from "https://raw.githubusercontent.com/paritytech/capi-crypto-wrappers/e15ce9d65afeb479ef095d4ae8b387c4adce3d3f/mod.ts" diff --git a/deps/dprint.ts b/deps/dprint.ts index 0911b2118..9bfb76da7 100644 --- a/deps/dprint.ts +++ b/deps/dprint.ts @@ -12,4 +12,5 @@ tsFormatter.setConfig({ }, { quoteProps: "asNeeded", "arrowFunction.useParentheses": "force", + semiColons: "asi", }) diff --git a/deps/polkadot/keyring.ts b/deps/polkadot/keyring.ts deleted file mode 100644 index 41d4ebe14..000000000 --- a/deps/polkadot/keyring.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "https://deno.land/x/polkadot@0.0.8/keyring/mod.ts" diff --git a/deps/polkadot/keyring/types.ts b/deps/polkadot/keyring/types.ts deleted file mode 100644 index 085ea4143..000000000 --- a/deps/polkadot/keyring/types.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "https://deno.land/x/polkadot@0.0.8/keyring/types.ts" diff --git a/deps/polkadot/types.ts b/deps/polkadot/types.ts deleted file mode 100644 index 0998cfd8d..000000000 --- a/deps/polkadot/types.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "https://deno.land/x/polkadot@0.0.8/types/mod.ts" diff --git a/deps/polkadot/util-crypto.ts b/deps/polkadot/util-crypto.ts deleted file mode 100644 index 7e454be7f..000000000 --- a/deps/polkadot/util-crypto.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "https://deno.land/x/polkadot@0.0.8/util-crypto/mod.ts" diff --git a/effects/extrinsic.test.ts b/effects/extrinsic.test.ts index b500b85a0..05dca1d3d 100644 --- a/effects/extrinsic.test.ts +++ b/effects/extrinsic.test.ts @@ -1,10 +1,8 @@ -import * as compat from "../compat/mod.ts" -import { KeyringPair } from "../deps/polkadot/keyring/types.ts" import * as A from "../deps/std/testing/asserts.ts" import * as T from "../test_util/mod.ts" import * as U from "../util/mod.ts" import { entryRead } from "./entryRead.ts" -import { CallData, extrinsic } from "./extrinsic.ts" +import { extrinsic } from "./extrinsic.ts" Deno.test({ name: "Balances.transfer", @@ -12,11 +10,16 @@ Deno.test({ await ctx.step("extrinsic events", async () => { await assertExtrinsicStatusOrder({ keypair: T.alice, - palletName: "Balances", - methodName: "transfer", - args: { - value: 12345n, - dest: compat.multiAddressFromKeypair(T.bob), + call: { + type: "Balances", + value: { + type: "transfer", + value: 12345n, + dest: { + type: "Id", + value: T.bob.publicKey, + }, + }, }, orderExpectation: ["ready", "inBlock", "finalized"], }) @@ -42,11 +45,16 @@ Deno.test({ await ctx.step("extrinsic events", async () => { await assertExtrinsicStatusOrder({ keypair: T.alice, - palletName: "Treasury", - methodName: "propose_spend", - args: { - value: 200n, - beneficiary: compat.multiAddressFromKeypair(T.bob), + call: { + type: "Treasury", + value: { + type: "proposeSpend", + value: 200n, + beneficiary: { + type: "Id", + value: T.bob.publicKey, + }, + }, }, orderExpectation: ["ready", "inBlock", "finalized"], }) @@ -55,21 +63,25 @@ Deno.test({ }) Deno.test({ + // TODO update for new polkadot + ignore: true, name: "Democracy.propose", fn: async (ctx) => { await ctx.step("extrinsic events", async () => { await assertExtrinsicStatusOrder({ keypair: T.alice, - palletName: "Democracy", - methodName: "propose", - args: { - proposal: { - type: "Inline", - value: U.hex.decode( - "0x123450000000000000000000000000000000000000000000000000000000000", - ), + call: { + type: "Democracy", + value: { + type: "propose", + proposal: { + type: "Inline", + value: U.hex.decode( + "0x123450000000000000000000000000000000000000000000000000000000000", + ), + }, + value: 2000000000000n, }, - value: 2000000000000n, }, orderExpectation: ["ready", "inBlock", "finalized"], }) @@ -77,23 +89,24 @@ Deno.test({ }, }) -interface AssertExtrinsicStatusOrderProps extends CallData { +interface AssertExtrinsicStatusOrderProps { orderExpectation: T.extrinsic.StatusOrderExpectation - keypair: KeyringPair + keypair: U.Sr25519 + call: unknown } export async function assertExtrinsicStatusOrder({ orderExpectation, keypair, - ...rest + call, }: AssertExtrinsicStatusOrderProps) { const extrinsicEvents = U.throwIfError( await T.extrinsic.collectExtrinsicEvents( extrinsic(T.westend)({ - sender: compat.multiAddressFromKeypair(keypair), - ...rest, + sender: keypair.address, + call, }) - .signed(compat.signerFromKeypair(keypair)), + .signed(keypair.sign), ).run(), ) T.extrinsic.assertStatusOrder(extrinsicEvents, orderExpectation) diff --git a/effects/extrinsic.ts b/effects/extrinsic.ts index 573587b30..9126e5ad1 100644 --- a/effects/extrinsic.ts +++ b/effects/extrinsic.ts @@ -11,21 +11,16 @@ import * as scale from "./scale.ts" const k0_ = Symbol() -export interface CallData { - palletName: string - methodName: string - args: Record -} - -export interface ExtrinsicProps extends CallData { +export interface ExtrinsicProps { sender: M.MultiAddress checkpoint?: U.HexHash mortality?: [period: bigint, phase: bigint] nonce?: string tip?: bigint + call: Call } -export function extrinsic>(client: Client) { +export function extrinsic, Call = unknown>(client: Client) { return >(props: Props): Extrinsic => { return new Extrinsic(client, props) } @@ -48,9 +43,7 @@ export class Extrinsic< const $extrinsic_ = $extrinsic(this.client) const $extrinsicProps = Z.rec({ protocolVersion: 4, - palletName: this.props.palletName, - methodName: this.props.methodName, - args: this.props.args, + call: this.props.call, }) const extrinsicBytes = scale.scaleEncoded($extrinsic_, $extrinsicProps, true) const extrinsicHex = extrinsicBytes.next(U.hex.encodePrefixed) @@ -86,9 +79,9 @@ export class SignedExtrinsic< const versions = const_(this.client)("System", "Version") .access("value") const specVersion = versions - .access("spec_version").as() + .access("specVersion").as() const transactionVersion = versions - .access("transaction_version").as() + .access("transactionVersion").as() // TODO: create match effect in zones and use here // TODO: MultiAddress conversion utils const senderSs58 = Z.ls(addrPrefix, this.props.sender).next(([addrPrefix, sender]) => { @@ -114,14 +107,12 @@ export class SignedExtrinsic< ? M.era.mortal(mortality[0], mortality[1]) : M.era.immortal }) - const extra = Z.ls(mortality, nonce, this.props.tip || 0) + const extra = Z.ls(mortality, nonce, this.props.tip || 0n) const additional = Z.ls(specVersion, transactionVersion, checkpointHash, genesisHash) const signature = Z.rec({ address: this.props.sender, extra, additional }) const $extrinsicProps = Z.rec({ protocolVersion: 4, - palletName: this.props.palletName, - methodName: this.props.methodName, - args: this.props.args, + call: this.props.call, signature, }) this.extrinsicBytes = scale.scaleEncoded($extrinsic_, $extrinsicProps, true) diff --git a/examples/.ignore b/examples/.ignore index e3b3fc809..7ac3786e8 100644 --- a/examples/.ignore +++ b/examples/.ignore @@ -1,2 +1,2 @@ -all.ts -multisig_transfer.ts +mod.ts +multisig_transfer.ts \ No newline at end of file diff --git a/examples/all.ts b/examples/all.ts deleted file mode 100644 index 5995dce1f..000000000 --- a/examples/all.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This script runs all examples in sequence. We should ultimately delete this script... -// ... but it's currently proving useful for local debugging. - -const ignore = ["all.ts", (await Deno.readTextFile("examples/.ignore")).split("\n")] -for await (const item of Deno.readDir("examples")) { - if (item.isFile && item.name.endsWith(".ts") && !ignore.includes(item.name)) { - await import(`./${item.name}`) - } -} diff --git a/examples/balance.ts b/examples/balance.ts index 2227f9e9b..d44904ddc 100644 --- a/examples/balance.ts +++ b/examples/balance.ts @@ -1,7 +1,8 @@ -import * as C from "../mod.ts" -import * as T from "../test_util/mod.ts" -import * as U from "../util/mod.ts" +import * as T from "#capi/test_util/mod.ts" +import * as U from "#capi/util/mod.ts" -const root = C.entryRead(T.polkadot)("System", "Account", [T.alice.publicKey]) +import { System } from "#capi/proxy/dev:polkadot/@v0.9.31/pallets/mod.ts" + +const root = System.Account.entry(T.alice.publicKey).read() console.log(U.throwIfError(await root.run())) diff --git a/examples/batch.ts b/examples/batch.ts index 54d0a8617..70c35bdb3 100644 --- a/examples/batch.ts +++ b/examples/batch.ts @@ -1,6 +1,9 @@ -import * as C from "../mod.ts" -import * as T from "../test_util/mod.ts" -import * as U from "../util/mod.ts" +import * as C from "#capi/mod.ts" +import * as T from "#capi/test_util/mod.ts" +import * as U from "#capi/util/mod.ts" + +import { extrinsic } from "#capi/proxy/dev:westend/@v0.9.31/mod.ts" +import { Balances, Utility } from "#capi/proxy/dev:westend/@v0.9.31/pallets/mod.ts" // TODO: uncomment these lines / use env upon solving `count` in zones // const getBalances = C.Z.ls( @@ -10,22 +13,18 @@ import * as U from "../util/mod.ts" // }), // ) -const tx = C.extrinsic(T.westend)({ - sender: C.compat.multiAddressFromKeypair(T.alice), - palletName: "Utility", - methodName: "batch_all", - args: { - calls: T.users.map((pair) => ({ - type: "Balances", - value: { - type: "transfer", - dest: C.compat.multiAddressFromKeypair(pair), +const tx = extrinsic({ + sender: T.alice.address, + call: Utility.batchAll({ + calls: T.users.map((pair) => + Balances.transfer({ + dest: pair.address, value: 12345n, - }, - })), - }, + }) + ), + }), }) - .signed(C.compat.signerFromKeypair(T.alice)) + .signed(T.alice.sign) .watch(function(status) { console.log(status) if (C.rpc.known.TransactionStatus.isTerminal(status)) { diff --git a/examples/derived.ts b/examples/derived.ts index 2906f389c..e5746e21d 100644 --- a/examples/derived.ts +++ b/examples/derived.ts @@ -1,5 +1,5 @@ -import * as C from "../mod.ts" -import * as U from "../util/mod.ts" +import * as C from "#capi/mod.ts" +import * as U from "#capi/util/mod.ts" const ids = C.entryRead(C.polkadot)("Paras", "Parachains", []) .access("value") diff --git a/examples/fee_estimate.ts b/examples/fee_estimate.ts index 4227e4528..b5778b1eb 100644 --- a/examples/fee_estimate.ts +++ b/examples/fee_estimate.ts @@ -1,14 +1,14 @@ -import * as C from "../mod.ts" -import * as T from "../test_util/mod.ts" +import * as T from "#capi/test_util/mod.ts" -const tx = C.extrinsic(T.westend)({ - sender: C.compat.multiAddressFromKeypair(T.alice), - palletName: "Balances", - methodName: "transfer", - args: { +import { extrinsic } from "#capi/proxy/dev:westend/@v0.9.31/mod.ts" +import { Balances } from "#capi/proxy/dev:westend/@v0.9.31/pallets/mod.ts" + +const tx = extrinsic({ + sender: T.alice.address, + call: Balances.transfer({ value: 12345n, - dest: C.compat.multiAddressFromKeypair(T.bob), - }, + dest: T.bob.address, + }), }) .feeEstimate diff --git a/examples/first_ten_keys.ts b/examples/first_ten_keys.ts index 2cc0ab0b1..68f53f22f 100644 --- a/examples/first_ten_keys.ts +++ b/examples/first_ten_keys.ts @@ -1,7 +1,7 @@ -import * as C from "../mod.ts" -import * as T from "../test_util/mod.ts" -import * as U from "../util/mod.ts" +import * as U from "#capi/util/mod.ts" -const root = C.keyPageRead(T.polkadot)("System", "Account", 10, []) +import { System } from "#capi/proxy/dev:polkadot/@v0.9.31/pallets/mod.ts" + +const root = System.Account.keys().readPage(10) console.log(U.throwIfError(await root.run())) diff --git a/examples/metadata.ts b/examples/metadata.ts index 8364a9744..1ce5f38ce 100644 --- a/examples/metadata.ts +++ b/examples/metadata.ts @@ -1,6 +1,6 @@ -import * as C from "../mod.ts" -import * as T from "../test_util/mod.ts" -import * as U from "../util/mod.ts" +import * as C from "#capi/mod.ts" +import * as T from "#capi/test_util/mod.ts" +import * as U from "#capi/util/mod.ts" const root = C.metadata(T.polkadot)() diff --git a/examples/mod.ts b/examples/mod.ts new file mode 100644 index 000000000..95d1e3dab --- /dev/null +++ b/examples/mod.ts @@ -0,0 +1,25 @@ +// This script runs all examples in sequence. We should ultimately delete this script... +// ... but it's currently proving useful for local debugging. + +// moderate --exclude multisig_transfer.ts + +export * from "./balance.ts" +export * from "./batch.ts" +export * from "./derived.ts" +export * from "./fee_estimate.ts" +export * from "./first_ten_keys.ts" +export * from "./metadata.ts" +export * from "./multisig_transfer.ts" +export * from "./polkadot_js_signer.ts" +export * from "./raw_rpc_client_call.ts" +export * from "./raw_rpc_client_subscription.ts" +export * from "./read_block.ts" +export * from "./read_bonded.ts" +export * from "./read_era_rewards.ts" +export * from "./read_events.ts" +export * from "./rpc_call.ts" +export * from "./rpc_subscription.ts" +export * from "./ticker.ts" +export * from "./transfer.ts" +export * from "./watch_blocks.ts" +export * from "./watch_events.ts" diff --git a/examples/multisig_transfer.ts b/examples/multisig_transfer.ts index 8f7b266c3..60a771022 100644 --- a/examples/multisig_transfer.ts +++ b/examples/multisig_transfer.ts @@ -1,8 +1,9 @@ -import { KeyringPair } from "../deps/polkadot/keyring/types.ts" -import { createKeyMulti } from "../deps/polkadot/util-crypto.ts" -import * as C from "../mod.ts" -import * as T from "../test_util/mod.ts" -import * as U from "../util/mod.ts" +import * as C from "#capi/mod.ts" +import * as T from "#capi/test_util/mod.ts" +import * as U from "#capi/util/mod.ts" + +import { extrinsic } from "#capi/proxy/dev:polkadot/@v0.9.31/mod.ts" +import { Balances, Multisig, System } from "#capi/proxy/dev:polkadot/@v0.9.31/pallets/mod.ts" // FIXME: remove this check once the Zones .bind(env) fix is merged const hostname = Deno.env.get("TEST_CTX_HOSTNAME") @@ -11,27 +12,19 @@ if (!hostname || !portRaw) { throw new Error("Must be running inside a test ctx") } -const entryRead = C.entryRead(T.polkadot) -const extrinsic = C.extrinsic(T.polkadot) - -const signatories = T.users - .slice(0, 3) - .map(({ publicKey }) => publicKey) - .sort() +const signatories = T.users.slice(0, 3).map((pair) => pair.publicKey) const THRESHOLD = 2 -const multisigPublicKey = createKeyMulti(signatories, THRESHOLD) +const multisigAddress = U.multisigAddress(signatories, THRESHOLD) // Transfer initial balance (existential deposit) to multisig address const existentialDeposit = extrinsic({ - sender: C.compat.multiAddressFromKeypair(T.alice), - palletName: "Balances", - methodName: "transfer", - args: { + sender: T.alice.address, + call: Balances.transfer({ value: 2_000_000_000_000n, - dest: C.MultiAddress.fromId(multisigPublicKey), - }, + dest: C.MultiAddress.Id(multisigAddress), + }), }) - .signed(C.compat.signerFromKeypair(T.alice)) + .signed(T.alice.sign) .watch(function(status) { console.log(`Existential deposit:`, status) if (C.rpc.known.TransactionStatus.isTerminal(status)) { @@ -43,19 +36,19 @@ const existentialDeposit = extrinsic({ const proposal = createOrApproveMultisigProposal("Proposal", T.alice) // Get the key of the timepoint -const key = C.keyPageRead(T.polkadot)("Multisig", "Multisigs", 1, [multisigPublicKey]) +const key = Multisig.Multisigs.keys(multisigAddress).readPage(1) .access(0) .access(1) // Get the timepoint itself -const maybeTimepoint = entryRead("Multisig", "Multisigs", [multisigPublicKey, key]) +const maybeTimepoint = Multisig.Multisigs.entry(multisigAddress, key).read() .access("value") .access("when") const approval = createOrApproveMultisigProposal("Approval", T.bob, maybeTimepoint) // check T.dave new balance -const daveBalance = entryRead("System", "Account", [T.dave.publicKey]) +const daveBalance = System.Account.entry(T.dave.publicKey).read() // TODO: use common env U.throwIfError(await existentialDeposit.run()) @@ -72,47 +65,37 @@ function createOrApproveMultisigProposal< ], >( label: string, - pair: KeyringPair, + pair: U.Sr25519, ...[maybeTimepoint]: Rest ) { + const call = Balances.transferKeepAlive({ + dest: T.dave.address, + value: 1230000000000n, + }) const maxWeight = extrinsic({ - sender: C.MultiAddress.fromId(multisigPublicKey), - palletName: "Balances", - methodName: "transfer_keep_alive", - args: { - dest: C.compat.multiAddressFromKeypair(T.dave), - value: 1_230_000_000_000n, - }, + sender: C.MultiAddress.Id(multisigAddress), + call, }) .feeEstimate .access("weight") .next((weight) => { return { - ref_time: BigInt(weight.ref_time), - proof_size: BigInt(weight.proof_size), + refTime: BigInt(weight.ref_time), + proofSize: BigInt(weight.proof_size), } }) return extrinsic({ - sender: C.compat.multiAddressFromKeypair(pair), - palletName: "Multisig", - methodName: "as_multi", - args: C.Z.rec({ + sender: pair.address, + call: C.Z.call.fac(Multisig.asMulti, null!)(C.Z.rec({ threshold: THRESHOLD, - call: { - type: "Balances", - value: { - type: "transfer_keep_alive", - dest: C.compat.multiAddressFromKeypair(T.dave), - value: 1_230_000_000_000n, - }, - }, - other_signatories: signatories.filter((value) => value !== pair.publicKey), - store_call: false, - max_weight: maxWeight, - maybe_timepoint: maybeTimepoint as Rest[0], - }), + call, + otherSignatories: signatories.filter((value) => value !== pair.publicKey), + storeCall: false, + maxWeight, + maybeTimepoint: maybeTimepoint as Rest[0], + })), }) - .signed(C.compat.signerFromKeypair(pair)) + .signed(pair.sign) .watch(function(status) { console.log(`${label}:`, status) if (C.rpc.known.TransactionStatus.isTerminal(status)) { diff --git a/examples/polkadot_js_signer.ts b/examples/polkadot_js_signer.ts index fc6d41903..91c43fedf 100644 --- a/examples/polkadot_js_signer.ts +++ b/examples/polkadot_js_signer.ts @@ -1,15 +1,19 @@ -import { TypeRegistry } from "../deps/polkadot/types.ts" -import * as C from "../mod.ts" -import * as T from "../test_util/mod.ts" -import * as U from "../util/mod.ts" +import { createTestPairs } from "https://deno.land/x/polkadot@0.0.8/keyring/mod.ts" +import { TypeRegistry } from "https://deno.land/x/polkadot@0.0.8/types/mod.ts" + +import * as C from "#capi/mod.ts" +import * as T from "#capi/test_util/mod.ts" +import * as U from "#capi/util/mod.ts" const root = C.extrinsic(T.westend)({ - sender: C.compat.multiAddressFromKeypair(T.alice), - palletName: "Balances", - methodName: "transfer", - args: { - value: 12345n, - dest: C.compat.multiAddressFromKeypair(T.bob), + sender: T.alice.address, + call: { + type: "Balances", + value: { + type: "transfer", + value: 12345n, + dest: T.bob.address, + }, }, }) .signed({ @@ -19,7 +23,7 @@ const root = C.extrinsic(T.westend)({ return Promise.resolve( tr .createType("ExtrinsicPayload", payload, { version: payload.version }) - .sign(T.alice), + .sign(createTestPairs().alice!), ) }, }) diff --git a/examples/raw_rpc_client_call.ts b/examples/raw_rpc_client_call.ts index 08b0cfdef..6ecbb5404 100755 --- a/examples/raw_rpc_client_call.ts +++ b/examples/raw_rpc_client_call.ts @@ -1,4 +1,4 @@ -import * as T from "../test_util/mod.ts" +import * as T from "#capi/test_util/mod.ts" const client = await T.westend.client diff --git a/examples/raw_rpc_client_subscription.ts b/examples/raw_rpc_client_subscription.ts index 1d9ad3b17..f9b6b4c2a 100755 --- a/examples/raw_rpc_client_subscription.ts +++ b/examples/raw_rpc_client_subscription.ts @@ -1,6 +1,6 @@ -import { assertNotInstanceOf } from "../deps/std/testing/asserts.ts" -import * as T from "../test_util/mod.ts" -import * as U from "../util/mod.ts" +import { assertNotInstanceOf } from "#capi/deps/std/testing/asserts.ts" +import * as T from "#capi/test_util/mod.ts" +import * as U from "#capi/util/mod.ts" const client = await T.polkadot.client diff --git a/examples/read_block.ts b/examples/read_block.ts index c2d3c0e08..6c7a9bbfd 100644 --- a/examples/read_block.ts +++ b/examples/read_block.ts @@ -1,6 +1,6 @@ -import * as C from "../mod.ts" -import * as T from "../test_util/mod.ts" -import * as U from "../util/mod.ts" +import * as C from "#capi/mod.ts" +import * as T from "#capi/test_util/mod.ts" +import * as U from "#capi/util/mod.ts" const extrinsicsRaw = C.chain.getBlock(C.polkadot)() .access("block") diff --git a/examples/read_bonded.ts b/examples/read_bonded.ts index 501a5f316..5a7dd1c60 100644 --- a/examples/read_bonded.ts +++ b/examples/read_bonded.ts @@ -1,9 +1,7 @@ -import * as C from "../mod.ts" -import * as T from "../test_util/mod.ts" -import * as U from "../util/mod.ts" +import * as C from "#capi/mod.ts" +import * as T from "#capi/test_util/mod.ts" +import * as U from "#capi/util/mod.ts" -const aliceStash = T.alice.derive("//stash") - -const aliceBonded = C.entryRead(T.polkadot)("Staking", "Bonded", [aliceStash.publicKey]) +const aliceBonded = C.entryRead(T.polkadot)("Staking", "Bonded", [T.aliceStash.publicKey]) console.log(U.throwIfError(await aliceBonded.run())) diff --git a/examples/read_era_rewards.ts b/examples/read_era_rewards.ts index ab9abb246..e849f622c 100644 --- a/examples/read_era_rewards.ts +++ b/examples/read_era_rewards.ts @@ -1,5 +1,5 @@ -import * as C from "../mod.ts" -import * as U from "../util/mod.ts" +import * as C from "#capi/mod.ts" +import * as U from "#capi/util/mod.ts" const idx = C.entryRead(C.westend)("Staking", "ActiveEra", []) .access("value") diff --git a/examples/read_events.ts b/examples/read_events.ts index e3de7e30c..0a93f595f 100644 --- a/examples/read_events.ts +++ b/examples/read_events.ts @@ -1,5 +1,5 @@ -import * as C from "../mod.ts" -import * as U from "../util/mod.ts" +import * as C from "#capi/mod.ts" +import * as U from "#capi/util/mod.ts" const root = C.entryRead(C.polkadot)("System", "Events", []) diff --git a/examples/rpc_call.ts b/examples/rpc_call.ts index 17eac9a23..75aac673c 100644 --- a/examples/rpc_call.ts +++ b/examples/rpc_call.ts @@ -1,5 +1,5 @@ -import * as C from "../mod.ts" -import * as U from "../util/mod.ts" +import * as C from "#capi/mod.ts" +import * as U from "#capi/util/mod.ts" const root = C.rpcCall<[], string[]>("rpc_methods")(C.polkadot)() diff --git a/examples/rpc_subscription.ts b/examples/rpc_subscription.ts index 08ef459da..281aa4cc3 100644 --- a/examples/rpc_subscription.ts +++ b/examples/rpc_subscription.ts @@ -1,6 +1,6 @@ -import * as C from "../mod.ts" -import * as T from "../test_util/mod.ts" -import * as U from "../util/mod.ts" +import * as C from "#capi/mod.ts" +import * as T from "#capi/test_util/mod.ts" +import * as U from "#capi/util/mod.ts" const root = C.chain.unsubscribeNewHeads(T.polkadot)( C.chain.subscribeNewHeads(T.polkadot)([], function(header) { diff --git a/examples/ticker.ts b/examples/ticker.ts index 53b745a6e..de256eaf1 100644 --- a/examples/ticker.ts +++ b/examples/ticker.ts @@ -1,6 +1,6 @@ -import * as C from "../mod.ts" -import * as T from "../test_util/mod.ts" -import * as U from "../util/mod.ts" +import * as C from "#capi/mod.ts" +import * as T from "#capi/test_util/mod.ts" +import * as U from "#capi/util/mod.ts" const root = C.entryWatch(T.polkadot)("Timestamp", "Now", [], function(entry) { console.log(entry) diff --git a/examples/transfer.ts b/examples/transfer.ts index e95a08bd6..8dbaefcda 100644 --- a/examples/transfer.ts +++ b/examples/transfer.ts @@ -1,21 +1,22 @@ -import * as C from "../mod.ts" -import * as T from "../test_util/mod.ts" -import * as U from "../util/mod.ts" +import * as C from "#capi/mod.ts" +import * as T from "#capi/test_util/mod.ts" +import * as U from "#capi/util/mod.ts" + +import { extrinsic } from "#capi/proxy/dev:westend/@v0.9.31/mod.ts" +import { Balances } from "#capi/proxy/dev:westend/@v0.9.31/pallets/mod.ts" let hash: undefined | C.rpc.known.Hash const env = C.Z.env() -const tx = C.extrinsic(T.westend)({ - sender: C.compat.multiAddressFromKeypair(T.alice), - palletName: "Balances", - methodName: "transfer", - args: { +const tx = extrinsic({ + sender: T.alice.address, + call: Balances.transfer({ value: 12345n, - dest: C.compat.multiAddressFromKeypair(T.bob), - }, + dest: T.bob.address, + }), }) - .signed(C.compat.signerFromKeypair(T.alice)) + .signed(T.alice.sign) const runTx = tx .watch(function(status) { diff --git a/examples/watch_blocks.ts b/examples/watch_blocks.ts index c3246517d..92b6e6c40 100644 --- a/examples/watch_blocks.ts +++ b/examples/watch_blocks.ts @@ -1,5 +1,5 @@ -import * as C from "../mod.ts" -import * as U from "../util/mod.ts" +import * as C from "#capi/mod.ts" +import * as U from "#capi/util/mod.ts" const root = C.blockWatch(C.polkadot)(async function blockWatchListener({ block }) { const extrinsicsDecoded = C diff --git a/examples/watch_events.ts b/examples/watch_events.ts index dc5b402eb..19f6e33fb 100644 --- a/examples/watch_events.ts +++ b/examples/watch_events.ts @@ -1,5 +1,5 @@ -import * as C from "../mod.ts" -import * as U from "../util/mod.ts" +import * as C from "#capi/mod.ts" +import * as U from "#capi/util/mod.ts" const root = C.entryWatch(C.rococo)("System", "Events", [], function(entry) { console.log(entry) diff --git a/fluent/mod.ts b/fluent/mod.ts new file mode 100644 index 000000000..be1706610 --- /dev/null +++ b/fluent/mod.ts @@ -0,0 +1,69 @@ +import * as C from "../mod.ts" +import * as U from "../util/mod.ts" + +export class Storage, K extends unknown[], V> { + constructor( + readonly client: C, + readonly type: C.M.StorageEntry["type"], + readonly modifier: C.M.StorageEntry["modifier"], + readonly pallet: string, + readonly name: string, + readonly $key: C.$.Codec, + readonly $value: C.$.Codec, + ) {} + + entry>(...key: Key): StorageEntry { + return new StorageEntry(this, key) + } + + keys>>( + ...partialKey: PartialKey + ): StorageKeys { + return new StorageKeys(this, partialKey) + } +} + +export class StorageEntry< + C extends C.Z.$, + K extends unknown[], + V, + Key extends C.Z.Ls$, +> { + constructor(readonly storage: Storage, readonly key: Key) {} + + read]>(...maybeHash: MaybeHash) { + return C.entryRead(this.storage.client)( + this.storage.pallet, + this.storage.name, + this.key, + ...maybeHash, + ).as<{ value: V }>() + } +} +export class StorageKeys< + C extends C.Z.$, + K extends unknown[], + V, + PartialKey extends C.Z.Ls$>, +> { + constructor(readonly storage: Storage, readonly partialKey: PartialKey) {} + + readPage< + Count extends C.Z.$, + Rest extends [start?: C.Z.Ls$, blockHash?: C.Z.$], + >(count: Count, ...rest: Rest) { + return C.keyPageRead(this.storage.client)< + string, + string, + Count, + PartialKey, + Rest + >( + this.storage.pallet, + this.storage.name, + count, + this.partialKey as [...PartialKey], + ...rest, + ).as() + } +} diff --git a/frame_metadata/Codec.test.ts b/frame_metadata/Codec.test.ts index a154dfe80..365b42bb0 100644 --- a/frame_metadata/Codec.test.ts +++ b/frame_metadata/Codec.test.ts @@ -32,8 +32,8 @@ Deno.test("Derive AccountInfo Codec", async () => { data: { free: 1340320999878n, reserved: 0n, - misc_frozen: 50000000000n, - fee_frozen: 0n, + miscFrozen: 50000000000n, + feeFrozen: 0n, }, } const encoded = codec.encode(decoded) diff --git a/frame_metadata/Codec.ts b/frame_metadata/Codec.ts index 72c99e266..c3b75e4ae 100644 --- a/frame_metadata/Codec.ts +++ b/frame_metadata/Codec.ts @@ -1,4 +1,5 @@ import * as $ from "../deps/scale.ts" +import { normalizeCase } from "../util/case.ts" import { $era } from "./Era.ts" import type * as M from "./mod.ts" import { TyVisitor } from "./TyVisitor.ts" @@ -24,7 +25,9 @@ export function DeriveCodec(tys: M.Ty[]): DeriveCodec { return $.tuple(...members.map((x) => this.visit(x))) }, objectStruct(ty) { - return $.object(...ty.fields.map((x): $.AnyField => [x.name!, this.visit(x.ty)])) + return $.object( + ...ty.fields.map((x): $.AnyField => [normalizeCase(x.name!), this.visit(x.ty)]), + ) }, option(_ty, some) { return $.option(this.visit(some)) @@ -38,14 +41,15 @@ export function DeriveCodec(tys: M.Ty[]): DeriveCodec { stringUnion(ty) { const members: Record = {} for (const { index, name } of ty.members) { - members[index] = name + members[index] = normalizeCase(name) } return $.stringUnion(members) }, taggedUnion(ty) { const members: Record = {} - for (const { fields, name: type, index } of ty.members) { + for (const { fields, name, index } of ty.members) { let member: $.AnyTaggedUnionMember + const type = normalizeCase(name) if (fields.length === 0) { member = [type] } else if (fields[0]!.name === undefined) { @@ -56,9 +60,9 @@ export function DeriveCodec(tys: M.Ty[]): DeriveCodec { member = [type, ["value", $value]] } else { // Object variant - const memberFields = fields.map((field, i) => { + const memberFields = fields.map((field) => { return [ - field.name || i, + normalizeCase(field.name!), this.visit(field.ty), ] as [string, $.Codec] }) diff --git a/frame_metadata/Extrinsic.ts b/frame_metadata/Extrinsic.ts index ef865e298..60d5722e0 100644 --- a/frame_metadata/Extrinsic.ts +++ b/frame_metadata/Extrinsic.ts @@ -5,27 +5,11 @@ import { hashers, Hex, hex } from "../util/mod.ts" import { $null, DeriveCodec } from "./Codec.ts" import { Metadata } from "./Metadata.ts" -export interface MultiAddress { - type: "Id" | "Index" | "Raw" | "Address20" | "Address32" - value: Uint8Array -} -// TODO: delete upon common generated core types -export namespace MultiAddress { - export function fromId(id: Uint8Array): MultiAddress { - return { - type: "Id", - value: id, - } - } -} - -export interface Signature { - type: "Sr25519" | "Ed25519" | "Secp256k" // TODO: `"Ecdsa"`?; - value: Uint8Array -} +// TODO: revisit +import { $multiAddress, $multiSignature, MultiAddress, MultiSignature } from "./primitives.ts" export type Signer = - | ((message: Uint8Array) => Signature | Promise) + | ((message: Uint8Array) => MultiSignature | Promise) | PolkadotSigner export interface PolkadotSigner { signPayload(payload: any): Promise<{ signature: string }> @@ -39,10 +23,8 @@ export interface Extrinsic { address: MultiAddress extra: unknown[] } - & ({ additional: unknown[] } | { sig: Signature }) - palletName: string - methodName: string - args: Record + & ({ additional: unknown[] } | { sig: MultiSignature }) + call: unknown } interface ExtrinsicCodecProps { @@ -55,9 +37,7 @@ interface ExtrinsicCodecProps { export function $extrinsic(props: ExtrinsicCodecProps): $.Codec { const { metadata, deriveCodec } = props const { signedExtensions } = metadata.extrinsic - const $sig = deriveCodec(findExtrinsicTypeParam("Signature")!) as $.Codec - const $sigPromise = $.promise($sig) - const $address = deriveCodec(findExtrinsicTypeParam("Address")!) + const $multisigPromise = $.promise($multiSignature) const callTy = findExtrinsicTypeParam("Call")! assert(callTy?.type === "Union") const $call = deriveCodec(callTy) @@ -69,7 +49,7 @@ export function $extrinsic(props: ExtrinsicCodecProps): $.Codec { const pjsInfo = [...extraPjsInfo, ...additionalPjsInfo] const toSignSize = $call._staticSize + $extra._staticSize + $additional._staticSize - const totalSize = 1 + $address._staticSize + $sig._staticSize + toSignSize + const totalSize = 1 + $multiAddress._staticSize + $multiSignature._staticSize + toSignSize const $baseExtrinsic: $.Codec = $.createCodec({ _metadata: [], @@ -77,16 +57,9 @@ export function $extrinsic(props: ExtrinsicCodecProps): $.Codec { _encode(buffer, extrinsic) { const firstByte = (+!!extrinsic.signature << 7) | extrinsic.protocolVersion buffer.array[buffer.index++] = firstByte - const call = { - type: extrinsic.palletName, - value: { - type: extrinsic.methodName, - ...extrinsic.args, - }, - } - const { signature } = extrinsic + const { signature, call } = extrinsic if (signature) { - $address._encode(buffer, signature.address) + $multiAddress._encode(buffer, signature.address) if ("additional" in signature) { const toSignBuffer = new $.EncodeBuffer(buffer.stealAlloc(toSignSize)) $call._encode(toSignBuffer, call) @@ -139,15 +112,15 @@ export function $extrinsic(props: ExtrinsicCodecProps): $.Codec { : toSignEncoded const sig = props.sign(toSign) if (sig instanceof Promise) { - $sigPromise._encode(buffer, sig) + $multisigPromise._encode(buffer, sig) } else { - $sig._encode(buffer, sig) + $multiSignature._encode(buffer, sig) } buffer.insertArray(extraEncoded) buffer.insertArray(callEncoded) } } else { - $sig._encode(buffer, signature.sig) + $multiSignature._encode(buffer, signature.sig) $extra._encode(buffer, signature.extra) $call._encode(buffer, call) } @@ -161,40 +134,27 @@ export function $extrinsic(props: ExtrinsicCodecProps): $.Codec { const protocolVersion = firstByte & ~(1 << 7) let signature: Extrinsic["signature"] if (hasSignature) { - const address = $address._decode(buffer) as MultiAddress - const sig = $sig._decode(buffer) + const address = $multiAddress._decode(buffer) as MultiAddress + const sig = $multiSignature._decode(buffer) const extra = $extra._decode(buffer) signature = { address, sig, extra } } - const call = $call._decode(buffer) as any - const { type: palletName, value: { type: methodName, ...args } } = call - return { protocolVersion, signature, palletName, methodName, args } + const call = $call._decode(buffer) + return { protocolVersion, signature, call } }, _assert(assert) { assert.typeof(this, "object") - assert - .key(this, "protocolVersion") - .equals($.u8, 4) + assert.key(this, "protocolVersion").equals($.u8, 4) const value_ = assert.value as any - // TODO: use `assert.key(this, "call")` upon merging https://github.com/paritytech/capi/pull/368 - $call._assert( - new $.AssertState({ - type: value_.palletName, - value: { - type: value_.methodName, - ...value_.args, - }, - }), - ) + $call._assert(assert.key(this, "call")) if (value_.signature) { const signatureAssertState = assert.key(this, "signature") - signatureAssertState.key($address, "address") - signatureAssertState.key($extra, "extra") - if ("additional" in signatureAssertState) { - signatureAssertState.key($additional, "additional") - } - if ("sig" in signatureAssertState) { - signatureAssertState.key($sig, "sig") + $multiAddress._assert(signatureAssertState.key(this, "address")) + $extra._assert(signatureAssertState.key(this, "extra")) + if ("additional" in value_.signature) { + $additional._assert(signatureAssertState.key(this, "additional")) + } else { + $multiSignature._assert(signatureAssertState.key(this, "sig")) } } }, diff --git a/frame_metadata/TyVisitor.ts b/frame_metadata/TyVisitor.ts index 7215ee400..afb2f27f0 100644 --- a/frame_metadata/TyVisitor.ts +++ b/frame_metadata/TyVisitor.ts @@ -39,6 +39,8 @@ export interface TyVisitorMethods { lenPrefixedWrapper(ty: Ty & StructTyDef, inner: Ty): T + all?(ty: Ty): T | undefined + circular(ty: Ty): T } @@ -71,6 +73,8 @@ export class TyVisitor { } _visit(ty: Ty) { + const allResult = this.all?.(ty) + if (allResult) return allResult if (ty.type === "Struct") { if (this.map && ty.path[0] === "BTreeMap") { return this.map(ty, ty.params[0]!.ty!, ty.params[1]!.ty!) diff --git a/frame_metadata/mod.ts b/frame_metadata/mod.ts index e6d1eff0c..7b6921cd8 100644 --- a/frame_metadata/mod.ts +++ b/frame_metadata/mod.ts @@ -4,5 +4,6 @@ export * from "./Era.ts" export * from "./Extrinsic.ts" export * from "./Key.ts" export * from "./Metadata.ts" +export * from "./primitives.ts" export * from "./scale_info.ts" export * from "./TyVisitor.ts" diff --git a/frame_metadata/primitives.ts b/frame_metadata/primitives.ts new file mode 100644 index 000000000..825b6eb88 --- /dev/null +++ b/frame_metadata/primitives.ts @@ -0,0 +1,104 @@ +import * as $ from "../deps/scale.ts" +import { $null } from "./Codec.ts" + +export const $multiSignature: $.Codec = $.taggedUnion("type", { + 0: ["Ed25519", ["value", $.sizedUint8Array(64)]], + 1: ["Sr25519", ["value", $.sizedUint8Array(64)]], + 2: ["Ecdsa", ["value", $.sizedUint8Array(65)]], +}) + +export const $multiAddress: $.Codec = $.taggedUnion("type", { + 0: ["Id", ["value", $.sizedUint8Array(32)]], + 1: ["Index", ["value", $null]], + 2: ["Raw", ["value", $.uint8Array]], + 3: ["Address32", ["value", $.sizedUint8Array(32)]], + 4: ["Address20", ["value", $.sizedUint8Array(20)]], +}) + +export type MultiSignature = + | MultiSignature.Ed25519 + | MultiSignature.Sr25519 + | MultiSignature.Ecdsa +export namespace MultiSignature { + export interface Ed25519 { + type: "Ed25519" + value: Uint8Array + } + export interface Sr25519 { + type: "Sr25519" + value: Uint8Array + } + export interface Ecdsa { + type: "Ecdsa" + value: Uint8Array + } + export function Ed25519( + value: MultiSignature.Ed25519["value"], + ): MultiSignature.Ed25519 { + return { type: "Ed25519", value } + } + export function Sr25519( + value: MultiSignature.Sr25519["value"], + ): MultiSignature.Sr25519 { + return { type: "Sr25519", value } + } + export function Ecdsa( + value: MultiSignature.Ecdsa["value"], + ): MultiSignature.Ecdsa { + return { type: "Ecdsa", value } + } +} + +export type MultiAddress = + | MultiAddress.Id + | MultiAddress.Index + | MultiAddress.Raw + | MultiAddress.Address32 + | MultiAddress.Address20 +export namespace MultiAddress { + export interface Id { + type: "Id" + value: Uint8Array + } + export interface Index { + type: "Index" + value: null + } + export interface Raw { + type: "Raw" + value: Uint8Array + } + export interface Address32 { + type: "Address32" + value: Uint8Array + } + export interface Address20 { + type: "Address20" + value: Uint8Array + } + export function Id( + value: MultiAddress.Id["value"], + ): MultiAddress.Id { + return { type: "Id", value } + } + export function Index( + value: MultiAddress.Index["value"], + ): MultiAddress.Index { + return { type: "Index", value } + } + export function Raw( + value: MultiAddress.Raw["value"], + ): MultiAddress.Raw { + return { type: "Raw", value } + } + export function Address32( + value: MultiAddress.Address32["value"], + ): MultiAddress.Address32 { + return { type: "Address32", value } + } + export function Address20( + value: MultiAddress.Address20["value"], + ): MultiAddress.Address20 { + return { type: "Address20", value } + } +} diff --git a/import_map_cache.json b/import_map_cache.json new file mode 100644 index 000000000..7b8624543 --- /dev/null +++ b/import_map_cache.json @@ -0,0 +1,11 @@ +{ + "scopes": { + "examples/": { + "#capi/": "./", + "#capi/proxy/": "./target/codegen/generated/@local/" + }, + "target/codegen/generated/": { + "/@local/": "./" + } + } +} diff --git a/import_map_localhost.json b/import_map_localhost.json new file mode 100644 index 000000000..e89366df4 --- /dev/null +++ b/import_map_localhost.json @@ -0,0 +1,7 @@ +{ + "scopes": { + "examples/": { + "#capi/": "http://localhost:5646/@local/" + } + } +} diff --git a/mod.ts b/mod.ts index 5667b3187..e495e8578 100644 --- a/mod.ts +++ b/mod.ts @@ -1,8 +1,8 @@ -export * as compat from "./compat/mod.ts" export * as $ from "./deps/scale.ts" export { BitSequence } from "./deps/scale.ts" export * as Z from "./deps/zones.ts" export * from "./effects/mod.ts" +export * as fluent from "./fluent/mod.ts" export * as M from "./frame_metadata/mod.ts" export { $era, @@ -13,4 +13,4 @@ export { type Signer, } from "./frame_metadata/mod.ts" export * as rpc from "./rpc/mod.ts" -export { contramapListener, hex, type Listener } from "./util/mod.ts" +export { contramapListener, type Hex, hex, type Listener } from "./util/mod.ts" diff --git a/test_util/common.ts b/test_util/common.ts index 3141af67b..1e789b054 100644 --- a/test_util/common.ts +++ b/test_util/common.ts @@ -15,10 +15,7 @@ export const RUNTIME_NAMES: { [N in RuntimeName as RUNTIME_CODES[N]]: N } = { } export function isRuntimeName(inQuestion: string): inQuestion is RuntimeName { - return inQuestion === "polkadot" - || inQuestion === "kusama" - || inQuestion === "polkadot" - || inQuestion === "polkadot" + return Object.values(RUNTIME_NAMES).includes(inQuestion as never) } export class InvalidRuntimeSpecifiedError extends Error { diff --git a/test_util/pairs.ts b/test_util/pairs.ts index faffa268a..35e0e76b9 100644 --- a/test_util/pairs.ts +++ b/test_util/pairs.ts @@ -1,36 +1,33 @@ -import { createTestPairs } from "../deps/polkadot/keyring.ts" -import { KeyringPair } from "../deps/polkadot/keyring/types.ts" -import { cryptoWaitReady } from "../deps/polkadot/util-crypto.ts" -import { ArrayOfLength } from "../util/mod.ts" +import { hex, Sr25519 } from "../util/mod.ts" -await cryptoWaitReady() +export const alice = pair( + "98319d4ff8a9508c4bb0cf0b5a78d760a0b2082c02775e6e82370816fedfff48925a225d97aa00682d6a59b95b18780c10d7032336e88f3442b42361f4a66011", +) +export const bob = pair( + "081ff694633e255136bdb456c20a5fc8fed21f8b964c11bb17ff534ce80ebd5941ae88f85d0c1bfc37be41c904e1dfc01de8c8067b0d6d5df25dd1ac0894a325", +) +export const charlie = pair( + "a8f2d83016052e5d6d77b2f6fd5d59418922a09024cda701b3c34369ec43a7668faf12ff39cd4e5d92bb773972f41a7a5279ebc2ed92264bed8f47d344f8f18c", +) +export const dave = pair( + "20e05482ca4677e0edbc58ae9a3a59f6ed3b1a9484ba17e64d6fe8688b2b7b5d108c4487b9323b98b11fe36cb301b084e920f7b7895536809a6d62a451b25568", +) +export const eve = pair( + "683576abfd5dc35273e4264c23095a1bf21c14517bece57c7f0cc5c0ed4ce06a3dbf386b7828f348abe15d76973a72009e6ef86a5c91db2990cb36bb657c6587", +) +export const ferdie = pair( + "b835c20f450079cf4f513900ae9faf8df06ad86c681884122c752a4b2bf74d4303e4f21bc6cc62bb4eeed5a9cce642c25e2d2ac1464093b50f6196d78e3a7426", +) -export interface Pairs { - all: ArrayOfLength - alice: KeyringPair - bob: KeyringPair - charlie: KeyringPair - dave: KeyringPair - eve: KeyringPair - ferdie: KeyringPair -} +export const aliceStash = pair( + "e8da6c9d810e020f5e3c7f5af2dea314cbeaa0d72bc6421e92c0808a0c584a6046ab28e97c3ffc77fe12b5a4d37e8cd4afbfebbf2391ffc7cb07c0f38c023efd", +) +export const bobStash = pair( + "c006507cdfc267a21532394c49ca9b754ca71de21e15a1cdf807c7ceab6d0b6c3ed408d9d35311540dcd54931933e67cf1ea10d46f75408f82b789d9bd212fde", +) + +export const users = [alice, bob, charlie, dave, eve, ferdie] -export const { all: users, alice, bob, charlie, dave, eve, ferdie } = pairs() -export function pairs(...args: Parameters): Pairs { - const raw = createTestPairs(...args) - const alice = raw["alice"]! - const bob = raw["bob"]! - const charlie = raw["charlie"]! - const dave = raw["dave"]! - const eve = raw["eve"]! - const ferdie = raw["ferdie"]! - return { - all: [alice, bob, charlie, dave, eve, ferdie], - alice, - bob, - charlie, - dave, - eve, - ferdie, - } +export function pair(secret: string) { + return Sr25519.fromSecret(hex.decode(secret)) } diff --git a/util/case.ts b/util/case.ts new file mode 100644 index 000000000..d6acb6eef --- /dev/null +++ b/util/case.ts @@ -0,0 +1,3 @@ +export function normalizeCase(ident: string) { + return ident.replace(/_(.)/g, (_, $1: string) => $1.toUpperCase()) +} diff --git a/util/memo.ts b/util/memo.ts new file mode 100644 index 000000000..2ac5e2aaf --- /dev/null +++ b/util/memo.ts @@ -0,0 +1,70 @@ +import { getOrInit } from "./map.ts" + +export class AsyncMemo { + running = new Map>() + + run(key: K, run: () => Promise) { + return getOrInit(this.running, key, () => run().finally(() => this.running.delete(key))) + } +} + +export class TimedMemo extends AsyncMemo { + done = new Map() + timers = new Set() + + constructor(readonly ttl: number, readonly signal: AbortSignal) { + super() + this.signal.addEventListener("abort", () => { + for (const timer of this.timers) { + clearTimeout(timer) + } + }) + } + + override async run(key: K, run: () => Promise, ttl = this.ttl) { + const existing = this.done.get(key) + if (existing) return existing + return super.run(key, () => + run().then((value) => { + this.done.set(key, value) + const timer = setTimeout(() => { + this.done.delete(key) + this.timers.delete(timer) + }, ttl) + this.timers.add(timer) + if (Deno.unrefTimer) { + Deno.unrefTimer(timer) + } + return value + })) + } +} + +export class PermanentMemo extends AsyncMemo { + done = new Map() + + override async run(key: K, run: () => Promise) { + const existing = this.done.get(key) + if (existing) return existing + return super.run(key, () => + run().then((value) => { + this.done.set(key, value) + return value + })) + } +} + +export class WeakMemo extends AsyncMemo { + done = new Map>() + finReg = new FinalizationRegistry((key) => this.done.delete(key)) + + override async run(key: K, run: () => Promise) { + const existing = this.done.get(key)?.deref() + if (existing) return existing + return super.run(key, () => + run().then((value) => { + this.done.set(key, new WeakRef(value)) + return value + })) + } +} diff --git a/util/mod.ts b/util/mod.ts index f33c8ac2e..69da5df1b 100644 --- a/util/mod.ts +++ b/util/mod.ts @@ -1,9 +1,13 @@ export * from "./branded.ts" +export * from "./case.ts" export * from "./Counter.ts" export * from "./error.ts" export * as hashers from "./hashers.ts" export * as hex from "./hex.ts" export * from "./Listener.ts" export * from "./map.ts" +export * from "./memo.ts" +export * from "./multisig.ts" +export * from "./sr25519.ts" export * from "./tuple.ts" export * from "./types.ts" diff --git a/util/multisig.ts b/util/multisig.ts new file mode 100644 index 000000000..cab5f7594 --- /dev/null +++ b/util/multisig.ts @@ -0,0 +1,25 @@ +import * as $ from "../deps/scale.ts" +import { Blake2_256 } from "./hashers.ts" + +const seed = "modlpy/utilisuba" // cspell:disable-line + +const codec = Blake2_256.$hash($.tuple( + $.constant(null, new TextEncoder().encode(seed)), + $.array($.sizedUint8Array(32)), + $.u16, +)) + +export function multisigAddress(signatories: Uint8Array[], threshold: number) { + return codec.encode( + [null, signatories.sort(sortUint8Array), threshold], + ) +} + +function sortUint8Array(a: Uint8Array, b: Uint8Array) { + for (let i = 0; i < a.length && i < b.length; i++) { + if (a[i] !== b[i]) { + return a[i]! - b[i]! + } + } + return a.length - b.length +} diff --git a/util/sr25519.ts b/util/sr25519.ts new file mode 100644 index 000000000..d98dfa78b --- /dev/null +++ b/util/sr25519.ts @@ -0,0 +1,33 @@ +import { + sr25519_from_seed, + sr25519_pubkey, + sr25519_sign, + sr25519_verify, +} from "../deps/capi_crypto_wrappers.ts" +import { MultiAddress, MultiSignature } from "../frame_metadata/primitives.ts" + +export class Sr25519 { + address + + constructor(readonly publicKey: Uint8Array, readonly secretKey: Uint8Array) { + if (publicKey.length !== 32) throw new Error("Invalid publicKey") + if (secretKey.length !== 64) throw new Error("Invalid secretKey") + this.address = MultiAddress.Id(this.publicKey) + } + + static fromSecret(secret: Uint8Array) { + return new Sr25519(sr25519_pubkey(secret), secret) + } + + static fromSeed(seed: Uint8Array) { + const pair = sr25519_from_seed(seed) + return new Sr25519(pair.slice(64), pair.slice(0, 64)) + } + + sign = (msg: Uint8Array) => + MultiSignature.Sr25519(sr25519_sign(this.publicKey, this.secretKey, msg)) + + static verify(pubkey: Uint8Array, msg: Uint8Array, sig: Uint8Array) { + return sr25519_verify(pubkey, msg, sig) + } +} diff --git a/words.txt b/words.txt index fdeb7551e..5a1be703f 100644 --- a/words.txt +++ b/words.txt @@ -1,205 +1,207 @@ +abortable +acala +ajun +ajuna +alloc +alphaville +amannn +astar +atomf +autoremove +aventus azuretools +baju +bajun +binaryen +bindgen +blake2_128Concat +bootnode bungcip +callables capi -denoland -dprint -esbenp -matklad -polkadot -atomf -serayuzgur -typebox -vadimcn -codegen -hasher -hashers -twox -subpaths +cdylib +chainspec +chainspecs +chainx +childstate cmds -paritytech -sufficients -bindgen -kusama -statemint -acala -lipsum +codegen +codegened +codespaces +combinators +contextfree +contramap +creds cummon -transcoders -unsanitized +curto +Curto +darwinia +datahighway +datetime +demux +demuxing deno -paka +denoland +dentnet +dentx +deployctl +deps +deref devcontainer -esque -onfinality -notset -smoldot -tomaka -monomorphization -discoverability +devcontainers devs -idents -zio -parachains +dico +discoverability +dispatchable +dprint +edgeware +efinity +esbenp +esque +extrinsics +ferdie +finalised +fragnova +framesystem genericize -lldb -abortable -parachain +genshiro +getrandom +glmr harrysolovay -creds -codespaces -extrinsics -rustup -roadmap -println +hashbrown +hasher +hashers +heiko +hydradx +idents +inherents instanceof -typeof +integritee +kabocha +kapex +karura +katal +katalchain keypair -todos -polkassembly -syncer +kico +kint +kintsugi +kton +kulupu +kusama +lami +layr +lipsum +litentry +lldb +ltex +Lxkf +mathchain +matklad +mdbook +merkle micnncim -stalebot -pendings -deref -notif -finalised -childstate -offchain -unfollow +monomorphization +moonriver +morfologik +movr multiaddr -binaryen -westend -schnorrkel -mdbook -hashbrown -serde +multiaddress multichain +multisigs multistep -deps -combinators -sibz -callables -blake2_128Concat -runtimes -struct -wasmbuild -schnorr -ristretto -katal -katalchain -astar -edgeware -karura -lami -polymesh -polyx -integritee -teer -kulupu -darwinia -kton -stafi -alphaville -kabocha -phala -litentry -robonomics -datahighway -valiu -nodle +neatcoin +neer +neovim +nextjs +nftmart +nocompile nodl -spiritnet -mathchain +nodle +noninteractive +notif +notset +offchain +onfinality +origintrail +paka +parachain +parachains +paritytech +pdex +pendings +phala poli polimec -secp -chainx -uniarts -uart -uink -neatcoin -layr -kico -dico -xxnetwork -hydradx -aventus -genshiro polkadex -pdex polkadexparachain -fragnova -polkasmith +polkadot polkafoundry -origintrail +polkagen +polkasmith +polkassembly +polymesh +polyx pontem -heiko -tnkr -neer -efinity -glmr -moonriver -movr -ajuna -ajun -bajun -baju -kapex -kintsugi -kint -tidefi -tifi -dentnet -dentx -tcess -contextfree -nftmart -ferdie -nextjs -demux -demuxing -datetime -multiaddress -cdylib +precommits +prevotes +println +ristretto rlib -alloc +roadmap +robonomics +rotr +runtimes +rustup +schnorr +schnorrkel +secp +serayuzgur +serde +shiki +sibz +smoldot +spiritnet +stafi +stalebot +statemigration +statemine +statemint +struct +subpaths +sufficients +suri +syncer syncstate -typegen -Lxkf -Tpeyg -getrandom -Curto -devcontainers -noninteractive -autoremove -trufflesecurity tailaiw -trufflehog -amannn -multisigs -curto -zombienet -ltex -morfologik -neovim +tcess +teer +tidefi +tifi +timepoint +tnkr +todos +tomaka tostring -framesystem -statemigration -statemine +Tpeyg +transcoders +trufflehog +trufflesecurity +twox +typebox +typegen +typeof +uart +uink +unfollow +uniarts +unsanitized +vadimcn +valiu +wasmbuild +westend westmint -chainspec -chainspecs -contramap -merkle -dispatchable xxhash -polkagen -rotr -codegened -timepoint -suri -prevotes -precommits -inherents -bootnode -nocompile +xxnetwork +zio +zombienet