Merge pull request #1008 from cosmos/ledger-show-address

Add showAddress method to Ledger
This commit is contained in:
Simon Warta 2022-01-26 14:26:23 +01:00 committed by GitHub
commit 9c5d6a842d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 218 additions and 110 deletions

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

1
.gitignore vendored
View File

@ -14,6 +14,7 @@ node_modules/
# IDE-specific
.vscode/
.idea/
# demo mnemonics from cli
*.key

View File

@ -12,6 +12,8 @@ and this project adheres to
JavaScript objects and the JSON representation of `cosmwasm_std::Binary`
(base64).
- @cosmjs/cosmwasm-stargate: Export `WasmExtension` and `setupWasmExtension`.
- @cosmjs/ledger-amino: Added `LedgerSigner.showAddress` and
`LaunchpadLedger.showAddress` to show the user's address in the Ledger screen.
### Changed

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -27,15 +27,13 @@ yarn demo-node
### Browser
Build the package for web:
Serve the project locally:
```sh
# Build the package for web
yarn pack-web
```
Host the `ledger-amino` package directory, for example using Python 3:
```sh
# Host the `ledger-amino` package directory, for example using Python 3
python3 -m http.server
```

View File

@ -1,4 +1,4 @@
div {
section {
display: flex;
align-items: center;
justify-content: center;

View File

@ -8,40 +8,63 @@
<script src="../dist/demo/ledger.js"></script>
</head>
<body>
<div>
<section>
<h1>Ledger Demo</h1>
</div>
<div>
</section>
<section>
<ol>
<li>Connect the Ledger device via USB</li>
<li>Open the Cosmos app on the Ledger device</li>
<li>Click the buttons below</li>
</ol>
</section>
<section>
<button onclick="createSigner().then((signer) => {window.signer = signer; console.log('Connected')}, console.error)">
Connect to Ledger
</button>
</div>
<div>
<section>
<h2>Accounts</h2>
<button onclick="getAccounts(window.signer)">
Get Accounts
</button>
</div>
<div id="accounts"></div>
<div>
<label>Account No.</label>
<input id="account-number" type="number" value="" onchange="updateMessage(this.value)" min="0" max="0"></input>
</div>
<div>
<label>Address</label>
<input id="address" type="text" value="" disabled></input>
</div>
<div>
<label>Message</label>
<textarea id="sign-doc">
</textarea>
</div>
<div>
<button onclick="sign(window.signer)">
Sign Message
<div id="accounts"></div>
</section>
<section>
<h2>Show address</h2>
<div>
<label>Account No.</label>
<input id="account-number1" type="number" value="" onchange="setPath(this.value)" min="0" max="0"></input>
</div>
<div>
<label>Path</label>
<input id="path" type="text" value="" disabled></input>
</div>
<button onclick="showAddress(window.signer)">
Show address
</button>
</div>
<div id="signature"></div>
</section>
<section>
<h2>Sign</h2>
<div>
<label>Account No.</label>
<input id="account-number2" type="number" value="" onchange="updateMessage(this.value)" min="0" max="0"></input>
</div>
<div>
<label>Address</label>
<input id="address" type="text" value="" disabled></input>
</div>
<div>
<label>Message</label>
<textarea id="sign-doc">
</textarea>
</div>
<div>
<button onclick="sign(window.signer)">
Sign Message
</button>
</div>
<div id="signature"></div>
</section>
</body>
</html>

View File

@ -1,19 +1,64 @@
const demo = require("../build/demo/node");
const { makeCosmoshubPath, makeSignDoc } = require("@cosmjs/amino");
const { pathToString } = require("@cosmjs/crypto");
const { toBase64 } = require("@cosmjs/encoding");
// eslint-disable-next-line @typescript-eslint/naming-convention
const { LedgerSigner } = require("@cosmjs/ledger-amino");
// eslint-disable-next-line @typescript-eslint/naming-convention
const TransportNodeHid = require("@ledgerhq/hw-transport-node-hid").default;
const interactiveTimeout = 120_000;
const accountNumbers = [0, 1, 2, 10];
const paths = accountNumbers.map(makeCosmoshubPath);
const defaultChainId = "testing";
const defaultFee = {
amount: [{ amount: "100", denom: "ucosm" }],
gas: "250",
};
const defaultMemo = "Some memo";
const defaultSequence = "0";
async function signMsgSend(signer, accountNumber, fromAddress, toAddress) {
const msg = {
type: "cosmos-sdk/MsgSend",
value: {
amount: [
{
amount: "1234567",
denom: "ucosm",
},
],
// eslint-disable-next-line @typescript-eslint/naming-convention
from_address: fromAddress,
// eslint-disable-next-line @typescript-eslint/naming-convention
to_address: toAddress,
},
};
const signDoc = makeSignDoc([msg], defaultFee, defaultChainId, defaultMemo, accountNumber, defaultSequence);
const { signature } = await signer.signAmino(fromAddress, signDoc);
return signature;
}
async function run() {
const signer = await demo.createSigner();
const ledgerTransport = await TransportNodeHid.create(interactiveTimeout, interactiveTimeout);
const signer = new LedgerSigner(ledgerTransport, { testModeAllowed: true, hdPaths: paths });
const accountNumbers = [0, 1, 2, 10];
const accounts = await demo.getAccounts(signer);
const accounts = await signer.getAccounts();
const printableAccounts = accounts.map((account) => ({ ...account, pubkey: toBase64(account.pubkey) }));
console.info("Accounts from Ledger device:");
console.table(accounts.map((account, i) => ({ ...account, accountNumber: accountNumbers[i] })));
console.table(printableAccounts.map((account, i) => ({ ...account, hdPath: pathToString(paths[i]) })));
console.info("Showing address of first account on device");
await signer.showAddress();
console.info("Showing address of 3rd account on device");
await signer.showAddress(paths[2]); // Path of 3rd account
const accountNumber0 = 0;
const address0 = accounts[accountNumber0].address;
console.info(
`Signing on Ledger device with account index ${accountNumber0} (${address0}). Please review and approve on the device now.`,
);
const signature0 = await demo.sign(signer, accountNumber0, address0, address0);
const signature0 = await signMsgSend(signer, accountNumber0, address0, address0);
console.info("Signature:", signature0);
// It seems the Ledger device needs a bit of time to recover
@ -24,8 +69,14 @@ async function run() {
console.info(
`Signing on Ledger device with account index ${accountNumber10} (${address10}). Please review and approve on the device now.`,
);
const signature1 = await demo.sign(signer, accountNumber10, address10, address10);
const signature1 = await signMsgSend(signer, accountNumber10, address10, address10);
console.info("Signature:", signature1);
}
run().catch(console.error);
run().then(
() => process.exit(0),
(err) => {
console.error(err);
process.exit(1);
},
);

View File

@ -1,60 +0,0 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { makeCosmoshubPath, makeSignDoc, StdFee, StdSignature } from "@cosmjs/amino";
import { toBase64 } from "@cosmjs/encoding";
import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";
import { LedgerSigner } from "../ledgersigner";
const defaultChainId = "testing";
const defaultFee: StdFee = {
amount: [{ amount: "100", denom: "ucosm" }],
gas: "250",
};
const defaultMemo = "Some memo";
const defaultSequence = "0";
export async function createSigner(): Promise<LedgerSigner> {
const interactiveTimeout = 120_000;
const ledgerTransport = await TransportNodeHid.create(interactiveTimeout, interactiveTimeout);
return new LedgerSigner(ledgerTransport, {
testModeAllowed: true,
hdPaths: [makeCosmoshubPath(0), makeCosmoshubPath(1), makeCosmoshubPath(2), makeCosmoshubPath(10)],
});
}
export async function getAccounts(signer: LedgerSigner): Promise<
ReadonlyArray<{
readonly algo: string;
readonly address: string;
readonly pubkey: string;
}>
> {
const accounts = await signer.getAccounts();
return accounts.map((account) => ({ ...account, pubkey: toBase64(account.pubkey) }));
}
export async function sign(
signer: LedgerSigner,
accountNumber: number,
fromAddress: string,
toAddress: string,
): Promise<StdSignature> {
const msgs = [
{
type: "cosmos-sdk/MsgSend",
value: {
amount: [
{
amount: "1234567",
denom: "ucosm",
},
],
from_address: fromAddress,
to_address: toAddress,
},
},
];
const signDoc = makeSignDoc(msgs, defaultFee, defaultChainId, defaultMemo, accountNumber, defaultSequence);
const { signature } = await signer.signAmino(fromAddress, signDoc);
return signature;
}

View File

@ -1,4 +1,5 @@
import { AccountData, makeCosmoshubPath, StdSignDoc } from "@cosmjs/amino";
import { pathToString, stringToPath } from "@cosmjs/crypto";
import { toBase64 } from "@cosmjs/encoding";
import { Uint53 } from "@cosmjs/math";
import { assert } from "@cosmjs/utils";
@ -9,6 +10,9 @@ import { LedgerSigner } from "../ledgersigner";
declare const window: any;
declare const document: any;
const accountNumbers = [0, 1, 2, 10];
const paths = accountNumbers.map(makeCosmoshubPath);
let accounts: readonly AccountData[] = [];
function createSignDoc(accountNumber: number, address: string): string {
@ -59,12 +63,22 @@ window.updateMessage = (accountNumberInput: unknown) => {
signDocTextArea.textContent = createSignDoc(accountNumber, address);
};
window.setPath = (accountNumberInput: unknown) => {
assert(typeof accountNumberInput === "string");
const accountNumber = Uint53.fromString(accountNumberInput).toNumber();
const path = pathToString(paths[accountNumber]);
const pathInput = document.getElementById("path");
pathInput.value = path;
};
// This must be called by the user an cannot be done on load (see "TransportWebUSBGestureRequired").
window.createSigner = async function createSigner(): Promise<LedgerSigner> {
const interactiveTimeout = 120_000;
const ledgerTransport = await TransportWebUSB.create(interactiveTimeout, interactiveTimeout);
return new LedgerSigner(ledgerTransport, {
testModeAllowed: true,
hdPaths: [makeCosmoshubPath(0), makeCosmoshubPath(1), makeCosmoshubPath(2)],
hdPaths: paths,
});
};
@ -73,7 +87,8 @@ window.getAccounts = async function getAccounts(signer: LedgerSigner | undefined
console.error("Please wait for transport to connect");
return;
}
const accountNumberInput = document.getElementById("account-number");
const accountNumberInput1 = document.getElementById("account-number1");
const accountNumberInput2 = document.getElementById("account-number2");
const addressInput = document.getElementById("address");
const accountsDiv = document.getElementById("accounts");
const signDocTextArea = document.getElementById("sign-doc");
@ -87,16 +102,33 @@ window.getAccounts = async function getAccounts(signer: LedgerSigner | undefined
}));
accountsDiv.textContent = JSON.stringify(prettyAccounts, null, "\n");
const accountNumber = 0;
accountNumberInput.max = accounts.length - 1;
accountNumberInput.value = accountNumber;
// Show address block
accountNumberInput1.max = accounts.length - 1;
accountNumberInput1.value = accountNumber;
window.setPath(accountNumber.toString());
// Sign block
accountNumberInput2.max = accounts.length - 1;
accountNumberInput2.value = accountNumber;
const address = accounts[0].address;
addressInput.value = address;
signDocTextArea.textContent = createSignDoc(accountNumber, address);
} catch (error) {
console.error(error);
accountsDiv.textContent = error;
}
};
window.showAddress = async function showAddress(signer: LedgerSigner | undefined): Promise<void> {
if (signer === undefined) {
console.error("Please wait for transport to connect");
return;
}
const path = stringToPath(document.getElementById("path").value);
await signer.showAddress(path);
};
window.sign = async function sign(signer: LedgerSigner | undefined): Promise<void> {
if (signer === undefined) {
console.error("Please wait for transport to connect");
@ -115,7 +147,3 @@ window.sign = async function sign(signer: LedgerSigner | undefined): Promise<voi
signatureDiv.textContent = error;
}
};
window.onload = async function onLoad(): Promise<void> {
window.signer = await window.createSigner();
};

View File

@ -1,2 +1,2 @@
export { LaunchpadLedger } from "./launchpadledger";
export { AddressAndPubkey, LaunchpadLedger } from "./launchpadledger";
export { LedgerSigner } from "./ledgersigner";

View File

@ -1,9 +1,10 @@
import { makeCosmoshubPath } from "@cosmjs/amino";
import { encodeSecp256k1Pubkey, makeCosmoshubPath, pubkeyToAddress, Secp256k1Pubkey } from "@cosmjs/amino";
import { HdPath, Secp256k1Signature } from "@cosmjs/crypto";
import { fromUtf8 } from "@cosmjs/encoding";
import { assert } from "@cosmjs/utils";
import Transport from "@ledgerhq/hw-transport";
import CosmosApp, {
AddressAndPublicKeyResponse,
AppInfoResponse,
PublicKeyResponse,
SignResponse,
@ -51,6 +52,11 @@ export interface LaunchpadLedgerOptions {
readonly minLedgerAppVersion?: string;
}
export interface AddressAndPubkey {
readonly address: string;
readonly pubkey: Secp256k1Pubkey;
}
export class LaunchpadLedger {
private readonly testModeAllowed: boolean;
private readonly hdPaths: readonly HdPath[];
@ -109,7 +115,7 @@ export class LaunchpadLedger {
public async getCosmosAddress(pubkey?: Uint8Array): Promise<string> {
const pubkeyToUse = pubkey || (await this.getPubkey());
return CosmosApp.getBech32FromPK(this.prefix, Buffer.from(pubkeyToUse));
return pubkeyToAddress(encodeSecp256k1Pubkey(pubkeyToUse), this.prefix);
}
public async sign(message: Uint8Array, hdPath?: HdPath): Promise<Uint8Array> {
@ -166,6 +172,29 @@ export class LaunchpadLedger {
await this.verifyCosmosAppIsOpen();
}
/**
* Shows the user's address in the device and returns an address/pubkey pair.
*
* The address will be shown with the native prefix of the app (e.g. cosmos, persistence, desmos)
* and does not support the usage of other address prefixes.
*
* @param path The HD path to show the address for. If unset, this is the first account.
*/
public async showAddress(hdPath?: HdPath): Promise<AddressAndPubkey> {
await this.verifyDeviceIsReady();
const hdPathToUse = hdPath || this.hdPaths[0];
// ledger-cosmos-js hardens the first three indices
const response = await this.app.showAddressAndPubKey(unharden(hdPathToUse), this.prefix);
this.handleLedgerErrors(response);
// eslint-disable-next-line @typescript-eslint/naming-convention
const { address, compressed_pk } = response as AddressAndPublicKeyResponse;
return {
address: address,
pubkey: encodeSecp256k1Pubkey(compressed_pk),
};
}
private handleLedgerErrors(
/* eslint-disable @typescript-eslint/naming-convention */
{

View File

@ -10,7 +10,7 @@ import {
import { HdPath } from "@cosmjs/crypto";
import Transport from "@ledgerhq/hw-transport";
import { LaunchpadLedger, LaunchpadLedgerOptions } from "./launchpadledger";
import { AddressAndPubkey, LaunchpadLedger, LaunchpadLedgerOptions } from "./launchpadledger";
export class LedgerSigner implements OfflineAminoSigner {
private readonly ledger: LaunchpadLedger;
@ -37,6 +37,18 @@ export class LedgerSigner implements OfflineAminoSigner {
return this.accounts;
}
/**
* Shows the user's address in the device and returns an address/pubkey pair.
*
* The address will be shown with the native prefix of the app (e.g. cosmos, persistence, desmos)
* and does not support the usage of other address prefixes.
*
* @param path The HD path to show the address for. If unset, this is the first account.
*/
public async showAddress(path?: HdPath): Promise<AddressAndPubkey> {
return this.ledger.showAddress(path);
}
public async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise<AminoSignResponse> {
const accounts = this.accounts || (await this.getAccounts());
const accountIndex = accounts.findIndex((account) => account.address === signerAddress);

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
const glob = require("glob");
const path = require("path");
const webpack = require("webpack");
const target = "web";
const demodir = path.join(__dirname, "dist", "demo");
@ -14,6 +15,11 @@ module.exports = [
path: demodir,
filename: "ledger.js",
},
plugins: [
new webpack.ProvidePlugin({
Buffer: ["buffer", "Buffer"],
}),
],
resolve: {
fallback: {
buffer: false,

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{

View File

@ -81,6 +81,7 @@ module.exports = {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{