Added code to support custom signed extension/user extension (#670)
* Added code to support custom signed extension/user extension * fix lint issues * added test * as per review comment * fix lint * as per review suggestion * fix lint * as per review comments * fix lint
This commit is contained in:
parent
fc9852327b
commit
82d0019b12
@ -15,9 +15,6 @@ export default class RequestExtrinsicSign implements RequestSign {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sign (registry: TypeRegistry, pair: KeyringPair): { signature: string } {
|
sign (registry: TypeRegistry, pair: KeyringPair): { signature: string } {
|
||||||
// inject the current signed extensions for encoding
|
|
||||||
registry.setSignedExtensions(this.payload.signedExtensions);
|
|
||||||
|
|
||||||
return registry
|
return registry
|
||||||
.createType('ExtrinsicPayload', this.payload, { version: this.payload.version })
|
.createType('ExtrinsicPayload', this.payload, { version: this.payload.version })
|
||||||
.sign(pair);
|
.sign(pair);
|
||||||
|
|||||||
@ -3,15 +3,25 @@
|
|||||||
|
|
||||||
import '../../../../../__mocks__/chrome';
|
import '../../../../../__mocks__/chrome';
|
||||||
|
|
||||||
|
import type { ExtDef } from '@polkadot/types/extrinsic/signedExtensions/types';
|
||||||
|
import type { SignerPayloadJSON } from '@polkadot/types/types';
|
||||||
|
|
||||||
|
import { ResponseSigning } from '@polkadot/extension-base/background/types';
|
||||||
|
import { MetadataDef } from '@polkadot/extension-inject/types';
|
||||||
|
import { KeyringPair } from '@polkadot/keyring/types';
|
||||||
|
import { TypeRegistry } from '@polkadot/types';
|
||||||
import keyring from '@polkadot/ui-keyring';
|
import keyring from '@polkadot/ui-keyring';
|
||||||
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
||||||
|
|
||||||
import { AccountsStore } from '../../stores';
|
import { AccountsStore } from '../../stores';
|
||||||
import Extension from './Extension';
|
import Extension from './Extension';
|
||||||
import State from './State';
|
import State, { AuthUrls } from './State';
|
||||||
|
import Tabs from './Tabs';
|
||||||
|
|
||||||
describe('Extension', () => {
|
describe('Extension', () => {
|
||||||
let extension: Extension;
|
let extension: Extension;
|
||||||
|
let state: State;
|
||||||
|
let tabs: Tabs;
|
||||||
const suri = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
|
const suri = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
|
||||||
const password = 'passw0rd';
|
const password = 'passw0rd';
|
||||||
|
|
||||||
@ -19,8 +29,20 @@ describe('Extension', () => {
|
|||||||
await cryptoWaitReady();
|
await cryptoWaitReady();
|
||||||
|
|
||||||
keyring.loadAll({ store: new AccountsStore() });
|
keyring.loadAll({ store: new AccountsStore() });
|
||||||
|
const authUrls: AuthUrls = {};
|
||||||
|
|
||||||
return new Extension(new State());
|
authUrls['localhost:3000'] = {
|
||||||
|
count: 0,
|
||||||
|
id: '11',
|
||||||
|
isAllowed: true,
|
||||||
|
origin: 'example.com',
|
||||||
|
url: 'http://localhost:3000'
|
||||||
|
};
|
||||||
|
localStorage.setItem('authUrls', JSON.stringify(authUrls));
|
||||||
|
state = new State();
|
||||||
|
tabs = new Tabs(state);
|
||||||
|
|
||||||
|
return new Extension(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
const createAccount = async (): Promise<string> => {
|
const createAccount = async (): Promise<string> => {
|
||||||
@ -142,4 +164,233 @@ describe('Extension', () => {
|
|||||||
}).toThrowError('Unable to decode using the supplied passphrase');
|
}).toThrowError('Unable to decode using the supplied passphrase');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('custom user extension', () => {
|
||||||
|
let address: string, payload: SignerPayloadJSON, pair: KeyringPair;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
address = await createAccount();
|
||||||
|
pair = keyring.getPair(address);
|
||||||
|
pair.decodePkcs8(password);
|
||||||
|
payload = {
|
||||||
|
address,
|
||||||
|
blockHash: '0xe1b1dda72998846487e4d858909d4f9a6bbd6e338e4588e5d809de16b1317b80',
|
||||||
|
blockNumber: '0x00000393',
|
||||||
|
era: '0x3601',
|
||||||
|
genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62',
|
||||||
|
method: '0x040105fa8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4882380100',
|
||||||
|
nonce: '0x0000000000000000',
|
||||||
|
signedExtensions: ['CheckSpecVersion', 'CheckTxVersion', 'CheckGenesis', 'CheckMortality', 'CheckNonce', 'CheckWeight', 'ChargeTransactionPayment'],
|
||||||
|
specVersion: '0x00000026',
|
||||||
|
tip: null,
|
||||||
|
transactionVersion: '0x00000005',
|
||||||
|
version: 4
|
||||||
|
} as unknown as SignerPayloadJSON;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('signs with default signed extensions', async () => {
|
||||||
|
const registry = new TypeRegistry();
|
||||||
|
|
||||||
|
registry.setSignedExtensions(payload.signedExtensions);
|
||||||
|
|
||||||
|
const signatureExpected = registry
|
||||||
|
.createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair);
|
||||||
|
|
||||||
|
tabs.handle('1615191860871.5', 'pub(extrinsic.sign)', payload, 'http://localhost:3000', {} as chrome.runtime.Port)
|
||||||
|
.then((result) => {
|
||||||
|
expect((result as ResponseSigning)?.signature).toEqual(signatureExpected.signature);
|
||||||
|
}).catch((err) => console.log(err));
|
||||||
|
|
||||||
|
await expect(extension.handle('1615192072290.7', 'pri(signing.approve.password)', {
|
||||||
|
id: state.allSignRequests[0].id,
|
||||||
|
password,
|
||||||
|
savePass: false
|
||||||
|
}, {} as chrome.runtime.Port)).resolves.toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('signs with user extensions, known types', async () => {
|
||||||
|
const types = {} as unknown as Record<string, string>;
|
||||||
|
|
||||||
|
const userExtensions = {
|
||||||
|
MyUserExtension: {
|
||||||
|
extrinsic: {
|
||||||
|
assetId: 'AssetId'
|
||||||
|
},
|
||||||
|
payload: {}
|
||||||
|
}
|
||||||
|
} as unknown as ExtDef;
|
||||||
|
|
||||||
|
const meta: MetadataDef = {
|
||||||
|
chain: 'Development',
|
||||||
|
color: '#191a2e',
|
||||||
|
genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62',
|
||||||
|
icon: '',
|
||||||
|
specVersion: 38,
|
||||||
|
ss58Format: 0,
|
||||||
|
tokenDecimals: 12,
|
||||||
|
tokenSymbol: '',
|
||||||
|
types,
|
||||||
|
userExtensions
|
||||||
|
};
|
||||||
|
|
||||||
|
state.saveMetadata(meta);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
address,
|
||||||
|
blockHash: '0xe1b1dda72998846487e4d858909d4f9a6bbd6e338e4588e5d809de16b1317b80',
|
||||||
|
blockNumber: '0x00000393',
|
||||||
|
era: '0x3601',
|
||||||
|
genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62',
|
||||||
|
method: '0x040105fa8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4882380100',
|
||||||
|
nonce: '0x0000000000000000',
|
||||||
|
signedExtensions: ['MyUserExtension'],
|
||||||
|
specVersion: '0x00000026',
|
||||||
|
tip: null,
|
||||||
|
transactionVersion: '0x00000005',
|
||||||
|
version: 4
|
||||||
|
} as unknown as SignerPayloadJSON;
|
||||||
|
|
||||||
|
const registry = new TypeRegistry();
|
||||||
|
|
||||||
|
registry.setSignedExtensions(payload.signedExtensions, userExtensions);
|
||||||
|
registry.register(types);
|
||||||
|
|
||||||
|
const signatureExpected = registry
|
||||||
|
.createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair);
|
||||||
|
|
||||||
|
tabs.handle('1615191860771.5', 'pub(extrinsic.sign)', payload, 'http://localhost:3000', {} as chrome.runtime.Port)
|
||||||
|
.then((result) => {
|
||||||
|
expect((result as ResponseSigning)?.signature).toEqual(signatureExpected.signature);
|
||||||
|
}).catch((err) => console.log(err));
|
||||||
|
|
||||||
|
await expect(extension.handle('1615192062290.7', 'pri(signing.approve.password)', {
|
||||||
|
id: state.allSignRequests[0].id,
|
||||||
|
password,
|
||||||
|
savePass: false
|
||||||
|
}, {} as chrome.runtime.Port)).resolves.toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('override default signed extension', async () => {
|
||||||
|
const types = {
|
||||||
|
FeeExchangeV1: {
|
||||||
|
assetId: 'Compact<AssetId>',
|
||||||
|
maxPayment: 'Compact<Balance>'
|
||||||
|
},
|
||||||
|
PaymentOptions: {
|
||||||
|
feeExchange: 'FeeExchangeV1',
|
||||||
|
tip: 'Compact<Balance>'
|
||||||
|
}
|
||||||
|
} as unknown as Record<string, string>;
|
||||||
|
|
||||||
|
const userExtensions = {
|
||||||
|
ChargeTransactionPayment: {
|
||||||
|
extrinsic: {
|
||||||
|
transactionPayment: 'PaymentOptions'
|
||||||
|
},
|
||||||
|
payload: {}
|
||||||
|
}
|
||||||
|
} as unknown as ExtDef;
|
||||||
|
|
||||||
|
const meta: MetadataDef = {
|
||||||
|
chain: 'Development',
|
||||||
|
color: '#191a2e',
|
||||||
|
genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62',
|
||||||
|
icon: '',
|
||||||
|
specVersion: 38,
|
||||||
|
ss58Format: 0,
|
||||||
|
tokenDecimals: 12,
|
||||||
|
tokenSymbol: '',
|
||||||
|
types,
|
||||||
|
userExtensions
|
||||||
|
};
|
||||||
|
|
||||||
|
state.saveMetadata(meta);
|
||||||
|
|
||||||
|
const registry = new TypeRegistry();
|
||||||
|
|
||||||
|
registry.setSignedExtensions(payload.signedExtensions, userExtensions);
|
||||||
|
registry.register(types);
|
||||||
|
|
||||||
|
const signatureExpected = registry
|
||||||
|
.createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair);
|
||||||
|
|
||||||
|
tabs.handle('1615191860771.5', 'pub(extrinsic.sign)', payload, 'http://localhost:3000', {} as chrome.runtime.Port)
|
||||||
|
.then((result) => {
|
||||||
|
expect((result as ResponseSigning)?.signature).toEqual(signatureExpected.signature);
|
||||||
|
}).catch((err) => console.log(err));
|
||||||
|
|
||||||
|
await expect(extension.handle('1615192062290.7', 'pri(signing.approve.password)', {
|
||||||
|
id: state.allSignRequests[0].id,
|
||||||
|
password,
|
||||||
|
savePass: false
|
||||||
|
}, {} as chrome.runtime.Port)).resolves.toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('signs with user extensions, additional types', async () => {
|
||||||
|
const types = {
|
||||||
|
myCustomType: {
|
||||||
|
feeExchange: 'Compact<AssetId>',
|
||||||
|
tip: 'Compact<Balance>'
|
||||||
|
}
|
||||||
|
} as unknown as Record<string, string>;
|
||||||
|
|
||||||
|
const userExtensions = {
|
||||||
|
MyUserExtension: {
|
||||||
|
extrinsic: {
|
||||||
|
myCustomType: 'myCustomType'
|
||||||
|
},
|
||||||
|
payload: {}
|
||||||
|
}
|
||||||
|
} as unknown as ExtDef;
|
||||||
|
|
||||||
|
const meta: MetadataDef = {
|
||||||
|
chain: 'Development',
|
||||||
|
color: '#191a2e',
|
||||||
|
genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62',
|
||||||
|
icon: '',
|
||||||
|
specVersion: 38,
|
||||||
|
ss58Format: 0,
|
||||||
|
tokenDecimals: 12,
|
||||||
|
tokenSymbol: '',
|
||||||
|
types,
|
||||||
|
userExtensions
|
||||||
|
};
|
||||||
|
|
||||||
|
state.saveMetadata(meta);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
address,
|
||||||
|
blockHash: '0xe1b1dda72998846487e4d858909d4f9a6bbd6e338e4588e5d809de16b1317b80',
|
||||||
|
blockNumber: '0x00000393',
|
||||||
|
era: '0x3601',
|
||||||
|
genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62',
|
||||||
|
method: '0x040105fa8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4882380100',
|
||||||
|
nonce: '0x0000000000000000',
|
||||||
|
signedExtensions: ['MyUserExtension', 'CheckTxVersion', 'CheckGenesis', 'CheckMortality', 'CheckNonce', 'CheckWeight', 'ChargeTransactionPayment'],
|
||||||
|
specVersion: '0x00000026',
|
||||||
|
tip: null,
|
||||||
|
transactionVersion: '0x00000005',
|
||||||
|
version: 4
|
||||||
|
} as unknown as SignerPayloadJSON;
|
||||||
|
|
||||||
|
const registry = new TypeRegistry();
|
||||||
|
|
||||||
|
registry.setSignedExtensions(payload.signedExtensions, userExtensions);
|
||||||
|
registry.register(types);
|
||||||
|
|
||||||
|
const signatureExpected = registry
|
||||||
|
.createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair);
|
||||||
|
|
||||||
|
tabs.handle('1615191860771.5', 'pub(extrinsic.sign)', payload, 'http://localhost:3000', {} as chrome.runtime.Port)
|
||||||
|
.then((result) => {
|
||||||
|
expect((result as ResponseSigning)?.signature).toEqual(signatureExpected.signature);
|
||||||
|
}).catch((err) => console.log(err));
|
||||||
|
|
||||||
|
await expect(extension.handle('1615192062290.7', 'pri(signing.approve.password)', {
|
||||||
|
id: state.allSignRequests[0].id,
|
||||||
|
password,
|
||||||
|
savePass: false
|
||||||
|
}, {} as chrome.runtime.Port)).resolves.toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import type { MetadataDef } from '@polkadot/extension-inject/types';
|
import type { MetadataDef } from '@polkadot/extension-inject/types';
|
||||||
import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@polkadot/keyring/types';
|
import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@polkadot/keyring/types';
|
||||||
|
import type { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
|
||||||
import type { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
|
import type { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
|
||||||
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountBatchExport, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestBatchRestore, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types';
|
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountBatchExport, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestBatchRestore, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types';
|
||||||
|
|
||||||
@ -33,6 +34,10 @@ function transformAccounts (accounts: SubjectInfo): AccountJson[] {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isJsonPayload (value: SignerPayloadJSON | SignerPayloadRaw): value is SignerPayloadJSON {
|
||||||
|
return (value as SignerPayloadJSON).genesisHash !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export default class Extension {
|
export default class Extension {
|
||||||
readonly #cachedUnlocks: CachedUnlocks;
|
readonly #cachedUnlocks: CachedUnlocks;
|
||||||
|
|
||||||
@ -349,6 +354,21 @@ export default class Extension {
|
|||||||
pair.decodePkcs8(password);
|
pair.decodePkcs8(password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { payload } = request;
|
||||||
|
|
||||||
|
if (isJsonPayload(payload)) {
|
||||||
|
// Get the metadata for the genesisHash
|
||||||
|
const currentMetadata = this.#state.knownMetadata.find((meta: MetadataDef) =>
|
||||||
|
meta.genesisHash === payload.genesisHash);
|
||||||
|
|
||||||
|
// set the registry before calling the sign function
|
||||||
|
registry.setSignedExtensions(payload.signedExtensions, currentMetadata?.userExtensions);
|
||||||
|
|
||||||
|
if (currentMetadata) {
|
||||||
|
registry.register(currentMetadata?.types);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result = request.sign(registry, pair);
|
const result = request.sign(registry, pair);
|
||||||
|
|
||||||
if (savePass) {
|
if (savePass) {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export function metadataExpand (definition: MetadataDef, isPartial = false): Cha
|
|||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { chain, genesisHash, icon, metaCalls, specVersion, ss58Format, tokenDecimals, tokenSymbol, types } = definition;
|
const { chain, genesisHash, icon, metaCalls, specVersion, ss58Format, tokenDecimals, tokenSymbol, types, userExtensions } = definition;
|
||||||
const registry = new TypeRegistry();
|
const registry = new TypeRegistry();
|
||||||
|
|
||||||
if (!isPartial) {
|
if (!isPartial) {
|
||||||
@ -43,7 +43,10 @@ export function metadataExpand (definition: MetadataDef, isPartial = false): Cha
|
|||||||
if (metaCalls && !isPartial) {
|
if (metaCalls && !isPartial) {
|
||||||
hasMetadata = true;
|
hasMetadata = true;
|
||||||
|
|
||||||
registry.setMetadata(new Metadata(registry, base64Decode(metaCalls)));
|
const metadata = new Metadata(registry, base64Decode(metaCalls));
|
||||||
|
const signedExtensions = metadata.asLatest.extrinsic.signedExtensions.toJSON() as string[];
|
||||||
|
|
||||||
|
registry.setMetadata(metadata, signedExtensions, userExtensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import type { Signer as InjectedSigner } from '@polkadot/api/types';
|
import type { Signer as InjectedSigner } from '@polkadot/api/types';
|
||||||
import type { ProviderInterface } from '@polkadot/rpc-provider/types';
|
import type { ProviderInterface } from '@polkadot/rpc-provider/types';
|
||||||
|
import type { ExtDef } from '@polkadot/types/extrinsic/signedExtensions/types';
|
||||||
import type { KeypairType } from '@polkadot/util-crypto/types';
|
import type { KeypairType } from '@polkadot/util-crypto/types';
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
@ -62,6 +63,7 @@ export interface MetadataDef extends MetadataDefBase {
|
|||||||
tokenSymbol: string;
|
tokenSymbol: string;
|
||||||
types: Record<string, Record<string, string> | string>;
|
types: Record<string, Record<string, string> | string>;
|
||||||
metaCalls?: string;
|
metaCalls?: string;
|
||||||
|
userExtensions?: ExtDef;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InjectedMetadataKnown {
|
export interface InjectedMetadataKnown {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user