From 406ca8470fe81924cddce83c0d58265cab5cef87 Mon Sep 17 00:00:00 2001 From: T6 Date: Mon, 26 Sep 2022 20:30:27 -0700 Subject: [PATCH] feat: polkadot signer (#244) --- effect/atoms/$Extrinsic.ts | 2 + effect/std/submitAndWatchExtrinsic.ts | 2 +- examples/polkadot_signer.ts | 53 ++++++++++++ frame_metadata/Extrinsic.ts | 116 +++++++++++++++++++++----- known/configs.ts | 2 +- util/hex.ts | 4 + 6 files changed, 155 insertions(+), 24 deletions(-) create mode 100644 examples/polkadot_signer.ts diff --git a/effect/atoms/$Extrinsic.ts b/effect/atoms/$Extrinsic.ts index 9c36bc7d3..8923f6313 100644 --- a/effect/atoms/$Extrinsic.ts +++ b/effect/atoms/$Extrinsic.ts @@ -5,10 +5,12 @@ export const $extrinsic = atomFactory("ExtrinsicCodec", ( deriveCodec: M.DeriveCodec, metadata: M.Metadata, sign?: M.SignExtrinsic, + prefix?: number, ) => { return M.$extrinsic({ deriveCodec, metadata, sign: sign!, + prefix: prefix!, }); }); diff --git a/effect/std/submitAndWatchExtrinsic.ts b/effect/std/submitAndWatchExtrinsic.ts index 990ae0ecb..512c56ad0 100644 --- a/effect/std/submitAndWatchExtrinsic.ts +++ b/effect/std/submitAndWatchExtrinsic.ts @@ -38,7 +38,7 @@ export interface SendAndWatchExtrinsicProps { export function sendAndWatchExtrinsic(props: Props) { const metadata = a.metadata(props.config); const deriveCodec = a.deriveCodec(metadata); - const $extrinsic = a.$extrinsic(deriveCodec, metadata, props.sign); + const $extrinsic = a.$extrinsic(deriveCodec, metadata, props.sign, props.config.addressPrefix); const runtimeVersion = a.rpcCall(props.config, "state_getRuntimeVersion", []); const senderSs58 = sys.anon([props.sender], (sender) => { return ((): string => { diff --git a/examples/polkadot_signer.ts b/examples/polkadot_signer.ts new file mode 100644 index 000000000..d074c1a8c --- /dev/null +++ b/examples/polkadot_signer.ts @@ -0,0 +1,53 @@ +import { TypeRegistry } from "https://deno.land/x/polkadot@0.0.8/types/mod.ts"; +import * as C from "../mod.ts"; +import * as t from "../test-util/mod.ts"; +import * as U from "../util/mod.ts"; + +const config = await t.config(); + +const root = C + .chain(config) + .pallet("Balances") + .extrinsic("transfer") + .call({ + value: 12345n, + dest: { + type: "Id", + value: t.bob.publicKey, + }, + }) + .signed( + { + type: "Id", + value: t.alice.publicKey, + }, + { + signPayload(payload: any) { + const tr = new TypeRegistry(); + tr.setSignedExtensions(payload.signedExtensions); + return Promise.resolve( + tr + .createType("ExtrinsicPayload", payload, { version: payload.version }) + .sign(t.alice), + ); + }, + }, + ) + .sendAndWatch((stop) => { + return (event) => { + if (typeof event.params.result === "string") { + console.log("Extrinsic", event.params.result); + } else { + if (event.params.result.inBlock) { + console.log("Extrinsic in block", event.params.result.inBlock); + } else if (event.params.result.finalized) { + console.log("Extrinsic finalized as of", event.params.result.finalized); + stop(); + } else { + console.log("Misc", event.params.result); + } + } + }; + }); + +U.throwIfError(await root.run()); diff --git a/frame_metadata/Extrinsic.ts b/frame_metadata/Extrinsic.ts index e61775485..6e09900b8 100644 --- a/frame_metadata/Extrinsic.ts +++ b/frame_metadata/Extrinsic.ts @@ -1,6 +1,8 @@ import * as $ from "../deps/scale.ts"; import { assert } from "../deps/std/testing/asserts.ts"; import * as H from "../hashers/mod.ts"; +import * as ss58 from "../ss58/mod.ts"; +import { hex } from "../util/mod.ts"; import { $null, DeriveCodec } from "./Codec.ts"; import { Metadata } from "./Metadata.ts"; @@ -21,7 +23,13 @@ export interface Signature { value: Uint8Array; } -export type SignExtrinsic = (message: Uint8Array) => Signature | Promise; +export type SignExtrinsic = + | ((message: Uint8Array) => Signature | Promise) + | PolkadotSigner; + +export interface PolkadotSigner { + signPayload(payload: any): Promise<{ signature: string }>; +} export interface Extrinsic { protocolVersion: number; @@ -41,6 +49,7 @@ interface ExtrinsicCodecProps { metadata: Metadata; deriveCodec: DeriveCodec; sign: SignExtrinsic; + prefix: number; } export function $extrinsic(props: ExtrinsicCodecProps): $.Codec { @@ -53,8 +62,12 @@ export function $extrinsic(props: ExtrinsicCodecProps): $.Codec { const callTy = props.metadata.tys[callTyI]; assert(callTy?.type === "Union"); const $call = deriveCodec(callTyI); - const $extra = getExtrasCodec(signedExtensions.map((x) => x.ty)); - const $additional = getExtrasCodec(signedExtensions.map((x) => x.additionalSigned)); + const [$extra, extraPjsInfo] = getExtensionInfo(pjsExtraKeyMap, "ty"); + const [$additional, additionalPjsInfo] = getExtensionInfo( + pjsAdditionalKeyMap, + "additionalSigned", + ); + const pjsInfo = [...extraPjsInfo, ...additionalPjsInfo]; const toSignSize = $call._staticSize + $extra._staticSize + $additional._staticSize; const totalSize = 1 + $address._staticSize + $sig._staticSize + toSignSize; @@ -74,30 +87,67 @@ export function $extrinsic(props: ExtrinsicCodecProps): $.Codec { }; const { signature } = extrinsic; if (signature) { + $address._encode(buffer, signature.address); if ("additional" in signature) { - $address._encode(buffer, signature.address); const toSignBuffer = new $.EncodeBuffer(buffer.stealAlloc(toSignSize)); $call._encode(toSignBuffer, call); const callEnd = toSignBuffer.finishedSize + toSignBuffer.index; - $extra._encode(toSignBuffer, signature.extra); - const extraEnd = toSignBuffer.finishedSize + toSignBuffer.index; - $additional._encode(toSignBuffer, signature.additional); - const toSignEncoded = toSignBuffer.finish(); - const callEncoded = toSignEncoded.subarray(0, callEnd); - const extraEncoded = toSignEncoded.subarray(callEnd, extraEnd); - const toSign = toSignEncoded.length > 256 - ? H.Blake2_256.hash(toSignEncoded) - : toSignEncoded; - const sig = props.sign(toSign); - if (sig instanceof Promise) { - $sigPromise._encode(buffer, sig); + if ("signPayload" in props.sign) { + const exts = [...signature.extra, ...signature.additional]; + const extEnds = []; + for (let i = 0; i < pjsInfo.length; i++) { + pjsInfo[i]!.codec._encode(toSignBuffer, exts[i]); + extEnds.push(toSignBuffer.finishedSize + toSignBuffer.index); + } + const extraEnd = extEnds[extraPjsInfo.length - 1] ?? callEnd; + const toSignEncoded = toSignBuffer.finish(); + const callEncoded = toSignEncoded.subarray(0, callEnd); + const extraEncoded = toSignEncoded.subarray(callEnd, extraEnd); + if (signature.address.type !== "Id") { + throw new Error("polkadot signer: address types other than Id are not supported"); + } + const payload: Record = { + address: ss58.encode(props.prefix, signature.address.value), + method: hex.encodePrefixed(callEncoded), + signedExtensions: signedExtensions.map((x) => x.ident), + version: extrinsic.protocolVersion, + }; + let last = callEnd; + for (let i = 0; i < pjsInfo.length; i++) { + const { key } = pjsInfo[i]!; + if (!key) throw new Error("polkadot signer: unknown extension"); + payload[key] = typeof exts[i] === "number" + ? exts[i] + : hex.encodePrefixed(toSignEncoded.subarray(last, extEnds[i]!)); + last = extEnds[i]!; + } + const signer = props.sign; + buffer.writeAsync(0, async (buffer) => { + const { signature } = await signer.signPayload(payload); + buffer.insertArray(hex.decode(signature)); + }); + buffer.insertArray(extraEncoded); + buffer.insertArray(callEncoded); } else { - $sig._encode(buffer, sig); + $extra._encode(toSignBuffer, signature.extra); + const extraEnd = toSignBuffer.finishedSize + toSignBuffer.index; + $additional._encode(toSignBuffer, signature.additional); + const toSignEncoded = toSignBuffer.finish(); + const callEncoded = toSignEncoded.subarray(0, callEnd); + const extraEncoded = toSignEncoded.subarray(callEnd, extraEnd); + const toSign = toSignEncoded.length > 256 + ? H.Blake2_256.hash(toSignEncoded) + : toSignEncoded; + const sig = props.sign(toSign); + if (sig instanceof Promise) { + $sigPromise._encode(buffer, sig); + } else { + $sig._encode(buffer, sig); + } + buffer.insertArray(extraEncoded); + buffer.insertArray(callEncoded); } - buffer.insertArray(extraEncoded); - buffer.insertArray(callEncoded); } else { - $address._encode(buffer, signature.address); $sig._encode(buffer, signature.sig); $extra._encode(buffer, signature.extra); $call._encode(buffer, call); @@ -146,7 +196,29 @@ export function $extrinsic(props: ExtrinsicCodecProps): $.Codec { function findExtrinsicTypeParam(name: string) { return metadata.tys[metadata.extrinsic.ty]?.params.find((x) => x.name === name)?.ty; } - function getExtrasCodec(is: number[]) { - return $.tuple(...is.map((i) => deriveCodec(i)).filter((x) => x !== $null)); + function getExtensionInfo( + keyMap: Record, + key: "ty" | "additionalSigned", + ): [codec: $.Codec, pjsInfo: { key: string | undefined; codec: $.Codec }[]] { + const pjsInfo = signedExtensions + .map((e) => ({ key: keyMap[e.ident], codec: deriveCodec(e[key]) })) + .filter((x) => x.codec !== $null); + return [$.tuple(...pjsInfo.map((x) => x.codec)), pjsInfo]; } } + +const pjsExtraKeyMap: Record = { + CheckEra: "era", + CheckMortality: "era", + ChargeTransactionPayment: "tip", + CheckNonce: "nonce", +}; + +const pjsAdditionalKeyMap: Record = { + CheckEra: "blockHash", + CheckMortality: "blockHash", + CheckSpecVersion: "specVersion", + CheckTxVersion: "transactionVersion", + CheckVersion: "specVersion", + CheckGenesis: "genesisHash", +}; diff --git a/known/configs.ts b/known/configs.ts index f754ce6b9..dc376fe79 100644 --- a/known/configs.ts +++ b/known/configs.ts @@ -34,5 +34,5 @@ export const statemint = new Statemint(); export class Subsocial extends Config_("wss://para.subsocial.network", 28) {} export const subsocial = new Subsocial(); -export class Westend extends Config_("wss://westend-rpc.polkadot.io", 0) {} +export class Westend extends Config_("wss://westend-rpc.polkadot.io", 42) {} export const westend = new Westend(); diff --git a/util/hex.ts b/util/hex.ts index cf93b67f1..1ec8780d3 100644 --- a/util/hex.ts +++ b/util/hex.ts @@ -17,3 +17,7 @@ export function encode(bytes: Uint8Array): string { } return str; } + +export function encodePrefixed(bytes: Uint8Array): string { + return "0x" + encode(bytes); +}