Skip to content

Commit

Permalink
feat: Add support for the Ledger Generic App (#1408)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
bee344 and TarikGul authored Jul 29, 2024
1 parent d751a92 commit d2f1640
Show file tree
Hide file tree
Showing 12 changed files with 130 additions and 29 deletions.
4 changes: 2 additions & 2 deletions packages/extension-base/src/background/handlers/Extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/extension-base/src/background/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ export interface RequestSigningApprovePassword {
export interface RequestSigningApproveSignature {
id: string;
signature: HexString;
signedTransaction?: HexString;
}

export interface RequestSigningCancel {
Expand Down Expand Up @@ -358,6 +359,7 @@ export type TransportResponseMessage<TMessageType extends MessageTypes> =
export interface ResponseSigning {
id: string;
signature: HexString;
signedTransaction?: HexString;
}

export interface ResponseDeriveValidate {
Expand Down
6 changes: 3 additions & 3 deletions packages/extension-chains/src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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';
Expand Down
1 change: 1 addition & 0 deletions packages/extension-inject/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface MetadataDef extends MetadataDefBase {
tokenSymbol: string;
types: Record<string, Record<string, string> | string>;
metaCalls?: string;
rawMetadata?: HexString;
userExtensions?: ExtDef;
}

Expand Down
1 change: 1 addition & 0 deletions packages/extension-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/extension-ui/src/Popup/Metadata/Request.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
69 changes: 59 additions & 10 deletions packages/extension-ui/src/Popup/Signing/LedgerSign.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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();
Expand All @@ -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 (
Expand Down Expand Up @@ -93,7 +142,7 @@ function LedgerSign ({ accountIndex, addressOffset, className, error, genesisHas
);
}

export default styled(LedgerSign)<Props>`
export default styled(LedgerSign) <Props>`
flex-direction: column;
padding: 6px 24px;
Expand Down
6 changes: 3 additions & 3 deletions packages/extension-ui/src/Popup/Signing/Request/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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}
/>
)}
Expand Down
28 changes: 22 additions & 6 deletions packages/extension-ui/src/hooks/useLedger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
}
Expand All @@ -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();

Expand All @@ -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;
}
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions packages/extension-ui/src/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
6 changes: 4 additions & 2 deletions packages/extension/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand Down Expand Up @@ -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 changes: 30 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit d2f1640

Please sign in to comment.