Skip to content

Commit

Permalink
Polkadot generic app support (#6326)
Browse files Browse the repository at this point in the history
* getAddress support

* fix: add metadata support

* fix: get polkadot app version

* fix: refactoring

* fix: refactoring

* fix: refactoring

* fix: refactoring

* fix: small refactoring

* fix: update doc

* feat: add Polkadot coin configuration

Signed-off-by: Stéphane Prohaszka <[email protected]>

* polkadot config location

* fix: polkadot transaction craft

* remove useless code

* fix: refactoring

* fix: runtime upgrade transaction

* fix: update doc

* fix: unit tests

* fix: update changelog

* fix: update pnpm-lock

* fix: update polkadot integration test APDU

---------

Signed-off-by: Stéphane Prohaszka <[email protected]>
Co-authored-by: hzheng-ledger <[email protected]>
Co-authored-by: Stéphane Prohaszka <[email protected]>
Co-authored-by: lvndry <[email protected]>
  • Loading branch information
4 people committed Jun 14, 2024
1 parent e0c89dc commit 4b7f19c
Show file tree
Hide file tree
Showing 24 changed files with 433 additions and 190 deletions.
7 changes: 7 additions & 0 deletions .changeset/warm-birds-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@ledgerhq/hw-app-polkadot": minor
"@ledgerhq/coin-polkadot": minor
"@ledgerhq/live-common": minor
---

polkadot generic nano app support
8 changes: 4 additions & 4 deletions libs/coin-modules/coin-polkadot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@
"@ledgerhq/logs": "workspace:^",
"@ledgerhq/types-cryptoassets": "workspace:^",
"@ledgerhq/types-live": "workspace:^",
"@polkadot/types": "10.9.1",
"@polkadot/types-known": "10.9.1",
"@polkadot/util": "12.5.1",
"@polkadot/util-crypto": "12.5.1",
"@polkadot/types": "11.2.1",
"@polkadot/types-known": "11.2.1",
"@polkadot/util": "12.6.2",
"@polkadot/util-crypto": "12.6.2",
"bignumber.js": "^9.1.2",
"expect": "^27.4.6",
"invariant": "^2.2.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TypeRegistry } from "@polkadot/types";
import { buildTransaction } from "./buildTransaction";
import { createFixtureAccount, createFixtureTransaction } from "../types/bridge.fixture";
import { faker } from "@faker-js/faker";
import { getCoinConfig } from "../config";

const registry = new TypeRegistry();

Expand All @@ -25,8 +26,34 @@ jest.mock("../network", () => {
};
});

jest.mock("../config");
const mockGetConfig = jest.mocked(getCoinConfig);

describe("buildTransaction", () => {
let spyRegistry: jest.SpyInstance | undefined;
beforeAll(() => {
mockGetConfig.mockImplementation((): any => {
return {
status: {
type: "active",
},
sidecar: {
url: "https://polkadot-sidecar.coin.ledger.com",
credentials: "",
},
staking: {
electionStatusThreshold: 25,
},
metadataShortener: {
url: "https://api.zondax.ch/polkadot/transaction/metadata",
},
metadataHash: {
url: "https://api.zondax.ch/polkadot/node/metadata/hash",
},
runtimeUpgraded: false,
};
});
});

afterEach(() => {
mockExtrinsics.mockClear();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { PolkadotAccount, Transaction } from "../types";
import { craftTransaction, type CreateExtrinsicArg } from "../logic";
import { isFirstBond, getNonce } from "./utils";
import { getCoinConfig } from "../config";

export const extractExtrinsicArg = (
account: PolkadotAccount,
Expand Down Expand Up @@ -28,10 +29,12 @@ export const buildTransaction = async (
transaction: Transaction,
forceLatestParams = false,
) => {
const runtimeUpgraded = getCoinConfig().runtimeUpgraded;
return craftTransaction(
account.freshAddress,
getNonce(account),
extractExtrinsicArg(account, transaction),
forceLatestParams,
runtimeUpgraded,
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "../network/sidecar.fixture";
import { createFixtureAccount, createFixtureTransaction } from "../types/bridge.fixture";
import { createRegistryAndExtrinsics } from "../network/common";
import { getCoinConfig } from "../config";

const mockPaymentInfo = jest.fn();
const mockRegistry = jest
Expand All @@ -19,8 +20,34 @@ jest.mock("../network/sidecar", () => ({
getTransactionParams: () => mockTransactionParams(),
}));

jest.mock("../config");
const mockGetConfig = jest.mocked(getCoinConfig);

describe("getEstimatedFees", () => {
const transaction = createFixtureTransaction();
beforeAll(() => {
mockGetConfig.mockImplementation((): any => {
return {
status: {
type: "active",
},
sidecar: {
url: "https://polkadot-sidecar.coin.ledger.com",
credentials: "",
},
staking: {
electionStatusThreshold: 25,
},
metadataShortener: {
url: "https://api.zondax.ch/polkadot/transaction/metadata",
},
metadataHash: {
url: "https://api.zondax.ch/polkadot/node/metadata/hash",
},
runtimeUpgraded: false,
};
});
});

beforeEach(() => {
mockPaymentInfo.mockClear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import BigNumber from "bignumber.js";
import { createFixtureAccount, createFixtureTransaction } from "../types/bridge.fixture";
import prepareTransaction from "./prepareTransaction";
import { faker } from "@faker-js/faker";
import { getCoinConfig } from "../config";

const mockCraftTransaction = jest.fn();
const mockEstimateFees = jest.fn();
Expand All @@ -10,7 +11,34 @@ jest.mock("../logic", () => ({
craftTransaction: () => mockCraftTransaction(),
}));

jest.mock("../config");
const mockGetConfig = jest.mocked(getCoinConfig);

describe("prepareTransaction", () => {
beforeAll(() => {
mockGetConfig.mockImplementation((): any => {
return {
status: {
type: "active",
},
sidecar: {
url: "https://polkadot-sidecar.coin.ledger.com",
credentials: "",
},
staking: {
electionStatusThreshold: 25,
},
metadataShortener: {
url: "https://api.zondax.ch/polkadot/transaction/metadata",
},
metadataHash: {
url: "https://api.zondax.ch/polkadot/node/metadata/hash",
},
runtimeUpgraded: false,
};
});
});

afterEach(() => {
mockCraftTransaction.mockClear();
mockEstimateFees.mockClear();
Expand Down
28 changes: 27 additions & 1 deletion libs/coin-modules/coin-polkadot/src/bridge/signOperation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
fixtureTransactionParams,
fixtureTxMaterialWithMetadata,
} from "../network/sidecar.fixture";
import { getCoinConfig } from "../config";

const mockPaymentInfo = jest.fn().mockResolvedValue({
weight: "WHATEVER",
Expand All @@ -27,6 +28,9 @@ jest.mock("../network/sidecar", () => ({
getTransactionParams: () => mockTransactionParams(),
}));

jest.mock("../config");
const mockGetConfig = jest.mocked(getCoinConfig);

describe("signOperation", () => {
// Global setup
const fakeSignature = u8aConcat(new Uint8Array([1]), new Uint8Array(64).fill(0x42));
Expand All @@ -41,7 +45,29 @@ describe("signOperation", () => {
fn(fakeSigner);
const signOperation = buildSignOperation(signerContext);
const deviceId = "dummyDeviceId";

beforeAll(() => {
mockGetConfig.mockImplementation((): any => {
return {
status: {
type: "active",
},
sidecar: {
url: "https://polkadot-sidecar.coin.ledger.com",
credentials: "",
},
staking: {
electionStatusThreshold: 25,
},
metadataShortener: {
url: "https://api.zondax.ch/polkadot/transaction/metadata",
},
metadataHash: {
url: "https://api.zondax.ch/polkadot/node/metadata/hash",
},
runtimeUpgraded: false,
};
});
});
it("returns events in the right order", done => {
// GIVEN
const account = createFixtureAccount();
Expand Down
11 changes: 9 additions & 2 deletions libs/coin-modules/coin-polkadot/src/bridge/signOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { buildOptimisticOperation } from "./buildOptimisticOperation";
import { buildTransaction } from "./buildTransaction";
import { calculateAmount } from "./utils";
import { signExtrinsic } from "../logic";
import polkadotAPI from "../network";
import { getCoinConfig } from "../config";

/**
* Sign Transaction with Ledger hardware
Expand All @@ -19,6 +21,7 @@ export const buildSignOperation =
({ account, deviceId, transaction }) =>
new Observable(o => {
async function main() {
const runtimeUpgraded = getCoinConfig().runtimeUpgraded;
o.next({
type: "device-signature-requested",
});
Expand All @@ -43,10 +46,14 @@ export const buildSignOperation =
.toU8a({
method: true,
});

let metadata = "";
if (runtimeUpgraded) {
const payloadString = Buffer.from(payload).toString("hex");
metadata = await polkadotAPI.shortenMetadata(payloadString);
}
const r = await signerContext(deviceId, signer =>
// FIXME: the type of payload Uint8Array is not compatible with the signature of sign which accept a string
signer.sign(account.freshAddressPath, payload as any),
signer.sign(account.freshAddressPath, payload, metadata),
);

const signed = await signExtrinsic(unsigned, r.signature, registry);
Expand Down
7 changes: 7 additions & 0 deletions libs/coin-modules/coin-polkadot/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ export type PolkadotConfig = {
staking?: {
electionStatusThreshold: number;
};
metadataShortener: {
url: string;
};
metadataHash: {
url: string;
};
runtimeUpgraded: boolean;
};

export type PolkadotCoinConfig = CurrencyConfig & PolkadotConfig;
Expand Down
21 changes: 20 additions & 1 deletion libs/coin-modules/coin-polkadot/src/logic/craftTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { CoreTransaction, PalletMethod, PolkadotOperationMode } from "../ty
import { loadPolkadotCrypto } from "./polkadot-crypto";
import polkadotAPI from "../network";
import { getAbandonSeedAddress } from "@ledgerhq/cryptoassets/index";
import { hexToU8a } from "@polkadot/util";
import { CoreTransasctionInfo, TransasctionPayloadInfo } from "../types";

const EXTRINSIC_VERSION = 4;
// Default values for tx parameters, if the user doesn't specify any
Expand Down Expand Up @@ -167,6 +169,7 @@ export async function craftTransaction(
nonceToUse: number,
extractExtrinsicArg: CreateExtrinsicArg,
forceLatestParams: boolean = false,
runtimeUpgraded: boolean = false,
): Promise<CoreTransaction> {
await loadPolkadotCrypto();

Expand Down Expand Up @@ -206,7 +209,7 @@ export async function craftTransaction(
).toHex();

const { blockHash, genesisHash } = info;
const unsigned = {
let unsigned: CoreTransasctionInfo | TransasctionPayloadInfo = {
address,
blockHash,
blockNumber,
Expand All @@ -220,6 +223,22 @@ export async function craftTransaction(
transactionVersion,
version: EXTRINSIC_VERSION,
};
if (runtimeUpgraded) {
const metadataHash = await polkadotAPI.metadataHash();
unsigned = {
address,
method,
nonce: nonceToUse,
genesisHash,
era,
blockHash,
transactionVersion,
specVersion,
version: EXTRINSIC_VERSION,
mode: 1,
metadataHash: hexToU8a("01" + metadataHash),
} as TransasctionPayloadInfo;
}

return {
registry,
Expand Down
13 changes: 9 additions & 4 deletions libs/coin-modules/coin-polkadot/src/logic/signTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TypeRegistry } from "@polkadot/types";
import { u8aConcat } from "@polkadot/util";
import { CoreTransasctionInfo } from "../types";
import { CoreTransasctionInfo, TransasctionPayloadInfo } from "../types";

/**
* Serialize a signed transaction in a format that can be submitted over the
Expand All @@ -12,14 +12,19 @@ import { CoreTransasctionInfo } from "../types";
* @param registry - Registry used for constructing the payload.
*/
export const signExtrinsic = async (
unsigned: CoreTransasctionInfo,
unsigned: CoreTransasctionInfo | TransasctionPayloadInfo,
signature: any,
registry: TypeRegistry,
): Promise<string> => {
const extrinsic = registry.createType("Extrinsic", unsigned, {
version: unsigned.version,
});
extrinsic.addSignature(unsigned.address, signature, unsigned);
if (typeof signature === "string") {
extrinsic.addSignature(unsigned.address, Buffer.from(signature, "hex"), unsigned as any);
} else {
// todo: remove it after runtime upgrade
extrinsic.addSignature(unsigned.address, signature, unsigned as any);
}
return extrinsic.toHex();
};

Expand All @@ -30,7 +35,7 @@ export const signExtrinsic = async (
* @param registry - Registry used for constructing the payload.
*/
export const fakeSignExtrinsic = async (
unsigned: CoreTransasctionInfo,
unsigned: CoreTransasctionInfo | TransasctionPayloadInfo,
registry: TypeRegistry,
): Promise<string> => {
const fakeSignature = u8aConcat(new Uint8Array([1]), new Uint8Array(64).fill(0x42));
Expand Down
Loading

0 comments on commit 4b7f19c

Please sign in to comment.