feat: Add support for the Ledger Generic App (#1408)
* initial commit * issue with payload * test commit * cleanup * Update signer and get result back to client * Cleanup * removed getMetadataRaw and useMetadataRaw * updated metadata update message * added assert checks * updated prefix search for Ledger * fixed ledger account import --------- Co-authored-by: tarikgul <tariksnow37@gmail.com>
This commit is contained in:
parent
d751a923c5
commit
d2f164084c
@ -409,14 +409,14 @@ export default class Extension {
|
||||
return true;
|
||||
}
|
||||
|
||||
private signingApproveSignature ({ id, signature }: RequestSigningApproveSignature): boolean {
|
||||
private signingApproveSignature ({ id, signature, signedTransaction }: RequestSigningApproveSignature): boolean {
|
||||
const queued = this.#state.getSignRequest(id);
|
||||
|
||||
assert(queued, 'Unable to find request');
|
||||
|
||||
const { resolve } = queued;
|
||||
|
||||
resolve({ id, signature });
|
||||
resolve({ id, signature, signedTransaction });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -299,6 +299,7 @@ export interface RequestSigningApprovePassword {
|
||||
export interface RequestSigningApproveSignature {
|
||||
id: string;
|
||||
signature: HexString;
|
||||
signedTransaction?: HexString;
|
||||
}
|
||||
|
||||
export interface RequestSigningCancel {
|
||||
@ -358,6 +359,7 @@ export type TransportResponseMessage<TMessageType extends MessageTypes> =
|
||||
export interface ResponseSigning {
|
||||
id: string;
|
||||
signature: HexString;
|
||||
signedTransaction?: HexString;
|
||||
}
|
||||
|
||||
export interface ResponseDeriveValidate {
|
||||
|
||||
@ -26,7 +26,7 @@ export function metadataExpand (definition: MetadataDef, isPartial = false): Cha
|
||||
return cached;
|
||||
}
|
||||
|
||||
const { chain, genesisHash, icon, metaCalls, specVersion, ss58Format, tokenDecimals, tokenSymbol, types, userExtensions } = definition;
|
||||
const { chain, genesisHash, icon, metaCalls, rawMetadata, specVersion, ss58Format, tokenDecimals, tokenSymbol, types, userExtensions } = definition;
|
||||
const registry = new TypeRegistry();
|
||||
|
||||
if (!isPartial) {
|
||||
@ -41,8 +41,8 @@ export function metadataExpand (definition: MetadataDef, isPartial = false): Cha
|
||||
|
||||
const hasMetadata = !!metaCalls && !isPartial;
|
||||
|
||||
if (hasMetadata) {
|
||||
registry.setMetadata(new Metadata(registry, base64Decode(metaCalls)), undefined, userExtensions);
|
||||
if (hasMetadata || !!rawMetadata) {
|
||||
registry.setMetadata(new Metadata(registry, hasMetadata ? base64Decode(metaCalls) : rawMetadata), undefined, userExtensions);
|
||||
}
|
||||
|
||||
const isUnknown = genesisHash === '0x';
|
||||
|
||||
@ -66,6 +66,7 @@ export interface MetadataDef extends MetadataDefBase {
|
||||
tokenSymbol: string;
|
||||
types: Record<string, Record<string, string> | string>;
|
||||
metaCalls?: string;
|
||||
rawMetadata?: HexString;
|
||||
userExtensions?: ExtDef;
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@paraspell/xcm-analyser": "^1.3.0",
|
||||
"@polkadot-api/merkleize-metadata": "^1.1.0",
|
||||
"@polkadot/api": "^12.2.1",
|
||||
"@polkadot/extension-base": "0.49.4-0-x",
|
||||
"@polkadot/extension-chains": "0.49.4-0-x",
|
||||
|
||||
@ -70,7 +70,7 @@ function Request ({ className, metaId, request, url }: Props): React.ReactElemen
|
||||
</Table>
|
||||
<div className='requestInfo'>
|
||||
<Warning className='requestWarning'>
|
||||
{t('This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata.')}
|
||||
{t('This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata. It will also allow the use of Ledger\'s Generic Polkadot App.')}
|
||||
</Warning>
|
||||
<Button
|
||||
className='btnAccept'
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
// Copyright 2019-2024 @polkadot/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ExtrinsicPayload } from '@polkadot/types/interfaces';
|
||||
import type { Chain } from '@polkadot/extension-chains/types';
|
||||
import type { SignerPayloadJSON } from '@polkadot/types/types';
|
||||
import type { HexString } from '@polkadot/util/types';
|
||||
|
||||
import { faSync } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { assert, objectSpread, u8aToHex } from '@polkadot/util';
|
||||
import { merkleizeMetadata } from '@polkadot-api/merkleize-metadata';
|
||||
|
||||
import { Button, Warning } from '../../components/index.js';
|
||||
import { useLedger, useTranslation } from '../../hooks/index.js';
|
||||
import { useLedger, useMetadata, useTranslation } from '../../hooks/index.js';
|
||||
import { styled } from '../../styled.js';
|
||||
|
||||
interface Props {
|
||||
@ -18,21 +22,44 @@ interface Props {
|
||||
className?: string;
|
||||
error: string | null;
|
||||
genesisHash?: string;
|
||||
onSignature?: ({ signature }: { signature: HexString }) => void;
|
||||
payload?: ExtrinsicPayload;
|
||||
onSignature?: ({ signature }: { signature: HexString }, signedTransaction: HexString) => void;
|
||||
payload?: SignerPayloadJSON;
|
||||
setError: (value: string | null) => void;
|
||||
}
|
||||
|
||||
function getMetadataProof (chain: Chain, payload: SignerPayloadJSON) {
|
||||
const m = chain.definition.rawMetadata;
|
||||
|
||||
assert(m, 'To sign with Ledger\'s Polkadot Generic App, the metadata must be present in the extension.');
|
||||
|
||||
const merkleizedMetadata = merkleizeMetadata(m, {
|
||||
base58Prefix: chain.ss58Format,
|
||||
decimals: chain.tokenDecimals,
|
||||
specName: chain.name.toLowerCase(),
|
||||
specVersion: chain.specVersion,
|
||||
tokenSymbol: chain.tokenSymbol
|
||||
});
|
||||
const metadataHash = u8aToHex(merkleizedMetadata.digest());
|
||||
const newPayload = objectSpread<SignerPayloadJSON>({}, payload, { metadataHash, mode: 1 });
|
||||
const raw = chain.registry.createType('ExtrinsicPayload', newPayload);
|
||||
|
||||
return {
|
||||
raw,
|
||||
txMetadata: merkleizedMetadata.getProofForExtrinsicPayload(u8aToHex(raw.toU8a(true)))
|
||||
};
|
||||
}
|
||||
|
||||
function LedgerSign ({ accountIndex, addressOffset, className, error, genesisHash, onSignature, payload, setError }: Props): React.ReactElement<Props> {
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const chain = useMetadata(genesisHash);
|
||||
const { error: ledgerError, isLoading: ledgerLoading, isLocked: ledgerLocked, ledger, refresh, warning: ledgerWarning } = useLedger(genesisHash, accountIndex, addressOffset);
|
||||
|
||||
useEffect(() => {
|
||||
if (ledgerError) {
|
||||
setError(ledgerError);
|
||||
}
|
||||
}, [ledgerError, setError]);
|
||||
}, [chain, ledgerError, setError]);
|
||||
|
||||
const _onRefresh = useCallback(() => {
|
||||
refresh();
|
||||
@ -41,21 +68,43 @@ function LedgerSign ({ accountIndex, addressOffset, className, error, genesisHas
|
||||
|
||||
const _onSignLedger = useCallback(
|
||||
(): void => {
|
||||
if (!ledger || !payload || !onSignature) {
|
||||
if (!ledger || !payload || !onSignature || !chain) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chain?.definition.rawMetadata) {
|
||||
setError('No metadata found for this chain. You must upload the metadata to the extension in order to use Ledger.');
|
||||
}
|
||||
|
||||
const { raw, txMetadata } = getMetadataProof(chain, payload);
|
||||
|
||||
const metaBuff = Buffer.from(txMetadata);
|
||||
|
||||
setError(null);
|
||||
setIsBusy(true);
|
||||
ledger.sign(payload.toU8a(true), accountIndex, addressOffset)
|
||||
ledger.signWithMetadata(raw.toU8a(true), accountIndex, addressOffset, { metadata: metaBuff })
|
||||
.then((signature) => {
|
||||
onSignature(signature);
|
||||
const extrinsic = chain.registry.createType(
|
||||
'Extrinsic',
|
||||
{ method: raw.method },
|
||||
{ version: 4 }
|
||||
);
|
||||
|
||||
ledger.getAddress(chain.ss58Format, false, accountIndex, addressOffset)
|
||||
.then(({ address }) => {
|
||||
extrinsic.addSignature(address, signature.signature, raw.toHex());
|
||||
onSignature(signature, extrinsic.toHex());
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
setError(e.message);
|
||||
setIsBusy(false);
|
||||
});
|
||||
}).catch((e: Error) => {
|
||||
setError(e.message);
|
||||
setIsBusy(false);
|
||||
});
|
||||
},
|
||||
[accountIndex, addressOffset, ledger, onSignature, payload, setError]
|
||||
[accountIndex, addressOffset, chain, ledger, onSignature, payload, setError]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -68,8 +68,8 @@ export default function Request ({ account: { accountIndex, addressOffset, genes
|
||||
}, [request]);
|
||||
|
||||
const _onSignature = useCallback(
|
||||
({ signature }: { signature: HexString }): void => {
|
||||
approveSignSignature(signId, signature)
|
||||
({ signature }: { signature: HexString }, signedTransaction?: HexString): void => {
|
||||
approveSignSignature(signId, signature, signedTransaction)
|
||||
.then(() => onAction())
|
||||
.catch((error: Error): void => {
|
||||
setError(error.message);
|
||||
@ -117,7 +117,7 @@ export default function Request ({ account: { accountIndex, addressOffset, genes
|
||||
error={error}
|
||||
genesisHash={json.genesisHash}
|
||||
onSignature={_onSignature}
|
||||
payload={payload}
|
||||
payload={json}
|
||||
setError={setError}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -6,13 +6,16 @@
|
||||
/* eslint-disable deprecation/deprecation */
|
||||
|
||||
import type { Network } from '@polkadot/networks/types';
|
||||
import type { HexString } from '@polkadot/util/types';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Ledger } from '@polkadot/hw-ledger';
|
||||
import { LedgerGeneric } from '@polkadot/hw-ledger';
|
||||
import { knownLedger } from '@polkadot/networks/defaults';
|
||||
import { settings } from '@polkadot/ui-settings';
|
||||
import { assert } from '@polkadot/util';
|
||||
|
||||
import chains from '../../../extension-ui/src/util/chains';
|
||||
import ledgerChains from '../util/legerChains.js';
|
||||
import useTranslation from './useTranslation.js';
|
||||
|
||||
@ -26,7 +29,7 @@ interface State extends StateBase {
|
||||
error: string | null;
|
||||
isLoading: boolean;
|
||||
isLocked: boolean;
|
||||
ledger: Ledger | null;
|
||||
ledger: LedgerGeneric | null;
|
||||
refresh: () => void;
|
||||
warning: string | null;
|
||||
}
|
||||
@ -44,8 +47,8 @@ function getState (): StateBase {
|
||||
};
|
||||
}
|
||||
|
||||
function retrieveLedger (genesis: string): Ledger {
|
||||
let ledger: Ledger | null = null;
|
||||
function retrieveLedger (genesis: string): LedgerGeneric {
|
||||
let ledger: LedgerGeneric | null = null;
|
||||
|
||||
const { isLedgerCapable } = getState();
|
||||
|
||||
@ -55,7 +58,11 @@ function retrieveLedger (genesis: string): Ledger {
|
||||
|
||||
assert(def, 'There is no known Ledger app available for this chain');
|
||||
|
||||
ledger = new Ledger('webusb', def.network);
|
||||
assert(def.slip44, 'Slip44 is not available for this network, please report an issue to update this chains slip44');
|
||||
|
||||
// All chains use the `slip44` from polkadot in their derivation path in ledger.
|
||||
// This interface is specific to the underlying PolkadotGenericApp.
|
||||
ledger = new LedgerGeneric('webusb', def.network, knownLedger['polkadot']);
|
||||
|
||||
return ledger;
|
||||
}
|
||||
@ -68,6 +75,7 @@ export default function useLedger (genesis?: string | null, accountIndex = 0, ad
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [address, setAddress] = useState<string | null>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ledger = useMemo(() => {
|
||||
setError(null);
|
||||
setIsLocked(false);
|
||||
@ -102,7 +110,15 @@ export default function useLedger (genesis?: string | null, accountIndex = 0, ad
|
||||
setError(null);
|
||||
setWarning(null);
|
||||
|
||||
ledger.getAddress(false, accountIndex, addressOffset)
|
||||
// This is used with a genesisHash only when importing the Ledger account
|
||||
// and when signing with Ledger. In both cases, the genesisHash is known and
|
||||
// will be in this array.
|
||||
const chosenNetwork = chains.find(({ genesisHash }) => genesisHash === genesis as HexString);
|
||||
|
||||
// Just in case, but this shouldn't be triggered
|
||||
assert(chosenNetwork, t('This network is not available, please report an issue to update the known chains'));
|
||||
|
||||
ledger.getAddress(chosenNetwork.ss58Format, false, accountIndex, addressOffset)
|
||||
.then((res) => {
|
||||
setIsLoading(false);
|
||||
setAddress(res.address);
|
||||
|
||||
@ -118,8 +118,8 @@ export async function approveSignPassword (id: string, savePass: boolean, passwo
|
||||
return sendMessage('pri(signing.approve.password)', { id, password, savePass });
|
||||
}
|
||||
|
||||
export async function approveSignSignature (id: string, signature: HexString): Promise<boolean> {
|
||||
return sendMessage('pri(signing.approve.signature)', { id, signature });
|
||||
export async function approveSignSignature (id: string, signature: HexString, signedTransaction?: HexString): Promise<boolean> {
|
||||
return sendMessage('pri(signing.approve.signature)', { id, signature, signedTransaction });
|
||||
}
|
||||
|
||||
export async function createAccountExternal (name: string, address: string, genesisHash: HexString | null): Promise<boolean> {
|
||||
|
||||
@ -68,7 +68,6 @@
|
||||
"decimals": "",
|
||||
"symbol": "",
|
||||
"upgrade": "",
|
||||
"This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata.": "",
|
||||
"Yes, do this metadata update": "",
|
||||
"Reject": "",
|
||||
"Restore from JSON": "",
|
||||
@ -176,5 +175,8 @@
|
||||
"Is your ledger locked?": "",
|
||||
"App \"{{network}}\" does not seem to be open": "",
|
||||
"Ledger error: {{errorMessage}}": "",
|
||||
"assetId": ""
|
||||
"assetId": "",
|
||||
"This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata. It will also allow the use of Ledger's Generic Polkadot App.": "",
|
||||
"No metadata found for this chain. You must upload the metadata to the extension in order to use Ledger.": "",
|
||||
"This network is not available, please report an issue to update the known chains": ""
|
||||
}
|
||||
|
||||
30
yarn.lock
30
yarn.lock
@ -668,6 +668,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polkadot-api/merkleize-metadata@npm:^1.1.0":
|
||||
version: 1.1.1
|
||||
resolution: "@polkadot-api/merkleize-metadata@npm:1.1.1"
|
||||
dependencies:
|
||||
"@polkadot-api/substrate-bindings": "npm:0.6.1"
|
||||
"@polkadot-api/utils": "npm:0.1.1"
|
||||
checksum: 10/dc0d1aa9e15049fdf263451ea1a8d7a4c5c5907e8b9b7ac8861f2e3281a82e99b5b07619a2c5cab105d8c14253007a006a7bc9d44fcaef1eb1d0356fc63d0485
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polkadot-api/metadata-builders@npm:0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "@polkadot-api/metadata-builders@npm:0.0.1"
|
||||
@ -704,6 +714,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polkadot-api/substrate-bindings@npm:0.6.1":
|
||||
version: 0.6.1
|
||||
resolution: "@polkadot-api/substrate-bindings@npm:0.6.1"
|
||||
dependencies:
|
||||
"@noble/hashes": "npm:^1.3.1"
|
||||
"@polkadot-api/utils": "npm:0.1.1"
|
||||
"@scure/base": "npm:^1.1.1"
|
||||
scale-ts: "npm:^1.6.0"
|
||||
checksum: 10/bce8bd7758174302b9e72da1addce04103ab0e9d0626ff2eacffdfb28c7a058071edaf4b47a7569bd7c8c73dd1595e1d78f90d419abafb86a6eadf078c83d1e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polkadot-api/substrate-client@npm:0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "@polkadot-api/substrate-client@npm:0.0.1"
|
||||
@ -718,6 +740,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polkadot-api/utils@npm:0.1.1":
|
||||
version: 0.1.1
|
||||
resolution: "@polkadot-api/utils@npm:0.1.1"
|
||||
checksum: 10/ffd30039cd91396abb5528132bea9a72465eae9cdd088ecbd463ce962aa51c01b9e8c7d3e64c16c8438c0e55966c4d36fe4d18744a01b2b61f7e778f61dbf2b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polkadot/api-augment@npm:12.2.1":
|
||||
version: 12.2.1
|
||||
resolution: "@polkadot/api-augment@npm:12.2.1"
|
||||
@ -996,6 +1025,7 @@ __metadata:
|
||||
"@fortawesome/free-solid-svg-icons": "npm:^6.5.1"
|
||||
"@fortawesome/react-fontawesome": "npm:^0.2.0"
|
||||
"@paraspell/xcm-analyser": "npm:^1.3.0"
|
||||
"@polkadot-api/merkleize-metadata": "npm:^1.1.0"
|
||||
"@polkadot/api": "npm:^12.2.1"
|
||||
"@polkadot/dev-test": "npm:^0.79.3"
|
||||
"@polkadot/extension-base": "npm:0.49.4-0-x"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user