Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Issue 3306] Update signing flow with metadata #3937

Open
wants to merge 2 commits into
base: subwallet-dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 17 additions & 69 deletions packages/extension-base/src/koni/background/handlers/Extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError';
import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers';
import { createSubscription } from '@subwallet/extension-base/background/handlers/subscriptions';
import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CronReloadRequest, CrowdloanJson, ExternalRequestPromiseStatus, ExtrinsicType, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, MetadataItem, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangePriceCurrency, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestConfirmationComplete, RequestConfirmationCompleteTon, RequestConnectWalletConnect, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetTransaction, RequestKeyringExportMnemonic, RequestMaxTransferable, RequestMigratePassword, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveOSConfig, RequestSaveRecentAccount, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseFindRawMetadata, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseNftImport, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSubscribeHistory, ResponseUnlockKeyring, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, StakingType, SufficientMetadata, ThemeNames, TransactionHistoryItem, TransactionResponse, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes';

Check failure on line 10 in packages/extension-base/src/koni/background/handlers/Extension.ts

View workflow job for this annotation

GitHub Actions / Build Development Preview

'MetadataItem' is declared but its value is never read.
import { AccountAuthType, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountExport, RequestAuthorizeCancel, RequestAuthorizeReject, RequestCurrentAccountAddress, RequestMetadataApprove, RequestMetadataReject, RequestSigningApproveSignature, RequestSigningCancel, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseType, SigningRequest, WindowOpenParams } from '@subwallet/extension-base/background/types';
import { TransactionWarning } from '@subwallet/extension-base/background/warnings/TransactionWarning';
import { ALL_ACCOUNT_KEY, LATEST_SESSION, XCM_FEE_RATIO } from '@subwallet/extension-base/constants';
Expand Down Expand Up @@ -55,7 +55,6 @@
import { SwapPair, SwapQuoteResponse, SwapRequest, SwapRequestResult, SwapSubmitParams, ValidateSwapProcessParams } from '@subwallet/extension-base/types/swap';
import { _analyzeAddress, BN_ZERO, combineAllAccountProxy, createTransactionFromRLP, isSameAddress, MODULE_SUPPORT, reformatAddress, signatureToHex, Transaction as QrTransaction, transformAccounts, transformAddresses, uniqueStringArray } from '@subwallet/extension-base/utils';
import { parseContractInput, parseEvmRlp } from '@subwallet/extension-base/utils/eth/parseTransaction';
import { metadataExpand } from '@subwallet/extension-chains';
import { MetadataDef } from '@subwallet/extension-inject/types';
import { getKeypairTypeByAddress, isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring';
import { EthereumKeypairTypes, SubstrateKeypairTypes, TonKeypairTypes } from '@subwallet/keyring/types';
Expand All @@ -71,12 +70,13 @@
import { TransactionConfig } from 'web3-core';

import { SubmittableExtrinsic } from '@polkadot/api/types';
import { Metadata, TypeRegistry } from '@polkadot/types';
import { ChainProperties } from '@polkadot/types/interfaces';
import { TypeRegistry } from '@polkadot/types';
import { AnyJson, Registry, SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
import { assert, hexStripPrefix, hexToU8a, isAscii, isHex, u8aToHex } from '@polkadot/util';
import { decodeAddress, isEthereumAddress } from '@polkadot/util-crypto';

import { getSuitableRegistry, MetadataSource, MetadataWithSource } from '../utils';

export function isJsonPayload (value: SignerPayloadJSON | SignerPayloadRaw): value is SignerPayloadJSON {
return (value as SignerPayloadJSON).genesisHash !== undefined;
}
Expand Down Expand Up @@ -2516,7 +2516,7 @@
}
}

/// Signing substrate request
// Signing substrate request
private async signingApprovePasswordV2 ({ id }: RequestSigningApprovePasswordV2): Promise<boolean> {
const queued = this.#koniState.getSignRequest(id);

Expand All @@ -2527,7 +2527,7 @@

// unlike queued.account.address the following
// address is encoded with the default prefix
// which what is used for password caching mapping
// which is used for password caching mapping
const { address } = pair;

if (!pair) {
Expand All @@ -2542,77 +2542,25 @@

const { payload } = request;

let registry: Registry;
let registry: Registry = new TypeRegistry();

if (isJsonPayload(payload)) {
const [, chainInfo] = this.#koniState.findNetworkKeyByGenesisHash(payload.genesisHash);
let metadata: MetadataDef | MetadataItem | undefined;

/**
* Get the metadata for the genesisHash
* @todo: need to handle case metadata store in db
*/
metadata = this.#koniState.knownMetadata.find((meta: MetadataDef) =>
meta.genesisHash === payload.genesisHash);
const [dbMetadata, dappMetadata] = await Promise.all([
{ metadata: await this.#koniState.chainService.getMetadataByHash(payload.genesisHash), source: MetadataSource.DB },
{ metadata: this.#koniState.knownMetadata.find((meta: MetadataDef) => meta.genesisHash === payload.genesisHash), source: MetadataSource.DAPP }
]);

if (metadata) {
// we have metadata, expand it and extract the info/registry
const expanded = metadataExpand(metadata, false);
const allMetadata: MetadataWithSource[] = [
{ metadata: dbMetadata.metadata, source: dbMetadata.source },
{ metadata: dappMetadata.metadata, source: dappMetadata.source }
].filter((item) => item.metadata);

registry = expanded.registry;
registry.setSignedExtensions(payload.signedExtensions, expanded.definition.userExtensions);
if (allMetadata.length === 0) {
registry.setSignedExtensions(payload.signedExtensions);
} else {
metadata = await this.#koniState.chainService.getMetadataByHash(payload.genesisHash);

if (metadata) {
registry = new TypeRegistry();

const _metadata = new Metadata(registry, metadata.hexValue);

registry.register(metadata.types);
registry.setChainProperties(registry.createType('ChainProperties', {
ss58Format: chainInfo?.substrateInfo?.addressPrefix ?? 42,
tokenDecimals: chainInfo?.substrateInfo?.decimals,
tokenSymbol: chainInfo?.substrateInfo?.symbol
}) as unknown as ChainProperties);
registry.setMetadata(_metadata, payload.signedExtensions, metadata.userExtensions);
} else {
// we have no metadata, create a new registry
registry = new TypeRegistry();
registry.setSignedExtensions(payload.signedExtensions);
}
}

if (!metadata) {
/*
* Some networks must have metadata to signing,
* so if the chain not active (cannot use metadata from api), it must be rejected
* */
if (
chainInfo &&
(_API_OPTIONS_CHAIN_GROUP.avail.includes(chainInfo.slug) || _API_OPTIONS_CHAIN_GROUP.goldberg.includes(chainInfo.slug)) // The special case for chains that need metadata to signing
) {
// For case the chain does not have any provider
if (!Object.keys(chainInfo.providers).length) {
reject(new Error('{{chain}} network does not have any provider to connect, please update metadata from dApp'.replaceAll('{{chain}}', chainInfo.name)));

return false;
}

const isChainActive = this.#koniState.getChainStateByKey(chainInfo.slug).active;

if (!isChainActive) {
reject(new Error('Please activate {{chain}} network before signing'.replaceAll('{{chain}}', chainInfo.name)));

return false;
}

registry = this.#koniState.getSubstrateApi(chainInfo.slug).api.registry as unknown as TypeRegistry;
}
registry = getSuitableRegistry(allMetadata, payload, chainInfo, this.#koniState);
}
} else {
// for non-payload, just create a registry to use
registry = new TypeRegistry();
}

const result = request.sign(registry as unknown as TypeRegistry, pair);
Expand Down
118 changes: 118 additions & 0 deletions packages/extension-base/src/koni/background/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2019-2022 @subwallet/extension-koni authors & contributors
// SPDX-License-Identifier: Apache-2.0

import { _ChainInfo } from '@subwallet/chain-list/types';
import { MetadataItem } from '@subwallet/extension-base/background/KoniTypes';
import { metadataExpand } from '@subwallet/extension-chains/bundle';
import { MetadataDef } from '@subwallet/extension-inject/types';

import { Metadata, TypeRegistry } from '@polkadot/types';
import { ChainProperties } from '@polkadot/types/interfaces';
import { Registry, SignerPayloadJSON } from '@polkadot/types/types';

import KoniState from './handlers/State';

export enum MetadataSource {
API = 'api',
DB = 'db',
DAPP = 'dapp'
}

export interface MetadataWithSource{
metadata: MetadataDef | MetadataItem | undefined;
source: MetadataSource;
}

export interface CachedChainProperties {
ss58Format: number;
tokenDecimals: number | undefined;
tokenSymbol: string | undefined;
}

const cachedChainProperties: Map<string, Promise<CachedChainProperties | null>> = new Map();

function getChainProperties (chainInfo: _ChainInfo, genesisHash: string): Promise<CachedChainProperties | null> {
const cachedPromise = cachedChainProperties.get(genesisHash);

if (cachedPromise) {
return cachedPromise;
}

const chainPropertiesPromise = new Promise<CachedChainProperties | null>((resolve) => {
const chainProperties: CachedChainProperties = {
ss58Format: chainInfo?.substrateInfo?.addressPrefix ?? 42,
tokenDecimals: chainInfo?.substrateInfo?.decimals,
tokenSymbol: chainInfo?.substrateInfo?.symbol
};

cachedChainProperties.set(genesisHash, Promise.resolve(chainProperties));
resolve(chainProperties);
});

cachedChainProperties.set(genesisHash, chainPropertiesPromise);

return chainPropertiesPromise;
}

export function getSuitableRegistry (metadatas: MetadataWithSource[], payload: SignerPayloadJSON, chainInfo: _ChainInfo | undefined, koniState: KoniState) {
const api = chainInfo ? koniState.getSubstrateApi(chainInfo.slug).api : undefined;
const apiSpecVersion = api?.runtimeVersion.specVersion.toString();
const payloadSpecVersion = parseInt(payload.specVersion);
const extendedMetadatas = [{
metadata: { specVersion: apiSpecVersion },
source: MetadataSource.API
},
...metadatas
];

const sortedMetadatas = extendedMetadatas
.filter((meta): meta is MetadataWithSource => meta.metadata !== undefined)
.map((meta) => ({
meta: meta.metadata,
source: meta.source,
distance: Math.abs(Number(meta.metadata?.specVersion) - payloadSpecVersion),
isHigher: Number(meta.metadata?.specVersion) >= payloadSpecVersion
}))
.sort((a, b) => {
if (a.distance !== b.distance) {
return a.distance - b.distance;
}

return Number(b.meta?.specVersion) - Number(a.meta?.specVersion);
});

const closestMetadata = sortedMetadatas[0];
let registry: Registry;

if (closestMetadata.source === MetadataSource.API) {
registry = api?.registry as unknown as TypeRegistry;
} else if (closestMetadata.source === MetadataSource.DB && chainInfo) {
registry = setupDatabaseRegistry(closestMetadata.meta as MetadataItem, chainInfo, payload);
} else {
registry = setupDappRegistry(closestMetadata.meta as MetadataDef, payload);
}

return registry;
}

export function setupDatabaseRegistry (metadata: MetadataItem, chainInfo: _ChainInfo, payload: SignerPayloadJSON) {
const registry = new TypeRegistry();
const _metadata = new Metadata(registry, metadata.hexValue);

const chainProperties = getChainProperties(chainInfo, payload.genesisHash);

registry.register(metadata.types);
registry.setChainProperties(registry.createType('ChainProperties', chainProperties) as unknown as ChainProperties);
registry.setMetadata(_metadata, payload.signedExtensions, metadata.userExtensions);

return registry;
}

export function setupDappRegistry (metadata: MetadataDef, payload: SignerPayloadJSON) {
const expanded = metadataExpand(metadata, false);
const registry = expanded.registry;

registry.setSignedExtensions(payload.signedExtensions, expanded.definition.userExtensions);

return registry;
}
2 changes: 2 additions & 0 deletions packages/extension-base/src/services/chain-service/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import type { ApiInterfaceRx } from '@polkadot/api/types';

import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _CrowdloanFund } from '@subwallet/chain-list/types';
import { MetadataItem } from '@subwallet/extension-base/background/KoniTypes';
import { AccountState, TxByMsgResponse } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/types';
import { _CHAIN_VALIDATION_ERROR } from '@subwallet/extension-base/services/chain-service/handler/types';
import { TonWalletContract } from '@subwallet/keyring/types';
Expand Down Expand Up @@ -94,6 +95,7 @@ export interface _SubstrateApiState {
}

export interface _SubstrateApi extends _SubstrateApiState, _ChainBaseApi, _SubstrateApiAdapter {
metadata?: MetadataItem;
api: ApiPromise;
isReady: Promise<_SubstrateApi>;
connect: (_callbackUpdateMetadata?: (substrateApi: _SubstrateApi) => void) => void;
Expand Down
46 changes: 33 additions & 13 deletions packages/extension-base/src/utils/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { ChainService } from '@subwallet/extension-base/services/chain-service';
import { _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types';

import { ApiPromise } from '@polkadot/api';
import { getSpecExtensions, getSpecTypes } from '@polkadot/types-known';
import { u8aToHex } from '@polkadot/util';
import { HexString } from '@polkadot/util/types';
Expand All @@ -25,6 +26,29 @@ export const getShortMetadata = (blob: HexString, extraInfo: ExtraInfo, metadata
return u8aToHex(_merkleizeMetadata.getProofForExtrinsicPayload(blob));
};

const storeMetadataV15: Map<string, HexString | undefined> = new Map();

const getMetadataV15 = (api: ApiPromise) => {
const genesisHash = api.genesisHash.toHex();

api.call.metadata.metadataAtVersion(15)
.then((metadataV15) => {
if (!metadataV15.isEmpty) {
const hexValue = metadataV15.unwrap().toHex();

storeMetadataV15.set(genesisHash, hexValue);
} else {
storeMetadataV15.set(genesisHash, undefined);
}
})
.catch((err) => {
console.error('Error:', err);
storeMetadataV15.set(genesisHash, undefined);
});

return storeMetadataV15.get(genesisHash);
};

export const cacheMetadata = (
chain: string,
substrateApi: _SubstrateApi,
Expand All @@ -42,26 +66,22 @@ export const cacheMetadata = (
return;
}

const systemChain = await api.rpc.system.chain();
const systemChain = api.runtimeChain;
// const _metadata: Option<OpaqueMetadata> = await api.call.metadata.metadataAtVersion(15);
// const metadataHex = _metadata.isSome ? _metadata.unwrap().toHex().slice(2) : ''; // Need unwrap to create metadata object
let hexV15: HexString | undefined;

const metadataV15 = await api.call.metadata.metadataAtVersion(15);
const metadataHex = api.runtimeMetadata.toHex();
const metadataV15 = getMetadataV15(api);

if (!metadataV15.isEmpty) {
hexV15 = metadataV15.unwrap().toHex();
}

chainService?.upsertMetadata(chain, {
const updateMetadata = {
chain: chain,
genesisHash: genesisHash,
specName: specName,
specVersion: currentSpecVersion,
hexValue: api.runtimeMetadata.toHex(),
hexValue: metadataHex,
types: getSpecTypes(api.registry, systemChain, api.runtimeVersion.specName, api.runtimeVersion.specVersion) as unknown as Record<string, string>,
userExtensions: getSpecExtensions(api.registry, systemChain, api.runtimeVersion.specName),
hexV15
}).catch(console.error);
userExtensions: getSpecExtensions(api.registry, systemChain, api.runtimeVersion.specName)
};

chainService?.upsertMetadata(chain, { ...updateMetadata, hexV15: metadataV15 }).catch(console.error);
}).catch(console.error);
};
Loading