Skip to content

Commit

Permalink
Merge pull request #21 from ChainSafe/willem/signer-provider-impl
Browse files Browse the repository at this point in the history
Implementation of signer-provider wrapper
  • Loading branch information
willemolding authored Sep 20, 2022
2 parents 8af580d + d6a0ebf commit 78e457d
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 31 deletions.
108 changes: 87 additions & 21 deletions protocol/substrate/signer-provider-js/src/__tests__/e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { PolywrapClient } from "@polywrap/client-js";
import { substrateSignerProviderPlugin } from "../";
import { enableFn } from "./mockExtensionInjector";
import { injectExtension } from '@polkadot/extension-inject';
import { web3Accounts, web3Enable, web3FromSource } from '@polkadot/extension-dapp';
import { stringToHex } from "@polkadot/util";
import { u8aToHex } from "@polkadot/util";
import { cryptoWaitReady, decodeAddress, signatureVerify } from '@polkadot/util-crypto';
import { TypeRegistry } from '@polkadot/types';

import { Account } from '../wrap';
import { substrateSignerProviderPlugin } from "../";
import { enableFn } from "./mockExtensionInjector";
import { Account, SignerResult } from '../wrap';
import { testPayload } from './testPayload';

describe("e2e", () => {

Expand Down Expand Up @@ -42,24 +44,88 @@ describe("e2e", () => {
expect(accounts[0].meta.name).toBe("alice");
});

it("Can use injected provider to sign data (smoke test)", async () => {
await web3Enable('e2e testing dApp');
const allAccounts = await web3Accounts();
const account = allAccounts[0];
const injector = await web3FromSource(account.meta.source);
const signRaw = injector?.signer?.signRaw;

if (!!signRaw) {
// WHY DO I NEED TO BIND HERE??
const { signature } = await signRaw.bind(injector.signer)({
address: account.address,
data: stringToHex('message to sign'),
type: 'bytes'
});
console.log(signature);
}
it("signRaw produces a valid signature from test account", async () => {
const account = await getAccount();
const data = "123"; // to be signed

const result = await client.invoke({
uri,
method: "signRaw",
args: { payload: { address: account.address, data } }
});

expect(result.error).toBeFalsy();
expect(result.data).toBeTruthy();
const signerResult = result.data as SignerResult;
expect(isValidSignature(data, signerResult.signature, account.address));
});

it("signRaw throws if an unmanaged account address is requested", async () => {
const unmanagedAddress = "000000000000000000000000000000000000000000000000";

const result = await client.invoke({
uri,
method: "signRaw",
args: { payload: { address: unmanagedAddress } }
});

expect(result.error?.message).toContain("Provider does not contain account: "+ unmanagedAddress);
});

it("signPayload produces a valid signature from test account", async () => {
const account = await getAccount();
const payload = testPayload(account.address)
const result = await client.invoke({
uri,
method: "signPayload",
args: { payload }
});

expect(result.error).toBeFalsy();
expect(result.data).toBeTruthy();
const signerResult = result.data as SignerResult;

// To verify the signature encode the extrinsic payload as hex
// then veify as with signRaw
const registry = new TypeRegistry();
const encodedPayload = registry
.createType('ExtrinsicPayload', payload, { version: payload.version })
.toHex();

expect(isValidSignature(encodedPayload, signerResult.signature, account.address));
});

it("signPayload throws if an unmanaged account address is requested", async () => {
const unmanagedAddress = "000000000000000000000000000000000000000000000000";

const result = await client.invoke({
uri,
method: "signPayload",
args: { payload: { address: unmanagedAddress } }
});

expect(result.error?.message).toContain("Provider does not contain account: "+ unmanagedAddress);
});

// -- helpers -- //

async function getAccount(): Promise<Account> {
const accountsResult = await client.invoke({
uri,
method: "getAccounts",
args: {},
});
const accounts: Account[] = accountsResult.data as Account[];
return accounts[0]
}

async function isValidSignature(signedMessage: string, signature: string, address: string): Promise<boolean> {
await cryptoWaitReady();
const publicKey = decodeAddress(address);
const hexPublicKey = u8aToHex(publicKey);
return signatureVerify(signedMessage, signature, hexPublicKey).isValid;
}

});


19 changes: 19 additions & 0 deletions protocol/substrate/signer-provider-js/src/__tests__/testPayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SignerPayloadJSON } from '../wrap';

/**
* Produces a minimal valid extrinsic payload in JSON form that can be used in testing
*/
export const testPayload = (address: string): SignerPayloadJSON => ({
address,
blockHash: "0x91820de8e05dc861baa91d75c34b23ac778f5fb4a88bd9e8480dbe3850d19a26",
blockNumber: "0",
era: "0x0703",
genesisHash: "0x91820de8e05dc861baa91d75c34b23ac778f5fb4a88bd9e8480dbe3850d19a26",
method: "0x0900142248692122",
nonce: "0",
specVersion: "1",
tip: "0",
transactionVersion: "",
signedExtensions: [],
version: 4,
});
50 changes: 43 additions & 7 deletions protocol/substrate/signer-provider-js/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import {
Args_getAccounts,
Args_signAndSubmitExtrinsic,
Args_signPayload,
Args_signRaw,
Module,
manifest,
SignerResult,
Account
} from "./wrap";

import { Client, PluginFactory } from "@polywrap/core-js";
import { web3Accounts, web3Enable } from '@polkadot/extension-dapp';
import { web3Accounts, web3Enable, web3FromSource } from '@polkadot/extension-dapp';
import type { Signer } from '@polkadot/api/types';

export interface SubstrateSignerProviderPluginConfig {}

Expand All @@ -27,15 +29,30 @@ export class SubstrateSignerProviderPlugin extends Module<SubstrateSignerProvide
return await web3Accounts();
}

async signAndSubmitExtrinsic(
args: Args_signAndSubmitExtrinsic,
async signPayload(
{ payload }: Args_signPayload,
client: Client
): Promise<SignerResult> {
await this._enableProvider();
return {
id: 0,
signature: "foo"
const { address } = payload;
const signer = await this._getSigner(address);
if (!signer || !signer?.signPayload) {
throw new Error("Provider for account: " + address + " does not have payload signing capabilities");
}
return signer.signPayload(payload);
}

async signRaw(
{ payload }: Args_signRaw,
client: Client
): Promise<SignerResult> {
await this._enableProvider();
const { address } = payload;
const signer = await this._getSigner(address);
if (!signer || !signer?.signRaw) {
throw new Error("Provider for account: " + address + " does not have raw signing capabilities");
}
return signer.signRaw({ ...payload, type: 'bytes' });
}

private async _enableProvider(): Promise<void> {
Expand All @@ -45,6 +62,25 @@ export class SubstrateSignerProviderPlugin extends Module<SubstrateSignerProvide
await web3Enable("substrate-signer-provider-plugin");
this._isProviderEnabled = true;
}


/**
* Searches the injected web3 providers for any accounts that match the
* given address and then return associated Signer.
*
* Requires this._enableProvider() be called first
*/
private async _getSigner(address: String): Promise<Signer> {
const accounts = await await web3Accounts();
const signingAccount = accounts.find(acc => acc.address == address);

if (!signingAccount) {
throw new Error("Provider does not contain account: " + address);
}

const injector = await web3FromSource(signingAccount.meta.source);
return injector?.signer
}
}

export const substrateSignerProviderPlugin: PluginFactory<SubstrateSignerProviderPluginConfig> = (
Expand Down
95 changes: 92 additions & 3 deletions protocol/substrate/signer-provider-js/src/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
type Module {
getAccounts: [Account!]!

signAndSubmitExtrinsic(
address: String!
data: Bytes!
signPayload(
payload: SignerPayloadJSON!
): SignerResult!

signRaw(
payload: SignerPayloadRaw!
): SignerResult!
}

Expand All @@ -20,7 +23,93 @@ type AccountMetadata {
source: String!
}

type SignerPayloadJSON {
"""
The ss-58 encoded address
"""
address: String!

"""
The checkpoint hash of the block, in hex
"""
blockHash: String!

"""
The checkpoint block number, in hex
"""
blockNumber: String!

"""
The era for this transaction, in hex
"""
era: String!

"""
The genesis hash of the chain, in hex
"""
genesisHash: String!

"""
The encoded method (with arguments) in hex
"""
method: String!

"""
The nonce for this transaction, in hex
"""
nonce: String!

"""
The current spec version for the runtime
"""
specVersion: String!

"""
The tip for this transaction, in hex
"""
tip: String!

"""
The current transaction version for the runtime
"""
transactionVersion: String!

"""
The applicable signed extensions for this runtime
"""
signedExtensions: [String!]!

"""
The version of the extrinsic we are dealing with
"""
version: UInt32!
}

type SignerPayloadRaw {
"""
The hex-encoded data for this request
"""
data: String!

"""
The ss-58 encoded address
"""
address: String!

"""
The type of the contained data ('bytes' | 'payload')
"""
type: String!
}

type SignerResult {
"""
The id for this request
"""
id: UInt32!

"""
The resulting signature in hex
"""
signature: String!
}

0 comments on commit 78e457d

Please sign in to comment.