Skip to content

Commit

Permalink
feat: Signer enhancements, merkleize-metadata for ledger (#252)
Browse files Browse the repository at this point in the history
  • Loading branch information
rossbulat authored Oct 26, 2024
2 parents a36401d + 10039cf commit d9bef35
Show file tree
Hide file tree
Showing 14 changed files with 136 additions and 74 deletions.
3 changes: 2 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@ledgerhq/hw-transport-webhid": "^6.29.2",
"@polkadot-api/merkleize-metadata": "^1.1.8",
"@polkadot-api/metadata-builders": "^0.8.2",
"@polkadot-api/observable-client": "^0.5.11",
"@polkadot-api/substrate-client": "^0.2.2",
Expand All @@ -37,7 +38,7 @@
"@walletconnect/universal-provider": "^2.15.2",
"@walletconnect/utils": "^2.15.3",
"@web3modal/wagmi": "^5.1.5",
"@zondax/ledger-substrate": "^0.44.2",
"@zondax/ledger-substrate": "^1.0.0",
"bignumber.js": "^9.1.2",
"buffer": "^6.0.3",
"compare-versions": "^6.1.1",
Expand Down
3 changes: 0 additions & 3 deletions packages/app/src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,3 @@ export const GithubRepoUrl =
'https://github.com/polkadot-cloud/developer-console';

export const DocsUrl = 'https://docs.polkadot.cloud';

export const ZondaxMetadataHashApiUrl =
'https://api.zondax.ch/polkadot/node/metadata/hash';
2 changes: 1 addition & 1 deletion packages/app/src/contexts/LedgerHardware/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const defaultLedgerHardwareContext: LedgerHardwareContextInterface = {
handleErrors: (err) => {},
handleGetAddress: (txMetadataChainId, accountIndex, ss58Prefix) =>
new Promise((resolve) => resolve()),
handleSignTx: (txMetadataChainId, uid, index, payload) =>
handleSignTx: (txMetadataChainId, uid, index, payload, txMetadata) =>
new Promise((resolve) => resolve()),
handleResetLedgerTask: () => {},
runtimesInconsistent: false,
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/contexts/LedgerHardware/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,15 @@ export const LedgerHardwareProvider = ({
txMetadataChainId: string,
uid: number,
index: number,
payload: AnyJson
payload: AnyJson,
txMetadata: AnyJson
) => {
try {
setIsExecuting(true);
const { app, productName } = await Ledger.initialise(txMetadataChainId);
setFeedback('Approve transaction on Ledger');

const result = await Ledger.signPayload(app, index, payload);
const result = await Ledger.signPayload(app, index, payload, txMetadata);

setIsExecuting(false);
setFeedback('Signed Transaction Successfully.');
Expand Down
17 changes: 10 additions & 7 deletions packages/app/src/contexts/LedgerHardware/static/ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ export class Ledger {
// Initialise ledger transport, initialise app, and return with device info.
static initialise = async (txMetadataChainId: string) => {
this.transport = await TransportWebHID.create();
const app = new PolkadotGenericApp(
Ledger.transport,
txMetadataChainId,
'https://api.zondax.ch/polkadot/transaction/metadata'
);
const app = new PolkadotGenericApp(Ledger.transport, txMetadataChainId);
const { productName } = this.transport.device;
return { app, productName };
};
Expand Down Expand Up @@ -85,12 +81,19 @@ export class Ledger {
static signPayload = async (
app: PolkadotGenericApp,
index: number,
payload: AnyJson
payload: AnyJson,
txMetadata: AnyJson
) => {
await this.ensureOpen();

const bip42Path = `m/44'/354'/${index}'/${0}'/${0}'`;
const result = await app.sign(bip42Path, Buffer.from(payload.toU8a(true)));
const buff = Buffer.from(txMetadata);

const result = await app.signWithMetadata(
bip42Path,
payload.toU8a(true),
buff
);

await this.ensureClosed();
return result;
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/contexts/LedgerHardware/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export interface LedgerHardwareContextInterface {
txMetadataChainId: string,
uid: number,
index: number,
payload: AnyJson
payload: AnyJson,
txMetadata: AnyJson
) => Promise<void>;
handleResetLedgerTask: () => void;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/contexts/TxMeta/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ export const defaultTxMeta: TxMetaContextInterface = {

getTxPayload: (instanceId) => undefined,
getTxPayloadUid: (instanceId) => 0,
getTxMetadata: (instanceId) => {},
getTxPayloadValue: (instanceId) => {},
setTxPayload: (instanceId, payload, payloadValue, uid) => {},
setTxPayload: (instanceId, txMetadata, payload, payloadValue, uid) => {},
removeTxPayload: (instanceId) => {},
incrementTxPayloadUid: (instanceId) => 1,

Expand Down
9 changes: 8 additions & 1 deletion packages/app/src/contexts/TxMeta/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,26 @@ export const TxMetaProvider = ({ children }: { children: ReactNode }) => {
const getTxPayload = (instanceId: ApiInstanceId) =>
txPayloadsRef.current[instanceId]?.payload;

// Get a JSON representation of a payload for a given api instance.
// Get a JSON representation of a payload for a given api instance.
const getTxPayloadValue = (instanceId: ApiInstanceId) =>
txPayloadsRef.current[instanceId]?.payloadValue;

// Get a uid for a given tx payload.
const getTxPayloadUid = (instanceId: ApiInstanceId) =>
txPayloadsRef.current[instanceId]?.uid;

// Get the metadata for a transaction.
const getTxMetadata = (instanceId: ApiInstanceId) =>
txPayloadsRef.current[instanceId]?.txMetadata || null;

// Increment a payload uid given an api instance.
const incrementTxPayloadUid = (instanceId: ApiInstanceId) =>
(txPayloadsRef.current[instanceId]?.uid || 0) + 1;

// Set a transaction payload and uid for a given api instance. Overwrites any existing payload.
const setTxPayload = (
instanceId: ApiInstanceId,
txMetadata: AnyJson,
payload: AnyJson,
payloadValue: AnyJson,
uid: number
Expand All @@ -108,6 +113,7 @@ export const TxMetaProvider = ({ children }: { children: ReactNode }) => {
payload,
payloadValue,
uid,
txMetadata,
},
},
setTxPayloadsState,
Expand Down Expand Up @@ -219,6 +225,7 @@ export const TxMetaProvider = ({ children }: { children: ReactNode }) => {
getTxPayload,
getTxPayloadValue,
getTxPayloadUid,
getTxMetadata,
setTxPayload,
removeTxPayload,
incrementTxPayloadUid,
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/contexts/TxMeta/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ export interface TxMetaContextInterface {
getTxPayload: (instanceId: ApiInstanceId) => AnyJson | undefined;
getTxPayloadValue: (instanceId: ApiInstanceId) => AnyJson | undefined;
getTxPayloadUid: (instanceId: ApiInstanceId) => number;
getTxMetadata: (instanceId: ApiInstanceId) => AnyJson;
setTxPayload: (
instanceId: ApiInstanceId,
txMetadata: AnyJson,
payload: AnyJson,
payloadValue: AnyJson,
uid: number
Expand All @@ -43,4 +45,5 @@ export interface TxPayload {
payload: AnyJson;
payloadValue: AnyJson;
uid: number;
txMetadata: AnyJson;
}
78 changes: 48 additions & 30 deletions packages/app/src/hooks/useBuildPayload/index.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,51 @@
// Copyright 2024 @polkadot-cloud/polkadot-developer-console authors & contributors
// SPDX-License-Identifier: AGPL-3.0

import { merkleizeMetadata } from '@polkadot-api/merkleize-metadata';
import type { AnyJson } from '@w3ux/types';
import type { UseBuildPayloadProps } from './types';
import { ZondaxMetadataHashApiUrl } from 'consts';
import { useImportedAccounts } from 'contexts/ImportedAccounts';
import type { ApiPromise } from '@polkadot/api';
import { objectSpread, u8aToHex } from '@w3ux/utils/util';

export const useBuildPayload = ({
api,
instanceId,
chainId,
unit,
ss58Prefix,
nonce: nonceRaw,
setPayload,
}: UseBuildPayloadProps) => {
const { getAccount } = useImportedAccounts();

// Request a metadata hash from Zondax API service.
const fetchMetadataHash = async () => {
const requestMetadataHash = await fetch(ZondaxMetadataHashApiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: unit }),
});
// Build metadata hash and return updated payload.
const fetchMetadataHash = async (a: ApiPromise, p: AnyJson) => {
const metadata = await a.call.metadata.metadataAtVersion(15);
const { specName, specVersion } = a.runtimeVersion;

const opts = {
base58Prefix: Number(a.consts.system.ss58Prefix.toString()),
decimals: a.registry.chainDecimals[0],
specName: specName.toString(),
specVersion: specVersion.toNumber(),
tokenSymbol: a.registry.chainTokens[0],
};

const response = await requestMetadataHash.json();
const merkleizedMetadata = merkleizeMetadata(metadata.toHex(), opts);
const metadataHash = u8aToHex(merkleizedMetadata.digest());
const payload = objectSpread({}, p, {
metadataHash,
mode: 1,
withSignedTransaction: true,
});
const newPayload = a.registry.createType('ExtrinsicPayload', payload);

return `0x${response.metadataHash}`;
return {
newPayload,
newTxMetadata: merkleizedMetadata.getProofForExtrinsicPayload(
u8aToHex(newPayload.toU8a(true))
),
};
};

// Build and set payload of the transaction and store it in TxMeta's txPayloads state.
Expand All @@ -52,9 +68,11 @@ export const useBuildPayload = ({
const nonce = api.registry.createType('Compact<Index>', nonceRaw);

// Construct the payload value.
const payload: AnyJson = {
const payloadJson: AnyJson = {
specVersion: api.runtimeVersion.specVersion.toHex(),
transactionVersion: api.runtimeVersion.transactionVersion.toHex(),
runtimeVersion: api.runtimeVersion,
version: tx.version,
address: from,
blockHash: lastHeader.hash.toHex(),
blockNumber: blockNumber.toHex(),
Expand All @@ -64,28 +82,28 @@ export const useBuildPayload = ({
nonce: nonce.toHex(),
signedExtensions: api.registry.signedExtensions,
tip: api.registry.createType('Compact<Balance>', 0).toHex(),
version: tx.version,
};

let payload;
let txMetadata = null;

// If the source is `ledger`, add the metadata hash to the payload.
if (source === 'ledger') {
const metadataHash = await fetchMetadataHash();
payload.mode = 1;
payload.metadataHash = metadataHash;
payload.withSignedTransaction = true;
const { newPayload, newTxMetadata } = await fetchMetadataHash(
api,
payloadJson
);
payload = newPayload;
txMetadata = newTxMetadata;
} else {
// Create the payload bytes.
payload = api.registry.createType('ExtrinsicPayload', payloadJson, {
version: payloadJson.version,
});
}

// Create the payload bytes.
const payloadBytes = api.registry.createType(
'ExtrinsicPayload',
payload,
{
version: payload.version,
}
);

// Persist both the payload and the payload bytes in state, indexed by its uid.
setPayload(instanceId, payloadBytes, payload, uid);
// Persist both the payload json and the payload bytes in state, indexed by its uid.
setPayload(instanceId, txMetadata, payload, payloadJson, uid);
}
};

Expand Down
1 change: 1 addition & 0 deletions packages/app/src/hooks/useBuildPayload/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface UseBuildPayloadProps {
nonce: number;
setPayload: (
instanceId: ApiInstanceId,
txMetadata: AnyJson,
payload: AnyJson,
payloadValue: AnyJson,
uid: number
Expand Down
8 changes: 4 additions & 4 deletions packages/app/src/hooks/useSubmitExtrinsic/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const useSubmitExtrinsic = ({
setTxFee,
setSender,
setTxPayload,
getTxPayloadValue,
getTxPayload,
getTxSignature,
removeTxPayload,
removeTxSignature,
Expand Down Expand Up @@ -154,7 +154,7 @@ export const useSubmitExtrinsic = ({
if (method === 'ExtrinsicSuccess') {
NotificationsController.emit({
title: 'Finalized',
subtitle: '"Transaction successful"',
subtitle: 'Transaction successful',
});
} else if (method === 'ExtrinsicFailed') {
NotificationsController.emit({
Expand Down Expand Up @@ -204,15 +204,15 @@ export const useSubmitExtrinsic = ({
setSubmitting(true);

const txSignature = getTxSignature(instanceId);
const txPayloadValue = getTxPayloadValue(instanceId);
const txPayload = getTxPayload(instanceId);

// handle signed transaction.
if (getTxSignature(instanceId)) {
try {
txRef.current.addSignature(
fromRef.current,
txSignature,
txPayloadValue
txPayload.toHex()
);

const unsub = await txRef.current.send(
Expand Down
11 changes: 9 additions & 2 deletions packages/app/src/library/SubmitTx/ManualSign/Ledger/Submit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const Submit = ({
} = useLedgerHardware();
const { getTxSignature } = useTxMeta();
const { getAccount } = useImportedAccounts();
const { getTxPayload, getTxPayloadUid } = useTxMeta();
const { getTxPayload, getTxPayloadUid, getTxMetadata } = useTxMeta();
const { instanceId, transactionVersion, chainId, ss58Prefix } =
useExtrinsicData();

Expand All @@ -44,8 +44,15 @@ export const Submit = ({
const uid = getTxPayloadUid(instanceId);
const accountIndex = getAddressIndex();
const payload = await getTxPayload(instanceId);
const txMetadata = getTxMetadata(instanceId);

await handleSignTx(txMetadataChainId, uid, accountIndex, payload);
await handleSignTx(
txMetadataChainId,
uid,
accountIndex,
payload,
txMetadata
);
};

// Check device runtime version.
Expand Down
Loading

0 comments on commit d9bef35

Please sign in to comment.